본문 바로가기

MY개발생각

[개발생각] 도메인에서 코틀린으로 컴포지션 표현

도메인에서, 비즈니스 로직을 구현하는경우, 도메인서비스를 나누고, 해당 서비스를 조합하여, 비즈니스 로직을 구현하는 케이스가 많다.

여기서 코드가 깔끔하지 않게 되는 부분이 존재한다.

 

아래 예제로 코드를 보면,

class UserRegisterService (
   val canUserJoinValidator:CanJoinTestUserValidator,
   val validUserChecker:ValidTestUserChecker,
   val rewardUserChecker:RewardTestUserChecker
) {
   
    fun register(user:User) {
      with(user) {
            canUserJoinValidator.canJoinCheck(this); //canUserJoinValidator.xxxx 매번 써줘야함 ㅠ
            validUserChecker.canValidUserCheck(this); //validUserChecker.xxxx 매번 써줘야함 ㅠ
            rewardUserChecker.doReward(this); //rewardUserChecker.xxxx 매번 써줘야함 ㅠ
       }
       //실제 유저 등록 로직.
    }
}

 

위 코드를 코틀린의 delegate로 구현하면 아래와 같이 표현할수있다.

동작은 동일하다.

class UserRegisterService (
   canUserJoinValidator:CanJoinTestUserValidator,
   validUserChecker:ValidTestUserChecker,
   rewardUserChecker:RewardTestUserChecker

) : CanJoinValidator by canUserJoinValidator, 
    ValidUserChecker by validUserChecker,
    RewardUserChecker by rewardUserChecker
{

    fun register(user:User) {
      with(user) {
            canJoinCheck(this); //canUserJoinValidator.xxxxx 기술없이, 메소드만 기술, 깔끔하다.
            canValidUserCheck(this); //canValidUserCheck.xxxxx 기술없이, 메소드만 기술, 깔끔하다.
            doReward(this);//doReward.xxxxx 기술없이, 메소드만 기술, 깔끔하다.
       }

       //실제 유저 등록 로직.
    }
}

컴포지션(구성)으로 비즈니스 로직을 조합할때 코드가 깔끔하게 된다.

delegate를 원활하게 사용하려면,

interface클래스가 필요한데, 도메인 레이어에서는 인프라스트럭처의 구현체를 모르게 하기위해,

인터페이스를 정의하기 때문에, delegate와도 궁합이 잘 맞기때문이다.

 

위와 같이 코딩을 하면,

 

구현 클래스를 중복으로 기술하는 번거로움이 없어지고,

해당 인터페이스의 구현체를 변경하려면, 클래스의 구현체만 교체하면 되니, 코드 수정도 깔끔해진다.

 

팩토리메소드 패턴을 해당 클래스안에 함수로도 녹일수있어서,

팩토리메소드 패턴을 같이 사용하면 훨씬 유연하게 코드를 구성할수있다.

 

좀더 나아가 잘사용하기 위해서는 아래와 같은 사항은 지켜주면 더 좋을것같다.

 

1. 델리게이터의 함수명을 명시적으로 잘지어야함 (함수만으로 그 행위를 알수있도록 함수명에 더 신경을 쓴다)

   -> 나는 이부분이 강제되는것 같아서, 오히려 너무나 좋은것같다. 함수명을 지을때, 좀더 고민을 하도록 만들게 되는 방법이라, 참 좋다!

 

2. 델리케이터를 사용하여, 구성을 하면, 멀티 구현체에 대한 고민을 미리하게 된다.

 

3. 되도록이면 도메인 레이어에서 사용하면 좋을듯하다, 

    -> 도메인 레이어에서는 행위를 하는 역할이 명확하기에, 델리게이션을 통해, 함수를 불러도 그 흐름을 명확히 알수있게 할수있는 반면,

         어플리케이션 레이어에서는 해당 도메인 서비스와 어플리케이션 서비스가 공존할수있기에,

        어느 서비스에서 온것인지 다소 애매모호하여, 출처를 알수없을수있기 때문이다. 도메인 레이어에서만 사용하도록 하면,

        좀더 깔끔하고 혼란없이 사용가능하지 않을까 한다.

        ( 이건, 개인적인 생각,의견이다... 정답은 없지..)

 

 

더불어 하나더, 위임, 인터페이스로 구현할때, 공통적으로 수행을 해야하는 함수,로직은,

인터페이스에 구현을 하면 더 깔끔해지는 경우가 있다.

 

예를 들면)

 

@Service
class UserRegisterService(
    canAJoinValidator:ACanJoinValidator,
    validTestUserChecker:AValidTestUserChecker
) : CanJoinValidator by canAJoinValidator,
    ValidTestUserChecker by validTestUserChecker,
    CanJoinTestUserValidators {

    fun register(doRegisterFunc:() -> Unit) {
        doRegister(0) {
            //등록 로직 처리
        }
    }
}

interface CanJoinTestUserValidators : CanJoinValidator, ValidTestUserChecker {
    fun doRegister(type:Int, doRegisterFunc:() -> Unit) {
        when(type) {
            0 -> {
                canJoinCheck()
                canValidCheck()
            }
            1 -> canJoinCheck()
            else -> {}
        }

        doRegisterFunc()
    }
}

 

공통된 비즈니스 로직의 흐름은 인터페이스를 만들고 해당 인터페이스에 정의를 하면, 공통되게 구현 클래스에서 사용할수있다.

 

인터페이스의 사용이 단순히 규약,스펙의 영역으로 생각을 하고 사용하면,

그 사용의 범위가 작아지고, 생각의 스펙트럼도 좁아지게 된다.

 

여러 패턴이나 기능에서의 인터페이스 클래스의 사용을 잘 알고 숙지를 하면, 

다양한 케이스에서 활용일 가능해지고, 그에 따라 코드도 깔끔하게 구현할수있다.

(결국, 개인 개발 스터디를 꾸준히 해야한다는 이야기..ㅠ)

 

최근 인터페이스 클래스의 기능이 하나하나 늘어나는것도 이런 활용을 극대화 하기 위한, 의도가 아닐까 생각된다.

(물론 자바에서 인터페이스에 default함수 기능이 들어간 가장큰 이유는... 레거시 호환때문인건 함정 ㅋㅋ

자바는 과거에도 레거시 코드와의 호환때문에 어쩔수없이 기능이 추가되거나, 하는 케이스가 많이 있었어서...)

 

결론은!!! 코틀린으로 빨리 갈아타라!!