본문 바로가기

Java/JAVA

자바 메서드 '잘' 작성하는 방법, 알면서 못 지키는 것들(이펙티브 자바 스터디 - 메서드)

어떻게 해야 메서드 잘 만들었다고 소문이 날까?

개발을 하면서 가장 많이 하는 일이면서 가장 난해한 것이 메서드 작성이 아닐까 싶다.

"하나의 메서드에서는 하나의 작업만 한다!" 라는 기본 원칙을 지키려고 하면서도 잘 안된다.

사소하게는 private으로 할지 public으로 해야 할지등 정해야할 것들이 너무도 많은 것이 메서드다.

역시나 한 번에 제대로 작성하려고하면 어렵다. 대신 유념하면서 고치고 또 고치면서 배우는게 코딩아닐까 싶다.

회사를 다닌다면 코드 리뷰를 하는 이유도 올바른, 좋은 메서드를 개발하기 위함이 아닐까 한다.

결국 좋은 메서드를 만드려면 기본 원칙을 잘 알고 시도하고 리팩토링을 거쳐봐야 한다. 그래서 아래에 기본 원칙을 소개하려고 한다.


메서드 작성 기본 원칙

- 파라미터가 유효한지 검사하라

메서드를 작성할 때 파라미터가 유효한지 제일 앞에서 검사해야한다.

메서드의 파라미터가 당연히 유효할 것이라는 생각을 버리고 public, protected 접근지정자가 붙으면 특히나 유념해서 null 체크나 음수, 양수등의 조건 필터를 반드시 넣어야한다.

Objects 클래스에서 requireNonNull로 null 체크를 할 수 있고 예외 메시지도 넣을 수 있다.

이 작업을 앞에서 하지 않고 중간에 확인하면 의도치 않은 에러가 발생할 확률이 올라가기 때문에 파라미터를 제일 앞에서 바로 검사하는 것이 좋다.

- 적시에 방어적 복사본을 만들어라

약간 과장하면 클라이언트가 내가 만든 불변식을 깨뜨리려고 하는 사람이라고 생각하고 프로그래밍 해야한다.

그래서 방어적 복사라는 말이 나온 것이다.

방어적 복사란 어떤 클래스의 메서드에서 클래스의 멤버 변수(객체)에 값을 쓰거나 가져올 때 복사본을 만들어서 get/set 을 사용하는 것이다. (예를 들어서 get/set 메서드일 뿐 다른 메서드도 멤버 변수에 접근한다면 복사본을 만들 필요가 있다.)

* 중요한 것은 파라미터의 유효성을 검사하기 전에 방어적 복사본을 만들고 이 복사본으로 유효성을 검사해야한다. 

방어적 복사를 할 때는 clone 메서드를 사용해서는 안된다.

생성자에서 받은 매개변수 값을 각각 방어적 복사해서 사용해야한다.

성능이 중요할 때는 이런 과정이 영향을 끼칠 수 있다. 따라서 이러한 경우에는 방어적 복사를 하지 않고 문제가 생길 수 있음을 주석과 문서에 명시하는 것으로 대체하면 된다.

- 메서드 설계를 잘하자

시그니처를 신중히 설계하라고 되어있지만 결국은 메서드 설계를 잘하자는 내용이다.

1) 메서드 이름을 잘 짓자.

메서드의 이름은 표준 명명 규칙에 따라서 짓고 같은 패키지에 속한 이름과 일관되게 짓는 것이 좋다.

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

하나의 클래스에 너무 많은 메서드가 있으면 다 익히기도 어렵고 구현이 엉켜서 에러를 만들 수 있다.

3) 파라미터는 4개 이하로 만들자.

파라미터를 단순히 필요하다고 5개 이상을 받도록 만들면 메서드를 사용하는 과정에서 헷갈리고 이상한 매개변수를 넣을 확률이 높아진다.

파라미터 개수를 줄이는 방법으로는 여러 메서드로 쪼개는 방법도 있고 파라미터 여러 개를 묶어주는 헬퍼 클래스를 만드는 방법도 있다. 또한 빌더 패턴을 메서드 사용에 호출하는 방법도 있다. (객체 생성에서 빌더 패턴은 다른 포스트에서 작성할 것이다)

4) 파라미터의 타입으로는 클래스보다 인터페이스가 낫다.

구체적으로 HashMap, HashSet이 아니라 Map, Set 인터페이스로 받는 것이 더 유연하다는 얘기다.

