Spring

연관관계 매핑

do_hyuk 2024. 1. 22. 22:26

포트폴리오 준비를 하면서 가장 헷갈렸던 내용으로 연관관계 매핑과 데이터베이스의 외래키와 똑같이 생각하다가

함정에 빠진적이 있기에 알아보겠다.

 

데이터베이스에서는 외래키라는 하나의 컬럼을 가지고 연관관계를 표현한다. 그러나 JPA에서는 객체를 매핑한다.

따라서 JPA에서의 연관관계 매핑은

  • 관계의 방향
  • 다중성(다대일? 일대다? 다대다?)
  • 연관관계의 주인

이 세가지가 중요하다. 아래에  정리한 내용에서 위의 3가지를 알아볼 것이다.


단방향 연관관계

연관관계 중 다대일(N:1)을 먼저 알아보겠다. 다음에서 설명할 예제의 객체들은 다음의 관계이다.

  • 게시글은 작성자가 있습니다.
  • 게시글은 한명의 작성자에게만 소속되어 있습니다.
  • 게시글은 작성자와 다대일 관계입니다.

위의 관계를 나타낸 객체 연관관계와 테이블 연관관계는 아래와 같다.

객체 연관관계
- 사람 객체는 Post.writer 로 작성자 객체와 연관관계를 맺는다.
- 게시글 객체와 작성자 객체는 단방향 관계이다. 게시글은 Post.writer를 통해 작성자를 알 수 있지만,
  작성자는 게시글을 알 수 없다.

테이블 연관관계
- 게시글 테이블은 Writer_id 외래 키로 작성자 테이블과 연관관계를 맺는다.
- 게시글 테이블과 작성자 테이블은 양방향 관계이다. 게시글 테이블의 Writer_id 외래키를 통해
  게시글과 작성자를 조인할 수 있고, 반대로 작성자와 게시글을 조인할 수도 있다.
  → 예 : 
             (1)  SELECT * FROM POST  P  JOIN  WRITER  W  ON  P.Writer_id = W.id
             (2)  SELECT * FROM WRITER  W  JOIN  POST  P  ON  P.Writer_id = W.id

 

위에서도 정리해 놓은 것처럼 참조를 통한 연관관계인 객체 연관관계는 언제나 단방향이다. 객체 간의 연관관계에서 양방향으로 만들고 싶다면 반대쪽에도 필드를 추가해서 보관해야 한다. 결국 양방향이라는 것은 단방향을 2개 만들어 서로 참조하도록 설계해야 하는 것이다.

 

그렇지만 데이터베이스 테이블은 외래키 하나로 양방향으로 조인할 수 있다.

 

단방향 연관관계인 Post 클래스와 Writer 클래스는 아래와 같다.

 

@Entity
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String content;

    @ManyToOne
    @JoinColumn(name = "author_id")
    private Writer writer;

    // getters and setters
}

 

@Entity
public class Writer {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="writer_id")
    private Long id;

    private String name;
    private String email;

    // getters and setters
}

 

이 때 사용한 어노테이션의 의미는 다음과 같다.

  • ManyToOne  :  다대일 관계라는 매핑 정보로 다대일 연관관계를 매핑할 때 필수로 사용해야 한다.
  • JoinColumn  :  외래키를 매핑할 때 사용한다. JoinColumn의 name 속성에는 매핑할 외래키 이름을 지정한다.
                                   이 어노테이션은 생략해도 된다. 만약 생략한다면, 외래키로 매핑되는 컬럼의 이름은
                                   필드명 + "_" + 참조하는 테이블의 컬럼명 이다.

양방향 연관관계

위의 예제에서는 항상 Post 클래스에서 Writer 클래스를 접근해야만 했다. 이번에는 Writer 클래스에서 Post 클래스를 접근하는

관계를 추가해보도록 하겠다. 다시 말하면 양방향으로 접근할 수 있는 양방향 연관관계로 매핑하겠다.

 

게시글과 작성자의 관계는 다대일이지만, 작성자와 게시글의 관계는 일대다이다. 따라서 일대다 관계는 여러 객체와 연관관계를 맺을 수 있기 때문에 컬렉션을 사용해야 한다. 나는 List 컬렉션을 사용하였다.

 

이 부분에서 헷갈리지 말아야 하는 부분은 바로 이것이다. 데이터베이스에서는 외래키 하나를 이용해 양방향으로 조회할 수 있다.

 

@Entity
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String content;

    @ManyToOne
    @JoinColumn(name = "author_id")
    private Writer writer;

    // getters and setters
}

 

@Entity
public class Writer {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="writer_id")
    private Long id;

	@OneToMany(mappedBy = "writer")
    private List<Post> postList = new ArrayList<>();
    
    private String name;
    private String email;

    // getters and setters
}

 

위와 같이 Writer 클래스에 postList라는 List를 추가했다. 그리고 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를

사용했다. mappedBy 속성은 양방향 매핑일 때 사용하게 되는데, 반대쪽 매핑의 필드 이름을 값으로 주면 된다. 위에서는

