BackEnd/ORM(JPA)

프록시와 연관관계 관리

프록시

더보기

Member를 조회할 때 Team도 함께 조회해야 할까?

 

Member를 조회할 때 Team도 함께 조회해야 할까?

회원과 팀 함께 출력

private static void printMemberAndTeam(String memberId) {
    Member member = em.find(Member.class, memberId)
    Team team = member.getTeam();

    System.out.println("username = " + member.getUsername);
    System.out.println("team = " + team.getName());
}

 회원만 출력

private static void printMemberAndTeam(String memberId) {
    Member member = em.find(Member.class, memberId)
    Team team = member.getTeam();
    System.out.println("username = " + member.getUsername);
    
}
Team과 member는 연관관계가 걸려있지만 member를 em.find로 entity를 찾아올때 team에 대한 정보까지 불러오는건 성능적으로 아쉽다. 그럴경우 사

프록시 기초

  • em.find() vs em.getReference()
  • em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
  • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
// 실제 DB에 Query가 날아가지 않고 가짜 객체가 존재
Member findMember = em.getReference(Member.class, member.getId());

System.out.println("findMember = " + findMember.getClass());

// 해당 객체를 사용할때 query를 날려 가짜 객체를 진짜 객체로 바꿈
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMEmber.name = " + findMember.getUsername());

프록시 특징

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

 

프록시 객체의 초기화

Member member = em.getReference(Member.class, "id1");
member.getName();

 

프록시의 특징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, 대신 instance of 사용)
private static void logic(Member m1, Member m2) {
	System.out.println("m1 == m2" + (m1.getClass() == m2.getClass()));
    // m1이 em.find로 받아온 객체인지 getReference로 가져온 proxy객체인지 알 수 없으니
    // jpa에서 객체비교는 instanceOf 메서드로 비교를 하자.
    System.out.println("m1 == m2" + (m1 instanceOf Member)));
}
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
Member m1 = em.find(Member.class, member1.getId());
Member reference = em.getReference(Member.class, member1.getId());
-> m1 == reference

// 1. 이미 영속성 컨텍스트 1차캐시에 있음
// 2. (진짜이유) 한 영속성 컨텍스트 안에서(tx) 같은 pk로 가져온 객체는 항상 같아야 한다.
// 2번 이유 JPA가 기본적으로 제공하는 메커니즘상 규약 규칙 약속 그리 되어야만 함

Member refMember = em.getReference(Member.class, member1.getId());
Member findMember = em.find(Member.class, member1.getId());
=> proxy proxy -> 객체가 같아야 한다
// em.find로 나중에 찾은 객체가 proxy가 아닌 entity라면 다른 객체가 되므로
// proxy객체를 가져오게 된다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생 (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
Member refMember = em.getReference(Member.class, member1.getId());

em.detach(refMember)
//em.clear();
//em.close();

refMember.getUserName(); 
// persistContext의 흐름이 끊겼기때문에 proxy target entity를 제어할 수 없음

프록시 확인

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

 

즉시 로딩과 지연 로딩

더보기

Member를 조회할 때 Team도 함께 조회해야 할까?

단순히 member 정보만 사용하는 비지니스 로직

println(member.getName());

 

지연 로딩 LAZY을 사용해서 프록시로 조회

@Entity
public class Member extends BaseEntity{
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

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

 

지연 로딩

			Team team = new Team();
            team.setName("teamA");
            em.persist(team);

            Member member = new Member();
            member.setUsername("user1");
            member.setCreatedBy("kim");
            member.setTeam(team);

            em.persist(member);
            em.flush();
            em.clear();

            Member findMember = em.find(Member.class, member.getId());

 

 

Member와 Team을 자주 함께 사용한다면?

 

즉시 로딩 EAGER를 사용해서 함께 조회

@ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn
     private Team team;

즉시 로딩(EAGER), Member조회시 항상 Team도 조회

JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회

 

프록시와 즉시로딩 주의

  • 가급적 지연 로딩만 사용(특히 실무에서)
  • 즉시 로딩 적용하면 예상하지 못한 SQL이 발생
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 설정
  • @OneToMany, @ManyToMany는 기본이 지연 로딩
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
n+1

JQPL 사용시 N+1 문제 해결법

