Effective Java : 아이템20. 완벽 공략

    들어가기 전

    이 글은 인프런 백기선님의 강의를 복습하며 작성한 글입니다. 


    이 글의 요약

    • 추상 클래스를 이용해서 Template 메서드 패턴을 만들 수 있고, 이를 이용해서 구현체마다 다른 비즈니스 로직 결과를 보여줄 수 있다.
    • Template Method 패턴은 상속 기반이기 때문에 상속의 단점을 고스란히 가진다.
    • Template CallBack 패턴은 Template Method 패턴의 장점을 가지면서, 좀 더 느슨한 의존관계를 가진다
    • 인터페이스의 default 메서드에 Object 클래스의 toString(), equals(), hashCode()를 재정의 할 수 없다. 

    완벽 공략

    • p132, 템플릿 메서드 패턴
    • p135, 디폴트 메서드는 equals, hashCode, toString 같은 Object 메서드를 재정의 할 수 없기 때문이다. 

     


    완벽 공략 36. 템플릿 메서드 패턴 (그림 붙이기)

    추상 클래스를 이용하면 템플릿 메서드 패턴을 구현해서 특정 인터페이스의 구현체를 좀 더 쉽게 구현할 수 있도록 도움을 준다. 템플릿 메서드 패턴의 의미는 아래와 같다.

    • 알고리즘 구조(인터페이스)를 서브 클래스가 확장할 수 있도록 템플릿으로 제공하는 방법
    • 추상 클래스는 템플릿을 제공하고, 하위 클래스는 구체적인 알고리즘을 제공한다. 

    템플릿 메서드 패턴은 상속을 사용하는 대표적인 디자인 패턴이다. 템플릿에는 메서드에 정의만 되어있다. 상속을 사용해서 템플릿 메서드의 일부분을 확장해서 사용할 수 있도록 한다. 위의 그림을 살펴보자. 

    • templateMethod() 내부에서는 step1(), step2()를 호출해서 비즈니스 로직(알고리즘)을 처리한다. templateMethod를 통해 템플릿 메서드 패턴은 알고리즘을 제공하는 것이다. 
    • step1(), step2()는 추상 메서드다. 
    • templateMethod()는 재정의 할 수 있는 메서드가 아니다. (final로 선언)

    이런 구조를 가진다면, 이 추상 클래스를 상속받은 구현체는 step1(), step2()만 재정의해서 추상 클래스가 제공하는 templateMethod()의 비즈니스 로직을 그대로 사용할 수 있게 된다. 아래 그림과 같다.

    FileProcessor라는 예제 코드를 살펴보자. FileProcessor에서는 각 메서드가 이렇게 대응된다. 

    • process() → templateMethod()에 대응
    • getResult() → Step()에 대응

    Process() 메서드는 비즈니스 로직을 제공하고, 이 비즈니스 로직 안에는 getResult()라는 메서드가 포함되어있다. getResult()는 추상 클래스이며, 하위 클래스가 어떻게 구현하느냐에 따라 비즈니스 로직의 결과가 달라질 수 있다.

    public abstract class FileProcessor {
    
        private String path;
    
        public FileProcessor(String path) {
            this.path = path;
        }
    
    	// TemplateMethod에 해당
        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);
            }
        }
    
    	// Step에 해당. 하위 클래스에서 구현해서 TemplateMethod에서 사용함.
        protected abstract int getResult(int result, int number);
    
    }

    FileProcessor의 구현체 중 하나인 Plus다. Plus는 getResult() 메서드에서 값을 더하는 연산을 제공한다. 그 결과 process() 메서드가 호출되면, 더한 값을 로컬 파일에다가 쓰기한다. 만약 Plus 대신 Minus를 구현한다면, getResult() 메서드에서 값을 빼는 연산을 제공하고, process()는 뺀 값을 로컬 파일에다가 작성할 것이다. 

    public class Plus extends FileProcessor{
    
        public Plus(String path) {
            super(path);
        }
    
        @Override
        protected int getResult(int result, int number) {
            return result + number;
        }
    }
    

     

    단점

    • Template Method 패턴은 상속을 기반으로 한 디자인 패턴이다. 따라서 상속이 가지고 있는 단점을 그대로 가져온다. 아이템 19에서 볼 수 있듯이 상속을 제대로 사용하기 위해서는 많은 것들이 선행되어야 하기 때문이다. 
    • 이것을 개선한 패턴이 Template Callback 패턴이다. 

    Template Callback 패턴

    Template Callback 패턴은 Template Method를 정의해두고, Template Method에 함수형 인터페이스를 전달하는 형태로 구현된다. 함수형 인터페이스마다 Template Method 비즈니스 로직이 변화된다. 다형성(?)을 제공하지만, 상속을 기반으로 한 것이 아니기 때문에 보다 안전하게 확장 가능해진다. 

    // Template Callback 패턴
    public class FileProcessor {
    
        private String path;
    
        public FileProcessor(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 타입의 Operator를 전달받는다. 
    • process() 메서드 내부에서 operator.apply() 메서드를 호출해서 메서드의 실행 결과가 result에 저장된다. 

    BiFunction은 자바에서 제공해주는 함수형 인터페이스고, 입력 두 개를 받아서 하나의 결과값만 반환하면 된다. 상속 기반으로 동작하지 않고, 함수만 넘겨주는 형태이기 때문에 보다 클래스의 의존성이 줄어들게 된다. 

    @FunctionalInterface
    public interface BiFunction<T, U, R> {
        R apply(T t, U u);
    }

    Template Callback 패턴을 사용할 때는 아래와 같이 사용할 수 있다. 함수형 인터페이스를 정의하고, 그 함수를 그냥 넘겨주시만 하면 된다. 

    public class Client {
        public static void main(String[] args) {
            FileProcessor fileProcessor = new FileProcessor("number.txt");
            System.out.println(fileProcessor.process(plus));
            System.out.println(fileProcessor.process(minus));
            System.out.println(fileProcessor.process(multiple));
        }
    
        public static BiFunction<Integer, Integer, Integer> plus = (a, b) -> a + b;
        public static BiFunction<Integer, Integer, Integer> minus = (a, b) -> a - b;
        public static BiFunction<Integer, Integer, Integer> multiple = (a, b) -> a * b;
    }
    

    그림으로 살펴보면 다음과 같이 동작하는 것으로 이해할 수 있다.


    완벽 공략 37. 인터페이스의 default 메서드와 Object 메서드

    • 인터페이스의 default 메서드로 Object 메서드를 재정의 할 수 없는 이유
    • default 메서드 핵심 목적은 "인터페이스의 진화".
    • 자바는 두 가지 규칙만 유지한다. (메서드를 읽을 때)
      • 클래스가 인터페이스를 이긴다
      • 더 구체적인 인터페이스가 이긴다. 

     

    default 메서드와 Object 메서드

    default 메서드로 object의 toString, hashCode,  equals를 재정의하려고 하면 컴파일 에러가 발생한다. 컴파일 에러가 발생한다는 것은 자바의 문법을 위반한 것이다. 왜 자바의 문법은 default 메서드에서 toString, hashCode, equals를 재정의 할 때 문제가 발생하는 것일까? 

     

    default 메서드의 용도가 아니다. 

    default 메서드의 용도는 어떤 인터페이스의 진화와 관련이 있다. 인터페이스에 새로운 기능을 추가할 때, 인터페이스의 모든 구현체를 유지하면서도 간단하게 추가 기능을 넣어줄 목적이다. 반면, 결정적으로 toString, hashCode, equals를 default 메서드로 구현하면 구현체에 큰 영향을 미친다. 그리고 구현체마다 HashCode가 같은 것은 말이 안된다. 

     

    자바 프로그램에서 메서드를 선택할 때는 두 가지 규칙만 유지함. 

    • 클래스가 인터페이스보다 우선된다. 
      • 클래스에 재정의한 default 메서드가 무조건 이긴다.
    • 더 구체적인 인터페이스가 우선된다. 
      • 서브 인터페이스(인터페이스를 상속한 인터페이스)에서 재정의 한 default 메서드가 더 우선시 된다.

    위 두 가지 규칙만 적용한다. 그런데 만약 toString()을 인터페이스의 default 메서드에서 재정의한다고 가정해보자. 

    • toString()은 Object 클래스의 메서드다. 즉, 클래스에서 구현된 메서드다.
    • Interface에서 default 메서드로 Object 클래스를 재정의하면, 인터페이스 메서드가 된다. 그런데 좀 더 구체적이다.

    이렇게 된다면, 클래스가 인터페이스보다 우선시 되지만, 인터페이스의 default 메서드는 클래스보다 더 구체적이다. 그렇다면 자바 컴파일러는 무엇을 선택해야할까? 굉장히 복잡한 선택이 되기 때문에 문법적으로 막아둔 것이다. 

    댓글

    Designed by JB FACTORY