보글보글 개발일지
article thumbnail
반응형
섹션 6~8 정리

섹션 6. 다양한 연관관계 매핑

연관관계 매핑시 고려사항

  1. 다중성
    • 다대일, 일대다, 일대일, 다대다(실무에서 쓰면 안됨)
  2. 단방향, 양방향
    • 테이블: 외래 키 하나로 양쪽 조인 가능 -> 방향이라는 개념이 없음
    • 객체: 참조용 필드가 있는 쪽으로 참조 가능. 한쪽만 참조하면 단방향, 양쪽 참조시 양방향
  3. 연관관계 주인
    • 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺음
    • 객체 양방향 관계는 A->B, B->A 처럼 참조가 2군데
    • 객체 양방향 관계는 참조가 2군데 있음. 둘중 테이블의 외래키를 관리할 곳을 지정해야함
    • 연관관계의 주인: 외래 키를 관리하는 참조
    • 주인의 반대편: 외래 키에 영향을 주지 않음, 단순 조회만 가능! (읽기)

다대일 [N:1] - 가장 많이 쓴다.

  • 다대일 단방향
    • 다 쪽에 외래키가 설정되어야함. 외래키 있는 곳에 참조를 건다.

@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;

team은 뭐 없다.

  • 다대일 양방향: 외래키가 있는 쪽이 연관관계의 주인. 양쪽을 서로 참조하도록 개발

일대다 [1:N]

  • 일 있는 쪽이 연관관계 주인
  • 일대다 단방향: 권장하지 않는다. 테이블 입장에서는 무조건! 다쪽에(MEMBER) 외래키가 들어간다. 

- Team.java

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

- 객체 지향적으로 손해를 좀 보더라도 (Member에서 팀 갈 일 없더라도) Member에 Team을 추가한다. 양방향으로!

  • 일대다 단방향 정리
    • 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인
    • 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있음
    • 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조
    • @JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블방식을 사용함(중간에 테이블을 하나 추가함)
  • 일대다 단방향 매핑의 단점
    • 엔티티가 관리하는 외래 키가 다른 테이블에 있음
    • 연관관계 관리를 위해 추가로 UPDATE SQL 실행
    • 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자

-일대다 양방향: 야매로 된다. 읽기 전용 필드를 사용한다. (insertable = false, updatable = false)

--> 그냥 다대일 양방향을 쓰자!

일대일 [1:1]

  • 일대일은 반대 관계도 일대일
  • 주 테이블이나 대상 테이블 중 외래키 선택 가능
    • 주테이블에 외래키 두거나
    • 대상 테이블에 외래키
  • 외래 키에 데이터베이스 유니크 제약조건 추가

@Entity
public class Locker {
    @Id @GeneratedValue
    private Long id;

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

- 양방향 하려면 한곳은 mappedBy를 적용해주면 된다.
- 외래키 있는 곳이 연관관계 주인!

@Entity
public class Locker {
    @Id @GeneratedValue
    private Long id;

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

- 일대일: 대상 테이블에 외래키 단방향 --> 지원 ㄴㄴ 양방향은 ㅇㅇ

대상 테이블에 외래 키 단방향 -지원 ㄴ
대상 테이블에 외래 키 양방향 ㅇㅇ

  • 일대일 정리
    • 주 테이블에 외래 키
      • 주 객체가 대상 객체의 참조를 가지는 것 처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾음
      • 객체지향 개발자 선호
      • JPA 매핑 편리
      • 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
      • 단점: 값이 없으면 외래 키에 null 허용
    • 대상 테이블에 외래 키
      • 대상 테이블에 외래 키가 존재
      • 전통적인 데이터베이스 개발자 선호
      • 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
      • 단점: 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨(프록시는 뒤에서 설명) - 치명적..

더 선호~ DBA분들 싫어할 수 있다.

다대다 [N:M]

결론: 실무에서 쓰면 안된다.

관계형 데베는 정규화된 테이블 2개로 다대다 관계를 표현 불가! --> 연결 테이블 추가해 일대다, 다대일 관계로 풀어내야함.

- 연결 테이블이 단순이 연결만 하고 끝나지 않음. 주문 시간, 수량 같은 데이터가 들어올 수 있음.

