08. 메소드
메서드 설계 시 주의할 점들에 대해 알아보자
사용성, 견고성, 유연성에 집중하자
#
매개변수가 유효한지 검사하라item 49
매개변수 검사를 제대로 안하면 무슨 일이 생길까
- 메서드가 잘 수행되는 것 처럼 보이다가 예상치 못한 곳에서 Exception
- 성공은 하는데, 객체를 이상하게 만들어버림
매개변수 검사에 실패하면 실패 원자성(failure atomicity)를 어기는 결과를 낳을 수 있다.
매개변수 값이 잘못됐을 때 던지는 예외를 문서화해야한다.
자바는 Null 처리가 까다롭다. 그런 측면에서 Kotlin의 Null 처리는 상당히 Nice 한데... 쩝 자바 쓰기 싫다...
매개변수의 Null 검증은
@Nullable
애노테이션으로 해볼 수 있지만 표준적인 방법은 아니다.
#
자바 7 requireNonNull 메서드의 사용자바 7에서 제공하는
java.util.Objects.requireNonNull
메서드는 유연하고 사용하기 편리하니 null 검사를 수동으로 하지 않아도 된다.참고로 두 번째 파라미터는 예외 메시지이다.
자바 9에서는 Objects에 범위 검사 기능도 더해졌다. (이 클래스를 계속 확장해나갈 모양인 듯?)
- checkFromIndexSize, checkFromToIndex, checkIndex라는 메서드들인데, null 검사 메서드만큼 유연하지는 않다. (써본적 없음)
- 예외 메시지를 지정하는 기능도 없고 리스트와 배열 전용으로 설계되었다.
#
public method가 아닌 경우의 null check- 사실 이 내용은 몰랐었다. Intellij의 자동 널체크 기능을 사용하다보면 assert 구문을 사용하는 경우가 종종 있었는데, 접근 제한자 때문이었을 줄이야
public 메서드가 아닌 메서드라면 단언문(assert)를 사용해 유효성 검사를 할 수 있다.
단언문은 몇 가지 면에서 일반적인 유효성 검사와 다르다.
첫 번째는, 실패하면 AssertionError를 던진다.
두 번째는, 런타임에선 아무런 효과도, 성능 저하도 없다.
#
Null 체크는 언제가 적기일까메서드 Body (핵심 비즈니스 로직이 자리하는 영역)가 실행하기 전에 매개변수 유효성 검사를 하는 편이 좋다.
물론 예외는 있는데, 검사 비용이 지나치게 비싸거나 실용적이지 않을 때, 혹은 계산 과정에서 암묵적으로 검사가 수행되는 경우에는 Body에서 수행되어도 상관없다.
가령
Collection.sort(List)
처럼 객체 리스트를 정렬하는 경우에서 리스트 안의 객체들은 서로 상호 비교를 할 수 있어야 한다.만약 리스트에 비교될 수 없는 타입의 객체가 들어있다면 그 객체와 비교할 때
ClassCastException
을 던질 것이다.그러나 이러한 암묵적 유효성 검사에 너무 심취하여 의존하면 실패 원자성을 해칠 수 있으니 주의하자.
#
요약메서드나 생성자를 작성할 때 그 매개변수들에 어떤 제약이 필요한지 생각하자
그 제약들은 문서화하고, 메서드 코드 시작 부분에서 명시적으로 검사하자
#
적시에 방어적 복사본을 만들라item 50
객체지향에서 가장 중요한 개념 중 캡슐화라는 개념이 있다.
연관성 있는 속성들을 하나의 객체로 묶고, 실제 내용을 외부로부터 감추어 은닉한다는 개념이다.
이번 챕터에서는 클라이언트가 이러한 캡슐화를 뚫으려고 눈에 불을 키고 있다고 가정하고 방어적 프로그래밍을 해야한다는 내용을 담고 있다.
#
캡슐화가 클라이언트에 의해 깨지는 순간period
를 불변으로 만들고 싶었으나, 생성자를 만드는 시점에 사용되는end
가 외부에서 값이 바뀌어버려서 불변이어야하는period
의 불변성이 지켜지지 않았다.자바 8 이후로는 비교적 쉽게 해결할 수 있는데 기본적으로 불변 인스턴스인
LocalDateTime
이나ZonedDateTime
을 사용해도 된다.Date는 낡은 API이니 새로운 코드를 작성할 때는 더이상 사용하면 안된다. (그러나 우리 팀은 여전히 쓰고 있다...)
이러한 외부 공격을 방어하기 위해서는 생성자에서 받은 가변 매개변수를 방어적으로복사 해야한다.
매개변수 검사를 마지막에 하는 것이 순서가 얼핏 이상해보이지만 나름 이유가 있다.
멀티 쓰레드 환경에서 원본 객체의 유효성 검사 이후 복사본을 만드는 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 떄문이다.
clone
메서드를 사용하지 않은 점도 주목할만 한데, 매개변수가 제 3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때clone
을 사용하지 않는 편이 좋다.왜냐하면 Date 클래스는
final
이 아니므로clone
을 Date가 정의한 것이 아닐수도 있다.만약 클라이언트가
Date
를 상속받는 하위 클래스를 넘겨버리면 하위 클래스의 인스턴스를 반환할 수도 있기 때문이다.생성자를 수정하면서 공격은 막아냈지만,
Period
는 여전히 변경가능하다.
- 이 공격은 가변 필드의 방어적 복사본을 반환하면 된다.
이렇게 하면
Period
는 이제 철옹성이다.item 13
에서도 언급했지만clone
은 지양하고 생성자나 정적 팩터리를 애용하자.
#
교훈우리는 일련의 작업을 통해 가급적이면 불변 객체를 조합해 객체를 구성하는 편이 더 낫다는 결론에 도달하게 된다.
저자는 이러한 방어적 복사를 생략하려면 명확한 문서화를 강조 강조 또 강조하고 있다. (모두 다 알지만 귀찮아서 안하는 그것)
#
메서드 시그니처를 신중하게 설계하라item 51
메서드 이름을 신중하게 짓자. 클린 코드의 기본!
편의 메서드를 너무 많이 만들지는 말자. 메서드가 너무 많은 클래스는 사용하고 문서화하기 어렵다. (마찬가지로 클래스의 크기는 가급적 작게 유지하자)
매개변수 목록은 가능한 짧게 유지하자. 역시나 클린코드에서 본 개념.
- 특히 같은 타입의 매개변수가 여러개 나오면 지옥
#
많은 매개변수 목록을 짧게 줄여주는 기술#
1. 여러 메서드로 쪼갠다쪼개진 메서드들은 각각은 원래 매개변수 목록의 부분집합을 받는다.
가령 자바 컬렉션 프레임워크의
List
인터페이스가 좋은 예시이다.리스트에서 지정된 범위에서 주어진 원소의 인덱스를 찾을 때 메서드로 구현하려면
부분 리스트의 시작
,부분 리스트의 끝
,찾을 원소
3개의 매개변수가 필요하다.List
에서는 부분 리스트를 반환하는subList
메서드와 원소의 인덱스를 알려주는indexOf
메서드를 별개로 제공한다.
#
2. 매개변수 여러 개를 묶어주는 도우미 클래스를 만든다레이어드 아키텍처에서 주로 쓰는 기법인데, 우리는 주로
DTO
라고도 부른다.넘겨주는 매개변수들을 하나의 클래스로 묶어버리는 기법이다.
#
3. 앞서의 두 기법을 쓰까모든 매개변수를 하나로 추상화한 객체를 정의하고, 클라이어늩에서 이 객체의 세터 메서드를 호출해 필요한 값을 설정하게 한다.
각 세터 메서드는 매개변수 하나 혹은 서로 연관된 몇 개만 설정하게 한다.
매개변수의 타입으로는 클래스보다는 인터페이스가 더 낫다.
Map
을 떠올려보라, 우리는 메서드에HashMap
타입을 명시해야만 사용할 수 있는 메서드를 만들어본적은 없을 것이다.
#
boolean 보다는 원소 2개짜리 이넘 타입을 사용하자- 이넘 타입을 사용하면 코드 가독성이 올라간다. 가급적 이넘 타입을 사용하자.
#
다중 정의는 신중히 사용하라item 52
이 코드의 출력 결과는 어떨까?
집합, 리스트, 그 외를 차례대로 출력할 것 같지만 실제로는 그 외만 세 번 찍힌다.
그 이유는 오버로딩된 메소드들 중 컴파일 타임에 정해지기 떄문이다.
컴파일 타임에서의 반복문 안의 c는 여전히
Collection<?>
타입이다.런타임에서야 타입이 바뀌겠지만 컴파일 타임에서 이미 매개변수의 타입이 정해졌으므로 계속해서 같은 메서드를 호출하는 것이다.
이처럼 재정의한 메서드는 동적으로 선택되고, 오버로딩한 메서드는 정적으로 선택되기 때문이다.
오버로딩은 때때로 혼동을 일으키기도 하므로, 안전하고 보수적으로 가려면 매개변수 수가 같은 오버로딩은 가급적 만들지 말자.
오버로딩 대신 메서드 이름을 다르게 지어주는 방법도 있으니까 말이다.
#
ObjectOutputStream의 좋은 예오버 로딩을 사용하지 않은 좋은 예는 ObjectOutputStream 클래스이다.
이 클래스는
write
메서드에 모든 기본 타입과 일부 참조 타입용 변형을 가진다.그런데 오버 로딩이 아닌, 모든 메서드에 다른 이름을 지어주었다.
writeBoolean
(boolean),writeInt
(int),writeLong
(long)과 같은 식이다.이 방식이 오버로딩보다 나은 점은,
read
메서드와 짝을 이루기 좋다는 것이다.readBoolean
,readInt
,readLong
등과 같이 말이다.
#
람다에서 주의할 부분만약 함수형 인터페이스를 인수로 받을 때 매개변수가 하나이고, 다른 함수형 인터페이스를 받는 오버로딩 메소드는 어떨까?
이 경우 역시 서로 다른 함수형 인터페이스라도 인수 위치가 같으면 혼란이 생길 수 있다.
메서드를 오버로딩할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받지 말자
서로 다른 함수형 인터페이스이더라도 근본적으로는 다르지 않기 때문에 의도대로 동작하지 않을 수 있다.
#
요약프로그래밍 언어에서 오버로딩을 지원하더라도 반드시 써야하는 것은 아니다.
특히 인수 개수가 같은 경우라면 오버로딩은 피하자.
만약 인수 개수가 같은 오버로딩을 해야한다면 메소드 명을 달리하자.
인수 개수가 같은 오버로딩 메소드는 그 메소드를 사용하는 개발자로부터 발생하는 잠재적 위험을 안고 있다.
#
가변인수는 신중히 사용하라item 53
- 가변인수 메서드는 명시한 타입의 인수를 0개 이상 받을 수 있다.
간단한 덧셈 예제에서의 가변인수 사용은 문제가 없다.
그러나 다음과 같은 코드처럼 인수가 1개 이상인 경우에만 사용할 수 있는 메서드라면 0개를 넣었을 때 런타임에서 에러가 발생할 것이다.
이 방식은 일단 코드 자체도 지저분하고 args 유효성 검사도 해야하며, min의 초기값을 Integer.MAX_VALUE로 설정하지 않고서는 for-each도 사용할 수 없다는 점이다.
다행히 훨씬 더 나은 대안이 있는데, 매개 변수를 두개 받으면 된다.
- 첫 번째 매개변수를 받고, 두 번째 값 부터는 가변 인수를 통해 받으면 된다.
#
가변인수와 성능가변인수는 내부적으로 배열을 새로 할당하고 초기화하는 작업을 진행한다.
그 말은 즉 가변인수가 있는 메서드를 호출할 때 마다 새롭게 배열을 할당하고 초기화한다는 사실이다.
이러한 초기화를 막기 위해, 메서드 오버로딩을 사용해서 특정 개수만큼은 메서드 오버로딩을 통해 정의하고, 그 이상은 가변인수를 사용하는 것이다.
#
null이 아닌, 빈 컬렉션이나 배열을 반환하라item 54
null을 반환하는 것은 왜 나쁠까?
이 메서드를 사용하는 클라이언트는 다음과 같은 로직을 필연적으로 사용해야한다.
컬렉션이나 배열과 같은 컨테이너가 비었을 때 null을 반환하면 매번 이런 방어 코드를 넣어야한다. -> 귀찮다.
그렇기에 가급적이면 null 보다는 빈 컬렉션을 반환해주는 편이 더 낫다.
- JPA 역시 조회 쿼리를 날릴 때 결과가 없으면 빈 컬렉션을 반환해준다.
#
옵셔널 반환은 신중하라자바 8에서 새롭게 추가된
Optional<T>
는 null이 아닌 T 타입 참조를 담거나 아무것도 담지 않을 수 있는 타입이다. (내가 자바를 싫어하게 된 가장 큰 이유)이 옵셔널을 사용하면 null을 반환하는 메소드 보다는 조금 더 안전하고 깔끔하게 사용할 수 있다는 장점이 있다.
옵셔널은 기본적으로 원소를 최대 1개 가질 수 있는
불변
컬렉션이다. 그렇다고 해서 정말로Collection<T>
를 구현한건 아니고, 원칙적으로는 그렇다.최대 1개를 가질 수 있으니 아무것도 가지지 않은 옵셔널은
비어있다
고 표현한다.
#
옵셔널 사용 시 주의점컬렉션, 스트림, 배열, 옵셔널은 옵셔널로 감싸면 안된다.
박싱된 기본 타입을 감싸는 옵셔널은 기본 타입 자체보다 무거울 수 밖에 없다. 박싱된 기본 타입을 담은 옵셔널을 반환하지 않도록 하자.
옵셔널은 필드에 선언하는 것도 확실히 좋은 상황은 아니다.
그러나 예외적으로 Primitive 타입인 필드의 값이 없음을 나타낼 때 getter가 옵셔널을 반환하게 해주는 것은 좋을 수 있다고 한다. (그럴거면 코틀린 쓰자)
#
요약- null 처리 깔끔하게 하고 싶어서 옵셔널 쓸거면 코틀린 쓰자
#
공개된 API 요소에는 항상 문서화 주석을 작성하라#
Reference이펙티브 자바 Effective Java 3/E
저자 : 조슈아 블로크