Spring/자바 ORM 표준 JPA 프로그래밍

[JPA 기본편] 6. 다양한 연관관계 매핑

kyung.Kh 2024. 5. 19. 03:26

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

1. 다중성

  • 다대일(@ManyToOne) : 주인 쪽에 표기
  • 일대다(@OneToMany) : 주인이 아닌 쪽에 표기. 일쪽에서 다를 조회하는 로직 필요 시 매핑(양방향 매핑)
  • 일대일(@OneToOne)
  • 다대다(@ManyToMany) : 사용하지 말 것

2. 단방향, 양방향

  • 테이블 : 방향이라는 개념이 없으며, 외래 키 하나로 양쪽에서 조인 가능
  • 객체 : 연관관계 매핑을 객체가 참조하는 방식으로 하여, 참조용 필드가 있는 쪽으로만 참조 가능
    • 한쪽 필드만 참조하면 단방향, 양쪽이 서로 참조하면 양방향
    • 연관관계의 주인을 결정해야 함

3. 연관관계의 주인

  • 객체의 양방향 관계는 참조가 2군데 있어서 둘중 테이블의 외래 키를 관리할 곳을 지정해야 함
  • 연관관계의 주인은 외래 키를 관리하는 쪽이고, 주인의 반대편은 외래 키에 영향을 주지 않아야 하고 단순 조회만 가능해야 한다.

다대일[N:1]

  • 다대일 단방향[N:1] : 주인인 '다'쪽에서는 '일'쪽을 참조 가능하나 반대는 불가능. 가장 많이 사용하는 연관관계
  • 다대일 양방향[N:1, 1:N] : 서로 참조하게 되며, 주인이 아닌 쪽에서는 조회가 가능해짐. 양방향 관계를 위해 객체에 반대의 경우를 추가해도 테이블에는 영향을 미치지 않는다.

다대일 관계 매핑 - 주요 속성

@Entity
@Getter
@Setter
public class Member {

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

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne  // '다'쪽이 연관관계의 주인
    @JoinColumn(name = "TEAM_ID")  // '다'쪽에서 걸어줌
    private Team team;
}

@Entity
@Getter
@Setter
public class Team {

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

    @OneToMany(mappedBy = "team")  // Member와 Team의 관계가 Team의 입장에서 일대다 관계...
    private List<Member> members = new ArrayList<>();  // mappedBy로 나의 반대편은 team과 걸려있다는 것을 명시
}

일대다[1:N]

  • 일대다 단방향[1:N] : '일'이 연관관계의 주인. 테이블의 일대다 관계는 항상 '다'쪽에 외래 키가 있음. @JoinCalumn을 꼭 사용해야 함.(그렇지 않으면 중간에 테이블이 하나 추가됨...)
  • 일대다 양방향[1:N, N:1] : 

'일'쪽에서 외래 키를 관리(주인)하겠다면 헷갈리기 때문에 추천하지 않음

다대일 관계에서 주인은 항상 '다'쪽에 있으므로 다대일 양방향 사용하는 것을 추천함.

즉, '일'쪽에서 '다'쪽을 조회할 필요가 있다면 설계가 깔끔하지 않게 되더라도 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자.

일대다 관계 매핑 - 주요 속성 (다대일과 비교해보면 여기에만 mappedBy가 있다)

// 일대다 양방향 매핑
@Entity
@Getter
@Setter
public class Member {

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

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne  // 주인이 아님
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)  // 읽기 전용
    private Team team;  // insertable=false, updateable=false를 기입함으로써 연관관계의 주인이 아님을 명시
}

@Entity
@Getter
@Setter
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<>();  // mappedBy로 나의 반대편은 team과 걸려있다는 것을 명시
}

일대일[1:1]

: 일대일 관계는 그 반대도 일대일이며, 주 테이블이나 대상 테이블 중, 어디에나 외래 키를 넣어도 된다.

: 외래키에 데이터베이스 유니크(UNI) 제약조건 추가

  • 주 테이블에 외래 키 : 객체지향 개발자가 선호하는 방법으로 주 테이블에 외래 키를 두고 대상 테이블을 찾는 방식이다. 주 객체가 대상 객체의 참조를 가지는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾는다. 다대일 양방향 매핑처럼 일대일 양방향에서는 외래 키가 있는 곳이 연관관계의 주인이며, 반대편은 mappedBy를 적용한다.
  • 대상 테이블에 외래 키 : 전통적인 DB 개발자가 선호하는 방법으로 대상 테이블에 외래키가 존재하는 방식이다.  주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경 시, 테이블 구조를 유지할 수 있다. 그러나 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩이 된다.
// 일대일 양방향과 단방향(12-13라인 없을 경우)
@Entity
@Getter @Setter
public class Locker {

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

	// 이 부분이 추가되면 양방향
    @OneToOne(mappedBy = "locker")
    private Member member;
}

@Entity
@Getter
@Setter
public class Member {

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

    ...
    
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")  // 주인
    private Locker locker;
}

다대다[N:M]

: 객체의 경우 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하다.

하지만 테이블의 경우 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 따라서 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 한다.(연결 테이블을 사용하여 @ManyToMany -> @OneToMany, @ManyToOne 두 개 사용)

사용하면 안되는 다대다 매핑

// 사용하면 안되는 다대다 매핑
@Entity
@Getter @Setter
public class Product {

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

    @ManyToMany(mappedBy = "products")
    private List<Member> members = new ArrayList<>();  // 이게 없으면 단방향 매핑
}

@Entity
@Getter @Setter
public class Member {

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

    ...

    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT")  // 여기서 중간 테이블 이름을 지정
    private List<Product> products = new ArrayList<>();
}

편리해 보이지만 실무에서 사용하면 안된다. 연결 테이블이 단순히 연결만 하고 끝나지 않으며 위의 예시와 달리 MEMBER_ID, PRODUCT_ID 외의 다른 것들도 많이 포함될 것이기 때문이다.

추천하는 다대다 매핑

두 번째 그림처럼 연결 테이블용 엔티티를 추가하고 @ManyToMany를 @OneToMany와 @ManyToOne으로 풀어낸다.

@Entity
@Getter @Setter
public class Member {

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

    @OneToMany(mappedBy = "member")  // 분리
    private List<MemberProduct> memberProducts = new ArrayList<>();
}

@Entity
@Getter @Setter
public class Product {

    @Id @GeneratedValue
    private Long id;

    ...

    @OneToMany(mappedBy = "product")  // 분리
    private List<MemberProduct> memberProducts = new ArrayList<>();
}

@Entity
@Getter @Setter
public class MemberProduct {  // 연결 테이블용 엔티티

    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")  // 주인
    private Member member;

    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")  // 주인
    private Product product;
}

 


[출처]

https://www.inflearn.com/course/ORM-JPA-Basic -> 이 글은 김영한님의 JPA 강의 중 6을 듣고 정리한 내용입니다.

https://velog.io/@sooyoungh/%EC%97%AC%EB%9F%AC%EA%B0%80%EC%A7%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90

728x90