OCP는 SOLID 원칙 중 하나인데요.
개방-폐쇄 원칙으로써, 다음과 같은 의미를 가집니다.
확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.
그런데 저는 실제로 프로젝트를 진행해보면서, 확장하는데 기존 코드를 수정하는 일이 너무 많았고
사실 OCP를 지킬 수 있는건가 라는 생각을 해왔습니다.
그래서 고민하던 중, 의존성 주입을 받을때 여러 클래스에 대한 것을 한꺼번에 주입받고 싶을때, List
이에 대해 공유하고자합니다.
AS-IS
햄버거를 만들 때, 특정 재료로 햄버거를 만든다고 해봅시다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
class ChefService(
private val shrimpService: ShrimpBurgerService,
private val cheeseService: CheeseBurgerService,
private val beefService: BeefBurgerService
) {
fun makeBurger(type: BurgerType) {
when (type) {
BurgerType.SHRIMP -> shrimpService.createBurger()
BurgerType.CHEESE -> cheeseService.createBurger()
BurgerType.BEEF -> beefService.createBurger()
else -> throw NotTypeException("재료가 없어요.")
}
}
}
위처럼 인자로 받은 type에 따라 특정 서비스가 호출되어 햄버거를 생성하게 되는 구조인데요.
그리고 각 버거 서비스는 아래와 같이 되어있습니다.
1
2
3
4
5
6
7
@Service
class ShrimpBurgerService {
fun createBurger() {
// 햄버거 생성
}
}
만약 이러한 구조에서, 새로운 햄버거를 만드는 요청이 들어왔다면 어떻게 될까요?
예를들어 치킨 버거를 만들 수 있게 해달라는 요청이 왔다고 가정해보면, ChefService 는 다음과 같이 변경되어야합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
class ChefService(
private val shrimpService: ShrimpBurgerService,
private val cheeseService: CheeseBurgerService,
private val beefService: BeefBurgerService,
private val chickenService: ChickenBurgerService
) {
fun makeBurger(type: BurgerType) {
when (type) {
BurgerType.SHRIMP -> shrimpService.createBurger()
BurgerType.CHEESE -> cheeseService.createBurger()
BurgerType.BEEF -> beefService.createBurger()
BurgerType.CHICKEN -> chickenService.createBurger()
else -> throw NotTypeException("재료가 없어요.")
}
}
}
기능을 확장했더니, 기존 코드를 수정하는 일이 발생했습니다.
햄버거 서비스는 신메뉴가 나올때마다 추가될 것이고, 신메뉴가 나올때마다 해당 서비스의 코드는 계속해서 추가되어야합니다.
이를 해결하기 위해서는 어떻게 해야할까요?
다형성
다형성은 하나의 객체가 여러가지 타입을 가질 수 있는 것을 의미합니다.
저희는 보통 다형성을 이야기할 때, 부모 클래스를 상속받아 메서드를 오버라이딩하여 구현하는 것을 통해 다형성을 구현할 수 있는데요.
이를 통해 자식 클래스는 부모의 함수를 서로 다른 방법으로 구현할 수 있게 됩니다.
마찬가지로, 위 버거 클래스에도 다형성을 적용해서 문제를 해결할 수 있습니다.
스프링에서는 한 부모 클래스를 상속받은 클래스가 여러개일 경우, List를 통해 여러가지 자식 클래스를 사용할 수 있습니다.
TO-BE
이제 각 재료별 버거 서비스에 대한 부모 클래스를 정의해봅시다.
1
2
3
4
interface BurgerService {
fun checkType(type: BurgerType): Boolean
fun createBurger() : Burger
}
위처럼 인터페이스를 정의하고, 각 재료별 버거 서비스에서 상속받으면 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
@Service
class ShrimpBurgerService : BurgerService {
override fun checkType(type: BurgerType) = type == BurgerType.SHRIMP
override fun createBurger() = Burger("새우버거")
}
@Service
class ChickenBurgerService : BurgerService {
override fun checkType(type: BurgerType) = type == BurgerType.CHICKEN
override fun createBurger() = Burger("치킨버거")
}
이제 ChefService를 수정해보면,
1
2
3
4
5
6
7
8
9
10
11
12
@Service
class ChefService(
private val burgerServices : List<BurgerService>
) {
fun makeBurger(type: BurgerType) {
for (burgerService in burgerServices) {
if (burgerService.checkType(type)) {
val burger = burgerService.createBurger()
}
}
}
}
리스트로 버거 서비스에대한 자식 클래스를 컴포넌트로 모두 받기 때문에 이제 makeBurger()
함수는 for문으로 버거 서비스를 순회하고, 맞는 BurgerType
인 경우 버거를 만들게 됩니다.
그리고 이제 새로운 버거를 만드는 서비스를 컴포넌트로 추가하더라도 ChefService
에는 수정이 필요없어집니다.
이처럼, 다형성을 잘 활용하면 OCP를 지키는 것도 생각보다 간단해집니다.
부모 인터페이스를 List로 받을 경우 자식 컴포넌트가 모두 할당받을 수 있다는 것도 이번 기회에 처음 알았는데요.
앞으로 비슷한 서비스가 계속 확장될 여지가 있고 상속 없이 구현되어있다면, 다형성을 활용해서 객체지향적 원칙을 지킬 수 있을 것 같습니다.