개발을 하다보면 VIP 유저, 일반 유저 등의 여러 상황과 정책에 대응하기 위해 하나의 인터페이스가 여러 구현체를 갖게되는 경우가 더러 있다.
위와 같은 경우 특정 상황에서 특정 구현체를 찾아주어야 하는 팩토리 클래스를 만들어야 하는데, 이 팩토리 클래스에 If-else문 혹은 switch, when 등을 사용하기 쉽상이다.
이때, 어떻게 대응해야할지 고민과 그 결과를 공유해보고자한다.
if-else를 사용한 팩토리 클래스
VVIP 유저, VIP 유저, GOLD, SILVER 유저마다 다르게 할인율을 계산해주는 정책이 있었다고 가정해보자.
보통의 상황에서 우리는 UserSalePolicy라는 인터페이스를 정의하고 각 유저에 맞는 정책들을 구현할 것이다.
interface UserSalePolicy {
fun sale(price: Long): Long
}
class VIPUserSalePolicy : UserSalePolicy {
override fun sale(price: Long): Long {
return (price * 0.95).toLong()
}
}
class VVIPUserSalePolicy : UserSalePolicy {
override fun sale(price: Long): Long {
return (price * 0.9).toLong()
}
}
class DefaultUserSalePolicy : UserSalePolicy {
override fun sale(price: Long): Long {
return price
}
}
그렇다면 각 유저의 등급에 맞는 정책들을 찾아주기 위해 Factory 클래스를 만들어보자.
enum class UserGrade {
VVIP,
VIP,
GOLD,
SILVER,
}
class UserSalePolicyFactory {
fun create(userGrade: UserGrade): UserSalePolicy {
return when (userGrade) {
UserGrade.VVIP -> VVIPUserSalePolicy()
UserGrade.VIP -> VIPUserSalePolicy()
UserGrade.GOLD -> DefaultUserSalePolicy()
UserGrade.SILVER -> DefaultUserSalePolicy()
}
}
}
이제 위 Factory를 어디서든 사용만 해주면 된다.
OCP 위반과 스프링 빈에서 사용될 때의 문제점
위 구현에서 문제점은 무엇이 있을까?
내가 생각한 문제들은 다음과 같다.
- 유저 등급이 추가될 때마다 Factory클래스의 메서드를 수정해야한다. 즉, OCP를 위반한다. 이렇게 되면 구현체가 늘어날 때마다 유지보수가 힘들어진다.
- 스프링 빈(대부분의 경우 서비스)의 메서드에서 쓰게되면 굳이 상태를 갖지 않아 하나만 생성해도 되는 객체들을 요청마다 만들어서 사용하게 된다.
if-else를 사용하지 않는 유연한 팩토리 클래스 구현하기
스프링 프레임워크에서 주로 사용하는 방법으로 if-else 문을 제거해보자.
먼저 UserSalePolicy 인터페이스에서 인터페이스의 구현체들이 어떤 경우에 자신이 처리할 수 있는지 알리기 위해 supports라는 메서드를 하나 추가한다.
interface UserSalePolicy {
fun sale(price: Long): Long
fun support(userGrade: UserGrade): Boolean
}
각 구현체에서는 supports 메서드를 구현한다.
class DefaultUserSalePolicy : UserSalePolicy {
override fun sale(price: Long): Long {
return price
}
override fun support(userGrade: UserGrade): Boolean {
return userGrade == UserGrade.GOLD || userGrade == UserGrade.SILVER
}
}
class VIPUserSalePolicy : UserSalePolicy {
override fun sale(price: Long): Long {
return (price * 0.95).toLong()
}
override fun support(userGrade: UserGrade): Boolean {
return userGrade == UserGrade.VIP
}
}
class VVIPUserSalePolicy : UserSalePolicy {
override fun sale(price: Long): Long {
return (price * 0.9).toLong()
}
override fun support(userGrade: UserGrade): Boolean {
return userGrade == UserGrade.VVIP
}
}
이제 Factory클래스에서 유저 등급에 맞는 적절한 구현체를 찾기 위해서는 UserSalePolicy 인터페이스의 구현체들을 모두 알아야할 필요가 있다.
모든 구현 정책들과 Factory를 스프링 빈으로 등록(당연히 내부의 상태를 관리하지 않기 때문에 가능하다)해두고, Factory에서는 의존성 주입을 통해 구현체들을 모두 주입받는다.
스프링에서는 여러 구현체가 존재할 때 이를 리스트로 받을 수 있다.
@Component
class VIPUserSalePolicy : UserSalePolicy {
override fun sale(price: Long): Long {
return (price * 0.95).toLong()
}
override fun support(userGrade: UserGrade): Boolean {
return userGrade == UserGrade.VIP
}
}
@Component
class UserSalePolicyFactory (
private val userSalePolicyList: List<UserSalePolicy>
){
fun create(userGrade: UserGrade): UserSalePolicy {
return userSalePolicyList.firstOrNull { it.support(userGrade) } ?: DefaultUserSalePolicy()
}
}
이제, 새로운 구현체가 생기더라도 팩토리 클래스를 수정할 필요가 없어 OCP 원칙을 지킬 수 있다.
enum을 사용하여 팩토리 클래스를 제거하기
위의 구조도 충분히 확장성있고, 좋은 설계라고 생각한다.
다만, 내가 위 구조에 대해 생각한 문제는 다음과 같다.
- 스프링에 의존적이다.
- 스프링에 의존적인 구조로 스프링의 빈주입 기법이 없다면 쓸 수 없다.
- 스프링에 의존하게 됨으로써, domain layer에 있을 수 없게 된다.
- 구조가 복잡하다
- 처음 보는 사람은 구조를 하나하나 파악하고, ‘스프링 빈으로 구현체만 만들면 구현체가 등록이되나?’라는 생각을 하게끔 만든다.
그래서 위와 같이 enum을 사용하는 경우라면 Factory를 굳이 만들지 않고, enum을 통해 다음과 같이 쉽게 if-else를 제거할 수 있다.
enum class UserGrade(
private val userSalePolicy: UserSalePolicy
) {
VVIP(VVIPUserSalePolicy()),
VIP(VIPUserSalePolicy()),
GOLD(DefaultUserSalePolicy()),
SILVER(DefaultUserSalePolicy());
fun sale(price: Long): Long {
return userSalePolicy.sale(price)
}
}
이렇게 된다면 모든 구현체들이 굳이 스프링에 의존하지 않더라도 if-else문을 제거할 수 있다.
상태를 갖지 않으므로, class가 아닌 object로 만들어도 괜찮을 듯 싶다.
혹시 구조에 의문이 있거나 궁금한 점이 있다면 언제든 댓글 남겨주세요! 감사합니다 🙂
'서버' 카테고리의 다른 글
Redis에서 사용하는 분산락 알고리즘인 RedLock에 대해 알아보자 (0) | 2023.09.16 |
---|---|
nGrinder 성능 테스트 측정 삽질 일기 (Docker를 통한 설치 + war 파일을 통한 설치 포함) (0) | 2023.09.02 |
서버의 구동 원리 - APM (0) | 2022.07.11 |
서버란? 서버 프로그램 NGINX (0) | 2022.07.11 |