본문 바로가기
JPA

6장. 다양한 연관관계 매핑

by ZIAHO 2025. 12. 4.

* 해당 포스팅은 김영한님의 '자바 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