1. 연관관계 매핑시 고려사항 3가지
1.1. 다중성 (Multiplicity)
객체 간의 관계를 수적으로 표현하는 개념이다. JPA는 4가지 다중성을 지원한다:
- 다대일 (N:1) - @ManyToOne
- 일대다 (1:N) - @OneToMany
- 일대일 (1:1) - @OneToOne
- 다대다 (N:M) - @ManyToMany (실무에서는 사용 제한적)
1.2. 단방향 vs 양방향
테이블의 관점
- 외래 키 하나로 양쪽 테이블 조인 가능
- 사실 방향이라는 개념이 존재하지 않음
- JOIN 쿼리로 어느 방향으로든 조회 가능
-- 양방향으로 모두 조회 가능
SELECT * FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.ID;
SELECT * FROM TEAM T JOIN MEMBER M ON T.ID = M.TEAM_ID;
객체의 관점
- 참조용 필드가 있는 쪽으로만 참조 가능
- 한쪽만 참조하면 단방향
- 양쪽이 서로 참조하면 양방향
- 객체의 양방향 관계는 사실 두 개의 단방향 관계
// 단방향: Member -> Team
class Member {
Team team;
}
// 양방향: Member <-> Team
class Member {
Team team;
}
class Team {
List<Member> members;
}
1.3. 연관관계의 주인 (Owner)
객체의 양방향 관계에서는 두 참조 중 하나를 연관관계의 주인으로 지정해야 한다:
- 연관관계의 주인: 외래 키를 관리하는 참조 (등록, 수정, 삭제 가능)
- 주인의 반대편: 읽기만 가능, 외래 키에 영향을 주지 않음
- 규칙: 외래 키가 있는 쪽이 연관관계의 주인이 됨
2. 다대일 [N:1]
2.1. 다대일 단방향
가장 흔하고 자연스러운 연관관계로, 데이터베이스의 외래 키 관계와 일치한다.

@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID") // 외래키 매핑
private Team team; // 다대일 관계
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
// Member에 대한 참조 없음 - 단방향
}
테이블 구조
CREATE TABLE MEMBER (
ID BIGINT PRIMARY KEY,
NAME VARCHAR(255),
TEAM_ID BIGINT, -- 외래키
FOREIGN KEY (TEAM_ID) REFERENCES TEAM(ID)
);
CREATE TABLE TEAM (
ID BIGINT PRIMARY KEY,
NAME VARCHAR(255)
);
사용 예시
// 저장
Team team = new Team("개발팀");
em.persist(team);
Member member = new Member("김개발");
member.setTeam(team); // 연관관계 설정
em.persist(member);
// 조회
Member findMember = em.find(Member.class, memberId);
Team findTeam = findMember.getTeam(); // 객체 그래프 탐색
2.2. 다대일 양방향
다대일 단방향에 반대 방향의 참조를 추가한 형태로, 실무에서 가장 많이 사용하는 패턴이다.

@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team; // 연관관계 주인
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team") // mappedBy로 주인 지정
private List<Member> members = new ArrayList<>();
}
연관관계 편의 메서드
@Entity
public class Member {
// ...
public void setTeam(Team team) {
// 기존 팀과 관계 제거
if (this.team != null) {
this.team.getMembers().remove(this);
}
// 새로운 관계 설정
this.team = team;
if (team != null) {
team.getMembers().add(this);
}
}
}
@Entity
public class Team {
// ...
public void addMember(Member member) {
members.add(member);
member.setTeam(this);
}
}
다대일 양방향 장점
- 객체 그래프 탐색이 양방향으로 가능
- JPQL 쿼리 작성이 편리해짐
- 객체의 협력 관계를 더 잘 표현 가능
3. 일대다 [1:N]
3.1. 일대다 단방향
일(1)이 연관관계의 주인이 되는 비직관적인 구조로, 실무에서 권장하지 않는 패턴이다.

@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID") // MEMBER 테이블의 TEAM_ID 컬럼 매핑
private List<Member> members = new ArrayList<>();
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
// Team에 대한 참조 없음
}
문제점
Team team = new Team("개발팀");
em.persist(team);
Member member1 = new Member("김개발");
Member member2 = new Member("이디자인");
// team에서 member 참조 설정
team.getMembers().add(member1);
team.getMembers().add(member2);
em.persist(member1);
em.persist(member2);
// 실행되는 SQL
// 1. MEMBER 테이블에 INSERT (TEAM_ID는 null)
// 2. TEAM_ID를 업데이트하는 UPDATE SQL 추가 실행
일대다 단방향의 단점
- 엔티티가 관리하는 외래키가 다른 테이블에 위치
- 연관관계 관리를 위해 추가 UPDATE SQL 실행
- 성능상의 이슈 발생 가능
- 예상치 못한 쿼리 발생 가능
3.2. 일대다 양방향
공식적으로 존재하지 않는 매핑 방식으로, 읽기 전용 필드를 사용해 흉내낸다.

