Effective Java : 아이템 64. 객체는 인터페이스를 사용해 참조하라.

    아이템 64. 객체는 인터페이스를 사용해 참조하라.

    • 적합한 인터페이스만 있다면 매개변수, 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언. 
      • 구현체만 간단히 갈아끼울 수 있기 때문에 코드 변화가 좀 더 유연하다. 
    • 클래스 타입을 사용해도 되는 경우.
      • 적합한 인터페이스가 없는 경우. 예를 들면 record 같은 값 클래스. 
      • 클래스 기반으로 작성된 프레임워크가 제공하는 객체들. 예를 들면 OutputStream 같은 것들. 
      • 인터페이스에 없는 특별한 메서드를 제공하는 클래스
        • 이런 메서드를 너무 많이 사용할 경우, 구현체를 바꾸는 것이 어려워짐. 가급적이면 덜 의존하도록 해야함.
    • 적합한 인터페이스가 없다면, 클래스의 계층구조 중 필요한 기능을 만족하는 가장 추상적인(상위) 클래스를 사용하자.

     


    코드의 모든 곳을 가능하다면 인터페이스로 선언하자. 

    인터페이스를 사용하는 가장 큰 이유는 변화에 좀 더 유연한 코드를 작성할 수 있기 때문이다. 예를 들어 인터페이스를 사용하는 상태에서 구현체를 바꾸고자 한다면 생성자 메서드만 갈아끼우면 될 정도로 간편하다. 반면 구현체를 직접 사용하고 있던 상황이었다면 아무래도 영향받는 범위가 넓어질 가능성이 있다. 

    public static void main(String[] args) {
        // Good example. use interface.
        Set<String> stringset = new LinkedHashSet<>();
        
        // Bad Example. use concrete class.
        LinkedHashSet<String> stringLinkedSet = new LinkedHashSet<>();
    }

    위의 코드에서 인터페이스 / 구현체를 직접 사용하는 케이스를 둘다 표기했다. 만약 구현체를 LinkedHashSet을 사용하다가 TreeSet으로 바꾸는 경우에는 각각 어떻게 동작할까? 

    • 인터페이스의 경우 생성자만 new TreeSet()으로 바꿔주면 됨. 
    • 클래스를 직접 사용하는 경우 생성자 / 클래스 선언부까지 모두 바꿔줘야함. 

    단적인 예시에서도 이렇게 볼 수 있지만, 만약 매개변수에서 구현체를 받을 경우에는 타입 캐스팅을 한다거나 하는 작업이 추가로 필요할 수도 있다. 이처럼 매개변수 / 변수 / 반환값으로 인터페이스를 이용하면 변화에 좀 더 유연한 코드가 된다. 


    선언과 구현 타입 같이 바꾸면 되는거 아님? → 문제있음. 

    인터페이스를 사용하지 않더라도 그냥 단순히 선언 / 구현 타입을 함께 바꿔주면 되는게 아니냐? 라고 생각할 수도 있다. 물론 그렇게 해서 잘 동작할 수도 있지만, 넓게 봤을 때는 잘못된 판단이 될 수 있다. 

    클라이언트가 클래스의 특정 메서드를 참조하는 경우가 있을 수 있다. 클라이언트가 A 구현체에서는 호출하던 메서드가 B 구현체에서는 지원되지 않는 경우가 있을 수 있다. 만약 구현체로만 코드를 작성했다면  이 경우 구현체를 선언 / 구현부분을 같이 갈아끼우면서 컴파일 에러가 발생할 것이다. 

    public class UseInterface2 {
        
        static interface MyInterface { void sayHello(); }
        
        static class MyConcreteInterface implements MyInterface{
            @Override public void sayHello() {}
            public void hello() {}
        }
    
        static class MyOtherConcreteInterface implements MyInterface{
            @Override public void sayHello() { }
        }
    
        public static void main(String[] args) {
            
            // 만약 MyOtherConcretInterface로 바꾸면? 
            MyConcreteInterface myConcreteInterface = new MyConcreteInterface();
            myConcreteInterface.hello();
        }
    }

    위 코드에서 예시를 볼 수 있다. 현재 클라이언트는 MyConcreteInterface라는 구현체 클래스를 직접 선언부분에서 사용하고 있다. 그리고 이 클래스만 가지고 있는 hello() 라는 메서드를 호출하고 있다. 

    만약 사용자가 선언 / 생성자 부분을 MyOtherConcretInterface로 바꾸는 경우, 이 클래스에는 hello() 메서드가 없기 때문에 컴파일 에러가 발생할 것이다.

    만약 인터페이스를 사용했다면 이런 부분을 쉽게 방지할 수 있을 것이다. 예를 들어 interface로 hello() 메서드를 올리고, 각 인터페이스에서 이 메서드를 지원하게 강제하면서 컴파일 에러를 방지할 수 있을 것이다. 


    인터페이스를 사용하지 않아도 되는 경우는? 

    인터페이스를 사용하면 유연하게 코드를 작성할 수 있어서 대부분의 경우 인터페이스를 사용하는 것이 권장된다. 하지만 적합한 인터페이스가 없는 경우에는 인터페이스를 사용하지 않아도 되는데, 주로 아래 세 가지 경우가 해당된다.

    • 값 객체는 사용할 필요가 없음 → record, dto 같은 것들
    • 클래스 기반으로 구현된 프레임워크 제공 클래스 → Outputstream 같은 것들
    • 인터페이스에 없는 특별한 메서드를 제공하는 클래스 → PriorityQueue 같은 것들
      • 이 기능을 너무 많이 사용할 경우 구현체 클래스를 바꾸는게 어려워 짐.

    값객체는 불변 객체로서 메서드끼리 값을 주고 받기 위해 사용된다. 이런 값객체들은 추상화 된 인터페이스가 대부분 존재하지 않고 필요도 없다. 

    프레임워크가 제공하는 클래스는 이미 클래스 기반으로 구현되었기 때문에 그렇게 사용할 수 밖에 없다. 만약 추상화 하고 싶다면, 사용자가 한번 더 인터페이스를 이용해서 Wrapping 하는 방식으로 사용해 볼 수 있을 것이다.

    또한 특정 클래스에서만 지원하는 특정한 기능 같은 경우에는 인터페이스 타입으로는 그 기능을 사용할 수 없다. 예를 들면 PriorityQueue 클래스 같은 것들이다. 예를 들어 PriorityQueue 클래스는 Queue 인터페이스가 지원하지 않는 comparator() 같은 기능을 지원한다. 만약 이 기능을 사용해야겠다면, 인터페이스가 아니라 구현체 타입을 직접 사용해야한다. 하지만 이런 기능을 너무 많이 사용하면 나중에 구현체를 바꾸려고 했을 때 영향을 받는 범위가 너무 넓어지게 될 것이다. 

    PriorityQueue<Object> objects = new PriorityQueue<>();
    objects.comparator()

    댓글

    Designed by JB FACTORY