널인가 0인가: 코틀린 JPA 엔티티 ID 전략
코틀린 JPA 엔티티의 숫자 ID를 nullable 또는 0으로 둘 때 신규 판별, 모호성, 검증 방법을 비교한다.
출처 및 AI 안내: 이 글은 제미니의 개발실무 유튜브를 기반으로 작성되었습니다.
gpt-5.6-sol모델을 사용해 생성·편집했습니다.
코틀린 JPA 엔티티에서 자동 생성되는 숫자 ID는 어색한 모델링 선택을 만든다. ID를 nullable로 선언하면 엔티티의 상태를 직접 표현할 수 있다. 영속화 전에는 데이터베이스 식별자가 없으므로 ID가 null이다. 그러나 저장하거나 조회한 뒤 애플리케이션 코드에서는 ID가 있다고 기대해도 코틀린은 사용할 때마다 널 검사나 !!를 요구한다.
ID의 기본값을 0인 non-null 값으로 두면 이런 반복을 없앨 수 있다. 대신 값의 부재를 센티널 값으로 바꾼다. 그러므로 이 결정은 문법 취향보다 크다. 애플리케이션이 새 엔티티를 어떻게 판별하고 모델 전체에서 0을 어떤 뜻으로 사용할지 정하는 일이다.
nullable 전략은 생명주기를 명시적으로 표현한다
숫자 ID가 nullable이면 null은 엔티티가 아직 영속화되지 않았다는 뜻이다. 설명하기 쉽고 생명주기의 차이와도 자연스럽게 맞는다. 저장소의 저장 로직은 ID가 없다는 사실로 신규 엔티티를 판별하고 갱신 쪽 경로가 아니라 영속화 경로를 선택할 수 있다.
비용은 그 경계를 지난 뒤 나타난다. 조회 결과로 받은 관리 상태 엔티티라서 ID가 존재한다고 알아도 코틀린 타입은 여전히 nullable이다. 엔티티를 도메인 객체로 쓰거나 여러 모듈에서 직접 사용하면 ID를 읽는 곳마다 단언이 쌓일 수 있다.
개발자가 값이 있다고 알고 있다는 이유만으로 그 단언이 공짜가 되지는 않는다. !!는 타입으로 가정을 증명하지 못한다는 표시이며, 가정이 틀리면 런타임 실패를 만든다. 도우미나 더 좁은 경계로 일부 소음을 가둘 수는 있지만 근본적인 타입 선택이 사라지지는 않는다.
0을 신규 엔티티의 센티널로 쓸 수 있다
다른 선택은 숫자 ID를 non-null로 선언하고 0으로 초기화하는 것이다. 여기서 확인한 저장소 경로의 신규 판별 로직은 숫자 ID가 0이면 새 엔티티로 취급한다. 따라서 코틀린 프로퍼티가 non-null이어도 저장 동작은 영속화 경로를 탈 수 있다.
사용 지점은 더 깔끔해진다. 영속화된 엔티티는 반복적인 단언 없이 non-null ID를 제공한다. JPA 엔티티를 상위 레이어에서 직접 사용하고 식별자를 자주 읽는 구조라면 매력적일 수 있다.
다만 이 편의는 저장 이후의 코드에서 얻는 것이다. 아직 저장하지 않은 엔티티에도 타입상 숫자 ID가 존재하므로 값만 보고 영속 상태라고 오해하지 않도록 경계를 정해야 한다.
그러나 이제 0은 팀의 계약이 된다. 모든 구성원이 0을 실제 식별자가 아니라 “아직 영속화되지 않음”으로 이해해야 한다. 준영속 또는 비영속 엔티티가 0을 가진 채 애플리케이션 안을 돌아다닐 수 있으므로 식별자를 비교하는 로직은 이 상태를 고려해야 한다. 데이터베이스에 ID가 0인 실제 행이 존재할 수 있다면 센티널의 뜻이 모호해지며 전략을 다시 살펴야 한다.
프레임워크가 0을 허용한다는 사실은 이 경로에서 해당 방식이 동작할 수 있다는 근거다. 모든 매핑, 식별자 정책, 프로젝트에 알맞다는 증명은 아니다. 구현 세부사항만으로 보편적인 승자를 정할 수 없다.
특히 숫자 ID가 데이터베이스에서 자동 생성된다는 전제를 확인해야 한다. 애플리케이션이 식별자를 직접 할당하거나 0이 유효한 값으로 들어올 수 있다면 같은 규칙을 적용할 수 없다. 센티널을 택했다면 생성자 기본값, 신규 판별, 저장 이후 할당이라는 생명주기가 한 세트로 맞아야 한다. 한 부분만 0을 특별하게 취급하면 타입은 단순해져도 모델의 상태는 더 이해하기 어려워진다.
취향 대신 실제 저장 경로를 검증한다
프로젝트의 실제 베이스 엔티티와 저장소 구성에서 이 선택을 테스트해야 한다. 집중된 테스트 하나로 각 전략의 새 엔티티를 저장하고, 저장소가 신규로 판별하는지, 식별자를 할당하는지, 기대한 영속 상태가 만들어지는지 확인할 수 있다. 저장 동작과 isNew 판단을 따라 디버깅하면 프레임워크의 동작을 추측하지 않고 눈으로 볼 수 있다.
애플리케이션이 실제로 허용하는 상태도 테스트에 넣는다.
null또는 0으로 막 생성한 엔티티- 영속화 뒤 반환된 엔티티
- 모델이 허용한다면 영속화 전 식별자 비교
- 데이터베이스나 마이그레이션 이력상 가능하다면 기존의 0 ID
이 테스트에는 두 가지 역할이 있다. 현재 동작 방식을 확인하고, 다음 개발자에게 센티널 규칙을 문서화한다. 라이브러리 동작이나 로컬 매핑이 달라지면 가정이 중요한 경계에서 실패 신호를 얻을 수 있다.
테스트가 통과했다면 실제 호출 코드도 살펴본다. ID를 읽는 위치가 저장소 어댑터 몇 곳에 불과한지, 아니면 도메인 로직과 응답 조립 전반에 퍼져 있는지에 따라 nullable의 비용은 크게 달라진다. 반대로 0 전략에서는 새 엔티티가 저장되기 전에 비교나 컬렉션 키로 사용되는지 확인해야 한다. 같은 0을 가진 두 객체를 식별자가 같다고 오해할 수 있기 때문이다.
가둘 수 있는 불편을 선택한다
nullable 전략은 의미의 명확성을 택한다. 없음을 없음으로 표현한다. 대가는 영속 ID가 필수인 코드에서 널을 처리하는 일이다. 0 전략은 더 깔끔한 코틀린 사용 지점을 택한다. 대가는 의미를 일관되게 보호해야 하는 관례적인 값이다.
엔티티의 위치에 따라 이 트레이드오프의 무게도 달라진다. 영속성 엔티티가 좁은 어댑터 뒤에 머문다면 nullable ID와 변환 경계의 비용을 가두기 쉽다. 엔티티를 애플리케이션 전반의 도메인 객체로 사용한다면 반복되는 단언이 충분한 소음을 만들 수 있고, 검증된 0 규칙이 더 매력적일 수 있다.
베이스 엔티티, 테스트, 팀 컨벤션에 선택한 규칙을 명시하자. 비슷한 엔티티에서 전략을 무심코 섞으면 타입이나 값만 보고 생명주기 상태를 추론할 수 없게 된다. 설명한 조건에서는 어느 쪽도 동작 가능한 선택이다. 유지보수 가능한 선택은 팀이 그 가정을 설명하고 테스트하며 일관되게 적용할 수 있는 쪽이다.