[6] 다양한 연관관계 매핑

ttt-1-2·2026년 4월 11일

교재: 자바 ORM 표준 JPA 프로그래밍 

6장은 다양한 연관관계를 다룬다 (다대일, 일대다, 일대일, 다대다).


1. 다대일

  • 다대일 관계의 반대 방향은 항상 일대다 관계고 일대다 관계의 방향은 항상 다대일 관계다.
    • 데이터베이스 테이블: 일(1), 다(N) 관계에서 외래 키는 항상 다쪽에 있다.
    • 객체 양방향 관계: 연관관계의 주인은 항상 다쪽이다.

ex: 회원(N), 팀(1)이 있으면 회원 쪽이 연관관계의 주인이다.

1.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;
	
	// ...
}
// 팀 엔티티

@Entity 
public class Team {

	@Id @GeneratedValue
	@Column(name="TEAM_ID")
	private Long id;
	
	private String name;
	
	// ...
}

→ 회원은 Member.team으로 팀 엔티티를 참조 할 수 있다. 팀에는 회원을 참조하는 필드가 없다. 회원과 팀은 다대일 단방향 연관관계다.

1.2 다대일 양방향 [N:1, 1:N]

// 회원 엔티티

@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
// 팀 엔티티

@OneToMany(mappedBy="team")
private List<Member> members = new ArrayList<Member>();
  • 양방향은 키가 있는 쪽이 연관관계의 주인이다.
    • Member.team : 외래 키(TEAM_ID)를 관리하는 연관관계의 주인이다.
    • Team.members : mappedBy로 매핑된 주인이 아닌 쪽이다. 조회와 객체 그래프 탐색에만 사용한다.
  • 양방향 연관관계는 항상 서로를 참조해야 한다.

2. 일대다

일대다 관계는 다대일 관계의 반대 반향이다. 일대다 관계는 엔티티를 하나 이상 참조할 수 있어서 자바 컬렉션인 Collection, List, Set, Map 중에 하나를 사용해야 한다.

2.1 일대다 단방향 [1:N]

하나의 팀은 여러 회원을 참조할 수 있는데 이런 관계를 일대다 관계라 한다. 팀은 회원들을 참조하지만 회원은 팀을 참조하지 않으면 둘의 관계는 일대다 단방향 관계다.

// 팀 엔티티

@OneToMany
// 일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 한다.
@JoinColumn(name="TEAM_ID") //  MEMBER 테이블의 TEAM_ID (FK)
private List<Member> members = new ArrayList<Member>()'
// 회원
@Id @GeneratedValue
@Column(name="MEMBER_ID")
private Long id;

private String username;
  • 일대다 단방향 매핑의 단점: 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다. Team에서 members를 추가하면 Member 테이블의 team_id를 변경해야 해서 INSERT 이후에 추가 UPDATE 쿼리가 발생한다 → 비효율적

일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자

2.2 일대다 양방향 [1:N, N:1]

일대다 양방향 매핑은 일반적으로 존재하지 않는다. 양방향 매핑에서 연관관계의 주인은 항상 외래 키를 가진 쪽이고, @OneToMany는 주인이 될 수 없고 @ManyToOne에는 mappedBy 속성이 없다.

다만 아래와 같이 일대다를 주인처럼 사용하는 방식으로 “형식적인 양방향” 구현은 가능하다.

// 팀 엔티티

@OneToMany
@JoinColumn(name="TEAM_ID")
private List<Member> members = new ArrayList<Member>()'
// 회원 엔티티

@ManyToOne
@JoinColumn(name="TEAM_ID", insertable=false, updatable=false)
private Team team;
  • 단점:
    • Member 쪽은 읽기 전용이 되고, 연관관계 관리가 Team에만 집중되어 구조가 비정상적이다.
    • 불필요한 UPDATE 쿼리가 발생해 성능상 비효율이 생긴다.

될 수 있으면 다대일 양방향 매핑을 사용하자!

3. 일대일 [1:1]

일대일 관계는 양쪽이 서로 하나의 관계만 가지는 구조다. (ex: 회원은 하나의 사물함만 사용하고, 사물함도 하나의 회원에 의해서만 사용된다.)

  • 일대일 관계는 그 반대 방향에서도 동일하게 일대일 관계가 된다.
  • 외래 키는 주 테이블이나 대상 테이블 둘 중 어느 곳에나 둘 수 있다.
    • 주 테이블 외래키: 주 테이블에 외래 키를 두고 대상 테이블을 참조한다. 일반적으로 객체지향 설계와 유사해 관리가 편리하다.
    • 대상 테이블 외래키: 대상 테이블에 외래 키를 두는 방식으로, 데이터베이스 설계 중심으로 볼 때 선택할 수 있다.

3.1 주 테이블에 외래 키

객체지향 개발자들은 주 테이블에 외래 키가 있는 것을 선호한다. JPA도 주 테이블에 외래 키가 있으면 더 직관적이고 편리하게 매핑할 수 있다.

단방향

// Member
@OneToOne
@JoinColumn(name="LOCKER_ID")
private Locker locker;

// Locker
@Id @GeneratedValue
@Column(name="LOCKER_ID")
private Long id;

객체 매핑에는 @OneToOne을 사용하고, @JoinColumn으로 외래 키를 지정한다. 이 경우 Member 테이블에 LOCKER_ID가 생성되어 Locker를 참조한다.

이 구조는 다대일 단방향과 거의 동일하게 동작하고 관계의 최대 개수가 1:1이라는 점만 다르다.

양방향

// Member
@OneToOne
@JoinColumn(name="LOCKER_ID")
private Locker locker;

