본문 바로가기
개발 공부/Java

이펙티브 자바 아이템 20 - 추상 클래스보다 인터페이스를 우선하라 - 완벽 공략

by 개발인생 2022. 11. 29.
반응형

아이템 20 - 추상 클래스보다 인터페이스를 우선하라 - 완벽 공략

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

템플릿 메서드 패턴

템플릿 메서드 패턴은 상속을 사용하는 대표적인 디자인 패턴 중 하나이다.

알고리즘의 구조를 서브 클래스가 확장할 수 있도록 템플릿으로 제공하는 방법이다.

상속을 사용해 템플릿 메서드의 일부분을 확장할 수 있다.

public abstract class FileProcessor {

    private String path;

    public FileProcessor(String path) {
        this.path = path;
    }

    public final int process() {
        try(BufferedReader reader = new BufferedReader(new FileReader(path))) {
            int result = 0;
            String line = null;
            while((line = reader.readLine()) != null) {
                result = getResult(result, Integer.parseInt(line));
            }
            return result;
        } catch (IOException e) {
            throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
        }
    }

    protected abstract int getResult(int result, int number);

}

어떤 경로에 해당하는 파일을 한줄씩 읽으며 어떤 오퍼레이션을 수행하는 역할을 하는 클래스를 작성한다.

위의 클래스에서 process 메서드는 템플릿 메서드 에 해당한다.

getResult 메서드는 템플릿 메서드 패턴의 Step1 에 해당한다.

getResult 메서드는 서브 클래스가 확장 할 수 있도록 만들어 둔다.

public class Plus extends FileProcessor {
    public Plus(String path) {
        super(path);
    }

    @Override
    protected int getResult(int result, int number) {
        return result + number;
    }

}

서브 클래스에서 getResult 메서드를 재정의해서 확장한다.

public class Client {

    public static void main(String[] args) {
        FileProcessor fileProcessor = new Plus("number.txt");
        System.out.println(fileProcessor.process());
    }
}

클라이언트 코드에서는 위와같이 사용한다.

위의 코드에서는 상속을 사용해 기능을 구현한다.

getResult 메서드는 상속을 사용하지 않고 기능을 확장하는 방법이 있다.

이 방법을 템플릿 콜백 이라고 부른다.

public class FileProcessorCallBack {

    private String path;

    public FileProcessorCallBack(String path) {
        this.path = path;
    }

    public final int process(BiFunction<Integer, Integer, Integer> operator) {
        try(BufferedReader reader = new BufferedReader(new FileReader(path))) {
            int result = 0;
            String line = null;
            while((line = reader.readLine()) != null) {
                result = operator.apply(result, Integer.parseInt(line));
            }
            return result;
        } catch (IOException e) {
            throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
        }
    }
}

process 의 인자로 BiFunction 이라는 함수형 인터페이스를 선언한다.

BiFunction 은 두 개의 인자를 받아 한개의 인자를 리턴하는 함수형 인터페이스이다.

process 메서드 내에서 인자로 전달받은 BiFunction 을 사용한다.

public class Client {

    public static void main(String[] args) {
        FileProcessorCallBack fileProcessorCallBack = new FileProcessorCallBack("number.txt");
        System.out.println(fileProcessorCallBack.process(Integer::sum));
    }
}

클라이언트 코드에서는 위와 같이 람다식이나 메서드 레퍼런스를 사용해 process 메서드를 실행한다.

이렇게 오퍼레이터를 전달받아 로직을 처리하면 상속을 사용하지 않고도 확장이 가능하도록 할 수 있다.

디폴트 메서드는 equals, hashCode, toString 같은 Object 메서드를 재 정의할 수 없다.

public interface MyInterface {

    default String toString() {
        return "myString";
    }

    default int hashCode() {
        return 10;
    }

    default boolean equals(Object o) {
        return true;
    }

}

인터페이스에 위와같이 default 매서드를 사용해 Object 메서드를 정의할 때 컴파일 에러가 난다.

왜 인터페이스의 default 매서드로 Object 메서드를 정의할 수 없게 만들었을까?

  • 디폴트 메서드의 용도가 아니다.

디폴트 메서드의 용도는 어떠한 메서드의 진화 와 관련이 있다.

메서드에 새로운 기능을 추가할 때 기존의 인터페이스를 구현한 클래스를 그대로 유지하면서

아주 간단한 추가 기능을 넣어줄 수 있게 제공한 것이 인터페이스의 default 매서드이다.

설계에 굉장히 큰 변화나 위험을 가져올 변화를 넣기위한 용도가 아니다.

default 매서드의 핵심적인 목표는 인터페이스의 진화 이다.

  • 복잡도 증가.

자바 프로그램에서 메서드를 선택할 때 어떤 메서드를 사용할 것인지에 대한 규칙 2가지가 있다.

클래스가 인터페이스를 이긴다

인터페이스는 어디까지나 선언이고 인터페이스에 작성한 default 매서드 역시 클래스에서 오버라이딩 이 가능하다.

클래스에 있는 메서드가 우선순위가 높다.

더 구체적인 인터페이스가 이긴다.

인터페이스도 상속이 가능하기 때문에 서브 인터페이스에서 재정의한 메서드가 우선순위가 높다.

public interface MyInterface {

    default String toString() {
        return "myString";
    }

    default int hashCode() {
        return 10;
    }

    default boolean equals(Object o) {
        return true;
    }

}
public class MyClass extends Object implements MyInterface {
}

만약 위의 클래스가 있다면 어디서 정의한 equals 메서드를 사용해야할까?

만약 인터페이스에서 정의한 equals 메서드를 사용하면

클래스가 인터페이스를 이긴다 라는 원칙이 깨지게 된다.

이러한 경우는 필요 이상으로 복잡도 를 증가시키기 때문에

인터페이스 default 매서드로 Object 메서드를 정의할 수 없게 만들었다.

게다가 굳이 인터페이스에 equals 메서드나 hashCode 메서드를 정의하는건 의미가 없다.

  • 불안정하다.

인터페이스를 구현하는 클래스에서 equals 메서드나 hashCode 메서드를 정의했지만

누군가 인터페이스에서 default 매서드로 Object 메서드를 정의하게 되면 클래스에서 정의한

equals 메서드나 hashCode 메서드의 동작이 깨지게 된다.

게다가 default 매서드로 Object 메서드를 정의한 인터페이스가 상속 구조를 타고 찾기 어려운 곳에 추가되는 경우도 있을 수 있다.


이러한 이유들로해서 인터페이스에서 default 매서드로 Object 메서드를 정의할 수 없도록 만들었다.

반응형

댓글