정규표현식

정규표현식이란?

정규 표현식은 패턴(pattern)으로 특정 목적을 위해 필요한 문자열 집합을 지정하기 위해 쓰이는 식이다.

메타문자

정규표현식에서 일정한 의미를 가지고 쓰는 특수문자를 메타 문자라고 부른다. 여기에 쓰이는 글자가 포함된 패턴을 넣으려면 해당 글자 앞에 \를 넣어줘야 한다.

  • ^ : 문자열의 시작
  • $ : 문자열의 종료. 옵션에 따라 문장의 끝 또는 문서의 끝에 매치된다.
  • . : 임의의 한 문자
  • []: 문자 클래스. 문자 클래스 안에 들어가 있는 문자는 그 바깥에서 하나의 문자로 취급된다.
    • ^ : 문자 클래스 내에서 ^는 not
    • - : ex) a-z는 a에서 z까지의 문자
  • | : or를 나타냄
  • ? : 앞 문자가 없거나 하나 있음
  • + : 앞 문자가 하나 이상임
  • * : 앞 문자가 0개 이상임
  • {n,m} : 앞 문자가 n개 이상 m개 이하. {0,1} 은 ?와 같은 의미다.
  • {n,} : 앞 문자가 n개 이상. 위의 형태에서 m이 생략된 형태이다. {0,} 이면 *와 같고 {1,} 이면 +와 같은 의미이다.
  • {n} : 앞 문자가 정확히 n개. {n,n} 과 같은 의미이다.
  • () : 하나의 패턴구분자 안에 서브 패턴을 지정해서 사용할 경우 괄호로 묶어주는 방식을 사용한다.
  • \s : 공백문자
  • \b : 문자와 공백 사이를 의미한다.
  • \d : 숫자 [0-9]와 같다.
  • \t : 탭문자
  • \w : 단어 영문자+숫자+_(밑줄) [0-9a-zA-Z_]
    문자 이스케이프는 대문자로 적으면 반대를 의미한다.

패턴 변경자

패턴구분자가 끝나면 그 뒤에 쓰는 것으로, 패턴에 일괄적으로 변경을 가할 때 사용한다. 정규식 엔진에 따라 변경자의 적용 방식이 상이하다.

  • i : 패턴을 대소문자 구분 없이 검사한다. 이 변경자를 사용할 경우 [a-z]로만 검사해도 자동으로 [a-zA-Z]와 같은 기능을 하게 된다. 영어가 아닌 언어(독일어, 프랑스어 등)를 다룰 때에는 버그 가능성이 있으므로 쓰지 않는 게 좋다. 대소문자라는 개념이 없는 한글, 한자, 가나문자는 이 패턴 변경자가 아무 역할도 하지 않는다.
  • s : 임의의 한 문자를 가리키는 . 메타 문자에 개행 문자(\n)도 포함시키도록 한다. 이 변경자를 사용하면 .이 줄바꿈도 임의의 한 문자로 취급하여 찾는다.
  • g : ^문자가 문장이 아닌 문서의 처음에, $ 문자가 문장의 끝(라인 피드 \n)이 아닌 주어진 문자열의 끝에 매치되게 변경한다.
  • m : 주어진 문자열에 줄바꿈이 있을 경우, 여러 줄로 취급하여 검사한다. (줄바꿈이 없다면 써도 의미가 없다.) 원래 정규표현식을 쓸 때 줄바꿈은 무시되는데, 이걸 사용하면 줄바꿈을 적용해서 검사한다. 그리고 ^은 한 줄의 시작, $는 한 줄의 끝으로 의미가 달라진다.
  • x : 공백 문자를 무시한다. 단, 이스케이프(역슬래쉬하고 같이 쓸 경우)하거나 문자 클래스 안에 있을 경우에는 예외. 정규식을 조금 더 읽기 편하게 만들어준다. 그러나 이 변경자를 지원하지 않는 엔진이 많은 게 단점이다.

Java

  • Java 경우에는 Pattern, Matcher 객체를 이용해서 원하는 문자열을 탐색할 수 있다.
  • String 객체의 replace도 정규식을 지원하는데 내부적으로는 Pattern, Matcher를 이용한다.
  • 특이하게도, Java에서는 Global 플래그가 디폴트로 적용되어 있다.
Pattern pattern = Pattern.compile("tomato", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher("tomato apple TOMATO");
while(matcher.find()) {
  System.out.println("count : " + matcher.groupCount());
  System.out.println("matched string : " + matcher.group());		// 패턴에 매칭된 문자열
  System.out.println("matched string : " + matcher.group(0));		// 패턴에 매칭된 문자열(matcher.group()과 동일)

}

- 결과 -
count : 0
matched string : tomato
matched string : tomato
count : 0
matched string : TOMATO
matched string : TOMATO

Javascript

  • Javascript 경우에는 RegExp와 String 객체에서 지원하는 함수를 이용해서 여러 정규식 처리를 할 수 있다.
  • RegExp 객체
    – exec() : 정규식 패턴과 일치하는 문자열을 배열로 반환한다. 없는 경우에는 null을 반환한다. 글로벌 플래그 사용시 lazy 하게 매칭된 문자열에 접근
    • test() : 정규식 패턴과 대응하는 문자열의 존재 여부를 true/false 로 반환한다.
    • match() : exec() 와 동일한 기능이지만, exec() 는 글로벌 플래그 사용시 즉시 매칭된 문자열 배열을 반환한다.
  • String 객체
    – search() : 정규식 패턴과 대응하는 문자열의 인덱스를 반환한다. 없는 경우에는 -1을 반환한다.
    • replace() : 정규식 패턴과 대응하는 문자열을 다른 문자열로 치환한다.
      – split() : 정규식 패턴을 기준으로 전체 문자열을 나눈다.
## exec() ##
let match;
const reg = /tomato/gi;
while((match = reg.exec('tomato apple TOMATO')) !== null) {
        // 매칭된 문자열 처리
}

- 결과 -
[ 'tomato',
  index: 0,
  input: 'tomato apple TOMATO',
  groups: undefined ]
[ 'TOMATO',
  index: 13,
  input: 'tomato apple TOMATO',
  groups: undefined ]

## test() ##
/tomato/gi.test('tomato apple TOMATO');

- 결과 -
true

## match() ##
"tomato apple TOMATO".match(/tomato/gi)

- 결과 -
[ 'tomato', 'TOMATO' ]

## replace() ##
"tomato apple TOMATO".replace(/tomato/gi, "banana")

- 결과 -
banana apple banana

## split() ##
"tomato apple TOMATO".split(/\s/)

- 결과 -
[ 'tomato', 'apple', 'TOMATO' ]

자주 쓰이는 패턴

1) 숫자만 : ^[0-9]*$

2) 영문자만 : ^[a-zA-Z]*$

3) 한글만 : ^[가-힣]*$

4) 영어 & 숫자만 : ^[a-zA-Z0-9]*$

5) E-Mail : ^[a-zA-Z0-9]+@[a-zA-Z0-9]+$

6) 휴대폰 : ^01(?:0|1|[6-9]) – (?:\d{3}|\d{4}) – \d{4}$

7) 일반전화 : ^\d{2,3} – \d{3,4} – \d{4}$

8) 주민등록번호 : \d{6} \- [1-4]\d{6}

9) IP 주소 : ([0-9]{1,3}) \. ([0-9]{1,3}) \. ([0-9]{1,3}) \. ([0-9]{1,3})

BigDecimal

조사하게 된 계기?

  • 할인 관련 API 작업을 진행중에 퍼센트 계산을 위해 나누기 연산을 하였는데이상한 오류를 발견했다. 분명 딱 떨어져야 하는 값인데! x.999999999로 나오고 있는!! ㅜㅜ 오픈 하루전에 찾아낸 버그라서.. 굉장히 마음 졸였었던 기억..

BigDecimal?

  • BigDecimal은 Java 언어에서 숫자를 정밀하게 저장하고 표현할 수 있는 유일한 방법이다.
  • 소수점을 저장할 수 있는 가장 크기가 큰 타입인 double은 소수점의 정밀도에 있어 한계가 있어 값이 유실될 수 있다.
  • Java 언어에서 돈과 소수점을 다룬다면 BigDecimal은 선택이 아니라 필수이다.
  • BigDecimal의 유일한 단점은 느린 속도와 기본 타입보다 조금 불편한 사용법 뿐이다.

double, 무엇이 문제인가?

double a = 10.0000;
double b = 3.0000;

// 기대값: 13
// 실제값: 13.000001999999999
a + b;

// 기대값: 7
// 실제값: 6.999999999999999
a - b;

// 기대값: 30
// 실제값: 30.000013000000997
a * b;

// 기대값: 3.33333...
// 실제값: 3.333332555555814
a / b;

BigDecimal 초기화

  • double 타입으로 부터 BigDecimal 타입을 초기화하는 방법으로 가장 안전한 것은 문자열의 형태로 생성자에 전달하여 초기화하는 것이다. double 타입의 값을 그대로 전달할 경우 앞서 사칙연산 결과에서 본 것과 같이 이진수의 근사치를 가지게 되어 예상과 다른 값을 얻을 수 있다.
  • https://stackoverflow.com/questions/7186204/bigdecimal-to-use-new-or-valueof/7186298#7186298
// double 타입을 그대로 초기화하면 기대값과 다른 값을 가진다.
// 0.01000000000000000020816681711721685132943093776702880859375
new BigDecimal(0.01);

// 문자열로 초기화하면 정상 인식
// 0.01
new BigDecimal("0.01");

// 위와 동일한 결과, double#toString을 이용하여 문자열로 초기화
// 0.01
BigDecimal.valueOf(0.01);

BigDecimal 비교 연산

  • BigDecimal은 기본 타입이 아닌 오브젝트이기 때문에 특히, 동등 비교 연산을 유의해야 한다. double 타입을 사용하던 습관대로 무의식적으로 == 기호를 사용하면 예기치 않은 연산 결과를 초래할 수 있다.
BigDecimal a = new BigDecimal("2.01");
BigDecimal b = new BigDecimal("2.010");

// 객체의 레퍼런스 주소에 대한 비교 연산자로 무의식적으로 값의 비교를 위해 사용하면 오동작
// false
a == b;

// 값의 비교를 위해 사용, 소수점 맨 끝의 0까지 완전히 값이 동일해야 true 반환
// false
a.equals(b);

// 값의 비교를 위해 사용, 소수점 맨 끝의 0을 무시하고 값이 동일하면 0, 적으면 -1, 많으면 1을 반환
// 0
a.compareTo(b);

BigDecimal 사칙 연산

  • Java에서 BigDecimal 타입의 사칙 연산 방법은 아래와 같다. 보다시피 double 타입보다 장황하고 귀찮은 편이다.
BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");

// 더하기
// 13
a.add(b);

// 빼기
// 7
a.subtract(b);

// 곱하기
// 30
a.multiply(b);

// 나누기
// 3.333333...
// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
a.divide(b);

