다양한 연관관계 매핑

0taetae·2025년 1월 12일
post-thumbnail

엔티티의 연관관계를 매핑할 때, 고려해야 할 3가지
1. 다중성 : 다대일, 일대다, 일대일, 다대다
2. 단방향, 양방향
3. 연관관계의 주인

이를 고려하여 모든 연관관계에 대해 자세히 알아볼 것이다.

📙다대일

데이터베이스 테이블의 일대다, 다대일 관계에서 외래 키항상 다쪽에 있다.
따라서 객체 양방향 관계에서 연관관계의 주인항상 다쪽이다.

  • @ManyToOne을 사용한다.

✏️다대일 단방향

다음 코드를 통해 다대일 단방향 연관관계를 알아보자.

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

위 코드를 살펴보면
회원은 Member.team으로 팀 엔티티를 참조할 수 있지만, 팀에는 회원을 참조하는 필드가 없다.
또한 @JoinColumn(name = "TEAM_ID")를 사용하여 Member.team 필드를 TEAM_ID 외래 키와 매핑했으므로, Member.team 필드로 회원 테이블의 TEAM_ID 외래키를 관리한다.
💡@Id @GeneratedValue : id 필드가 이 엔티티의 PK이고, PK의 값을 자동으로 생성한다.

✏️다대일 양방향

다음 코드를 통해 다대일 양방향 관계를 알아보자.

// 회원 엔티티
@Entity
public void 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<Member>();
   
   public void addMember(Member member){
      this.members.add(member);
      // 무한루프에 빠지지 않도록 체크 
      if(member.getTeam() != this){
         member.setTeam(this);
      }
   }
}
  • 양방향은 외래 키가 있는 쪽이 연관관계의 주인이다.
    • 연관관계의 주인만 외래 키를 관리한다.
    • 주인이 아닌 쪽은 조회를 위한 JPQL이나 객체 그래프를 탐색할 때 사용한다.
  • 양방향 연관관계는 항상 서로를 참조해야 한다.
    • 한 쪽만 참조하면 양방향 연관관계가 성립하지 않는다.

위 코드를 살펴보면
Member.team은 연관관계의 주인이고, Team.members은 연관관계의 주인이 아니다.

📙일대다

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

  • @OneToMany를 사용한다.

✏️일대다 단방향

다음은 일대다 단방향으로 매핑한 팀 엔티티와 회원 엔티티 코드이다.

// 팀 엔티티
@Entity
public class Team{
   @Id @GeneratedValue
   @Column(name = "TEAM_ID")
   private Long id;
   
   private String name;
   
   @OneToMany
   @JoinColumn(name = "TEAM_ID") // Member테이블의 TEAM_ID (FK)
   private List<Member> members = new ArrayList<Member>();
}

// 회원 엔티티
@Entity
public class Member {
   @Id @GeneratedValue
   @Column(name = "MEMBER_ID")
   private Long id;
   
   private String username;
}

💡일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 한다.
-> 그렇지 않으면, JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용해서 매핑한다.

  • 일대다 단방향 매핑의 단점
    • 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다.
      -> 연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야 한다.

✏️일대다 양방향

일대다 양방향 매핑은 존재하지 않으며, 다대일 양방향 매핑을 사용해야 한다.
일대다 양방향 매핑을 하고자 한다면, 일대다 단방향 매핑 반대편에 같은 외래 키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 하나 추가하면 된다.

다음은 일대다 양방향 관계를 알아보는 팀 엔티티, 회원 엔티티 코드이다.

// 팀 엔티티
@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<Member>();
}

// 회원 엔티티
@Entity
public class Member{
   @Id @GeneratedValue
   @Column(name = "MEMBER_ID")
   private Long id;
   private String username;
   
   @ManyToOne
   @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
   private Team team;

}

위 코드는 일대다 단방향 매핑 반대편에 다대일 단방향 매핑을 추가한 것이다.

  • TEAM_ID 외래 키 칼럼을 매핑
  • 대다일 쪽은 insertable = false, updatable = false로 설정하여 읽기만 가능하도록 하였다.

📙일대일

✏️일대일 관계의 특징

  • 일대일 관계는 그 반대도 일대일 관계이다.
  • 일대일 관계는 주 테이블이나 대상 테이블 둘 중 어느 곳이나 외래 키를 가질 수 있다.
    1. 주 테이블에 외래 키
      • 주 테이블이 외래 키를 가지고 있으므로 주 테이블만 확인해도 대상 테이블과 연관관계까 있는지 알 수 있다.
    2. 대상 테이블에 외래 키
      • 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있다.
  • 단방향으로 참조할 대상을 담고 있는 필드에 @OneToOne를 사용하면 된다.
  • 참조 키 방식은 참조할 때 사용하는 칼럼에 @JoinColumn를 사용하면 된다.
    반면에, 식별자 공유 방식은 @PrimaryKeyJoinColumn을 사용하면 된다.

