티스토리 뷰

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로 설정하는 만큼 원칙을 지키고자 한다면 설계를 먼저 점검해 봐도 좋을 것 같다는 생각이 들었습니다.

참고

댓글
최근에 올라온 글
최근에 달린 댓글
«   2024/06   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
Total
Today
Yesterday