[인프런 김영한] JPA 연관관계 - 다양한 연관관계 (N:1, 1:N, 1:1, N:M)

2021. 2. 5. 16:58프로그래밍 언어/Spring Framework

[인프런 김영한] JPA 연관관계 - 다양한 연관관계 (N:1, 1:N, 1:1, N:M)


해당 글은 인프런 김영한강사님의 영상을 보고 정리한 글입니다.

Spring Boot, Spring Data JPA를 사용해 실습하였습니다.

김영한 인프런 : www.inflearn.com/users/@yh

 

인프런 - 김영한의 강의들을 만나보세요.

우아한형제들 개발 팀장 (전: 카카오, SK플래닛) 저서: 자바 ORM 표준 JPA 프로그래밍

www.inflearn.com


▣ Goal

 

1. 연관관계 매핑시 고려할 사항

2. 다대일[N:1] (단방향, 양방향)

3. 일대다[1:N] (단방향, 양방향)

4. 일대일[1:1] (단방향, 양방향)

5. 다대다[N:N] (단방향, 양방향)

 


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

1. 다중성

  | 데이터베이스의 관점으로 다중성을 생각하면 된다.

 - @ManyToOne

 - @OneToMany

 - @OneToOne

 - @ManyToMany -> 실무에서 사용 X

 

2. 단방향, 양방향

 * 테이블

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

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

 

 * 객체

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

 - 한쪽만 참조하면 단방향, 양쪽이 서로 참조(단방향(->) + (<-)단방향)하면 양방향

 

 

3. 연관관계의 주인

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

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

 * 연관관계의 주인 : 외래키를 관리하는 잠조

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

 


lombok을 사용하였으며

@Entity, @Getter, @Setter는 포함하지 않았습니다.

▣ 다대일[N:1] - 단방향

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;
}
public class Team {

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

    private String name;

( N : 1 )

* N 쪽에 외래키(Team team)가 있도록 설계해야 한다.

 

 

 

▣ 다대일[N:1] - 양방향

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;
}
public class Team {

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

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

( N : 1 )

 

* Team에 member를 참조하고 있지만 테이블에는 전혀 영향이 없다.

* 외래키가 있는 쪽이 연관관계의 주인(Member)

* 양쪽을 서로 참조하도록 개발

 


 

▣ 일대다[1:N] - 단방향

 

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<>();

}
public class Member {

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

    @Column(name = "USERNAME")
    private String username;
}
// 실행문
Member member = new Member();
member.setUsername("member1");
memberRepository.save(member);

Team team = new Team();
team.setName("teamA");
team.getMembers().add(member); // ???
teamRepository.save(team);

( 1 : N )

 

* 객체 연관관계에서는 Team이 주인이 될 수 있다.

하지만 결국에는 DB관점에서 봐야 하는데 테이블 연관관계에서의 FK는 Member에 외래키가 들어갈수밖에 없다.

 

* 실행은 된다. DB를 확인하면 정상적으로 데이터가 들어온것을 확인할 수도 있다.

하지만 아래 Update쿼리가 추가적으로 실행이 되었다. -> 성능상의 단점

update
    member 
set
    team_id=? 
where
    member_id=?

'team.getMembers().add(member);'

결국 Team에서 members가 바뀌면 DB의 Member에 업데이트 쿼리가 실행이 된다.

 

Team Entity에서 Members를 참조 한다. 하지만 테이블 관계로 보았을 때Member 테이블에 FK가 있기 때문에 업데이트 쿼리가 나가는 것이다.-> Entity가 관리하는 외래키가 다른 테이블에 있다.

 

* @JoinColumn를 적지 않으면 조인 테이블 방식을 사용하며, 중간에 테이블이 생성이 된다.테이블이 하나 더 추가되고 신경써야 하기 때문에 1:N을 사용한다면 꼭 넣어주자.

 

반대로 조회할 일이 없다고 해도, 가급적 다대일[N:1] 양방향을 사용하자

 

 

 

▣ 일대다[1:N] - 양방향

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<>();

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

}

( 1 : N )

* 일대다 양방향은 공식적으로 존재하지 않는다.* 읽기 전용 필드를 사용해서 양방향처럼 사용하는 방법이며 권장하지 않는다.

 

* insertable = false, updatable = false = false로 하면 읽기 전용이 된다. 참조 Entity를 읽기 전용으로 매핑한다.- insertable = false : 저장시 해당 필드도 함께 저장되며, false로 설정하면 DB에 저장하지 않는다.- updatable = false : 업데이트시 해당 필드도 함께 업데이트되며, false로 설정하면 DB에 업데이트하지 않는다.

 


▣ 일대일[1:1] - 주 테이블에 외래키 단방향※ 주 테이블 : 많이 Access하는 테이블을 지정

public class Locker {

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

    private String name;
}
// 주 테이블
public class Member {


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