5) boolean 보다는 원소 2개짜리 enum이 낫다.

enum을 쓰게되면 코드를 읽고 쓰는게 더 용이하다.

- 다중 정의는 웬만하면 하지 마라

메서드를 같은 이름으로 파라미터 타입만 다르게 정의하는 것이 다중 정의다.

다중 정의가 혼동을 일으킬 우려가 있으니 그냥 사용하지 말자 대신 메서드 이름을 다르게 지어주는 것으로 대체하자.

parseInt, parseDouble, ... 이런식으로 지어주는 것이 헷갈리지 않고 더 좋다.

생성자의 경우에는 이름을 다르게 짓는 것이 불가능하므로 헷갈릴만한 파라미터가 나오면 그냥 instanceof 로 정확한 타입을 찾아서 형변환(캐스팅)해주는 것으로 대체한다.

- 가변 인수는 꼭 필요할 때만 쓰자

가변 인수가 뭐냐면 메서드에 들어올 파라미터 수가 여러 개로 올 수 있게 할 때 쓰는 문법이다.

1
2
3
4
5
6
public int sum(int... args){
    int sum = 0;
    for(int arg : args)
        sum += arg;
    return sum;
}
cs

위의 예제코드처럼 (int...)으로 int 타입의 파라미터가 여러개 올 수 있는 것을 가변 인수라고 한다.

가변 인수는 메서드가 호출될 때마다 배열이 복사되기 때문에 성능에 큰 영향을 미칠 수 있어 자주 호출된다면 사용하지 말아야 한다.

가변 인수대신 사용할 것은 아까하지말라던 다중정의를 쓰는 것이다.

만약 sum 메서드 호출의 대부분이 args가 3개 이하라면 아래와 같이 구현해놓는 것이다.

1
2
3
4
5
public void sum(){}
public void sum(int a1){}
public void sum(int a1, int a2){}
public void sum(int a1, int a2, int a3){}
public void sum(int a1, int a2, int a3, int ...rest){}
cs

이렇게 구현하면 3개 이하를 사용하는 코드에서는 가변 인수를 사용하지 않다가 소수로 몇 번 사용하는 경우에만 가변 인수가 있는 메서드를 호출할 것이다.

가변 인수를 꼭 써야만 하는 곳이라면 써라

- null이 아닌 빈 컬렉션이나 배열을 반환하라

그냥 null을 반환해버리면 반환된 객체가 null이 아닌지도 체크를 매번해줘야한다.

그런데 빈 컬렉션이나 배열을 반환하면 size()라든지 contains(...) 메서드로 비어있는지 사용가능한지 알 수 있다.

- Optional 반환은 신중히 하라

Optional에 대해서 포스팅도 이미 있다. Optional은 특정 조건에서 반환할 값이 없을 때 null 대신 래퍼로 감싸는 타입이다.

Optional은 nullPointerException으로부터 조금 더 자유로워(?)지기 위해서 나온 타입이다.

따라서 Optional을 반환하는 메서드를 만드려면 절대 null을 반환하는 Optional을 리턴해서는 안된다.

그리고 Optional은 아무래도 래퍼하고 그걸 다시 풀고, 값이 없을 때 대체하는 값을 넣고 하는 등의 오버헤드가 있으니 성능 저하는 반드시 동반한다.

컬렉션, 스트림, 배열, 옵셔널같은 어떤 객체를 담을 수 있는 컨테이너타입은 절대 optional로 감싸면 안된다.

그럴바엔 빈 컬렉션, 배열을 반환하는 것이 훨씬 좋다.

박싱된 기본 타입(Integer, Boolean, Double, ...)을 담은 옵셔널을 반환하지 말라

이미 OptionalInt, OptionalLong, OptionalDouble이 있다.

기본타입이 들어있는 것을 반환할 때는 위의 메서드를 사용하도록 하자. 그러면 성능저하가 덜하다.

Optional을 컬렉션의 key, value, 배열의 원소로 사용하는게 적절한 상황은 없다. 사용하지 말라.

그러면 언제 써야할까?

딱 1개의 상황이다.

메서드의 결과를 알 수 없으며, 클라이언트가 이 상황을 특별하게 처리해야 할때 Optional<T>를 반환하게 하면 된다.


위와 같이 메서드를 작성할 수 있도록 유념하고 잘못한 부분이 있다면 다시 고치면 된다.