[Effective Java] 8장 메서드 (1장)

정리
아이팀 49. 메서드나 생성자를 작성할 때면 그 매개변수들에 어떤 제약이 있을지 생각해야한다. 그 제약들을 문서화하고 메서드 코드 시작 부분에서 명시적으로 검사해야한다.

아이팀 50. 클래스가 클라이언트로부터 받은 혹은 클라이언트로 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적으로 복사해야한다. 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면 방어적 복사를 수행하는 대신 해당 구성요소를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시하도록 하자

아이팀 52. 프로그래밍 언어가 다중정의를 허용한다고 해서 다중정의를 꼭 써야하는 것은 아니다. 일반적으로 매개변수의 수가 같을 때는 다중정의를 피하는 게 좋다. 만약 어쩔수없이 다중정의를 해야한다면 매개변수의 형변환하여 정확한 다중정의 메서드가 선택되도록 해야 한다. 이것이 불가능하다면 기존의 클래스를 수정해 새로운 인터페이스를 구현해야 하는데, 같은 객체를 입력받는 다중정의 메서드들이 모두 동일하게 동작하도록 만들어야한다.


내용
아이템 49. 매개변수가 유효한지 검사하라
● 메서드와 생성자 대부분은 입력 매개변수의 값이 특정 조건을 만족하기를 바란다. 이런 특정 조건에 대한 내용을 반드시 문서화해야 하며 메서드 몸체가 시작되기 전에 검사해야한다.

● 오류는 가능한 빨리 잡아야한다. 오류를 발생한 즉시 잡지 못하면 해당 오류를 감지하기 어려워지고, 감지하더라도 오류의 발생 지점을 찾기 어려워지기 때문이다.

● 매개변수 검사를 제대로 하지 못하면, 메서드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다. 혹은 메서드가 잘 수행되지만 잘못된 결과를 반환하거나 이 메서드와 관련없는 에러를 낼 수 있다.
즉, 실패 원자성을 어기는 결과가 나올 수 있다.

● public 과 protected 메서드는 매개변수 값이 잘못됐을 때, 던지는 예외를 문서화해야 한다. 매개변수의 제약을 문서화한다면 그 제약을 어겼을 때 발생하는 예외도 함께 기술해야 한다.

● null 검사를 할 경우 자바7에 추가된 java.util.Object.requireNonNull 메서드는 유연하고 사용하기 편하니, 더이상 null 검사를 수동으로 하지 않아도 된다.

● 공개되지 않은 메서드라면 메서드가 호출되는 상황을 통제할 수 있다. 유효한 값만이 메서드에 넘겨지리라는 것을 보증해야한다. public이 아닌 메서드라면 assert(단언문)을 사용해 매개변수 유효성을 검증할 수 있다.

● assert(단언문)은 몇 가지 면에서 일반적인 유효성 검사와 다르다.
1. 실패하면 AssertionError를 던진다.
2. 런타임에는 아무런 성능 저하도 없다. 단, java를 실행할 때 명령줄에서 -ea 혹은 --enableassertions 플래그를 설정하면 런타임에 영향을 준다.

● 메서드가 직접 사용하지는 않으나 나중에 쓰기 위해 저장하는 매개변수(생성자인 경우가 많다.)는 특히 더 신경 써서 검사 해야한다. 이유는 다른 메서드에서 에러를 낼 때 원인을 추적하기가 어려워 디버깅하기 어려워진다. 생성자 매개변수의 유효성 검사는 클래스 불변식을 어기는 객체가 만들어지지 않게 하는 데 꼭 필요하다.

● 유효성 검사 비용이 지나치게 높거나 실용적이지 않을 때, 혹인 계산 과정에서 암묵적으로 검사가 수행될때는 예외이다.


아이템 50. 적시에 방어적 복사본을 만들라
● 자바는 네이티브 메서드를 사용하지 않기 때문에 C, C++ 같이 안전하지 않은 언어에서 흔히 보는 버퍼 오버런, 배열 오버런, 와일드 포인터 같은 메모리 충돌 오류에서 안전하다.
그래서 자바로 작성한 클래스는 시스템의 다른부분에서 바꿀 수 없기 때문에 불변식이 지켜진다.

● 하지만 불변식을 깨려는 행위가 있을 수 있기 때문에 방어적으로 프로그래밍해야 한다.
인스턴스 내부를 공격하는 예
Date start = new Data();
Date start = new Data();
Period p = new Period(start, end); // Period 의 Date는 모두 private final로 선언이 되어 있다.
end.setYear(78); // p의 내부를 수정했다. 불변식이 깨진다.

