연관관계 매핑

객체 연관관계와 테이블 연관관계의 차이점

  • 객체는 참조(주소)값을 기준으로 연관관계를 맺는다.(단방향)
    ex) X->Y , Y->X
  • 테이블은 외래키를 기준으로 연관관계를 맺는다. (JOIN operation , 양방향)
    ex) X JOIN Y 는 Y JOIN X 도 가능하다.

단방향 연관관계

  • 두 entity 중 어느 한쪽만 다른 한쪽을 참조하는 경우을 단방향이라고 한다.
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
//회원 entity
@Entity
public class Member {

@Id
@Column(name = "member_id")
private String id;

private String username;

}
// 게시글 entity
@Entity
public class Board {

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

private String title;

private String content;
// 연관관계
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;

}

예를 들어 회원이 여러개의 게시글을 사용할 수 있는 다대일(게시글 N : 회원 1) 상황을 고려하면 단방향에서는 회원->게시글을 참조하거나, 게시글->회원을 참조하거나 어느 한쪽만 참조하는 경우를 생각할 수 있다. 위 예시 코드는 게시글이 회원을 참조하는 상황을 가정하였다.

연관관계 매핑을 위한 annotation은 다음과 같이 사용되었다.

  • @ManyToOne : 다대일 관계 매핑
  • @JoinColumn : 외래키 매핑 , name 속성에 외래키 이름을 지정한다. 여기서는 member_id값이 외래키가 된다.

양방향 연관관계

  • 두 entity간에 서로 참조하는 관계를 양방향 연관관계라고 한다. 위 예에서 Member -> board는 일대다 연관관계이다. 일대다 연관관계에서 ‘다’측의 객체는 Collection (List,Set,Map…) 으로 매핑될 수 있다. (정확히 말하면 단방향 연관관계 2개를 application에서 양방향인 것 처럼 보이게 할 뿐이다.)

  • 단방향 연관관계에 비해 갖는 장점은 반대 방향으로도 객체 그래프 탐색 기능이 추가되었다는 장점이 있다. 반면 단점으로는 연관관계를 매핑할때 객체에서 양쪽 방향을 모두 관리해야 된다는 단점이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
public class Member {

@Id
@Column(name = "member_id")
private String id;

private String username;

@OneToMany(mappedBy = "member")
private List<Board> boardList = new ArrayList<>();

}
  • @OneToMany : 일대다 관계
  • mappedBy 속성 : 양방향 매핑일떄 반대쪽 (Board) entity에서 자신이 매핑된 필드 이름 값

연관관계의 주인

양방향 연관관계에서는 주의할점이 있다. 두 연관관계중에 하나를 연관관계의 주인으로 정해야 한다는 것이다.
연관관계의 주인이라 함은 외래키를 가지고 있는 entity 를 말한다. 즉 외래키를 가지는 DB 와 매핑되는 Entity가 연관관계의 주인인 것이다.

연관관계의 주인이 아닌 entity가 mappedBy 속성을 사용해, 연관관계의 주인을 지정해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
public class Member {

//...
@OneToMany(mappedBy = "member")
private List<Board> boardList = new ArrayList<>();
}

@Entity
public class Board {

// 연관관계의 주인이다.
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;

}

정리하면 연관관계의 주인만이 DB에서 외래키를 가진 table과 매핑되고, 외래키를 관리한다 , 즉 저장하려면 연관관계의 주인을 통해서 이루어져야 한다. 반면 연관관계의 주인이 아닌 entity는 읽기만 가능하다.
따라서 연관관계(외래키)를 저장할떄에는 연관관계의 주인을 통해서만 저장할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
// 연관관계의 주인 board entity
transaction.begin();

Member member = new Member("id1", "user1");
em.persist(member);

Board boardA = new Board("title1","content1",member);
em.persist(boardA);

Board boardB = new Board("title2","content2",member);
em.persist(boardB);

다음과 같이 연관관계의 주인이 아닌 다대일에서 ‘일’ 쪽인 entity가 연관관계를 저장하면 예외는 터지지 않으나, DB에는 null값이 할당된다.

1
2
3
4
5
6
7
8
9
10
Board boardA = new Board("title1","content1");
em.persist(boardA);

Board boardB = new Board("title2","content2");
em.persist(boardB);

Member member = new Member("id1", "user1");
member.getBoardList().add(boardA); // null
member.getBoardList().add(boardB); // null
em.persist(member);

