JPA

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

수수한개발자 2022. 6. 18.
728x90

연관관계 매핑시 고려사항 3가지

  • 다중성
  • 단방향, 양방향
  • 연관관계의 주인

다중성

  • 다대일 : @ManyToOne
  • 일대다 : @OneToMany
  • 일대일 : @OneToOne
  • 다대다 : @ManyToMany

JPA에서 다중성을 위해 제공하는 어노테이션들이다.

처음에는 헷갈릴수도 있는데 전부다 DB에 매핑하기위해 제공하는것이므로 데이터베이스의 관점에서 생각하면 될것같다.

 

단방향, 양방향

 테이블

• 외래 키 하나로 양쪽 조인 가능

• 사실 방향이라는 개념이 없음

객체

• 참조용 필드가 있는 쪽으로만 참조 가능

• 한쪽만 참조하면 단방향

• 양쪽이 서로 참조하면 양방향

 

연관관계 주인

• 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺음

• 객체 양방향 관계는 A->B, B->A 처럼 참조가 2군데

• 객체 양방향 관계는 참조가 2군데 있음. 둘중 테이블의 외래 키 를 관리할 곳을 지정해야함

• 연관관계의 주인: 외래 키를 관리하는 참조

• 주인의 반대편: 외래 키에 영향을 주지 않음, 단순 조회만 가능

 

다대일

다대일 단방향 [N:1]

N쪽에 해당하는 Member가 연관 관계의 주인으로 정해주면 된다. 항상 N쪽에 외래키가 가야한다.

가장 많이 사용하는 연관관계이다.

다대일의 반대는 일대다 밑에서 설명한다.

 

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

 

  • 양방향은 외래키가 있는 쪽이 연관관계의 주인이다.
  • 양방향 연관관계는 항상 서로를 참조해야 한다.

중요!!Team객체에 List members로 연관관계로 맺어도 테이블에 영향을 전혀 안준다.mappedBy="team" 속성은 읽기전용이기 때문이다. Team의 members의 주인은 Member엔티티의 team이다.

 

일대다

일대다 단방향 [1:N]

일대다 단방향 관계는 팀 엔티티가 회원 엔티티의 외래키를 관리하고있다.

보통 자신이 매핑한 테이블의 외래키를 관리하는데, 이 매핑은 반대로 팀 엔티티가 관리하고있다.

Member member = new Member();
member.setUsername("member1");
em.persist(member);

Team team = new Team();
team.setName("teamA");
//
team.getMembers().add(member);
//
em.persist(team);

위와같은 코드로 진행되는데 team.setName("teamA")는 회원엔티티를 그냥 DB에 넣으면 되는데 주석사이에있는 코드는 어떻게 할 방법이 없습니다. 테이블을 보면 TEAM_ID(FK)는 회원 테이블에 있기때문에 맨위에 3줄을 할 당시에는 team이 없어서 null인 상태였다가 주석사이의 코드가 실행되는순간 update쿼리가 한번 더 나가게 됩니다. 

그리고 항상 @JoinColumn(name="")을 써주어야 합니다. 여기서 name=""은 매핑하고자하는 컬럼이름이 써줍니다. 여기서는 팀 엔티티의 List members 위의 @JoinColumn(name="TEAM_ID") 로 매핑해줘야합니다.

사용 안할시에 @JoinTable 전략으로 사용하게 되는데 이러면 새로운 테이블이 하나 생기면서 팀테이블과 회원 테이블을 이어주는 중간테이블이 생긴다.

 

단점

  • 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다.
  • INSERT SQL 외에 UPDATE SQL이 추가로 발생한다.
  • 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자

 

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

일대다 단방향 매핑 반대편에 같은 외래 키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 추가한다.

(insertable=  false, updateable = false)

@Entity
public class Team {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();

}
@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
    private Team team;

}

일대다 단방향 매핑 반대편에 다대일 단방향 매핑을 추가하였다. 이때 일대다 단방향 매핑과 같은 TEAM_ID 외래키 컬럼을 매핑했다. 둘 다 같은 키를 관리하면 문제가 발생할 수 있으므로 다대일 쪽은 읽기 전용으로 만들었다. 이 방법은 사용하지 않는 것을 권장한다.

 

일대일[1:1]

일대일 관계는 양쪽이 서로 하나의 관계만 갖는다. 테이블 관계에서 일대다, 다대일은 항상 N쪽이 외래 키를 가지지만, 일대일 관계는 주 테이블이나 대상 테이블 중 어느 곳이나 외래 키를 가질 수 있다.

  • 주 테이블에 외래 키
    • 주 객체가 대상 객체를 참조하는 것처럼 주 테이블에 외래키를 두고 대상 테이블을 참조한다.
    • 객체 지향 개발자들이 선호하는 방식이다.
    • 장점 : 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인가능
    • 단점 : 값이 없으면 외래 키에 null 허용
  • 대상 테이블에 외래 키
    • 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있는 경우가 많다. 가령, Member가 Locker를 하나씩 가질 수 있는 규칙이 바뀌어서 Lock를 여러 개 가질 수 있도록 만들 수 있다. 이때 Lock이 외래 키를 관리하면 규칙이 변경되어도 테이블 구조는 바뀌지 않는다.
    • 전통적인 데이터베이스 개발자들이 선호하는 방식이다.
    • 장점 : 주테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
    • 단점 : 프록시 기능의 한계로 지연로딩으로 설정해도 항상 즉시 로딩됨(프록시는 뒤에서 설명함)

