본문 바로가기

java/이펙티브 자바

Item51~Item52

Item47~Item48 가을
Item49~Item50 인호
Item51~Item52 워니/민이
Item53~Item54 민이/워니
Item55~Item56 진성
Item57~Item58 x(1주차때함)
Item59~Item60 제이크

 

item 51. 메서드 시그니처를 신중히 설계하라

1. 메서드 이름을 신중히 짓자
2. 편의 메서드를 너무 많이 만들지 마라
3. 매개변수 목록은 짧게 유지하자
   3-1. 여러 메서드로 쪼개라
   3-2. 매개변수 여러개를 묶어주는 도우미 클래스 사용하라
   3-3. 객체 생성에 사용한 빌더 패턴을 메서드 호출에 응용해라.
4. 매개변수 타입으로 클래스보다 인터페이스가 낫다

1. 메서드 이름을 신중히 짓자

  • 항상 표준 명명 규칙을 따라야 한다. 이해할 수 있고, 같은 패키지에 속한 다른 이름들과 일관되게 짓는 게 최우선 목표다.
  • 긴 이름은 피하도록 한다. 애매하면 자바 라이브러리의 API 가이드를 참조하도록 한다. 대부분은 납득할만한 수준이다.

2. 편의 메서드를 너무 많이 만들지 마라

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

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

  • 4개 이하가 좋다. 일단 4개가 넘어가면 매개변수를 전부 기억하기가 쉽지 않다.
  • 같은 타입의 매개변수 여러 개가 연달아 나오는 경우가 특히 해롭다.
    • 사용자가 매개변수 순서를 기억하기도 어렵고, 실수로 순서를 바꿔 입력해도 그대로 컴파일되고 실행된다. 그리고, 의도와 다르게 동작한다.

3-1. 여러 메서드로 쪼개라

  • 쪼개진 메서드 각각은 원래 매개변수 목록의 부분집합을 받는다.
  • 잘못하면 메서드가 너무 많아질 수 있지만, 직교성(orthohonality)을 높여 오히려 메서드 수를 줄여주는 효과도 있다.
  • ex) List 인터페이스: subList 메서드

부분리스트의 시작, 부분리스트의 끝, 찾을 원소 ==> 3개의 매개변수

findElementAtSubList(int fromIndexOfSubList, int toIndexOfSubList, Object element);

여러 메서드로 쪼갠다면 아래와 같은 방식으로 쪼갤 수 있습니다.

List<E> subList(int fromIndex, int toIndex);//지정된 범위의 부분 리스트를 구하는 기능

int indexOf(Object o);//주어진 원소를 찾는 기능

subList() List를 구한 후, indexOf()로 원소를 찾으면 됩니다

 

더보기

주어진 요구사항을 다시 생각해볼게요. 지정된 범위의 부분 리스트에서 주어진 원소를 찾아야 하는 요구사항입니다. 이 요구사항에는 기능을 두 가지로 분리할 수 있습니다. 지정된 범위의 부분 리스트를 구하는 기능 주어진 원소를 찾는 기능입니다. 이 두 기능을 생각해보면 공통점이 없습니다.

공통점이 없는데 하나의 메서드로 쓰이는 게 맞을까요? 아니죠. 분리되어야 합니다 이렇게 분리해서 기능으로 만든다면 다른 곳에서도 쉽게 조합해서 사용할 수 있습니다.

'공통점이 없는 기능이 잘 분리되었다'를 전문적인 말로 '직교성이 높다'라고 합니다. (저도 처음들었습니다ㅎㅎ) 아키텍처에도 직교성을 대입해본다면 마이크로 서비스 아키텍처는 직교성이 높고, 모놀리식 아키텍처는 직교성이 낮다고 할 수 있습니다.

기능을 쪼개다보면 자연스럽게 중복이 줄고 결합성이 낮아집니다. 코드를 수정하고 테스트하기 쉬워지는 거죠. 그렇다고 무한정 작게 나누는 게 좋은 건 아닙니다. API 사용자의 눈높이에 맞게, API가 다루는 개념의 추상화 수준에 맞게 조절해야 합니다.