Post 클래스에 writer와 매핑할 것이기 때문에 writer를 적으면 된다.

 

이 때 간과하지 말아야 할 개념은 연관관계의 주인이다.


연관관계의 주인?

위의 예제에서 @OneToMany만 있어도 될 것 같은데, mappedBy 속성까지 있어야 하는 이유는 무엇일까?

 

엄밀하게 말하자면 객체에는 양방향 연관관계라는 것은 없다. 서로 다른 단방향 연관관계 2개를 잘 묶어서 양방향인 것처럼 보이게 할 뿐이다. 반면에 데이터베이스 테이블은 외래키 하나로 양쪽이 서로 조인을 해서 양방향 연관관계를 맺는다.

 

Entity를 단방향으로 매핑하면 참조를 하나만 사용하기 때문에 이 참조로 외래키를 관리하면 된다. 그러나 Entity를 양방향으로

매핑하면 두 곳에서 서로를 참조하게 되기 때문에 객체의 연관관계를 관리하는 포인트는 두 곳으로 늘어나게 된다.

 

엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래키는 하나다?

 

이 문제를 해결하기 위해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을

연관관계의 주인이라 한다.

 

양방향 연관관계 매핑에서는 두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다.

 

연관관계의 주인만이 데이터베이스 연관관계와 매핑되며 외래키를 관리(등록, 수정, 삭제) 할 수 있다. 

반면에 주인이 아닌 쪽은 읽기만 할 수 있다.

 

이 때, 주인은 mappedBy 속성을 사용하지 않는다.

또한 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.

 

그렇다면 위의 예제이서 Post가 주인이 되어야 할까? 아니면 Writer가 주인이 되어야 할까?

 

이 과정에서 생각해봐야 할 것은 연관관계의 주인을 정한다는 것은 외래키 관리자를 선택하는 것이다. 따라서 위의 예제에서는 Post가주인이 되어야 본인 테이블에 있는 외래키를 관리할 수 있다. 만약에 Writer Entity에 있는 post가 주인이 된다면 물리적으로 다른

테이블의 외래키를 관리해야 한다. 따라서 주인이 아닌 post에는 mappedBy="writer속성을 사용해 주인이 아님을 설정해야 한다.


양방향 연관관계에서의 주의사항

양뱡향 연관관계에서 조심해야 할 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 넣는 경우이다.

다음 예제를 보겠다.

Post p1 = new Post("string1");
entityManager.persist(p1);

Post p2 = new Post("string2");
entityManager.persist(p2);

Writer w = new Writer();
w.getPost().add(p1);
w.getPost().add(p2);

entityManager.persist(w);

 

위의 코드에서는 p1, p2 객체의 w 멤버변수를 설정하는 것이 아니라 writer 객체의 post 리스트를 수정했다.

이 후 데이터베이스를 조회해보면 Post 테이블의 writer_id에는 null 값이 있을 것이다. 주인인 Post에 값을 입력하지 않고,

주인이 아닌 Writer에 값을 넣었기 때문이다.

 

그렇다면 주인인 Entity에만 값을 넣는 것이 바람직한 것일까?

 

객체 관점에서 가장 안전한 것은 주인/주인이 아닌 Entity 모두 값을 입력해주는 것이 안전하다.

 

사실 이런 경우에는 편의 메소드를 구현하는 것이 좋다. 편의 메소드란 한 번에 양방향 관계를 설정하는 메소드를 말한다.

아래의 함수를 보겠다.

@Entity
public class Post {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="post_id")
    private Long id;

    private String title;
    private String content;

    @ManyToOne
    @JoinColumn(name = "writer_id")
    private Writer writer;

    public Post(String title){
    	this.title = title;
    }

    public void setWriter(Writer writer) {
        if (writer != null) {
            writer.removePost(this);
        }
        this.writer = writer;
        writer.addPost(this);
    }
}

 

@Entity
public class Writer {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="writer_id")
    private Long id;

	private String name;
    
    private String email;
	
    @OneToMany(mappedBy = "writer")
    private List<Post> postList = new ArrayList<>();
    
    public void addPost(Post post){
    	this.post.add(post);
    }
    
    public void removePost(Post post){
    	this.post.remove(post);
    }
	
    // getters and setters
}

 

위의 예제에는 Post 클래스에 setWriter()를 , Writer 클래스에 addPost(), removePost() 메소드를 구현했다. 

따라서 Post.setWriter()메소드를 호출하면 Post와 Writer 객체 모두 양방향 관계를 설정하게 된다. 이런 메소드를 편의 메소드라고 한다.

 

따라서 객체의 양방향 연관관계를 사요하기 위해서는 로직을 견고하게 작성해야 한다.

 

위의 내용을 정리하면 다음과 같다.

  • 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
  • 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.

마지막으로 연관관계의 주인을 정하는 기준은 외래키의 위치와 관련해서 정해야 하며 비즈니스 중요도로 접근하면 안된다는 점을 명심하자.

 

[Ref] 자바 ORM 표준 JPA 프로그래밍