주 테이블에 외래키

단방향

일단 비즈니스 룰이 있어야한다. 멤버는 하나의 락커만 사용할 수 있고 락커도 하나의 멤버만 가질수있다고 가정할때의 예제이다.

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

}

데이터베이스에는 LOCKER_ID 외래 키에 대해 유니크 제약 조건을 추가해야 한다. 물론 JPA 단에서 DDL을 수행할 때 자동으로 유니크 제약 조건을 걸어 주는 방법도 있다. @JoinColumn의 속성으로 unique 옵션을 true로 활성화하거나 @Table의 속성으로 unique 제약 조건을 추가해 주면 된다. unique 옵션이 여러 개 추가될 수 있으므로 후자의 방법을 추천한다.

 

양방향

@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로 정했고, Locker는 mappedBy 를 이용하여 Locker.member 가 연관 관계의 주인이 아니라고 설정하였다.

 

대상 테이블에 외래키

단방향

일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 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;

}

참고로 위와 같이 설계하면 Locker.member 는 지연 로딩이 가능하지만, Member.locker 은 지연 로딩이 불가능하다. 이 개념은 추후 프록시와 지연 로딩 파트에서 알아 보자.

 

다대다[N:N]

데이터베이스 

객체 

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없고, 중간에 연결 테이블을 추가해야 한다. 하지만 객체는 객체2개는 컬렉션을 사용해서 다대다 관계를 만들 수 있다. 여기서 패러다임 불일치를 @ManyToMany로 해결할 수 있다.

 

다대다 : 단방향

@Entity
public class Member {

    @Id
    @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 ArrayLit<>();

}
@Entity
public class Product {

    @id
    @Column(name = "PRODUCT_ID")
    private String id;

    private String name;

}

@JoinTable 속성

  • name
    • 연결 테이블을 지정한다.
  • joinColumns
    • 현재 방향인 회원과 매핑할 조인 컬럼 정보를 지정한다.
  • inverseJoinColumns
    • 반대 방향인 상품과 매핑할 조인 컬럼 정보를 지정한다.

다대다 : 양방향

Member 코드는 그대로 두고, Product 코드를 수정한다.

@Entity
public class Product {

    @id
    @Column(name = "PRODUCT_ID")
    private String id;

    private String name;

    @ManyToMany(mappedBy = "products")
    private List<Member> members;

}

 

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

@ManyToMany를 사용하면 연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지고 여러 가지로 편하다. 하지만 회원이 상품을 주문하면 연결 테이블에 단순히 주문한 회원 아이디와 상품 아이디만 담고 끝나지 않는다. 보통은 연결 테이블에 주문 수량 컬럼이나 주문한 날짜 같은 컬럼이 더 필요하다.

이렇게 새로운 컬럼이 필요하면 @ManyToMany를 사용할 수 없다. 그래서 연결 테이블을 매핑하는 연결 엔티티를 만들고 1:N, N:1 관계로 설계하는 것이 바람직하다.

@Entity
public class Member {

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

    private String username;

    @OneToMany(mappedBy = "member")
    private List<MemberProduct> products = new ArrayLit<>();

}
@Entity
public class Product {

    @id
    @Column(name = "PRODUCT_ID")
    private String id;

    private String name;

}

회원 엔티티는 회원 상품 엔티티와 연관 관계를 맺어 주고, 상품 엔티티와의 관계는 해제하였다.

 

@Entity
@IdClass(MemeberProductId.class)
public class MemberProduct {

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

    @Id
    @ManyOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;

    private int orderAmount;

}

public class MemberProductId implements Serializable {

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

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

    // hashCode and equals 재정의

}

회원 상품 엔티티를 보면 기본 키를 매핑하는 @Id와 외래 키를 매핑하는 @JoinColumn을 동시에 사용하여 기본 키와 외래 키를 한 번에 매핑했다. 또한, @IdClass를 사용해서 복합 기본 키를 매핑했다.

복합 기본 키

회원상품 엔티티는 기본 키가 2개 존재하는 복합 키 형태이다. 이를 구현하려면 @IdClass를 사용해서 식별자 클래스를 지정할 수 있다. 자세한 식별자 클래스 활용법은 추후 설명한다.

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

권장하는 기본 키 생성 전략은 데이터베이스에 위임하는 것이다. 위와 같이 회원 엔티티와 상품 엔티티에 대해 대리 키를 만들고, 주문 (위에서 보았던 회원 상품) 엔티티도 대리 키를 만들어 주면 된다. 회원 엔티티와 상품 엔티티는 기존 코드에서 ID만 대리 키 방식으로 변경하면 되니, 주문 엔티티만 살펴 보자.

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

}

대리 키를 사용함으로써 복합 키 방식보다 훨씬 간단해졌다. 이처럼 새로운 기본 키를 할당해서 다대다 관계를 풀어내는 것이 좋다.

 

결국엔 다대다는 연결 테이블용 엔티티를 추가(연결 테이블을 엔티티로 승격)

@ManyToMany -> @OneToMany, @ManyToOne

다대다를 사용하지말고 일대다 다대일 관계로 정리를 해야 한다.

 

출처

김영한 - 자바 ORM 표준 JPA 프로그래밍

728x90

댓글