먼저 다형성을 이용하지 않고, 운전자와 자동차의 관계를 개발해보자.

package poly.car0;
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");
}
}
package poly.car0;
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();
}
}
package poly.car0;
public class CarMain0 {
public static void main(String[] args) {
Driver driver = new Driver();
K3Car k3Car = new K3Car();
driver.setK3Car(k3Car);
driver.drive();
}
}
/*
자동차를 운전합니다.
K3Car.startEngine
K3Car.pressAccelerator
K3Car.offEngine
*/
현재 Driver는 k3Car라는 인스턴스 변수를 가지고 있고, 원래는 초기화를 하지 않았기 때문에 초기값은 null이지만 외부에서 setK3Car 메서드를 호출하면서 실제 인스턴스에 있는 참조값이 넘어오는 것이다. 이제 참조값을 통해 여러 메서드를 호출할 수 있다.

이제 새로운 요구사항이 들어왔다고 해보자. 바로 Model3 차량을 추가해야 하는 요구사항이다. 이를 위해 Driver 코드를 많이 변경해야 한다.

package poly.car0;
public class Model3Car {
public void startEngine() {
System.out.println("Model3Car.startEngine");
}
public void offEngine() {
System.out.println("Model3Car.offEngine");
}
public void pressAccelerator() {
System.out.println("Model3Car.pressAccelerator");
}
}
package poly.car0;
public class Driver {
private K3Car k3Car;
private Model3Car model3Car;
public void setK3Car(K3Car k3Car) {
this.k3Car = k3Car;
}
public void setModel3Car(Model3Car model3Car) {
this.model3Car = model3Car;
}
public void drive() {
System.out.println("자동차를 운전합니다.");
if (k3Car != null) {
k3Car.startEngine();
k3Car.pressAccelerator();
k3Car.offEngine();
} else if (model3Car != null) {
model3Car.startEngine();
model3Car.pressAccelerator();
model3Car.offEngine();
}
}
}
package poly.car0;
public class CarMain0 {
public static void main(String[] args) {
Driver driver = new Driver();
K3Car k3Car = new K3Car();
driver.setK3Car(k3Car);
driver.drive();
Model3Car model3Car = new Model3Car();
driver.setK3Car(null);
driver.setModel3Car(model3Car);
driver.drive();
}
}
/*
자동차를 운전합니다.
K3Car.startEngine
K3Car.pressAccelerator
K3Car.offEngine
자동차를 운전합니다.
Model3Car.startEngine
Model3Car.pressAccelerator
Model3Car.offEngine
*/
위의 코드를 분석해보자. K3Car를 운전하던 운전자가 Model3 차량으로 변경해서 운전한다. driver.setK3Car(null)을 통해 기존 K3Car의 참조를 제거하고, driver.setModel3Car(model3Car)를 통해서 새로운 model3Car의 참조를 추가하고 메서드를 호출하는 것이다.
현재 상황을 보자면, Driver는 K3Car도 알고 있고, Model3Car도 알고 있다. 하지만 특정 차량을 타다가 바꾸고 싶다면 참조를 빼고 다시 끼워 넣어야 한다.

겨우 차량 1대를 추가했을 뿐인데, 코드량이 훌쩍 많이진 것이 확 체감된다. 만약 수십, 수백 개의 차량이 추가된다면…? 그야말로 재앙이다. 지금 차량이 늘어날 때마다 운전자의 코드를 계속해서 뜯어 고쳐야 하는 안타까운 상황이다. 역할과 구현으로 분리해야 할 때가 온 것이다. 이제 클라이언트 코드를 수정할 일이 없도록 만들자.
Driver 코드 상에 구체적인 K3Car, Model3Car 말고, 차(Car) 역할만 적혀 있도록 설계하면 된다.

package poly.car1;
public interface Car {
void startEngine();
void pressAccelerator();
void offEngine();
}
package poly.car1;
public class K3Car implements Car {
@Override
public void startEngine() {
System.out.println("K3Car.startEngine");
}
@Override
public void pressAccelerator() {
System.out.println("K3Car.pressAccelerator");
}
@Override
public void offEngine() {
System.out.println("K3Car.offEngine");
}
}
package poly.car1;
public class Model3Car implements Car {
@Override
public void startEngine() {
System.out.println("Model3Car.startEngine");
}
@Override
public void pressAccelerator() {
System.out.println("Model3Car.pressAccelerator");
}
@Override
public void offEngine() {
System.out.println("Model3Car.offEngine");
}
}
package poly.car1;
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를 가지고, 외부에서 setCar(Car car) 메서드를 누군가 호출함으로써 새로운 자동차를 참조하거나 변경할 수 있도록 멤버 변수에 참조값을 설정한다.
package poly.car1;
public class CarMain1 {
public static void main(String[] args) {
Driver driver = new Driver();
K3Car k3Car = new K3Car();
driver.setCar(k3Car);
driver.drive();
Model3Car model3Car = new Model3Car();
driver.setCar(model3Car);
driver.drive();
}
}
/*
자동차를 설정합니다: poly.car1.K3Car@4f023edb
자동차를 운전합니다.
K3Car.startEngine
K3Car.pressAccelerator
K3Car.offEngine
자동차를 설정합니다: poly.car1.Model3Car@7adf9f5f
자동차를 운전합니다.
Model3Car.startEngine
Model3Car.pressAccelerator
Model3Car.offEngine
*/
그림을 통해 과정을 자세하게 들여다 보면 아래와 같다.