기능을 잘 쪼갠(직교성이 높게 개발한) 예시가 List 인터페이스입니다. List 인터페이스를 보면 말 그대로 subList()는 부분 리스트를 반환하고 indexOf() 메서드는 주어진 원소의 인덱스를 알려줍니다. 별개로 제공된 두 메서드를 조합하면 우리가 원하던 지정된 범위의 부분 리스트에서 인덱스를 찾는 기능을 완성시킬 수 있는 거죠.

 

참고: https://github.com/Meet-Coder-Study/book-effective-java/blob/main/8%EC%9E%A5/51_%EB%A9%94%EC%84%9C%EB%93%9C_%EC%8B%9C%EA%B7%B8%EB%8B%88%EC%B2%98%EB%A5%BC_%EC%8B%A0%EC%A4%91%ED%9E%88_%EC%84%A4%EA%B3%84%ED%95%98%EB%9D%BC_%EC%9D%B4%ED%98%B8%EB%B9%88.md

 

3-2. 매개변수 여러개를 묶어주는 도우미 클래스 사용하라

도우미 클래스: 매개변수 여러개를 묶어준다 : 정적 멤버 클래스

ex) 카드게임 클래스

카드의 숫자, 카드의 무늬를 묶어서 전달

dealing(String gamerName, String rank, String suit)

 

도우미 클래스

class Blackjack {
    // 도우미 클래스 (정적 멤버 클래스)
    static class Card {
        private String rank;
        private String suit;
    }

    public void dealing(gamerName, card);
}

 

dealing(String gamerName, Card card) // 매개변수 줄어들음

 

3-3. 객체 생성에 사용한 빌더 패턴을 메서드 호출에 응용해라.

  • 이 기법은 매개변수가 많을 때, 특히 그중 일부는 생략해도 괜찮을 때 도움이 된다.
  • 먼저 모든 매개변수를 하나로 추상화한 객체를 정의하고, 클라이언트에서 이 객체의 세터(setter) 메서드를 호출해 필요한 값을 설정하게 한다.
  • 이때 각 세터 메서드는 매개변수 하나 혹은 서로 연관된 몇 개만 설정하게 한다.
  • 클라이언트는 먼저 필요한 매개변수를 다 설정한 다음, 앞어 설정한 매개변수들의 유효성을 검사한다.
  • 마지막으로, 설정이 완료된 객체를 넘겨 원하는 계산을 수행한다.

 

4. 매개변수 타입으로 클래스보다 인터페이스가 낫다

  • 매개변수로 적합한 인터페이스가 있다면 (이를 구현한 클래스가 아닌) 그 인터페이스를 직접 사용하도록 한다.
    • ex. 메서드에 HashMap을 넘길 일은 전혀 없다. 대신 Map을 사용한다. 그러면 다른 Map 구현체도 인수로 건넬 수 있다.
  • 인터페이스 대신 클래스를 사용하면 클라이언트에게 특정 구현체만 사용하도록 제한하는 꼴이며, 혹시라도 입력 데이터가 다른 형태로 존재한다면 명시한 특정 구현체의 객체로 옮겨 담느라 비싼 복사 비용을 치러야 한다.

5. boolean 보다는 원소 2개짜리 열거 타입을 사용해라.

  • 단, 메서드 이름상 boolean을 받아야 의미가 더 명확할 때는 예외다.
  • 열거 타입을 사용하면 코드를 읽고 쓰기가 더 쉬워지며, 나중에 선택지를 추가하기도 쉽다.
  • 열거 타입을 사용하면 개별 열거 타입 상수 각각에 특정 동작을 수행하는 메서드를 정의해둘 수도 있다.

 

 

 

 

 

 


 

 

item 52. 다중 정의는 신중히 사용하라

다중 메서드:  정적 : 이미 정해져 있는 걸 호출한다. 늘 똑같으니 정적.

재정의 메서드: 동적 : 호출 당시의 상황에 맞는 메서드를 부른다.. 그때그때 다르니 동적

 

컴파일 타임: 다중 메서드 호출 정함, for 문 안의 c는 항상 Collection<?> 타입