대개 연관관계는 다대일,일대다 등에서 ‘다’ 쪽이 연관관계의 주인이 된다. 그 이유는 DB 를 생각해도 당연하다. 따라서 @ManyToOne에는 애초에 mappedBy 속성이 없다.

1
2
3
4
5
6
7
8
9
@Target({METHOD, FIELD}) 
@Retention(RUNTIME)

public @interface ManyToOne {

Class targetEntity() default void.class;
CascadeType[] cascade() default {};
FetchType fetch() default EAGER;

반면 OneToMany는 연관관계의 주인을 지정하는 mappedBy속성이 있는 것을 확인할 수 있다.

1
2
3
4
5
6
public @interface OneToMany {

Class targetEntity() default void.class;
CascadeType[] cascade() default {};
FetchType fetch() default LAZY;
String mappedBy() default "";

양방향 연관관계의 주의점

  • 위 내용에 따르면, 연관관계의 주인이 외래키를 관리(저장)한다고 하였다. 따라서 객체 관점에서는 연관관계의 주인쪽에서만 연관관계의 주인이 아닌 entity 필드에 값을 넣어주고, 연관관계의 주인이 아닌 entity는 연관관계의 주인인 entity 필드에 값을 넣어줄 필요가 없을까?

  • JPA 기술 상으로는 문제가 없으나, 순수 객체의 관점에서는 불일치 상태가 된다.

예를 들면 아래와 같은 테스트 코드에서 실제 DB에는 정상적으로 연관관계가 저장되었을지는 몰라도, 두 entity간에는 다른 정보를 가지고 있다.

1
2
3
4
5
6
7
8
9
10
11
@Test
void testBiDirection(){
Member member = new Member("id1", "user1");
Board boardA = new Board("title1","content1");
Board boardB = new Board("title2","content2");
// 연관관계 설정
boardA.setMember(member);
boardB.setMember(member);

assertThat(member.getBoardList().size()).isEqualTo(2); // fail!!!
}

연관관계 편의 메소드

  • 위와 같이 객체간의 불일치한 상태를 일치시켜주기 위해서 두 entity중에 하나에 한번에 양방향 관계를 설정해주는 연관관계 편의 method를 정의한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Entity
    @Getter
    @NoArgsConstructor
    public class Member {
    //...
    //연관관계편의 메소드
    public void addBoard(Board board){
    this.boardList.add(board);
    board.setMember(this);
    }
    }
    테스트를 해보면 정상적으로 성공되는 것을 확인할 수 있다. 경우에 따라 기존의 연관관계를 삭제하고 연관관계를 추가해야하는 상황이 올수도 있으니, 이 또한 고려해야 한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Test
    void testBiDirection(){
    EntityManager em = emf.createEntityManager();
    EntityTransaction transaction = em.getTransaction();
    transaction.begin();

    Member member = new Member("id1", "user1");
    Board boardA = new Board("title1","content1");
    Board boardB = new Board("title2","content2");
    // 연관관계 설정
    member.addBoard(boardA);
    member.addBoard(boardB);
    assertThat(member.getBoardList().size()).isEqualTo(2);
    }

추가로 양방향 매핑시에 두 entity간에 toString이 서로를 순환참조할 수 있으므로, 대개는 entity에 toString method를 overriding 하진 않지만 (dto사용)
그래도 조심하자.

정리

  • 단방향 매핑을 먼저 시도하고, 비즈니스 로직에 따라 양방향을 사용하도록 변경하자, 그 이유는 양방향을 매핑하면 객체에서 양쪽 방향을 모두 고려해야 되는데, 이는 코드도 복잡해지고 , 굳이 반대 방향으로 객체 그래프 탐색을 하지 않는다면 필요없는 기능이다.

  • 양방향 매핑에서 외래키를 관리할 entity를 연관관계의 주인이라고 부르며 보통 다대일,일대다에서 ‘다’ 측의 entity이다.
    (‘일’측을 연관관계의 주인으로 설정할 수는 있지만, DB 외래키를 가진 table과 다른 entity가 외래키를 관리하게 된다.)

  • 양방향 매핑을 사용해야 한다면 , Entity 객체간에 연관관계를 맺어주는 연관관계 편의 method를 활용하자. 그래야 객체 관점에서도 일관성 있는 정보를 가지게 되며 테스트하기도 용이해진다.

Comments