티스토리 뷰
Entity를 생성할 때 위도, 경도와 같이 비슷한 유형의 데이터를 하나로 묶어 사용하거나 특정 필드에 대한 로직을 분리하고자 할 때 @Embeddable
을 통해 값 객체를 생성하여 사용합니다. 이때 NullPointerException
이 발생할 수 있는데 문제가 발생할 수 있는 상황과 해결할 수 있는 방법에 대해 정리해보고자 합니다.
문제
@Getter
@Entity
@Table(name = "member")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends AuditingEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "email", length = 255)
private String email;
@Embedded
private Introduction introduction;
//...
}
회원에 대한 데이터를 가지고 있는 Member Entity를 구현하였을 때, 소개에 대한 제약사항이 존재하여 Introduction
이라는 값 객체를 구현하였습니다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class Introduction {
private static final int MAXIMUM_LENGTH = 100;
@Column(name = "introduction", length = MAXIMUM_LENGTH)
private String value;
public Introduction(String value) {
validate(value);
this.value = convertWhenEmpty(value);
}
private void validate(String value) {
if (value != null && value.length() > MAXIMUM_LENGTH) {
throw new InvalidArgumentException(MemberExceptionType.INVALID_INTRODUCTION);
}
}
private String convertWhenEmpty(String value) {
if (value == null || value.isEmpty()) {
return null;
}
return value;
}
}
소개는 최대 100글자까지 가능하기에 100글자가 넘는 경우 예외를 던지도록 하였습니다. 또한 소개를 입력하지 않는 경우도 서비스에서 허용하고 있기에 null
이거나 ""
와 같이 비어있는 경우 일관성을 위해 DB에 null로 저장하도록 구현하였습니다.
여기서 문제가 발생하게 됩니다.
Spring Data JPA
의 기본 구현체로 Hibernate
를 사용하고 있는데, Hibernate 구현체에서 @Embedded 객체의 모든 필드의 값이 null이면 해당 객체 자체를 null로 설정하게 됩니다.
public String getIntroduction() {
return this.introduction.getValue();
}
Member 객체에서 소개를 조회하고 싶을 때 소개가 비어있을 때는 getValue
의 값이 null이기를 기대했지만, introduction 자체가 null이 되어버리기 때문에 예상하지 못했던 NullPointerException
이 발생하게 됩니다. 이를 막기 위해서는 아래와 같이 null 방어 코드를 작성해야 합니다.
public String getIntroduction() {
if (this.introduction == null) {
return null;
}
return this.introduction.getValue();
}
값 객체를 사용할 때마다 매번 방어 코드를 작성하는 일은 매우 번거로운 일이 될 것이기에 이를 해결하기 위해서는 아래와 같은 방법을 사용해 볼 수 있습니다.
해결
Null일 수 없는 값을 값 객체에 함께 두는 방법
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class Introduction {
private static final int MAXIMUM_LENGTH = 100;
@Column(name = "introduction", length = 255)
private String value;
@CreatedDate
@Column(name = "created_at", columnDefinition = "datetime(6)", nullable = false, updatable = false)
private LocalDateTime createdAt;
//...
}
@Embedded 객체가 null이 되는 조건은 해당 객체의 모든 필드가 null인 상황이여만 하기 때문에, 1개의 필드라도 null이 되지 않는 경우 해당 객체는 null이 되지 않습니다. 따라서 생성 시간과 같이 절대 null이 될 수 없는 필드를 값 객체 내부에 둔다면 NullPointerException을 막을 수 있습니다. 하지만 이는 AuditingEntity를 재사용하기도 불편하고 비슷한 데이터를 묶는 값 객체와는 거리가 먼 구현 방식이라고 생각합니다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class Introduction {
private static final int MAXIMUM_LENGTH = 100;
@Column(name = "introduction", length = 255)
private String value;
@Formula("0")
private int dummy;
//...
}
위의 방법보다 나은 방법으로 더미 데이터를 포함되게 하는 방법도 존재합니다. 해당 더미 데이터의 값을 조회 시마다 설정하도록 하여 객체 자체가 null이 되는 것을 막을 수 있습니다.
이 문제를 많은 사람들도 겪게 되어 이에 대한 논의가 활발하게 이루어졌고, 결국 Hibernate에서 해당 설정을 변경할 수 있게 2016년 5.1 패치로 수정하게 되었습니다.
설정을 통해 Null을 방지하는 방법
//application.properties
spring.jpa.properties.hibernate.create_empty_composites.enabled=true
위의 설정을 설정 파일에 맞게 적용하면 @Embedded 객체의 모든 필드가 null이어도 해당 객체 자체가 null이 되는 것을 방지할 수 있습니다.
마치며
위의 해결 방법으로 문제를 해결할 수 있지만, Hibernate 기본 스펙이 @Embedded 객체의 모든 필드가 null일 때 해당 객체를 null로 설정하는 만큼 원칙을 지키고자 한다면 설계를 먼저 점검해 봐도 좋을 것 같다는 생각이 들었습니다.
참고
'Spring > Spring Boot' 카테고리의 다른 글
[Spring Boot] Apple OAuth 로그인 구현(1) - Apple OAuth 정리 (0) | 2023.08.06 |
---|---|
[Spring Boot] 스프링 부트 프로젝트에 Swagger 적용 (10) | 2023.07.16 |
[Spring Boot] @DataJpaTest 데이터베이스 환경 문제 (3) | 2023.07.07 |
[Spring Boot] 프로메테우스, 그라파나를 이용한 스프링 부트 모니터링 (3) | 2023.07.02 |
[Spring] 영속성 컨텍스트는 어디까지 유지되는가 (4) | 2023.03.12 |