런타임 : 재정의한 메서드를 호출 함: 가장 하위에 재정의한 매서드가 실행됨.  

1. 다중 메서드 오류 예시
2. 재정의 메서드 오류 예시
3. 1번 수정: classfy메서드를 하나로 합쳐라. 그리고 분류하라.
4. 다중 정의가 혼동을 일으키는 상황을 피해라.
  3.1 매개변수 수가 같은 다중 정의는 만들지 말자.
       ex 1) ObjectOutputStream write 메서드
       ex 2) ObjectInputStream read 메서드
 3.2 매개변수 수가 같음, 근본적으로 매개변수가 다르다면 괜찮다.
    ex) ArrayList 
5. 평화의 시대 끝 : 오토박싱, 제네릭, 람다, 메서드 참조
   5.1 오토박싱, 제네릭 등장 -> 근본적으로 다르지 않게 됨
      1) List의 remove 메서드의 오류 예제
        원인: List<E>인터페이스가 remove(Object)와 remove(int)를 다중정의 했다
   5.2 람다와 메서드 참조 등장 -> 부정확한 메서드 참조
       1) submit 다중 정의 메서드 : 기대처럼 작동 안한다.
          (1) 참조된 메서드 println 다중 정의 : 부정확한 메서드 참조
          (2) 호출한 메서드 submit 다중 정의 : Callable<T> 를 받는 메서드가 있음
       2) 해결법
          (1) 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받지 마라
          - Xlint:overloads를 지정해라. : 다중 정의를 경고해줌
   5.3 근본적 다른 타입
   5.4 다중 정의 메서드로 구분하기 어려워짐
      1) 공통 인터페이스 CharSequence -> contentEqual 메서드가 다중 정의 됨
       2) 실패한 다중 정의 메서드

 

1. 다중 메서드(오버로딩) 오류 예시

원래 의도: 매개변수의 런타임 타입에 기초해 -> 다중 정의 메서드로 자동 분배

 

public class CollectionClassifier {
    public static String classify(Set<?> s) {
        return "Set";
    }

    public static String classify(List<?> lst) {
        return "List";
    }

    public static String classify(Collection<?> c) {
        return "Unknown Collection";
    }

    public static void main(String[] args) {
        Collection<?>[] collections = {
                new HashSet<>(),
                new ArrayList<>(),
                new HashMap<String, String>().values()
        };

        for (Collection<?> c : collections)
            System.out.println(classify(c));
    }
}

잘 동작할 것 같은 코드이다. 하지만 이 프로그램은 "그 외"만 3번 출력한다. 다중정의된 classify() 메서드는 컴파일 타임에 어떤 메서드가 호출될 것인지 정해지기 때문이다.

 

for문 안에 Collection<?> c는 런타임에 타입이 결정되고 달라진다. 즉, 컴파일 타임에는 항상 Collection <?> 타입이라는 것이다.

 

컴파일 타임에 어떤 classify() 메서드가 호출될 것인지 결정되기 때문에  classify(Collection<?>) 메서드만 3번 호출되는 것이다.

2. 재정의 메서드 오류 예시

class Wine {
    String name() { return "포도주"; }
}

class SparklingWine extends Wine {
    @Override String name() { return "발포성 포도주"; }
}

class Champagne extends SparklingWine {
    @Override String name() { return "샴페인"; }
}

public class Overriding {
    public static void main(String[] args) {
        List<Wine> wineList = List.of(
                new Wine(), new SparklingWine(), new Champagne());

        for (Wine wine : wineList)
            System.out.println(wine.name());
    }
}

위 코드를 실행하면 "포도주", "발포성 포도주", "샴페인"을 차례대로 출력한다. for문에서의 컴파일 타임 타입이 모두 Wine인 것에 무관하게 항상 가장 하위에서 정의한 재정의 메서드가 실행되는 것이다. 다중정의 예제에서 의도했던 것처럼 직관과 일치한다.

 

 

3. (1번 코드 수정) classfy 메서드를 하나로 합쳐라. 그리고 분류하라.

