Language/Java

[effective java] 아이템 51. 메서드 시그니처를 신중히 설계하라.

JOYERIM 2023. 7. 17. 07:16

 

 

 

 

 

 

아이템 51은 API 설계를 위한 다양한 Tip들을 설명한다.

 

 

 

 

 

 

메소드 이름을 신중히 짓자.

 

  • 표준 명명 규칙(item 68)을 따르자.
    • 예를 들어, Java에서는 메소드 이름을 camel case를 사용한다
  • 이해하기 쉬운 이름을 사용하자.
  • 일관된 이름을 사용하자.
    • 같은 패키지나 클래스 내에서 메소드 이름은 일관성을 유지해야 한다.
  • 커뮤니티에서 널리 사용하는 이름을 사용하자.
  • 너무 긴 이름은 피하자.

 

 

 

 

 

 

 

편의 메소드는 적당히 만들자.

 

  • 클래스/인터페이스에 메소드가 너무 많으면 복잡성이 커지고, 코드의 가독성이 저하된다. 또한, 문서화, 테스트, 유지보수 등의 작업을 복잡하게 만든다.
  • 클래스/인터페이스는 해당 클래스/인터페이스가 수행해야 할 기능에 필요한 메소드만 제공해야 한다. 즉, 각 메소드는 한 가지 기능을 완벽히 수행하도록 설계되어야 하며, 그 이상의 역할을 수행하지 않아야 한다.
  • 편의 메소드는 신중하게 고려하여 필요한 경우에만 추가해야 한다.
    • 편의 메소드는 주요 기능을 보완하거나 단순화하는데 사용되는 메소드이다.
    • 사용하기 쉽게 만드는 데 도움이 될 수 있지만, 필요 이상으로 많은 편의 메소드는 오히려 복잡성만 증가시킨다.
  • 확신이 없다면 메소드를 추가하지 마라.

 

 

 

 

 

 

 

 

 

매개변수 목록은 짧게 유지하자.

 

 

일반적으로 매개변수는 4개 이하로 유지하는 것이 좋다. 매개변수 목록을 줄이는 방법에 대해 알아보자.

 

 

방법 1. 여러 메소드로 분리

 

메소드를 여러 개로 분리하고, 각각의 메소드는 원래 매개변수 목록의 부분집합을 받는다. 메소드 수가 늘어나지만, 각 메소드의 목적이 명확해지며, 메소드 간의 관계를 이해하기 쉽게 만든다. 이를 통해 코드의 직교성이 높아진다.

 

직교성은 서로 독립적인 기능들이 잘 분리되어 있는 상태를 말하며, 이를 통해 불필요한 메소드를 최소화하고 코드의 복잡성을 줄일 수 있다.

 

예를 들어, java.util.List 인터페이스에서는 특정 범위의 부분리스트에서 특정 원소의 인덱스를 찾는 것을 subList() 메소드와 indexOf() 메소드로 분리하여 제공한다. 아래 코드를 보도록 하자.

 

다음은 특정 범위의 리스트에서 특정 원소를 찾는 메소드를 가지고 있는 클래스이다. 이 메소드는 (리스트, 시작 인덱스, 종료 인덱스, 찾을 원소의 값) 4개의 매개변수를 받는다.

public class ListSearcher {
    public int findElementInSublist(List<Integer> list, int fromIndex, int toIndex, int element) {
        for (int i = fromIndex; i < toIndex; i++) {
            if (list.get(i) == element) {
                return i;
            }
        }
        return -1; // element not found
    }
}


이를 여러 메소드로 분리하여 java.util.List의 subList()와 indexOf()와 같이 쪼개 보겠습니다.

public class ListSearcher {
    public List<Integer> getSublist(List<Integer> list, int fromIndex, int toIndex) {
        return list.subList(fromIndex, toIndex);
    }

    public int findElement(List<Integer> list, int element) {
        return list.indexOf(element);
    }
}


이러한 분리를 통해 메서드 간의 직교성이 높아져 코드가 더 이해하기 쉽고 관리하기 편리해진다. 동시에, 불필요한 메서드를 최소화하고 코드의 복잡성을 줄일 수 있다.

 

 

방법 2. 도우미 클래스 생성

 

여러 매개변수를 하나의 클래스로 묶어 도우미 클래스를 만드는 방법이다. 이 클래스는 보통 정적 멤버 클래스로 구현되며, 연관된 매개변수들을 하나의 개념으로 묶을 수 있다.

 

