자바 객체지향에서 자주 사영 하는 상속(Inheritance)을 잘못사용하는 경우 클래스가 기하급수적으로 늘어나는 '클래스 폭발(Class Explosion)'을 맞고 유지보수에 어려움을 느낄수있다.
흔히 아는 롤(LoL)의 '챔피언과 스킨‘ 을 예시로, 상속 대신 합성(Composition)을 사용하는 이유와 OCP를 지키는 전략 패턴(Strategy Pattern)에 대해 이야기 해보겠다.
1️⃣ 만약 스킨을 '상속'으로 구현한다면? (문제 상황)
가렌(Garen)이라는 챔피언이 있다고 치자.
새로운 스킨이 출시될 때마다 상속을 사용해서 구현하면 대략 이런 그림이 나온다.
class Garen { ... }
class PrestigeGaren extends Garen { ... }
class PoolPartyGaren extends Garen { ... }
만약 챔피언이 100명이고 스킨이 10개씩이라면? 무려 1,000개의 클래스를 만들어야 한다. 심지어 게임 도중(런타임)에 유저가 스킨을 갈아끼우는 것도 불가능하다.
이럴 때 필요한 게 전략 패턴이다.
2️⃣ 해결책: 변하는 것을 분리하자 (인터페이스와 합성)
객체지향 설계의 핵심 원칙 중 하나는 "변하는 것과 변하지 않는 것을 분리하는 것"이다.
변하지 않는 것: 챔피언이 공격을 한다는 행위 자체 (Champion)
변하는 것:챔피언이 공격할 때 나가는 이펙트, 즉 스킨 (Skin)
변하는 부분인 Skin을 인터페이스로 빼고, Champion이 이 Skin을 상속이 아닌 상태(필드)로 가지도록(합성, Composition) 만들어보자.
Step 1. 변하는 부분을 인터페이스로 추출 (Skin)
// 변하는 부분의 뼈대
public interface Skin {
void displayEffect();
}
// 구체적인 스킨들 (언제든 추가 가능)
public class BasicSkin implements Skin {
@Override
public void displayEffect() {
System.out.println("효과: 일반 이펙트가 출력됩니다.");
}
}
public class PrestigeSkin implements Skin {
@Override
public void displayEffect() {
System.out.println("효과: 황금빛 프레스티지 이펙트가 터집니다! ✨");
}
}Step 2. 챔피언은 스킨(인터페이스)에만 의존한다
public interface Champion {
void attack();
void changeSkin(Skin skin);
}
public class Garen implements Champion {
// ⭐ 핵심: 구체적인 스킨 클래스가 아닌, 인터페이스(Skin)에 의존한다.
private Skin skin;
// 생성자로 외부에서 스킨을 주입(Injection) 받음
public Garen(Skin skin) {
this.skin = skin;
}
@Override
public void changeSkin(Skin skin) {
System.out.println("\n[시스템] 가렌의 스킨을 교체합니다...");
this.skin = skin; // 런타임 교체 가능
}
@Override
public void attack() {
System.out.print("가렌 공격 ➜ ");
skin.displayEffect(); // 실제 동작은 주입받은 객체에 위임
}
}3️⃣ 실제 사용 및 테스트
이제 클라이언트에서 챔피언을 만들고 스킨을 자유롭게 갈아끼워보자.
public class Main {
public static void main(String[] args) {
// 1. 기본 스킨으로 가렌 생성
Champion garen = new Garen(new BasicSkin());
garen.attack();
// 출력: 가렌 공격 ➜ 효과: 일반 이펙트가 출력됩니다.
// 2. 게임 중 프레스티지 스킨으로 교체
garen.changeSkin(new PrestigeSkin());
garen.attack();
// 출력: 가렌 공격 ➜ 효과: 황금빛 프레스티지 이펙트가 터집니다! ✨
}
}4️⃣ 왜 이렇게 설계해야 할까?
이게 바로 실무에서 지향하는 전략 패턴(Strategy Pattern)이다. 이렇게 짰을 때 얻는 이점은 명확하다.
1. 다형성(Polymorphism) 극대화
> Garen은 자기가 무슨 스킨을 꼈는지 알 필요가 없다. 그냥 Skin 인터페이스의 규약만 믿고 displayEffect()를 호출하면 끝이다.
2. OCP (개방-폐쇄 원칙) 완벽 준수
> 내일 당장 '수영장 파티 스킨'이 새로 출시된다고 해보자. 기존의 Garen이나 Champion 코드는 단 한 줄도 수정할 필요가 없다. 그냥 PoolPartySkin 구현체 클래스 하나 띡 만들어주면 된다.
실제 백엔드 프로젝트에서 사용가능한 예시로는
Champion을 주문 서비스(OrderService)로, Skin을 결제 모듈(KakaoPay, NaverPay)로 생각해보자. 새로운 결제 수단이 추가되어도 핵심 비즈니스 로직은 안전하게 보호된다.
결론: 무지성 상속은 피하고, 인터페이스와 합성을 활용하자.
[Ref.]
_1.인프런 이펙티브자바 백기선 강의 참고
댓글