✏️주 테이블에 외래 키

  1. 단방향
    회원과 사물함의 일대일 단방향 관계를 알아보자

    • 주 테이블 : Member / 대상 테이블 : Locker
    @Entity
    public class Member {
       @Id @GeneratedValue
       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을 사용했고
      데이터베이스에는 LOCKER_ID 외래 키에 유니크 제약조건을 추가했다.
  2. 양방향
    반대 방향을 추가해서 일대일 양방향 관계를 알아보자.

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

    양방향 연관관계이므로 연관관계의 주인을 정해야 한다.
    위 예제에서는 MEMBER 테이블이 외래 키를 가지고 있으므로 Member.locker가 연관관계의 주인이다.

✏️대상 테이블에 외래 키

  1. 단방향
    대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다.
    💡 단방향 관계를 수정하거나, 양방향 관계로 만들고 연관관계의 주인을 다르게 설정해야한다.
    위의 예제로 말해보자면, 단방향 관계를 Locker에서 Member 방향으로 수정하거나, Locker를 연관관계의 주인으로 설정해야 한다.
  2. 양방향
    다음 예제를 통해 대상 테이블에 외래 키가 있는 양방향 관계를 알아보자
    @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;
    }
    일대일 매핑에서 대상 테이블에 외래 키를 두고자 한다면,
    위 예제와 같이 주 엔티티인 Member 엔티티 대신에 대상 엔티티인 Locker를 연관관계의 주인으로 만들어서 LOCKER 테이블의 외래 키를 관리하도록 하면 된다.

📙다대다

RDBMS는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
그래서 다대다 관계를 일대다, 다대일 관계로 풀어낸다.

✏️다대다 단방향

// 회원 엔티티
@Entity
public class Member {
   @Id @Column(name = "MEMBER_ID")
   private String 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<Product>():
}

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

위 예제를 살펴보면 @ManyToMany와 @JoinTable을 사용하여 연결 테이블을 바로 매핑하였다. 그래서 두 엔티티를 연결하는 것 없이 매핑을 완료할 수 있다.

@JoinTable의 속성

  • @JoinTable.name : 연결 테이블을 지정
  • @JoinTable.joinColumns : 현재 방향인 엔티티와 매핑할 조인 칼럼 정보를 지정
  • @JoinTable.inverseJoinColumns : 반대 방향인 엔티티와 매핑할 조인 칼럼 정보를 지정

✏️다대다 양방향

  • 역방향도 @ManyToMany를 사용
  • 양쪽 중 원하는 곳에 mappedBy로 연관관계의 주인을 지정, mappedBy가 없는 곳이 연관관계의 주인
  • 양방향 연관관계는 연관관계 편의 메소드를 추가해서 관리하는 것이 편리하다.

✏️복합 기본 키

JPA에서 복합 키를 사용하려면 별도의 식별자 클래스로 만들어야 한다.

다음은 복합 키를 위한 식별자 클래스의 특징이다.

  • 복합 키는 별도의 식별자 클래스로 만들어야 한다.
  • Serializable을 구현해야 한다.
  • equals와 hashCode 메소드를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public 이어야 한다.
  • @IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있다.

✏️식별 관계와 비식별 관계

  • 식별 관계는 받아온 부모 테이블의 기본 키(식별자)를 자신의 기본 키 + 외래 키로 사용하는 것을 말한다.
  • 비식별 관계는 받아온 식별자를 외래 키로만 사용하고 새로운 식별자를 추가하는 것을 말한다.
    💡 비식별 관계를 사용하는 것이 복합키를 위한 식별자 클래스를 만들지 않아도 되기 때문에 단순하고 편리하게 ORM 매핑을 할 수 있다.

📙정리

연관관계 매핑 시 고려사항

  • 다중성
  • 방향
  • 연관관계 주인

다대일

  • @ManyToOne
  • 외래 키는 항상 '다'쪽
  • 양방향 관계에서 '다'쪽이 연관관계의 주인

일대다

  • @OneToOne
  • 주 테이블이나 대상 테이블에 외래 키 지정
  • 양방향일 경우, 연관관계 주인 지정

다대다

  • @ManyToMany

연관관계 주인

  • 외래 키를 관리하는 엔티티
  • 양방향 관계에서 한 쪽만 연관관계의 주인으로 지정
  • 주인이 아닌 쪽에 mappedBy 속성 사용

식별 관계

  • 부모 테이블의 기본 키를 자식 테이블의 기본 키 + 외래 키로 사용

비식별 관계

  • 부모 테이블의 기본키를 자식 테이블의 외래 키로만 사용

0개의 댓글