본문 바로가기
Language & Tool/sql

🗄️연관관계 매핑 전략과 핵심 개념(다대일, 일대다, 연관관계의 주인)

by 박수무당벌레 2026. 7. 2.

1. 다대일 (@ManyToOne)

여러 명의 회원이 하나의 팀에 속한다.

  • 특징: 데이터베이스 테이블 구조상 외래 키(FK)는 N(다) 쪽인 회원(MEMBER) 테이블에 존재한다.
    • 가장 흔하게 사용되는 연관관계. (기본적인 필수 매핑 방식) 
    • 기준 객체가 N쪽. (본인 테이블에 FK가 있음)
    • 연관관계의 주인으로, FK를 직접 관리/등록/수정
    • 예: 회원과 팀
  • 구현 방법: N에 해당하는 테이블(MEMBER)에 1에 해당하는 테이블(TEAM)의 PK를 FK로 추가한다.
    • 작동: member.setTeam(team)을 호출하고 저장하면 바로 MEMBER 테이블의 TEAM_ID FK 값이 업데이트된다.
  • 장점: 객체의 참조(team)와 테이블의 외래 키(TEAM_ID) 위치가 일치하므로 직관적이고 관리가 매우 쉽다.
@Entity
public class Member { // 'N' 입장
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "team_id") // MEMBER 테이블의 team_id FK와 매핑
    private Team team;
}
💡 한쪽 테이블의 레코드가 다른 테이블의 여러 레코드와 연결될 수 있는 구조로, 엄밀히 일대다와는 다르다.
💡 다대일과 일대다는 관계를 바라보는 기준(시선)의 차이가 다르다. 주로 테이블의 외래 키(FK) 위치와 연관관계의 주인 개념을 기준으로 구분한다.

ℹ️ @JoinColumn을 사용하는 이유 (역할)

  • 개념 차이 해결(데이터 연관짓는 방식): 자바 객체는 참조(team)로 관계를 맺고, DB 테이블은 외래 키 컬럼(TEAM_ID)으로 관계를 맺음.
  • 외래 키(FK) 이름 지정: 객체의 참조 필드가 실제 DB의 어떤 외래 키 컬럼과 연결될지 이름을 명시.
  • 올바른 연관관계 저장: 객체 모델과 DB 테이블 모델 간의 차이를 매핑하여 데이터가 정확하게 저장 및 조회되도록 돕는다.

2. 일대다 (@OneToMany)

하나의 팀이 여러 명의 회원을 가진다.

  • 특징: 1(일) 쪽인 팀(Team) 객체 입장에서 N(다) 목록(members)을 관리하는 방식.
    • 상대방(N) 테이블에 FK가 있음
    • 연관관계의 주인이 아니므로, mappedBy로 읽기 전용으로 설정하는 것이 일반적
    • 단독 매핑보다는 @ManyToOne과 함께 양방향으로 사용 권장 (아래 [5번]에 추가 설명)
    • 예: 팀과 회원 (팀이 기준)
  • ⚠️ 주의점 (단방향 일대다 매핑의 단점):
    • 객체는 Team이 회원 목록을 관리하지만, 정작 DB의 외래 키(FK)는 MEMBER 테이블에 있다.
    • 따라서 team.getMembers().add(member)로 회원을 추가하면, Team을 저장/수정할 때 MEMBER 테이블에 UPDATE 쿼리가 추가로 실행되는 단점이 발생한다.
@Entity
public class Team { // '1' 입장
    @Id @GeneratedValue
    private Long id;

    @OneToMany
    @JoinColumn(name = "team_id") // 상대방(MEMBER) 테이블의 FK를 지정해야 함
    private List<Member> members = new ArrayList<>();
}
💡 Tip!
항상 외래 키(FK)가 위치한 엔티티가 연관관계의 주인(Owner)이 되도록 매핑하고, 주인이 아닌 쪽은 mappedBy 속성을 지정하는 것이 기본 원칙.
ℹ️ JPA에서는 주 테이블에서 대상 테이블의 FK를 직접 관리하는 단방향 매핑을 지원하지 않으므로, 반드시 양방향 매핑(mappedBy)으로 설정해야 한다.