결과적으로 Car 인터페이스는 껍데기만 제공하고, 실제 기능에 대한 구현은 K3Car, Model3Car 등 인스턴스의 오버라이딩 된 메서드로 되어 있다. setCar() 메서드를 통해 참조값을 유연하게 교체하여 다른 인스턴스를 참조하도록 참조값을 car 멤버 변수에 넣는다. 멤버 변수는 Car 타입이기 때문에 Car를 바라보지만, 하위 인스턴스에 메서드 오버라이딩을 해 놓았으므로 해당 인스턴스의 기능이 호출되는 것이다.
좋은 객체 지향 설계 원칙 중에 OCP 원칙이라는 것이 있다. 바로 새로운 기능의 추가나 변경이 있을 때, 기존 코드는 확장할 수 있어야 한다는 것이다. 그리고 기존 코드는 수정되지 않아야 한다.
뭔가 이상하다. 확장해야 하는데 기존 코드를 수정하지 않을 수가 있나? 일단 위의 예제에서 작성했던 코드가 이 이상한 작업을 잘 수행한 코드다.
확인을 위해 위의 코드에서 새로운 차량을 하나 추가해보도록 하자.
package poly.car1;
public class NewCar implements Car {
@Override
public void startEngine() {
System.out.println("NewCar.startEngine");
}
@Override
public void pressAccelerator() {
System.out.println("NewCar.pressAccelerator");
}
@Override
public void offEngine() {
System.out.println("NewCar.offEngine");
}
}
package poly.car1;
public class CarMain1 {
public static void main(String[] args) {
Driver driver = new Driver();
K3Car k3Car = new K3Car();
driver.setCar(k3Car);
driver.drive();
Model3Car model3Car = new Model3Car();
driver.setCar(model3Car);
driver.drive();
NewCar newCar = new NewCar();
driver.setCar(newCar);
driver.drive();
}
}
/*
자동차를 설정합니다: poly.car1.K3Car@4f023edb
자동차를 운전합니다.
K3Car.startEngine
K3Car.pressAccelerator
K3Car.offEngine
자동차를 설정합니다: poly.car1.Model3Car@7adf9f5f
자동차를 운전합니다.
Model3Car.startEngine
Model3Car.pressAccelerator
Model3Car.offEngine
자동차를 설정합니다: poly.car1.NewCar@5674cd4d
자동차를 운전합니다.
NewCar.startEngine
NewCar.pressAccelerator
NewCar.offEngine
*/
새로운 차량 NewCar를 추가했다. 프로그램 전체에서 이 차량을 이용하는 핵심 클라이언트는 누굴까? 바로 Driver다. 근데 지금 NewCar 클래스 하나 만들고, main() 메서드에 인스턴스를 생성하고, 참조값 넣어서 메서들 호출하는 코드만 추가해준 것이 전부다. 이처럼 새로운 차량을 추가해도 Driver의 코드에는 손도 대지 않았다.

확장에 열려 있다는 의미는... Car 인터페이스를 사용해서 새로운 차량을 자유롭게 추가할 수 있다는 말이다. 그리고 Car 인터페이스를 사용하는 클라이언트 코드 Driver도 Car 인터페이스를 통해 새롭게 추가된 차량을 자유롭게 호출할 수 있다.
코드 수정은 닫혀 있다는 의미는... 당연히 새로운 차를 추가하게 되면 기능이 추가되기 때문에 기존 코드의 수정은 불가피하다. 분명히 코드 어딘가 수정해야 하는데, 여기서 변하지 않는 부분은 새로운 자동차를 추가할 때 가장 영향을 받는 중요한 클라이언트, 바로 Car의 기능을 사용하는 Driver다. 변하는 부분은 새로운 차를 생성하고, Driver에게 필요한 차를 전달해주는 main() 메서드다.
최종 정리하면…
Car를 사용하는 클라이언트 코드인 Driver 코드의 변경 없이 새로운 자동차를 확장할 수 있다.
디자인 패턴 중 가장 중요한 패턴을 하나 꼽으라면 바로 “전략 패턴” 을 꼽을 수 있다. 이 패턴은 알고리즘을 클라이언트 코드의 변경 없이 쉽게 교체할 수 있다. 위의 코드가 바로 전략 패턴을 사용한 코드다. Car 인터페이스가 바로 “전략” 을 정의하는 인터페이스가 되고, 각각의 차량이 전략의 구체적인 구현이 되는 것이다. 그리고 전략을 클라이언트 코드(Driver)의 변경 없이 손쉽게 교체할 수 있다.