public static String classify(Collection<?> c) {
    return c instanceof Set  ? "집합" :
            c instanceof List ? "리스트" : "그 외";
}

 

4. 다중 정의가 혼동을 일으키는 상황을 피해라

  • API 사용자가 매개변수를 넘길 때, 어떤 다중정의 메서드가 호출될지 모른다면 프로그램은 오작동하기 쉽다.
  • 헷갈릴 수 있는 코드는 작성하지 말자. (위의 다중정의 예시처럼)
  • 안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말자.
  • 가변 인수를 매개변수로 사용한다면 다중정의는 사용하면 안 된다.

이 규칙들만 잘 따르면 다중정의가 혼동을 일으키는 일을 피할 수 있다. 이 외에 다중정의하는 대신 메서드 이름을 다르게 지어주는 방법도 존재한다.

  4.1 매개변수 수가 같은 다중 정의는 만들지 말자.

다중정의를 사용하지 않고, 메서드 이름을 다르게 사용하면 사용할때 어떤거를 불러올지 짐작 하기 쉽다

    ex 1) ObjectOutputStream write 메서드

....
    public void writeBoolean(boolean val) throws IOException {
        bout.writeBoolean(val);
    }

    /**
     * Writes an 8 bit byte.
     *
     * @param   val the byte value to be written
     * @throws  IOException if I/O errors occur while writing to the underlying
     *          stream
     */
    public void writeByte(int val) throws IOException  {
        bout.writeByte(val);
    }

    /**
     * Writes a 16 bit short.
     *
     * @param   val the short value to be written
     * @throws  IOException if I/O errors occur while writing to the underlying
     *          stream
     */
    public void writeShort(int val)  throws IOException {
        bout.writeShort(val);
    }

    /**
     * Writes a 16 bit char.
     *
     * @param   val the char value to be written
     * @throws  IOException if I/O errors occur while writing to the underlying
     *          stream
     */
    public void writeChar(int val)  throws IOException {
        bout.writeChar(val);
    }

    /**
     * Writes a 32 bit int.
     *
     * @param   val the integer value to be written
     * @throws  IOException if I/O errors occur while writing to the underlying
     *          stream
     */
    public void writeInt(int val)  throws IOException {
        bout.writeInt(val);
    }

    /**
     * Writes a 64 bit long.
     *
     * @param   val the long value to be written
     * @throws  IOException if I/O errors occur while writing to the underlying
     *          stream
     */
    public void writeLong(long val)  throws IOException {
        bout.writeLong(val);
    }

    /**
     * Writes a 32 bit float.
     *
     * @param   val the float value to be written
     * @throws  IOException if I/O errors occur while writing to the underlying
     *          stream
     */
    public void writeFloat(float val) throws IOException {
        bout.writeFloat(val);
    }
....

    ex 2) ObjectInputStream read 메서드

 

 4.2 매개변수 수가 같음, 근본적으로 매개변수가 다르다면 괜찮다.

ex) ArrayList 

 int 받는 생성자, Collection을 받는 생성자 는 호출되어도 헷갈릴 일이 없다.

 

5. 평화의 시대 끝 : 오토박싱, 제네릭, 람다, 메서드 참조

5.1 오토박싱, 제네릭 등장 -> 근본적으로 다르지 않게 됨

 

 1) List의 remove 메서드의 오류 예제

 

import java.util.*;

// 이 프로그램은 무엇을 출력할까? (315-316쪽)
public class SetList {
    public static void main(String[] args) {
        Set<Integer> set = new TreeSet<>();
        List<Integer> list = new ArrayList<>();

        for (int i = -3; i < 3; i++) {
            set.add(i);
            list.add(i);
        }
        for (int i = 0; i < 3; i++) {
            set.remove(i);
            list.remove(i);
        }
        System.out.println(set + " " + list);
    }
}

 

[-3, -2, -1, 0, 1, 2] 의 값에서 [0, 1, 2]를 지우니까 [-3, -2, -1]이 출력을 예상한다.

[-3, -2, -1] [-2, 0, 2]

Process finished with exit code 0

 

성공
실패

원인: List<E>인터페이스가 remove(Object)와 remove(int)를 다중정의 했다 :  매개변수 타입이 다르지 않게 작동함.

 

