* 해당 포스팅은 김영한님의 '자바 ORM 표준 JPA 프로그래밍' 교재를 학습한 후 내용을 정리한 글입니다.
📁 고려사항
엔티티의 연관관계를 매핑할 때는 다음 3가지를 고려해야 한다.
1. 다중성
- 다대일(N:1): @ManyToOne
- 일대다(1:N): @OneToMany
- 일대일(1:1): @OneToOne
- 다대다(N:M): @ManyToMany
2. 단방향, 양방향
- 테이블: 외래 키 하나로 양쪽 조인 가능 (방향 개념 없음)
- 객체: 참조용 필드가 있는 쪽으로만 참조 가능
- 한쪽만 참조: 단방향
- 양쪽이 서로 참조: 양방향
3. 연관관계의 주인
- 양방향 관계에서 연관관계의 주인을 정해야 함
- 외래 키를 관리하는 참조
- 주인이 아닌 쪽은 읽기만 가능
- 주인이 아닌 쪽에 mappedBy 속성으로 주인 지정
📁 다대일 (N:1)
데이터베이스 테이블의 일(1), 다(N) 관계에서 외래 키는 항상 다(N)쪽에 있다.
따라서 객체 양방향 관계에서 연관관계의 주인은 항상 다(N) 쪽이다.
가장 많이 사용하는 연관관계이다.
1. 다대일 단방향[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; // 참조
// getter, setter...
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
// getter, setter...
}
1.1 테이블 구조:
MEMBER 테이블 TEAM 테이블
┌───────────┬────────┐ ┌────────┬──────┐
│ MEMBER_ID │ NAME │ │ TEAM_ID│ NAME │
├───────────┼────────┤ ├────────┼──────┤
│ TEAM_ID(FK)│ │ │ │ │
└───────────┴────────┘ └────────┴──────┘
2. 다대일 양방향[N:1, 1:N]
@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);
}
}
}
2.1 특징
- 양방향은 외래 키가 있는 쪽이 연관관계의 주인 (Member.team이 주인)
- Team.members는 조회를 위한 JPQL이나 객체 그래프 탐색 시 사용
2.2 주의점
- 양쪽에 참조를 다 해주는 것이 안전
- 연관관계 편의 메소드는 한곳에만 작성
📁 일대다 (1:N)
일대다 관계는 엔티티 하나 이상 참조할 수 있으므로 자바 컬렉션을 사용해야 한다.
1. 일대다 단방향[1:N]
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID") // MEMBER 테이블의 TEAM_ID (FK)
private List<Member> members = new ArrayList<>();
// getter, setter...
}
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
// Team에 대한 참조가 없음
}
1.1 테이블 구조:
TEAM 테이블 MEMBER 테이블
┌────────┬──────┐ ┌───────────┬────────┐
│ TEAM_ID│ NAME │ │ MEMBER_ID │ NAME │
├────────┼──────┤ ├───────────┼────────┤
│ │ │ │ TEAM_ID(FK)│ │
└────────┴──────┘ └───────────┴────────┘
1.2 특징
- 본인 테이블이 아닌 대상 테이블의 외래 키를 관리
- 연관관계 관리를 위한 UPDATE SQL이 추가로 실행되어 성능 문제와 관리 부담
- 결론: 일대다 단방향보다 다대일 양방향을 권장
2. 일대다 양방향[1:N, N:1]
- 다대일 양방향을 사용하자.
📁 일대일 (1:1)
일대일 관계는 양쪽이 서로 하나의 관계만 가진다.
1. 특징
- 일대일 관계는 그 반대도 일대일
- 주 테이블이나 대상 테이블 둘 중 어느 곳이나 외래 키를 가질 수 있음
2. 주 테이블에 외래키
2.1 단방향
@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;
}
2.1.1 테이블 구조:
MEMBER 테이블 LOCKER 테이블
┌───────────┬────────────┐ ┌──────────┬──────┐
│ MEMBER_ID │ USERNAME │ │ LOCKER_ID│ NAME │
├───────────┼────────────┤ ├──────────┼──────┤
│ LOCKER_ID(FK)│ UNIQUE │ │ │ │
└───────────┴────────────┘ └──────────┴──────┘
2.1.2 특징
- 다대일 단방향과 거의 비슷
- 외래 키에 UNIQUE 제약조건 추가
2.2 양방향
@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;
@OneToOne(mappedBy = "locker")
private Member member;
}
2.2.1 특징
- 양방향이므로 연관관계의 주인을 정해야 함
- MEMBER 테이블이 외래 키를 가지고 있으므로 Member.locker가 연관관계의 주인
3. 대상 테이블에 외래 키
3.1 단방향
- JPA에서 지원하지 않음
3.2 양방향
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne(mappedBy = "member")
private 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;
}
3.2.1 테이블 구조
MEMBER 테이블 LOCKER 테이블
┌───────────┬────────┐ ┌──────────┬─────────────┐
│ MEMBER_ID │ NAME │ │ LOCKER_ID│ NAME │
├───────────┼────────┤ ├──────────┼─────────────┤
│ │ │ │ MEMBER_ID(FK)│ UNIQUE │
└───────────┴────────┘ └──────────┴─────────────┘
3.2.2 특징
- 프록시를 사용할 때 외래 키를 직접 관리하지 않는 일대일 관계는 지연 로딩으로 설정해도 즉시 로딩됨
📁 다대다 (N:M)
관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
=> 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.
회원 테이블 회원_상품 테이블 상품 테이블
┌──────┐ ┌──────┬──────┐ ┌──────┐
│회원ID│ │회원ID │상품ID│ │상품ID│
└──────┘ └──────┴──────┘ └──────┘
1:N N:1
* 객체는 2개로 다대다 관계가 가능하다:
class Member {
List<Product> products;
}
class Product {
List<Member> members;
}
1. 다대다 단방향
@Entity
public class Member {
@Id @Column(name = "MEMBER_ID")
private String 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;
}
1.1 @JoinTable
- name: 연결 테이블 지정
- joinColumns: 현재 방향인 회원과 매핑할 조인 컬럼 정보 지정
- inverseJoinColumns: 반대 방향인 상품과 매핑할 조인 컬럼 정보 지정
1.2 생성되는 SQL:
CREATE TABLE MEMBER_PRODUCT (
MEMBER_ID VARCHAR(255) NOT NULL,
PRODUCT_ID VARCHAR(255) NOT NULL,
PRIMARY KEY (MEMBER_ID, PRODUCT_ID)
)
1.3 저장:
Product productA = new Product();
productA.setId("productA");
productA.setName("상품A");
em.persist(productA); // INSERT INTO PRODUCT ...
Member member1 = new Member();
member1.setId("member1");
member1.setUsername("회원1");
member1.getProducts().add(productA); // 연관관계 설정 => INSERT INTO MEMBER_PRODUCT ...
em.persist(member1); // INSERT INTO MEMBER ...
1.4 조회:
Member member = em.find(Member.class, "member1");
List<Product> products = member.getProducts();
for (Product product : products) {
System.out.println("product.name = " + product.getName());
}
2. 다대다 매핑의 한계와 극복: 연결 엔티티
2.1 @ManyToMany의 한계:
- 연결 테이블에 추가 컬럼을 넣을 수 없음 ex) 주문 수량, 주문 날짜 등 컬럼 추가 불가
2.2 해결방법: 연결 엔티티 사용
@Entity
public class Member {
@Id @Column(name = "MEMBER_ID")
private String id;
private String username;
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts;
}
@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; // MemberProductId.member와 연결
@Id
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product; // MemberProductId.product와 연결
private int orderAmount; // 추가 컬럼
}
public class MemberProductId implements Serializable {
private String member; // MemberProduct.member와 연결
private String product; // MemberProduct.product와 연결
// equals, hashCode 구현 필수
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
}
2.3 특징:
- 복합 기본 키 사용 (@IdClass)
- 식별자 클래스의 특징:
- Serializable 구현
- equals, hashCode 구현
- 기본 생성자 필수
- public 클래스
- 복합키 사용은 매우 복잡
3. 다대다: 새로운 기본 키 사용 (추천)
연결 테이블에 새로운 기본 키를 사용하는 것.
데이터베이스에서 자동으로 생성해주는 대리 키를 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 int orderAmount;
}
3.1 특징:
- 복합 키 대신 새로운 기본 키(대리 키) 사용
- 복합 키를 사용하지 않으므로 코드가 단순해짐
- 영구히 변하지 않는 키 보장
- ORM 매핑 시 복잡도 감소
📁 권장사항
- 다대일 양방향을 기본으로 사용
- 다대다 사용 지양 → 일다대, 다대일로 풀어내기
- 연관관계의 주인은 외래 키의 위치와 관련하여 설정 (비즈니스 로직으로 연관관계의 주인 선택 X)
'JPA' 카테고리의 다른 글
| 8장. 프록시와 연관관계 정리 (0) | 2025.12.09 |
|---|---|
| 7장. 고급 매핑 (0) | 2025.12.07 |
| 5장. 연관관계 매핑 기초 (0) | 2025.12.02 |
| 4-2장. 필드, 컬럼 매핑 (0) | 2025.11.28 |
| 4-1장. 엔티티 매핑 (0) | 2025.11.28 |