Table of contents

상황

Springboot + JPA에서 Entity 매핑

  1. @EmbeddedId를 사용해 복합키를 지정
@Entity
public class Post extends AbstractEntity {

    @OneToMany(
            mappedBy = "post",
            cascade = CascadeType.ALL,
            orphanRemoval = true
    )
    private List<PostKeyword> keywords = new ArrayList<>();
    // ...
}

@Entity(name = "PostKeyword")
@Table(name = "post_keyword")
public class PostKeyword extends AbstractEntity {

    @EmbeddedId
    private PostKeywordId id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("postId")
    private Post post;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("keywordId")
    private Keyword keyword;
    
    // ...
}

@Embeddable
public class PostKeywordId implements Serializable {

    @Column(name = "post_id")
    private long postId;

    @Column(name = "keyword_id")
    private String keywordId;
    
    // ...
}
  1. 게시글(Post)에 키워드를 추가, 예외 발생
// service
@Transactional
public PostKeyword addKeyword(long customerId, long id, String keywordId) {
    Post post = postRepository.findByCustomerIdAndId(customerId, id);
    Keyword keyword = keywordRepository.getOne(keywordId);

    return post.addKeyword(keyword);
}

// post
public PostKeyword addKeyword(Keyword keyword) {
    PostKeyword postKeyword = new PostKeyword(this, keyword);
    this.keywords.add(postKeyword);

    return postKeyword;
}

// PostKeyword
public PostKeyword(Post post, Keyword keyword) {
    this.post = post;
    this.keyword = keyword;

    this.baseRank = keyword.getBaseRank();
}

예외

org.hibernate.propertyAccessException: could not set field value by reflection

원인

직접적인 원인은 NullPointerException이다. post의 addKeyword 메소드에서 PostKeywordId를 생성한다. 이 때, Post와 Keyword를 생성자로 전달한다. PostKeyword 클래스에는 EmbeddedId 어노테이션이 지정된 PostKeywordId 타입의 id 속성이 존재한다. 그러나 해당 생성자에서는 id 속성의 값을 설정하지 않았으며, id 속성은 null이 된다. 결국 id 속성에 접근할 때 NullPointerException가 발생한다.

그렇다면 어디서 PostKeyword 클래스의 id 속성에 접근하는가?

// SimpleJpaRepository
@Transactional
public <S extends T> S save(S entity) {

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

예외 스택을 추적하다보면 SimpleJpaRepository의 save가 호출됐음을 알 수 있다. save메소드는 em.persist를 통해 entity를 영속성 컨테이너에 저장한다.

그 이후 DefaultPersistEventListener의 onPersist라는 훅 메소드에서 entityIsTransient 메소드를 호출하고 다시 entityIsTransient에서 saveWithGeneratedId 메소드를 호출한다.

마지막으로 saveWithGeneratedId 메소드에서 setPropertyValue를 호출하면서 PostKeyword의 id 속성에 접근한다. 그러나, id 속성이 null이기 때문에 NullPointerException가 발생하는 것이다.

해결법

해결법은 간단하다. EmbeddedId 어노테이션이 지정된 속성이 null이 아니기만 하면 된다.

@Entity(name = "PostKeyword")
@Table(name = "post_keyword")
public class PostKeyword extends AbstractEntity {

    @EmbeddedId
    private PostKeywordId id = new PostKeywordId();
    
    // ...
}