최근 기존 레거시 코드를 리팩토링하는데 있어 쓸만하고 유용한 패턴 몇 가지를 소개하겠다. 코드를 작성하다보면, 어느 순간 비슷한 역할을 하는 중복코드가 생기게 되고 이를 공통모듈로 추상화하고 싶은 욕구가 생긴다. 그럴 때 우리는 보통 인터페이스를 활용한다.
다음과 같이 공통된 기능을 정의하는 인터페이스가 존재하고, 그 인터페이스의 구현체가 여러개 있을 때..
enum class SignType(
) {
FACEBOOK,
WECHAT,
GOOGLE,
APPLE,
companion object {
@JsonCreator
fun from(str: String): SignType? {
return SignType.values().firstOrNull {
it.name == str.uppercase()
}
}
}
}
interface SocialSignService {
fun supports(signType: SignType): Boolean
fun signIn(socialTokenDto: SocialTokenDto): TokenDto //로그인
fun signUp(socialSignUpDto: SocialSignUpDto, acceptLanguage: String): TokenDto //회원가입
}
@Service
class AppleSocialSignServiceImpl(
private val socialUtil: SocialUtil,
private val om: ObjectMapper,
private val appleTemplate: RestTemplate,
) : SocialSignService {
private val log = KotlinLogging.logger { }
override fun supports(signType: SignType): Boolean {
return signType == SignType.APPLE
}
@Transactional
override fun signIn(socialTokenDto: SocialTokenDto): TokenDto {
TODO()
}
@Transactional
override fun signUp(socialSignUpDto: SocialSignUpDto, acceptLanguage: String): TokenDto {
TODO()
}
}
@Service
class FaceBookSocialSignServiceImpl(
private val om: ObjectMapper,
private val facebookTemplate: RestTemplate,
private val socialUtil: SocialUtil,
) : SocialSignService {
private val log = KotlinLogging.logger { }
override fun supports(signType: SignType): Boolean {
return signType == SignType.FACEBOOK
}
@Transactional
override fun signIn(socialTokenDto: SocialTokenDto): TokenDto {
TODO()
}
@Transactional
override fun signUp(socialSignUpDto: SocialSignUpDto, acceptLanguage: String): TokenDto {
TODO()
}
}
@Service
class GoogleSocialSignServiceImpl(
private val googleTemplate: RestTemplate,
private val om: ObjectMapper,
private val socialUtil: SocialUtil,
) : SocialSignService {
private val log = KotlinLogging.logger { }
override fun supports(signType: SignType): Boolean {
return signType == SignType.GOOGLE
}
@Transactional
override fun signIn(socialTokenDto: SocialTokenDto): TokenDto {
TODO()
}
@Transactional
override fun signUp(socialSignUpDto: SocialSignUpDto, acceptLanguage: String): TokenDto {
TODO()
}
}
이렇게 SocialSignService 타입의 빈들이 여러개 띄워졌으므로, 이를 Dto의 SignType에 따라 다른 구현체를 반환해주는 팩토리메서드를 하나 만든다.
@Component
class SocialSignServiceFactory(
private val socialSignServices: List<SocialSignService>
) {
fun getSocialSignServiceBySignType(signType: SignType): SocialSignService {
return socialSignServices.firstOrNull { it.supports(signType) }
?: throw SocialTypeNotFoundException()
}
}
컨트롤러에서는 다음과 같이 해주면된다.
@PostMapping("/social")
fun signUpWithSocialToken(
@RequestHeader(
value = "accept-language",
required = false,
defaultValue = DEFAULT_LANGUAGE
) acceptLanguage: String,
@Valid @RequestBody socialSignUpDto: SocialSignUpDto
): SuccessResponse<*> {
return SuccessResponse(
ResultCode.OK, "social sign up: ${socialSignUpDto.signType} ",
socialSignServiceFactory.getSocialSignServiceBySignType(socialSignUpDto.signType)
.signUp(socialSignUpDto, acceptLanguage)
)
}
이렇게 동일한 인터페이스를 구현한 여러 클래스의 경우 로직도 거의 대동소이한 경우가 많다. 이럴 경우 공통된 코드들을 따로 모아서 제공해주는 유틸리티 클래스를 만들어 줄 수 있는데, 나 같은 경우, SocialUtil 이라는 클래스에 정의해두었다.
@Component
class SocialUtil(
private val userRepository: UserRepository,
private val om: ObjectMapper,
private val userProfileRepository: UserProfileRepository,
private val userEmailRepository: UserEmailRepository,
private val tokenService: TokenService,
private val encoder: BCryptPasswordEncoder,
private val awsSesService: AwsSesService,
) {
private val log = KotlinLogging.logger { }
fun getSignInTokenDtoBySocialToken(
socialToken: String,
method: (socialToken: String) -> OAuth2UserInfo
): BasicTokenDto {
val socialAuthDto = method.invoke(socialToken)
TODO()
}
fun getSignUpTokenDtoBySocialToken(
socialSignUpDto: SocialSignUpDto,
acceptLanguage: String,
method: (socialToken: String) -> OAuth2UserInfo
): BasicTokenDto {
val socialToken = socialSignUpDto.token
val socialAuthDto = method.invoke(socialToken)
TODO()
}
fun socialVerifyByToken(
uri: String,
authorizedErrorCode: AccountErrorCode,
parseErrorCode: AccountErrorCode,
restTemplate: RestTemplate,
method: (responseEntity: ResponseEntity<String>) -> OAuth2UserInfo
): OAuth2UserInfo {
val responseEntity = restTemplate.getForEntity(uri, String::class.java)
val responseCode = responseEntity.statusCodeValue
if (responseCode != 200) {
// 어떤 이유로든지 타임아웃이라든지 어떤 이유를 에러 코드로 전달해야함
throw UserSignUpSocialUnauthorizedException(authorizedErrorCode)
}
try {
return method.invoke(responseEntity)
} catch (e: Exception) {
log.error { e.stackTraceToString() }
throw UserSignUpSocialParsingException(code = parseErrorCode)
}
}
}
코틀린의 고차함수를 사용하면, 더욱 쉽게 리팩토링 할 수 있다.
@Service
class GoogleSocialSignServiceImpl(
private val googleTemplate: RestTemplate,
private val om: ObjectMapper,
private val socialUtil: SocialUtil,
) : SocialSignService {
.... 생략
@Transactional
override fun signIn(socialTokenDto: SocialTokenDto): TokenDto {
return getGoogleSignInTokenDto(socialTokenDto.token)
}
@Transactional
override fun signUp(socialSignUpDto: SocialSignUpDto, acceptLanguage: String): TokenDto {
return getGoogleSignUpTokenDto(socialSignUpDto, acceptLanguage)
}
fun getGoogleSignInTokenDto(socialToken: String) = socialUtil.getSignInTokenDtoBySocialToken(
socialToken = socialToken,
method = {
val uri = "/tokeninfo?id_token=$socialToken"
makeGoogleAuthDto(uri)
}
)
fun getGoogleSignUpTokenDto(socialSignUpDto: SocialSignUpDto, acceptLanguage: String) = socialUtil.getSignUpTokenDtoBySocialToken(
socialSignUpDto = socialSignUpDto,
acceptLanguage = acceptLanguage,
method = {
val uri = "/tokeninfo?id_token=${socialSignUpDto.token}"
makeGoogleAuthDto(uri)
}
)
이런 식으로 하면, 특정 도메인에 종속된 구현체 클래스는 그 도메인에 속한 로직만 담당하면 되고, 나머지는 공통모듈로 몰아넣을 수 있게 역할이 분리된다. 코드의 유지보수와 가독성에도 큰 도움이 된다.
위와 거의 비슷하지만, 조금 다른 패턴을 살펴보자면, 흔히 전략패턴이라고 불리는 인터페이스를 활용한 템플릿 콜백 패턴이 있다.
enum class PGType {
Kakao, Naver
}
interface PaymentStrategy {
fun supports(pgType: PGType): Boolean
fun createOrder(order: Order,
commonBillingProperty: CommonBillingProperty
) : Any
fun createSubscription(
commonBillingProperty: CommonBillingProperty,
subscription: Subscription
): Any
}
@Component
class KakaoStrategy(
private val om: ObjectMapper,
) : PaymentStrategy {
override fun supports(pgType: PGType): Boolean {
return pgType == PGType.Kakao
}
fun createOrder(order: Order,
commonBillingProperty: CommonBillingProperty
) : KakaoOrderRes {
TODO()
// ... 이하동문
}
@Component
class NaverStrategy(
private val om: ObjectMapper,
) : PaymentStrategy {
override fun supports(pgType: PGType): Boolean {
return pgType == PGType.Naver
}
// ... 이하동문
전에는 특정 도메인에 속한 구현체가 공통모듈을 호출하는 역할을 했다면, 이번에는 반대이다.
@Service
class PaymentService(
private val paymentStrategys: List<PaymentStrategy>,
private val orderRepository: OrderRepository,
private val eventPublisher: ApplicationEventPublisher,
private val awsSesService: AwsSesService,
) {
fun getPaymentStrategy(pgType: PGType): PaymentStrategy {
return paymentStrategys.firstOrNull { it.supports(pgType) }
?: throw RuntimeException("cant support this $pgType ")
}
@Transactional
fun createOrder(
orderReq: OrderReqDto,
principalDetails: PrincipalDetail,
): Any {
val paymentStrategy = getPaymentStrategy(orderReq.pgType)
//공통코드
paymentStrategy.createOrder() //도메인 로직 호출
TODO()
}
.....
}
이렇게 모든 구현제의 메서드들을 인터페이스 메서드로 추상화시킬 수 있으면 참 좋을려만, 현실은 그렇게 녹녹치 않다. 어떨 때는 규격이 너무 달라, 추상화를 못 시키거나 하기 힘든 환경일 수 있다. 예를 들어 나 같은 경우 특정 PG의 경우, REST API를 제공해주지 않았다. 그래서, 완전히 다른 방식으로 접근 할 수 밖에 없었다. 그럴 때는 타입 캐스팅을 활용한다.
@Component
class NaverStrategy(
private val om: ObjectMapper,
) : PaymentStrategy {
override fun supports(pgType: PGType): Boolean {
return pgType == PGType.Naver
}
// ... 이하동문
fun specialCaseFunc(){
}
....
@Service
class PaymentService(
private val paymentStrategys: List<PaymentStrategy>,
) {
...
@Transactional
fun naverSpecialCaseFunc(pgType: PGType){
val naverStrategy = getPaymentStrategy(pgType) as NaverStrategy
naverStrategy.specialCaseFunc()
}
처음부터 디자인 패턴을 고려하며 짜긴 보다, 일단 막코딩 하고, 어느정도 구현이 됐으면(그럼 패턴이 보인다)
수시때때로 점검해서 고치는 식으로 하자. 이와 관련해서 좋은 생각을 할 수 있는 아티클들이 많다. 몇 가지 소개해본다.
리팩토링 - 조금씩, 문제가 생기지 않게, 자주
https://velog.io/@gomjellie/The-Wet-Codebase?utm_source=oneoneone
https://www.youtube.com/watch?v=mNPpfB8JSIU&ab_channel=%EB%8D%B0%EB%B8%8C%EC%9B%90%EC%98%81DVWY
https://mangkyu.tistory.com/252
https://www.podo-dev.com/blogs/299