본문 바로가기

CS

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

JPA에서 엔티티 간 연관관계를 매핑할 때, 연관관계의 주인과 방향성, 외래 키 관리 방식에 대한 이해는 필수입니다. 연관관계의 주인은 외래 키를 실제로 관리하는 엔티티이며, 주인이 아닌 엔티티는 조회를 위한 용도로만 사용됩니다.

 

이번 포스팅에서는 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:N) 관계를 설명하고, 각 매핑 방식과 특성, 장단점을 예제 코드와 함께 자세히 알아보겠습니다. 

 


다대일(N:1) 관계

 

 

다대일 관계는 가장 일반적으로 사용되는 연관관계입니다. 여러 엔티티가 하나의 엔티티에 연관될 때 사용되며, 예를 들어 여러 명의 회원(Member)이 하나의 팀(Team)에 소속되는 경우입니다.

 

데이터베이스 테이블에서는 외래 키가 항상 다쪽(Member) 테이블에 존재하기 때문에, JPA에서는 외래 키를 가진 Member가 연관관계의 주인이 됩니다. 다대일 관계에서 외래 키는 항상 다쪽에 존재한다. 즉, 연관관계의 주인은 항상 다쪽입니다.

 


다대일 단방향 (N:1)

다대일 단방향 관계에서는 "다쪽" 엔티티인 Member가 "일쪽" 엔티티인 Team을 참조합니다. 이때 Team은 Member를 참조하지 않기 때문에 탐색 방향이 단방향입니다. 예를 들어, 회원이 자신이 속한 팀을 확인할 수는 있지만, 팀은 자신에게 속한 회원들을 직접 알지 못합니다. 왜냐하면 관계를 맺음으로써 "탐색"을 할 수 있냐, 없냐가 결정되기 때문입니다.

 

@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을 참조하고, @JoinColumn을 통해 TEAM_ID라는 외래 키로 매핑됩니다. 단방향 연관관계는 구조가 단순하고 유지보수가 용이하지만, 반대로 팀이 자신에게 속한 회원들을 조회하려면 별도의 JPQL 쿼리를 작성해야 합니다.

 


다대일 양방향 (N:1, 1:N)

다대일 양방향 관계에서는 Member와 Team이 서로를 참조합니다. Member는 자신이 속한 팀을 알 수 있고, Team은 자신에게 속한 회원들의 목록을 알 수 있습니다. 외래 키가 Member 테이블에 존재하기 때문에, Member 엔티티가 연관관계의 주인이 됩니다. 반면 Team은 mappedBy 속성을 통해 연관관계의 주인이 아님을 명시합니다.

@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) {
        members.add(member);
        if (member.getTeam() != this) {
            member.setTeam(this);
        }
    }
}

 

 

양방향 관계에서는 무한 루프 문제가 발생할 수 있기 때문에 setTeam과 addMember 메서드에서 중복 추가를 방지하는 로직을 포함해야 합니다. 양방향 관계는 객체 그래프 탐색이 더 유연하지만, 두 엔티티 간 관계를 모두 관리해야 하므로 복잡도가 증가합니다.

 

해당 포스팅 코드에서는 둘 다 연관관계 편의 메서드를 만들었지만, 사실 어느 한 쪽에서만 편의메서드를 만드는 것이 데이터 무결성에도 좋고 관리하는 입장에서도 더 좋습니다. 특히 연관관계의 주인 쪽에서만 관계를 관리하도록 설계하는 것이 가장 안전하고 명확한 방법입니다.

 

 

결론은 양방향은 외래 키가 있는 쪽이 연관관계의 주인이고, 양방향 관계는 항상 서로를 참조해야 한다라는 게 핵심입니다.


일대다(1:N) 관계

 

일대다 관계는 다대일 관계의 반대 방향입니다. 하나의 엔티티가 여러 엔티티를 참조할 때 사용되며, 주로 컬렉션(List, Set 등)을 사용해 매핑합니다. 다만, 외래 키는 항상 다쪽(Member)에 존재하기 때문에 JPA에서 @OneToMany는 연관관계의 주인이 될 수 없습니다.


일대다 단방향 (1:N)

일대다 단방향 관계에서는 Member 테이블Team 엔티티의 외래 키를 관리합니다. 이 관계에서는 Team 엔티티가 Member 객체의 목록(List<Member>)을 포함하고 있지만, 실제 데이터베이스에서는 Member 테이블이 TEAM_ID(FK)라는 외래 키를 통해 Team을 참조합니다.