예를 들어, 카드 게임에서 카드의 숫자와 무늬를 나타내는 두 개의 매개변수를 Card라는 도우미 클래스로 묶어서 하나의 매개변수로 사용할 수 있다.

 

 

방법 3. 빌더 패턴 응용

 

빌더 패턴은 객체 생성 시 매개변수의 수가 많을 때 유용한 디자인 패턴인데, 이를 메소드 호출에도 응용할 수 있다. 이 방법은 매개변수가 많고 일부는 생략 가능할 때 사용하기 좋다. 

 

아래 예시를 살펴보자. 먼저, 여러 매개변수들을 묶을 도우미 클래스를 구현한다. 이 클래스는 여러 매개변수를 필드로 가지며, 이들 매개변수를 설정하기 위한 세터 메서드를 제공한다.

public class SearchParams {
    private int start;
    private int end;
    private String target;

    public SearchParams setStart(int start) {
        this.start = start;
        return this;
    }

    public SearchParams setEnd(int end) {
        this.end = end;
        return this;
    }

    public SearchParams setTarget(String target) {
        this.target = target;
        return this;
    }

    public int getStart() {
        return start;
    }

    public int getEnd() {
        return end;
    }

    public String getTarget() {
        return target;
    }
}


위와 같은 SearchParams 클래스를 사용하면 클라이언트는 메서드 호출 시 아래와 같이 쉽게 매개변수를 설정할 수 있다.

SearchParams params = new SearchParams().setStart(0).setEnd(100).setTarget("apple");


이제 이 SearchParams 객체를 매개변수로 받는 메서드를 만들어보자. 이 메서드는 SearchParams 객체로부터 필요한 값을 받아서 작업을 수행합니다.

public class Searcher {
    public int search(SearchParams params) {
        
        int start = params.getStart();
        int end = params.getEnd();
        String target = params.getTarget();

        // 추가 동작 수행
        
        return -1;
    }
}


이렇게 하면 매개변수가 많은 메서드를 보다 쉽게 호출하고 관리할 수 있게 된다. 또한, 클라이언트 코드는 필요한 매개변수만 설정하면 되므로 훨씬 깔끔해진다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

매개변수 타입으로는 클래스보다 인터페이스를 사용하자.(item 64)

 

매개변수로 적합한 인터페이스가 있다면, (이를 구현한 클래스가 아닌) 인터페이스를 직접 사용하자.

 

예를 들어 인자로 Map을 넘긴다면, HashMap뿐만 아니라 어떤 Map 구혀체도 인수로 건넬 수 잇다.

 

 

 

 

 

 

 

 

 

boolean 보다는 원소 2개짜리 열거 타입을 사용하자.

 

boolean 변수를 사용하면, 그 값이 true일 때와 false일 때가 무엇을 의미하는지 명확하게 이해하기 어렵다. 하지만 열거 타입을 사용하면 각 값이 무엇을 의미하는지 명확하게 알 수 있다.

만약 온도 단위를 구분하기 위해 boolean 변수를 사용하게 되면, 해당 변수가 true일 때 섭씨인지 화씨인지 알기 어렵다. 하지만 열거 타입을 사용하면 각 값이 섭씨인지 화씨인지 명확하게 알 수 있다. 또한 나중에 다른 온도 단위(예: 절대온도)가 추가되어야 하는 경우, 열거 타입을 확장하는 것이 더욱 쉽다.

다음으로, 각 열거 타입 상수가 자신의 메소드를 가질 수 있다. 이를 통해 각 온도 단위에 따른 계산 로직을 해당 열거 타입 상수 내부에 숨겨 넣을 수 있다. 아래 예시를 살펴보자.

 

public enum TemperatureScale {
    CELSIUS {
        @Override
        public double toCelsius(double temp) {
            return temp;
        }

        @Override
        public double toFahrenheit(double temp) {
            return temp * 9.0 / 5.0 + 32.0;
        }
    },

    FAHRENHEIT {
        @Override
        public double toCelsius(double temp) {
            return (temp - 32.0) * 5.0 / 9.0;
        }

        @Override
        public double toFahrenheit(double temp) {
            return temp;
        }
    };

    public abstract double toCelsius(double temp);
    public abstract double toFahrenheit(double temp);
}

 

각 상수는 섭씨로 변환하는 toCelsius() 메소드와 화씨로 변환하는 toFahrenheit() 메소드를 갖는다. 이렇게 하면 각 온도 단위에 따른 계산 로직을 열거 타입 내부에 숨겨 넣을 수 있다.