JPA의 일대다 단방향 매핑시 발생하는 문제 정리


Author -< Book 과 같은 일대 다 관계에서 생각없이 Author쪽에만 Book List에 Mapping을 걸때 어떤 문제가 생길까? 예시와 함께 알아보자


샘플 Class

1. Author

@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "author_id")
    private List<Book> books = new ArrayList<>();

    // 생성자, 게터/세터 등 생략

    public void addBook(Book book) {
        books.add(book);
    }

    public void removeBook(Book book) {
        books.remove(book);
    }
}

2. Book

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    // 생성자, 게터/세터 등 생략

    @Override
    public String toString() {
        return "Book [id=" + id + ", title=" + title + "]";
    }
}

예시 코드는 Author 엔티티와 Book 엔티티 간의 일대다 관계를 보여주며 Author 엔티티는 books라는 컬렉션으로 Book 엔티티와 관계를 맺고 있다.

테스트

다음과 같이 간단한 테스트를 실행해보자

@Test
public void testOneToManyRelationship() {
    Author author = new Author();
    author.setName("John Doe");

    Book book1 = new Book();
    book1.setTitle("Book 1");

    Book book2 = new Book();
    book2.setTitle("Book 2");

    author.addBook(book1);
    author.addBook(book2);

    // 2개의 Book을 가진 Author 생성
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    entityManager.getTransaction().begin();
    entityManager.persist(author); // A. 생성시점
    entityManager.getTransaction().commit();
    entityManager.close();

    entityManager = entityManagerFactory.createEntityManager();
    Author savedAuthor = entityManager.find(Author.class, author.getId());

    // 1개의 Book 삭제
    savedAuthor.removeBook(book1);

    entityManager.getTransaction().begin();
    entityManager.merge(savedAuthor); // B. 삭제시점
    entityManager.getTransaction().commit();
    entityManager.close();
}

A. 생성시점의 persist() 에서는 단순하게 Author Insert 1회, Book Insert 2회가 발생할 것으로 예상할 수 있다.
하지만 예상과 다르게 Book Update 2회가 불필요하게 발생한다.

Hibernate: insert into author (name) values (?)

Hibernate: insert into book (id, title) values (null, ?)
Hibernate: insert into book (id, title) values (null, ?)

Hibernate: update book set author_id=? where id=?
Hibernate: update book set author_id=? where id=?

B. 삭제시점의 merge() 메소드를 호출하면 JPA는 Author 엔티티의 books 컬렉션에서 Book을 제거한 상태를 감지하고 데이터베이스에 해당 변경을 반영하기 위해 SQL UPDATE 문을 실행한다.
하지만 이 경우, 일대다 단방향 매핑에서는 Author 엔티티만이 Book 엔티티를 관리하고, Book 엔티티는 자신을 소유하는 엔티티에 대한 정보를 가지고 있지 않기 때문에, Author 엔티티의 books 컬렉션에서 Book을 제거하는 것만으로는 변경 사항을 정확히 표현할 수 없다.

실제로는 다음과 같은 행위가 일어난다.

Author의 Book 2개 모두 delete 되면서 Book1을 Book2가 Insert된다. 그리고 마지막에 Book 테이블에서 Book1 delete가 실행된다.

만약 Author의 Book이 수백 수천개라면? 수백 수천회의 Insert가 일어날 수 있다.

단순히 Author를 삭제한다고 해도 다음과 같은 결과가 나온다.

Hibernate: update book set author_id=null where author_id=? and id=?
Hibernate: delete from book where id=?

결론

일대다 매핑은 사용하지 말고 양방향으로 매핑하여 사용하자. 다른 매핑 관계 설정도 모두 쌍방으로 하자.

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    // 생성자, 게터/세터 등 생략

    // Author 필드 추가
    @ManyToOne
    @JoinColumn(name = "author_id")
    private Author author;

    @Override
    public String toString() {
        return "Book [id=" + id + ", title=" + title + "]";
    }
}