JPA

JPA 연관관계 매핑

voider 2020. 11. 14. 16:25

다양한 연관관계 매핑

연관관계 매핑할 때 고려해야 할 세 가지가 있다.

  • 다중성

    먼저 연관이 있는 두 테이블의 관계를 파악해야 한다. 1:1관계인지 1:N관계인지, 다중성을 고려해야 한다.

  • 방향

    두 엔티티 중 한쪽만 참조하는 단방향인지, 양쪽 모두 참조하는 양방향인지 고려해야 한다.

  • 연관관계 주인

    단방향 관계라면 관계를 설정하는 쪽이 주인이 된다. 하지만 양방향이라면 관계의 주인을 정해야 한다. 보통 '다(N)'가 되는 쪽이 외래 키를 가지고 있기 때문에 연관관계의 주인이 된다.

다중성

연관관계가 가진 다중성

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

다중성을 판단하기 어려울 때는 반대방향을 생각하면 된다. 일대다의 반대는 항상 다대일이고, 일대일의 반대는 항상 일대일이다. 보통 다대일 - 일대다 관계를 가장 많이 쓴다. 다대다는 거의 사용하지 않고 중간 테이블을 만들어서 일대다 - 다대일로 바꾸어 쓴다.

방향

테이블은 외래 키 하나로 조인해서 양방향 쿼리가 가능하다. 그래서 데이터베이스에는 방향이라는 개념이 없다. 무조건 양방향이기 때문이다. 하지만 객체는 다르다. 객체는 참조하는 필드를 가지고 있어야만 그 필드를 조회할 수 있다.

User클래스에서 Reservation클래스를 조회하기 위해서는 해당 타입의 필드가 있어야 한다. 그래야 User.reservation.getStartTime() 이런 식으로 조회할 수 있다. Reservation클래스에서 User를 조회하기 위해서는 마찬가지로 User타입 필드를 가지고 있어야 한다.

연관관계 주인

데이터베이스는 외래 키 하나로 연관관계를 맺는다. 연관 관계를 관리하는 포인트가 외래 키다. 반면 엔티티를 양방향으로 매핑하면 User.reservationReservation.User 두 곳에서 연관관계를 관리하게 된다.

JPA는 연관관계 중 하나를 정해서 데이터베이스 외래 키를 관리한다. 이것을 연관관계 주인이라고 한다. User 혹은 Reservation 둘 중 하나가 외래 키를 관리하는 주인이 되어야 한다. 보통은 외래 키를 가진 테이블이 연관관계의 주인이 된다. 연관관계의 주인이 아닌 테이블은 방향 애노테이션 속성 mappedBy를 이용해서 주인이 무엇인지 명시해야 한다. 보통은 @ManyToOne을 가지는 쪽이 주인이 되기 때문에 @ManyToOne에는 mappedBy 속성이 없다.

다대일

다대일 관계의 반대는 항상 일대다 관계다. 반대도 마찬가지다. 데이터베이스 테이블의 일대다 관계에서 외래 키는 항상 '다(N)'쪽이 가진다. 따라서 양방향 관계에서 연관관계 주인은 항상 '다(N)'가 된다.

다대일(N:1) 단항뱡

이 다이어그램을 APPLY의 관점에서 보자면 N:1의 관계다.

약간 억지를 부려서 APPLY에서는 USER를 조회할 수 있지만 USER에서는 APPLY를 조회할 필요가 없다고 가정한다면 이렇게 가정할 수 있다.

@Entity
public class Apply {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "apply_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id") //user_id필드와 매핑
    private User user;
    ...
    ...
}
@Entity
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long id;
    private String phone;
    private String pw;
    private String email;
    private boolean enable;
    ...
    ...
}

User에서 Apply를 조회할 일이 없다면 User는 Apply를 몰라도 된다. Apply에서 설정한 @JoinColumn(name="user_id")는 Apply.user필드를 USER테이블의 USER_ID외래 키와 매핑한다. 이제 Apply.user필드로 USER_ID 외래 키를 관리한다.

다대일 양방향(N:1, 1:N)

위의 단방향 예제에서 User도 자신의 Apply내역을 조회할 수 있도록 변경해야 한다면 User와 Apply는 양방향 관계가 된다.

@Entity
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long id;
    ...

    @OneToMany(mappedBy = "user")
    private List<Apply> applies;

    ...
    ...
}

연관관계의 주인은 외래 키를 가진 쪽이다.

일대다 - 다대일 양방향 관계에서는 항상 '다(N)'에 외래 키가 있다. 여기서는 Apply - User의 관계가 N:1이므로 Apply가 연관관계의 주인이 된다. 따라서 연관관계 주인이 아닌 User는 mappedBy속성으로 연관관계의 주인이 누구인지 명시했다.

일대다 (1:N)

일대다 관계는 엔티티를 하나 이상 참조하므로 List나 Map같은 컬렉션을 사용한다.

사실 Apply에서는 User를 조회할 필요가 없다. 회원이 자신이 어떤 신청을 했는지 알 수 있으면 될 뿐, 그 반대는 없어도 상관없다. 따라서 User - Apply는 1:N으로 설계할 수 있다. 문제는 외래 키가 Apply에 있다는 것이다. 그래서 외래 키가 없는 테이블에서 외래 키를 관리한다. 그러니까 User엔티티에서 user_id를 관리한다.

단점

매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점. 외래 키가 원래 있어야 할 곳에 있으면 저장, 연관관계 처리를 한 번의 INSERT SQL으로 끝낼 수 있지만, 외래 키를 다른 엔티티에서 관리하면 UPDATE SQL을 추가로 실행해야 한다.

해결

일대다(1:N) 매핑보다는 양방향 매핑을 사용하자

반대쪽 테이블에서 @ManyToOne으로 양방향 연관관계를 맺어준다면 위와 같은 문제는 발생하지 않는다. 양방향 매핑을 한다고 해서 테이블의 모양이 변하는 것이 아니므로 엔티티만 약간 수정하면 된다. 특별한 경우가 아니라면 일대다를 사용해야 한다면 양방향 매핑을 권장한다.

일대일(1:1)

일대일 관계는 어느 쪽이든 외래 키를 가질 수 있다.

  1. 주 테이블에 외래 키

    주 객체가 대상 객체를 참조하는 것처럼, 주 테이블에 외래 키를 두고 대상 테이블을 참조한다. 외래 키를 객체 참조와 비슷하게 사용할 수 있다. 객체지향 개발자들이 선호한다.

  2. 대상 테이블에 외래 키

    데이터베이스 개발자는 보통 대상 테이블에 외래 키를 두는 것을 선호한다. 테이블 관계를 일대일에서 일대다로 변경해야 할 일이 생겼을 때 구조를 그대로 유지할 수 있기 때문이다.

다대다(N:N)

관계형 데이터베이스에 N:N관계는 없다. N:N관계를 구현하기 위해서는 중간에 테이블을 둬서 양방향을 풀어내야 한다. JPA에서는 @JoinTable을 이용해서 N:N 구현이 불가능한 것은 아니지만, 양방향의 외래 키를 제외한 다른 필드가 추가되면 사용할수 없게 된다는 단점이 있다. 중간 테이블을 두어서 1:N , N:1양방향으로 풀어내는 것을 권장한다.

 


참고

자바 ORM표준 JPA 프로그래밍 - 김영한