woowacourse-study / 2022-jpa-study

🔥 우아한테크코스 4기 JPA 스터디 (22.06.13~22.07.02) 🔥
5 stars 1 forks source link

[섹션 6, 섹션 7] 토르 제출합니다. #18

Closed injoon2019 closed 2 years ago

injoon2019 commented 2 years ago

6. 다양한 연관관계 매핑

연관 관계 매핑시에는 3가지를 고려하자.

  1. 다중성
  2. 단방향, 양방향
  3. 연관관계의 주인

1. 다중성

DB설계상 다 쪽에 외래키가 가야한다. 참고로 다대일, 일대다, 일대일 등에서 처음 등장하는 글자가 연관관계의 주인이다. 즉, 이전까지는 강의에서 다쪽이 항상 주인이었지만 꼭 그렇지는 않아도 된다. 일이 주인일 수도 있다.

1. 다대일 단방향

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

Member

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

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

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

    ...
}

일대다 단방향

일이 연관관계의 주인이되는 경우다. Team과 Member가 있다면 Team이 주인이 되는 경우. 멤버 객체에서 팀을 알 필요가 없는 경우다. DB 설계상으로는 무조건 다 쪽에 외래키가 있어야 한다. 그러니 외래키가 있다고 꼭 주인이되는건 아니다! 이전 강의에서는 마치 외래키와 주인이 필요충분 조건인 것처럼 말해서 오해했다.

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

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

    ...
}

하지만 이렇게 하면 team.getMembers().add(member) 코드를 작성하면 team에 insert 할 수 없어서 Member에 업데이트가 된다. 운영이 힘들어진다.

@JoinColumn을 꼭 사용해야 한다. 그렇지 않으면 조인 테이블 방식을 사용(중간에 테이블을 하나 추가)한다. 이러면 성능상 손해를 본다.

일대다 양방향

이런 매핑은 공식적으로 존재하지 않는다. @JoinColumn(insertable=false, updatable=false)를 이용해서 읽기 전용 필드를 만들어 흉내내는 것이다.

일대일

일대일은 주 테이블에 외래키를 줘도 되고 대상 테이블에 외래 키를 줘도 된다.

일대일: 주 테이블에 외래 키 양방향

Member

@Entity
public class Member {
    ...

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
    ...
}
@Entity
public class Locker {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToOne(mappedBy = "locker")
    private Member member;
}

일대일: 대상 테이블에 외래 키 단방향

이거는 JPA에서 지원이 안된다.

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

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

어디서 외래 키를 갖고 있는게 나을까?

나중에 도메인 규치깅 변경되어 하나의 Member가 여러 개의 Locker를 사용할 수 있다면 Locker에서 유니크 제약 조건만 빼면 된다. 개발자 입장에서는 Member가 locker를 가지고 있는게 낫다 (Member가 로직상 더 많이 사용된다고 할 때)

주 테이블에 외래키를 가지는 것은 객체지향 개발자가 선호한다. 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인이 가능하다. 단점은 값이 없으면 외래 키에 null을 허용해야 한다.

대상 테이블에 외래키를 가지는 것은 전통적인 DB 개발자가 선호한다. 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조를 유지할 수 있다. 단점은 지연 로딩으로 설정을 해도 항상 즉시 로딩된다.

다대다

결론부터 말하면 사용하면 안된다.

다대다를 풀어내기 위해 연결 테이블이 생긴다. 하지만 연결테이블에 주문시간, 수량 같은 데이터를 담아야할 수도 있는데 이렇게 하면 담을 수 없다.

이런 한계를 극복하기 위해 연결 에비을을 엔티티로 승격하면 극복이 가능하다.

2. 단방향, 양방향

두 객체에서 한쪽만 참조하면 단방향이고 양쪽이 서로 참조하면 양방향이다. 참조용 필드가 있는 쪽으로만 참조 가능하다.

3. 연관관계의 주인

객체 양방향은 A->B, B->A처럼 참조가 2군데다. 둘중 테이블의 외래 키를 관리할 곳을 지정해야 한다. 연관관계의 주인은 외래 키를 관리하는 참조를 가진다.

7. 고급 매핑

상속관계 매핑

객체는 명확한 상속 관계가 있다. 이것을 DB에서 구현하는 세 가지 방법이 있다.

Item이라는 상위 클래스를 ALBUM, MOVIE, BOOK이 상속했다고 생각해보자.

조인 전략

ALBUM에 인서트를 하면 ITEM과 ALBUM에 각각 insert하고, 조회를 할 때는 JOIN으로 조회해서 가져오는 방식이다.

