숑숑이의 개발일기

연관관계 매핑이란?

객체의 참조와 테이블의 외래 키를 매핑하는 것을 의미한다. JPA에서는 연관 관계에 있는 상대 테이블의 PK를 멤버 변수로 갖지 않고, 엔티티 객체 자체를 통째로 참조한다.

 

방향

데이터 모델링에서는 관계를 맺어주기만 하면 자동으로 양방향 관계가 되어 서로 참조한다. 그러나 객체지향 모델링에서는 구현하고자 하는 서비스에 따라 단방향 관계인지 양방향 관계인지 적절한 선택이 필요하다.

  • 단방향 관계 : 두 엔티티가 관계를 맺을 때, 한 쪽의 엔티티만 참조하고 있는 것
  • 양방향 관계 : 두 엔티티가 관계를 맺을 떄, 양 쪽이 서로 참조하고 있는 것

 

다중성

어떤 엔티티를 중심으로 상대 엔티티를 바라보느냐에 따라 다중성이 달라진다. 관계에 있는 두 엔티티는 아래 중 하나의 관계를 갖는다. 다중성은 데이터베이스를 기준으로 결정한다.

  • OneToMany : 일대다 (1:N)
  • ManyToOne : 다대일 (N:1)
  • OneToOne : 일대일 (1:1)
  • ManyToMany : 다대다 (N:N)

 

연관관계의 주인

연관관계에는 주인(ownership)이라는 개념이 있다. 주인은 연관관계의 관리 주체이며, 이 연관관계를 관리할 책임을 갖고 있다. 주인엔티티는 DB에 저장되는 FK를 어떤 엔티티가 삽입, 수정, 삭제를 담당할 지 정하는 과정에서 자연스럽게 정해진다. 주인이 아닌 엔티티는 읽기만 할 수 있다.

 

양방향이든 단방향이든 @OneToMany 어노테이션을 달고 있는 엔티티가 부모 엔티티가 된다.

 

이제 가상의 시나리오를 세워보자. 하나의 게시판(Board)에는 여러 글을 작성할 수 있다. 하나의 글(Post)는 하나의 게시판(Board)에 작성할 수 있다.

 

@ManyToOne

단방향

엔티티의 관계를 표현하고 FK 관리에 있어 가장 자연스러워 많이 쓰이는 연관관계다. @JoinColumn 어노테이션이 함께 쓰인다. @JoinColumn은 엔티티 테이블에 FK 칼럼을 정의해준다.

@Entity
@Getter @Setter
public class Post {
    @Id @GeneratedValue
    private Long id;

    private String title;

    @ManyToOne
    @JoinColumn(name="board_id")
    private Board board;
}

@Entity
@Getter @Setter
public class Board {
    @Id @GeneratedValue
    private Long id;

    private String title;
}

 

양방향

양방향으로 만들기 위해선 일(1)쪽에 @OneToMany를 추가한다. 그리고 mappedBy로 연관관계의 주인을 지정해준다. 

@Entity
@Getter @Setter
public class Post {
    @Id @GeneratedValue
    private Long id;

    private String title;

    @ManyToOne
    @JoinColumn(name="board_id")
    private Board board;
}

@Entity
@Getter @Setter
public class Board {
    @Id @GeneratedValue
    private Long id;

    private String title;
    
    @OneToMany(mappedBy = "board")
    List<Post> posts = new ArrayList<>();
}

 

@OneToMany

연관관계의 주인을 일(1)쪽에 둔 것으로 실무에서는 거의 쓰지 않도록 한다. 데이터베이스 입장에서는 무조건 다(N)쪽에서 외래키를 관리하는데, 일(1)쪽에서 다(N)쪽 객체를 조작(생성, 수정, 삭제)를 할 수 있기 때문이다. 업데이트 쿼리 때문에 성능상 이슈는 크지 않지만 일(1)만 수정 시 다른 쿼리가 발생할 수 있어 혼동을 야기하므로 사용을 지양한다.

 

단방향

@Entity
@Getter @Setter
public class Post {
    @Id @GeneratedValue
    @Column(name = "POST_ID")
    private Long id;

    @Column(name = "TITLE")
    private String title;
}

@Entity
@Getter @Setter
public class Board {
    @Id @GeneratedValue
    private Long id;
    private String title;

    @OneToMany
    @JoinColumn(name = "POST_ID")
    List<Post> posts = new ArrayList<>();
}

양방향이 아니므로 mappedBy를 삭제한다. @JoinColumn을 이용해 조인한다.

 

양방향

공식적으로 존재하지않는다. 실무에서 사용을 금지한다. @JoinColumn(updatable = false, insertable = false)로 설정할 수 있지만, 일대다 양방향을 사용할 때는 다대일 양방향을 사용하는 것이 좋다.

 

Fetch Type

Fetch Type에는 EAGERLAZY가 존재한다. 

  • EAGER : 참조 객체들의 데이터까지 전부 읽어오는 방식 (한 번의 쿼리로 모든 정보를 가져옴)
  • LAZY : 참조 객체들의 데이터는 무시, 해당 엔티티의 데이터만 가져옴

EAGER는 언제나 한 번의 쿼리로 모든 정보를 가져온다. 반대로 LAZY의 경우 참조 객체의 데이터를 사용하기 위해 여러 번의 쿼리를 사용할 수 있다.

=> 참조 객체와 항상 함께 로드되어야 하는 조건을 가진 엔티티에 대해선 LAZY보단 EAGER 방식이 더 좋다.

 

그럼에도 불구하고 Fetch Type은 항상 Lazy를 써야하는 게 맞다. 어떠한 엔티티의 참조가 많아진다면 해당 엔티티를 가져올 때 알 수 없는 테이블들이 전부 join되어 등장할 것이다. 테이블 설계가 복잡해질수록 하나의 엔티티가 참조하는 테이블은 늘어날 것이고 그에따라 쿼리문도 복잡해질 것이다. 다르게 말해 논리적인 Layer 분리가 이뤄지지 않은 셈이 된다. 

 

https://velog.io/@bread_dd/JPA는-왜-지연-로딩을-사용할까
https://jeong-pro.tistory.com/231
profile

숑숑이의 개발일기

@숑숑-

풀스택 개발자 준비중입니다