단방향 관계이기 때문에 Member는 Team을 참조할 수 있지만, Team에서는 Member를 직접 참조하지 않습니다. 따라서 외래 키 관리의 주체는 Member 테이블입니다.

 

@Entity
public class Team {
    @Id
    @GeneratedValue
    @Column(name = "Team_ID")
    private Long id;
    
    private String name;
    
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();
}

 

단방향 일대다 관계에서 @OneToMany와 @JoinColumn을 사용하면, 외래 키인 TEAM_ID는 Member 테이블에 존재하지만, JPA에서는 이를 Team 엔티티에서 관리하도록 설정합니다. 이 구조는 외래 키가 실제로는 Member 테이블에 있으면서 Team 엔티티가 이를 관리하는 방식으로 동작하기 때문에, 삽입 시 외래 키 값이 먼저 NULL로 저장되고 이후 추가적인 UPDATE SQL이 실행되는 비효율성이 있습니다.

 

이러한 비효율성은 데이터베이스 설계와 어긋나는 점이며, 실무에서는 이 방식이 잘 사용되지 않는 주된 이유입니다. 대신, 다대일 양방향 관계를 사용하여 Member 엔티티가 외래 키를 직접 관리하도록 구현하는 것이 일반적입니다. 이렇게 하면 관계 설정이 더 직관적이고 효율적으로 작동하며, 불필요한 추가 SQL 실행을 방지할 수 있습니다.


일대다 양방향 (1:N, N:1)

JPA에서는 일대다 양방향 관계라는 개념은 존재하지 않습니다. 대신 다대일 양방향 관계로 구현해야 하며, 주인은 외래 키를 가진 다쪽(Member) 엔티티입니다. 일대다 양방향처럼 보이도록 구현할 수도 있지만, 여전히 비효율적인 UPDATE SQL이 발생합니다.

 

@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
    private Team team;
}

@Entity
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();
}

 

위와 같은 방식은 여전히 성능과 유지보수에 문제가 있기 때문에, 다대일 양방향 매핑을 사용하는 것이 더 바람직합니다.

 


일대일(1:1) 관계

 

일대일 관계는 두 엔티티가 서로 하나의 관계만을 가질 때 사용됩니다. 예를 들어, Member(회원)와 Locker(사물함) 사이의 관계가 일대일 관계입니다. 일대다, 다대일 관계와 달리 외래 키를 어느 쪽에 두어도 무방합니다. 따라서 설계에 따라 주 테이블에 외래 키를 둘지, 대상 테이블에 외래 키를 둘지 선택해야 합니다.

 


주 테이블에 외래 키

주 테이블에 외래 키를 두는 방식은 주로 객체 참조와 유사하게 사용할 수 있기 때문에 선호됩니다. 주 테이블만 확인해도 연관관계가 있는지 파악할 수 있습니다.


일대일 단방향 관계 (주)

단방향에서는 주 테이블(Member)이 대상 테이블(Locker)을 참조합니다.

 

@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;
}

주 테이블에 @JoinColumn을 통해 LOCKER_ID 외래 키가 매핑됩니다. 이 경우 Member만 Locker를 참조할 수 있습니다


일대일 양방향 관계 (주)

양방향에서는 Member와 Locker가 서로를 참조합니다.

 

@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;
}

양방향 관계에서는 mappedBy 속성을 사용하여 연관관계의 주인을 Member로 설정합니다. 이를 통해 Locker 엔티티는 읽기 전용 상태로 유지됩니다.


대상 테이블에 외래 키

대상 테이블에 외래 키를 두는 방식은 전통적인 데이터베이스 설계에서 선호됩니다. 대상 테이블에 외래 키를 두면 일대일에서 일대다 관계로 쉽게 변경할 수 있습니다.


 

일대일 단방향 관계 (대상)

JPA는 대상 테이블에 외래 키가 있는 단방향 일대일 관계를 지원하지 않습니다. 따라서 양방향 관계로 매핑해야 합니다.


 

일대일 양방향 관계 (대상)

양방향에서는 대상 테이블이 외래 키를 관리하고, 주 테이블은 이를 읽기 전용으로 참조합니다.

@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;
}

 

Locker가 외래 키(MEMBER_ID)를 관리하며, Member는 mappedBy를 통해 읽기 전용으로 유지됩니다.