테이블 정규화, 외래키 참조 무결성 제약조건 활용 가능, 저장공간 효율화 등이 장점이다. 조회시 조인을 많이 사용하므로 성능이 떨어질 수 있고 복잡할 수 있는게 단점이다. 데이터 저장시에 INSERT SQL이 두 번 나간다. 하지만 성능에 크게 영향은 없다.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public abstract class Item {

    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private int price;
}

@DiscriminatorColumn을 넣어주면 DTYPE이라는 필드가 생기는데, 이것이 있어야 쿼리가 뭐 때문에 들어왔는지 구분이 되기 때문에 해주는게 좋다.

@Entity
public class Album extends Item{

    private String artist;
}

@Entity
public class Movie extends Item{

    private String director;
    private String actor;
}

@Entity
public class Book extends Item{

    private String author;
    private String jsbn;
}

단일 테이블 전략

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public abstract class Item {

    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private int price;
    ...
}

하나의 테이블에 모든 자식 클래스의 필드까지 다 들어간다.

조인이 필요 없으므로 조회가 빠른게 장점이다. 단점은 자식 엔티티가 매핑한 컬럼에 null을 허용해주고, 단일 테이블에 저장하므로 테이블이 커진다.

단일테이블에서는 DTYPE이 필수다 (안넣어도 저절로 생긴다). 어느 타입인지 알아야하기 때문.

간단한 상황에서는 단일테이블 전략도 종종 쓴다.

구현 클래스마다 테이블 전략

상위 클래스 테이블을 없애고 각 속성을 다 하위에 내린 것이다. 사실 쓰면 안된다.

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@DiscriminatorColumn
public abstract class Item {

    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private int price;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}

이 전략은 부모 타입으로 조회하면 전 테이블을 다 찾아본다 (union). 비효율적

Mapped Superclass

테이블은 전혀 다른데 객체 입장에서 공통 속성만 묶는 것이다. 상속과 전혀 관련이 없다. 실제로 DB를 운영하다보면 누가, 몇시에 변경/등록했다 이런 것은 기본적으로 가져가는 필드다.

@MappedSuperclass
public class BaseEntity {

    private String createdBy;
    private LocalDateTime createdDate;
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;
    ...
}
@Entity
public class Member extends BaseEntity {
   ...
}

BaseEntity 테이블이 실제로 생기는 것이 아니다.

injoon2019 commented 2 years ago

생각정리

이전 강의에서 '다'쪽에 FK가 있고, 다가 주인이 되는 것이 필요충분 조건인 것처럼 이해를 했다. 이 부분이 이번 섹션에서 헷갈려서 정리를 한다.

  1. '다' 쪽에 FK가 있어야 한다 -> O. 그렇지 않으면 DB에 데이터 중복이 발생한다.

  2. '다'와 연관관계 주인은 필요충분 -> X. 일대다 관계를 보면 일이 주인이다.

  3. FK와 연관관계 주인은 필요충분 -> X. 일대다를 보면 다가 주인이 아닌데 FK를 관리한다.

  4. mappedBy가 있으면 연관관계의 주인이 아니다 -> O

  5. JoinColumn이 붙어 있다고 FK가 있는 것은 아니다. -> O. 일대다 단방향 예시를 보면 Member에 FK가 있지만 @JoinColumn은 Team에 걸려있다. 그러면 @JoinColumn의 정확한 역할? (6번과 연계)

  6. @JoinColumn(name = "x")가 있는 곳은 연관관계 주인이다 -> (?). 예제코드들 보고 귀납적으로 결론. 확실 X.

  7. 그러면 질문. @JoinColumn은 생략이 가능하다. (조인테이블 방식 사용) 일대다 단방향 관계 예시를 다시 생각해보자.

Team

@Entity
public class Team {

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

    @OneToMany
    private List<Member> members = new ArrayList<>();

    ...
}

Member

@Entity
public class Member {

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

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

    ...
}

여기서 @JoinColumn을 Team에서 생략한 상황이라고 하면 이때 연관관계 주인을 알기 위해서는 Team에는 @OneToMany가 있지만 Member에는 아무것도 없으니 그걸로 알 수 있는 것?

awesomeo184 commented 2 years ago

제가 스터디때 기깔나게 설명해드리겠습니다.

injoon2019 commented 2 years ago

단방향에서는 애초에 주인 관계를 생각할 필요가 없다. 참조를 가지고 있는 쪽에서 데이터를 넣으면 된다.

injoon2019 commented 2 years ago

참고 영상 1: https://www.youtube.com/watch?v=brE0tYOV9jQ&t=241s 참고 영상 2: https://www.youtube.com/watch?v=hsSc5epPXDs

injoon2019 commented 2 years ago

질문. @JoinColumn(name)에 오는 값은 테이블상의 FK인가?