JIGGAG

final이거나 abstract이거나

2021년 10월 18일

final이거나 abstract이거나


상속으로 발생하는 문제

상속으로 객체를 확장해서 이용하게 되면 점점 객체들의 관계가 복잡해진다.

A -> A' -> A'' -> A''' 계속 확장해나가면 편한 것은 분명한데 왜 관계가 복잡해지는 것일까?

class Documents {
	contents(): number[] {
		return [1,2,3];
	}

	length(): number {
		return this.contents().length;
	}
}

class EncryptedDocument extends Documents {
	contents(): number[] {
		return super.contents().concat(super.contents());
	}
}
const documents = new Documents();
console.log(documents.length());	// 3

const encrypted = new EncryptedDocument();
console.log(encrypted.length());	// 6

Documents를 확장해서 EncryptedDocument에서 contents 메서드를 오버라이드하였다. length 메서드는 contents 메서드에 의존적이기 때문에 오버라이드된 contents에 의해 기존의 동작과 전혀 다른 값을 반환하게 되었다.

length를 변경한적이 없는데 값이 이상해져버린 것이다.

상속은 부모 클래스를 자식 클래스에서 이어받는 단방향 프로세스이다. 그러나 위의 예시처럼 부모 클래스에서 lengthcontents에 의존하는 형태로 (이렇게 자신의 메서드끼리 의존하는 것 자체가 문제를 만드는 원인이다) 선언되어있던 것이 메서드 오버라이딩을 통해 자식 클래스의 메서드를 올바르지 않은 형태가 되어버리게 하였다.


final이나 abstract

클래스를 final이나 abstract로 상속을 제한한다면 위의 문제를 해결할 수 있다.

final 클래스는 상속 자체가 불가능하다. 따라서 부모-자식 클래스를 만들 수 없고 독립적으로 동작하기 떄문에 컨텍스트가 한정된 범위에서 이뤄진다. 상속이 불가능하기 때문에 final 인터페이스를 구현하도록하고 생성자로 받아오는 캡슐화를 이용해야한다.

약간의 자유도를 추가한 abstract 클래스는 완전하게 닫혀있는 형태가 아니다. 일부는 final로 변경할 수 없는 형태이지만 abstract의 특징은 일부는 직접 구현할 수 있는 형태이다.


그럼 상속을 언제 써야할까

상속을 통해 클래스의 역할을 확장하는 것이 아니라 정제하는 경우에 사용하는 것이다.

확장이 새로운 동작을 하도록 하는 것이라면 정제는 불완전한 것을 완전하게 다듬는 것이다.

추상 클래스를 통해 불완전한 상태를 만들고 사용하려는 곳에서 정제를 통해 하고자하는 것을 명확하게 할 수 있도록 하는 것이다.

abstract class Documents {
	abstract contents(): number[];

	length(): number {
		return this.contents().length;
	}
}

class DefaultDocument extends Documents {
	contents(): number[] {
		return [1,2,3];
	}
}

class EncryptedDocument extends Documents {
	contents(): number[] {
		return [11,22,33];
	}
}
const documents = new DefaultDocument();
console.log(documents.length());	// 3

const encrypted = new EncryptedDocument();
console.log(encrypted.length());	// 3

위의 문제를 해결하고자 abstract클래스로 변경하였다. 결론적으로는 length 메서드에서 contents 메서드를 의존하는 것은 동일하다.

그렇다면 어떻게 문제를 해결한 것일까?

contents 메서드를 각각 클래스에서 직접 구현하도록 하므로써 이 메서드의 의도를 명확하게 하였다. 상속과 오버라이드를 통해 의도를 모른채 변경되는 일을 해결하였다.

의도를 명확하게 표현하자


RAII???

객체가 살아있는 동안에만 리소스를 확보하는 것으로 객체를 초기화할 때 리소스를 확보하고 더 이상 객체를 사용하지 않게 된다면 리소스를 해제하는 것이다.

리소스를 갖고 있을 필요가 없는데 계속 들고 다니는 것은 불필요하다. 객체의 삶과 같이 리소스를 주고 뺴고 하자는 것인데, 여기서 또 다시 봉착하였다.

그정도로 리소스가 부담이 되는 것일까?


인터페이스

final, abstract 클래스로 오버라이드를 제한하는 것을 생각하다보니 인터페이스로 구현을 해야만 하도록 강제하는 것과 같은 목적이지 않을까?

abstract가 그 사이 어디쯤에서 오버라이드 제한하기도 하면서 구현을 강제하는 것인데, 인터페이스로도 동일하게 할 수 있지 않을까?