3. 일대일 (@OneToOne)

하나의 회원이 하나의 보관함만 가진다
  • 특징: 양쪽 엔티티가 서로 단 하나씩만 연결되는 구조.
    • 주 테이블이나 대상 테이블 중 어느 곳에나 외래 키(FK)를 배치할 수 있다.
    • 주로 보안상의 이유로 테이블을 분리하거나, 특정 데이터가 자주 쓰이지 않아 테이블을 쪼갤 때 사용한다.
    • 예: 회원과 보관함, 회원과 여권
  • 구현 방법: 어느 한쪽 테이블의 PK를 다른 쪽 테이블의 FK로 지정하되, 그 FK에 UNIQUE(중복 방지) 제약 조건을 걸어준다.
    • 동작 원리 (주테이블에 FK 배치):
      1. Member 엔티티가 Locker 엔티티의 참조를 가진다.
      2. @JoinColumn(name = "locker_id")로 지정된 컬럼에 상대방의 PK 값이 저장된다.
      3. FK에 UNIQUE 제약조건이 추가되어 1:1 관계가 유지된다.
  • 외래 키(FK) 위치 선택:
    • 주 테이블에 FK 배치 (권장): 주 테이블만 조회해도 대상 테이블과의 연관관계를 바로 확인할 수 있어 개발자들이 선호.
      • JPA 조회가 단순하고 빠르다. (객체지향적 조회의 편리성)
    • 대상 테이블에 FK 배치: 전통적인 DB 개발자들이 선호하며, 관계가 1:N으로 확장될 때 테이블 구조를 유지하기 유리하다. (mappedBy 필수)
      • DB 외래 키는 Locker에 있지만, 자바 객체의 주도권(연관관계의 주인)을 Member로 두고 싶거나 반대로 설정할 때 사용한다.
@Entity
public class Member { // 주 테이블
    @Id @GeneratedValue
    private Long id;

    @OneToOne
    @JoinColumn(name = "locker_id") // 외래 키(FK)를 주 테이블(Member)이 관리
    private Locker locker;
}

@Entity
public class Locker { // 대상 테이블 (양방향 설정 시)
    @Id @GeneratedValue
    private Long id;

    @OneToOne(mappedBy = "locker") // 읽기 전용 (Member 엔티티의 locker 필드)
    private Member member;
}

4. 다대다 (@ManyToMany)

여러 명의 학생이 여러 개의 과목을 신청해 듣는다
  • 특징: 양쪽 테이블(엔티티)가 서로 상대방의 레코드를 여러 개 가질 수 있는 구조
    • 관계형 데이터베이스는 정규화된 2개의 테이블만으로 N:M 관계를 표현할 수 없다.
      • 따라서 중간에 두 테이블 사이에 관계를 중개해 주는 연결 테이블(Junction/Association Table)을 두고, 1:N, N:1 관계로 풀어내야 한다. (아래 구현 방법에 대한 내용은 [5번]에 추가 설명)
    • 예: 학생과 과목, 회원과 상품
  • 구현 방법: 중간에 연결 테이블을 생성해, 각각의 PK를 모아 외래키로 연결하는 1:N 관계 2개로 풀어낸다. 
    • 예: 수강신청 테이블을 생성하여 학생ID와 과목ID를 저장
  • ⚠️ 주의점 (실무 사용 금지 권장):
    • @ManyToMany 어노테이션을 사용하면 JPA가 자동으로 중간 연결 테이블을 생성해주지만, 이 테이블에는 연결용 FK 외에 다른 컬럼(예: 수강 신청일, 주문 수량, 생성일자 등)을 추가할 수 없습니다.
    • 또한 시스템이 예상치 못한 복잡한 중간 테이블 쿼리를 실행하므로 실무에서는 사용을 지양합니다.
