오봉이와 함께하는 개발 블로그
상속의 나쁜 예 (강한 결합) - 내 코드가 그렇게 이상한가요? 본문
상속
상속을 사용하면 슈퍼 클래스의 로직을 서브 클래스에서 그대로 사용할 수 있어 슈퍼 클래스에 공통 로직을 두는 저장소 개념으로 사용할 수 있지만, 너무 일반화 시키면 강한 결합이 발생한다.
코드
아래 코드를 보자
public abstract class DiscountBase {
public int price;
public int getDiscountedPrice() {
int discountPrice = price - 3000;
if (discountPrice < 0) {
return 0;
}
return discountPrice;
}
}
public class RegularDiscount extends DiscountBase {
}
public class SummerDiscount extends DiscountBase {
}
현재 일반 할인, 여름 할인 모두 3,000원을 할인하는 정책이지만 일반 할인의 할인 가격이 4,000원으로 바뀌었다고 하자
public class RegularDiscount extends DiscountBase {
@Override
public int getDiscountedPrice() {
int discountPrice = price - 4000;
if (discountPrice < 0) {
return 0;
}
return discountPrice;
}
}
할인 금액을 제외하고 로직이 같게 된다. 일반화를 하려다가 오히려 관리해야 할 코드가 더 늘어나게 되었다.
혹은 더 일반화를 한다 생각해 아래와 같이 바꿀 수도 있을 것이다.
public abstract class DiscountBase {
public int price;
public int getDiscountedPrice() {
int discountPrice = price - getDiscountCharge();
if (discountPrice < 0) {
return 0;
}
return discountPrice;
}
public int getDiscountCharge() {
return 3000;
}
}
public class RegularDiscount extends DiscountBase {
@Override
public int getDiscountCharge() {
return 4000;
}
}
위와 같이 코드를 변경하면 일반 할인은 4,000원 여름 할인은 3,000원이 적용되어 일반화가 잘 된 코드라 생각할 수 있지만, RegularDiscount에서 getDiscountCharge를 오버라이딩 하여 사용하기 위해 슈퍼 클래스의 getDiscountedPrice의 로직을 파악해야 하는 문제가 발생한다.
하나의 로직으로 봐야하는 getDiscountedPrice가 두 개의 클래스에 나눠서 구현되어 있다. 지금은 코드가 한눈에 들어와 문제가 없지만 복잡한 로직을 작성하게 되면 디버깅도 어렵고 코드를 눈으로 읽으며 따라가기도 어렵다.
설상가상 여름 할인 로직도 변경되었다고 가정하자.
public class SummerDiscount extends DiscountBase {
@Override
public int getDiscountedPrice() {
return (int) (price * (1.00 - 0.05));
}
}
물론 돌아가는 코드가 가장 좋은 코드라고 하는 관점에서 보면 좋은 코드겠지만, 유지보수가 용이한 코드인가? 라는 질문에는 자신있게 맞다고 할 사람이 있을지 모르겠다.
SummerDiscount의 getDiscountedPrice와 DiscountBase의 getDiscountedPrice는 단지 슈퍼 클래스의 메서드 이름만 사용하고 있을 뿐 더이상 큰 관계가 없다. DiscountBase와 SummerDiscount의 메서드가 많아졌을 때 SummerDiscount에서 DiscountBase와 일부와만 관련 있는 메서드가 등장하면 지옥을 맛보게 되지 않을까 생각한다.
public abstract class DiscountBase {
public int price;
public int getDiscountedPrice() {
int discountedPrice = 0;
if (this instanceof RegularDiscount) {
discountedPrice = price - 4000;
if (discountedPrice < 0) {
discountedPrice = 0;
}
} else if (this instanceof SummerDiscount) {
discountedPrice = (int) (price * (1.00 - 0.05));
}
return discountedPrice;
}
}
위 코드를 보면 RegularDiscount와 SummerDiscount 클래스의 용도는 단지 DiscountBase의 분기만을 위한 코드로 변경되었다.
할인 정책이 100개가 생긴다 가정했을 때 조건 분기를 100개 해야 하는 말도 안 되는 코드가 완성되는 것이다.
또한 할인 로직은 RegularDiscount와 SummerDiscount에 캡슐화되어 있어야 하는데 DiscountBase에 모두 구현되어 있어 비즈니스 개념이 모호해지고 분산된다.
내가 아닌 타인이 이런 말도 안 되는 코드를 보게 되었을 때 어떤 기분일지 모두 생각해보자.
슈퍼 클래스의 로직 중 특정 상태 혹은 조건에 따라 상속받는 쪽에서 차이가 있는 로직만 구현하여 동작을 다르게 할 수 있는 템플릿 메서드 라는 디자인 패턴도 있다.
즉, 잘 설계한 상속은 문제가 없지만, 잘못 설계한 상속이 문제가 되고, 쉽게 문제를 발생할 수 있기 때문에 가능한 컴포지션을 사용하라는 말이 있는 것이다.
상속을 사용하기 전에 컴포지션이나 값 객체 등 다른 방법을 사용할 수 없는지 생각하고, 상속이 최선이라 판단된다면 단일 책임 원칙(SRP: Single Responsibility Principle)에 위배되지 않도록 구현하자.
'자바' 카테고리의 다른 글
Java에서 사용하는 함수형 프로그래밍 (1) (1) | 2024.01.15 |
---|---|
Sync & Async / Blocking & Non-Blocking (0) | 2023.11.23 |
강한 결합 깨기 (상속 part) - 내 코드가 그렇게 이상한가요? (0) | 2023.11.16 |
결합도와 책무 - 내 코드가 그렇게 이상한가요? (1) | 2023.11.14 |
성숙한 클래스 - 내 코드가 그렇게 이상한가요? (0) | 2023.11.12 |