
좋은 객체지향 프로그래밍은 변경에 유연하고 확장에 강한 코드를 만드는 것을 목표로 한다.
이를 위해서는 역할과 구현의 분리, 즉 다형성을 활용한 설계가 필수적이다.
이번 글에서는 다형성을 실제 설계에 적용하는 과정을 예제와 함께 살펴본다.
객체지향에서 다형성을 제대로 활용하기 위해서는 역할(인터페이스) 과 구현(클래스) 을 명확히 분리해야 한다.
클라이언트는 구체적인 구현이 아닌 역할(인터페이스) 에만 의존해야 한다.
아래는 다형성을 적용하지 않은 설계다.
public class K3Car {
public void startEngine() {
System.out.println("K3Car.startEngine");
}
public void offEngine() {
System.out.println("K3Car.offEngine");
}
public void pressAccelerator() {
System.out.println("K3Car.pressAccelerator");
}
}
public class Driver {
private K3Car k3Car;
public void setK3Car(K3Car k3Car) {
this.k3Car = k3Car;
}
public void drive() {
System.out.println("자동차를 운전합니다.");
k3Car.startEngine();
k3Car.pressAccelerator();
k3Car.offEngine();
}
}
Driver는 K3Car에 직접 의존하고 있다.
만약 Model3Car라는 새로운 차가 추가되면 Driver 클래스도 수정해야 한다.
이는 변경에 닫혀 있지 않은 코드이며, 유지보수가 어려워진다.
인터페이스를 도입하면 클라이언트 코드(Driver)는 구체적인 자동차 종류를 몰라도 된다.
public interface Car {
void startEngine();
void offEngine();
void pressAccelerator();
}
public class K3Car implements Car {
@Override
public void startEngine() {
System.out.println("K3Car.startEngine");
}
@Override
public void offEngine() {
System.out.println("K3Car.offEngine");
}
@Override
public void pressAccelerator() {
System.out.println("K3Car.pressAccelerator");
}
}
public class Model3Car implements Car {
@Override
public void startEngine() {
System.out.println("Model3Car.startEngine");
}
@Override
public void offEngine() {
System.out.println("Model3Car.offEngine");
}
@Override
public void pressAccelerator() {
System.out.println("Model3Car.pressAccelerator");
}
}
이제 Driver는 인터페이스 Car에만 의존한다.
public class Driver {
private Car car;
public void setCar(Car car) {
System.out.println("자동차를 설정합니다: " + car);
this.car = car;
}
public void drive() {
System.out.println("자동차를 운전합니다.");
car.startEngine();
car.pressAccelerator();
car.offEngine();
}
}
Driver는 이제 자동차의 종류와 상관없이 Car 인터페이스만 사용한다.
이는 클라이언트 코드가 구현체와 분리된 구조로, 다형성이 제대로 적용된 설계다.
좋은 객체지향 설계 원칙 중 하나인 OCP는 다음과 같다.
앞선 자동차 예제처럼 새로운 차종이 추가되어도 Driver의 코드는 바뀌지 않는다.
Car 인터페이스를 구현하기만 하면 얼마든지 확장이 가능하다.
이것이 바로 OCP를 지키는 설계다.
이 구조는 전략 패턴(Strategy Pattern) 의 전형적인 형태다.
전략 패턴은 알고리즘(전략)을 클라이언트 코드의 변경 없이 교체할 수 있게 한다.
Car 인터페이스 → 전략을 정의한다. K3Car, Model3Car → 전략의 구체적인 구현이다. Driver → 전략을 사용하는 클라이언트다. 런타임 중에도 전략을 교체할 수 있으므로 유연한 구조를 제공한다.
결제 수단이 여러 개인 프로그램에서도 다형성을 적용할 수 있다.
다음은 결제 로직을 인터페이스로 추상화한 예시다.
public interface Pay {
boolean pay(int amount);
}
public class KakaoPay implements Pay {
public boolean pay(int amount) {
System.out.println("카카오페이 결제를 시도합니다.");
return true;
}
}
public class NaverPay implements Pay {
public boolean pay(int amount) {
System.out.println("네이버페이 결제를 시도합니다.");
return true;
}
}
public class DefaultPay implements Pay {
public boolean pay(int amount) {
System.out.println("결제 수단이 없습니다.");
return false;
}
}
public abstract class PayStore {
public static Pay findPay(String option) {
if (option.equals("kakao")) return new KakaoPay();
if (option.equals("naver")) return new NaverPay();
return new DefaultPay();
}
}
public class PayService {
public void processPay(String option, int amount) {
System.out.println("결제를 시작합니다: " + option);
Pay pay = PayStore.findPay(option);
boolean result = pay.pay(amount);
if (result) {
System.out.println("결제가 성공했습니다.");
} else {
System.out.println("결제가 실패했습니다.");
}
}
}
PayService는 구체적인 결제 수단이 아닌 Pay 인터페이스에만 의존한다.
따라서 결제 수단이 추가되어도 서비스 로직은 전혀 수정할 필요가 없다.
이 역시 다형성과 OCP의 결합된 형태다.