// ❌ [비권장] @ManyToMany 단순 매핑 방식
@Entity
public class Student {
    @Id @GeneratedValue
    private Long id;

    @ManyToMany
    @JoinTable(name = "student_course") // 자동으로 생성될 연결 테이블명
    private List<Course> courses = new ArrayList<>();
}

5. 실무 권장 패턴

1) 양방향 매핑 (1:N / N:1 관계인 경우)

  • 특징: 단순 1:N / N:1 관계에서 양쪽에서 서로 참조 가능하도록 설정하는 기법 (단방향 조회만 가능하던 것을 반대쪽에서도 객체 참조(조회)할 수 있도록 양방향화)
    • @ManyToOne을 연관관계의 주인(FK 관리자)으로 두고, 필요할 때 @OneToMany를 추가하여 읽기전용 양방향 관계를 만듦 (가장 표준적인 전략)
    • 원래부터 N 쪽에 FK 존재했기 때문에 DB 테이블에는 아무 변화가 없다. (원래 MEMBER 테이블에 있던 team_id FK 사용)
  • 구현:
    • Member(N) : @ManyToOne (외래 키 관리자 / 주인)
    • Team(1) : @OneToMany(mappedBy = "team") (읽기 전용)
// N (다) - 외래 키를 가지며 연관관계의 주인
@Entity
public class Member {
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}

// 1 (일) - 읽기 전용 (mappedBy 지정)
@Entity
public class Team {
    @OneToMany(mappedBy = "team") // Member 엔티티의 'team' 필드에 의해 매핑됨을 명시
    private List<Member> members = new ArrayList<>();
}
💡요약: DB 테이블의 외래 키(FK)가 위치한 '다(N)' 쪽 엔티티에 @ManyToOne을 설정해 FK를 관리하게 하고, '일(1)' 쪽에서 조회 기능이 필요하면 @OneToMany(mappedBy = "...")를 추가하는 것이 안전하다.

2) 중간 엔티티(연결 테이블) 승격 (N:M 관계인 경우)

  • 특징: 다대다 관계를 중간 엔티티(테이블)를 직접 만들어 양쪽에 실무에서 사용 가능한 형태(1:N + N:1)로 분해(매핑)하는 설계 방식. (가장 안전)
    • @ManyToMany 대신 중간 연결 엔티티를 두고, 양쪽에 @ManyToOne 2개 작성
    • DB에 중간 엔티티와 그 전용 테이블이 새로 생성 및 추가된다. (Orders(주문)라는 중간 엔티티 + ORDERS 테이블 생성)
    • JPA가 자동으로 만드는 N:M 연결 테이블의 한계(추가 컬럼 불가능)를 극복.
  • 구현:
    • Member(1) ↔ Orders(N) ↔ Item(1)
    • Orders 엔티티가 Member와 Item의 FK를 각각 @ManyToOne으로 가지며, orderCount, orderDate 같은 추가 필드를 마음껏 정의할 수 있다.
// 1. 회원 (1)
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "member")
    private List<Orders> orders = new ArrayList<>();
}

// 2. 주문 (중간 엔티티 - N:M을 1:N, N:1로 해쇄)
@Entity
public class Orders {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;

    @ManyToOne
    @JoinColumn(name = "item_id")
    private Item item;

    private int orderCount; // 연결 테이블에 필요한 추가 컬럼 작성 가능!
    private LocalDateTime orderDate;
}

// 3. 상품 (1)
@Entity
public class Item {
    @Id @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "item")
    private List<Orders> orders = new ArrayList<>();
}

6. 연관 관계 데이터 조회하기 (JOIN)

  • 위에서 설정된 연관 관계를 바탕으로 데이터를 함께 묶어서 조회할 때는 JOIN 문을 사용한다.
-- 부서와 사원 테이블을 연결하여 조회하는 예시
SELECT e.사원명, d.부서명
FROM 사원 e
INNER JOIN 부서 d ON e.부서ID = d.부서ID;