부모 클래스
의 변경이 자식 클래스
에 side-effect를 발생시키는 현상을 말합니다.class Parent {
void methodA() {
print("A");
methodB(); // methodB 호출
}
void methodB() {
print("B");
}
}
class Child extends Parent {
void methodB() {
super.methodB(); // 부모의 methodB도 호출
print("Child's B");
}
}
가장 쉬운 예시로 위 코드를 보겠습니다.
이 예시 코드에서 만약 부모 클래스의 methodA가 수정되어 methodB를 두 번 호출하도록 변경된다면 자식 클래스의 동작도 예상치 못하게 변경됩니다.
예를 들어 Child.methodA();를 호출한다면 Child는 알지 못하는 변경에 의해 본인의 methodB를 두 번 부르게 됩니다.
또는 이렇게 생각할 수 있습니다.
"Child의 methodB를 override 할 때, super.methodB를 호출하지 않고 새롭게 정의해서 쓰면 되는게 아닌가?"
이 경우 만약 Parent 클래스의 methodB를()가 중요한 검증 로직을 포함하고 있었다면 이를 호출하지 않으므로써 필수적인 알고리즘 단계를 건너뛰는 오류를 야기할 수 있으며, 이는 "계약 위반(Contract Violation)"이라는 새로운 문제가 발생하는 것 입니다.
class Parent {
void validateBalance() {
originMethod();
checkNetwork(); // 나중에 추가된 필수 네트워크 체크
print("checkNetwork후에 잔액 검증 하는 메서드");
}
void checkNetwork() { // 새로 추가된 필수 검증 메서드
print("네트워크 상태 확인하는 메서드");
}
}
조금 더 구체적인 예시는 "부모 클래스에 새로운 의존성이 추가되었을 때" 발생하는 문제 입니다.
Parent 클래스에 checkNetwork라는 새로운 의존성이 생겨버리면 Parent를 상속하는 많은 클래스들은 이를 알 수 없습니다.
이에 따라서 Child 클래스에서 side-effect가 발생하기 쉽습니다.
abstract class Parent {
// 템플릿 메서드 - final로 선언하여 오버라이드 방지
final void methodA() {
step1(); // 하위 클래스에서 구현
if (shouldExecuteStep2()) { // 선택적 기능을 제공하는 hook메서드
step2(); // 하위 클래스에서 구현
}
_commonStep(); // 공통 기능은 접근 불가능하도록
}
void step1(); // 추상 메서드 - 반드시 구현해야 함
void step2() { // 선택적으로 구현 가능한 메서드 (훅 메서드)
print("기본 step2 실행"); // 기본 구현을 제공하고 사용시 추가 구현
}
bool shouldExecuteStep2() { // 훅 메서드 - 실행 여부 결정
return false; // 기본값
}
void _commonStep() { // private 메서드 - 변경 불가능한 공통 기능
print("변경 불가능한 공통 기능 실행");
}
}
// 구현 클래스 1
class Child1 extends Parent {
void step1() {
print("Child1의 step1 구현");
}
}
// 구현 클래스 2
class Child2 extends Parent {
void step1() {
print("Child2의 step1 구현");
}
void step2() {
print("Child2의 커스텀 step2 구현");
}
bool shouldExecuteStep2() {
return true; // step2 실행하도록 구현
}
}
템플릿 메서드 패턴은 알고리즘의 구조를 메서드에 정의하고, 일부 단계를 하위 클래스에서 구현할 수 있도록 하는 패턴입니다.
알고리즘의 뼈대는 상위 클래스에서 정의하고, 일부 단계의 구현은 하위 클래스에 위임하는 형태입니다.
"템플릿"을 통해 알고리즘의 구조를 명확하게 하고 하위 클래스에서 수정 가능한 부분이 명확히 정의하기 때문에
공통 기능의 무결성이 보장되며 확장은 용이하되 기존 구조는 깨지지 않습니다.
(패턴 자체에 대해서는 다른 글에서 더 자세히 다뤄보겠습니다)
class WithdrawValidator {
void validate() {
print("잔액 검증");
}
}
class BankAccount {
final WithdrawValidator validator; // 컴포지션으로, 상속이 아니라 내부변
BankAccount(this.validator);
void withdrawMoney() {
validator.validate();
processingWithdraw();
}
}
컴포지션은 "has-a" 관계를 만드는 것으로, 상속("is-a" 관계) 대신 인스턴스를 내부에 포함시켜 사용하는 방식입니다.
상속과 달리 결합도가 매우 낮아 내부 구현 변경에 '덜'취약하며 테스트가 용이한 장점이 있습니다.
abstract class BalanceValidator {
void validate();
}
class StandardValidator implements BalanceValidator {
void validate() {
print("표준 잔액 검증");
}
}
class CustomValidator implements BalanceValidator {
void validate() {
print("커스텀 잔액 검증");
}
}
BalanceValidator라는 부모 클래스의 구현을 포기하고 "인터페이스로 사용"하여 상세 구현을 모두 자식 클래스에서 한다면 해당 문제는 해결됩니다.