근거가 있는 트레이드 오프를 하자. feat. Entity 와 식별자.

Joshua_Kim·2025년 6월 29일
8
post-thumbnail

엔터티의 식별자가 null 이라니!?

👨 : 엔터티에 식별자가 없을 수가 있는거에요?!!
나 : !??!!???

지난 목요일, Entity 설계에 대한 리뷰를 보고 당황했었다.
빠르게 Entity 에 대한 리뷰를 받고 병합 후 바로 다음 작업으로 이어가려고 했었는데,
예상치 못한 곳에 리뷰가 되었다.

그리고, 그 리뷰를 보고 여러 생각을 하게 되었다.
리뷰를 단 동료는 팀 내에서 가장 DDD 에 대한 학습에 열정적이고, 총명한 분이었다.
이분이 리뷰를 달았다는 것은, 분명 의미있는 대화를 나눌 수 있는 주제라는 것이다.

당장 자리로 찾아가 이야기를 나눠보았다.

내가 Entity 설계를 하고 코드로 구현하면서 식별자(id) 를 nullable 하게 한 이유는 다음과 같았다.

  1. 현재 우리 백엔드 코드의 대부분 Entity 의 id 가 nullable 하다.
  2. 그 이유는, 우리가 id 생성에 대한 책임을 Repository 에 맡기기 때문이라고 이해했다.
  3. 2번 이유가 내게 자연스러웠던 것은, 기존에 개발하던 관성속에 Auto-Increment 된 전략으로 id 를 생성했기 때문이었다.

위의 3가지 생각의 흐름으로, 위의 MedicalService 라는 Entity 의 id 를 nullable 하게 구현했다.
하지만, 위의 3가지 생각은, 부끄럽게도 현재와 지금까지 구현되었던 코드들의 현상에 대한 해석에 그치지 않았다.
구체적으로는, 다음의 질문들에 대해 내가 설명하지 못했다.

  1. 왜 Entity 의 식별자의 생성의 책임을 Repository 에 맡기는가? (무슨 이득이 있는가?)
  2. Entity 설계에 식별자가 nullable 하다면, 소프트웨어에서 계속 nullable 한 식별자를 관리해야 하는가?

DDD 를 지향하고 지켜오는 우리 팀내에서 좀더 명확하고 좋은 이유를 찾을 필요가 있었다.

리뷰를 달아준 동료와 함께 Pair Programming 을 하면서 저 문제를 어떻게 해결해볼것인지 고민을 시작했다.

시도 - 식별자를 non-null 하게 변경한다.


data class MedicalService(
    val id: String, // 요기 수정
    ...
)


@Document(collection = "medical_services")
data class MedicalServiceDataModel(
    val id: String? = null,
    ...
)

object MedicalServiceDataModelMapper {
    fun toDocument(entity: MedicalService): MedicalServiceDataModel =
        MedicalServiceDataModel(
            id = entity.id,
            ...

        )

    fun toEntity(document: MedicalServiceDataModel): MedicalService =
        MedicalService(
            id = document.id!!, // 요기 수정
            ...
		)
}

"각 Entity 에는 다른 객체와 구분해줄 식별성 (심지어 속성이 같은 다른 객체와도 구분할 수 있는)을 만들어내는 수단이 반드시 있어야 한다. 식별에 사용되는 속성은 시스템의 상태 (시스템이 분산돼 있거나 저장돼 있는 경우에도) 와 관계없이 해당 시스템 내에서 유일해야 한다." - 도메인 주도 설계 (에릭 에반스)

처음 시도해본 것은, Entity 의 id 를 non-null 하게 바꾼 것이다.
그러면서 Mongodb Document 인 MedicalServiceDataModel 과의 변환을 담당하는 Mapper 클래스에도 변화를 주었다.

코드는 변경한 지점이 매우 적지만, 이것은 꽤나 유의미한 변화를 주었다.

  1. Entity 는 진정한 의미의 식별자를 명확하게 가진다.
  2. Entity 의 식별자 부여를 Repository 에 위임하고, 데이터계층의 인덱싱등의 이점을 가져간다.
  3. Entity 의 존재성은 영속된 이후라는 것을 분명히 한다.
  4. 영속된 Entity 는 id 가 분명히 존재한다는 것을 인지하고, 코드에서 나타낸다 (!!)

위와 같은 결론과, 논의를 하며 우리는 꽤나 행복했고 만족했다.

그때, 실제 구현에 대한 생각이 머릿속을 스치면서 다시 막막해졌다.

문제 발생.

		fun init(
            title: String,
            configuration: MedicalServiceConfiguration,
            ...
        ): MedicalService {
            validateInvariants(
                title = title,
                configuration = configuration,
                ...
            )
            // 인스턴스 리턴
            return MedicalService(
                id = null,
                title = title,
                ...
            )
        }
		
        // 불변식 검증
        private fun validateInvariants(
            title: String,
            configuration: MedicalServiceConfiguration,
            ...
        ) {
            validateTitle(title)
            validateConfiguration(configuration)
            ...
        }

