Entity 설계 시 클라이언트 null 검증 vs DB 제약 조건 not null
도메인 엔티티 필드의 유효성을 검증하는 방법에는 여러 가지가 있다.
1. 필드에 @NonNull 붙이기
2. 필드에 @NotNull 붙이기
3. nullable = false 설정하기
각 어노테이션의 차이점과 내가 생각하는 적절한 대안이란 무엇일까 고민해본 부분에 대해서 작성해보려고 한다.
@Column(nullable = false)
JPA는 @Entity를 설계한 뒤 실제 Hibernate sql을 보면 자동으로 DDL을 생성한다. @Column을 통해 칼럼에 대한 제약 조건을 걸어줄 수 있는데 nullable = false로 두면 create table query에서 해당 칼럼에 not null 조건이 자동으로 명시된다.
하지만 이 @Column에 의한 DDL 조건은 스프링 자바 애플리케이션 내부가 아닌 DB 컬럼 속성 조건이기 때문에 엔티티 객체 필드의 nullable false를 보장하지 못한다.
실험을 해보자! 간단하게 유저 테이블에 pk, userId, email 필드를 구성했다. 여기서 email이 nullable = false이다.
@Entity
public class User extends AuditingEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "user_id", length = 20)
private String userId;
@Column(name = "email", nullable = false, length = 50)
private String email;
}
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@RequiredArgsConstructor
@DataJpaTest
class UserRepositoryTest {
private final UserRepository userRepository;
@Test
void save() {
User user = new User("rla124", null);
User savedUser = userRepository.save(user);
assertThat(savedUser).isEqualTo(user);
}
}
테스트 결과는? 실패!

해당 오류는 app 내부가 아닌 실제 db 내에서 발생한 예외이다.
nullable = false의 경우 DDL이기 때문에 User 객체 상에서 email 칼럼이 null일 경우에 대해서는 처리하지 않고 실제 db에 쿼리가 실행되어야만 필드가 null이기 때문에 예외가 발생하게 된다.
그리고 이때 예외가 터져도 쿼리는 실행 되었기 때문에 pk id++하게 된다.
이를 해결할 수 있는 방법이 아래 어노테이션이다.
@NotNull
NotNull은 대표적으로 validation을 할 수 있는 어노테이션이다.
DTO class 또는 record의 필드를 검증할 수도 있고 엔티티에서도 사용이 가능하다.
공식 문서(Hibernate NotNull vs nullable = false) 참고
The @NotNull annotation is defined in the Bean Validation specification.
실제 공식 문서를 보면 이 게시글에서 보인 동일한 테스트 조건에서 (즉, email 필드에 nullable = false가 아니라 @NotNull을 붙이고 email 필드가 null인 엔티티 객체 insert 실험을 했을 때) constraintViolationException이 터진다.
in this case, our system threw javax.validation.ConstraintViolationException.
nullable = false를 적용했을 때처럼 bean validation @NotNull에 대해서 마찬가지로 create table DDL에서 칼럼에 not null 조건을 부여한다.
하지만 위와 다른 점은 insert 쿼리가 찍히지 않는다는 점이다.
Hibernate didn’t trigger the SQL insert statement. Consequently, invalid data wasn’t saved to the database.
이 부분이 핵심인데 nullable = false의 경우 필드가 null인 쿼리가 실행이 되고 예외가 던져졌지만,
@NotNull은 쿼리가 실행되기 전에 스프링 자바 애플리케이션에서 먼저 필드가 null인 엔티티가 영속화되는 시점에 예외(javax)를 던졌다는 것이다.
두 마리 토끼를 다 잡는 방법이다. DDL not null + 영속성 컨텍스트에 엔티티 필드 null도 막고! 쿼리도 날라가지 않고!
그래서 이 경우에는 nullable = false가 필요 없게 된다.
@NonNull
/**
* If put on a parameter, lombok will insert a null-check at the start of the method / constructor's body, throwing a
* {@code NullPointerException} with the parameter's name as message. If put on a field, any generated method assigning
* a value to this field will also produce these null-checks.
*/
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE, ElementType.TYPE_USE})
@Retention(RetentionPolicy.CLASS)
@Documented
public @interface NonNull {
}
이 어노테이션은 런타임 시 유효성 검증을 하지 않으며 필드에 해당 어노테이션을 붙여도 DDL에 적용되지 않는다.
notnull처럼 message를 지정할 수는 없지만 파라미터 인자에 붙일 경우 NPE를 발생시킬 수 있다.
(notnull은 메소드 파라미터에서는 아무런 역할도 하지 못한다.)
필드에 NonNull을 붙일 경우, nullable false DDL 설정이 반드시 필요하다.
또한, 객체 필드가 null일 경우에 대해 별도의 검증 로직이 필요하다.
엔티티와 서비스/DTO 간 사용자 입력 데이터 검증 책임 분리
이 게시글의 결론을 정리하려고 한다.
자바 단에서 엔티티 필드의 null + ddl을 다 잡을 수 있는 방법은 @NotNull이다. 위 공식 문서 링크도 아래와 같이 @NotNull을 권장하고 있다.
As a rule of thumb, we should prefer the @NotNull annotation over the @Column(nullable = false) annotation.
그리고 만약 @NotNull을 쓰지 않을 경우 nullable = false로 DDL 설정은 하되 엔티티에 NonNull을 붙이는 것이 아니라 DTO 필드 혹은 서비스 레이어에서 사용자 입력 데이터에 대한 null 검증을 하는 것이 좋다고 생각한다.
그 이유는 entity는 db 매핑에 집중시키고 다른 계층에서 검증을 하도록 역할을 위임함으로써 책임 분리와 유지 보수성을 명확하게 하기 위함이다.
'SpringBoot > 오개념 때려잡기' 카테고리의 다른 글
[Spring] @EntityScan은 언제 필요할까 (0) | 2025.04.27 |
---|---|
[Spring] SecurityFilterChain 구성 요소 (0) | 2024.09.01 |
[Spring] @NoArgsConstructor (access = AccessLevel.PROTECTED)의 타당성 (0) | 2024.08.11 |
[Spring] JPA 일대다 양방향 관계 주의할 점: MappedBy와 연관 관계 주인 (0) | 2024.05.13 |