// 나누기
// 3.333
a.divide(b, 3, RoundingMode.HALF_EVEN);

// 나누기 후 나머지
// 전체 자리수를 34개로 제한
// 1
a.remainder(b, MathContext.DECIMAL128);

// 절대값
// 3
new BigDecimal("-3").abs();

// 두 수 중 최소값
// 3
a.min(b);

// 두 수 중 최대값
// 10
a.max(b);

BigDecimal 소수점 처리

  • RoundingMode.HALF_EVEN은 Java의 기본 반올림 정책으로 금융권에서 사용하는 Bankers Rounding와 동일한 알고리즘이다. 금융권에서는 시스템 개발시 혼란을 막기 위해 요구사항에 반올림 정책을 명확히 명시하여 개발한다.
// 소수점 이하를 절사한다.
// 1
new BigDecimal("1.1234567890").setScale(0, RoundingMode.FLOOR);

// 소수점 이하를 절사하고 1을 증가시킨다.
// 2
new BigDecimal("1.1234567890").setScale(0, RoundingMode.CEILING);
// 음수에서는 소수점 이하만 절사한다.
// -1
new BigDecimal("-1.1234567890").setScale(0, RoundingMode.CEILING);

// 소수점 자리수에서 오른쪽의 0 부분을 제거한 값을 반환한다.
// 0.9999
new BigDecimal("0.99990").stripTrailingZeros();

// 소수점 자리수를 재정의한다.
// 원래 소수점 자리수보다 작은 자리수의 소수점을 설정하면 예외가 발생한다.
// java.lang.ArithmeticException: Rounding necessary
new BigDecimal("0.1234").setScale(3);

// 반올림 정책을 명시하면 예외가 발생하지 않는다.
// 0.123
new BigDecimal("0.1234").setScale(3, RoundingMode.HALF_EVEN);

// 소수점을 남기지 않고 반올림한다.
// 0
new BigDecimal("0.1234").setScale(0, RoundingMode.HALF_EVEN);
// 1
new BigDecimal("0.9876").setScale(0, RoundingMode.HALF_EVEN);

BigDecimal 나누기 처리

BigDecimal b10 = new BigDecimal("10");
BigDecimal b3 = new BigDecimal("3");

// 나누기 결과가 무한으로 떨어지면 예외 발생
// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
b10.divide(b3);

// 반올림 정책을 명시하면 예외가 발생하지 않음
// 3
b10.divide(b3, RoundingMode.HALF_EVEN);

// 반올림 자리값을 명시
// 3.333333
b10.divide(b3, 6, RoundingMode.HALF_EVEN);

// 3.333333333
b10.divide(b3, 9, RoundingMode.HALF_EVEN);

// 전체 자리수를 7개로 제한하고 HALF_EVEN 반올림을 적용한다.
// 3.333333
b10.divide(b3, MathContext.DECIMAL32);

// 전체 자리수를 16개로 제한하고 HALF_EVEN 반올림을 적용한다.
// 3.333333333333333
b10.divide(b3, MathContext.DECIMAL64);

// 전체 자리수를 34개로 제한하고 HALF_EVEN 반올림을 적용한다.
// 3.333333333333333333333333333333333
b10.divide(b3, MathContext.DECIMAL128);

// 전체 자리수를 제한하지 않는다.
// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result. 예외가 발생한다.
b10.divide(b3, MathContext.UNLIMITED);

BigDecimal과 Java Stream

// POJO 목록에서 BigDecimal 타입을 가진 특정 필드의 합을 반환
BigDecimal sumOfFoo = fooList.stream()
    .map(FooEntity::getFooBigDecimal)
    .filter(foo -> Objects.nonNull(foo))
    .reduce(BigDecimal.ZERO, BigDecimal::add);

// 특정 BigDecimal 필드를 기준으로 오름차순 정렬된 리스트를 반환
foolist.stream()
    .sorted(Comparator.comparing(it -> it.getAmount()))
    .collect(Collectors.toList());

// 위와 동일한 기능, 정렬된 새로운 리스트를 반환하지 않고 원본 리스트를 바로 정렬
foolist.sort(Comparator.comparing(it -> it.getAmount()));

BigDecimal 유닛 테스트

  • BigDecimal은 JUnit에서 Assertion 기능을 제공하지 않아 유닛 테스트가 불편하다. AssertJ 라이브러리를 이용하면 아래와 같이 네이티브하게 BigDecimal에 대한 유닛 테스트를 수행할 수 있다.
  • https://joel-costigliola.github.io/assertj/
BigDecimal a = BigDecimal.valueOf(0.1);
BigDecimal b = BigDecimal.valueOf(0.10);
BigDecimal c = BigDecimal.valueOf(0.101);
BigDecimal d = BigDecimal.valueOf(0.001);

// equals()와 동일하기 때문에 소수점 마지막 0까지 동일해야 true
// false
assertThat(a).isEqualTo(b));

// compareTo()와 동일하기 때문에 소수점 마지막 0이 달라도 true
// true
assertThat(a).isEqualByComparingTo(b);

// 두 수가 주어진 오차 범위를 만족하면 true
// true
assertThat(a).isCloseTo(c, within(d));

BigDecimal 관련 라이브러리

  • big-math 라이브러리는 java.lang.Math 클래스의 BigDecimal 버전이라고 할 수 있다. BigDecimal 기반 연산과 관련된 여러 유용한 기능을 제공한다. 
  • https://github.com/eobermuhlner/big-math

이펙티브 자바

아이템15. 클래스와 멤버의 접근 권한을 최소화하라

컴포넌트가 잘 설계되고 안 되고의 차이는 클래스 내부 구현 정보를 외부 컴포넌트로부터 얼마나 잘 숨겼지에 따라 결정된다. 즉, 구현과 API를 깔끔히 분리하는 것이다. 이것은 정보은닉 혹은 캡슐화라고 흔히 불린다.

15-1. 정보은닉의 장점
  • 개발 속도가 높다. 여러 컴포넌트를 병렬로 개발할 수 있기 때문이다.
  • 관리비용이 낮다. 빨리 파악할 수 있고, 다른 컴포넌트로 교체의 비용도 적다. 같은 이유로 성능최적화에 도움이 된다.
15-2. 정보은닉을 제대로 구현하기

자바는 언어레벨에서 정보은닉을 위한 다양한 장치를 제공해주는데 클래스, 인터페이스, 접근제어자 등이 있다. 특히 접근제어자를 통해 정보은닉을 잘 구현해낼 수 있다. 핵심은 모든 클래스와 멤버의 접근성을 가능한 좁혀야 한다. 올바르게 동작하는 한 가장 낮은 접근 수준을 부여하는게 좋다. public 으로 만들어두면 영원히 하위호환성을 고려해주며 변경해야한다.

  1. 패키지 외부에서 쓸 일이 없다면 package-private(접근제어자의 default값으로 아무것도 안 붙인 상태)로 하라. 그러면 내부 구현이므로 좀 더 쉽게 교체할 수 있다.
  2. 한 클래스에서만 사용되는 package-private 클래스는 사용하는 클래스의 private static 으로 중첩시켜 써라. 이렇게 중첩하면 바깥 클래스에서만 접근할 수 있다.
  3. 테스트 코드는 같은 패키지 경로에 두면 package-private 를 테스트할 수 있으므로, 테스트를 위해 접근제어자를 푸는 것은 올바르지 않다.
  4. public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다. public 필드는 불변을 보장할 수 없기 때문이다. 또한 스레드에 안전하지 않다.
  5. public static final는 기본 타입이나 불변 객체를 참조해야한다. public static final는 다른 객체를 참조하도록 바꿀 순 없지만, 참조된 객체 자체가 수정될 수가 있기 때문이다.
    • 이런 경우 Collections.unmodifiableList(), Map 등으로 불변화해서 참조하자

멤버 접근성을 좁히지 못하게 하는 제약이 1가지 있다. 상위 클래스의 메소드를 재정의할 때는 그 접근 수준을 상위 클래스에서보다 좁게 설정할 수 없다는 것이다.

아이템16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라

public 클래스의 필드가 public이라면 이것을 사용하는 클라이언트가 생길 수가 있으므로 변경비용이 비싸진다. 또한 위에서 설명했듯이 불변식을 보장할 수도 없고, 외부에서 필드에 접근할 때 부수적인 작업(복사본 던지기, 파라미터 검증 등)을 할 수도 없다. 이런 단점들은 private 필드에 public 접근자(getter/setter)를 두면 해결된다.

public 클래스가 아닌 package-private 클래스 또는 private 중첩 클래스라면 public 필드는 전혀 문제가 되지 않는다. 오히려 선언, 사용에서 더 깔끔하기도 하다. 어차피 내부에서만 동작하기 때문이다.

여기서 private 중첩 클래스는 이해되는데, package-private 클래스는 좀 의문이 들 수 있다. 책의 저자는 아마도 한 패키지는 한 개발자(=같은 개발자)가 개발하기 때문에 오용하지 않을 것이라고 판단한 것 같다.

아이템17. 변경 가능성을 최소화하라

불변 클래스는 인스턴스의 내부 값을 수정할 수 없는 클래스이다. 객체가 파괴되는 순간까지 값이 절대 달라지지 않는다. 이런 특성으로 스레드 안전해지므로 따로 동기화할 필요도 없어진다. 불변 클래스에서는 한번 만든 인스턴스를 캐싱해서 재활용할 수도 있다. 다음 예제에서 Complex는 불변이다. public static final ZERO = new Complex(0, 0);

또한 불변 객체는 방어적 복사도 필요없다. clone 메소드나 복사생성자, 복사팩토리도 필요없다. 대표적인 실수로 String의 복사 생성자를 쓸데없이 넣은 것이다. String Docs를 보면 public String(String original) 에 이런 문구가 있다. Unless an explicit copy of original is needed, use of this constructor is unnecessary since Strings are immutable.

getter가 있다고 꼭 setter를 만들지 말자. 꼭 필요한 경우가 아니라면 불변으로 만들자. 완전히 불변으로 만들 수 없을 때는 변경할 수 있는 부분을 최소한으로 줄이자

17-1. 불변 클래스를 만드는 규칙
  1. 객체의 상태를 변경하는 메소드를 제공하지 않는다.
  2. 클래스를 확장할 수 없도록 한다. 하위 클래스에서 부주의하게 객체의 상태를 변하게 하는 것을 막을 수 있다.
    • final Class 로 선언할 수도 있지만, final을 선언안하고도 모든 멤버와 생성자를 private, package-private으로 선언하고 public static factory를 제공해서 사실상 확장할게 없게 할 수도 있다.
  3. 모든 필드를 final로 선언한다. 시스템에서 권장하는 가장 명확한 방법이다.
  4. 모든 필드를 private로 선언한다. 클라이언트가 객체에 직접 접근하여 값을 수정하는 일을 막아준다.
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. (?)
17-2. 불변객체의 단점
  1. 값이 다르면 반드시 독립된 객체로 만들어줘야 한다. 내부 필드값 중 99개가 같고 1개만 달라도, 새로 만들어줘야하는 것이다.
  2. 객체를 완성하기까지 단계가 많고, 중간 단계의 객체들이 모두 버려지면 성능문제가 생길 수 있다.
    • 해결방법으로는 다단계 연산을 기본으로 제공하는 것이다. 가변 동반 클래스라고 한다. (불변클래스인 String으로 예를들자면, StringBuilder, StringBuffer이 있다)
