들어가며
JPA 사용시 Entity 에서 관련된 데이터들을 하나로 묶을 때 VO 객체로 @Embeddable 을 사용할 수 있다. @Embeddable 사용 시 각각의 컬럼에 대해 null 값을 허용하도록 만들 수 있는데, 이 경우 NullPointerException 이 발생할 가능성이 있다. 이번 글은 해당 문제상황과 해결 방법에 대한 글이다.
문제가 발생한 환경
고객 주소 엔티티에서 배달 정보에 대한 관련된 값들을 묶기 위해 아래 VO 를 만들어서 사용하고 있다.
각각 필드는 고객이 입력할 수도 있고, 입력하지 않을 수도 있기 때문에 optional 한 필드로 구성했다.
- @Column(nullable=false) 명시 X
- 생성시 null 체크 X

Repository 에서는 아래처럼 고객 주소 엔티티를 불러오고 있다. (최대 주소지 개수는 20개로 페이징처리는 따로 하지 않았다.)

이후 application service 에서 고객주소 엔티티의 getter 에서 해당 VO 를 읽고 Result 객체로 변환해서 web 계층에 넘기면, web 계층에서 클라이언트에게 응답으로 넘겨주는 구조다.

문제 발생 상황
해당 이슈는 통합테스트에서 잡을 수 있었다. 테스트에서는 아래처럼 @SqlGroup 을 두고 setup.sql 로 초기 데이터를 집어넣고 있다. schema 는 flyway 를 사용하고 있으므로, migration script 가 적용받도록 그대로 두고 setup 과 delete 만 각각 sql 파일만 구성했다.

setup.sql 에 nullable 한 필드에 대해서는 null 값을 집어넣는 케이스를 두고 테스트를 진행하고 있었다.

여기서 deliveryInfo 가 null 로 매핑되었고, response 객체로 변환하는 과정에서 필드 접근이 일어나 NullPointException이 발생했다.

문제 원인
이 글을 적기 전까지 모든 필드가 nullable 할 수 있는 @Embeddable 을 구성해 본 적이 없어서 해당 이슈에 대해 모르고 있었다.
확인해보니, @Embeddables 로 설정한 객체는 모든 필드가 null 일 경우 null 로 매핑된다. (아래 문서 참조) 따라서 위의 CustomerDeliveryInfoGuide 가 null 이 되었고, 응답 객체 매핑에서 NPE 가 발생한 것이다.
해결 방법
Hibernate 5.1 feature realease 를 보면 아래 옵션을 확인할 수 있다.

hibernate.create_empty_composites.enabled
개발자가 다음 중 하나를 선택할 수 있게 제공하는 옵션이다.
- 모든 컬럼이 null일 때 객체를 null로 간주 (기존 방식)
- 모든 컬럼이 null이더라도 비어 있는 객체(empty instance) 를 생성
이 옵션으로 해결하면 좋겠지만, 현 시점에서 해당 옵션은 사용이 권장되지 않는다.

글을 쓰는 시점에서 최신 버전(Hibernate 6.2) 기준으로 deprecated 되었고 프로덕션에서 사용을 권장하지 않는다고 명시되어 있다.
따라서 이를 해결하려면 다음과 같은 방법을 써야 한다.
- 항상 존재하는 어떤 필드를 추가해서 넣어준다.
- 기존에 있는 필드가 null 이 되지 않도록 기본값을 세팅한다.
- 별도 엔티티로 분리한다.
어떻게 해결했는가?
그러면 어떻게 해결해야 하는가?
위에 두 가지 방법을 적용하기 전에, 먼저 내 설계가 잘못되었는지 검증했다.
고객 주소지부터 보면, CustomerAddress 는 Customer 가 존재해야 생성될 수 있지만 Customer 와 독립적으로 수정, 삭제가 이루어진다.
=> 생명주기 일치 안하니까 별도 엔티티로 분리한다.
그러면, 고객 주소지 내부의 CustomerDeliveryGuideInfo 는 어떻게 판단할까? 아래와 같이 기준을 정하고 생각해보았다.
CustomerDeliveryGuideInfo가 항상 존재해야 한다면
- CustomerAddress가 생성될 때 CustomerDeliveryGuideInfo도 무조건 생성되어야 한다.
- 내부 필드 값(riderMessage, entrancePassword 등)이 비어 있을 수는 있지만, 객체 자체는 null이 되어서는 안 된다.
CustomerDeliveryGuideInfo가 있을 수도 있고, 없을 수도 있다면
- 어떤 주소지에서는 배달 안내 정보가 아예 필요 없는 케이스가 존재하는 경우가 이에 해당한다.
- 이 경우 @Embedded를 쓰면 안되고, 별도의 엔티티로 분리하고, 연관관계를 맺는 것이 올바를 것 같다.
지금 상황에서는 주소지마다 반드시 배달 정보가 존재해야 한다. 배달 안내 정보는 비어있을 수는 있어도 배달 안내 정보가 존재하지 않는 주소지는 없다. 또한, 배달 안내 정보만 따로 변경하는 유스케이스도 없다. 따라서 주소지라는 개념에 종속된 일부에 해당하므로 @Embeddable 설계 자체는 문제가 되지 않는다고 판단했는데, Hibernate 매핑에서 기술적 이슈때문에 객체 자체가 null 이 되어 버리므로, 그대로는 사용할 수 없는 상황이다.
결론적으로 아래와 같이 구성해서 해결했다.
정적팩토리 메서드를 이용해 "아직 설정되지 않은 배달 안내정보"라는 의미로 객체를 생성해서 반환하고

고객 주소지 엔티티의 getter 에서 고객이 배달안내정보를 설정하지 않은 경우, 위에서 만든 메서드를 호출해, 설정되지 않은 주소지 객체를 반환한다.

VO 객체와 기존 로직을 수정하지 않고, 테스트가 정상적으로 통과하는 것을 확인할 수 있었다.

'JPA' 카테고리의 다른 글
| [Hibernate] Hibernate6.2 부터 변경된 @Enumerated(EnumType.STRING) 매핑방식 (0) | 2025.04.26 |
|---|---|
| [JPA] EntityManager 정리 (0) | 2024.09.01 |