    @Column(name = "USERNAME")
    private String username;
   
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;

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

}

( 1 : 1 )

* 주 테이블이나 대상 테이블 중에 외래키 선택 가능* 외래키에 데이터베이스 유니크 제약조건 추가

 

    create table member (
       member_id bigint not null,
        username varchar(255),
        locker_id bigint,
        team_id bigint,
        primary key (member_id)
    )

| 실행해 보면 member Table에 locker가 들어가 있는것을 확인할 수 있다.

 

 

 

 

▣ 일대일[1:1] - 주 테이블에 외래키 양방향

public class Locker {

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

    private String name;

    // 읽기 전용
    @OneToOne(mappedBy = "locker")
    private Member member;
}
// 주 테이블
public class Member {


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

    @Column(name = "USERNAME")
    private String username;
   
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;

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

}

( 1 : 1 )

 

* 다대일 양방향 매핑처럼 외래키가 있는 곳이 연관관계의 주인


▣ 일대일[1:1] - 대상 테이블에 외래키 단방향

* 단방향 관계는 JPA에서 지원하지 않는다.

 

 

▣ 일대일[1:1] - 대상 테이블에 외래키 양방향

public class Locker {

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

    private String name;
}
// 주 테이블
public class Member {


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

    @Column(name = "USERNAME")
    private String username;
   
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;

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

}

( 1 : 1 )

* 일대일[1:1] 주 테이블에 외래키 양방향과 매핑 방법은 같다.

 


◈ 일대일 관계에서 외래키를 어디에 넣어야 할까 (Member와 Table 예시)

DBA 관점 | Locker에 FK를 넣는게 차후 변경에 용이하다.* 현재는 1명의 Member가 1개의 Locker를 가질 수 있는데 차후

1명의 Member가 N개의 Locker를 가질 수 있다고 변경할 때 테이블 구조를 변경할 필요가 없다.

단점 : 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨

개발자 관점 | Member에 FK를 넣는게 성능상 좋다. , JPA 매핑이 편하다.

* Member 테이블을 조회할 일이 많다면 Locker 값도 함께 가지고 온다. 조인이 필요가 없고 조인도 필요가 없다.

주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인이 가능하다.

단점 : 값이 없드면 외래키에 null을 허용하게 된다.

 


* 다대다는 기본적으로 실무에서 사용 하지 않기를 권장하는 연관관계이다. *

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없기 때문에 

연결 테이블을 중간에 추가해서 다대일 관계로 풀어내는게 바람직한 해결 방법이다.

 

▣ 다대다[N:M] - 단방향

public class Member {

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

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


    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;


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

    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT")
    private List<Product> products = new ArrayList<>();
}
public class Product {

    @Id
    @GeneratedValue
    private Long id;

    private String name;


}
Hibernate: 
    
    create table member (
       member_id bigint not null,
        username varchar(255),
        locker_id bigint,
        team_id bigint,
        primary key (member_id)
    )

Hibernate: 
    
    create table member_product (
       member_member_id bigint not null,
        products_id bigint not null
    )

Hibernate: 
    
    create table product (
       id bigint not null,
        name varchar(255),
        primary key (id)
    )

| @ManyToMany 어노테이션을 지정, JoinTable에 생성될 테이블 이름을 적어주면 다대다 단방향 구조가 된다.

실제 쿼리문도 create table member_product를 자동으로 생성하게 됨.

 

 

 

▣ 다대다[N:M] - 양방향

public class Member {

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

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


    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;


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

    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT")
    private List<Product> products = new ArrayList<>();
}
public class Product {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

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

}

| 다대다 양방향일경우 Product 쪽에 @ManyToMany 어노테이션에 mappedBy를 적어주면

다대다 양방향이 완성된다.

 

 

 

◈ 다대다 매핑의 한계와 극복

[한계]

▶ 실무에서는 사용할수가 없는데 그 이유로는

 - 연결 테이블이 단순히 연결만 하고 끝나지 않음

 - 연결 테이블에 주문시간, 수량같은 데이터가 들어올 수 있음

 - 연결 테이블로 인해 생각하지 못한 쿼리가 나갈 수 있다.

 

[극복]

▷연결 테이블용 Entity를 추가(연결 테이블을 Entity로 승격)

Member [N:M] Product -> Member [1:N] Order [N:1] Product

 

public class Member {

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

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

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

}
public class MemberProduct {

    @Id
    @GeneratedValue
    private Long id;

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

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

}
public class Product {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

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


}

| 다대다 개념을 사용하고 싶으면 기존에 자동으로 생성되었던 연결 테이블을 MemberProduct라고 클래스를 생성해서 Entity로 만들어 사용한다.

중간에 테이블이 하나 더 생기기 때문에 OneToMany와 ManyToOne으로 변경해서 사용하는게 훨씬 유연하다.