17-3. [실무권장] guava의 Immutable Collections을 사용하자
  • Immutable Collections Docs
  • 이를 통해 불변 컬렉션을 손쉽게 만들 수 있다. 이 때 java의 컬렉션API의 Collections.unmodifiableList 와는 다르게 null을 허용하지 않는다.
  • 네이밍은 보통 ImmutableXXX 이다. 주요 API로 Map과 Set 계열은 of, copyOf, builder가 가능하며 List 계열은 asList가 있다. 자세한 건 문서를 참조하자
  • java 9부터는 Immutable static factory 메소드가 추가되었다. java9 이상을 사용한다면 Immutable Collections를 쉽게 사용하기 위해 guava를 사용할 필요가 없다.

아이템18. 상속보다는 컴포지션을 사용하라

상속은 잘못 사용하면 오류를 내기 쉬운 소프트웨어를 만들게 된다. 메소드 호출과 달리 상속은 캡슐화를 깨트린다. 슈퍼클래스의 구현에 의존하게되며, 슈퍼클래스의 구현 내용이 바뀌었다면 서브클래스도 이에 맞춰 진화해야한다. (상속은 상위클래스의 결함까지 그대로 승계된다) 슈퍼클래스의 메소드가 아닌 새로운 메소드를 서브클래스에서 정의했는데, 운없게도 다음 배포판에서 슈퍼클래스에 새로 생긴 메소드명과 겹쳐서 컴파일이 실패할 수도 있다.

이럴 떄 해결방법으로 컴포지션(composition, 구성)은 private 필드를 만들고 상속대신 참조하는 것이다. 새 클래스의 메소드(전달 메소드)들은 참조 클래스의 메소드들을 호출하여 전달한다.

상속은 반드시 하위클래스가 상위클래스의 진짜 하위타입인 상황에만 쓰여야 한다. A를 상속하는 B를 작성하려한다면 ‘B가 정말 A인가?’라고 자문해보자. 맞다면 상속하고, 아니라면 컴포지션하자.

아이템19. 상속을 고려해 설계하고 문서화하라. 그렇지 않았다면 상속을 금지하라

아이템18에서는 상속을 염두에 두지않고 설계했고 상속의 주의점을 문서화해놓지 않은 외부클래스를 상속할 떄 위험을 경고했다. 상속을 고려한 문서화는 메소드를 재정의했을때 일어나는 일을 정확히 정리하는 것이다. 재정의가능한 공개메소드에서 같은 위치(self-use)의 메소드(재정의 가능한 공개)를 호출할 수도 있기 때문이다. 이것은 상속이 캡슐화를 해치기 때문에 일어나는 현상이다.

문서에서 Implementation Requirements 로 시작하는 절이 있는데, 이게 내부 동작 방식을 설명하는 곳이다.

클래스의 내부 동작 과정 중간에 호출될 수 있는 메소드(재정의 가능한 공개, hook 메소드)를 선별하여 protected 같은 접근제어자로 공개해야할 수도 있다. (이 부분을 재정의해서 다른 메소드의 성능개선 등을 이뤄낼 수 있으니)

상속용으로 설계한 클래스의 테스트는 하위클래스를 몇 개 직접 만들어보는 수 밖에 없다.

또한 상속용 클래스의 생성자는 어떤 경우에도 재정의 가능 메소드를 호출해서는 안된다. 이는 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로, 하위 클래스에서 재정의한 메소드가 하위 클래스의 생성자보다 먼저 호출되기 때문이다. (private, final, static은 재정의가 불가능하니 호출해도 된다)

아이템20. 추상 클래스보다는 인터페이스를 우선하라

자바가 제공하는 다중구현 메커니즘은 인터페이스와 추상클래스로 가능하다. 자바8부터는 인터페이스도 default 메소드를 쓸 수 있게 되면서, 둘다 인스턴스 메소드를 구현한 형태로 제공할 수 있게 되었다. 인터페이스와 추상클래스의 가장 큰 차이는 추상클래스를 구현한 서브클래스는 반드시 추상클래스의 하위 타입이 되어야만 한다는 것이다. (클래스는 단일상속)

새로운 인터페이스는 클래스에 쉽게 넣을 수 있지만, 새로운 추상클래스를 끼워넣으려면 계층구조가 복잡(이미 어떤 클래스를 상속받고있는데, 또 상속받아야하니)해진다. 이런 경우처럼 인터페이스는 믹스인(mixin) 정의에 안성맞춤이다. 또한 인터페이스 간에 상속을 통해 유연하게 만들 수도 있다.

인터페이스와 추상 골격구현 클래스를 제공하여 2개의 장점을 취하는 방법도 있다. 인터페이스의 이름이 XXX라면, 보통 추상 골격구현 클래스는 AbstractXXX가 된다. 주로 이런 구조는 템플릿메소드 패턴에 많이 쓰인다.

아이템21. 인터페이스는 구현하는 쪽을 생각해 설계하라

자바8 이전에는 인터페이스에 새로운 메소드를 추가할 경우 보통 컴파일 오류가 난다. 구현 클래스들에서 구현을 하지 않았기 때문이다. 자바8부터 인터페이스에 default 메소드, static 메소드가 등장하면서 새로운 메소드를 모든 구현클래스의 도움없이 추가하는 방법이 생기긴 하였으나, 모든 상황에서 불변식을 해치지 않는 default 메소드를 작성하는 것은 매우 어렵다. (책에서는 예제로 apache commons의 SynchronizedCollection을 들고 있다.)

추가된 default 메소드는 제거하거나 시그니처를 수정하는 등의 행동 역시 신중히 해야한다. 사용하는 클라이언트에서 전부 깨질 수 있기때문이다.

아이템22. 인터페이스는 타입을 정의하는 용도로만 사용하라

인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입역할을 한다. 클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 사용측에 이야기해주는 것이다. 인터페이스는 오직 이 용도로만 사용해야 한다.

22-1. 인터페이스를 잘못 사용한 예

  • 상수 인터페이스 : public static final 필드들만 있는 인터페이스
    • 사용하는 쪽에서 상수용 인터페이스는 아무런 의미가 없다. 혼란만 줄 뿐이다.
    • Integer.MAX_VALUE, enum, 정적 유틸리티 클래스(XXXConstants) 등에서 쓰는게 훨씬 더 좋다.

아이템23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라

태그달린 클래스는 2가지 이상의 값을 표현하는 클래스이다. 값의 의미 분기처리를 태그로 한다.

class Figure {
    enum Shape { RECTANGLE, CIRCLE };
    final Shape shape; // 태그

    // 사각형일 때만 필요한 필드
    double length;
    double width;

    // 원일 때만 필요한 필드
    double radius;

    // 원 생성자
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    // 사각형 생성자
    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    double area() {
        switch(shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError(shape);
        }
    }
}

불필요한 코드들이 너무 많고, 장황하며, 오류를 내기 쉽고 비효율적이다.
이것은 클래스 계층구조로 바꾸는 것이 좋다.

abstract class Figure {
    abstract double area();
}

class Circle extends Figure {
    final double radius;

    Circle(double radius) { this.radius = radius; }

    @Override
    double area() { return Math.PI * (radius * radius); }
}

class Rectangle extends Figure {
    final double length;
    final double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width  = width;
    }
    
    @Override
    double area() { return length * width; }
}

위에서 열거한 단점들이 해소되었으며, 컴파일 타임의 검사를 최대한 활용(추상메소드 구현 여부 등)하게 되었다. 또한 다른 도형을 추가할때도 확장성있는 형태가 되었다.

아이템24. 멤버 클래스는 되도록 static 으로 만들라

중첩 클래스(nested class)는 다른 클래스 안에 정의된 클래스를 말한다. 중첩 클래스는 자신을 감싼 바깥 클래스에만 쓰여야하며, 그 외의 쓰임새가 있다면 톱레벨 클래스로 만들어야 한다.

중첩클래스와 멤버클래스는 동일한 말로 이해하면 된다. 중첩클래스에는 정적 멤버클래스, (비정적) 멤버클래스, 지역클래스, 익명클래스가 있는데, 자세하게 알고 싶다면 다음 포스팅을 참고하자.

중첩클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들어라. 비정적(non-static)일 경우 바깥 인스턴스에 대한 숨은 참조(바깥 클래스명.this)를 가져올 수 있게 되는데 비용이 들어간다. 또한 이 참조때문에 GC가 제때 수거하지 못해서 메모리 누수가 생길 수도 있다.

비정적 멤버클래스는 바깥 인스턴스에 접근할 일이 있을 때 사용하면 된다. 컬렉션 패키지들이 이를 잘 활용하였는데, 보통 자신의 컬렉션 뷰(keySet, entrySet, values 등)를 반환할 때 사용한다. 다음은 HashMap에서 values()의 코드 일부이다.

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    public Collection<V> values() {
        Collection<V> vs = values;
        if (vs == null) {
            vs = new Values();
            values = vs;
        }
        return vs;
    }

    final class Values extends AbstractCollection<V> {
        public final void clear() {
            HashMap.this.clear();
        }
        ...
    }
    ...
}

또한 톱레벨 클래스 내부에서만 쓰인다면 private 중첩 클래스로 작성하여 접근범위를 최소화시키는 게 좋다.

아이템25. 톱레벨 클래스는 한 파일에 하나만 담으라

소스 파일 하나에 톱레벨 클래스를 여러개 선언하여도, 자바 컴퍼일러는 문제삼지 않는다. 하지만 문제가 되는 경우는 존재한다. 예를들면 한 파일에 클래스 2개가 정의되어있는데, 다른 파일에 똑같은 클래스명으로 클래스 2개가 정의되어 있는 경우이다. 이럴 때는 컴파일이 실패하거나, 컴파일 순서에 따라 어떻게 동작할 지 예측할 수 없게 된다.

따라서 한 파일에는 하나의 톱레벨 클래스, 인터페이스만 작성하자

제네릭은 jdk1.5 부터 사용할 수 있다. 제네릭을 지원하기 전에는 컬렉션에서 객체를 꺼낼 때 마다 형변환을 해야 했다. 1.5 부터는 제네릭을 사용하면 컬렉션에 담을 수 있는 타입을 컴파일러에게 알려주며, 컴파일러가 알아서 형변환 코드를 추가한다. 또한 엉뚱한 객체를 넣는 코드가 있다면 컴파일 타임에 차단해준다.

아이템26. raw type은 사용하지 마라

26-1. raw type 이란?