@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team; // 읽기 전용
}
@JoinColumn 속성
- insertable = false: INSERT 시 이 필드 제외
- updatable = false: UPDATE 시 이 필드 제외
권장사항: 일대다보다는 다대일 양방향 매핑을 사용할 것
4. 일대일 [1:1]
4.1. 일대일 관계의 특징
- 일대일 관계는 그 반대도 일대일
- 주 테이블이나 대상 테이블 중 외래 키 선택 가능
- 외래 키에 UNIQUE 제약조건 추가 필요
4.2. 주 테이블에 외래 키
단방향

@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker; // 일대일 관계
}
@Entity
public class Locker {
@Id @GeneratedValue
private Long id;
private String name;
// Member에 대한 참조 없음
}
양방향

@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker; // 연관관계 주인
}
@Entity
public class Locker {
@Id @GeneratedValue
private Long id;
private String name;
@OneToOne(mappedBy = "locker")
private Member member; // 읽기 전용
}
4.3. 대상 테이블에 외래 키
단방향
지원이 안된다. (JPA 표준에서 지원하지 않음)

양방향

@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@OneToOne(mappedBy = "member")
private Locker locker; // 읽기 전용
}
@Entity
public class Locker {
@Id @GeneratedValue
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "MEMBER_ID")
private Member member; // 연관관계 주인
}
4.4. 일대일 관계 선택 기준
| 기준 | 주 테이블에 외래 키 | 대상 테이블에 외래 키 |
| 선호층 | 객체지향 개발자 | 데이터베이스 개발자 |
| 장점 | 주 테이블만 조회해도 대상 테이블 데이터 확인 가능 | 테이블 구조 변경 없이 일대다로 변경 가능 |
| 단점 | 외래키에 null 허용 필요 | 프록시 기능 한계로 항상 즉시 로딩됨 |
| JPA 매핑 | 편리함 | 제약사항 있음 |
실무 권장사항
- 주 테이블에 외래 키 방식을 기본으로 사용
- null 허용이 문제되면 대상 테이블에 외래 키 방식 고려
5. 다대다 [N:M]
5.1. 다대다 관계의 문제점
관계형 데이터베이스에서는 두 테이블로 직접적인 다대다 관계를 표현할 수 없다. 연결 테이블을 통해 일대다, 다대일 관계로 풀어야 한다.
테이블 구조

5.2. @ManyToMany 사용 (비권장)
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@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 @GeneratedValue
private Long id;
private String name;
@ManyToMany(mappedBy = "products")
private List<Member> members = new ArrayList<>();
}
@ManyToMany의 한계
- 연결 테이블에 추가 컬럼을 넣을 수 없음
- 실무에서는 연결 테이블에 주문수량, 주문일시 등 추가 데이터 필요
- 엔티티와 테이블이 불일치
- 예상치 못한 쿼리 발생 가능
5.3. 다대다 한계 극복: 연결 엔티티 사용 (권장)
연결 테이블을 엔티티로 승격시켜 일대다, 다대일 관계로 매핑한다.

@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();
}
@Entity
public class Product {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "product")
private List<MemberProduct> memberProducts = new ArrayList<>();
}
@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;
private int count; // 추가 필드
private LocalDateTime orderDate; // 추가 필드
}
복합키 대신 대리키 사용 (추천)
@Entity
public class Order {
@Id @GeneratedValue
private Long id; // 대리키 사용
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
private int orderAmount;
private LocalDateTime orderDate;
// 복합 유니크 제약조건
@Table(
uniqueConstraints = {
@UniqueConstraint(
columnNames = {"MEMBER_ID", "PRODUCT_ID"}
)
}
)
}
6. 실전 예제: 다양한 연관관계 매핑
6.1. 도메인 확장
기존 주문 시스템에 배송과 카테고리 기능을 추가한다:

요구사항
- 주문과 배송은 1:1 관계 (@OneToOne)
- 상품과 카테고리는 N:M 관계 (@ManyToMany → 연결 엔티티로 변경)
6.2. 엔티티 설계

