ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 아이템 20. 추상 클래스보다는 인터페이스를 우선하라
    Study/Effective Java 2023. 8. 3. 15:06

    자바 8부터 인터페이스도 디폴트 메서드(default method)를 제공할 수 있다.

    자바가 제공하는 다중 구현 메커니즘은 인터페이스, 추상 클래스 이렇게 두 가지다.
    자바 8부터 인터페이스도 디폴트 메서드(default method)를 제공할 수 있게 되어 이제는 두 메커니즘 모두 인스턴스 메서드를 구현 형태로 제공할 수 있다.

    public interface TimeClient {
        void setTime(int hour, int minute, int second);
        void setDate(int day, int month, int year);
        void setDateAndTime(int day, int month, int year,
                            int hour, int minute, int second);
        LocalDateTime getLocalDateTime();
    
        static ZoneId getZonedId(String zoneString) {
            try {
                return ZoneId.of(zoneString);
            } catch (DateTimeException e) {
                System.err.println("Invalid time zone: " + zoneString + "; using default time zone instead.");
                return ZoneId.systemDefault();
            }
        }
    
        default ZonedDateTime getZonedDateTime(String zoneString) {
            return ZonedDateTime.of(getLocalDateTime(), getZonedId(zoneString));
        } // 인터페이스를 진화시킬 수 있다.
    
    }

    구현이 명백한 것은 인터페이스의 디폴트 메서드를 사용해 프로그래머의 일감을 덜어줄 수 있다.

    추상클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다. 자바는 단일 상속만을 지원하므로 추상클래스 방식은 새로운 타입을 정의하는 데 커다란 제약을 안게 된다.

     

    기존 클래스에도 손쉽게 새로운 인터페이스를 구현해넣을 수 있다.

    인터페이스는 클래스 선언에 implements 구문만 추가하면 끝이다.

    반면 기존 클래스 위에 새로운 추상 클래스를 끼워넣기는 어려운 게 일반적이다. 두 클래스가 같은 추상 클래스를 확장하길 원한다면, 그 추상 클래스는 계층구조상 두 클래스의 공통 조상이어야 한다.

     

    인터페이스는 믹스인(mixin)정의에 안성맞춤이다.

    원래의 '주된 타입'외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다.

    public class SimpleTimeClient implements TimeClient, Comparable {
        // ... 코드생략
        @Override
        public int compareTo(Object o) {
            // ... 코드생략
        }
    }

    Comparable은 자신을 구현한 클래스의 인스턴스끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스다. 대상 타입의 주된 기능에 선택적 기능을 '혼합(mixed in)' 한다고 해서 믹스인이라 부른다.

     

    인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.

    타입을 계층적으로 정의하면 수많은 개념을 구조적으로 잘 표현할 수 있지만, 현실에는 계층을 엄격히 구분하기 어려운 개념도 있다.

    public interface SingerSongwriter extends Singer, Songwriter { // Singer, Songwriter <= 계층구조가 없음.
        AudioClip strum();
        void actSensitive();
    }

     

    래퍼 클래스(아이템 18)와 함께 사용하면 인터페이스는 기능을 향상 시키는 안전하고 강력한 수단이 된다.

    추상클래스로 정의해두면 그 타입에 기능을 추가하는 방법은 상속뿐이다. 상속해서 만든 클래스는 래퍼 클래스보다 활용도가 떨어지고 깨지기는 더 쉽다.

     

    인터페이스와 추상 골격 구현(skeletal implementation) 클래스를 함께 제공하는 식으로 인터페이스와 추상 클래스의 장점을 모두 취할 수 있다.

    인터페이스 - 타입 정의, 디폴트 메서드 구현
    추상 골격 클래스 - 나머지 메서드 구현

    단순히 골격 구현을 확장하는 것만으로 이 인터페이스를 구현하는 데 필요한 일이 대부분 완료된다. => 템플릿 메서드 패턴

    관례상 인터페이스 이름이 Interface라면 그 골격 구현 클래스의 이름은 AbstractInterface로 짓는다. 컬렉션 프레임워크의 AbstractCollection, AbstractSet, AbstractList, AbstractMap 각각이 바로 핵심 컬렉션 인터페이스의 골격 구현이다.

    public class IntArrays {
        static List<Integer> intArrayAsList(int[] a) { // int 배열을 받아 Integer 인스턴스의 리스트 형태로 보여주는 어댑터(Adapter)이기도 하다.
            Objects.requireNonNull(a);
    
            // 다이아몬드 연산자를 이렇게 사용하는 건 자바 9부터 가능하다.
            // 더 낮은 버전을 사용한다면 <Integer>로 수정하자.
            return new AbstractList<>() {
                @Override public Integer get(int i) {
                    return a[i];  // 오토박싱(아이템 6)
                }
    
                @Override public Integer set(int i, Integer val) {
                    int oldVal = a[i];
                    a[i] = val;     // 오토언박싱
                    return oldVal;  // 오토박싱
                }
    
                @Override public int size() {
                    return a.length;
                }
            };
        }
    }

    만약 AbstractList를 사용하지 않고 List를 쓰려고 했다면, 이렇게나 많은 메서드를 구현해야한다. Skeleton 역할을 하는 AbstractList를 사용하여 아주 일부분만 재정의할 수 있다.

     

    시뮬레이트한 다중 상속 (simulated multiple inheritance)

    인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 private 내부 클래스를 정의하고, 각 메서드 호출을 내부 클래스의 인스턴스에 전달한다. 래퍼 클래스와 비슷한 이 방식은 다중 상속의 많은 장점을 제공하는 동시에 단점은 피하게 해준다.

    public abstract class AbstractCat {
    
        protected abstract String sound();
    
        protected abstract String name();
    }
    public class MyCat extends AbstractCat {
        @Override
        protected String sound() {
            return "인싸 고양이 두 마리가 나가신다!";
        }
    
        @Override
        protected String name() {
            return "유미";
        }
    
        public static void main(String[] args) {
            MyCat myCat = new MyCat();
            System.out.println(myCat.sound());
            System.out.println(myCat.name());
        }
    }

    AbstractCat을 상속받은 MyCat이 있다.

    ▼ Flyable interface

    public interface Flyable {
        void fly();
    }

    ▼ AbstractFlyable

    public class AbstractFlyable implements Flyable {
        @Override
        public void fly() {
            System.out.println("너랑 딱 붙어있을게!");
        }
    }

    MyCat 클래스가 이미 AbstractCat을 상속받고 있기 때문에, AbstractFlyable 을 추가로 상속 받지 못한다.

    public class MyCat extends AbstractCat implements Flyable {
    
        private MyFlyable myFlyable = new MyFlyable();
    
        @Override
        protected String sound() {
            return "인싸 고양이 두 마리가 나가신다!";
        }
    
        @Override
        protected String name() {
            return "유미";
        }
    
        public static void main(String[] args) {
            MyCat myCat = new MyCat();
            System.out.println(myCat.sound());
            System.out.println(myCat.name());
            myCat.fly();
        }
    
        @Override
        public void fly() {
            this.myFlyable.fly();
        }
    
        private class MyFlyable extends AbstractFlyable {
            @Override
            public void fly() {
                System.out.println("날아라.");
            }
        }
    }

    1) Flyable interface를 구현하고 2) private class를 AbstractFlyable을 상속을 받는다. 3) Flyable interface 를 구현한 메서드에, 선언한 private MyFlyable myFlyable 필드의 메서드를 위임하면 마치 상속을 한 것처럼 사용할 수 있다.

     

    템플릿 메서드 패턴

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

    Abstract class

    public abstract class FileProcessor {
        private String path;
    
        public FileProcessor(String path) {
            this.path = path;
        }
    
        public final int process() { // template method
            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);
    
    }

     Concrete class

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

    client

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

     

    템플릿 콜백 패턴

    Plus 클래스는 FileProcessor를 상속받았다. 상속을 사용하지 않고도 확장할 수 있다.

    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);
            }
        }
    
    //    protected abstract int getResult(int result, int number);
    
    }

    BiFunction은 두 개의 인자를 받아서 하나의 인자값을 돌려주는 오퍼레이션을 정의할 수 있는 함수형 인터페이스이다. BiFunction을 인자로 받으면서 FileProcessor를 추상클래스가 아닌 클래스로 정의한다.

    public class Client {
        public static void main(String[] args) {
            FileProcessor fileProcessor = new FileProcessor("number.txt");
            System.out.println(fileProcessor.process((a, b) -> a + b));
            System.out.println(fileProcessor.process(Integer::sum)); // method reference
        }
    }

    Client에서 FileProcessor의 processor를 호출하며 (a, b) -> a + b를 넘겨주면 된다. 이것을 method reference로 간추릴 수도 있다.

     

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

    Object 클래스를 오버라이딩하려고 하면 "Default method 'toString' overrides a memeber of 'java.lang.Object'" 컴파일 에러가 발생한다.

    왜 자바 개발자들은 오버라이딩을 막았을까?

    1) The key goal of adding default methods to Java was "interface evolution", not "poor man's traits."

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

    2) Adds complexity.

    복잡도를 증가시킨다.

    Rule #1. Classes win over interfaces.
    => 클래스가 인터페이스를 이긴다.
    Rule #2. More specific interfaces win over less specific ones (where specificity means "subtyping").
    => 더 구체적인 인터페이스가 덜 구체적인 인터페이스를 이긴다.

    만약 interface의 default메서드에서 Object 메서드를 오버라이딩하게되면 이 두가지 규칙 중 무엇을 따라야할 지 복잡해진다.

    3) Really only makes sense in toy examples.

    사실상 toy project에서나 필요한 기능이다. toString, hashCode, equals 기능은 데이터가 있어야 유의미한 기능이라 인터페이스에서 재정의했을 때 의미가 없다.

    4) It's brittle.

    깨지기 쉽다. 갑자기 구현하고 있는 Interface에서 Object 메서드들을 오버라이딩했을 때, 클래스의 동작이 바뀌게 된다.

    댓글

Designed by Tistory.