김영한 님의 자바 ORM 표준 JPA 프로그래밍을 읽고 정리한 내용입니다.
😀 다양한 연관관계 매핑
- 엔티티 연관관계 매핑 시 고려할 3가지
- 다중성
- 단방향/양방향
- 연관관계의 주인
1. 다중성
연관관계는 보통 4가지 다중성을 갖는다.
- 다대일
- 일대다
- 일대일
- 다대다
다대일, 일대다 관계를 많이 사용하며 다대다 관계는 거의 사용되지 않는다.
2. 단방향/양방향
테이블과는 달리 객체는 참조용 필드를 가지며, 그 필드를 통해 연관 객체를 조회한다.
한 쪽이 다른 한 쪽을 일방적으로 참조하면 단방향이며 서로 참조하면 양방향이다.
3. 연관관계의 주인
테이블은 외래키 하나로 두 테이블이 연관 관계를 가질 수 있기 때문에 연관관계를 관리하는 포인트는 외래키 하나이다.
엔티티가 양방향으로 참조되면 A → B, B → A 둘 중 하나를 정하여 외래키를 관리해야 한다.
보통은 외래키를 가진 쪽을 연관관계의 주인으로 선택한다.
주인이 아닌 방향은 읽기만 가능하다.
연관관계의 주인은 mappedBy
속성을 사용할 수 없으며, 주인이 아닌 쪽에 mappedBy
로 주인 필드(외래키)를 지정한다.
🎨 다대일
다대일과 일대다는 항상 반대 방향에 존재한다.
외래키는 항상 다쪽에 있기 때문에 연관관계의 주인은 항상 다쪽이다.
회원(N)과 팀(1)이 있으면 회원이 연관관계의 주인이다.
🔗 다대일 단방향
회원 엔티티
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; @ManyToOne @JoinColumn(name = "TEAM_ID") private Team team; ... }
팀 엔티티
@Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; ... }
회원은 Member.team
으로 참조가 가능하지만, 팀에선 회원을 참조할 필드가 없어 단방향이다.
테이블로 구성한다면 외래키는 Member
에만 존재한다.
🔗 다대일 양방향
회원 엔티티
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; @ManyToOne @JoinColumn(name = "TEAM_ID") private Team team; public void setTeam(Team team) { this.team = team; // 무한루프 방지 if (!team.getMembers().contains(this)) { team.getMembers().add(this); } } }
팀 엔티티
@Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<>(); public void addMember(Member member) { this.members.add(member); // 무한루프 방지 if (member.getTeam() != this) { member.setTeam(this); } } }
- 팀 엔티티에 List가 추가되었고, 주인 필드인
Member.team
을 가르키고 있다.Team.members
는 주인이 아니므로 조회가 필요할 때 사용한다.
- 양방향 연관관계는 항상 서로를 참조해야 함에 주의한다.
- setTeam, addMember는 서로를 참조할 때 무한루프에 빠지지 않도록 처리되어 있다.
📎 일대다
🔍 일대다 단방향
하나의 팀이 여러 팀원을 참조할 수 있는 경우가 일대다 단방향 관계이다.
팀 엔티티
@Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany @JoinColumn(name = "TEAM_ID") private List<Member> members = new ArrayList<>(); ... }
회원 엔티티
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; ... }
팀 엔티티의 @JoinColumn
을 보면 일대다인 경우에도 N인 쪽에 외래키가 존재함을 알 수 있다.
엔티티가 관리하는 테이블과 다른 테이블에 외래키가 있기 때문에 insert 시 문제가 발생한다.
1) member 1, 2 생성
2) team 1 생성 및 member 1, 2의 소속을 team 1로 옮김.
3) member 1, 2, team 1 저장
- member 1, 2의 저장엔 문제가 없다.
- team 1 저장 시 team 1을 참조하는 member의 정보가 team 엔티티 내부에 없다.
- 따라서, JPA는 update Member set TEAM_ID = ? where MEMBER_ID를 한번 더 실행할 수밖에 없다.
이러한 문제는 쿼리 성능에 문제를 끼치며 관리도 어려워진다.
일대다 대신 다대일을 사용하는 것을 권장한다.
🔍 일대다 양방향
다대일 양방향과 같은 말이지만 JPA에선 약간의 차이가 있다.
일단. 외래키는 무조건 다쪽에 존재해야 한다는 점엔 변화가 없다.
팀 엔티티
@Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany @JoinColumn(name = "TEAM_ID") private List<Member> members = new ArrayList<>(); ... }
회원 엔티티
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; @ManyToOne @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) private Team team; ... }
다대일은 읽기 전용 필드 Team
이 추가되어 있다(insertable, updatable).
일대다 단방향 매핑이 갖는 성능, 관리 측면 문제가 그대로 존재하므로 웬만하면 다대일 단방향을 사용하는 방법이 권장된다.
👭 일대일
- 일대일 관계는 반대도 일대일이다.
- 일대일은 어느 곳이든 외래 키를 가질 수 있다.
- 외래키 하나만 있으면 양쪽 조회가 가능하다.
외래키 소유 전략은 2가지이다.
- 주 테이블이 외래키 소유
- 외래키를 객체 참조처럼 쓸 수 있어서 객체 지향 개발에 편리하다.
- 주 테이블이 외래키를 가지므로 주 테이블만 확인해도 대상 테이블과 연관 관계를 확인할 수 있다.
- 대상 테이블이 외래키 소유
- 일반적인 DB 개발자들은 이 방법을 선호한다.
- 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조가 그대로 유지된다.
회원이 하나의 라커룸을 가지는 예시로 일대일 관계를 알아본다.
주 테이블은 회원, 대상 테이블은 라커룸이다.
🤼♂️ 주 테이블에 외래키
회원 엔티티
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; @OneToOne @JoinColumn(name = "LOCKER_ID") private Locker locker; ... }
라커룸 엔티티
@Entity public class Locker { @Id @GeneratedValue @Column(name = "LOCKER_ID") private Long id; private String name; ... }
주 테이블인 회원에 Locker
필드(외래키)가 포함되어 있다.
👯♂️ 양방향
라커룸 엔티티를 약간만 바꾸어본다.
@Entity
public class Locker {
@Id
@GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "member")
private Member member;
...
}
주인 필드인 회원 엔티티가 외래키를 가지므로 주인 필드를 나타내는 mappedBy
속성과 @OneToOne
어노테이션을 추가했다.
🤼♀️ 대상 테이블에 외래키
JPA 2.0 이상
JPA 2.0부터 대상 테이블에 외래키가 있는 매핑이 지원된다.
위 예시와 반대로 Locker
에 Member
필드를 추가하면 된다.
👯♀️ 양방향
위 예시와 반대로 Member.locker
가 주인 필드인 Locker.member
를 가리키면 된다.
👨👩👧👧 다대다
RDBMS는 정규화된 2개의 테이블로 다대다 관계를 표현할 수 없으며 두 테이블을 연결하는 별도의 테이블이 필요하다.
회원과 상품이 다대다의 관계라고 하면 먼저 회원_상품 테이블을 생성한다.
- 회원 : 회원_상품 = 1:N
- 회원_상품 : 상품 = M:1
객체는 테이블과는 다르게 @ManyToMany
를 사용하여 다대다 관계를 깔끔하게 만들 수 있다.
👨👧👧 다대다 단방향
회원 엔티티
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; @ManyToMany @JoinTable(name = "MEMBER_PRODUCT", joinColumns = @JoinColumn(name = "MEMBER_ID"), inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID")) private List<Product> products = new ArrayList<>(); ... }
상품 엔티티
@Entity public class Product { @Id @Column(name = "PRODUCT_ID") private String id; private String name; ... }
회원 엔티티에는 @ManyToMany
로 다대다 매핑 처리가 되어있다.
@JoinTable
은 연결 테이블을 매핑하는 어노테이션이다.
속성을 정리해보자.
- name
- 연결할 별도의 테이블 이름을 지정한다.
- joinColumns
- 현재 방향인 회원 엔티티를 기준으로, 매핑할 조인 컬럼 정보를 지정한다.
- inverseJoinColumns
- 반대 방향인 상품 엔티티를 기준으로, 매핑할 조인 컬럼 정보를 지정한다.
회원_상품 테이블은 @JoinTable
의 name 속성에서만 존재할 뿐, 접근할 객체는 존재하지 않는다.
Product product1 = new Product();
product1.setId("product1");
product1.setName("상품1");
em.persist(product1);
Member member1 = new Member();
member1.setId("member1");
member1.setUsername("회원1");
member1.getProducts().add(product1);
em.persist(member1);
실제로 코드를 작성할 때도 회원_상품 테이블을 전혀 신경쓰지 않고 처리할 수 있다.
member.getProducts()
를 호출할 때 JPA가 해석하여 실제 테이블에 사용할 쿼리를 확인하면 MEMBER_PRODUCT이 FROM 절에 포함된다.
👨👩👧 다대다 양방향
다른 양방향과 마찬가지로 역방향도 @ManyToMany
를 지정하고, mappedBy
로 주인 필드를 지정한다.
상품 엔티티에 역방향 참조 추가
@Entity
public class Product {@Id @Column(name = "PRODUCT_ID") private String id; private String name; @ManyToMany(mappedBy = "products") private List<Member> members; ...
}
🗝다대다 매핑 한계와 극복
실제 업무를 하다 보면 회원_상품 테이블은 단순히 연결을 위해서 양쪽 외래키만 포함하는 테이블로 만들지는 않는다.
이를테면 상품 주문 시간이나 주문량 같은 정보가 포함될 수 있다.
그렇게 되면 위에서 @ManyToMany
를 사용한 예제는 사용할 수 없기 때문에 별도의 엔티티를 만들어야 한다.
추가적으로 회원과 회원_상품, 회원_상품과 상품의 관계를 별도로 정의한다.
회원 엔티티
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; @OneToMany(mappedBy = "member") private List<MemberProduct> emberProducts = new ArrayList<>(); ... }
상품 엔티티
@Entity public class Product { @Id @Column(name = "PRODUCT_ID") private String id; private String name; ... }
회원_상품 엔티티
@Entity @IdClass(MemberProductId.class) public class MemberProduct { @Id @ManyToOne @JoinColumn(name = "MEMBER_ID") private Member member; @Id @ManyToOne @JoinColumn(name = "PRODUCT_ID") private Product product; private Date orderAmount; ... }
MemberProductId
정의@NoArgsConstructor @EqualsAndHashCode public class MemberProductId implements Serializable { private String member; private String product; }
회원_상품 엔티티에는 @IdClass
어노테이션이 포함된다.
@IdClass
는 복합키를 사용하겠다는 의미이다.
MemberProductId
의 각 필드는 MemberProduct
엔티티의 Member
필드와 Product
필드에 매핑된다.
복합키
복합키는 별도의 클래스를 정의해야 하며(
MemberProductId
), 이 클래스는 public으로 선언되어야 한다.Serializable
을 구현해야 한다.Equals, Hashcode를 재정의해야 한다.
기본 생성자가 있어야 한다.
@EmbeddedId
를 사용하여 정의하는 방법도 있다.회원_상품의 예시처럼 외래키를 자신의 기본키로 사용하는 것을 식별관계라고 한다.
// 데이터 저장하기
Member m1 = new Member();
// m1 데이터 설정
em.persist(member1);Product p1 = new Product();
// p1 데이터 설정
em.persist(p1);MemberProduct mp = new MemberProduct();
mp.setMember(m1);
mp.setProduct(p1);
...em.persist(mp);
MemberProductId mpId = new MemberProductId();
// mpId 데이터 설정MemberProduct mp = em.find(MemberProduct.class, mpId);
Member m1 = mp.getMember();
Product p1 = mp.getProduct();
...
💡다대다 테이블의 기본 키를 새로 정의하기
복합 키를 사용하면 식별자 클래스도 새로 정의해야 하는 등 굉장히 번거롭다.
반면 DB에서 자동 생성하는 Long 타입 대리키를 사용하면 간편하게 사용할 수 있다.
회원_상품 테이블을 주문이라고 다시 명명하고 새로 작성한다.
주문 엔티티
@Entity public class Order { @Id @GeneratedValue @Column(name = "ORDER_ID") private Long id; @ManyToOne @JoinColumn(name = "MEMBER_ID") private Member member; @ManyToOne @JoinColumn(name = "PRODUCT_ID") private Product product; private Date orderAmount; ... }
MemberProduct
를 재정의한 Order
는 식별 관계가 사라졌다.
다대다 테이블은 복합키를 사용한 식별 관계, 대리키를 사용한 비식별 관계 중 적절한 방법을 선택하여 구현할 수 있다.
'Java > JPA' 카테고리의 다른 글
JPA 사용 시 주의할 점 (1) | 2021.07.22 |
---|---|
[JPA] 연관관계 매핑 기초 (1) | 2020.02.26 |
[JPA] 엔티티 매핑 (0) | 2020.02.22 |
[JPA] EntityManager, 영속성 컨텍스트 (0) | 2020.02.17 |