  • 한계 극복
    • @ManyToMany -> @OneToMany,@ManyToOne
    • 연결 테이블용 엔티티 추가( 연결 테이블을 엔티티로 승격)

실전 예제 3 - 다양한 연관관계 매핑

- 일대일 관계: Delivery - Order

@OneToOne(mappedBy = "delivery")
private Order order;
@OneToOne
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery;

- 다대다 관계: Category - Item

@ManyToMany//하나의 카테고리 여러개 아이템 들어갈 수 있고 한 아이템도 여러 카테고리에 소속 가능
@JoinTable(name= "CATEGORY_ITEM",
joinColumns = @JoinColumn(name = "CATEGORY_ID"),//내가 조인
        inverseJoinColumns = @JoinColumn(name = "ITEM_ID")//중간쪽이 조인
)
private List<Item> items = new ArrayList<>();
@ManyToMany(mappedBy = "items")
private List<Category> categories = new ArrayList<>();

- 실전에서는 ManyToMany 쓰지말기!

- N:M은 1:N, N:1로. 중간 테이블이 단순하지 않고, 필드 추가가 불가능하며, 엔티티 테이블 불일치와 같은 제약이 있다.

-ManyToOne은 연관관계 주인이 되는 것이 보통이다.

섹션 7. 고급 매핑

상속관계 매핑

[상속관계 매핑]

  • 관계형 데베에는 상속관계 ㄴㄴ
  • 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사!
  • 상속관계 매핑: 객체의 상속과 구조과 DB의 슈퍼타입 서브타입 관계를 매핑
  • 슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 법:
    • 각각 테이블로 변환: 조인 전략
    • 통합 테이블로 변환: 단일 테이블 전략
    • 서브 타입 테이블로 변환: 구현 클래스마다 테이블 전략
  • 세가지중 어떤거 써도 좋다.

[1.  조인전략] - 이게 정석!!

조인 전략

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Item {
    @Id @GeneratedValue
    private Long id;

    private String name;
    private int price;
}
@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 isbn;
}

조회하는 경우 join해서 가져온다. 

@DiscriminatorColumn을 사용할 수 있다.

- name은 DTYPE이 기본이다!

@Entity
@DiscriminatorValue("A")
public class Album extends Item{

    private String artist;
}

- 값 지정해서 분류 가능.

  • 장점
    • 외래 키 참조 무결성 제약조건 활용가능
    • 저장공간 효율화
    • 테이블 정규화
  • 단점
    • 조회 쿼리가 복잡함
    • 데이터 저장시 INSERT SQL 2번 호출(생각보다 단점 ㄴㄴ)
    • 조회시 조인을 많이 사용, 성능 저하

[2. 단일 테이블 전략]

단일 테이블 전략- 논리 모델 하나로 다 합침.

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn

 

- @DiscriminatorColumn 없어도 DTYPE 꼭 필수로 생성이 된다.

  • 장점
    • 조인이 필요 없으므로 일반적으로 조회 성능이 빠름. 
    • 조회 쿼리가 단순함
  • 단점
    • 자식 엔티티가 매핑한 컬럼은 모두 null 허용
    • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 상황에 따라서 조회 성능이 오히려 느려질 수 있다.

 [3. 구현 클래스마다 테이블 전략] - 별로. 변경 관점에서 좋지 않다

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

@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
  • 이 전략은 데이터베이스 설계자와 ORM 전문가 둘 다 추천X
  • 장점
    • 서브 타입을 명확하게 구분해서 처리할 때 효과적
    • not null 제약조건 사용 가능
  • 단점
    • 여러 자식 테이블을 함께 조회할 때 성능이 느림(UNION SQL 필요)
    •  자식 테이블을 통합해서 쿼리하기 어려움 

Mapped Surperclass - 매핑 정보 상속

  • 상속관계 매핑과 별로 관계가 없다.
  • id, name이 계속 반복되어 나온다. 속성만 상속해서 쓰고, DB는 다 따로 쓰는것. 객체입장에서 속성만 상속해서 쓰는 것.

public class BaseEntity {
    private String createdBy;
    private LocalDateTime createdDate;
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;  
}
  •  위와 같이 BaseEntity를 만든다. getter, setter도 만든다.
public class Member extends BaseEntity {
  • extends BaseEntity! --> @MappedSuperclass를 넣어준다.
  • 아래처럼 컬럼 명도 설정 가능하다.
@Column(name = "INSERT_MEMBER")
private String createdBy;
  • 상속관계 매핑 ㄴㄴ
  • 엔티티 ㄴㄴ, 테이블과 매핑 ㄴㄴ
  • 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공
  • 직접 생성해서 사용할 일이 없으므로 추상 클래스 권장
  • 조회, 검색 불가(em.find(BaseEntity) 불가)
  • 테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할
  • 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용
  • 참고: @Entity 클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속 가능

실전 예제 4 - 상속관계 매핑

[요구사항 추가]

