지난 포스팅에서는 OCP와 DIP가 위배 되는 경우에 대해서 알아보았습니다. 이번에는 스프링이 아닌 순수 자바로 해결해보겠습니다.
잠깐 지난 글의 클래스 다이어그램을 살펴보겠습니다.
public class DriverImpl implements Driver{
Car car = new K3();
Engine engine = new Electronic();
@Override
public void findOwnCarDetail() {
car.findCarName();
car.findCarBrand();
engine.WayToMove();
}
}
DriverImpl 역할에 구현체가 직접 new 키워드로 선언 되어 있습니다. 이럴 경우 얼핏보면 OCP와 DIP를 잘 지킨 것 같지만 사실 아니라고 지난 포스팅에서 말씀드렸습니다.
이번에도 갓영한님의 말을 빌리겠습니다.
"로미오와 줄리엣 공연을 하면 로미오 역할을 누가 할지 줄리엣 역할을 누가 할지는 배우들이 정하는게 아니다. 이전 코드는 마치 로미오 역할(인터페이스)을 하는 레오나르도 디카프리오(구현체, 배우)가 줄리엣 역할(인터페이스)을 하는 여자 주인공(구현체, 배우)을 직접 초빙하는 것과 같다. 디카프리오는 공연도 해야하고 동시에 여자 주인공도 공연에 직접 초빙해야 하는 다양한 책임을 가지고 있다."
정말 그 어떤 예시보다 최고의 예시인것 같네요. 배우는 배우에만 몰두해야합니다. 공연 장소를 정하고, 역할에는 누굴 캐스팅 할지에 대해서는 배우가 아닌 [공연 기획자]가 결정해야 하는 부분입니다. 우리는 이런 역할을 해줄 [공연 기획자]가 필요합니다.
그리고 자바에서는 [공연 기획자]를 AppConfig라는 클래스가 담당하고 있습니다.
public class AppConfig {
public Driver driver(){
return new DriverImpl(car(), engine());
}
public Car car(){
return new K3();
}
public Engine engine(){
return new Electronic();
}
}
위 AppConfig는 순수 자바로만 구성되어 있습니다. 스프링에서 제공되는 어노테이션은 사용되지 않았습니다. 스프링으로 해결하는 방법은 다음 글에서 진행하겠습니다.
AppConfig에서 Car 역할(인터페이스)로는 어떤 구현체를 사용할 것인지, Engine 역할(인터페이스)로는 어떤 구현체를 사용할 것인지 주입해줘야 합니다. 그리고 Driver 역할(인터페이스)을 생성자를 통해 외부에서 생성할 수 있습니다.
public class DriverImpl implements Driver{
private final Car car;//역할만 알려주고 구현체는 무엇인지 알려줄 필요가 없다.
private final Engine engine;
public DriverImpl(Car car, Engine engine) {
this.car = car;//역할을 정해준다.
this.engine = engine;//역할을 정해준다.
}
@Override
public void findOwnCarDetail() {
car.findCarName();
car.findCarBrand();
engine.WayToMove();
}
}
Driver 구현체를 위와 같이 바꿔줄 수 있습니다. 이전 코드에서는 new K3(), new Fuel() 이런식으로 직접 역할을 지정해주었습니다. 하지만 위 코드는 자바가 이를 알아서 주입을 시켜줍니다.
이전에서는 구현체에 직접 K3, Fuel 지정해주었다. 한마디로 구현 객체가 프로그램의 제어 흐름을 스스로를 조종했습니다. 하지만 AppConfig를 생성한 뒤로는 구현 객체는 역할(인터페이스)들만 호출하지, 구현체에 대해서는 호출하지 않습니다. Driver 구현 객체는 Car, Engine이라는 역할에 어떤 구현체가 주입될 것인지는 전혀 알 수 없죠.
이렇듯 프로그램에 대한 제어 흐름을 외부가 아닌 AppConfig가 가지게 되는데 이를 [제어의 역전(IoC, Inversion of Control)]이라고 합니다. 또한 AppConfig가 구현 객체를 주입시켜주는 것을 [의존관계 주입(DI, Dependency Injection)]라고 합니다.
그리고 AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 DI 컨테이너(IoC 컨테이너)라고 합니다.
public class DrivingApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
Driver driver = appConfig.driver();
driver.findOwnCarDetail();
}
}
이런 식으로 알아서 객체가 알아서 주입되므로 외부에선 AppConfig를 보지 않는 한 어떠한 역할에 어떤 객체를 사용하는 지 알수 없죠. 알 필요도 없죠. 우리가 운전하는데 있어서 운전하는 방법만 알면 되지 엔진이 어떻게 작동하고, 바퀴는 어떻게 굴러가는 지 알 필요는 없잖아요? 하물며 자동차마다 엔진의 종류가 다른데 그거 몰라도 운전만 잘 하잖아요.
만약 K3 전기차가 아닌 K5 연료차로 바꾸고 싶다면 AppConfig의 코드만 수정하면 됩니다.
public class AppConfig {
public Driver driver(){
return new DriverImpl(car(), engine());
}
public Car car(){
return new K5();
}
public Engine engine(){
return new Fuel();
}
}
코드에서 알 수 있듯이 Driver 구현체에 더 이상 직접으로 객체를 생성하지 않습니다. 구현체에 의존하지 않죠. 또한 전기를 연료로 바꿀 때 구현체의 코드를 직접적으로 수정할 필요도 없습니다. AppConfig라는 [공연 기획자]가 다 해주니깐요.
이번 글에서 AppConfig라는 DI 컨테이너를 이용해서 OCP와 DIP를 해결해 보았습니다. 지금 당장은 코드 수가 적기 때문에 AppConfig를 만들어서 여기다가 선언하는 것이 어렵지 않습니다. 하지만 코드량이 많아질 경우 AppConfig로만 관리하기 힘들어집니다.
따라서 다음 글에서는 스프링을 사용하여 OCP와 DIP를 해결해보겠습니다. 어느 정도 왜 스프링을 사용하는 지 짐작이 가시죠?
'...' 카테고리의 다른 글
[Spring] Security +Google Oauth2 + JWT 구현하기 (1) - JWT 생성하기 (0) | 2022.06.04 |
---|---|
[Kotlin] 제네릭 타입과 variance 한정자를 활용하라 (2) | 2022.04.29 |
[Java] 스프링을 왜 사용할까?(1) - OCP와 DIP의 위배 (4) | 2022.01.20 |
[Docker] Mysql 컨테이너 sql-mode 관련 설정 with ONLY_FULL_GROUP_BY 에러 해결하기 (0) | 2021.12.12 |
[Linux] VM을 NFS로 사용하기 (0) | 2021.12.08 |