티스토리 뷰
★ 어려움!
* 이펙티브 자바 2/E를 읽고 공부하기 위해 기록한 게시글입니다.
* 여기서 말하는 계승(Inheritance, 상속)은 구현 계승이란 의미로, 한 클래스가 다른 클래스를 "extends" 한다는 것을 뜻함.
(인터페이스 계승 - 한 클래스가 어떤 인터페이스를 implements 하는 경우, 인터페이스가 인터페이스를 extends 하는 경우는 포함하지 않는다.)
16. 계승(상속)하는 대신 구성하라
계승은 재사용을 돕는 강력한 도구이지만, 항상 최선의 선택인 것은 아니다.
- 계승을 적절하게 사용하지 못한 소프트웨어는 깨지기 쉽다.
- 상위 클래스와 하위 클래스 구현을 같은 개발자가 통제하는 단일 패키지 안에서 사용하는 것은 안전하다
- 계승을 고려해 설계되고, 그에 맞는 문서를 갖춘 클래스에 사용하는 것도 안전하다.
- 하지만, 일반적인 객체 생성 가능 클래스라면, 해당 클래스가 속한 패키지 밖에서 계승을 시도하는 것은 위험하다.
메서드 호출과는 달리 계승은 캡슐화 원칙을 위반한다. 하위 클래스가 정상 동작하기 위해서는 상위 클래스의 구현에 의존할 수 밖에 없다.
-> 상위 클래스가 변경되면 하위 클래스 코드는 수정된 적이 없어도 망가질 수 있다.
@Getter
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
public InstrumentedHashSet() {}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@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);
}
}
위 코드는 잘 동작할 것 같아 보이지만 막상 실행해보면 정상 동작하지 않는다.
왜냐하면 super.addAll 메서드는 add 메서드를 기반으로 동작하기 때문에 중복 카운트가 되기 때문.
// AbstractCollection.class
public boolean add(E e) {
throw new UnsupportedOperationException();
}
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
// HashSet.class
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// InstrumentedHashSet.class
@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);
}
- 하위 클래스인 InstrumentedHashSet에서 addAll 메서드를 삭제하면 이 문제를 교정(fix)할 수는 있으나, 이 클래스가 정상 동작한다는 것은 상위 클래스의 addAll 메서드가 add 위에서 구현되었다는 사실에 여전히 의존한다.
- InstrumentedHashSet의 addAll 메서드가 인자에 담긴 각 원소마다 add를 호출하도록 하는 것은 조금 더 나은 방법이다. 아예 상위 클래스의 addAll 메서드를 아예 사용하지 않는 방법이다. 하지만 이러한 방법도 만능은 아니다.
- 결국 최종적인 해결 방법은 상위 클래스의 메서드를 수정하는 것인데, 이것은 어려울 뿐만 아니라 오류를 일으키기 매우 쉬운 작업이다. 또한 항상 사용할 수 있는 방법도 아니다. 또한 상위 클래스에 새로운 메서드를 추가하는 것은 하위 클래스의 구현을 망가뜨릴 수 있고 수많은 의도치 않은 문제들을 야기할 수 있다.
위와 같은 문제들은 결국 메서드 재정의 때문에 발생한 것이다. 기존 메서드 재정의 대신 아예 새로운 메서드를 만들어 사용하는 것은 조금 더 안전한 방법이긴 하나, 위험성이 아예 사라지는 것은 아니다.
- 새 릴리스에 추가된 상위 클래스의 메서드가 우연히 하위 클래스에 정의된 시그니처와 동일한데 리턴값만 다르다면, 하위 클래스의 메서드는 더이상 컴파일되지 않는다.(??) 컴파일 에러가 생긴다. (메서드 오버라이딩 시 서로 동일한 시그니처를 가져야 한다)
- 새로운 상위 클래스의 메서드 일반 규약이 기존 하위 클래스 메서드와 맞을 지 알 수 없다
* 시그니처: 메서드 이름 + 파라미터의 개수와 타입 + 리턴 타입
결국 계승을 사용함으로써 캡슐화 원칙을 위반했고, 캡슐화 원칙을 위반함으로써 두 클래스가 서로 강하게 결합, 의존하면서 변화에 대처하기가 매우 어려워진 것이다.
그럼 대체 어떻게 하라는 것일까??
지금껏 설명한 문제들을 모두 피할 수 있는 방법이 있다. 기존 클래스를 계승하는 대신, 새로운 클래스에 기존 클래스 객체를 참조하는 private 필드를 하나 두는 것이다. 이러한 설계기법을 구성(Composition)이라고 부르는데, 기존 클래스가 새 클래스의 일부(Component)가 되기 때문이다.
- 새로운 클래스에 포함된 각각의 메서드는 기존 클래스에 있는 메서드 가운데 필요한 것을 호출해서 그 결과를 반환하면 된다. 이러한 구현 기법을 전달(forwarding) 이라고 하고, 전달 기법을 사용해 구현된 메서드를 전달 메서드(forwarding method)라고 부른다.
- 구성 기법을 통해 구현된 클래스는 기존 클래스의 구현 세부사항에 종속되지 않기 때문에 견고하고, 기존 클래스에 또 다른 메서드가 추가되더라도 새로운 클래스에는 영향을 끼치지 않는다.
@Getter // 계승 대신 구성을 사용하는 Wrapper 클래스
public class InstrumentedHashSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedHashSet(Set<E> s) {
super(s);
}
@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);
}
}
////
// 재사용 가능한 전달(forwarding) 클래스
public class ForwardingSet<E> implements Set<E> {
// HashSet을 직접적으로 extends 하지 않고 변수로 참조하면서 메서드 호출
private final Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
@Override
public boolean add(E e) {
return s.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}
...
}
InstrumentedSet과 같은 설계가 가능한 이유는, HashSet이 제공하는 기능을 규정하고 있는 Set이라는 인터페이스가 존재하는 덕분이다. 위와 같은 설계는 안정적일 뿐만 아니라 유연성도 매우 높다.
- InstrumentedSet 는 결국 Set 인터페이스를 구현하며, Set 객체를 인자로 받아 필요한 기능을 갖춘 다른 Set 객체로 변환하는 구실을 한다.
- 이와 같은 클래스를 Wrapper class라고 부르는데, 다른 Set 객체를 감싸고 있기 때문이다.
- 계승을 이용한 접근법은 단 하나의 클래스에만 적용 가능하고, 상위 클래스 생성자마다 별도의 생성자를 만들어야 하는 반면, Wrapper class 기법을 쓰면 어떠한 Set 구현도 원하는 대로 수정할 수 있고 기존 생성자도 그대로 사용할 수 있다.
- 이미 사용중인 객체에 일시적으로 원하는 기능을 넣는 데도 사용된다. 이러한 기법을 데코레이터 패턴이라 부름.
-> 기존 Set 객체에 기능을 덧붙여 장식하는 구실을 하기 때문. 구성과 전달 기법을 아울러서 "위임" 이라고 부르기도 한다.
대체 무슨소리지.. 싶었는데 직접 extends 하지 말고, private 변수로 둔 다음 변수에게 메서드 실행을 위임하고 그 결과만 받아서 활용하라는 것인듯 하다. 전달 클래스에서도 보면 extends HashSet이었던 것을 private Set으로 만들어놓고 Set에게 메서드 실행을 위임하고 있다. InstrumentedSet은 이러한 전달 클래스(ForwardingSet)를 활용하는 데코레이터 중 하나로 사용되는 것.
- 전달 클래스를 하나 만들어놓으면 InstrumentedSet과 같은 데코레이터를 다양하게 만들어서 사용할 수 있다! 확장에는 열려있고 변경에는 닫혀있는 객체지향스러운 개발이 가능해진다!!
=======
뭔지 감이 잘 안오니까 대표적인 데코레이터 패턴 하나를 구경하고 가자.
바로 InputStream / OutputStream 클래스!
BufferedInputStream br = new BufferedInputStream(new FileInputStream("test.txt"));
인터페이스: InputStream / 타깃(포장될 요소): FileInputStream
추상 데코레이터: FilterInputStream
구상 데코레이터: BufferedInputStream
// 타깃(데코레이터에 의해 포장될 요소)
public class FileInputStream extends InputStream {
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
...
}
public int read(byte b[], int off, int len) throws IOException {
return readBytes(b, off, len);
}
...
}
// 추상 데코레이터 (전달 클래스?) - 데코레이터에 사용되는 공통 분모 추출
// 구상 데코레이터가 따로 재정의하지 않은 메서드는 연결 부품의 메서드를 그대로 사용하고
// 따로 재정의한 메서드는 데코레이터만의 부가기능을 추가해서 사용하는 방식.
class FilterInputStream extends InputStream {
// 연결 부품을 담아놓는다 (여기서는 fileInputStream)
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
public int read() throws IOException {
return in.read();
}
public void close() throws IOException {
in.close();
}
....
}
// 구상 데코레이터 - 추상 데코레이터의 메서드를 부가기능에 맞게 구현
public class BufferedInputStream extends FilterInputStream {
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
public BufferedInputStream(InputStream in, int size) {
super(in); // 추상 데코레이터의 생성자 이용
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
// 추상 데코레이터 메서드 오버라이드
public synchronized int read() throws IOException {
if (pos >= count) {
fill(); // 여기 안에서 InputStream in의 read 메서드를 호출함
// 변수 in은 공통 분모인 추상 데코레이터 FilterInputStream을 통해 담아놓고 있음.
// 이 코드에서는 fileInputStream의 read 메서드가 된다.
// 타깃인 FileInputStream을 Wrapping 해서 부가기능을 추가해줌!!
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}
}
위와 같은 구조.
추상 데코레이터 FilterInputStream이 데코레이터들의 공통 분모를 미리 뽑아놓고
구현 데코레이터들 (BufferedInputStream 등)이 추상 데코레이터에 정의된 메서드를 재정의해서 사용한다
타깃이나 구현 데코레이터나 결국 모두 동일한 인터페이스인 InputStream을 구현하고 있기 때문에 구현 데코레이터가 타깃을 받는 것도 가능하고, 구현 데코레이터를 이어 받는것도 가능하다.
(타깃이나 데코레이터나 모두 동일한 인터페이스로 접근하기 때문에 자신이 최종 타깃으로 위임하는지, 다음 데코레이터로 위임하는지 알 수 없다.)
public static void main(String[] args) throws IOException {
...
FileInputStream fs = new FileInputStream(file); // 타깃
BufferedInputStream br = new BufferedInputStream(fs); // 타깃 -> 데코레이터
LineNumberInputStream lis = new LineNumberInputStream(br); // 데코레이터 -> 데코레이터
...
}
이러한 데코레이터를 사용했을 때의 이점이 바로 타깃의 코드를 손대지 않고 + 클라이언트가 호출하는 방법도 변경하지 않은 채로 새로운 기능을 추가하고자 할 수 있다는 것이다.
이거 잘 보면 위에 있는 ForwardingSet(전달 클래스) / InstrumentedSet(Wrapper 클래스) 구조랑 그냥 똑같다 ㄷㄷ..
전달 클래스 = 추상 데코레이터 / Wrapper 클래스 = 구상 데코레이터로 보는 것이 맞는 것 같다.
그리고 데코레이터 패턴이 가능한 이유는 바로 잘 설계된 인터페이스가 있기 때문..!! 인터페이스가 자바를 구한다!
참고: https://eungeun506.tistory.com/60
근데 디게 어렵다.. 돌아서면 까먹을듯ㄱ...
=======
계승은 하위 클래스가 상위 클래스의 하위 자료형이 확실한 경우에만 하는 것이 바람직하다.
- 클래스 B는 클래스 A와 "IS-A"관계가 성립할 때에만 A를 계승해야 한다.
= "B는 확실히 A인가?" 에 그렇다고 답할 수 있어야 한다.
(아니다 라고 답한다면, B 안에 A 객체를 참조하는 private 필드를 두고 B에는 작고 간단한 API를 구현하는 것이 좋다 - A는 B의 핵심적 부분이 아니며 B의 구현 세부사항에 불과하다.)
(결론)
* 계승은 강력한 도구이지만, 캡슐화 원칙을 침해하므로 문제를 일으킬 소지가 있다.
* 계승은 상위 클래스와 하위 클래스 간 IS-A 관계가 성립할 때만 사용하자. (IS-A 관계가 성립한다 해도, 각 클래스가 서로 다른 패키지에 있거나, 계승을 고려해 만들어진 상위 클래스가 아니라면 하위 클래스는 깨지기 쉽다. 이러한 문제를 피하기 위해서는 구성/전달 기법(="데코레이터 패턴") 을 사용)
* 계승 매커니즘은 상위 클래스의 문제를 하위 클래스에 전파시킨다. 만일 상위 클래스에 결함이 있다면, 이러한 결함이 하위 클래스까지 전파되어도 괜찮은가를 고려해야 한다. (구성 기법을 이용하면 그러한 약점을 감추는 새로운 API를 설계할 수 있도록 해준다.)
* 포장 클래스는 하위 클래스보다 견고할 뿐만 아니라 유연하고 강력하다.
책이 생각보다 어렵고 잘 안읽힌다. 아이고...
'JAVA' 카테고리의 다른 글
추상 클래스 보다는 인터페이스 (0) | 2022.07.24 |
---|---|
기타 (0) | 2022.07.24 |
변경 가능성을 최소화하라 (0) | 2022.07.23 |
getter/setter 메서드를 활용하자 (0) | 2022.07.23 |
클래스와 멤버의 접근 권한을 최소화하자 (0) | 2022.07.22 |