public final class Period {
    private final Date start;
    private final Date end;

   public Period(Date start, Date end) {
       if(start.compateTo(end) > 0) throw new IllegalArgumentException (start + "가 " + end + "보다 늦다.");
       thist.star = start;
       this.end = end;
   }
... // 이하 생략
}

● 자바 8 이후에는 Date 대신 불변인 Instant를 사용하면 된다. Date는 낡은 API이니 새로운 코드를 작성할 때는 더 이상 사용하면 안된다. 만약 사용했을 경우 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사(defensive copy)를 해야한다.
매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한다.
방어적으로 복사하는 예
public final class Period {
    private final Date start;
    private final Date end;

   public Period(Date start, Date end) {
       thist.star = new Date(start.getTime());
       this.end = new Date (end.getTime());
       if(this.start.compateTo(this.end) > 0) throw new IllegalArgumentException (start + "가 " + end + "보다 늦다.");
   }
... // 이하 생략
}

● 매개변수가 제 3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안된다. 이유는 Date는 final이 아니므로 clone이 Date가 정의한 게 아닐 수  있다. 즉 clone이 아예 다른 하위 클래스의 인스턴스를 반환할 수 있다.
인스턴스 내부를 공격하는 예
Date start = new Data();
Date start = new Data();
Period p = new Period(start, end); // Period 의 Date는 모두 private final로 선언이 되어 있다.
p.end().setYear(78); // p의 내부를 수정했다. 불변식이 깨진다.

public final class Period {
      private final Date start;
      private final Date end;

    public Date start(){
       return start;
    }
    public Date end(){
       return end;
    }
... // 이하 생략
}
● 가변 필드의 방어적 복사본을 반환하면 인스턴스 공격을 막을 수 있다.
public Date start(){
   return new Date(start.getTime());
}
public Date end(){
   return new Date(end.getTime());
}
● 접근자 메서드는 생성자와 달리 방어적 복사에 clone을 사용해도 된다. 그렇더라도 인스턴스를 복사하는데 일반저으로 생성자나 정적 팩터리를 쓰는게 좋다.

● 클래스가 불변이든 가변이든, 가변인 내부 객체를 클라이언트에 반환할 대는 반드시 심사숙고를 해야한다. 길이가 1 이상인 배열은 무조건 가변임을 잊지말자.
그래서 내부에서 사용하는 배열을 클라이언트에 반환할 때는 항상 방어적 복사를 수행해야한다. (혹은 방어적 복사를 수행해야 한다)

● 방어적 복사에는 성능 저하게 따르고, 또 항상 쓸 수 있는 것이 아니기 때문에 호출자가 컴포넌트 내부를 수정하지 않으리란 확신하면 방어적 복사를 생략할 수 있다.
이 때, 호출자에서 해당매개변수나 반환값을 수정하지 말아햐 함을 명확히 문서화하는 게 좋다.

● 매서드나 생성자의 매개변수로 넘기는 행위가 그 객체의 통제권을 명백히 이전함을 뜻한다. 이처럼 통제권을 이전하는 메서드를 호출하는 클라이언트는 해당 객체를 더 이상 직접 수정하는 일이 없다고 문서에 남겨야한다.

● 통제권을 넘겨받기호 한 메서드나 생성자를 가진 클래스들은 공격에 취약하다. 따라서 방어적 복사를 생략해도 되는 상황은 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때로 한정해야한다. (예로는 래퍼 클라스 패턴이 있음)

아이템 51. 메서드 시그니처를 신중히 설계하라
● 메서드 이름을 신중히 짓자. 항상 표준 명명 규칙을 따라야한다. 이해할 수 있고, 같은 패키지에 속한 다른 이름들과 일관되게 짓는 게 최우선 목표다. 긴 이름은 피하고 애매하면 자바 라이브러리의 API가이드를 참조하는 것이 좋다.

● 편의 메서드를 너무 많이 만들지 말자. 모든 메서드는 각각 자신의 소임을 다해야한다. 메서드가 너무 많은 클래스는 익히고, 사용하고, 문서화하고, 테스트하고, 유지보수 하기 힘들다. 인터페이스도 마찬가지이다. 아주 자주 사용할 것 같은 메서드 이외에는 만들지 않는 것이 좋다.

