개발하는 자몽

[JPA] OneToMany 양방향 관계 본문

Java/Spring

[JPA] OneToMany 양방향 관계

jaamong 2023. 6. 14. 18:16
백기선님 유튜브 영상

백기선 님의 해당 유튜브 영상을 보고 작성한 글입니다.

 

 

 

뭐가 문제일까?

Goal

Book 엔티티와 BookStore 엔티티를 양방향 관계를 맺도록 하기

 

실행

Book 엔티티와 BookStore 엔티티가 아래 코드와 같을 때 아래 테스트 코드를 실행해 보자.

 

Book Entity

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter @Setter
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    private String isbn;

    private String title;

    @ManyToOne
    private BookStore bookStore;
}

 

BookStore Entity

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.HashSet;
import java.util.Set;

@Entity
@Getter @Setter
public class BookStore {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "bookStore")
    private Set<Book> books = new HashSet<>();

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

 

서점에 책을 등록하는 테스트 코드

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DemoApplicationTests {

	@Autowired
	BookStoreRepository bookStoreRepository;

	@Autowired
	BookRepository bookRepository;

	@Test
	void contextLoads() {
		BookStore bookStore = new BookStore();
		bookStore.setName("누군가의 서점");
		bookStoreRepository.save(bookStore);

		Book book = new Book();
		book.setTitle("빈둥거리는 삶");

		bookStore.add(book); //서점에 책 추가

		bookRepository.save(book);
	}
}

 

Actual

테스트 코드를 실행하고 각 테이블을 확인해 보자. BookStore 테이블에는 Book의 PK값인 id가 잘 들어가 있지만, Book 테이블에는 BookStore의 id가 null값인 것을 확인할 수 있다.

 

book_store 테이블 조회
book 테이블 조회

 

 

@ManyToOne과 @OneToMany(mappedBy)를 사용했는데 왜 이런 문제가 발생한 걸까?

 

 

해결

단방향 관계

Book 엔티티는 그대로 두고 맨 처음 BookStore는 아래 코드처럼 조금 수정했다. @OneToMany에 mappedBy를 사용하지 않았다.

package com.example.demo;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.HashSet;
import java.util.Set;

@Entity
@Getter @Setter
public class BookStore {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany
    private Set<Book> books = new HashSet<>();

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

 

Book 엔티티와 BookStore 엔티티 코드를 보면 @ManyToOne와 @OneToMany를 사용하여 서로를 참조하고 있다. 이렇게 서로 참조하고 있는 Book과 BookStore는 양방향 관계가 아닌, 서로 다른 두 개의 단방향 관계이다. 이 상태에서 다시 테스트 코드를 실행해보자. 

 

실행하면 book과 book_store 테이블 외에 book_store_books라는 테이블이 생긴 것을 알 수 있다. 이 테이블은 BookStore와 Book의 id를 갖고 있는 조인 테이블(Join Table)이다. 이렇게 되는 원인은 Hibernate의 @OneToMany 기본 매핑 방법이 JoinTable이기 때문이다. 

JPA는 ORM으로 매핑 시 따로 연관관계를 설정해주지 않으면 두 엔티티가 관계를 맺을 때, 한쪽의 엔티티만 참조하게 된다. 

 

양방향 관계

위처럼 mappedBy를 사용하지 않으면 서로 객체를 참조하는 양방향 매핑이 아닌, 각각 참조하는 단방향 매핑이 된다. (물론 양방향 매핑 방법이 mappedBy만 있는 것은 아니다)

 

@ManyToOne과 @OneToMany를 사용하여 양방향 관계를 맺을 땐 mappedBy를 사용하여 관계의 주인을 설정해야 한다. 이때 mappedBy가 설정되지 않은 엔티티, 즉 Many인 쪽이 관계의 주인(owner)이 된다. (반대쪽을 주인으로 설정하는 방법도 있지만, 여기서는 다루지 않는다)

 

이번에는 처음처럼 mappedBy를 사용하여 다시 테스트 코드를 실행해 보자. 이렇게 스키마를 만들면 테이블이 두 개만 존재한다. 또한 Book에서 BookStore의 FK(book_store_id)를 갖게 된다.

 

 

 

관계의 주인이라는 것은 관계의 주인 쪽에 관계가 설정되어야 한다는 의미이다. 이렇게 해야 DB에 반영이 된다. 다시 BookStore 엔티티 코드를 보자.

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.HashSet;
import java.util.Set;

@Entity
@Getter @Setter
public class BookStore {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "bookStore")
    private Set<Book> books = new HashSet<>();

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

add()를 보면 BookStore 자기 자신에게만 관계를 설정하고, Book에는 관계를 설정하고 있지 않아서, DB에 반영할 것이 없다. 이 경우 각 객체의 상태가 변경되어도 아무런 일이 벌어지지 않는다. 왜냐하면 관계의 주인인 Book에 아무런 변화가 일어나지 않았기 때문이다. 

 

따라서 아래 코드와 같이 Book 쪽에도 관계의 변경을 설정해 줘야 한다. 

package com.example.demo;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.HashSet;
import java.util.Set;

@Entity
@Getter @Setter
public class BookStore {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "bookStore")
    private Set<Book> books = new HashSet<>();

    public void add(Book book) {
        book.setBookStore(this); // 추가된 코드, Book에도 관계의 변경을 설정하자.
        this.books.add(book);
    }
}

이렇게 관계의 주인인 Book 자기 자신에게  BookStore를 설정해줘야 DB에 반영된다. 다시 테스트 코드를 실행해보자.

 

이번엔 제대로 BookStore의 FK 값이 들어가 있다!

 

 

SUMMARY

양방향 관계를 맺을 때 객체 관점으로 바라보면, Book에도 BookStore가 설정되어야 하고, BookStore가 가지고 있는 Book에도 Book을 추가해 주는 게 맞다.

...
public class BookStore {    
	
    ...
    
    @OneToMany(mappedBy = "bookStore")
    private Set<Book> books = new HashSet<>(); //BookStore가 가지고 있는 Book

    public void add(Book book) {
        book.setBookStore(this); //Book에 BookStore 넣어주기
        this.books.add(book); //BookStore가 가지고 있는 Book에 book 넣어주기
    }
Comments