[Design Pattern] 코틀린 고차함수와 인터페이스 활용

공부는 혼자하는 거·2023년 6월 7일
0

Spring Tip

목록 보기
42/52

리팩토링

최근 기존 레거시 코드를 리팩토링하는데 있어 쓸만하고 유용한 패턴 몇 가지를 소개하겠다. 코드를 작성하다보면, 어느 순간 비슷한 역할을 하는 중복코드가 생기게 되고 이를 공통모듈로 추상화하고 싶은 욕구가 생긴다. 그럴 때 우리는 보통 인터페이스를 활용한다.
다음과 같이 공통된 기능을 정의하는 인터페이스가 존재하고, 그 인터페이스의 구현체가 여러개 있을 때..

코틀린 고차함수 활용

    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

profile
시간대비효율

0개의 댓글