● 매개변수 목록은 짧게 유지하자. 4개 이하가 좋다. 매개변수 전부를 기억하는 것이 어렵기 때문이다. 특히 같은 타입의 매개변수 여러 개가 연달아 나오는 경우가 특히 안 좋다. 실수로 바뀌 입력해도 그대로 컴파일이되고 실행이 되기 때문이다.

● 과하게 긴 매개변수 목록을 짧게 줄여주는 기술 3가지가 있다.
1. 여러 메서드로 쪼갠다. 조개진 메서드 각각은 원래 매개변수 목록의 부분집합을 받는다. 잘못하면 메서드가 너무 많아질 수 있지만 직교성을 높여 오히려 줄여주는 효과도 있다.
2. 매개변수 여러 개를 묶어주는 도우미 클래스를 만드는 것이다. 일반적으로 이런 도우미 클래스는 정적 멤버 클래스로 둔다. 특히 잇따른 매개변수 몇 개를 독립된 하나의 개념으로 볼 때 추천하는 방법이다.
3. 앞서의 두 기법을 혼합한 것으로 객체 생성에 사용한 빌더 패턴을 메서드 호출에 응용한다고 보면 된다. 이 기법은 매개변수가 많을 때, 특히 그중 일부는 생략해도 괜찮을 때 도음이 된다.
먼저 모든 매개변수를 하나로 추상화한 객체를 정의하고, 클라이언트에서 이 객체의 setter메서드를 호출해 필요한 값을 설정하게 하는 것이다. 이때 각 setter 메서드는 매개변수 하나 혹은 서로 연관된 몇 개만 설정하게 한다. 클라이언트는 먼저 필요한 매개변수를 다 설정한 다음, excute 메서드를 호출해 앞서 설정한 매개변수들의 유효성을 검사한다. 마지막으로 설정이 완료된 객체를 넘겨 원하는 계산을 수행한다.

● 매개변수의 타입으로는 클래스보다 인터페이스가 더 낫다. 매개변수로 적합한 인터페이스가 있다면 그 인터페이스를 직접 사용하자

● boolean보다는 원소 2개짜리 열거 타입이 낫다. 열거 타입을 사용하면 코드를 읽고 쓰기가 더 쉬워진다.


아이템 52. 다중정의 신중히 사용하라
● 컬렉션을 Set, List, Collection으로 구분하고 출력하고 할 때 Collection만 출력이 된다. 다중정의(Overroding)된 세 classify 중 어느 메서드를 호출할지가 컴파일타임에 정해지기 때문이다.

● 재정의한 메서드는 동적으로 선택되고, 다중정의한 메서드는 정적으로 선택되기 때문이다. 매서드를 재정의한 다음 '하위 클래스의 인스턴스'에서 그 메서드를 호출하면 재정의한 메서드가 실행된다.
컴파일 타임이 모두 상위 클래스와는 무관하게 항상 가장 하위에서 정의한 재정의 메서드가 실행된다.

● 매개변수가 같은 다중정의는 만들지 말자. 특히 가변인수를 사용하는 메서드라면 다중정의를 아예 하지말아야한다. 다중정의하는 대신 메서드 이름을 다르게 지어주는 것이 좋다.

● 생성자의 경우 이름을 다르게 지을 수가 없다. 정적 팩터리를 사용하면 된다.

● 생성자를 오버라이딩할 시에는 매개변수 중 하나 이상이 근본적으로 달라야한다. 근본적으로 다르다는 건 두 타입의 null이 아닌 값을 서로 어느 쪽으로든 형변환할 수 없다는 의미이다.
이 조건이 충족한다면 어느 다중정의 메서드를 호출할지가 매개변수들의 런타임 타입만으로 결정된다.

● 제네릭과 오토박싱이 등장하면서 메서드간의 매개변수 타입이 더는 근본적으로 다르지 않게 되었다. List 인터페이스가 취약해졌다. 

● 다중정의된 메서드들이 함수형 인터페이스를 인수로 받을 때, 비록 서로 다른 함수형 인터페이스라도 인수 위치가 같으면 혼란이 생긴다. 따라서 메서드를 다중정의할 때, 서로 다름 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다.



참고
●  TOCTOU(검사시점/사용시점) 공격 : 멀티스레딩 환경에서 원복 객체의 유효성을 검사한 후 복사본을 만드는 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정하여 위험을 주는 공격
직교성 : 서로 영향을 주는 성분이 없는 것을 말한다. 공통점이 없는 기능들이 잘 분리되어 있다. 혹은 기능을 원자적으로 쪼개 제공한다.

댓글

가장 많이 본 글