클래스와 인터페이스 선언에 타입 매개변수(ex. <E>)가 있으면 각각 제네릭 클래스, 제네릭 인터페이스라고 부른다. 이것을 제네릭 타입이라고 한다. (ex. List<E>) 제네릭 타입을 정의하면 raw type도 정의되는데, 여기서 List<E>의 raw type이란 List 이다. 즉, 타입 매개변수를 쓰지 않은 경우를 말한다. 이것은 제네릭이 도입되기 전의 코드들의 호환성을 위한 것이다.

26-2. raw type의 사용을 비추천한다.

오류의 발견은 컴파일 타임에 되는게 가장 이상적이다. raw type을 사용할 경우, unchecked 경고가 나오며, 잘못된 타입을 add할 수도 있다. 이것은 런타임에서 문제가 생길 것이다. (ClassCastException 등) 따라서 raw type을 쓰지말고, 제네릭 타입을 쓴다면 컴파일러의 검사력(정적언어의 장점을 활용)과 타입 불변, 안정성을 얻을 수 있다.

자바와 같은 JVM언어 중에 코틀린은 제네릭을 쓰지 않으면 컬렉션을 쓰지 못하도록 아예 막아버렸다. 자바는 하위호환성 때문에 쓰지말라고 권고하면서도 막지 못한 셈..

26-3. raw type, 와일드 카드(<?>), <Object>의 차이

만약 타입 매개변수를 신경쓰지 않고 쓰고싶다해도 raw type보단 제네릭의 와일드카드를 쓰는 것이 좋다. (ex. Set<?>) 이렇게 하면 어떤 타입도 받을 수 있으면서 안전하며 유연해진다.

이 둘의 차이점은 raw type 은 타입에 안전하지 않으나, 와일드카드는 안전하다. 아래 예제를 보자

List rawList = new ArrayList<String>(); // 런타임에는 아무런 타입이 남지 않기때문에 컴파일 성공
List<?> wildList = new ArrayList<String>(); // 컴파일 성공
List<Object> genericList = new ArrayList<String>(); // 컴파일 실패

rawList.add("redboy"); // 잘 동작한다.

wildList.add("redboy"); // 제네릭 타입 매개변수에 의존성이 있는데, <?>는 타입을 알지 못하므로 컴파일에 실패한다. 타입안정성이 있는 셈
wildList.clear(); // 제네릭 타입 매개변수에 의존이 없으므로 동작한다.

<?>와 <Object>의 차이는 구체적 인스턴스의 차이인데, <?>는 제네릭 타입 매개변수에 의존성이 있는 코드가 있다면 컴파일러가 실패처리한다. <Object>는 내부에서 또 다시 형 변환해야하므로 코드가 좀 더 복잡해지며, 제네릭의 장점이 사라진다.

26-4. raw type을 쓰는 예외
  • class 리터럴은 raw type으로 써야한다. List.class 는 되지만, List<String>.class 은 허용되지 않는다.
  • instanceof 연산자는 런타임에서 타입을 비교한다. 제네릭 타입은 런타임에서 소거되므로 제네릭 타입으로 비교할 수 없다.
    • ex. o instanceof Set

아이템27. 비검사 경고를 제거하라

비검사 경고(ex. warning: [unchecked] unchecked ...)를 제거할수록 타입 안정성이 높아진다고 볼 수 있다. 만약 타입 안정성이 확실한데 컴파일러의 경고를 없애고 싶다면 @SuppressWarnings("unchecked")를 사용하자. 로그에 파묻혀서 필요한 로그를 발견하기 어렵게 하지않도록 말이다.

이때 @SuppressWarnings의 범위를 최대한 줄여서 달자. 메소드레벨, 클래스레벨보단 비검사 경고가 뜨는 지역변수 레벨에 다는 것이 가장 좋다. 또한 이때 타입에 안전한 이유를 주석으로 추가해두는 것이 좋다.

아이템28. 배열보다는 리스트를 사용하라

28-1. 배열과 리스트의 차이

배열은 공변(covariant)이다. class Sub extends Super 이라면 Sub[]는 Super[]의 하위 타입이다. 그러나 리스트는 불공변(invariant)이다. List<Sub>와 List<Super>은 하위-상위 타입의 관계가 아니다.

예를들어 List<String>에는 문자열만 넣을 순 있으나, List<Object>에는 어떤 객체도 넣을 수 있다. 이 둘은 서로 하는 일을 바꾼다면 제대로 수행하지 못한다.

아래 코드 예제를 보자.

Object[] objectArray = new Integer[1];
objectArray[0] = "Hello world"; // 런타임에 ArrayStoreException 발생

List<Object> objectList = new ArrayList<Integer>; // 컴파일 실패
objectList.add("Hello world"); // 위에서 이미 컴파일에 실패했으며, 타입이 달라 넣을 수도 없다

배열이든 리스트이던 Integer용 저장소에 String을 넣을 순 없으나, 전자는 런타임에 실수를 알 수 있고, 후자는 컴파일타임에 알 수 있다. 후자가 당연히 좋으니, 배열보다는 리스트를 사용하자. 계속 위에서 부터 같은 말을 하고 있지만 타입안정성을 얻을 수 있다.

다만 성능적인 측면에선 배열이 앞설 수 있다. 참고로 제네릭 배열은 애초에 만들 수 없게 막아두었다.

아이템29. 이왕이면 제네릭 타입으로 만들어라

다음은 일반 클래스를 제네릭 클래스로 만드는 방법이다.

  1. 클래스 선언에 타입 매개변수를 추가
  2. 일반 타입(ex. Object)를 타입 매개변수로 교체
  3. 비검사(unchecked) 경고 해결해주기

다음은 일반 클래스 Stack 을 제네릭 클래스로 바꿔본 예제이다.

public class Stack { // => Stack<E>
    private Object[] elements; // => E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    // => @SuppressWarnings("unchecked")
    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY]; // => (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) { // => push(E e)
        ensureCapacity();
        elements[size++] = e;
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }

    public Object pop() { // => E pop()
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size]; // => E result = elements[--size];
        elements[size] = null;
        return result;
    }

    ...
}

제네릭화(gernerification)의 장점은 위에서 계속 말한대로이다.

참고로 기본 타입은 제네릭 타입으로 쓸 수 없다. (ex. List<int>) 이럴 땐 박싱된 기본타입으로 쓰면 된다.

아이템30. 이왕이면 제네릭 메소드로 만들어라

클래스와 마찬가지로 메소드도 제네릭이 가능하다면 사용하자. 사용자 측에서 형변환하는 것보다 훨씬 안전하고 유연해진다. 제네릭 클래스 작성법과 비슷하다.

public static Set union(Set s1, Se s2) { // => <E> Set<E> union(Set<E> s1, Set<E> s2)
    Set result = new HashSet(s1); // => Set<E> result = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}
30-1. 제네릭 싱글톤 팩토리

때때로 불변 객체를 여러 타입으로 활용할 수 있게 만들어야 할 때가 있는데, 이때는 제네릭 싱글톤 팩토리를 만들면 된다. Collections.reverseOrder, Collections.emptySet이 좋은 예제이다.

@SuppressWarnings("unchecked")
public static <T> Comparator<T> reverseOrder() {
    return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
}

만약 제네릭을 쓰지 않았다면 요청 타입마다 형변환하는 정적 팩토리를 만들었어야 할 것이다. (타입별로 정적메소드가 1개씩..)

30-2. 재귀적 타입 한정

자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있다.

다음과 같이 타입 매개변수을 한정적으로 기술해주는 방식이다. 이를 통해 모든 타입 E는 자신과 비교할 수 있다라는 것을 나타냈다. max 메소드의 리턴값은 Comparable<E>을 구현했으므로, 다른 E와 비교할 수 있는 것이다.

public static <E extends Comparable<E>> E max(Collection<E> c)

아이템31. 한정적 와일드카드를 사용해 API 유연성을 높여라

제네릭의 매개변수화 타입(ex. E)은 불공변이다. 아이템 29의 제네릭 클래스 Stack을 예로 들어보자. 아래 메소드가 추가되었다.

public void pushAll(Iterable<E> src) { // 타입 매개변수는 클래스 레벨에 정의됨
    for (E e : src) push(e);
}

// 메인메소드
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers); // ERROR 발생. incompatible types: Iterable<Integer>

이것은 에러가 발생하며, 원인은 불공변때문이다. Iterable<Number>가 넘어와야 하는데 Iterable<Integer>가 넘어왔으며, 이 둘은 서로 다른 타입이기 때문에 컴파일 에러가 발생한 것이다. 사실 논리적으로는 문제가 없어야하는데, 컴파일러는 이것을 문제로 삼았다.

31-1. 한정적 와일드카드 타입을 사용하자

위 같은 경우 한정적 와일드카드 타입을 사용하면 쉽게 해결할 수 있다. E의 Iterable이 아닌 E의 하위타입의 Iterable로 만들면 된다. 아래처럼하면 Iterable<Integer>도 문제없이 컴파일된다.

public void pushAll(Iterable<? extends E> src) {
    for (E e : src) push(e);
}

만약 E의 상위타입을 표현하고 싶다면 <? super E> 라고 쓰면 된다.

이것은 어느정도 공식화되어있는데, PECS(producer-extends, consumer-super)라고 기억해두면 좋다. 매개변수화 타입 T가 생산자라면, <? extends T>를 사용하고, 소비자라면 <? super T>를 사용해라. 위의 pushAll()은 인스턴스를 생산하고 있으므로 생산자이다.

위의 30-2 에서 max 메소드도 한정적 와일드카드를 이용해 다듬을 수 있다. 입력 매개변수(c)는 E 인스턴스를 생산하므로 extends 이고, 타입 매개변수는 E 인스턴스를 소비하므로 super 이다.

public static <E extends Comparable<? super E>> E max(Collection<? extends E> c)

어떤 인터페이스를 직접 구현한 클래스를 확장한 타입을 지원하기 위해 한정적 와일드카드가 필요하다. 말이 복잡하지만.. 한정적 와일드카드를 씀으로써 계층구조를 유연하게 이용할 수 있다.

31-2. 타입 매개변수와 와일드카드 메소드

타입 매개변수와 와일드카드에는 공통된 부분이 있어서, 메소드를 정의할 때 어느 것을 사용해도 괜찮다.

public static <E> void swapA(List<E> list, int i, int j) {...}
public static void swapB(List<?> list, int i, int j) {...}

책에서는 swapB 스타일을 선호하는데, 신경써야할 타입 매개변수가 없는 점을 들었다. 그러나 결국 swapB는 겉으로 보기에만 깔끔할 뿐, swapA를 내부적으로 호출하는 wrapping method 일 뿐이다. (나같으면 swapA 스타일로 할 것 같은데..)

아이템32. 제네릭과 가변인수를 함께 쓸 때는 신중해라

가변인자는 제네릭과 함께 jdk 1.5에 추가되었으나, 이 둘을 혼용하면 타입 안정성이 깨질 수 있다.

가변인자를 받는 메소드를 호출하면 호출시점에 배열이 생긴다. 즉, 아이템 28에서 애초에 만들 수 없다던 제네릭 배열이 만들어지는 것이다. 이것은 가변인자가 실무에서 매우 유용하기 때문에 모순이지만 수용한 것이다. 다음은 제네릭 가변인자 메소드다.

