JPA (6) 연관 관계 매핑 - 2

김영한님의 자바 ORM 표준 JPA 프로그래밍 강의 정리

다대일 (N:1)

다대일 단방향

아래와 같이 두 개의 테이블이 있을 때, 여러 멤버가 팀에 속할 수 있다면 멤버 테이블에서 외래 키를 가지고 다대일의 관계를 가지게 된다.

Member Team
member_id (PK) team_id(PK)
team_id (FK) name

객체에서 멤버에서만 팀을 참조하고 팀에서는 멤버를 참조하지 않는다면 다대일 관계에서 단방향으로만 연관 관계가 만들어진다.

// Member
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}

// Team
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
}

다대일 단방향 관계는 가장 많이 사용되는 연관 관계이며 반대는 일대다 관계다.

다대일 양방향

양방향 관계는 멤버를 참조하지 않았던 팀에서 멤버를 참조할 수 있도록 추가해주면 된다.

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

일대다 (1:N)

일대다 단방향

일대다 관계에서는 1에 해당하는 쪽이 연관 관계의 주인이다. 테이블은 위와 같다고 가정했을 때 객체에서 연관 관계 매핑이 달라진다. @OneToMany 어노테이션으로 일대다를 나타내지만 1에 해당하는 쪽이 연관 관계의 주인 역할을 하기 때문에 @JoinColumn을 꼭 추가해줘야 한다. @JoinColumn을 사용하지 않을 경우에는 JoinTable 방식을 사용하게 돼서 테이블이 하나 더 추가된다. 일대다 단방향은 엔티티가 관리하는 외래 키가 DB에서는 다른 테이블에 있어 연관 관계를 관리하기 위해서 추가 UPDATE 쿼리를 실행하게 된다. 일대다 단방향 보다는 다대일 양방향 매핑을 사용하는 것이 더 좋다.

// Team
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    @JoinColumn(name = "team_id")
    private List<Member> members = new ArrayList<>();
}

// Member
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
}

일대다 양방향

일대다 양방향은 공식적으로 있지는 않지만 읽기 전용 필드를 이용해 양방향처럼 사용하는 방법이다. 반대편에도 @JoinColumn을 추가해주는데 대신 insertable, updateable 옵션을 모두 false로 주도록 한다. 추천하는 방법은 아니므로 다대일 양방향 매핑을 사용하는 것이 좋다.

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    @JoinColumn(name = "team_id")
    private List<Member> members = new ArrayList<>();
}

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "team_id", insertable=false, updatable=false)
    private Team team;
}

일대일 (1:1)

일대일 관계는 반대도 일대일이다. 두 테이블 중 한 곳에서 외래 키를 선택하면 되고 외래 키에는 UNIQUE 조건을 추가하면 된다. 일대일 매핑은 다대일 단방향 매핑과 매우 유사하다. 멤버와 라커라는 테이블이 있을 때, 메인이 되는 테이블이 멤버라고 할 때, 멤버에 외래 키를 두고 객체에서도 멤버 클래스가 연관 관계의 주인이 될 수 있다.

Member Locker
member_id (PK) locker_id (PK)
username name
locker_id (FK)  
// Member
@Entity
public class Member {
    
    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    
    private String username;
    
    @OneToOne
    @JoinColumn(name = "locker_id")
    private Locker locker;    
}

// Locker
@Entity
class Locker {
    @Id @GeneratedValue
    @Column(name = "locker_id")
    private Long id;
    
    private String name;
}

여기서 양방향으로 할 경우에는 라커 클래스에도 멤버 객체를 참조할 수 있도록 필드를 추가하고 mappedBy를 적용하면 된다.

@Entity
public class Locker {
    @Id @GeneratedValue
    @Column(name = "locker_id")
    private Long id;
    
    private String name;
    
    @OneToOne(mappedBy = "locker")    
    private Member member;
}

하지만, DB에서는 외래 키가 메인이 아닌 대상이 되는 라커에 존재하는데 객체에서는 멤버에서 외래 키를 관리하며 단방향으로 연관 관계를 설정하는 것은 불가능하다. 양방향으로 설정하는 것은 가능하다.

Member Locker
member_id (PK) locker_id (PK)
username name
  member_id(FK)
// Member
@Entity
public class Member {
    
    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    
    private String username;
    
    @OneToOne(mappedBy = "member")
    private Locker locker;    
}

// Locker
@Entity
public class Locker {
    @Id @GeneratedValue
    @Column(name = "locker_id")
    private Long id;
    
    private String name;
    
    @OneToOne
    @JoinColumn(name = "member_id")
    private Member member;
}

결국 일대일 관계는 외래 키를 메인 테이블에 두는지 대상 테이블에 두는지 두 가지 방법이 있다. 메인 테이블에 두게 되면 JPA 매핑이 편리하고 메인 테이블만 조회해도 대상 테이블의 데이터를 확인할 수 있다. 하지만 값이 없을 경우 외래 키에 null을 허용하게 된다. 대상 테이블에 외래 키를 둘 경우에는 일대일에서 일대다의 관계로 변경할 때도 테이블 구조를 유지할 수 있는 게 장점이지만 지연 로딩으로 설정해도 항상 즉시 로딩이 된다.

다대다 (N:M)

RDB에서는 정규화된 테이블 2개로는 다대다 관계를 표현할 수 없어서 두 테이블을 연결하는 테이블을 추가해야한다. 그리고 그 테이블과 일대다, 다대일 관계로 연결하게 된다. 객체에서는 컬렉션을 이용해서 두 개의 객체만으로도 다대다 관계를 표현할 수 있다. JPA에서는 @ManyToMany를 사용하고 @JoinTable로 연결 테이블을 지정한다. 단방향, 양방향 모두 지정할 수 있으며 양방향일 때는 mappedBy를 설정해준다.

@Entity 
public class Member {
    // 다른 필드
    
    @ManyToMany
    @JoinTable(name = "member_product")
    private List<Product> products = new ArrayList<>();
}

@Entity
public class Product {
    // 다른 필드
    
    @ManyToMany(mappedBy = "products")
    private List<Member> members = new ArrayList<>();
}

다대다 연결은 실무에서는 사용하지 않도록 해야한다. 연결 테이블이 단순하게 연결만 하는 것이 아니라 실무에서는 다른 상황도 생길 수 있고 중간에 테이블이 하나 더 있기 때문에 쿼리의 문제도 있다.

다대다의 문제를 해결하기 위해서는 연결 테이블을 엔티티로 사용해서 다대다를 일대다, 다대일 연결 관계로 풀면 된다. MemberProduct 엔티티가 추가되면서 이후에 추가로 필요한 필드가 생기면 추가할때도 더 쉽다.

@Entity
public class MemberProduct {
    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;
    
    @ManyToOne
    @JoinColumn(name = "product_id")
    private Product product;    
}

@Entity 
public class Member {
    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    
    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts = new ArrayList<>();
}

@Entity
public class Product {
    @Id @GeneratedValue
    @Column(name = "product_id")
    private Long id;
    
    @OneToMany(mappedBy = "product")
    private List<MemberProduct> memberProducts = new ArrayList<>();
}