6장 다양한 연관관계 매핑

이주호·2024년 12월 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;
    
    //Getter, Setter

팀 엔티티

@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;
    
    //Getter, Setter

회원은 Member.team으로 팀 엔티티를 참조할 수 있지만 팀은 회원을 참조하는 필드가 없다. 이런 관계를 회원과 팀 기준으로 다대일 단방향 연관관계라고 한다.

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

회원 엔티티

@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<Member>();
    
    public void addMember(Member member) {
        this.members.add(member);
        if (member.getTeam() != this) {	//무한루프에 빠지지 않도록 체크
            member.setTeam(this);
        }
    }
    
    ...
}

양방향은 외래 키가 있는 쪽이 연관관계의 주인이다. 일대다와 다대일 관계에서는 항상 다쪽이 외래키를 가지고 있으므로 여기서는 Member.team이 연관관계의 주인이다. JPA는 외래 키를 관리할 때 연관관계의 주인만을 사용한다. 주인이 아닌 Team.members는 조회를 위한 JPQL이나 객체 그래프 탐색을 사용할 때 이용한다.

양방향 연관관계는 항상 서로를 참조해야 한다.

어느 한 쪽만 참조하면 양방향 연관관계가 성립되지 않는다. 항상 서로 참조하기 위해서 편의 메서드를 사용하면 좋은데 이런 편의 메서드는 한쪽에만 작성하거나 양쪽에 작성할 수 있다. 양쪽에 작성할 경우 무한 루프에 빠지지 않도록 주의해야 하며, 여기서는 무한 루프에 빠지지 않도록 검사하는 로직을 추가하였다.


일대다

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

일대다 단방향 [1:N]

예를 들어, 하나의 팀은 여러 명의 회원을 참조할 수 있는데 이를 1:N 관계라고 한다. 이 때, 팀은 회원들을 참조하지만 회원은 팀을 참조하지 않으면 단방향 관계이다.
(일대다 관계는 JPA 2.0부터 지원함)

일대다 단방향 관계는 특이한 구조를 가지는데 테이블에서는 항상 다쪽이 외래키를 관리하는데 객체에서는 일쪽에서만 참조를 하기 때문이다. 다시 설명하면 다 쪽인 Member 엔티티에는 외래 키를 매핑할 수 있는 참조 필드가 없고 반대쪽인 Team 엔티티에만 참조 필드인 members가 있어 팀 엔티티에서 반대편 테이블의 외래 키를 관리하는 특이한 모습이 나타난다.

//일대다 단방향 팀 엔티티
@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는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용해서 매핑한다. (자세한 사항은 7장에서)

일대다 단방향 매핑의 단점

매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 문제가 있다. 본인 테이블에 외래키가 있다면 엔티티의 저장과 연관관계 처리를 INSERT로 끝내면 될 것을 UPDATE를 추가로 진행해야 한다.

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

        Team team1 = new Team("team1");
        team1.getMembers().add(member1);
        team1.getMembers().add(member2);

        em.persist(member1);    //INSERT-member1
        em.persist(member2);    //INSERT-member2
        em.persist(team1);      //INSERT-team1,
                                //UPDATE-member1.fk, UPDATE-member2.fk

        transaction.commit();

위를 실행하면 아래와 같은 SQL이 실행된다.

insert into Member (MEMBER_ID, username) values (null, ?)
insert into Member (MEMBER_ID, username) values (null, ?)
insert into Team (TEAM_ID, name) values (null, ?)
update Member set TEAM_ID=? where MEMBER_ID=?
update Member set TEAM_ID=? where MEMBER_ID=?

회원 엔티티는 팀 엔티티를 모르고 연관관계의 정보는 팀 엔티티의 members가 관리하기 때문에 회원 엔티티를 저장할 때, MEMBER 테이블의 TEAM_ID 외래 키에 아무 값도 저장되지 않는다. 팀 엔티티를 저장할 때에서야 Team.members의 참조 값을 확인해서 회원 테이블에 있는 TEAM_ID 외래 키를 업데이트한다.

성능상의 문제와 관리의 문제점으로 인해 일대다 단방향 매핑보다는 다대일 양방향 매핑을 권장한다.

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

연관관계의 주인이 1인 일대다 양방향은 존재하지 않는다. 연관관계의 주인이 다쪽인 다대일 양방향 매핑을 사용해야 한다. 관계형 데이터베이스의 특성상 일대다, 다대일 관계는 항상 다쪽에 외래키가 존재하기 때문에 연관관계의 주인이 일쪽에 있는 일대다 양방향 매핑은 존재하지 않는다.
대신에 일대다 단방향 매핑 반대편에 같은 외래 키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 하나 추가하여 일대다 양방향 매핑을 할 수 있다.(정확히 말하면 일대다 양방향 매핑처럼 보이게 하는 방법)

//일대다 양방향 팀 엔티티
@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;
    //
}    

일대다 단방향 매핑 반대편에 다대일 단방향 매핑을 추가했다. (Member 엔티티에 @ManyToOne부분)
이때 일대다 단방향 매핑과 같은 TEAM_ID 외래 키 컬럼을 매핑했다. 이렇게 되면 둘 다 같은 키를 관리하므로 문제가 발생할 수 있어 다대일 쪽에 insertable = false, updatable = false를 설정하여 읽기만 가능하게 했다.
이 방법은 일대다 단방향 매핑이 가지는 단점을 그대로 가져가므로 될 수 있으면 다대일 양방향 매핑을 사용하자.


일대일 [1:1]

일대일 관게는 주테이블이나 대상 테이블 중 누가 외래 키를 가질지 선택해야 한다.

주 테이블 외래 키