// Arrays
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}
32-1. @SafeVarargs

jdk 1.7부터 도입된 @SafeVarargs를 사용하면 제네릭 가변인수와 관련된 경고를 숨길 수 있다. @SafeVarargs는 제네릭 가변인자 메소드 작성자가 그 메소드가 타입 안전함을 보장하는 장치다. 컴파일러는 이것을 믿고 경고를 하지않는다.

다음은 제네릭 가변인자 메소드를 안전하게 작성하는 방법이다. 이를 지킨다면 @SafeVarargs를 달아도 되며, 달지 못한다면 작성하면 안된다.

  1. 메소드가 제네릭 가변인자 배열에 아무것도 저장하지 않는다.
  2. 그 배열의 참조가 밖으로 노출되지 않는다. (return varargs)
  3. 즉, 순수하게 인수들을 전달하는 역할만 해야한다. (ex. Arrays.asList 메소드 참조)

참고로 @SafeVarargs은 재정의할 수 없는 메소드(static, final)에만 붙일 수 있다. 재정의한 메소드에서는 안전할 지 보장이 안되기 때문이다.

아이템33. 타입 안전 이종 컨테이너를 고려하라

33-1. 타입 안전 이종 컨테이너는 언제 쓰이는가?

Set<E>Map<K,V> 처럼 클래스 레벨에서 매개변수화 할 수 있는 타입의 수는 제한적이다. (ex. Map 은 2개)

타입의 수에 제약없이 유연하게 필요한 경우, 특정 타입 외에 다양한 타입을 지원해야하는 경우가 있을 수 있다. 이 때 클래스 대신 키를 매개변수화한 다음 get/set 할때 키 타입을 제공해주면 된다. 이것을 타입 안전 이종(heterogeneous) 컨테이너 패턴이라고 한다. (컨테이너 = 클래스 라고 이해하면 될 듯하다)

컴파일타임 타입 정보와 런타임 타입 정보를 위해 메소드에서 주고 받는 class 리터럴을 타입 토큰이라고 한다. (ex. Integer.class는 class 리터럴이며 타입토큰은 Class<Integer>) 타입 토큰은 타입 안전성이 필요한 곳에 주로 사용된다.

아래는 적절한 예제이다.

private Map<Class<?>, Object> map = new HashMap<>(); // 제네릭을 중첩해서 썼으므로 class 리터럴이면 뭐든 넣을 수 있다.

public <T> void put(Class<T> type, T instance) {
    map.put(Objects.requireNonNull(type), type.cast(instance));
}

public <T> T get(Class<T> type) {
    return type.cast(map.get(type)); // 동적 형 변환
}

// 메인메소드
put(String.class, "Redboy");
get(String.class);
33-2. 타입 안전 이종 컨테이너의 제약과 슈퍼 타입 토큰

실체화 불가 타입에는 사용할 수 없다. String, String[]은 사용할 수 있지만, List<String>은 사용할 수 없다. List<String>.class 라는 리터럴을 얻을 수 없기 때문이다.

이것을 해결하기 위해 슈퍼타입 토큰을 사용할 수 있다. 슈퍼 타입을 토큰으로 사용한다는 의미이며, 상속과 Reflection을 조합해서 List<String>.class 같이 사용할 수 없는 class 리터럴을 타입 토큰으로 사용하는 것과 같은 효과를 낼 수 있다.

아이템 42 – 익명 클래스보다는 람다를 사용하라

함수 객체와 람다식
  • 함수 객체 : 추상 메서드를 하나만 담은 인터페이스로 람다식이 등장하기 전에는 익명 클래스를 사용해서 주로 만들었음Collections.sort(words, new Comparator<String>() { public int compare(String s1, String s2) { return Integer.compare(s1.length(), s2.length()); } })
  • 자바8에와서 추상 메서드 하나짜리 인터페이스는 특별한 의미를 인정받아 특별한 대우를 받게됨
    람다식을 이용해서 훨씬 간결한 코드를 만들 수 있게됨
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
  • 타입을 명시해야 코드가 더 명확할 때만 제외하고는, 람다의 모든 매개변수 타입은 생략하자
람다의 단점
  • 람다는 이름이 없고 문서화도 못해서 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다를 쓰지 말아야 한다
    • 람다는 한 줄일 때 가장 좋고 길어야 세 줄 안에 끝내는 게 좋다
    • 람다가 길거나 가독성이 떨어진다면 더 간단히 줄여보거나 람다를 쓰지 않는 쪽으로 리팩토링 필요함
람다로 대체할 수 없는 것
  • 추상 클래스나 추상 메서드가 여러 개인 인터페이스의 인스턴스를 만들 때는 람다를 쓸 수 없어서 익명 클래스를 써야 함
  • 함수 객체가 자신을 참조해야 하는 경우 익명 클래스를 써야 함
    • 람다는 자신을 참조할 수 없음, 람다에서의 this 키워드는 바깥 인스턴스를 가리킴
    • 익명클래스의 this 키워드는 인스턴스 자신을 가리킴
람다 사용시 유의할 점
  • 직렬화 형태가 구현별로 다를 수 있어서 람다를 직렬화하는 일은 극히 삼가야 한다(익명 클래스의 인스턴스도 마찬가지)

아이템 43 – 람다보다는 메서드 참조를 사용하라

  • 함수 객체를 람다보다도 더 간결하는 만드는 방법이 바로 메서드 참조임

<람다식 사용 방법>

map.merge(key, 1, (count, incr) -> count + incr);

<메서드 참조 사용 방법>

map.merge(key, 1, Integer::sum);

메서드 참조를 사용하는 편이 보통은 더 짧고 간결하므로, 람다로 구현했을 때 너무 길거나 복잡하다면 메서드 참조가 좋은 대안이 되어줌

때로는 람다가 메서드 참조보다 간결할 때도 있음, 주로 메서드와 람다가 같은 클래스에 있을 때임

service.execute(GoshThisClassNameIsHumongous::action);
service.execute(() -> action());
메서드 참조 유형
메서드 참조 유형같은 기능을 하는 람다
정적Integer::parseIntstr -> Integer.parseInt(str)
한정적(인스턴스)Instant.now()::isAfterInstant then = Instant.now(); t -> then.isAfter(t)
비한정적(인스턴스)String::toLowerCasestr -> str.toLowerCase()
클래스 생성자TreeMap<K,V>::new() -> new TreeMap<K,V>()
배열 생성자int[]::newlen -> new Int[len]

메서드 참조 쪽이 짧고 명확하다면 메서드 참조를 쓰고 그렇지 않을 때만 람다를 사용하라

아이템 44 – 표준 함수형 인터페이스를 사용하라

  • 필요한 용도에 맞는게 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용하라
  • java.util.function 패키지에는 총 43개의 인터페이스가 있지만 기본 6개의 인터페이스만 기억하면 나머지를 충분히 유추해낼 수 있다
인터페이스함수 시그니처
UnaryOperatorT apply(T t)String::toLowerCase
BinaryOperatorT apply(T t1, T t2)BigInteger::add
Predicateboolean test(T t)Collection::isEmpty
Function<T,R>R apply(T t)Arrays::asList
SupplierT get()Instant::now
Consumervoid accept(T t)System.out::println
주의점
  • 표준 함수형 인터페이스 대부분은 기본 타입만 지원함, 그렇다고 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 말 것
    • 동작은 하지만 계산량이 많을 때는 성능이 처참히 느려질 수 있다
전용 함수형 인터페이스 구현이 필요한 경우
  • 자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다
  • 반드시 따라야 하는 규약이 있다
  • 유용한 디폴트 메서드를 제공할 수 있다
@FunctionalInterface의 목적
  • 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려준다
  • 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 해준다
  • 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다

아이템 45 – 스트림은 주의해서 사용하라

  • 스트림 파이프라인은 지연 평가(lazy evaluation)된다. 평가를 종단 연산이 호출될 때 이뤄지며, 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다
  • 스트림 API는 다재다능하여 사실상 어떠한 계산이라도 해낼 수 있다. 하지만 할 수 있다는 뜻이지, 해야 한다는 뜻은 아니다
  • 스트림을 제대로 사용하면 프로그램이 짧고 깔끔해지지만, 잘못 사용하면 읽기 어렵고 유지보수도 힘들어진다

<Stream을 사용하지 않은 코드>

public class Anagrams {
    public static void main(String[] args) throws IOException {
        File dictionary = new File(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);

        Map<String, Set<String> groups = new HashMap<>();
        try(Scanner s = new Scanner(dictionary)) {
            while (s.hasNext()) {
                String word = s.next();
                groups.computeIfAbsent(alphabetize(word),
                    (unused) -> new TreeSet<>()).add(word);
            }
        }

        for (Set<String group : groups.values())
            if (group.size() >= minGroupSize)
                System.out.println(group.size() + ": " + group);
    }

    private static String alphabetize(String s) {
        char[] a = s.toCharArray();
        Arrays.sort(a);
        return new String(a);
    }
}

<Stream을 과도하게 사용한 코드>

public class Anagrams {
    public static void main(String[] args) throws IOException {
        Path dictionary = Paths.get(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);

        try (Stream<String> words = Files.lines(dictionary)) {
            words.collect(
                groupingBy(word -> word.chars().sorted()
                            .collect(StringBuilder::new,
                                (sb, c) -> sb.append((char) c),
                                StringBuilder::append).toString()))
            .values().stream()
            .filter(group -> group.size() >= minGroupSize)
            .map(group -> group.size() + ": " + group)
            .forEach(System.out::println);
        }
    }
}

<Stream을 적절하게 사용한 코드>

public class Anagrams {
    public static void main(String[] args) throws IOException {
        Path dictionary = Paths.get(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);

        try (Stream<String> words = Files.lines(dictionary)) {
            words.collect(groupingBy(word -> alphabetize(word)))
                .values().stream()
                .filter(group -> group.size() >= minGroupSize)
                .forEach(g -> System.out.println(g.size() + ": " + g));
        }
    }

    // alphabetize 메서드는 위의 예제 코드와 동일함
}
  • 스트림을 과용하면 프로그램이 읽거나 유지보수하기 어려워진다
  • 람다에서는 타입 이름을 자주 생략하므로 매개변수 이름을 잘 지어야 스트림 파이프라인 가독성이 유지된다
  • 도우미 메서드를 적절히 활용하는 일의 중요성은 일반 반복 코드에서보다는 스트림 파이프라인에서 훨씬 크다
  • 기존 코드는 스트림을 사용하도록 리팩터링하되, 새 코드가 더 나아 보일 때만 반영하자
함수 객체로는 할 수 없지만 코드 블록으로는 할 수 있는 일
  • 지역 변수를 읽고 수정하는 것, 람다에서는 final이거나 사실상 final인 변수만 읽을 수 있다
  • return문을 사용해 메서드를 빠져나가거나 break, countinue문을 사용하는 것
