9장 - 일반적인 프로그래밍 원칙
💡 지역변수의 범위를 최소화하라
“지역변수의 유효 범위를 최소로 줄이면 코드 가독성과 유지보수성이 높아지고 오류 가능성은 낮아진다”
1. 지역변수의 범위를 줄이는 방법
- 지역변수의 범위를 줄이는 가장 강력한 기법은 역시 ‘가장 처음 쓰일 때 선언하기’다.
- 사용하려면 멀었는데, 미리 선언부터 해두면 코드가 어수선해져 가독성이 떨어짐.
- 미리 선언해두면 변수를 실제로 사용하는 시점엔 타입과 초깃값이 기억나지 않을 수도 있음
→ 거의 모든 지역변수는 선언과 동시에 초기화해야 한다.
- 메서드를 작게 유지하고 한 가지 기능에 집중하기
- 한 메서드에서 여러 가지 기능을 처리한다면 그중 한 기능과만 관련된 지역변수라도 다른 기능을 수행하는 코드에서 접근할 수 있을 것임.
→ 메서드를 기능별로 잘 쪼개자.
2. for vs while
- 변수의 값을 반복문이 종료된 뒤에더 써야 하는 상황이 아니라면
while
문보다는for
문을 쓰는 편이 나음---for
,for-each
형태의 반복문에서는 반복 변수의 범위가 반복문의 몸체, 그리고for
키워드와 몸체 사이의 괄호 안으로 제한되기 때문.for
문을 사용하면 복사+붙여넣기 오류를 컴파일타임에 잡아줌.for
문의 변수 유효 범위는for
문의 범위와 일치하여 똑같은 이름의 변수를 여러 반복문에서 써도 서로 아무런 영향을 주지 않음. 더 세련되기까지 하다.for
문은while
문보다 짧아서 가독성이 좋음
→ for
문이 더 Good!
💡 전통적인 for 문보다는 for-each 문을 사용하라
“가능한 모든 곳에서
for
문이 아닌for-each
문을 사용하자”
1. for vs for-each
for
문
// 배열 순회하기 - for문
for (int i = 0; i < a.length; i++) {
... // a[i]로 무언가를 수행
}
→ 반복자와 인덱스 변수는 모두 코드를 지저분하게 할 뿐. 우리에게 진짜 필요한 건 원소들 뿐이다.
for-each
문(향상된 for 문; enhanced for statement)
for (Element e : elements) {
... // e로 무언가를 수행
}
- 콜론(
:
)은 “안의(in)”라고 읽음 - 위 반복문은 “
elements
안의 각 원소e
에 대해”라고 읽음 for-each
문의 장점- 반복자와 인덱스 변수를 사용하지 않으니 코드가 깔끔해지고 오류가 날 일도 없음.
- 하나의 관용구로 컬렉션과 배열을 모두 처리할 수 있어서 어떤 컨테이너를 다루는지 신경쓰지 않아도 됨
for-each
문은 컬렉션과 배열은 물론Iterable
인터페이스를 구현한 객체라면 무엇이든 순회할 수 있음
2. for-each 문을 사용할 수 없는 경우
- 파괴적인 필터링(destructive filtering)
- 컬렉션을 순회하면서 선택된 원소를 제거해야 한다면 반복자의
remove
메서드를 호출해야함. - 자바 8부터는
Collection
의removeIf
메서드를 사용해 컬렉션을 명시적으로 순회하는 일을 피할 수 있음
- 컬렉션을 순회하면서 선택된 원소를 제거해야 한다면 반복자의
- 변형(transforming)
- 리스트나 배열을 순회하면서 그 원소의 값 일부 혹은 전체를 교체해야 한다면 리스트의 반복자나 배열의 인덱스를 사용해야 함
for-each
는 임시 객체를 만들어서 사용하기 때문
- 병렬 반복(parallel iteration)
- 여러 컬렉션을 병렬로 순회해야 한다면 각각의 반복자와 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야 함.
💡 라이브러리를 익히고 사용하라
“바퀴를 다시 발명하지 말자”
1. 표준 라이브러리 사용 이점
- 표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과 우리보다 앞서 사용한 다른 프로그래머들의 경험을 활용할 수 있음.
- 핵심적인 일과 크게 관련 없는 문제를 해결하느라 시간을 허비하지 않아도 됨.
- 프로그래머들은 하부 공사를 해결하기보다는 애플리케이션 기능 개발에 집중하고 싶어 함.
- 따로 노력하지 않아도 성능이 지속해서 개선됨.
- 사용자가 많고, 업계 표준 벤치마크를 사용해 성능을 확인하기 때문에 표준 라이브러리 제작자들은 더 나은 방법을 꾸준히 모색할 수 밖에 없음.
- 기능이 점점 많아짐.
- 라이브러리에 부족한 부분이 있다면 개발자 커뮤니티에서 이야기가 나오고 논의된 후 다음 릴리스에 해당 기능이 추가되곤 함.
- 작성한 코드가 많은 사람에게 낯익은 코드가 됨
- 자연스럽게 다른 개발자들이 더 읽기 좋고, 유지보수하기 좋고, 재활용하기 쉬운 코드가 됨.
2. 알아두어야할 표준 라이브러리
- 자바 프로그래머라면 적어도
java.lang
,java.util
,java.io
와 그 하위 패키지들에는 익숙해져야 함. - 컬렉션 프레임워크나 스트림 라이브러리,
java.util.concurrent
의 동시성 기능 등 잘 알아두면 큰 도움이 됨. - 자바 표준 라이브러리에서 원하는 기능을 찾지 못하면, 그 다음 선택지는 고품질의 서드파티 라이브러리를 찾아보자. ex) 구글의 구아바 라이브러리
💡 정확한 답이 필요하다면 float와 double은 피하라
“근사치 :
float
,double
/ 정확한 값 :BigDecimal
”
1. float와 double
float
와double
은 이진 부동소수점 연산에 쓰이며, 넓은 범위의 수를 빠르게 정밀한 ‘근사치’로 계산하도록 설계 되었음.- 예시로 아래 코드는 0.610000000000001을 출력한다.
System.out.println(1.03 - 0.42);
→ float
와 double
은 정확한 결과가 필요할 때(ex. 금융 계산)는 사용하면 안 된다.
2. BigDecimal
- 금융 계산에는
BigDecimal
,int
혹은long
을 사용해야 한다. BigDecimal
의 단점- 기본 타입보다 쓰기가 훨씬 불편하고, 훨씬 느리다.
- 단발성 계산이라면 느리다는 문제는 무시할 수 있지만, 쓰기 불편하다는 점은 못내 아쉬울 것임.
BigDecimal
의 대안으로int
혹은long
타입을 쓸 수 있음- 이 경우 다룰 수 있는 값의 크기가 제한되고, 소수점을 직접 관리해야 함.
- 권장
- 소수점 추적은 시스템에 맡기고, 코딩 시의 불편함이나 성능 저하를 신경쓰지 않겠다면
BigDecimal
을 사용하라BigDecimal
이 제공하는 여덟 가지 반올림 모드를 이용하여 반올림을 완벽히 제어할 수 있음
- 성능이 중요하고 소수점을 직접 추적할 수 있고 숫자가 너무 크지 않다면
int
나long
을 사용하라- 숫자를 아홉자리 십진수로 표현할 수 있다면 →
int
사용 - 열여덟 자리 십진수로 표현할 수 있다면 →
long
사용 - 열여덟 자리를 넘어가면 →
BigDeceimal
사용
- 숫자를 아홉자리 십진수로 표현할 수 있다면 →
- 소수점 추적은 시스템에 맡기고, 코딩 시의 불편함이나 성능 저하를 신경쓰지 않겠다면
💡 박싱된 기본 타입보다는 기본 타입을 사용하라
“기본 타입은 간단하고 빠르다”
1. 기본 타입 vs 박싱된 기본 타입
기본타입 | 박싱된 기본 타입 | |
---|---|---|
예시 | int | BigInteger |
식별성(identity) | X | O |
Nullable | X | O |
메모리 사용 효율 | ↑ | ↓ |
2. 박싱된 기본 타입의 문제점
- 박싱된 기본 타입에
==
연산자를 사용하면 오류가 일어날 수 있다.- 박싱된 기본 타입에
==
연산자를 사용하면 값이 아닌 객체 참조의 식별성을 검사하므로 원하지 않는 결과가 나올 수 있음.
- 박싱된 기본 타입에
- 연산에서 기본 타입과 박싱된 기본 타입을 혼용하면 언박싱이 이뤄지는데, 언박싱 과정에서
NullPointerExeption
을 던질 수 있음 - 기본 타입을 박싱하는 작업은 필요 없는 객체를 생성하는 부작용을 나을 수 있음.
- 반복문 같은 곳에서 기본 타입과 박싱된 기본 타입을 혼용한 연산을 수행하면,
필요 없는 객체가 계속 생성될 수도 있음..
- 반복문 같은 곳에서 기본 타입과 박싱된 기본 타입을 혼용한 연산을 수행하면,
💡 다른 타입이 적절하다면 문자열 사용을 피하라
“문자열은 잘못 사용하면 번거롭고, 덜 유연하고, 느리고, 오류 가능성도 크다”
- 입력받을 데이터가 진짜 문자열일 때만 문자열을 사용하자
- 많은 사람이 데이터를 받을 때 주로 문자열을 사용하는데, 이러면 적당한 타입으로 교체해줘야 한다.
→ 기본 타입이든 참조 타입이든 적절한 값 타입이 있다면 그것을 사용하고, 없다면 새로 하나 작성해라
- 문자열은 열거 타입을 대신하기에 적합하지 않다.
- 문자열은 혼합 타입을 대신하기에 적합하지 않다.
String compoundKey = className + "#" + i.next();
→ 이 경우에 차라리 전용 클래스를 만드는 편이 낫다. 보통 private
정적 멤버 클래스로 선언.
- 문자열은 권한을 표현하기에 적합하지 않다.
💡 문자열 연결은 느리니 주의하라
“많은 문자열 연결 시
+
연산자 대신StringBuilder
의append
메서드를 사용하라”
- 문자열 연결 연산자로 문자열 n개를 잇는 시간은 n^2임.
- 성능을 포기하고 싶지 않다면
StringBuilder
를 사용하자
// StringBuilder 사용 예시
public String statement() {
String Builder b = new StringBuilder(numItems() * LINE_WIDTH);
for(int i=0; i<numItems(); i++)
b.append(lineForItem(i))
return b.toString();
}
💡 객체는 인터페이스를 사용해 참조하라
“인터페이스로 참조하면 더 유연하고 세련된 프로그램을 만들 수 있다”
- 적합한 인터페이스가 있다면 매개변수뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하라.
- 인터페이스를 타입으로 사용하는 습관을 길러두면 프로그램이 훨씬 유연해질 것이다
- 나중에 구현 클래스를 교체하고자 한다면 새 클래스의 생성자 혹은 정적 팩터리를 호출해주면 됨.
- 구현 클래스 교체 시 주의점 : 원래의 클래스가 인터페이스의 일반 규약 이외의 특별한 기능을 제공하며, 주변 코드가 이 기능에 기대어 동작한다면 새로운 클래스도 반드시 같은 기능을 제공해야 함.
- ex)
LinkedHashSet
이 따르는 순서 정책을 가정하고 동작하는 상황에서 구현 클래스를HashSet
으로 바꾸면 문제가 될 수 있음. (HashSet
은 반복자의 순회 순서를 보장하지 않기 때문)
- 나중에 구현 클래스를 교체하고자 한다면 새 클래스의 생성자 혹은 정적 팩터리를 호출해주면 됨.
- 적합한 인터페이스가 없다면 당연히 클래스로 참조해야 한다.
- 값 클래스 인 경우.
- ex)
String
,BigInteger
등
- ex)
- 클래스 기반으로 작성된 프레임워크가 제공하는 객체들.
- ex)
OutptStream
등
- ex)
- 인터페이스에는 없는 특별한 메서드를 제공하는 클래스들.
- ex)
PriorityQueue
는Queue
에 없는comparator
메서드를 제공함.
- ex)
- 값 클래스 인 경우.
- 적합한 인터페이스가 없다면 클래스의 계층구조 중 필요한 기능을 만족하는 가장 덜 구체적인(상위의) 클래스를 타입으로 사용하자
💡 리플렉션보다는 인터페이스를 사용하라
“리플렉션은 복잡한 특수 시스템을 개발할 때 강력한 기능이지만, 단점도 많다”
1. 리플렉션 기능
- 리플렉션 기능(
java.lang.reflect
)을 이용하면 프로그램에서 임의의 클래스에 접근할 수 있음 Class
객체가 주어지면 그 클래스의 생성자, 메서드, 필드에 해당하는Constructor
,Method
,Field
인스턴스를 가져올 수 있음- 이 인스턴스들로는 그 클래스의 멤버 이름, 필드 타입, 메서드 시그니처 등을 가져올 수 있음
- 이 인스턴스들을 이용해 각각에 연결된 실제 생성자, 메서드, 필드를 조작할 수도 있음
- 리플렉션을 이용하면 컴파일 타임에 존재하지 않던 클래스도 이용할 수 있음
2. 리플렉션 단점
- 컴파일타임 타입 검사가 주는 이점을 하나도 누릴 수 없다
- 프로그램이 리플렉션 기능을 써서 존재하지 않는 혹은 접근할 수 없는 메서드를 호출하려 시도하면 (주의해서 대비 코드를 작성해두지 않았다면) 런타임 오류가 발생함
- 리플렉션을 이용하면 코드가 지저분하고 장황해진다.
- 성능이 떨어진다.
- 리플렉션을 통한 메서드 호출은 일반 메서드 호출보다 훨씬 느리다.
→ 리플렉션은 인스턴스 생성에만 쓰고, 이렇게 만든 인스턴스는 인터페이스나 상위 클래스로 참조해 사용하자.
💡 네이티브 메서드는 신중히 사용하라
“네이티브 메서드가 성능을 개선해주는 일은 많지 않다”
1. 네이티브 메서드의 쓰임
- 자바 네이티브 인터페이스(JNI)는 자바 프로그램이 네이티브 메서드를 호출하는 기술
- 네이티브 메서드란 C나 C++ 같은 네이티브 프로그래밍 언어로 작성한 메서드를 말함
- 네이티브 메서드의 주요 쓰임
- 레지스트리 같은 플랫폼 특화 기능을 사용
- 네이티브 코드로 작성된 기존 라이브러리를 사용
- 성능 개선을 목적으로 성능에 결정적인 영향을 주는 영역만 따로 네이티브 언어로 작성
- 성능을 개선할 목적으로 네이티브 메서드를 사용하는 것은 거의 권장하지 않음
- JVM은 엄청난 속도로 발전해왔음. 대부분의 작업에서 지금의 자바는 다른 플랫폼에 견줄만한 성능을 보임.
2. 네이티브 메서드의 단점
- 네이티브 언어가 안전하지 않으므로 네이티브 메서드를 사용하는 애플리케이션도 메모리 훼손 오류들로부터 더 이상 안전하지 않음.
- 네이티브 언어는 자바보다 플랫폼을 더 많이 타서 이식성도 낮다. 디버깅도 어렵다
- 가비지 컬렉터가 네이티브 메모리는 자동 회수하지 못하고, 심지어 추적조차 할 수 없다.
- 자바 코드와 네이티브 코드의 경계를 넘나들 때마다 비용도 추가된다.
→ 네이티브 메서드를 사용하려거든 한번 더 생각해보자. 저수준 자원이나 네이티브 라이브러리를 사용해야만 해서 어쩔 수 없더라도 네이티브 코드는 최소한만 사용하고 철저히 테스트하라.
💡 최적화는 신중히 하라
“섣부른 최적화가 만악의 근원 - 도널드 크누스”
- 성능 때문에 견고한 구조를 희생하지 말자. 빠른 프로그램보다는 좋은 프로그램을 작성하라.
- 좋은 프로그램은 정보 은닉 원칙을 따르므로 개별 구성요소의 내부를 독립적으로 설계할 수 있음. → 시스템의 나머지에 영향을 주지 않고도 각 요소를 다시 설계할 수 있음.
- 성능을 제한하는 설계를 피하라
- 완성 후 변경하기가 가장 어려운 설계 요소는 바로 컴포넌트끼리, 혹은 외부 시스템과의 소통 방식이다. → API, 네트워크 프로토콜, 영구 저장용 데이터 포맷 등
- API를 설계할 때 성능에 주는 영향을 고려하라.
- 컴포지션으로 해결가능함에도 상속으로 설계한
public
클래스 → 상위 클래스에 영원히 종속되며 그 성능제약까지도 물려받게 됨.
- 컴포지션으로 해결가능함에도 상속으로 설계한
- 성능을 위해 API를 왜곡하는 건 매우 안 좋은 생각이다.
- 각각의 최적화 시도 전후로 성능을 측정하라
- 시도한 최적화 기법이 성능을 눈에 띄게 높이지 못하는 경우가 많고, 심지어 더 나빠지게 할 때도 있음.
- 프로파일링 도구, jmh 등을 활용하자
→ 빠른 프로그램을 작성하려 안달하지 말자. 좋은 프로그램을 작성하다 보면 성능은 따라 오기 마련이다.
💡 일반적으로 통용되는 명명 규칙을 따르라
“표준 명명 규칙을 체화하여 자연스럽게 베어 나오도록 하자”
1. 철자 규칙
- 철자 규칙은 패키지, 클래스, 인터페이스, 메서드, 필드, 타입 변수의 이름을 다룬다.
- 패키지
- 패키지와 모듈 이름은 각 요소를 점(
.
)으로 구분하여 계층적으로 짓는다.- ex)
com.google
- ex)
- 패키지 이름의 나머지는 해당 패키지를 설명하는 하나 이상의 요소로 이뤄 진다.
- 일반적으로 8자 이하의 짧은 단어로 한다. ex)
util
- 일반적으로 8자 이하의 짧은 단어로 한다. ex)
- 인터넷 도메인 이름 뒤에 요소 하나만 붙인 패키지가 많지만, 많은 기능을 제공하는 경우엔 계층을 나눠 더 많은 요소로 구성해도 좋다. (하위 패키지; subpackage)
- 패키지와 모듈 이름은 각 요소를 점(
- 클래스와 인터페이스
- (열거 타입과 어노테이션을 포함해) 클래스와 인터페이스의 이름은 하나 이상의 단어로 이뤄지며, 각 단어는 대문자로 시작한다.
max
,min
처럼 널리 통용되는 줄임말을 제외하고는 단어를 줄여쓰지 않도록 한다.
- (열거 타입과 어노테이션을 포함해) 클래스와 인터페이스의 이름은 하나 이상의 단어로 이뤄지며, 각 단어는 대문자로 시작한다.
- 메서드와 필드
- 메서드와 필드 이름은 첫 글자를 소문자로 쓴다는 점만 빼면 클래스 명명 규칙과 같다.
- 상수 필드는 예외다. 상수 필드를 구성하는 단어는 모두 대문자로 쓰며 단어 사이는 밑줄로 구분한다.
- 지역변수에도 다른 멤버와 비슷한 명명 규칙이 적용된다. 단, 약어를 써도 좋다.
- 입력 매개변수도 지역변수의 하나다. 하지만 메서드 설명 문서에까지 등장하는 만큼 일반 지역변수보다는 신경을 써야한다.
- 메서드와 필드 이름은 첫 글자를 소문자로 쓴다는 점만 빼면 클래스 명명 규칙과 같다.
- 타입 변수
- 타입 매개변수 이름은 보통 한 문자로 표현한다. 대부분은 다음의 다섯가지 중 하나다. 임의의 타입엔
T
, 컬렉션 원소의 타입은E
, 맵의 키와 값에는K
와V
, 예외에는X
, 메서드 반환 타입에는R
- 타입 매개변수 이름은 보통 한 문자로 표현한다. 대부분은 다음의 다섯가지 중 하나다. 임의의 타입엔
- 철자 규칙 정리 표
식별자 타입 | 예 |
---|---|
패키지와 모듈 | org.junit.jupiter.api, com.google.common.collect |
클래스와 인터페이스 | Stream, FutureTask, LinkedHashMap, HttpClient |
메서드와 필드 | remove, groupingBy, getCrc |
상수 필드 | MIN_VALUE, NEGATIVE_INFINITY |
지역변수 | i, denom, houseNum |
타입 매개변수 | T, E, K, V, X, R, U, V, T1, T2 |
2. 문법 규칙
- 문법 규칙은 철자 규칙과 비교하면 더 유연하고 논란도 많다.
- 패키지
- 패키지에 대한 규칙은 따로 없음
- 클래스와 인터페이스
- 객체를 생성할 수 있는 클래스의 이름은 보통 단수 명사나 명사구를 사용함.
- ex)
Thread
,PriorityQueue
등
- ex)
- 객체를 생성할 수 없는 클래스의 이름은 보통 복수형 명사로 지음
- ex)
Collectors
,Collections
등
- ex)
- 인터페이스 이름은 클래스와 똑같이 짓거나,
able
혹은ible
로 끝나는 형용사로 짓는다.- ex)
Runnable
,Iterable
,Accessible
등
- ex)
- 어노테이션은 워낙 다양하게 활용되어 지배적인 규칙이 없이 명사, 동사, 전치사, 형용사가 두루 쓰임
- 객체를 생성할 수 있는 클래스의 이름은 보통 단수 명사나 명사구를 사용함.
- 메서드와 필드
- 어떤 동작을 수행하는 메서드의 이름은 동사나 (목적어를 포함한) 동사구로 짓는다.
- ex)
append
,drawImage
등
- ex)
boolean
값을 반환하는 메서드라면 보통is
나 (드물게)has
로 시작하고 명사나 명사구, 혹은 형용사로 기능하는 아무 단어나 구로 끝나도록 짓는다.- ex)
isDigit
,isEmpty
,isEnabled
등
- ex)
- 반환타입이
boolean
이 아니거나 해당 인스턴스의 속성을 반환하는 메서드의 이름은 보통 명사, 명사구, 혹은get
으로 시작하는 동사구로 짓는다.- ex)
size
,hashCode
,getTime
등
- ex)
- 객체의 타입을 바꿔서, 다른 타입의 또 다른 객체를 반환하는 인스턴스 메서드의 이름은 보통
toType
형태로 짓는다- ex)
toString
,toArray
등
- ex)
- 객체의 내용을 다른 뷰로 보여주는 메서드의 이름은
asType
형태로 짓는다.- ex)
asList
등
- ex)
- 객체의 값을 기본 타입 값으로 반환하는 메서드의 이름은 보통
typeValue
형태로 짓는다.- ex)
intValue
등
- ex)
- 정적 팩터리의 이름은 다양하지만
from
,of
,valueOf
,instance
,getInstance
,newInstance
,getType
,newType
을 흔히 사용한다. boolean
타입의 필드 이름은 *보통 *boolean
접근자 메서드에서 앞 단어를 뺀 형태다.- ex)
initialized
,composite
등
- ex)
boolean
이 아닌 다른 타입의 필드라면 명사나 명사구를 사용한다.- ex)
height
,digits
,bodyStyles
등
- ex)
- 지역변수 이름도 필드와 비슷하게 지으면 되나, 조금 더 느슨하다.
- 어떤 동작을 수행하는 메서드의 이름은 동사나 (목적어를 포함한) 동사구로 짓는다.
'📚 Reading > Tech' 카테고리의 다른 글
이펙티브 자바 - 10장. 예외 (0) | 2023.01.24 |
---|---|
이펙티브 자바 - 8장. 메서드 (0) | 2023.01.24 |
이펙티브 자바 - 7장. 람다와 스트림 (0) | 2023.01.24 |
이펙티브 자바 - 6장. 열거 타입과 애너테이션 (0) | 2023.01.24 |
이펙티브 자바 - 5장. 제네릭 (0) | 2023.01.24 |