아이템 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()
'프로그래밍 언어 > JAVA' 카테고리의 다른 글
Effective Java : 아이템 62. 다른 타입이 적절하다면 문자열 사용을 피하라. (0) | 2023.08.26 |
---|---|
Effective Java : 아이템 61. 박싱된 기본 타입보다는 기본 타입을 사용하라. (0) | 2023.08.26 |
Effective Java : 아이템 63. 문자열 연결은 느리니 주의하라 (0) | 2023.08.26 |
Effective Java : 아이템 52. 다중정의는 신중히 사용하라 (0) | 2023.08.26 |
Effective Java : 아이템 55. 옵셔널 반환은 신중히 하라. (0) | 2023.08.22 |