스트림 사용이 안성맞춤인 일
  • 원소들의 시퀀스를 일관되게 반환한다
  • 원소들의 시퀀스를 필터링한다
  • 원소들의 시퀀스를 하나의 연산을 사용해 결합한다
  • 원소들의 시퀀스를 컬렉션에 모은다
  • 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다

아이템 46 – 스트림에서는 부작용 없는 함수를 사용하라

  • 스트림은 그저 또 하나의 API가 아닌, 함수형 프로그래밍에 기초한 패러다임이다
  • 스트림이 제공하는 표현력, 속도, 병렬성을 얻으려면 이 패러다임도 함께 받아들여야 한다
  • 스트림 패러다임의 핵심은 계산을 입력의 변환으로 재구성 하는 부분이다
  • 이때 각 변환 단계는 가능한 이전 단계의 결과를 받아 처리하는 순수 함수여야 한다
    • 순수 함수 : 오직 입력만이 결과에 영향을 주는 함수
  • 다른 가변 상태를 참조하지 않고 함수 스스로도 다른 상태를 변경하지 않도록 하려면 스트림 연산에 건네는 함수 객체는 모두 부작용 (side effect)가 없어야 한다
  • forEach 연산은 종단 연산 중 기능이 가장 적고 가장 ‘덜’ 스트림답다, forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고 계산하는 데는 쓰지 말자
  • 스트림을 사용하려면 수집기를 잘 알아둬야 한다
가장 중요한 수집기 팩터리
  • toList
  • toSet
  • toMap
  • groupingBy
  • joining

아이템 47 – 반환 타입으로는 스트림보다 컬렉션이 낫다

  • 스트림은 반복을 지원하지 않는다
    • 반복하기 위한 방법은 있으나 추가적인 변환 코드 작성이 필요하다
  • Collection 인터페이스는 Iterable의 하위 타입이고 stream 메서드도 제공하니 반복과 스트림을 동시에 지원한다
  • 따라서 원소 시퀀스를 반환하는 공개 API의 반환 타입에는 Collection이나 그 하위 타입을 쓰는게 일반적으로 최선이다
  • 단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안됨
    • 원소의 갯수가 많다면 전용 컬렉션을 구현할지 고민하라

아이템 48 – 스트림 병렬화는 주의해서 적용하라

  • 자바8에서부터 parallel 메서드 한 번 호출로 파이프라인을 병렬 실행할 수 있는 스트림을 지원
  • 자바로 동시성 프로그램을 작성하기가 점점 쉬워지고는 있지만, 이를 올바르고 빠르고 작성하는 일은 여전히 어려운 작업임
  • 동시성 프로그래밍을 할 때에는 안정성(safety), 응답 가능(liveness) 상태를 유지하기 위해 애써야하는데, 병렬 스트림 파이프라인 프로그래밍에서도 다를 바 없음
  • 환경이 아무리 좋더라도 데이터 소스가 Stream.iterate거나 중간 연산으로 limit를 쓰면 파이프라인 병렬화로는 성능 개선을 기대할 수 없음
  • 대체로 스트림의 소스가 ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스이거나 배열, int 범위, long 범위일 때 벙렬화의 효과가 가장 좋음
    • 이 자료구조들은 모두 데이터를 원하는 크기로 정확하고 손쉽게 나눌 수 있기 때문
  • 스트림 파이프라인의 종단 연산의 동작 방식 역시 병렬 수행 효율에 영향을 줌
    • 중단 연산 중 병렬화에 가장 적합한 것은 축소(reduction)
      • Stream의 reduce 메소드 중의 하나, 혹은 min, max, count, sum
    • anyMatch, allMatch, noneMatch처럼 조건에 맞으면 바로 반환되는 메서드도 적합함
    • 가변 축소 연산을 수행하는 Stream의 collect 메서드는 병렬화에 적합하지 않음 (컬렉션들을 합치는 부담이 큼)
  • 스트림을 잘못 병렬화하면 성능이 나빠질 뿐만아니라 결과 자체가 잘못되거나 예상 못한 동작 발생 가능성도 있음
  • 스트림 병렬화는 오직 성능 최적화 수단임을 기억해야함
    • 다른 최적화와 마찬가지로 변경 전후로 반드시 성능을 테스트하여 병렬화 사용 가치가 있는지 확인 필요
  • 조건이 잘 갖춰지면 parallel 메서드 호출하나로 거의 프로세서 코어 수에 비례하는 성능 향샹 만끽 가능함

8장 메서드

아이템 49 – 매개변수가 유효한지 검사하라

  • 메서드 몸체가 실행되기 전에 매개변수를 확인하면 잘못된 값이 넘어왔을 때 즉각적이고 깔끔한 방식으로 예외를 던질 수 있다.
  • 자바 7에 추가된 java.util.requireNonNull 메서드는 유연하고 사용하기도 편하니, 더 이상 null 검사를 수동으로 하지 않아도 된다.
  • 메서드나 생성자를 작성할 때 그 매개변수들에 어떤 제약이 있을지 생각해야 하고 그 제약들은 문서화하고 코드 시작 부분에서 명시적으로 검사해야 한다.

아이템 50 – 적시에 방어적 복사본을 만들라

  • 클라이언트가 여러분의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.
  • 가변 필드를 생성자 매개변수로 받아 클래스 필드에 저장하는 경우 각각을 방어적으로 복사해야 한다.
  • Date는 낡은 API이니 새로운 코드를 작성할 때는 더 이상 사용하면 안된다.
  • 매개변수의 유효성 검사를 하기 전에 방어적 복사본을 만들고 이 복사본으로 유효성을 검사해야 한다.
  • 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.
  • 가변 필드를 반환하는 get 접근자 메서드에서는 가변 필드의 방어적 복사본을 반환하도록 해야 한다.

아이템 51 – 메서드 시그니처를 신중히 설계하라

메서드 이름을 신중히 짓자

이해할 수 있고 같은 패키지에 속한 다른 이름들과 일관되게 짓는 게 최우선 목표다.

편의 메서드를 너무 많이 만들지 말자

메서드가 너무 많으면 학습, 문서화, 테스트, 유지보수하기 어렵다.

매개변수 목록은 짧게 유지하자. 4개 이하가 좋다.
  • 과하게 긴 매개변수 목록을 짧게 줄여주는 기술 세 가지
    • 여러 메서드로 쪼갠다. (직교성을 높인다)
      • 예) List 인터페이스에서 지정된 부분 범위의 부분리스트에서 인덱스를 찾는 메서드를 별도로 제공하지 않고 subList, indexOf를 제공한 것
    • 매개변수 여러 개를 묶어주는 도우미 클래스를 만든다.
      • 예) class Point { int x; int y; }
    • 객체 생성에 사용한 빌더 패턴을 메서드 호출에 응용한다.
  • 매개변수 타입으로는 클래스보다는 인터페이스가 더 낫다.
  • boolean보다는 원소 2개짜리 열거 타입이 낫다.

아이템 52 – 다중정의는 신중히 사용하라

  • 다중정의(overloading)은 어느 메서드를 호출할지가 컴파일타임에 정해진다. 재정의(override)한 메서드는 동적으로 선택되고, 다중정의한 메서드는 정적으로 선택된다.
  • API 사용자가 매개변수를 넘기면서 어떤 다중정의 메서드가 호출될지를 모른다면 프로그램이 오동작하기 쉽다. 다중정의가 혼동을 일으키는 상황을 피해야 한다.
  • 안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말자.
    • 다중정의하는 대신 메서드 일므을 다르게 지어주는 방법이 있다.
  • 다중정의된 메서드들이 함수형 인터페이스를 인수로 받을 때 비록 서로 다른 함수형 인터페이스라도 인수 위치가 같으면 혼란이 생긴다. 따라서 메서드를 다중정의할 때 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다.
  • 프로그래밍 언어가 다중정의를 허용한다고 해서 다중정의를 꼭 활용하란 뜻은 아니다.

아이템 53 – 가변인수는 신중히 사용하라

<인수가 1개 이상이어야하는 가변인수 메서드 – 잘못 구현한 예!>

static int min(int... args) {
    if (args.length == 0) {
        throw new IllegalArgumentException("인수가 1개 이상 필요합니다.");
    }
    int min = args[0];
    for (int i = 1; i < args.length; i++) {
        if (args[i] < min)
            min = args[i];
    return min;
    }
}
  • 가장 심각한 문제는 인수를 0개만 넣어 호출하면 런타임에 실패한다는 점. (코드도 지저분함) args 유효성 검사를 명시적으로 해야한다.

<인수가 1개 이상이어야할 때 가변인수를 제대로 사용하는 방법>

static int min(int firstArg, int... remainingArgs) {
    int min = firstArgs;
    for (int arg : remainingArgs)
        if (arg < min)
            min = arg;
    return min;
}
성능에 민감한 경우
  • 가변인수 메서드는 호출될 때마다 배열을 새로 하나 할당하고 초기화하여 성능이 민감한 경우 문제가 될 수 있다.
  • 가변인수의 유연성이 필요할 때 선택할 수 있는 패턴public void foo() { } public void foo(int a1) { } public void foo(int a1, int a2) { } public void foo(int a1, int a2, int a3) { } public void foo(int a1, int a2, int a3, int... rest) { }

아이템 54 – null이 아닌, 빈 컬렉션이나 배열을 반환하라

  • 컬렉션이나 배열 같은 컨테이너가 비었을 때 null을 반환하는 메서드를 사용할 때면 항시 방어 코드를 넣어줘야 한다.
  • 빈 컨테이너를 할당하는 데도 비용이 드니 null을 반환하는게 성능상 낫다는 주장도 있으나 틀린 주장이다.
    • 이 정도의 성능 차이는 신경 쓸 수준이 못 된다.
    • 빈 컬렉션과 배열을 굳이 새로 할당하지 않고도 반환할 수 있다.
  • 빈 컬렉션 할당이 성능을 눈에 띄게 떨어뜨리는 방법
    • Collections.emptyList, Collections.emptySet, Collections.emptyMap
  • 배열을 쓸 대도 절대 null을 반환하지 말고 길이가 0인 배열을 반환하라.

아이템 55 – 옵셔널 반환은 신중히 하라

옵셔널의 장점

옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류 가능성이 작다.

옵셔널 사용 시 주의점
  • 옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자, 옵셔널을 도입한 취지를 완전히 무시하는 행위다.
  • 반환값으로 옵셔널을 사용한다고 무조건 득이 되는 건 아니다. 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안된다.
옵셔널을 메서드 반환 타입으로 사용해야 하는 경우

결과가 없을 수 있으며 클라이언트가 이 상황을 특별하게 처리해야 하는 경우

  • 박싱된 기본 타입을 담은 옵셔널을 반환하지 말고 int, long, double 전용 옵셔널 클래스인 OptionalInt, OptionalLong, OptionalDouble을 사용하자.
  • 옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는게 적절한 상황은 거의 없다.

