본문 바로가기
백엔드/JPA

@NotNull과 @Column(nullabe=false)

by ARlegro 2025. 4. 20.

[1편 : @NotNull vs @Column(nullabe=false) ]

> 1편 : 2개의 애노테이션 비교<br>
> 2편 : @Column을 쓰는 이유

JPA를 이용해서 스프링 프로젝트를 할 때,
특정 필드의 null값을 허용하지 않는 방법에서 @NotNull@Column(nullabe=false)가 있다는 것을 알고 있었지만
2개의 큰 차이를 이해하지 못하고 있다는 것을 느껴 좀 더 자세히 알아보기로 했다.

언뜻 보기에는 @NotNull@Column(nullabe=false)가 비슷해 보이고

실제로 JPA 엔티티에 사용될 경우 둘 다 NOT NULL 제약조건을 추가해주기 때문에 뭐가 다르지라고 느낄 수 있다.

이 둘의 차이를 알아볼 것!

실험 대상 엔티티

기본 엔티티

@Entity
public class Item {

    @Id
    @GeneratedValue(strategy = UUID)
    private UUID id;

    private String name;
}

위의 클래스를 기반으로 테스트를 해볼 것이다.

@NotNull 적용 시

@NotNull 기본 개념

  • @NotNullBean Validation 명세에 정의된 어노테이션이다.
  • 엔티티뿐만 아니라, DTO 같은 일반 객체에도 적용된다.
  • @Column(nullabe=false)와 달리, 엔티티뿐만 아니라 어떠한 빈이나, 클래스(ex. DTO)에도 사용될 수 있다.

엔티티 세팅 및 테스트

@Entity
public class Item {
    @Id
    @GeneratedValue(strategy = UUID)
    private UUID id;

    @NotNull
    private String name;
}
@DataJpaTest
@AutoConfigureTestDatabase(replace = NONE)
public class ItemNullTest {

  @Autowired
  private ItemRepository itemRepository;

  @DisplayName("@NotNull 테스트")
  @Test
  void notNullTest() {
    itemRepository.save(new Item());
  }
}

결과 확인

✅ 테이블 생성 쿼리

    create table item (
        id uuid not null,
        name varchar(255) not null,
        primary key (id)
    )
Hibernate: 
    create table item (
        id uuid not null,
        name varchar(255) not null,
        primary key (id)
    )

테이블 생성시에 하이버네이트가 자동으로 @NotNull이 붙은 필드를 대상으로 not null 제약조건을 넣어준다

그 이유는, Hiberanate가 내부적으로 아래의 설정을 통해 Bean Validation 어노테이션을 DDL 메타데이터로 자동 변환하는 기능 때문이다.

  • 활성화(자동)
    • spring.jpa.properties.hibernate.validator.apply_to_ddl=true (기본값)
  • 참고 : 비활성화 방법 (기본값 : true)
  • spring.jpa.properties.hibernate.validator.apply_to_ddl=false

✅Insert 쿼리 확인

그렇다면 null값을 넣고 save()메서드 호출 뒤에는 어떻게 될까??

not null 제약 조건을 위배했기 때문에 예상대로 오류가 발생했는데,

 

> Insert 쿼리는 발생하지 않았다.

오류 메시지를 보자

 

✅ 오류 메시지

ConstraintViolationException:

image.png

jakarta.validation.ConstraintViolationException

jakarta.validation.ConstraintViolationException: 
Validation failed for classes [com.example.nullable_test.Item] 
during persist time for groups [jakarta.validation.groups.Default, ]

List of constraint violations:[
    ConstraintViolationImpl{interpolatedMessage='널이어서는 안됩니다', 
    propertyPath=name, rootBeanClass=class com.example.nullable_test.Item, 
    messageTemplate='{jakarta.validation.constraints.NotNull.message}'}
]

엔티티를 저장하는 insert쿼리는 나가지 않고 ConstraintViolationException예외를 터트렸다.