// Locker
@OneToOne(mappedBy="locker")
private Member member;

MEMBER 테이블이 외래 키를 가지고 있어서 Member 엔티티에 있는 Member.locker가 연관관계의 주인이다.

반대 매핑인 사물한의 Locker.member는 mappedBy를 선언해서 연관관계의 주인이 아니라고 설정했다.

3.2 대상 테이블에 외래 키

단방향

일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다.

JPA2.0부터 일대다 단방향 관계에서 대상 테이블에 외래 키가 있는 매핑을 허용했다. 하지만 일대일 단방향은 이런 매핑을 허용하지 않는다.

양방향

// Member
@OneToOne(mappedBy="member")
private Locker locker;

// Locker
@OneToOne
@JoinColumn(name="MEMBER_ID")
private Member member;

이 경우 대상 엔티티인 Locker가 연관관계의 주인이 되며, MEMBER_ID 외래 키를 통해 관계를 관리한다. Member는 mappedBy를 사용해 읽기 전용으로 관계를 조회만 한다.

4. 다대다 [N:N]

  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 그래서 보통 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.
  • 객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있다. ex: 회원 객체는 컬렉션을 사용해서 상품들을 참조하면 되고 반대로 상품들도 컬렉션을 사용해서 회원들을 참조하면 된다.

4.1 다대다: 단방향

// Member
@ManyToMany
@JoinTable(name="MEMBER_PRODUCT", 
					joinColumns = @JoinColumn(name="MEMBER_ID"),
					inverseJoinColumns = @JoinColumn(name="PRODUCT_ID")
private List<Product> products = new ArrayList<Product>();
// Product
@Id @Column(name="PRODUCT_ID")
private String id;

회원 엔티티와 상품 엔티티를 @ManyToMany 로 매핑했다. 중요한 포인트: @ManyToMany@JoinTable 을 사용해서 연결 테이블을 바로 매핑했다.

속성을 정리하면:

  • @JoinTable.name : 연결 테이블 지정
  • @JoinTable.joinColumns : 현재 방향인 회원과 매핑할 조인 컬럼 정보 지정
  • @JoinTable.inverseJoinColumns : 반대 방향인 상품과 매핑할 조인 컬럼 정보를 지정

4.2 다대다: 양방향

다대다 매핑이므로 역방향도 @ManyToMany 를 사용한다. 양쪽 중 원하는 곳에 mappedBy로 연관관계의 주인을 지정한다.

// 역방향 추가
// Product

@ManyToMany(mappedBy="products") 
private List<Member> members;

4.3 다대다: 매핑의 한계와 극복, 연결 엔티티 사용

@ManyToMany

  • 장점: 연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지고 여러 가지로 편리하다.
  • 한계: 예를 들어 회원이 상품을 주문하면 단순히 회원 ID와 상품 ID만 저장하는 것이 아니라 주문 수량, 주문 날짜와 같은 추가 정보가 필요하다. 하지만 @ManyToMany에서는 연결 테이블이 엔티티로 존재하지 않기 때문에 이러한 컬럼을 매핑할 수 없다.

→ 이를 해결하기 위해 연결 테이블을 별도의 엔티티로 만들어 다대다 관계를 일대다, 다대일 관계로 풀어야 한다.

// Member
@Id @Column(name="MEMBER_ID")
private String id;

@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts;
// Product
@Id @Column(name="PRODUCT_ID")
private String id;

private String name;
// MemberProduct (회원상품 엔티티)

@Id
@ManyToOne
@JoinColumn(name="MEMBER_ID")
private Member member; // MemberProductId.member와 연결

@Id
@ManyToOne
@JoinColumn(name="PRODUCT_ID")
private Product product; // MemberProductId.product와 연결

위 예제에서 MemberProduct 엔티티는 memberproduct를 함께 기본 키로 사용하는 복합 키 구조를 가진다. 즉, MEMBER_IDPRODUCT_ID를 조합해 하나의 식별자로 사용하며, 같은 회원과 상품의 조합은 하나만 존재할 수 있다.

회원상품은 회원과 상품의 기본 키를 받아서 자신의 기본 키로 사용한다. 이처럼 부모 엔티티의 기본 키를 자식 엔티티의 기본 키로 함께 사용하는 관계를 식별관계(Identifying Relationship) 라고 한다.

→ 복합 키를 사용하는 방법이 복잡하다.

4.4 다대다: 새로운 기본키 사용

추천하는 방식은 데이터베이스에서 자동으로 생성되는 대리 키(Long 타입)를 기본 키로 사용하는 것이다. 기존의 복합 키 대신 ORDER_ID와 같은 단일 기본 키를 두고, MEMBER_ID, PRODUCT_ID는 외래 키로만 사용한다.

// 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;
// Member
@OneToMany(mappedBy="member")
private List<Order> orders = new ArrayList<Order>();

// Product
@Id @Column(name="PRODUCT_ID")
private String id;
private String name;

이 방식은 엔티티가 독립적인 식별자를 가지게 되어 식별관계가 아닌 비식별관계로 구성된다. 그 결과 매핑이 단순해지고, 코드 작성과 유지보수가 훨씬 수월해진다.

다대다 연관관계 정리

다대다 관계를 일대다 다대일 관계로 풀어내기 위해 연결 테이블을 만들 때 식별자를 어떻게 구성할지 선택해야 한다.

  • 식별 관계: 받아온 식별자를 기본 키 + 외래 키로 사용한다.
  • 비식별 관계: 받아온 식별자는 외래 키로만 사용하고 새로운 식별자를 추가한다.

0개의 댓글