
현재 개발중인 프로젝트는 MSA 구조로 개발중인 Spring boot 프로젝트이며, 특정 서비스는 JAVA 대신 Kotlin으로 작성되고 있다.
Entity 코드를 작성하던 도중 엔티티 생성 시 기본 값에 대한 고민이 발생 해 공부 해 본 부분을 정리해 본다.
스프링 부트에서 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을 기본 값으로 두어 현실의 객체 모델링 방식을 반영하는 것이 맞을까?
코드를 작성하다 위 두 방식 중 어떤 방식이 좋은지 고민이 되어 공부해 보려 한다.
ID 부분에는 @GeneratedValue(strategy = GenerationType.IDENTITY) 전략을 사용하여 ID 생성을 자동증가 하도록 DB에 위임하는 방식이다.
JPA는 엔티티를 영속화 할 때 별도의 값을 생성하거나, 전달하지 않는다. DB에 새로운 레코드 삽입을 요청할 뿐이다.
위 코드에 따르면 자동 증가된 ID를 생성하여 레코드에 삽입하고 해당 값을 JPA에 알려준다.
JPA는 DB로 부터 받은 ID 값으로 엔티티의 ID 필드를 업데이트 한다. 이 시점에서 엔티티는 영속 상태가 되는 것이다.
코틀린은 type에 ?를 붙임으로서 null이 가능한 변수임을 명시적으로 표현한다.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var boardId: Long? = null
boardId를 nullable 타입(Long?)으로 선언Null 상태로 두다가 DB에 저장되면 자동으로 ID가 할당됨@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var boardId: Long = 0L
boardId를 non-nullable 타입(Long)으로 선언하고 기본값으로 0L을 할당0L로 초기화됨배경 지식에서 설명하였듯, @GeneratedValue(strategy = GenerationType.IDENTITY) 전략은 ID 생성을 전적으로 DB에 위임하는 방식이다.
어떤 방식으로 초기 값이 설정 되던간에, JPA는 해당 값을 쿼리에 포함하지 않고, DB에도 전달하지 않는다.
아래 표는 Chat GPT에게 도움을 받아본 Hibernate 내부 처리 및 DB 별 처리 방식이다.

Long? = null) 인 경우null 을 기본키가 없는 상태로 판단.Long = 0) 인 경우'ID가 없음을 의미'하도록 동작한다.위 내용을 코드를 기반으로 분석해보자
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() 메서드
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));
}
}
따라서 초기 값이 0L이든 Null이든 잘 작동하는 것을 알 수 있다.
엔티티 생명주기 명확화
NullSafety 극대화 및 안정성 확보
NPE(Null Pointer Exception)을 원천 차단할 수 있다.(?., !!. ?:)를 사용해 null 처리를 간결하게 할 수 있다.번거로운 Nullable 필드 처리
Null check 회피 가능
의미 없는 0L 값으로 오류 및 혼란 야기
Nullable 방식을 선택하기로 결정
위 근거를 바탕으로 Nullable 한 아래 방식을 선택하기로 하였다.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val boardId: Long? = nul
코틀린은 NullSafe를 중시 하기때문에, Null 값을 회피하는 것이 더 안전할 것이라 생각하였으나, Null값이 있을 수 있는 부분은 Nullable하게 두고, Null 처리를 명확하게 하는 것이 오히려 안전한 방식임을 알게 되었다.
내부 동작에서 JPA는 Null 과 0L 모두 새 객체로 인식하지만, 0L의 경우 DB에 값을 insert 할 위험이 있으며 이후 DB에 따라 오류가 발생할 여지도 있다.
Nullable 한 방식은 안정적이고 잘 설계되었고, 유지보수가 용이한 코드임을 확인할 수 있었다.
따라서 해당 방식을 선택하고자 한다
위 글은 제가 공부하면서 얻은 정보들을 정리한 글입니다.
오타, 잘못된 개념 등에 대해 지적해 주시면 제게 많은 배움이 있을 것 같습니다.
감사합니다