즉, @NotNull을 사용하면 하이버네이트는 실제 SQL 쿼리(Insert)를 실행하기 전 단계인 pre-persist 단계에서 Bean Validation을 수행하며 예외를 터트린다는 것이다.

이는, 검증을 DB에서 하는 것이 아니라 스프링 차원에서 하는 것이다.

@Column(nullable = false) 적용 시

@Column 기본 개념

  • JPA 명세에 포함된 애노테이션이다.
  • 실제 데이터베이스 열의 특정 특성을 나타내기 위해 사용
  • 따라서, DTO같은 다른 일반 클래스에는 사용할 수 없고, 엔티티에만 적용이 가능하다
  • 목적 : 주로, DDL 스키마 생성 시 사용된다.
  • 이 애노테이션을 생략하면 @Column의 기본값이 적용된다.(예외는 있는데 일단은 딥하게 X)

엔티티 세팅 및 테스트

@Data
@Entity
public class Item {

  @Id
  @GeneratedValue(strategy = UUID)
  private UUID id;

  @Column(nullable = false) // 기본값 : true
  private String name;
@DataJpaTest
@AutoConfigureTestDatabase(replace = NONE)
public class ItemNullTest {

  @Autowired
  private TestEntityManager tem;

  @DisplayName("@Column(nullable=false) 테스트")
  @Test
  void nullableTest() {
    tem.persistAndFlush(new Item());
  }

결과 확인

테이블 생성 쿼리

2025-04-20T19:22:38.752+09:00 DEBUG 36528 
    create table item (
        id uuid not null,
        name varchar(255) not null,
        primary key (id)
    )
Hibernate: 
    create table item (
        id uuid not null,
        name varchar(255) not null,
        primary key (id)
    )

@NotNull의 경우와 마찬가지로, 테이블 생성시에 하이버네이트가 자동으로 not null 제약조건을 넣어준다

✅ Insert 쿼리 확인

❓그렇다면 null값을 넣고 save()메서드 호출 뒤에는 어떻게 될까❓

  • not null 제약 조건을 위배했기 때문에 예상대로 오류가 발생했다.
  • 하지만 이번에는 오류 메시지가 다르고, Insert 쿼리도 발생했다.

Insert쿼리

2025-04-20T19:22:39.523+09:00 DEBUG 36528 --- [           main] org.hibernate.SQL                        : 
    insert 
    into
        item
        (name, id) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        item
        (name, id) 
    values
        (?, ?)

✅ 오류 메시지 :

오류 메시지를 보자

Caused by: org.postgresql.util.PSQLException: → org.hibernate.exception.ConstraintViolationException

image.png

@NotNull에서는 ConstraintViolationException예외만 터졌었는데,

이번 경우에는 ConstraintViolationException + PSQLException 오류 메시지가 떴다.

org.hibernate.exception.**ConstraintViolationException**: could not execute statement [오류: "name" 칼럼(해당 릴레이션 "item")의 null 값이 not null 제약조건을 위반했습니다.
  Detail: 실패한 자료: (d069f405-8c04-453f-a88c-39ba797b3f9a, null)] [insert into item (name,id) values (?,?)]

    at org.hibernate.exception.internal.SQLStateConversionDelegate.convert(SQLStateConversionDelegate.java:97)
    at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:58)
    .....
Caused by: org.postgresql.util.**PSQLException**: 오류: "name" 칼럼(해당 릴레이션 "item")의 null 값이 not null 제약조건을 위반했습니다.
  Detail: 실패한 자료: (d069f405-8c04-453f-a88c-39ba797b3f9a, null)
    at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2733)
    .....

@Column 분석 정리

@Column(nullabe = false) 를 사용하는 경우에는 SQL 쿼리가 실제 실행되었고 DB에서 예외가 발생했다.

종합 결론