  • FetchType.LAZY 기본
  • Fetch Join 활용
    • (em.createQuery("select m from Member m join fetch m.team", Member.class)
  • Entity Graph 어노테이션 활용
  • BatchSize (1 + 1 query)

 

지연 로딩 활용

더보기
  • MemberTeam은 자주 함께 사용 -> 즉시 로딩
  • MemberOrder는 가끔 사용 -> 지연 로딩
  • OrderProduct는 자주 함께 사용 -> 즉시 로딩

지연 로딩 활용 - 실무

  • 모든 연관관계에 지연 로딩을 사용해라!
  • 실무에서 즉시 로딩을 사용하지 마라!
  • JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라! (뒤에서 설명)
  • 즉시 로딩은 상상하지 못한 쿼리가 나간다.

 

영속성 전이 : CASCADE

더보기

영속성 전이: CASCADE

  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때
  • 예) 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장

 

영속성 전이: 저장

@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)

 

영속성 전이: CASCADE - 주의!

  • 영속성 전이는 연관관계 매핑하는 것과 아무 관련이 없음
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐
  • 관련된 테이블이 다수 일때 사용하면 예상치 못한 결과 초래
  • 단일 연관관계시(종속, 같은 라이프사이클)에만 사용하자.

 

CASCADE의 종류

  • ALL: 모두 적용
  • PERSIST: 영속
  • REMOVE: 삭제
  • MERGE: 병합
  • REFRESH: REFRESH
  • DETACH: DETACH

 CASCADE 적용 전

//자식
@Entity
public class Child {

    @Id @GeneratedValue
    private Long id;

    private String name;
    @ManyToOne
    @JoinColumn(name = "parent_id")
    public Parent parent;
}
//부모
@Entity
public class Parent {

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

    @OneToMany(mappedBy = "parent")
    private List<Child> childList = new ArrayList<Child>();

    public void addChild(Child child) {
        childList.add(child);
        child.setParent(this);
    }

    // getters and setters
}
Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

em.persist(parent);
em.persist(child1);
em.persist(child2);

CASCADE 적용 후 

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<Child>();
Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

em.persist(parent);

 

고아 객체

더보기

고아 객체

  • 고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
  • orphanRemoval = true
  • Parent parent1 = em.find(Parent.class, id);
  • parent1.getChildren().remove(0); // 자식 엔티티를 컬렉션에서 제거
  • DELETE FROM CHILD WHERE ID = ?

 

고아 객체 - 주의

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
  • 참조하는 곳이 하나일 때 사용해야함!
  • 특정 엔티티가 개인 소유할 때 사용
  • @OneToOne, @OneToMany만 가능
  • 참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.

 

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

더보기

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

  • CascadeType.ALL + orphanRemovel=true
  • 스스로 생명주기를 관리하는 엔티티는 em.persis()로 영속화, em.remove()로 제거
  • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있음
  • 도에민 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용

 

실전 예제 - 5. 연관관계 관리

더보기

글로벌 페치 전략 설정

  • 모든 연관관계를 지연 로딩으로
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 지연 로딩으로 변경

 

영속성 전이 설정

  • Order -> Delivery를 영속성 전이 ALL 설정
  • Order -> OrderItem을 영속성 전이 ALL 설정

@ManyToOne and @OneToOne -> Fetch Lazy로 전환

Order -> Delivery cascade = CascadeType.ALL로 영속성 전이시킴

example) Order Class Code

@Entity
@Table(name = "ORDERS")
public class Order extends BaseEntity {

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

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

    @OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "DELIVERY_ID")
    private Delivery delivery;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();
    
	// constructor , getter/setters
}

 

 

 


References by

 

 

추후에 DDD와 aggregate Root정리하기

 

DDD란?

 

aggregate Root란?

'BackEnd > ORM(JPA)' 카테고리의 다른 글

객체지향 쿼리 언어(JPQL)  (0) 2023.01.09
값 타입  (0) 2023.01.09
고급 매핑  (0) 2023.01.03
다양한 연관관계 매핑  (0) 2022.12.31
연관관계 매핑 기초  (0) 2022.12.30