// 주문 엔티티
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@OneToOne
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery; // 1:1 관계
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems = new ArrayList<>();
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
}
// 배송 엔티티
@Entity
public class Delivery {
@Id @GeneratedValue
private Long id;
@OneToOne(mappedBy = "delivery")
private Order order;
private String city;
private String street;
private String zipcode;
@Enumerated(EnumType.STRING)
private DeliveryStatus status;
}
// 상품 엔티티
@Entity
public class Product {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
private int stockQuantity;
// N:M 관계를 연결 엔티티로 구현
@OneToMany(mappedBy = "product")
private List<CategoryProduct> categoryProducts = new ArrayList<>();
}
// 카테고리 엔티티
@Entity
public class Category {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "category")
private List<CategoryProduct> categoryProducts = new ArrayList<>();
// 자기 참조 (계층 구조)
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Category parent;
@OneToMany(mappedBy = "parent")
private List<Category> child = new ArrayList<>();
}
// 연결 엔티티: 상품-카테고리
@Entity
public class CategoryProduct {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
@ManyToOne
@JoinColumn(name = "CATEGORY_ID")
private Category category;
private LocalDateTime addedDate;
}
6.3. @ManyToOne 주요 속성
@ManyToOne(
optional = false, // 필수 관계 (기본값: true)
fetch = FetchType.LAZY // 지연 로딩 (권장)
// cascade = CascadeType.ALL // 영속성 전이 (필요시 사용)
)
@JoinColumn(
name = "TEAM_ID",
nullable = false, // not null 제약조건
foreignKey = @ForeignKey(name = "FK_MEMBER_TEAM") // 외래키 이름
)
private Team team;
fetch 속성
- FetchType.LAZY: 지연 로딩 (성능 최적화)
- FetchType.EAGER: 즉시 로딩 (N+1 문제 주의)
cascade 속성
- CascadeType.ALL: 모든 작업 전파
- CascadeType.PERSIST: 저장만 전파
- CascadeType.REMOVE: 삭제만 전파
6.4. @OneToMany 주요 속성
@OneToMany(
mappedBy = "team",
fetch = FetchType.LAZY, // 컬렉션은 항상 LAZY 사용
cascade = CascadeType.ALL, // 필요에 따라 설정
orphanRemoval = true // 고아 객체 제거
)
private List<Member> members = new ArrayList<>();
orphanRemoval
- true: 컬렉션에서 제거된 엔티티 자동 삭제
- false: 컬렉션에서 제거만 하고 DB 삭제는 안함
7. 실무 적용 가이드라인
7.1. 연관관계 선택 기준
- 다대일 (N:1)
- 기본으로 사용
- 가장 자연스러운 관계
- 성능상 이점 있음
- 일대다 (1:N)
- 가능하면 사용하지 말 것
- 다대일 양방향으로 대체
- 불가피한 경우에만 사용
- 일대일 (1:1)
- 주 테이블에 외래키 방식 선호
- nullable 고려하여 선택
- 다대다 (N:M)
- 실무에서 사용하지 말 것
- 연결 엔티티로 풀어서 사용
7.2. fetch 전략
// 기본값 (권장)
@ManyToOne(fetch = FetchType.LAZY)
@OneToMany(fetch = FetchType.LAZY) // 컬렉션은 항상 LAZY
// 필요할 때만 EAGER 사용
@ManyToOne(fetch = FetchType.EAGER) // N+1 문제 주의
7.3. cascade 설정
// 자주 함께 저장/삭제되는 관계에서 사용
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems;
// 단독으로 사용되는 경우 cascade 사용하지 않음
@ManyToOne // cascade 설정 없음
private Member member;
7.4. 연관관계 편의 메서드
// 양방향 관계일 때 항상 제공
@Entity
public class Order {
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
public void removeOrderItem(OrderItem orderItem) {
orderItems.remove(orderItem);
orderItem.setOrder(null);
}
}
8. 정리
다양한 연관관계 매핑을 효과적으로 사용하기 위한 핵심 원칙:
- 단순함을 유지하라: 다대일 단방향으로 시작, 필요시 양방향으로 확장
- 외래키 위치를 따라라: 연관관계의 주인은 외래키가 있는 곳
- 지연 로딩을 기본으로: FetchType.LAZY를 기본값으로 사용
- 다대다를 피하라: 연결 엔티티를 사용하여 일대다, 다대일 관계로 풀어라
- 객체의 일관성을 유지하라: 편의 메서드로 양방향 관계 일관성 보장
연관관계 매핑은 JPA의 가장 강력한 기능이지만, 잘못 사용하면 복잡성과 성능 문제를 초래할 수 있다. 각 관계 유형의 특징과 제약사항을 이해하고, 프로젝트의 요구사항에 맞는 최적의 매핑 전략을 선택하는 것이 중요하다.
'Spring > JPA' 카테고리의 다른 글
| [Basic-7] 프록시와 연관관계 관리 (0) | 2026.01.06 |
|---|---|
| [Basic-6] 고급 관계 매핑 (0) | 2026.01.06 |
| [Basic-4] 연관관계 매핑 기초 (0) | 2026.01.06 |
| [Basic-3] 엔터티 매핑 (Entity Mapping) (0) | 2026.01.06 |
| [Basic-2] 영속성 컨텍스트 (Persistence Context) (0) | 2026.01.06 |