아이템 56 – 공개된 API 요소에는 항상 문서화 주석을 작성하라

  • API를 쓸모 있게 하려면 잘 작성된 문서도 곁들여야 한다.
  • API를 올바로 문서화하려면 공개된 모든 클래스, 인터페이스, 메서드, 필드 선언에 문서화 주석을 달아야 한다.
  • 메서드용 문서화 주석에는 해당 메서드와 클라이언트 사이의 규약을 명료하게 기술해야 한다.
    • how가 아닌 what을 기술해야 한다.
  • 자기사용 패턴은 자바 8에 추가된 @impleSpec 태그로 문서화한다.
  • 열거 타입을 문서화할 때는 상수들에도 주석을 달아야 한다.
  • 애너테이션 타입을 문서화할 때는 멤버들에도 모두 주석을 달아야 한다.
  • 클래스 혹은 정적 메서드가 스레드 안전하든 아니든, 스레드 안전 수준을 반드시 API 설명에 포함해야 한다.

아이템57. 지역변수의 범위를 최소화하라

지역변수의 유효 범위를 최소로 줄이면 코드 가독성과 유지 보수성은 높아지고 오류 가능성은 낮아진다. 아래는 지역변수의 범위를 좁히는 방법들이다.

57-1. 지역변수는 사용할 때 선언 및 할당해라

지역변수의 범위를 줄이는 가장 좋은 방법은 처음 쓸 때 선언과 동시에 초기화하는 것이다. 맨 위에서 미리 선언부터 잔뜩 해두면 가독성이 떨어진다. 또 사용시점엔 초기 값이 기억이 안날 수도 있다. 여기서 try-catch 문은 예외이다. 초기화하다가 예외를 던질수 있다면, try 바로 위에 선언하고, try 블록 안에서 초기화해주어야 한다.

57-2. while문보다는 for을 사용하라

반복 변수의 값을 반복문이 종료된 뒤에도 써야하는 상황이 아니라면 while 문보다는 for 문을 쓰는 편이 낫다. while문은 반복변수를 반복문 바깥 블록에 선언해야하기 때문이다.

  • 컬렉션 순회 관용구
for (Element e : c) {
    ... // e로 무언가 수행
}
  • 반복자 사용 관용구
for (Iterator<Element> i = c.iterator(); i.hasNext();) {
    Element e = i.next();
    ... // e로 무언가 수행
}
57-3. 메소드를 작게 만들어라

메소드를 애초에 작게 유지하고 한가지 기능에 집중하게 만들어라

아이템58. 전통적인 for문보다는 foreach문을 사용하여라

향상된 for문(foreach)을 사용하면 반복자와 인덱스 변수를 사용하지 않으니 코드가 깔끔해지고 오류가 날 일도 사라진다. 또한 컬렉션과 배열을 모두 처리할 수 있다. (Iterable 인터페이스를 구현한다면 뭐든 순회할 수 있다)

아래는 for문으로 원소를 제거하려고 시도한 예제이다. 컴파일도 잘되고 동작도 하지만 엉뚱한 결과를 출력한다.

// 메인메소드
ArrayList<String> list = new ArrayList<>();
list.addAll(Arrays.asList("a", "b", "c", "d"));
System.out.println(list); // [a, b, c, d]

for (int i = 0; i < list.size(); i++) { // list의 원소들이 제거되면서 2번만 순회한다
    list.remove(i);
    System.out.println(list); // 처음엔 [b, c, d] , 이후에 [b, d] 출력
}
58-1. foreach문을 사용할 수 없는 경우
  1. 파괴적인 필터링(deftructive filtering) : 컬렉션을 순회하면서 선택된 원소를 제거하려면 반복자의 remove를 사용해야 하므로 foreach를 쓸 수 없다. (ConcurrentModificationException 발생)
  2. 변형(transforming) : 리스트나 배열을 순회하면서 그 원소의 값 일부 혹은 전체를 변경해야한다면 반복자나 인덱스를 사용해야 하므로 foreach를 쓸 수 없다.
  3. 병렬 반복(parallel iteration) : 여러 컬렉션을 병렬로 순회해야 한다면 각각의 반복자와 인덱스 변수를 사용해야 하므로 foreach를 쓸 수 없다.

아이템59. 라이브러리를 익히고 사용하라

아주 특별하거나 해당 프로젝트에서만 쓰이는 기능이 아니라면 누군가 이미 라이브러리 형태로 구현해놓았을 가능성이 크다. 아래는 라이브러리를 사용하면 얻는 이점들이다.

59-1. 표준 라이브러리를 사용하면 좋은 이유
  1. 표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과 이것을 사용한 다른 개발자의 경험, 노하우 문서까지 활용할 수 있다. (책에서 예제로 든 Random보다는 jdk 1.7부터 나온 ThreadLocalRandom을 추천, Random은 멀티 스레드에서 사용할 경우 Seed 값이 겹치는 경우가 발생할 수 있으나, ThreadLocalRandom은 안전하다.)
  2. 핵심 비즈니스 로직 외에 들이는 시간이 줄어든다.
  3. 따로 노력하지 않아도 성능이 지속적으로 개선되며 기능이 추가된다. 라이브러리 개발자들이 계속 노력하기 때문이다.
  4. 코드가 많은 사람들에게 낯익은 코드가 되며 유지보수성과 재활용성이 좋아진다.

라이브러리가 너무 방대하여 모든 API를 아는 것은 힘들지만 적어도 아래 패키지들은 잘 알면 꽤 유용하다.

  1. java.lang
  2. java.io
  3. java.util : 컬렉션, 스트림, 동시성
59-2. 서드파티 라이브러리

원하는 기능이 없다면 서드파티 라이브러리들을 사용해보자.

  1. apache commons : 자바관련 공통 컴포넌트 개발, StringUtils를 한번도 안써본 사람은 있어도 한번만 쓴 사람은 없다..
  2. google guava : 컬렉션, 캐싱, 문자열 처리 등 구글에서 만든 자바 라이브러리들
  3. jackson
  4. jsoup
  5. 많지만 라이브러리 소개 코너가 아니므로 생략..

아이템60. 정확한 답이 필요하다면 float와 double은 피하라

float와 double 타입은 과학과 공학 계산용으로 설계되었다. 이진 부동소수점 연산에 쓰이며 넓은 범위의 수를 빠르게 정밀한 근사치로 계산하도록 세심하게 설계되었다. 따라서 정확한 결과가 필요할 때는 사용하면 안된다. (ex. 금융) 0.1 혹은 10의 음의 거듭 제곱 수(0.01, 0.001 등)를 표현할 수 없기 때문이다. (ex. 1.03 – 0.42 = 0.6100000000000001)

대안으로는 2가지 방법이 있다. BigDecimal을 사용하거나 int와 long을 쓰되 소수점을 직접 관리해주는 방법이다.

  1. BigDecimal은 잘 계산되지만 원시 타입보다 쓰기 불편하고 훨씬 느리다.
  2. int와 long은 쉽게 사용할 수 있지만 자릿수를 다시 맞춰주는 등의 작업이 필요하다.

따라서 아홉 자리 십진수로 표현할 수 있다면 int를 사용하고, 열여덟 자리 십진수로 표현할 수 있다면 long 을 쓰자. 이것도 넘어간다면 BigDecimal 을 쓰면 된다.

아이템61. 박싱된 기본 타입보다는 기본 타입을 사용하라

아래는 원시타입과 박싱된 원시타입의 주된 차이이다.

  1. 원시타입은 값만 가지고 있으나, 박싱된 원시타입은 값과 식별성까지 갖고 있다. 즉, 박싱된 원시타입은 값이 같아도 다르다고 식별될 수 있다.
  2. 원시타입의 값은 언제나 유효하지만 박싱된 원시타입은 null을 가질 수 있다.
  3. 원시타입이 박싱된 원시타입보다 시간과 메모리 사용면에서 더 효율적이다.
61-1. 박싱된 원시타입의 문제

다음 비교자는 박싱된 원시타입에서 발생할 수 있는 첫번째 문제이다.

Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1)

// 메인메소드
naturalOrder.compare(new Integer(82), new Integer(82)); // 기대값 0, 실제값 1

앞에서 i < j 는 잘 작동하였으나, 뒤에서 i == j 이 값의 동등을 비교하지 않고 인스턴스 동치성을 판단한 것이다. 따라서 true 대신 false 가 반환되면서 오동작하였다. 즉, 박싱된 기본 타입에 == 연산자를 사용하면 오류가 발생한다.

아래는 두번째 문제이다.

Integer i;
if (i == 82) {
    ...
}

이 코드는 NullPointerException을 던진다. 원시타입과 박싱된 원시타입을 혼용한 연산에서는 박싱된 원시타입의 박싱이 자동으로 풀리기 때문이다. null을 언박싱했기 때문에 예외가 발생했다.

61-2. 박싱된 원시타입을 사용해야 할 때
  1. 타입 매개변수를 사용 : 제네릭에서 보았듯이 타입 매개변수에는 원시타입을 쓸 수 없다. 예를들면 컬렉션은 원시타입을 담을 수 없으므로 원소, 키, 값으로 박싱된 원시타입을 써야한다.
  2. 리플렉션을 통해 메소드를 호출할 때는 박싱된 원시타입을 써야한다.

아이템62. 다른 타입이 적절하다면 문자열 사용을 피하라

문자열은 텍스트를 표현하도록 설계되었다. 그런데 문자열은 원래 의도하지 않은 용도로도 쓰이는 경향이 있다. 문자열은 다른 값 타입을 대신하기에 적합하지 않다.

  1. 문자열은 값 자료형(value type)을 대신하기에 적합하지 않다. 수치형이라면 int, BigInteger 등을 쓰자.
  2. 문자열은 열거 타입을 대신하기에 적합하지 않다. (아이템34)
  3. 문자열은 혼합 타입(aggregate type)을 대신하기에 적합하지 않다. 여러 요소가 혼합된 데이터를 하나의 문자열로 표현하는 것은 대체로 좋지 않다. 구분자가 겹치면 escape 처리 및 파싱해서 써야하고 오류가능성도 커진다. 차라리 전용 클래스를 만드는 편이 낫다.
  4. 문자열은 권한(capability)을 표현하기엔 적합하지 않다. 문자열을 사용해 기능 접근 권한을 표현하는 것은 어렵다.

아이템63. 문자열 연결은 느리니 주의하라

문자열 연결 연산자(+)는 여러 문자열을 하나로 합쳐주는 편리한 방법이다. 그러나 이 연산자를 많이 사용하면 성능저하가 생길 수 있다. 문자열 연결 연산자로 문자열 n개를 잇는 시간은 n의 제곱에 비례한다. 문자열은 불변이므로 양쪽 내용 모두를 복사하므로 생기는 결과이다.

대표적인 예제는 반복문 내 result += str 같은 예제일 것이다. 성능을 포기하고 싶지 않다면 String 대신 StringBuilder 또는 StringBuffer를 사용하자.

63-1. StringBuilder 와 StringBuffer 의 차이