  • 상품의 종류는 음반, 도서, 영화가 있고 이후 더 확장될 수 있다. - 상속관계 매핑
  • 모든 데이터는 등록일과 수정일이 필수다. - MappedSuperclass

[Album.java]

package jpabook.jpashop.domain;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
public class Album extends Item{

    private String artist;
    private String etc;

    public String getArtist() {
        return artist;
    }

    public void setArtist(String artist) {
        this.artist = artist;
    }

    public String getEtc() {
        return etc;
    }

    public void setEtc(String etc) {
        this.etc = etc;
    }
}

[Book.java]

@Entity
public class Book extends Item{
    private String author;
    private String isbn;

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }
}

[Movie.java]

@Entity
public class Movie extends Item{
    private String director;
    private String actor;

    public String getDirector() {
        return director;
    }

    public void setDirector(String director) {
        this.director = director;
    }

    public String getActor() {
        return actor;
    }

    public void setActor(String actor) {
        this.actor = actor;
    }
}

[Member.java]

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public abstract class Item {
    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;
    private int stockQuantity;
}
  • 싱글 테이블 전략 사용
@MappedSuperclass
public class Member extends BaseEntity {
  • BaseEntity 상속받게 해준다. 
  • 실무에서 상속관계를 쓰는가? - 테이블 단순하게 유지하려고 상속 안쓰기도 한다. 정답은 없다. ..

섹션 8. 프록시와 연관관계 처리

프록시

[Member을 조회할 때 Team도 함께 조회해야할까?]

  • 회원만 출력하는데 팀을 조회하는건 최적화가 안된거다. -> 낭비

[프록시 기초]

  • em.getReference(): DB에 쿼리가 안나가는데 객체가 조회가 된다. 데베 조회를 미루는 가짜(프록시) 엔티티 객체 조회
  • em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
  • 프록시 안에는 껍데기는 똑같은데 안에는 텅텅 비어있음


[프록시의 특징]

  • 실제 클래스를 상속받아서 만들어짐
  • 실제 클래스와 겉모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시인지 구분하지 않아도됨. (이론상)
  • 프록시 객체는 실제 객체의 참조를 보관
  • 프록시 객체를 호출시 프록시 객체는 실제 객체의 메소드 호출
  •  

[프록시 객체의 초기화]

member.getName(); 을 호출하면 멤버에 값이 없어서 영속성 컨텍스트에 값 요청 -> 디비를 조회해서 실제 멤버 객체 가져온다. 즉 실제 엔티티를 생성한다. 타겟 있는걸 진짜 객체를 연결해줌.

* 프록시 값이 없을 때 진짜값을 달라고 영속성 컨텍스트에 요청하는 것을 초기화라고 한다.

- 두번 호출하면 처음엔 값을 요청, 두번째에는 이미 있는 값 불러옴

  • 프록시의 특징
    1. 프록시 객체는 처음 사용할 때 한 번만 초기화
    2. 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님,(중요) 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
    3. 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, 대신 instance of 사용-m1 instanceof Member)
      (특히 메소드 만들어서 할때 매개변수에 프록시가 올지 실제 객체가 올지 모른다!)
    4. 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
      Member m1 = em.find(Member.class, member1.getId()); 이후에
      Member reference = em.getReference(Member.class, member1.getId());
      를 하면? 둘다 class가 Member가 뜬다. JPA 에서는 == 비교가 한 영속성 컨텍스트에서 가져온거면 true를 반환한다.
      프록시에서 가져온 것이라도 이미 영속성에 있는거라 실제 엔티티를 반환! 즉 true가 보장됨

      Member reference1 = em.getReference(Member.class, member1.getId());
      Member reference2 = em.getReference(Member.class, member1.getId());
      이렇게 두번하면 같은 프록시가 반환된다. == 비교때 true가 보장되어야하기 때문!

      Member referenceMember = em.getReference(Member.class, member1.getId());
      Member findMember = em.find(Member.class, member1.getId()); 이후 getClass하면
      처음에는 당연히 프록시가 나온다. 두번째도 프록시로 반환된다.

    5. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
      (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림) - 실무에서 많이 나온다

위 경우 프록시 초기화 불가! em.close했을 경우에도 불가능.

[프록시 확인]

