해당 내용은 '스프링 입문을 위한 자바 객체 지향의 원리와 이해'와 인프런 김영한님의 '스프링 핵심 원리 - 기본편' 강의를 참고하였습니다.
이전 글에서 객체 지향 설계 원칙의 함정이라는 내용을 썼었다.
그 글의 마지막에 스프링에서는 DI를 통해서 다형성, 그리고 OCP, DIP를 가능하게 하였다고 했는데 우리는 그에 대한 코드를 보며 확인해보자.
이해를 위해 간단한 코드를 작성하였다.
빠른 예시 작성을 위해 설계는...그말싫 :3
GasolineOil.java
public class GasolineOil implements FuelTank{
@Override
public Energy getEnergy() {
String type = "Gasoline";
Energy energy = new Energy();
energy.setEnergy(type);
return energy;
}
}
ElectricBattery.java
public class ElectricBattery implements FuelTank {
@Override
public Energy getEnergy() {
String type = "eletric";
Energy energy = new Energy();
energy.setEnergy(type);
return energy;
}
}
CarImpl.java
public class CarImpl implements Car{
// private final FuelTank fuelTank = new GasolineOil();
private final FuelTank fuelTank = new ElectricBattery();
@Override
public Energy runEngine() {
return fuelTank.getEnergy();
}
}
위의 코드에서 구현 객체를 변경하려면 클라이언트 코드를 변경해야한다. (Gasoline -> ElectricBattery)
그렇기에 이는 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경에는 닫혀있어야 한다
라는 OCP 원칙을 위반하는 코드라고 할 수 있다.
또한 위의 코드에서 CarImpl는 CarFuelTank 라는 추상화 클래스에 의존하는 것 뿐아니라, ElectricBattrey라는 구체화 클래스에도 동시에 의존하고 있다.
즉, CarImpl 클라이언트는 구현 클래스를 직접 선택(의존)하고 있다고 할 수 있다.
이는 프로그래머는 추상화에 의존해야한다.
는 DIP 원칙에도 위반된다고 할 수 있다.
위의 코드에 대해 우리는 의존성 주입
을 통해 문제점을 해결할 것이다.
아래와 같은 파일을 하나 만들어보자.
ApplicationConfig.java
public class ApplicationConfig {
public Car carFuelTank() {
return new CarImpl(getFuelTank());
}
public FuelTank getFuelTank() {
return new GasolineOil();
// return new ElectricBattery();
}
}
위의 코드를 통해서 우리는 이제 구현 객체를 생성하고, 연결하도록 위의 코드에 책임을 쥐어줄 것이다.
또한 이제 위처럼 ApplicationConfig의 생성에 따라 위의 CarImpl 클래스의 코드는 다음과 같이 고쳐져야 할 것이다.
CarImpl.java
public class CarImpl implements Car {
// private final FuelTank fuelTank = new GasolineOil();
// private final FuelTank fuelTank = new ElectricBattery();
private final FuelTank fuelTank;
public CarImpl(FuelTank fuelTank) {
this.fuelTank = fuelTank;
}
@Override
public Energy runEngine() {
return fuelTank.getEnergy();
}
}
그 전 코드에서 구현객체의 생성에 대한 책임
, 그리고 메서드를 실행하는 책임
이라는 2가지 책임을 떠맡았던 CarImpl 클라이언트는 이제 한 가지 책임만 갖게 되었고, 이는 곧 한 클래스는 하나의 책임만 가져야한다
라는 SRP 원칙에 맞는 객체지향 설계라는 것을 알 수 있다.
자, 이제 저 생성자를 통해서 들어오게 되어있는 FuelTank는 어디서 들어올까?
위의 ApplicationConfg 코드를 보면 getFuelTank()
메서드를 통해서 생성된 객체를 주입받는 것을 알 수 있다.
자, 이제 앱을 실행하는 부분을 보자.
CarApp.java
public class CarApp {
public static void main(String[] args) {
// DI 도입
ApplicationConfig applicationConfig = new ApplicationConfig();
Car car = applicationConfig.carFuelTank();
System.out.println("energy = " + car.runEngine().getEnergy());
}
}
위의 코드처럼 ApplicationConfig 객체를 생성해서 이를 통해 의존성들을 주입받는다.
우리는 아까 위에서 가솔린 오일을 연료로 쓰기로 하였으니 결과는 다음과 같이 나올 것이다.
energy = Gasoline
이제 연료를 바꾸려면 어떻게 해야할까?
우리는 이제 맨 처음처럼 OCP와 DIP를 위반하지 않으면서 바꿀 수 있다.
이제 CarImpl
는 처음의 코드처럼 구체화 클래스인 GasolineOil
또는 ElectricBattery
에 의존하지 않는다. 이제는 오직 FuelTank
라는 추상화 클래스에만 의존한다.
이제 ApplicationConfg가 GasolineOil
객체 인스턴스를 클라이언트 코드 대신에 클라이언트 코드에 의존 관계를 주입해준다!
이제 프로그래머는 추상화에 의존해야한다.
라는 DIP 원칙이 적용되었음을 알 수 있다.
또한 연료를 변경할 때, ApplicationConfig가 의존관계를 GasolineOil
에서 ElectricBattery
로 변경해서 CarImpl 클라이언트 코드에 주입하기에, 클라이언트 코드는 변경하지 않아도 된다.
이를 통해서 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경에는 닫혀있어야 한다
는 OCP 의 원칙에도 새롭게 변경한 코드가 적용된다는 것을 알 수 있다.
이를 통해서 의존성 주입
을 통해서 우리는 코드를 작성할 때 좋은 객체지향 설계를 위한 원칙을 적용할 수 있었다!