본문 바로가기

Reading/Effective Java

[Effective-Java] Item 51. 메소드 시그니처를 신중히 설계하라

메소드 이름을 신중히 짓자

  • 항상 표준 명명 규칙을 따르자. 그리고 이해할 수 있고, 같은 패키지에 속한 다른 이름들과 일관되게 짓는 것을 최우선으로 두자
  • 그 다음 목표로 개발자 커뮤니티에서 널리 받아들여지는 이름을 사용하자.
  • 긴 이름은 피하고, 애매하면 자바 라이브러리 API 가이드를 참조하자.

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

  • 메소드가 너무 많은 클래스와 인터페이스는 익히기, 문서화, 사용, 테스트, 유지보수를 어렵게한다.
  • 클래스나 인터페이스는 자신의 각 기능을 완벽히 수행하는 메소드로 제공하자
  • 자주 쓰일 경우에만 약칭 메소드를 두고, 확신이 서지 않으면 만들지 말자.

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

  • 4개가 넘어가면 매개변수 기억이 힘들어져 IDE에 의존할 확률이 높으니, 가능한 4개 이하로 두자. 
  • 같은 타입의 매개변수가 연달아 나오면 순서 기억도 어렵고 순서를 바꿔도 컴파일되니 의도한대로 동작하지 않을 수 있음을 기억하자

매개변수의 타입으로 클래스보다 인터페이스를 사용하자

  • 예를들어 메소드 매개변수 타입으로 HashMap 구현체가 아닌 Map 인터페이스를 넘기면 TreeMap, ConcurrentHashMap, 부분 맵 등 어떤 Map 구현체도 인수로 건넬 수 있고, 아직 존재하지 않는 Map도 가능하다.
  • 구체 클래스를 매개변수 타입으로 사용하면 클라이언트는 특정 구현체만 사용하도록 제한된다.
  • 따라서 입력 데이터가 다르면 명시한 특정 구현체의 객체로 옮겨 담느라 복사 비용이 든다.

boolean보다는 원소 2개짜리 열거 타입이 낫다

메소드 이름상 boolean을 받아야 의미가 더 명확할 때는 예외고, 열거 타입을 사용하면 코드를 읽고 쓰기, 선택지를 추가하기 쉽다.

 

public enum TemperatureScale { FAHRENHEIT, CELSIUS }

public static void main(String[] args) {
    // boolean보다 열거타입이 더 명확하다.
    Thermometer.newInstance(true);
    Thermometer.newInstance(TemperatureScale.CELSIUS);
}

 

그리고 나중에 온도도 지원해야 한다면 화씨, 섭씨별 온도 단위 계산식을 열거 타입 상수의 메소드 안으로 리팩토링해 넣을 수 있다.

예를들어 double 값을 받아 섭씨 온도로 변환해주는 메소드를 열거 타입 상수 각각에 정의하는 것처럼!

따라서 이런 장점들을 이용할 수 있는 열거 타입을 지원하는게 더 좋다.

 

 

긴 매개변수 목록을 짧게 줄여주는 기술


1. 여러 메소드로 쪼개기

쪼개진 메소드 각각은 원래 매개변수 목록의 부분집합을 받아 잘못하면 메소드가 너무 많아질 수 있지만 직교성(orthogonality)을 높여 오히려 메소드 수를 줄여줄 수 있다.

 

List 인터페이스에서 지정된 범위의 부분 리스트에서 인덱스를 찾는 기능을 메소드로 구현하려면

(부분리스트 시작, 부분리스트 끝, 찾을 원소), 총 3개의 매개변수가 필요하지만 List는 그럴 필요가 없다.

 

List<Integer> lists = new ArrayList<>();
List<Integer> integers = lists.subList(0, 5);
integers.indexOf(3);

 

다음과 같이, 부분리스트를 반환해서 그 부분리스트로 어느 인덱스에 있는지 indexOf 메소드를 이용하면 되니까!

두 메소드를 조합해 원하는 목적을 이룰 수 있고, 강함과 유연함이 균형을 이룬 API가 만들어진 것이다.

 

이제 위에서 말한 직교성에 대해, 그리고 이 예시에 적용한 직교성을 알아보자.

 

 

*직교성 - 소프트웨어에서의 각 요소들의 일종의 독립성이나 결합도

직교성이 높다고 하면 공통점이 없는 기능들이 잘 분리되어 있고(독립성 높음) 기능을 원자적으로 쪼개 제공하는(결합도 낮음) 것이다.

List 인터페이스를 예시로 나타내면, 부분 리스트 얻기와 주어진 원소의 인덱스 구하기는 서로 관련이 없기에 두 기능을 개별 메소드로 제공해야 직교성이 높다고 할 수 있었던 것이다.

 