주 객체가 대상 객체를 참조하는 것처럼 주 테이블에 외래키를 두고 대상 테이블을 참조한다. 외래 키를 객체 참조와 비슷하게 사용할 수 있어 객체지향 개발자들이 선호한다. 또한 JPA도 주 테이블에 외래 키가 있으면 좀 더 편리하게 매핑할 수 있고 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다.

단방향


@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을 사용했고 데이터베이스에는 LOCKER_ID 외래키에 유니크 제약 조건을 걸었다. 이 관계는 다대일 단방향(@ManyToOne)과 거의 유사하다.

양방향

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이다. 따라서 반대편인 Locker.member에 mappedBy 속성을 선언한다.

대상 테이블에 외래 키

단방향

일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다. 매핑할 수 있는 방법도 없다.
이 때는 단방향 관계를 Locker에서 Member에서 수정하거나, 양방향 관계로 만들고 Locker를 연관관계의 주인으로 설정해야 한다.

양방향

//일대일 대상 테이블에 외래 키, 양방향

@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 테이블의 외래 키를 관리하도록 했다.


다대다 [N:N]

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없어 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.
예를 들어 회원은 상품을 여러 개 주문할 수 있고, 상품은 회원들에 의해 주문되는 이런 관계가 다대다 관계이다.

다대다: 단방향

객체에서는 @ManyToMany를 사용하여 다대다 관계를 편리하게 매핑할 수 있다.

@Entity
public class Member {

    @Id 
    @Column(name = "MEMBER_ID")
    private Long id;

    private String username;

    @ManyToMany
    @JoinTable(name = "MEBMER_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 = "PRODJCUT_ID")
    private String id;
    
    private String name;
    ...
}

@MantyToMany와 @JoinTable을 사용해서 연결 테이블을 바로 매핑했다. 따라서 회원과 상품을 연결하는 회원_상품(Member_Product) 엔티티 없이 매핑을 완료할 수 있다.

다대다 : 양방향

역방향도 @ManyToMany를 사용하영 양방향을 매핑을 한다. 이 때 mappedBy를 사용하여 연관관계의 주인도 설정한다.

@Entity
public class Product {

    @Id @Column(name = "PRODUCT_ID")
    private String id;
    
    @ManyToMany(mappedBy = "products")  // 역방향 추가
    private List<Member> members;
    //
}    

member.getProducts().add(product);
product.getMembers().add(member);
와 같은 방법으로 다대다 양방향 연관관계를 설정한다.

양방향 연관관계는 다음과 같이 편의 메소드를 추가해서 관리하는 것이 편리하다.

public void addProduct(Product product) {
	...
    products.add(product);
    product.getMembers().add(this);
}

이러면 다음과 같이 양방향 연관관계를 설정할 수 있다.
member.addProduct(product);

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

실무에서는 연결테이블에 외래키만 존재하는 것이 아니라 다른 컬럼들도 추가로 필요하다. @ManyToMany를 통해서 연결 엔티티를 안 만들었지만 이럴 경우에는 추가한 컬럼들을 매핑할 수 없기 때문에 연결 엔티티를 사용해야 한다.


@Entity
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private Long id;

	//역방향
    @OneToMany(mappedBy = "member")
    private List<Product> products = new ArrayList<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;  //MemberProductId.product와 연결

    private int orderAmount;

    ...
}
//회원상품 식별자 클래스
public class MemberProductId implements Serializable {

    private String member;  //MemberProduct.member와 연결
    private String product; //MemberProduct.product와 연결

    @Override
    public int hashCode() {
        //
    }

    @Override
    public boolean equals(Object obj) {
        //
    }
}

회원상품(MemberProduct)엔티티를 보면 @Id와 @JoinColumn을 동시에 써서 기본 키와 외래 키를 한 번에 매핑했다. 그리고 @IdClass를 사용해서 복합 기본 키를 매핑했다. 이 엔티티는 기본 키가 MEMBER_ID와 PRODUCT_ID로 이루어진 복합키이다. 이럴 경우 JPA에서는 복합키를 사용하기 위해서 식별자 클래스를 만들어야 한다. 그리고 엔티티에 @IdClass를 사용해서 식별자 클래스를 지정하면 된다.

식별자 클래스 특징

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

식별 관계

부모 테이블의 기본 키를 받아서 자신의 기본 키 + 외래 키로 사용하는 것을 데이터베이스 용어로 식별관계(Identifying Relationship)이라고 한다.

이처럼 복합키를 사용하면 ORM매핑에서 해야할 일이 늘어난다. 식별자 클래스를 만들어야 하고 @IdClass나 @EmbeddedId도 사용해야 한다. 이를 극복하기 위해 복합 키를 사용하지 않고 간단히 다대다 관계를 구성하는 방법을 알아보자.

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

추천하는 기본 키 생성 전략은 데이터베이스에서 자동으로 생성해주는 대리 키를 Long값으로 사용하는 것이다. 간편하고 거의 영구히 쓸 수 있으며 비지니스에 의존적이지도 않다.

@Entity
public class 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;

    private int orderAmount;
    ...
}

위와 같이 새로운 기본 키를 만들고 MEMBEER_ID, PRODUCT_ID는 외래 키로만 사용한다.
이 처럼 새로운 기본 키를 생성하면 식별자 클래스를 사용하지 않아서 코드가 한결 단순해진다.

다대다 연관관계 정리

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

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

객체 입장에서는 비식별 관계를 사용하는 것이 복합 키를 위한 식별자 클래스를 만들지 안혹 편리하게 ORM 매핑을 할 수 있어 식별관계보다는 비식별 관계를 추천한다.

profile
코드 위에서 춤추고 싶어요

0개의 댓글