Spring/Spring Data JPA

[자바 ORM 표준 JPA 프로그래밍] 6. 다양한 연관관계 매핑

noahkim_ 2023. 12. 27. 20:14

김영한 님의 "자바 ORM 표준 JPA 프로그래밍" 책을 정리한 포스팅 입니다.

 

1. 다대일 & 일대다

구분 다대일 일대다
다대일 + 일대다
관계 방향 단방향 (N → 1) 단방향 (1 → N)
양방향 (N → 1 + 1 → N)
설명 여러 개의 엔티티가 하나의 엔티티 참조 하나의 엔티티가 여러 개의 엔티티 참조
서로 참조 (객체 탐색 유리)
외래 키 위치 N쪽 N쪽 N쪽
연관관계 주인 N쪽 N쪽 N쪽
장점 구조가 단순하고 성능에 유리함 구조가 단순하고 성능에 유리함
객체 그래프 탐색 유리
비즈니스 로직 구현 용이
단점 INSERT시 외래 키 직접 관리 필요 INSERT시 외래 키 직접 관리 필요
연관관계 주인 설정 필요
객체간 참조 유지 필수
사용 예 단순 조회
트랜잭션 설계가 명확한 경우
복잡한 비즈니스 로직
객체 탐색이 자주 필요한 경우
복잡한 비즈니스 로직
객체 탐색이 자주 필요한 경우

 

예제) 다대일

더보기
@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    
    @ManyToOne
    @JoinColumn(name=”TEAM_ID”)
    private Team team;
}
Team team = new Team("TeamA");

Member member = new Member("Member1");

member.setTeam(team);

em.persist(team);
em.persist(member);

 

예제) 일대다

더보기
@Entity
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();
    
    public void addMember(Member member) {
        members.add(member);
    }
}
  • Member 테이블의 TEAM_ID를 외래키로 참조하여 연관 member들을 List로 받아옵니다.

 

Team team = new Team("TeamA");

Member member1 = new Member("member1");
Member member2 = new Member("member2");

team.addMember(member1);
team.addMember(member2);

em.persist(team);
em.persist(member1);
em.persist(member2);

 

예제) 다대일 + 일대다 (양방향)

더보기
@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String username;

    @ManyToOne // 연관관계의 주인
    @JoinColumn(name = "TEAM_ID") // 외래키가 member 테이블에 생성됨
    private Team team;

    public Member() {}

    public Member(String username) {
        this.username = username;
    }

    public void setTeam(Team newTeam) {
        // 기존 team과 관계 제거
        if (this.team != null) {
            this.team.getMembers().remove(this);
        }

        // 새로운 team과 연결
        this.team = newTeam;

        // 새로운 team에 현재 member가 없다면 추가
        if (newTeam != null && !newTeam.getMembers().contains(this)) {
            newTeam.getMembers().add(this);
        }
    }
}
@Entity
public class Team {

    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team") // 연관관계의 주인이 아님
    private List<Member> members = new ArrayList<>();

    public Team() {}
    public Team(String name) { this.name = name; }

    // 양방향 동기화 도우미
    // 생략 가능 (연관관계 주인만 연산 가능하기 때문)
    public void addMember(Member member) {
        members.add(member);
        member.setTeam(this); // 양방향 연관관계 설정
    }
}
  • 양방향은 외래 키가 있는 쪽이 연관관계의 주인입니다.
  • 양방향 연관관계는 항상 서로를 참조해야 합니다.

 

Team teamA = new Team("TeamA");
Team teamB = new Team("TeamB");

Member member1 = new Member("member1");
Member member2 = new Member("member2");

// 양방향 연관관계 설정
member1.setTeam(teamA);
member2.setTeam(teamB);

em.persist(teamA);
em.persist(teamB);
em.persist(member1);
em.persist(member2);

 

2. 다대다

@JoinTable

  • 두 테이블 사이에 존재하는 연결 테이블을 명시적으로 매핑할 때 사용
속성 설명
name
연결테이블 이름을 지정합니다.
joinColumns
현재 방향의 조인 컬럼 정보를 지정합니다.
inverseJoinColumns
반대 방향의 조인 컬럼 정보를 지정합니다.

 

종류

항목 단방향 양방향 연결 엔티티 방식 (권장)
애노테이션 @ManyToMany + @JoinTable 주인: @ManyToMany + @JoinTable
반대: @ManyToMany(mappedBy="...")
@ManyToOne +
@IdClass or @EmbeddedId
관계 표현 방식 엔티티 2개로 표현
(연결 테이블은 자동 생성됨)
양쪽 모두 참조 가능
연결 테이블을 엔티티로 분리
주인 설정 단방향이므로 주인 개념 X 외래키가 있는 쪽이 주인
(mappedBy로 지정)
외래키를 가지는 필드를 주인으로 설정
추가 컬럼
✅ (등록일, 상태 등 컬럼 추가 가능)
식별 관계
✅ (복합 외래키 → 복합 기본키 방식)
사용 추천 단순한 관계 단순한 관계이지만 주의 필요
✅ 실무 권장 (확장성, 유연성)

 

예제

 

예제) 단방향

더보기
@Entity
public class Member {
    @Id
    @Column(name = "MEMBER_ID")
    private String id;
    
    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT",
               joinColumns = @JoinColumn(name = "MEMBER_ID"),
               inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
    private List<Product> products = new ArrayList<Product>();    

	//...
}
@Entity
public class Product {
    @Id
    @Column(name = "PRODUCT_ID")
    private String id;
    
    private String name;    
    ...        
 }

 

 

예제) 양방향

더보기
@Entity
public class Member {

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

	//...
}
@Entity
public class Product {
    @Id
    @Column(name = "PRODUCT_ID")
    private String id;
    
    private String name;    
    
    @ManyToMany(mappedBy = "products")
    private List<Member> members;    
    ...        
 }
  • 다대다 매핑이므로 역방향도 @ManyToMany 사용합니다.

 

예제) 연결 엔티티

더보기

복합 식별키 클래스 

public class MemberProductId implements Serializable {

    private String member;  //MemberProduct.member와 연결
    private String product; //MemberProduct.product와 연결   
    
    @Override
    public boolean equals(Object o) {...}
    
    @Override
    public int hashCode() {...}
}
  • Serializable, default constructor, equals(), hashCode() 구현 필수

 

연결 테이블

@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;
    private DateTime createdDate;
    // ...
}

 

3. 일대일

  • 양쪽이 서로 하나의 관계만 가집니다.

 

예제

더보기
@Entity
public class Member {
 
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;
 
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    Locker locker;
}
@Entity
public class Locker {
 
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "LOCKER_ID")
    private Long id;
 
    private String name;
}