@NotNull과 @Column(nullabe = false) 모두 DB에 null값이 들어가는 것을 방지해주는 효과가 있지만

약간의 목적과 과정이 달랐다.

  • @NotNull : Bean Validation에 기반해, SQL 실행 전 애플리케이션 레벨에서 검증 수행
  • @Column(nullabe = false) : DDL 스키마에만 영향을 줌. 따라서 SQL 실행 후 검증은 DB에서 수행

✅ 검증 시점의 관점으로 볼 때 @NotNull

이번 내용만 보면, DB에 접근하기전에 검증이 일어나는 것이 더 좋으므로 @NotNull을 더 선호해야하는 게 맞다. (하지만 실제론 약간 다름)

실제론 왜 @Column을 쓰는 경우도 많은 데 그 이유는 나중에 볼 것

[2편] 그럼에도 @Column(nullable=false)을 쓰는 이유

1편에서 말했던 내용대로라면, @Column보다 @NotNull이 훨씬 좋은데 왜 @Column을 쓰는 것일까?

그 이유를 알아보자

주장 : Entity에는 검증을 최소화하고, DTO에서만 유효성 검사를 해야 한다

  • 이 말은 주로, DDD(도메인 주도 개발) 관점에서 아주 유효한 주장이다.
  • 일반적으로 스프링 MVC 아키텍쳐를 설계할 때 검증은 컨트롤러의 DTO에서 검증을한다.
// DTO
public class ItemDTO {
    @NotNull
    private String name;
}

// Entity
public class Item {
    @Column(nullable = false)
    private String name;
}

❗근데 엔티티를 생성, 수정 등을 하는 Service계층에서 또 검증을 하면 ‘오버 검증’이라고 생각하기 때문

  • 이런 오버 검증은 코드 유지보수가 어렵다는 문제도 있다.

@Column(nullable=false) 장점

1️⃣ 명시적 (추론력 상승) ⭐⭐⭐

  • 혼자 or 팀원들과 코드 리뷰를 할 때 이 코드만 보고도 DB가 어떤 제약조건을 가진지 예상할 수 있다.
  • 보통 스키마는 애플리케이션 코드 작성하는 부분과 다른 위치에 있으므로, 따로 찾아봐야 할텐데, 이렇게 명시하면 찾아보지 않아도 된다.

2️⃣ Hibernate 의존성 감소 ⭐⭐

  • Hibernate가 DDL을 자동으로 만들어주지 않더라도 nullabe=false를 하면 여전히 NotNull 제약이 추가된다.
  • 만약, Hibernate가 아닌 다른 ORM 기술 or 다른 접근 기술을 사용시 유연하게 대응할 수 있다,.

3️⃣ 다른 도구와의 호환 ⭐

@Column(nullable = false) 보고 스키마 자동 생성, ERD 뷰 만들어주는 도구들이 있는데, 이 코드가 없으면 툴 자동화 연동이 깨지는 문제가 발생한다.

운영 단계에서 지침

참고 : 일반적으로 운영 및 배포 단계에서는 Hibernate DDL 생성 기능을 안 쓴다.

대신, 직접 DDL 스크립트를 수동으로 작성해서 실행시키게 한다.

그렇기 때문에 운영 및 배포할 때 @Column(nullabe=false)를 같이, 코드에 제약 조건을 표현하는 것이 필요없다고 느낄 수 있다.

하지만 앞서 말했던, @Column의 장점들을 살리기 위해 개발자 간 암묵적 계약으로@Column(nullable=false) 를 표현해 두는 것이 좋은 것 같다.

'백엔드 > JPA' 카테고리의 다른 글

@ManyToOne의 optional, cascade 속성  (0) 2025.04.21
JPA 소개  (0) 2025.04.18
개선된 hibernate 6.0 @EntityGraph 에 미친 영향  (0) 2025.03.12
상속관계매핑 전략별 속도 비교  (0) 2025.02.14