코프링 엔티티 생성 시 ID 기본 값은 어떤게 좋을까?

Hyeseong_M·2025년 3월 3일

Spring

목록 보기
3/3
post-thumbnail

현재 개발중인 프로젝트는 MSA 구조로 개발중인 Spring boot 프로젝트이며, 특정 서비스는 JAVA 대신 Kotlin으로 작성되고 있다.

Entity 코드를 작성하던 도중 엔티티 생성 시 기본 값에 대한 고민이 발생 해 공부 해 본 부분을 정리해 본다.


목차

  1. 들어가며
  2. 개념 및 배경 지식
  3. 각 방식의 비교
  4. 두 방식 모두 작동하는 이유
  5. 각 방식의 장단점
  6. 추천하는 방식
  7. 결론

0. 🤔들어가며

스프링 부트에서 Java를 사용하여 Entity를 생성할 때에는 아래와 같은 코드를 사용한다.

@Getter
@NoArgsConstructor
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long userId;

    @Column(nullable = false)
    private String email;

    @Column(nullable = false)
    private String password;

	//각종 필드 및 메서드 생략
}

위 코드에서 특히 ID 부분에 집중해보자.

JAVA Entity를 생성할 때에는 위와 같은 방식으로 NULL 값 혹은 0L 값을 ID의 초기 값으로 넣어 생성해 왔다.

허나, 코틀린은 Null이 가능한 Type을 완전히 다르게 취급하여 Null 안정성을 강조한다.

코틀린은 Null 처리를 명확하게 하기 위해 Null이 가능한 값과 불가능한 값의 타입을 다르게 정의한다.
즉 위 코드를 코틀린으로 작성했을 때 기본 값이 Null인지, 0L인지에 따라 아래와 같이 타입이 달라지는 것이다.

//1번 방식 (초기 값이 Null)
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val boardId: Long? = null

//2번 방식 (초기 값이 0L)
1. @Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val boardId: Long = 0L,

🤔그럼 어떤 방식이 맞는가

0L을 기본값으로 두어 Null을 피하는게 안정적인 방식일까?
Null을 기본 값으로 두어 현실의 객체 모델링 방식을 반영하는 것이 맞을까?

코드를 작성하다 위 두 방식 중 어떤 방식이 좋은지 고민이 되어 공부해 보려 한다.


1. 개념 및 배경지식

JPA 관련 배경지식

DB 자동증가 기능 활용

ID 부분에는 @GeneratedValue(strategy = GenerationType.IDENTITY) 전략을 사용하여 ID 생성을 자동증가 하도록 DB에 위임하는 방식이다.

JPA는 ID 생성 시 관여X

JPA는 엔티티를 영속화 할 때 별도의 값을 생성하거나, 전달하지 않는다. DB에 새로운 레코드 삽입을 요청할 뿐이다.

DB는 ID 생성 후 JPA에 반환

위 코드에 따르면 자동 증가된 ID를 생성하여 레코드에 삽입하고 해당 값을 JPA에 알려준다.

JPA는 반환받은 ID로 엔티티 업데이트

JPA는 DB로 부터 받은 ID 값으로 엔티티의 ID 필드를 업데이트 한다. 이 시점에서 엔티티는 영속 상태가 되는 것이다.

Kotlin 관련 배경지식

null이 될수 있는 type

코틀린은 type에 ?를 붙임으로서 null이 가능한 변수임을 명시적으로 표현한다.


2. 각 방식의 비교

2.1. Nullable Type 사용

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var boardId: Long? = null
  • boardId를 nullable 타입(Long?)으로 선언
  • 엔티티가 처음 생성될 때 Null 상태로 두다가 DB에 저장되면 자동으로 ID가 할당됨

2.2. 0L 방식 사용

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var boardId: Long = 0L
  • boardId를 non-nullable 타입(Long)으로 선언하고 기본값으로 0L을 할당
  • DB에 저장되면 자동으로 ID가 할당되지만, 엔티티 생성 시 기본값 0L로 초기화됨

3. 두 방식 모두 작동하는 이유

3.1. 개념적 설명

배경 지식에서 설명하였듯, @GeneratedValue(strategy = GenerationType.IDENTITY) 전략은 ID 생성을 전적으로 DB에 위임하는 방식이다.
어떤 방식으로 초기 값이 설정 되던간에, JPA는 해당 값을 쿼리에 포함하지 않고, DB에도 전달하지 않는다.

아래 표는 Chat GPT에게 도움을 받아본 Hibernate 내부 처리 및 DB 별 처리 방식이다.

nullable(Long? = null) 인 경우

  • 자동으로 null을 감지하여 ID를 할당.
  • JPA는 null 을 기본키가 없는 상태로 판단.
  • Hibernate에서 새 엔티티로 판단하여 DB에 Insert(AutoIncreament) 사용 (표 참고)

