JAVA/Effective Java

클래스와 인터페이스 - 규칙 16 계승하는 대신 구성하라.

반응형

상속은 코드를 재사용 할 수 있도록 돕는 강력한 도구이지만, 항상 최선이라 할 수 없다.

제대로 구현하지 못한 경우 문제를 유발할 가능성이 크기 때문이다.

그리고 같은 패키지 내부에 존재하는 클래스를 상속받아야한다.

상속은 일반적으로 캡슐화 원칙을 위반한다.
그 이유는 상위클래스의 일부 개념이 변경될 경우, 하위클래스는 그대로 영향을 받을 수 밖에 없기 때문이다.

이를 예방하기 위해서는 문서를 제대로 구현해 놓아야 한다.

문제가 야기될 수 있는 상황은 다음과 같다.
1. 기존의 재정의 하여 사용하였을 경우.
-> 문제가 될 수 있는 상황을 예를 들면, HashSet을 상속하고 add관련 메서드를 재정의하여 객체를 추가한 횟수를 확인하는 로직을 추가하는 로직을 만들어보자



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Effective16<E> extends HashSet<E> {
    private int addCount = 0;
    
    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }
    
    @Override
    public boolean addAll(Collection<extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }
    
    public int getCount() {
        return addCount;
    }
}
cs



다음과 같이 HashSet<E>을 상속받아 기존의 add 관련 함수를 재정의 한다음 다음과 같이 호출하였을 때, 출력되는 count값은 어떻게 될까?


1
2
3
4
5
6
7
public static void main(String args[]) {
    Effective16<String> set = new Effective16<>();
        
    set.addAll(Arrays.asList("babo""cjung"));
        
    System.out.println(set.getCount());
}
cs



위의 로직을 보면 당연히 count가 2가 나올 것 같지만 4가 나온다. 

그 이유는 HashSet에서 제공하는 addAll은 내부적으로 all을 사용하기 때문에 재정의한 all에 접근하여 카운트가 더 증가되는 것이다.

2. 다른 이름을 가진 새로운 메서드를 사용한다.
-> 1번의 방법보다 조금더 안전하긴 하지만 만약 상위 HashSet 클래스에 동일한 메서드나 반환형만 다른 메서드 타입이 생성될 경우 문제를 야기 할 수 있기 때문에 문제가 될 수 있다.


해결 방법
기존 클래스를 상속하는 대신 새로운 클래스에 기존 클래스 객체를 참조하는 private필드를 하나 두는 composition이라는 기법을 사용한다. (Has a 기법)

이렇게 가지고 있는 필드에 접근하여 필요한 메서드만 호출하여 사용하는 방법이다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class ForwardingSet<E> implements Set<E> {
 
    private final Set<E> s;
    public ForwardingSet(Set<E> s) {
        this.s = s;
    }
    
    @Override
    public boolean add(E arg0) {
        return s.add(arg0);
    }
 
    @Override
    public boolean addAll(Collection<extends E> arg0) {
        return s.addAll(arg0);
    }
}
 
public class Effective16<E> extends ForwardingSet<E> {
    
    public Effective16(Set<E> s) {
        super(s);
    }
 
    private int addCount = 0;
    
    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }
    
    @Override
    public boolean addAll(Collection<extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }
    
    public int getCount() {
        return addCount;
    }
}
cs




정리하면
상속은 캡슐화의 원칙을 침해 하므로 문제를 발생시킬 소지가 있다는 것을 잊지말아야한다. 그러므로 상속은 하위 클래스가 상위 클래스의 하위 자료형이 확실하고, IS-A관계가 성립되는 경우에만 사용한다.

그렇기에 해당 기능을 사용해야 하는 경우에는 has a 관계를 사용하면 안되는지 다시 한번 확인 해 보는 것이 좋다.


출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙16 인용.



반응형