Rlog

Domain 모델에 Domain 로직을 담아야 하는 이유 본문

Architecture

Domain 모델에 Domain 로직을 담아야 하는 이유

dev_roach 2022. 5. 1. 22:13
728x90

최근 DDD 와 관련된 공부들을 꾸준히 해나가고 있는데, 사실 이건 DDD 에 해당하는 것이 아니라 객체지향에서도 당연한 일이기도 하다. 객체가 자신의 행위(Behavior) 에 대한 인터페이스를 제공하는 것은 당연하기 때문이다. 함수형 프로그래밍에서는 객체를 모델로 만들어 두고, 행위를 정의하는 곳을 분리하여 두는 것으로 알고 있다. 하여튼 각각의 프로그래밍 패러다임으로 어떻게 구현하든 도메인 모델에 그와 관련된 행위가 정의되어 있는 것이 좋은 이유에 대해 설명해보려고 한다.

 

이건 사실 좋은 코드를 작성하는 가장 원칙적인 기준인 응집도에 관한 문제이기도 하다.

응집도에 대한 것을 모른다면 내가 적은 아래 글을 읽어보길 바란다. 나는 좋은 코드를 작성하는 원칙은 전부 응집도와 결합도 부터 시작한다고 생각한다.

https://devroach.tistory.com/58

 

응집도와 결합도

응집도와 결합도는 소프트웨어 품질을 결정 짓는 요소이다. 대다수의 사람들은 코드를 작성할때 "응집도" 와 "결합도" 를 생각하지 않은채 관성적으로 코드를 적고는 한다. 응집도와 결합도는

devroach.tistory.com

만약 우리의 웹 싸이트에서 유저라는 사람이 주소를 바꾼다고 가정해보자. 유저의 데이터 모델링이 된 코드는 아래와 같다.

open class User {
    var email: String? = null
    var age: Int? = null
    var mainAddress: String? = null
    var subAddress: String? = null
    var mobilePhone: String? = null
}

그리고 여기서 기획자에게 User 의 주소를 바꾸는 일을 요청받았다.  

나는 기획자와 토론하여 mainAddress 와 subAddress 를 바꾸는 것을 사용자의 주소를 바꾸는 것으로 정의하기로 했다.

즉, 행위(사용자의 주소를 바꾸는 것) 가  mainAddress 와 subAddress 를 바꾸는 것으로 정의된 것이다.

위와 같은 모델에서 사용자의 주소를 바꾸는 행위를 하기 위해서 어떻게 해야할까?

아마도 UseCase(특정 상황) 을 구현하는 Service 로직에서는 아래와 같이 작성될 것이다.

fun register(request: UserRegiserRequest) {
	// ...
    user.mainAddress = request.mainAddress
    user.subAddress = request.subAddress
}

그런데 나 말고 개발자 B 가 기획자에게  아래와 같은 요청을 받았다.

A 라는 특정 상황에서도 사용자의 주소를 바꿔주세요

그래서 개발자 B 는 아래와 같이 코드를 작성했다.

fun aCase(request: AUseCaseRequest) {
	// ...
	user.MainAddress = request.mainAddress
}

왜 위와 같은 현상이 발생했을까? 생각해보면 아래와 같은 이유들이 존재할 것이다.

  1. 사용자가 주소를 바꾸는 것이 mainAddress 와 subAddress 를 바꾸는 것이라는 것을 기획자와 나 말고는 아무도 알 수 없음.
  2. 주소를 바꾸는 행위에 대한 응집도가 낮음

그렇다면 우리가 좋은 코드를 작성하기 위해서는 어떻게 해야할까? 일단 행위(사용자의 주소를 바꾸는 것) 가  mainAddress 와 subAddress 를 바꾸는 것으로 정의된 것 에 대한 응집도를 높여야 할 것이다. 그렇다면 아래와 같이 코드를 작성해보자.

open class User {
    var email: String? = null
        protected set
    var age: Int? = null
        protected set
    var mainAddress: String? = null
        protected set
    var subAddress: String? = null
        protected set
    var mobilePhone: String? = null
        protected set

    fun changeAddress(mainAddress: String, subAddress: String) {
        this.mainAddress = mainAddress
        this.subAddress = subAddress
    }
}

이제 User 의 주소를 바꾸기 위해서는 changeAddress 메소드를 사용하면 된다. 이렇게 되면 개발자 B 그리고 다른 팀원들 또한 주소를 변경하기 위해서는 이를 이용해야 함을 알 수 있을 것이다. 기존에는 데이터를 노출하는것에 집중했으나, 현재는 행위를 노출하는 것에 집중했기 때문이다. 이렇게 메소드를 만들어두면 아까의 register 함수와 aCase 함수 또한 아래와 같이 변경될 것이다.

fun register(request: UserRegiserRequest) {
    // ...
    user.changeAddress(
    	mainAddress = request.mainAddress
        subAddress = request.subAddress
    )
}

fun aCase(request: aCaseRequset) {
    // ...
    user.changeAddress(
    	mainAddress = request.mainAddress
        subAddress = request.subAddress
    )
}

이렇게 User 라는 도메인 모델에 대해서 도메인과 관련된 로직을 잘 담고있다면 Domain 모델과 관련된 행위에 관한 메소드들은 응집도가 당연히 높을것 이고, 팀원간 같은 목적에 대해 다른 코드를 작성하는 일을 방지할 수 있다. 만약 Domain 모델 뿐만 아니라 다른 계층과 연관되어 해야 하는 작업은 어떻게 해야할까? 라는 고민도 있을 것이다. 예를 들어 데이터베이스에서 사용자를 가져온 뒤 사용자의 주소를 변경하고 데이터베이스에 저장하는 일과 같은 상황을 가정해보자.

 

위와 같은 상황일 경우 보통 나의 경우에서는 application 계층을 이용하는 편이다.

class ChangedUserAddressUseCase(
    private val userRepository: UserRepository
) {

    fun execute(request: ChangedUserAddress) {
        val user = userRepository.findByIdOrNull(request.userEmail) ?: throw IllegalArgumentException("등록되지 않은 사용자 이메일입니다.")

        user.changeAddress(
            mainAddress = request.mainAddress,
            subAddress = request.subAddress
        )

        userRepository.save(user)
    }
}

내 기준에서 application 이란 비즈니스 로직의 목적을 달성하기 위해 그와 관련된 계층들이 결합되어 운용되는 계층으로 사용한다. 

이와 같은 경우는 팀마다 다를 수 있고, 응집도만 높일 수 있다면 큰 차이는 없지 않을까 싶다. 다소 개인적인 기준에서 나는 이렇게 코드를 작성한다는 이야기였다. 저번 응집도 포스팅에서 남겼던 이야기에서 조금 더 발전된 이야기를 한 것 같지만, 최근 리뷰를 하거나 내가 코드를 아무 생각없이 작성하다보면 나 또한 도메인 모델이아닌 데이터 모델처럼 짜버리는 경우도 많아서 이를 조심해야 겠다는 생각겸 DDD 를 공부하면서 느낀점을 포스트에 정리해보았다.