non-nullable(Long = 0) 인 경우

  • 자동으로 0L을 더미 값으로 감지하여 ID를 할당한다.
  • 0L은 Jpa 구현체 (Hibernate)에서 내부적으로 특별 처리 되어 'ID가 없음을 의미'하도록 동작한다.
  • Hibernate에서 새 엔티티로 판단하지만 0L이라는 ID 필드의 값을 삽입하려 시도.
    (즉, 신규 엔티티이지만, 0L이라는 유효한 ID 값을 갖고있다고 판단 함)

3.2. 코드 분석

위 내용을 코드를 기반으로 분석해보자

spring Data JPA의 save() 메서드

@Transactional
    public <S extends T> S save(S entity) {
        Assert.notNull(entity, "Entity must not be null");
        if (this.entityInformation.isNew(entity)) {
            this.entityManager.persist(entity);
            return entity;
        } else {
            return (S)this.entityManager.merge(entity);
        }
    }
  • isNew() 메서드를 통해서 새로운 엔티티이면 persist()를 호출하여 저장하고, 그렇지 않으면 merge()를 호출하여 기존 값을 업데이트 한다.

isNew() 메서드

 public boolean isNew(T entity) {
        ID id = (ID)this.getId(entity);
        Class<ID> idType = this.getIdType();
        if (!idType.isPrimitive()) {
            return id == null;
        } else if (id instanceof Number) {
            return ((Number)id).longValue() == 0L;
        } else {
            throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
        }
    }
  • entity의 ID가 Null이면 true를 반환한다
  • entity의 ID가 숫자형 0L이면 true를 반환한다.

따라서 초기 값이 0L이든 Null이든 잘 작동하는 것을 알 수 있다.


4. 각 방식의 장단점

4.1. Nullable 방식

장점

엔티티 생명주기 명확화

  • null 값을 통해 엔티티의 생명주기 (영속화 이전 / 이후) 구분이 코드 레벨에서도 명확하다.
  • 영속화 된 후에야 비로소 ID가 부여되는 방식
  • 현실의 객체 모델링 방식을 그대로 반영한다.
  • ID 값을 DB에서 생성한다는 IDENTITY 전략의 의도를 명확히 표현할 수 있다.

NullSafety 극대화 및 안정성 확보

  • 코틀린의 강력한 Null safety 기능을 활용하여 NPE(Null Pointer Exception)을 원천 차단할 수 있다.
  • ID가 Null일 수 있음을 항상 고려하고 코딩하며 런타임 오류를 예방한다.
  • 다양한 Null safety 연산자(?., !!. ?:)를 사용해 null 처리를 간결하게 할 수 있다.

단점

번거로운 Nullable 필드 처리

  • 코틀린의 Nullable 필드는 null 가능성을 항상 염두에 두고 코드를 작성해야하기에 번거로움이 발생할 수 있음
    (코틀린의 Nullsafety 기능을 사용하여 안전한 코드를 작성할 수 있도록 하는 실질적 장점이 될 수 있음.)

4.2. Non-Nullable 방식

장점

Null check 회피 가능

  • ID에 Null 값이 들어가지 않기 때문에 Nullcheck 필요성이 없음.
    (다만 영속화 되지 않은 엔티티 Id 필드에 더미 값을 넣은 것과 같아 의미 왜곡의 여지가 있어 실질적 단점일 수 있음)

단점

의미 없는 0L 값으로 오류 및 혼란 야기

  • 할당되지 않은 ID에 0L이라는 더미 값을 넣어 의미를 왜곡 할 수 있음.
  • 영속화 이전 / 이후를 혼동하게 만들 수 있음
  • DB별 0L에 대한 처리 차이로 혼동 및 오류 발생 가능 (e.g. PostgreSQLDB는 ID값이 0 부터 A.I.)

5. 추천하는 방식

Nullable 방식을 선택하기로 결정

  1. Kotlin의 null safety기능 활용 가능
  2. 객체 지향 설계 원칙에 부합
  3. 의미론적, 상태적 명확성 확보 (영속, 비영속 상태 비교 가능)

위 근거를 바탕으로 Nullable 한 아래 방식을 선택하기로 하였다.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val boardId: Long? = nul

6. 결론

코틀린은 NullSafe를 중시 하기때문에, Null 값을 회피하는 것이 더 안전할 것이라 생각하였으나, Null값이 있을 수 있는 부분은 Nullable하게 두고, Null 처리를 명확하게 하는 것이 오히려 안전한 방식임을 알게 되었다.

내부 동작에서 JPA는 Null0L 모두 새 객체로 인식하지만, 0L의 경우 DB에 값을 insert 할 위험이 있으며 이후 DB에 따라 오류가 발생할 여지도 있다.

Nullable 한 방식은 안정적이고 잘 설계되었고, 유지보수가 용이한 코드임을 확인할 수 있었다.

따라서 해당 방식을 선택하고자 한다

위 글은 제가 공부하면서 얻은 정보들을 정리한 글입니다.
오타, 잘못된 개념 등에 대해 지적해 주시면 제게 많은 배움이 있을 것 같습니다.
감사합니다


7. 참고 자료

https://ittrue.tistory.com/482

profile
Dev_Hyeseong

0개의 댓글