다대다(N:N) 관계

 

다대다 관계는 관계형 데이터베이스에서는 직접적으로 표현할 수 없습니다. 대신 연결 테이블을 사용해 다대다 관계를 일대다와 다대일 관계로 풀어서 표현합니다. 객체에서는 @ManyToMany를 통해 다대다 관계를 만들 수 있습니다. 그러나 실무에서는 다대다 관계를 직접 사용하기보다는 연결 엔티티를 만들어서 사용합니다.


다대다 단방향

단방향 다대다 관계는 한 엔티티에서 다른 엔티티로만 탐색할 수 있습니다.

@Entity
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "Member_ID")
    private Long 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
    @GeneratedValue
    @Column(name = "PRODUCT_ID")
    private Long id;
    
    private String name;
}

다대다 관계에서는 @JoinTable을 사용하여 연결 테이블을 명시적으로 설정합니다. 하지만 연결 테이블에 추가 컬럼(예: 주문 수량, 주문 날짜 등)이 필요할 경우 @ManyToMany는 한계가 있습니다.

 

 

@JoinTable의 속성은 다음과 같다

name 연결 테이블 지정
joinColumns  조인 칼럼 정보를 지정
inverseColumns  반대편 조인 칼럼 정보를 지정

 


다대다 양방향

양방향 다대다 관계에서는 두 엔티티가 서로를 참조하며, mappedBy 속성을 사용하여 Product가 연관관계의 주인이 아님을 명시합니다.

@Entity
public class Product {
    @Id
    @GeneratedValue
    @Column(name = "PRODUCT_ID")
    private Long id;
    
    private String name;
    
    @ManyToMany(mappedBy = "products")
    private List<Member> members = new ArrayList<>();
}

 


연결 엔티티 사용

실무에서는 다대다 관계를 연결 테이블을 별도의 엔티티로 만들어 사용합니다. 이 방식을 통해 추가 컬럼(예: 주문 수량, 주문 날짜 등)을 매핑할 수 있습니다.

@Entity
public class Member {
  // 역방향
  @OneToMany(mappedBy = "member")
  private List<MemberProduct> memberProducts;
  ...
}

@Entity
public class Product {
  ...
}

@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; // MemberProductI.product와 연결
  
  private int orderAmount;
}
public class MemberProductId implements Serializable {
  private String member; // MemberProduct.member와 연결
  private String product; // MemberProduct.product와 연결
  
  // hashCode and equals
  @Override
  public boolean equals(Object o) {...}
  
  @Override
  public int hashCode() {...}
}

 

 

복합 키를 사용하기 위해서는 별도의 식별자 클래스를 만들어야 하며 회원상품 식별자 클래스와 특징은 다음과 같고 회원상품은 회원에서 기본 키를 받아서 자신의 기본 키로 사용함과 동시에 회원과의 관계를 위한 외래 키로 사용하는 식별 관계

equals와 hashCode 메서드를 구현해야 한다
기본 생성자가 있어야 한다.
식별자 클래스는 public이다.
복합키에서는 Serializable를 하여야 한다

 

 


비식별 관계 (대리 키 사용)

복합 키 대신 대리 키를 사용해 더 간단하게 매핑할 수 있습니다.

 

비식별 관계에서는 연결 테이블에 새로운 단일 식별자(대리 키)를 추가합니다. 두 외래 키는 복합 키가 아니라 단순 외래 키로 사용됩니다. 이 방식을 사용하면 매핑이 훨씬 단순해지고 유지보수가 용이합니다.

@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;
}

 


 

JPA의 연관관계는 상황과 요구사항에 따라 신중하게 선택해야 합니다. 특히 다대다 관계에서는 연결 엔티티를 사용하는 것이 더 유연하며, 복합 키보다는 비식별 관계를 사용하는 것이 권장됩니다. 이번 포스팅을 통해 연관관계에 대한 이해가 깊어지셨기를 바랍니다.

'CS' 카테고리의 다른 글

Thread Pool이란?  (1) 2025.01.20
HTTPS란?  (0) 2025.01.13
[JPA] 연관관계 매핑 기초  (0) 2024.12.23
[JPA] JPA는 무엇이고 사용하는 이유는 무엇일까?  (1) 2024.12.03
TCP/IP 4계층 모델  (1) 2024.10.07