  • 프록시 인스턴스의 초기화 여부 확인
    • PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인 방법
    • entity.getClass().getName() 출력(..javasist.. or HibernateProxy…)
  • 프록시 강제 초기화
    • org.hibernate.Hibernate.initialize(entity);
  • 참고: JPA 표준은 강제 초기화 없음
    • 강제 호출: member.getName()

즉시 로딩과 지연 로딩

[지연 로딩 LAZY를 시용해서 프록시로 조회]

Member.java

@ManyToOne(fetch = FetchType.LAZY) 
@JoinColumn(name = "TEAM_ID")
private Team team;

Team은 프록시고 멤버는 아니다. 따라서

위와 같이 조회하면 Team은 프록시로 조회가 되고, 실제 팀의 속성을 사용하는 시점에 프록시 객체가 초기화 되면서 디비에서 값을 가져온다.

[Member과 Team을 자주 같이쓰면 즉시로딩 EAGER을 사용해서 함께 조회하라!]

@ManyToOne(fetch = FetchType.EAGER) //**
@JoinColumn(name = "TEAM_ID")
private Team team;

member 로딩할 때 team도 조인해서 한번에 조인한다!

[프록시와 즉시로딩 주의]

  • 가급적 지연 로딩만 사용(특히 실무에서)
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다. : JPQL을 써서 멤버를 쫙 가지고 왔더니 팀이 즉시 로딩이네 -> 팀을 가져온다. 쿼리가 두번 나간다. 처음 쿼리를 하나 날렸는데 그것 때문에 N개가 추가로 생긴다. 
    • 추가로 join fetch 하면 해결이 된다.(주로 해결)
    • 어노테이션
    • 배치 사이즈
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩-> LAZY로 설정
  • @OneToMany, @ManyToMany는 기본이 지연 로딩

[지연 로딩 활용]

이론적으로는 자주 함께 사용시 즉시로딩, 가끔 사용하면 지연로딩을 써야한다.

하지만! 실무에서는 다 지연로딩 쓰면 된다! JPQL fetch 조인이나 엔티티 그래프 기능을 사용. 즉시 로딩은 상상치 못한 쿼리가 나간다.

영속성 전이(CASCADE)와 고아 객체

부모 저장할 때 자식도 같이 저장하는 것

연관관계 관계없고 parent persist 할때 cascade 선언한거 밑에있는 애들도 다같이 persist!

Parent.java

Child.java

 

persist 를 세번해줘야한다. 

parent 만 persist해도 child까지 persist되게 하려면?

cascade 옵션을 준다!

그러면 em.persist(parent)만 해도 child까지 전부 persist가 가능하다. 

[옵션]

ALL, PERSIST 정도만 쓴다. 하나의 부모가 자식들을 관리할때 의미가 있다. 

게시판, 첨부파일.. 가능! 파일을 여러군데에서 관리하면 쓰면 안된다. 

다른데도 child랑 연관관계 있으면 쓰면 안된다.  소유자가 하나일때 사용하기! 하나에서 손댔다가 다른데도 사라지면 큰일난다. 

 

[고아 객체]

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제

orphanRemoval = true를 주고 

위와 같이 child list를 찾아서 삭제하면

delete 쿼리가 나간것을 확인할 수 있다.  컬렉션에서 빠진애는 삭제가 된다. 

하지만 함부로 쓰면 안된다! 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제
참조하는 곳이 하나일 때 사용!특정 엔티티가 개인 소유할 때 사용

 

cascade 지우고 고아 옵션만 주고 부모를 지우면 자식도 다 지워진다.

참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거기능을 활성화 하면 부모를 제거할 때 자식도 같이 제거된다. CascadeType.REMOVE랑 똑같이 동작한다. CascadeType.ALL하고 부모 지워도 자식 지워진다. 

[영속성 전이 + 고아 객체 , 생명주기]

  • CascadeType.ALL + orphanRemoval=True
  • 스스로 생명주기 관리하는 엔티티는 em.persist()로 영속화, em.remove로 제거
  • 두 옵션 모두 활성화하면 부모 엔티티를 통해 자식의 생명주기 관리 가능 :em.persist, remove할때 부모지우면 자식도 지워진다. 생명 주기는 JPA통해서 관리한다. child는 부모가 관리한다. 
  • 도메인 주도 설계의 Aggregate Root 개념을 구현할 때 유용

 

 

 

 

 

 

 

반응형
profile

보글보글 개발일지

@보글

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!