직교성을 높여 오히려 메소드 수를 줄여준다는 것은, 기능을 원자적으로 쪼개다보면 자연스럽게 중복이 줄고 결합성이 낮아져 코드 수정과 테스트가 용이해진다는 것이다. 직교성이 높은 설계는 가볍고 구현하기 쉬우며 유연하고 강력하다.

위에서 설명한 부분리스트에서 특정 값을 찾는 것은 총 3개의 매개변수가 필요하고 이를 메소드로 쪼갤 수 있는 방법은 7가지가 존재한다. 그래서 편의성을 높인다는 생각에 고수준의 복잡한 기능을 하나씩 추가하다 보면 생각지도 못한만큼 큰 API가 만들어질 수 있다.

 

그렇다고 무한정 원자로 쪼개서는 안되며, API가 다루는 개념의 추상화 수준에 맞게 조절해야한다. 특정 조합의 패턴이 자주 사용되거나 최적화해서 성능 개선할 수 있다면 오히려 직교성이 낮더라도 편의 기능으로 제공되는게 나을 수 있기때문에 상대적으로 설계해야한다.

 

견해:

이와 같은 직교성은 소프트웨어 설계 전 영역으로 확대할 수 있다.

 

출처: 지식덤프

 

저수준에 CPU에선 명령어 설계 철학 RISC와 CISC가 있다. CISC는 캐시 영역에서 데이터 영역과 명령어 영역 구분없이 모두 사용하고 있어 직교성이 낮다고 볼 수 있고 RISC영역은 데이터 영역과 명령어 영역이 독립적으로 구분되어있고 결합도가 낮아 직교성이 높다고 볼 수 있다. 명령어 수와 레지스터, 메모리 접근 등 많은 조건 차이가 있지만, 나는 직교성에 국한한 내용은 캐시파트라고 생각했다.

 

출처: ncube

그리고 저수준에서 고수준으로 올라오면 클래스, 패키지, 모듈 설계에도 똑같이 적용할 수 있다. Monolithic Architecture는 하나의 서버에 다양한 기능들이 한 곳에 몰려있음으로 독립성이 낮고 결합도가 높아 직교성이 낮다고 할 수 있고, Micro Service Architecture는 독립된 여러 서비스로 구성되어 결합도가 낮아 직교성이 높다고할 수 있다.

 

직교성 하나에만 국한해 해석한 견해라 설명이 부족하거나 잘못되었을 수도 있습니다.

 

출처: [ Monolithic / MicroService Architecture ] / [ CISC / RISC ] / [ Monolithic / MicroService 2 ]

 

2. 매개변수 여러 개를 묶어주는 도우미 클래스 만들기

도우미 클래스는 일반적으로 정적 클래스로 두고, 특히 잇따른 매개변수 몇 개를 독립된 하나의 개념으로 볼 수 있을때 추천하는 기법이다.

카드게임을 클래스로 만든다고 할 때, 카드의 숫자와 무늬를 뜻하는 두 매개변수를 항상 같은 순서로 전달할 것이다.

이 둘을 묶는 도우미 클래스를 만들면 하나의 매개변수로 주고받고 API는 물론 클래스 내부 구현도 깔끔해진다.

다음은 도우미 클래스의 예시를 짜봤다.

 

public class Game {
    private List<Card> cards = new ArrayList<>();
    public void addCardV2(Suit suit, Rank rank){
        // 도우미 클래스가 없다면 매개변수 2개로 로직을 처리해야한다.
    }
    public void addCard(Card card){
        // 도우미 클래스 덕분에 매개변수 1개로 로직을 바로 처리할 수 있다.
    }
    
    ...
    
    public static class Card{
        public enum Suit { SPADE, HEART, DIAMOND, CLUB }
        public enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN,
            EIGHT, NINE, TEN, JACK, QUEEN, KING }
        private final Suit suit;
        private final Rank rank;
        ...
        
    }
}

 

3. 객체 생성에 사용한 빌더 패턴을 메소드 호출에 응용하기

매개변수가 많고 특히 일부는 생략해도 괜찮을 때 추천하는 기법이다. 빌더패턴을 이용해 선택 필드를 setter로 설정하고 필요한 매개변수를 다 설정하면 execute 메소드를 호출하여 설정한 매개변수들의 유효성을 검사하고, 원하는 계산을 수행하면 된다. 여기서 말하는 execute 메소드가, 좀 더 통용적으로는 실제 자신의 객체를 반환해주는 build() 메소드라고 보면 될 것 같다.

 

 

 

* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.