다른 결과가 출력된다. 왜 [-2, 0, 2]가 출력될까? 그 이유는 List의 remove가 다중정의되있기 때문이다.

 

 

해결법

list.remove((Integer) i);

 

 

 

 

 

 

5.2 람다와 메서드 참조 등장 -> 부정확한 메서드 참조

 1) submit 다중 정의 메서드 : 기대처럼 작동 안한다.

// 1번. Thread의 생성자 호출
new Thread(System.out::println).start();

// 2번. ExecutorService의 submit 메서드 호출
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);

2번은 컴파일 오류가 발생한다.

submit 다중 정의 메서드 중에는 Callable<T> 를 받는 메서드도 있고,

void를 반환하는 println 의 경우 정상적으로 작동할거라고 예상했겠지만, println도 다중정의 되어있는 메서드기 때문에 우리의 예상대로 동작하지 않는다. 

(1) 참조된 메서드 println 다중 정의 : 부정확한 메서드 참조

...
    public void println() {
        newLine();
    }

    /**
     * Prints a boolean and then terminate the line.  This method behaves as
     * though it invokes {@link #print(boolean)} and then
     * {@link #println()}.
     *
     * @param x  The {@code boolean} to be printed
     */
    public void println(boolean x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

    /**
     * Prints a character and then terminate the line.  This method behaves as
     * though it invokes {@link #print(char)} and then
     * {@link #println()}.
     *
     * @param x  The {@code char} to be printed.
     */
    public void println(char x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
...

(2) 호출한 메서드 submit 다중 정의 : Callable<T> 를 받는 메서드가 있음

 

※Runnable,Callable 차이
Runnable: 어떤 객체도 리턴하지 않습니다. Exception을 발생시키지 않습니다.
Callable: 특정 타입의 객체를 리턴합니다. Exception을 발생킬 수 있습니다.

2) 해결법

 (1) 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받지 마라

  - Xlint:overloads를 지정해라. : 다중 정의를 경고해줌

 

 

5.3 근본적 다른 타입

클래스 타입

배열 타입

  • Object 외의 클래스 타입과 배열 타입은 근본적으로 다름
  • Serializable과 Cloneable 외의 인터페이스 타입과 배열 타입도 근본적으로 다름
  • String과 Throwable처럼 상위/하위 관계가 아닌 두 클래스는 '관련 없다(unrelated)' 라고 함

 

5.4 다중 정의 메서드로 구분하기 어려워짐

   1) 공통 인터페이스 CharSequence -> contentEqual 메서드가 다중 정의 됨.

   Stringbuffer, StringBuilder, String, CharBuffer

   이 두 메서드는 같은 객체를 입력하면 완전히 '같은 작업'을 수행해줌.

   어떤 다중 정의 메서드가 불려지는지 몰라도 '결과가 같다면 다행'임.

  • 자바 5에서 StringBuffer, StringBuilder, String, CharBuffer 등의 비슷한 부류의 타입을 위한 공통 인터페이스로 CharSequence가 등장함
  • 그 결과 String에도 CharSequence를 받은 contentEquals가 다중정의 됨
    • but, 두 메소드는 완전히 같은 작업을 수행하기 때문에 해로울 건 없음
    • 이런 경우 인수를 포워드하여 두 메소드가 동일한 일을 하도록 보장함
    public boolean contentEquals(StringBuffer sb) {
        return contentEquals((CharSequence) sb);
    }

 

   2) 실패한 다중 정의 메서드

   String 클래스 valueOf(char[]) valueOf(Object) 같은 객체를 건내도 다른 일을 수행한다.

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

public static String valueOf(char data[]) {
    return new String(data);
}

'java > 이펙티브 자바' 카테고리의 다른 글

[item 69] 예외는 진짜 예외 상황에만 사용하라  (0) 2022.02.06
[item 65] 리프렉션보다는 인터페이스를 사용하라  (0) 2022.01.30
item 43~46  (0) 2022.01.16
Item29~30  (0) 2022.01.09
[item 21~22]  (0) 2022.01.01