이전 포스팅에서 좋은 객체 지향 설계란 무엇인가 생각해보았다.
https://braindisk.tistory.com/25
역할과 구현을 잘 분리하면 다형성이 어느 정도 보장됨을 느낄 수 있었지만 분명 부족한 부분이 있었다. 오늘은 그 부족함에 대해서 코드와 함께 살펴볼 것이다.
먼저 전체적인 도메인 설계를 진행하자.
전체 도메인 설계는 개발자가 아니어도 보고 이해할 수 있도록 구성되어 있다.
클래스 다이어그램은 개발자가 보기 편하게 구성되어 있다.
객체 다이어그램은 런타임에 동적으로 구성되는 다이어그램이다. 런타임에 Car 구현 객체가 변경되면 객체 다이어그램도 변경될 가능성이 있다.
이제 위에 설계를 토대로 인터페이스를 정의하고 구현 객체를 생성하는 순서로 개발을 진행해보자.
먼저 자동차의 역할을 인터페이스로 정의해보자. (아주 간단하게)
public interface Car {
public void engineStart();
public void drive();
public void engineStop();
}
특정 자동차 구현 객체를 정의해보자. (K5)
public class K5 implements Car {
private final String carName = "K5";
@Override
public void engineStart() {
System.out.println(carName + " 엔진 스타뜨");
}
@Override
public void drive() {
System.out.println(carName + " 드라이브");
}
@Override
public void engineStop() {
System.out.println(carName + " 엔진 스탑");
}
}
그리고 조금 비싼 BMW
public class BMW implements Car {
private final String carName = "BMW i8";
@Override
public void engineStart() {
System.out.println(carName + " 엔진 스타뜨");
}
@Override
public void drive() {
System.out.println(carName + " 드라이브");
}
@Override
public void engineStop() {
System.out.println(carName + " 엔진 스탑");
}
}
운전자 객체를 정의해보자.
public class Driver {
private final String driverName;
private final int driverAge;
private final String driverAddress;
public Driver(String driverName, int driverAge, String driverAddress) {
this.driverName = driverName;
this.driverAge = driverAge;
this.driverAddress = driverAddress;
}
@Override
public String toString() {
return "DriverImpl{" +
"driverName='" + driverName + '\'' +
", driverAge=" + driverAge +
", driverAddress='" + driverAddress + '\'' +
'}';
}
}
이제 면허 서비스 인터페이스를 정의해보자. 면허는 자동차 정보와 운전자 정보를 모두 볼 수 있어야 한다.
public interface LicenseService {
public void printCarName();
public void printDriverInfo();
}
면허 서비스 구현 객체를 정의해보자.
public class LicenseImpl implements LicenseService {
private final Car car = new K5();
private final Driver driver = new Driver("Kim", 25, "suwon-si");
@Override
public void printCarName() {
System.out.println("car Name = " + car.getCarName());
}
@Override
public void printDriverInfo() {
System.out.println(driver.toString());
}
}
이제 우리는 경찰이 되었다고 생각하자. 아래 애플리케이션은 경찰이 면허증을 검사하는 코드이다.
public class LicenseApp {
public static void main(String[] args) {
System.out.println("검문입니다~!");
License license = new LicenseImpl();
license.printCarName();
license.printDriverInfo();
}
}
애플리케이션은 문제없이 잘 실행된다. 그런데 문제가 있다. 만약 자동차의 종류가 변경된다면 우리는 LicenseServiceImpl 구현체를 변경해야 한다. 분명 역할과 구현을 잘 나눴음에도 불구하고 요구사항 변경에 유연하게 대처할 수 없는 것이다. 지금은 코드가 간단해서 LicenseServiceImpl 구현체의 코드만 수정하면 된다고 위안 삼을 수 있지만, 코드가 훨씬 복잡해지면 장담할 수 없는 상황이 올 수도 있다.
즉 객체 지향 설계 원칙 중 OCP, DIP의 원칙을 잘 지키지 못하는 것이다. OCP에 따라서 변경에는 닫혀있고 확장에는 열려 있어야 하는데 변경에도 열려있는 상황이다. 또한 DIP에 따라서 객체는 추상화에만 의존해야 하는데 다음과 같이 구현 객체에 의존하고 있다. (분명하다) 또한 운전자 정보를 동적으로 변경할 수 있는 방법도 필요하다.
public class LicenseServiceImpl implements LicenseService {
private final Car car = new K5();
private final Driver driver = new Driver("Kim", 25, "suwon-si");
...
}
이를 해결하기 위한 방법은 의존성 주입을 사용하면 된다. 면허 서비스가 의존하고 있는 객체를 동적으로 주입해주는 방법이다. 의존성 주입은 생성자를 사용하면 간단하게 구현할 수 있다.
public class LicenseServiceImpl implements LicenseService {
private final Car car;
private final Driver driver;
public LicenseServiceImpl(Car car, Driver driver) {
this.car = car;
this.driver = driver;
}
...
}
이제 다음 문제는 면허 서비스 구현 객체를 생성할 때 의존성 주입을 어떻게 처리해줄지다. 이 문제는 의존성을 전역에서 관리하는 AppConfig로 다음과 같이 사용 영역과 구성 영역을 분리해서 해결할 수 있다. 즉 관심 영역을 분리해서 AppConfig는 의존성만을 관리하도록 하고 다른 인터페이스나 객체들은 자신들의 역할에 집중할 수 있도록 하는 것이다.
AppConfig 코드를 살펴보면 의존 관계가 어떻게 되는지 시각적으로 한 번에 파악할 수 있다. AppConfig 코드를 보면 동적으로 운전자 정보를 넘겨주었을 때 LicenseServiceImpl를 반환하도록 했고 Car의 구현체는 K5로 설정했음을 알 수 있다. (운전자 정보를 저장하는 서비스나 저장소가 있으면 좋지만 간단하게 예제를 구성하느라 부족한 점이 있음..) 이렇게 의존 관계가 한눈에 보이는 것도 큰 도움이 된다고 생각한다.
public class AppConfig {
public LicenseService licenseService(Driver driver) {
return new LicenseServiceImpl(car(), driver);
}
public Car car() {
return new K5();
}
}
public class LicenseServiceApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
Driver driver = new Driver("owner", 25, "suwon-si");
LicenseService licenseService = appConfig.licenseService(driver);
licenseService.printCarName();
licenseService.printDriverInfo();
}
}
결론적으로 의존 관계 주입을 해주는 AppConfig를 사용하면 Car의 구현체를 변경하더라도 LicenseServiceImpl의 코드를 직접 수정할 일이 없고 OCP, DIP를 만족함을 알 수 있다. (변경 후 LicenseServiceImpl이 추상화에 의존하고 있다) 그리고 이 과정을 편리하게 만들어준 것이 스프링이라고 한다. 앞으로 스프링에 대해서 알아가면서 포스팅을 이어가 보도록 하겠다.
reference
'Spring' 카테고리의 다른 글
스프링 컴포넌트 스캔 (0) | 2022.04.03 |
---|---|
싱글톤의 단점과 스프링의 싱글톤 컨테이너 (0) | 2022.04.03 |
POJO에서 스프링으로의 전환 (0) | 2022.03.26 |
IoC (0) | 2022.03.25 |
좋은 객체 지향 설계란 (0) | 2022.03.24 |