StringBuffer는 멀티스레드 환경을 염두에 두고 설계되었기 때문에, 메소드들마다 synchronized 가 걸려있다. 그러나 StringBuilder는 없다. 따라서 멀티스레드 환경에서 thread-safe 하게 쓰고 싶다면 StringBuffer 를 쓰고, 싱글스레드라면 StringBuilder를 쓰는게 유리하다.

63-2. 컴파일러의 문자열 연결 최적화

jdk 1.5부터는 문자열 연결 연산자(+)를 자동으로 최적화해준다. 아래의 예제를 보자

String plusOpStr1 = "a" + "b" + "c"; // 컴파일 시 String plusOpStr1 = "abc"; 로 바뀐다
String plusOpStr2 = "x" + plusOpStr1 + "y"; // 컴파일 시 String plusOpStr2 = (new StringBuilder("x")).append(plusOpStr1).append("y").toString(); 로 바뀐다
String plusOpStr3 = "";
for (int i = 0; i < 100; i++) {
    plusOpStr3 += "f"; // 컴파일 시 plusOpStr3 = (new StringBuilder(String.valueOf(plusOpStr3))).append(f).toString(); 로 바뀐다
}

단순한 상수 문자열 연결은 컴파일러가 이미 알아서 합쳐주었고, 가변변수는 StringBuilder의 append, toString으로 변하였다. 이를 활용해서 가독성있게 긴 문자열을 여러 줄로 나눠서 쓸 수도 있겠다. 다만 반복문에서는 append만 하는 것이 아니라, StringBuilder 를 계속 new 해서 쓰고 있기 때문에 명시적으로 써주는 게 성능에 좋을 것 같다.

아이템64. 객체는 인터페이스를 사용해 참조하라

아이템51에서 매개변수 타입으로 클래스가 아니라 인터페이스를 사용하라고 권장했다. 이것은 객체는 클래스가 아닌 인터페이스를 참조하라고 확장할 수 있다. 적합한 인터페이스만 있다면 매개변수 뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하라. 객체의 실제 클래스를 사용해야 할 상황은 오직 생성자로 생성할 때 뿐이다.

인터페이스를 타입으로 사용하는 습관을 길러두면 프로그램이 훨씬 유연해질 것이다. (ex. List<String> nameList = new ArrayList<>();) 나중에 구현 클래스를 교체하고자 한다면 새 클래스의 생성자(또는 정적 팩토리)를 호출해주기만 하면 된다. (ex. List<String> nameList = new LinkedList<>();)

만약 적합한 인터페이스가 없다면 당연히 클래스로 참조해야 한다. 이 경우 보통 값 타입(String, Integer 등)에 해당된다. 또한 PriorityQueue 와 같이 Queue 인터페이스에 없는 새로운 메소드를 제공한다면 직접 참조해야한다.

64-1. 인터페이스화에 대한 개인적인 고찰

OCP(개방 폐쇄 원칙), DIP(의존 역전 원칙) 등에서 한결같이 하는 말이 구체클래스보단 인터페이스에 의존함으로써 해결하자는 것이다. 이 책의 저자 역시 이번 아이템에서 인터페이스를 최대한 쓸 것을 언급하고 있다. 이해는 가지만 서비스개발에서도 이것을 ‘언제’ 지켜야하는 지는 논쟁이 될 수 있다.

Service – ServiceImpl 구조 : Spring 의 여러 교과서들이 말하고 있는 구조이다. 그러나 비즈니스 로직을 처리하는 서비스가 구현 클래스를 2개 이상 가질 일이 거의 없어도 꼭 인터페이스를 만들고 의존해야할까? 일단 인터페이스없이 만들고 새로운 구현체가 추가되는 시점에 인터페이스를 만들자는 의견도 있다.

DAO의 경우 DB에 의존하지 않고 테스트하기 위해 인터페이스를 만들고 MockDAO로 교체하여 테스트할 때 쓴다. 라고 말할 수도 있다. (mock을 직접 만드는 것 보단 mockito 추천!)

그러나 다른 의견도 있다. 구현 클래스는 캡슐내부이며 보이지 않는 게 좋다. 외부와의 약속만을 표기한 인터페이스는 그 역할을 언어레벨에서 해낸다. 인터페이스는 관심의 경계를 나누도록 도와주는 장치이며 프로토콜을 정하는 규약이다.

아이템65. 리플렉션보다는 인터페이스를 사용하라

리플렉션 기능을 이용하면 프로그램에서 임의의 클래스에 접근할 수 있다. Class 객체가 주어지면 그 클래스의 Constructor, Method, Field 인스턴스를 가져올 수 있고, 이 인스턴스 내의 각종 데이터(메소드 시그니처, 필드명 등)도 가져올 수 있다.

Method.invoke()를 사용하면 어떤 클래스의 어떤 객체가 가진 어떤 메소드라도 호출할 수 있다. 리플렉션을 사용하면 컴파일 당시에 존재하지 않던 클래스도 이용할 수 있다. 그러나 아래와 같은 단점이 있다.

  1. 컴파일타임 타입 검사가 주는 이점을 누릴 수 없다. 예외 검사도 마찬가지이며, 예외발생 시 런타임 오류가 발생한다.
  2. 리플렉션을 이용하면 코드가 지저분해지고 장황해진다.
  3. 성능이 떨어진다.

따라서 리플렉션은 아주 제한된 형태로만 사용하며 단점을 피하고 이점만 취해야 한다. 리플렉션은 인스턴스 생성에만 쓰고, 리플렉션으로 만든 인스턴스는 인터페이스나 상위 클래스로 참조해서 사용하자.

아래 코드는 실행인자로 java.util.TreeSet a b c d 를 넘기면 [a, b, c, d] 를 출력하는 코드이다. 인터페이스를 참조하도록 하여서 컴파일타임의 검사를 조금이나마 이용하였다.

public static void main(String[] args) {
    Class<? extends Set<String>> cl = null;
    Set<String> s = null;
    try {
        cl = (Class<? extends Set<String>>) Class.forName(args[0]);
        Constructor<? extends Set<String>> cons = cl.getDeclaredConstructor();
        s = cons.newInstance();
    } catch (ReflectiveOperationException e) {
         System.err.println("리플렉션 도중 에러 발생");
         System.exit(1);
    }
    s.addAll(Arrays.asList(args).subList(1, args.length));
    System.out.println(s);
}

위 코드는 런타입에 ClassNotFoundException, NoSuchMethodException, InvocationTargetException 등 6가지를 던질 수 있었으나 jdk 1.7 부터 지원되는 ReflectiveOperationException 를 통해 그나마 간소화하였다. 그럼에도 불구하고 리플렉션이 아니었다면 생성자 호출 한 줄로 끝났을 것이다.

아이템66. 네이티브 메소드는 신중히 사용하라

자바 네이티브 인터페이스(Java Natvie Interface, JNI)는 자바에서 native 메소드(C, C++ 등으로 작성)를 호출하는 기술이다. natvie 메소드의 쓰임은 다음과 같다.

  1. 레지스트리 같은 플랫폼 특화 기능을 사용
  2. native 코드로 작성된 기본 라이브러리를 사용
  3. 성능 개선을 목적으로 성능에 결정적인 영향을 주는 영역만 따로 natvie 언어로 작성

그러나 java가 계속 발전해가면서 native 메소드를 사용할 일이 점차 사라지고 있다. (ex. java 9부터는 process API가 강화됨) native 메소드는 메모리 훼손으로부터 안전하지 않으며 디버깅도 어렵다. GC는 natvie 메모리를 자동 회수하지 못하기 때문이다. 또한 java 코드와 native 메소드 사이에 바인딩 역시 코드작성을 해야해서 비용이 들어간다.

아이템67. 최적화는 신중히 하라

성능때문에 견고한 구조를 희생하지 말아야 한다. 빠른 프로그램보다는 좋은 프로그램이 좋다. 애초에 설계 단계에서 성능을 반드시 염두에 두어야 한다. 아키텍처의 결함이 성능을 제한하는 상황이라면 전체를 다시 작성하지 않고는 해결이 불가능하기 때문이다.

  1. 성능을 제한하는 설계를 피하라. 완성 후 변경하기 어려운 설계 요소는 외부 컴포넌트, 시스템과의 소통 방식이다. (API, 프로토콜, 파일데이터 등)
  2. API를 설계할 때 성능에 주는 영향을 고려하라. public 메소드에서 내부 데이터를 변경할 수 있게 만들면 불필요한 방어적 복사를 유발한다. 또한 컴포지션으로 해결할 수 있는 public클래스를 상속으로 처리한다면 영원히 상위 클래스에 종속되고 성능까지 물려받는다.
  3. 각각의 최적화 시도 전후로 성능을 측정하라. 프로파일링 도구는 최적화 노력을 어디에 집중해야할지 알려준다.

아이템68. 일반적으로 통용되는 명명 규칙을 따르라

자바의 명명규칙은 크게 철자와 문법으로 나뉜다.

68-1. 철자규칙

철자규칙은 패키지, 클래스, 인터페이스, 메소드, 필드, 타입변수의 이름을 다룬다.

  • 패키지와 모듈명은 각 요소를 점(.)으로 이으며 계층적으로 짓는다. 모두 소문자 혹은 숫자로 지어야한다. 도메인이 있다면 역순으로 사용한다.
  • 클래스와 인터페이스 이름은 대문자로 시작하며 줄여쓰지 않도록 한다. 약자의 경우라도 첫글자만 대문자로 하는 것을 권장한다.
  • 메소드와 필드명은 첫글자를 소문자로 쓴다. 단, 상수필드는 모두 대문자로 쓰며 단어 사이는 밑줄(_)로 구분한다.
  • 타입 매개변수명은 한 문자로 표현한다. 일반적으로 아래와 같이 쓴다.
    • T : 임의의 타입. Type의 약자
    • E : 컬렉션의 원소. Element의 약자
    • K, V : Map의 키와 값. Key와 Value의 약자
    • X : 예외. Exception의 약자
    • R : 메소드의 반환 타입. Return의 약자
68-2. 문법규칙

문법규칙은 논쟁의 소지가 있을 수 있으며, 유연하다.

  • 객체를 생성할 수 있는 클래스명은 보통 명사, 명사구를 사용한다. (ex. Thread)
  • 객체를 생성할 수 없는 클래스명은 보통 복수형 명사로 짓거나 형용사로 짓는다.
  • 메소드명은 동사, 동사구로 짓는다.
  • 해당 인스턴스의 속성을 반환하는 메소드는 명사 또는 get으로 시작하는 동사구로 짓는다. (boolean 제외)
  • 객체의 타입을 바꿔서 다른 타입의 또 다른 객체를 반환하는 메소드는 보통 to타입 형태로 짓는다. (toString, toArray, toPath 등)
  • 객체의 내용을 다른 뷰로 보여주는 메소드는 as타입 형태로 짓는다. (asList 등)
  • 객체의 값을 기본타입 값으로 반환하는 메소드는 타입Value 형태로 짓는다. (intValue 등)