Entity 를 생성하고 영속할때, 아래와 같은 과정을 거친다.

  1. Entity 에서 init() 이라는 메서드를 통해 본인의 인스턴스를 생성한다.
  2. 이 과정에서 Entity 의 속성들에 대한 불변식 검증이 이루어진다. (validateInvariants())
  3. 영속장치를 통해 영속을 하면서 id 가 부여가 되기 때문에, 인스턴스 생성시에는 null 을 부여하여 생성한다.
  4. 그리고 이 생성된 인스턴스를 영속장치에서 영속을 시킨다.

위와 같은 경우를 보았을 때, 인스턴스 생성과 함께 생성에 대한 불변식을 검증하는 것은 상당한 이점이 있어 보였다.
해당 Entity 의 속성에 대한 판단에 대한 책임은 그 Entity 에게 있는것이 자연스러웠다.

시도 - Entity 가 식별자를 부여하도록 한다.

또다른 시도는, Entity 가 직접 식별자를 부여하도록 하는 것이다.

방법은 UUID 등을 직접 id 에 부여하는 등 여러 방법이 있을 수 있다.

그러나, 이 방법을 택하지는 않기로 했다.
그 이유는 다음과 같다.

  1. 기존의 우리가 사용하고 있는 영속장치의 id 규칙에 대한 변경이 필요하므로 더 큰 논의가 필요했다.
  2. 기존에 누리고 있던 영속장치에서 부여한 id 를 사용함으로써 얻었던 장점들 (인덱싱등) 을 포기할 수 없었다.

결국, 모든 것은 트레이드 오프다.

우리의 논의는, 각각의 방법에 대한 장단을 정리하고 트레이드 오프를 해보는 것으로 단계를 넘겼다.

1.식별자를 non-null 하게 하거나 직접 부여 한다.

이득

  • Entity 의 식별자의 의미를 분명하게 할 수 있다.
  • Entity 의 식별자 부여에 대한 책임을 명확하게 Entity 가 가지게 할 수 있다.

손해

  • 인스턴스 생성 시 불변식 검증에 대한 책임을 Entity 가 가질 수 없게 된다.
  • 식별자를 직접 부여했을 때, DB 인덱싱등의 이점을 누릴 수 없다.

2. 식별자를 기존 처럼 nullable 하도록 한다.

이득

  • 인스턴스 생성 시 불변식 검증을 Entity 안에서 해결할 수 있다.
  • 저장계층에서 부여된 ID 를 통해 DB 조회 시 인덱싱등의 이점을 누릴 수 있다.

손해

  • Entity 의 id 가 nullable 하다고 표현된 것을 소프트웨어에서 의식하며 관리해야 한다.

결국 우리는,식별자를 nullable 하도록 하는 것으로 결론을 지었다.

결론적으로만 말하면, 코드가 변한것은 아무것도 없었다.
하지만, 이 논의를 통해서 내가 얻은 이해도와 깨달음은 그 무엇보다 컸다.

💡 얻은 것.

  1. 왜 우리의 코드에서 Entity 의 식별자가 nullable 하도록 만들어졌는지 이유를 설명할 수 있게 되었다.
  2. 그것에 대한 장점과 단점은 무엇인지 이야기할 수 있게 되었다.
  3. 언제든지 이 방법의 장점을 이길 다른 방법이 있다면, 그것으로 변경할 수 있는 유연함에 대한 근거를 얻게 되었다.

개발자는 코드를 작성하면서 수많은 선택의 기로에 놓인다.
그리고, 그 선택을 할 때 본인의 이유와 근거를 분명히 가져야 한다.
동료와 이번 주제로 논의하며 이야기할 때, 내 스스로 이 부분이 많이 부족함을 다시 한 번 자각하게 되었다.

'기존에 이렇게 해왔으니까'
'다른 코드들이 이래왔으니까'
'이 방법이 더 익숙하니까'

등의 이유는 명확한 트레이드 오프의 근거가 될 수 없다.
막연하게 직감과 느낌으로 '이래야할 것 같은데' 에서 벗어나서 명확한 이유를 가지는 것을 훈련해야겠다고 다시 생각했다.

강남언니의 KOS 팀에 합류하고나서 우스갯소리로 하는 말이 있다.
"여기 합류하고 나서 코드치는게 너무 어려워졌어요"

반은 농담이고 반은 진심이다.
내가 작성하는 코드에 대해 이유를 가지는 것.
이런 똑똑하고 총명한 동료와 함께 가치 있는 논의를 하는 팀에 있다는 것에 새삼 감사하다.

profile
인문학 하는 개발자 💻

1개의 댓글

comment-user-thumbnail
2025년 7월 11일

좋은 글 잘 읽었습니다! 생산적인 리뷰가 가능한 동료가 있으셔서 든든하실 것 같아요!
의문이 생기면 그 의문에 대한 현상만 바라보고 이야기하는게 아니라 이유를 찾고 더 깊게 질문해보며 해결을 하는 습관을 가지면 좋겠다고 생각했습니다!

답글 달기