JPA(Java Persistence API) 프로그래밍 - 프록시와 연관관계 관리

u-nij·2022년 8월 17일
0

JPA 프로그래밍

목록 보기
7/10
post-thumbnail
post-custom-banner

이 글은 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 듣고 정리한 글입니다.

프록시

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

JpaMain.java

public class JpaMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member = em.find(Member.class, 1L);

            printMember(member);
            printMemberAndTeam(member);

            
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }
        emf.close();
    }

    private static void printMember(Member member) { // Member 호출
        System.out.println("username = " + member.getUsername());
    }

    private static void printMemberAndTeam(Member member) { // Member와 Team 호출
        String username = member.getUsername();
        System.out.println("username = " + username);

        Team team = member.getTeam();
        System.out.println("team = " + team);
    }
  • 경우에 따라, 호출하고 싶은 것들이 다르다.
  • printMemberAndTeam(Member)로 데이터를 한 번에 가져온 후에, member만 출력하는 것은 낭비.
  • → JPA가 지연 로딩, 프록시로 해결할 수 있다.

프록시 기초

  • em.find() vs em.getReference()
  • em.find() : DB를 통해 실제 엔티티 객체 조회
  • em.getReference() : DB 조회를 미루는 가짜(프록시) 엔티티 객체 조회 (DB에 쿼리가 안 날라가는데 객체가 조회됨)

JpaMain.java

public class JpaMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member = new Member();
            member.setUsername("hello");
            em.persist(member);

            em.flush();
            em.clear();

            // Member findMember = em.find(Member.class, member.getId());
			Member findMember = em.getReference(Member.class, member.getId()); // 💡
          	System.out.println("findMember.id = " + findMember.getId());
			System.out.println("findMember.username = " + findMember.getUsername());

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }
        emf.close();
    }
  • getReference() 호출 시점에는 DB에 쿼리를 날리지 않음
  • getReference() 호출 결과 값이 실제로 사용되는 시점에 DB에 쿼리를 날림
    - getId() 호출 시점에서는, 위에서 getId()를 사용함으로써 이미 값이 있기 때문에 쿼리가 날라가지 않음
    - getUsername()은 없는 데이터이기 때문에 SELECT 쿼리를 날림
  • System.out.println("findMember = " + findMember.getClass());
    • hibernate가 만든 가짜 class ⇒ 프록시 class

프록시 특징

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

프록시 객체의 초기화

Member member = em.getReference(Member.class, "id1"); // 프록시 객체를 조회
member.getName(); // 실제로 사용할 때, 초기화 과정이 일어남 (SELECT 쿼리 날림)
member.getName(); // 엔티티가 이미 있기 때문에 초기화가 필요X

  • 프록시 객체는 처음 사용할 때 한 번만 초기화
    • Proxy 내의 Member 객체를 실제 Entity의 Member 객체와 연결해줌
  • 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님.
    초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능한 것.
  • 프록시 객체는 원본 엔티티를 상속받음. 따라서 타입 체크시 주의해야 함
    => == 비교 대신 instanceof 사용 (JPA에서 타입 체크시 유용)
  System.out.println((m1 instanceof Member)); // true
  System.out.println((m2 instanceof Member)); // true

프록시가 아닌 Member와 프록시인 Member와 타입이 맞지 않기 때문

  Member m1 = em.find(Member.class, member1.getId());
  Member m2 = em.find(Member.class, member2.getId());
  System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass())); // trueMember m1 = em.find(Member.class, member1.getId());
  Member m2 = em.getReference(Member.class, member2.getId());
  System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass())); // false
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면, em.getReference()를 호출해도 실제 엔티티 반환
Member member = em.find(Member.class, member1.getId());
System.out.println("member = " + m1.getClass());

Member reference = em.getreference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass()); // 실제 엔티티 반환

// JPA에서 == 비교시 한 영속성 컨텍스트에서 가져왔거나, pk가 같으면 항상 true 반환
System.out.println("a == a" :  + (m1 == reference));

  • 프록시로 한 번 조회가 되면, em.find()에서도 프록시를 호출. (JPA에서 ==을 보장하기 위해)
Member refMember = em.getreference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass());

Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember = " + findMember.getClass());

System.out.println("refMember == findMember" :  + (refMember == findMember));

  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
    • 하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림
      (..반드시 만나게 될 예외)
public class JpaMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member1 = new Member();
            member1.setUsername("hello");
            em.persist(member1);

            em.flush();
            em.clear();

						Member refMember = em.getReference(Member.class, member1.getId());
            System.out.println("refMember = " + refMember.getClass());

						em.detach(refMember);
						// 혹은, em.close();
						// 혹은, em.clear();

						refMember.getUsername();

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace(); // 예외 출력
        } finally {
            em.close();
        }
        emf.close();
    }
}

프록시 확인

프록시 인스턴스의 초기화 여부 확인

  • PersistenceUnitUtil.isLoaded(Object entity)
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember)); // false 출력
refMember.getUsername(); // 강제 초기화
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember)); // true 출력

프록시 클래스 확인 방법

  • entity.getClass().getName() 출력 (..javasist.. or HibernateProxy…)

프록시 강제 초기화

  • org.hibernate.Hibernate.initialize(entity);
  • 참고 : JPA 표준은 강제 초기화 없음
  • 강제 호출 : member.getName()

즉시 로딩과 지연 로딩

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

  • 단순히 member 정보만 사용하는 비즈니스 로직의 경우, team을 불러올 필요 없음.

Member.java

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

JpaMain.java

public class JpaMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

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

            Member member1 = new Member();
            member1.setUsername("member1");
            member1.setTeam(team);
            em.persist(member1);

            em.flush();
            em.clear();

            Member m = em.find(Member.class, member1.getId());

            System.out.println("m = " + m.getTeam().getClass());

            System.out.println("============");
            m.getTeam().getName(); // 💡 실제 team을 사용하는 시점
            System.out.println("============");

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace(); // 예외 출력
        } finally {
            em.close();
        }
        emf.close();
    }
}

  • 실제 team을 사용하는 시점에 초기화(DB 조회)하는 것을 알 수 있다

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

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

Member.java

@Entity
public class Member extends BaseEntity {

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

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

    @ManyToOne(fetch = FetchType.EAGER) // 프록시 객체 조회
    @JoinColumn
    private Team team;
}

  • Member 조회시 항상 Team도 같이 조회
  • JPA 구현체는 가능하면 JOIN을 사용해서 SQL을 한번에 함께 조회한다.

프록시와 즉시로딩 사용시 주의할 점!

  • 가급적 지연 로딩만 사용(특히 실무에서)
  • 즉시 로딩만 적용하면 예상하지 못한 SQL이 발생한다.
    • 쿼리가 많이 나감..! → 성능 저하
  • 즉시 로딩은 JPQL에서 N(추가 쿼리)+1(최초 쿼리) 문제를 일으킨다.
    • Member 쿼리가 발생한 후, Team을 가져오기 위해 별도의 쿼리가 나간다.
       try {
            Team team = new Team();
            team.setName("teamA");
            em.persist(team);

            Member member1 = new Member();
            member1.setUsername("member1");
            member1.setTeam(team);
            em.persist(member1);

            em.flush();
            em.clear();

            List<Member> members = em.createQuery("select m from Member m", Member.class)
                    .getResultList();
			// SQL: select * from Member
			// SQL: select * from Team where TEAM_ID = XXX

            tx.commit();
        }

  • @ManyToOne, @OneToOne은 기본이 즉시 로딩
    → LAZY로 설정
  • @OneToMany, @ManyToMany는 기본이 지연 로딩

지연 로딩 활용 - 실무

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

영속성 전이: CASCADE

  • 즉시로딩, 지연로딩, 연관관계 매핑과 전혀 상관이 없음
  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속상태로 만들고 싶을 때
    • ex) 부모 엔티티를 저장할 때, 자식 엔티티도 함께 저장
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐
  • 하나의 부모가 자식들을 관리할 때는 의미 있음. (단일소유자일 때)
    • ex) 게시물과 그 게시물의 첨부파일 경로
    • 다른 곳에서도 자식들과 연관관계가 있을 경우에는 쓰면 안 됨.
    • ex) 첨부 파일을 여러 경로에서 관리할 경우

영속성 전이 : 저장

Parent.java

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

    private String name;

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

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

Child.java

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

    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

JpaMain.java

        try {
            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를 설정하면 persist하지 않아도 자동으로 영속화됨

            tx.commit();
        }

실행 결과

CASCADE의 종류

  • ALL : 모두 적용 (라이프사이클이 거의 유사할 때)
  • PERSIST : 영속 (저장할 때만 사용)
  • REMOVE : 삭제
  • MERGE : 병합
  • REFRESH : REFRESH
  • DETACH : DETACH

주의!

  • 전제 2가지가 모두 만족할 때 사용할 것!
  • 소유자가 하나일 때
  • 라이프사이클이 거의 유사할 때

고아 객체

  • 고아 객체 제거 : 부모 엔티티와 연관관게가 끊어진 자식 엔티티를 자동으로 삭제
  • orphanRemoval = true

Parent.java

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

    private String name;

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

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

JpaMain.java

        try {
            Child child1 = new Child();
            Child child2 = new Child();

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

            em.persist(parent);

            em.flush();
            em.clear();

            Parent findParent = em.find(Parent.class, parent.getId());
            findParent.getChildList().remove(0); // 고아 객체 제거

            tx.commit();
        }

실행 결과

주의!

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

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

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

    DDD(Domain Driven Design)의 Aggregate Root
    DDD에서 Entity마다 Repository를 만드는 경우가 많은데, 이럴 때 여러 Entity를 묶어 하나처럼 사용하는 경우가 많다. Aggregate는 이러한 연관 객체의 묶음이다. 이 안에 포함되어 있는 특정 Entity를 Aggregate Root라고 한다. 여러 엔티티를 묶어서 가져오는 경우가 많을 땐 개발에서 Aggregate Root에 해당하는 Entity에 대해서만 Repository를 만드는 경우가 많다.

    • Repository는 Aggregate Root만 선택하고, 나머지는 Repo를 만들지 않는 것이 더 낫다.
    • Aggregate Root를 통해 하위의 Repo의 생명 주기를 관리한다.

참고

DDD, Aggregate Root 란?

profile
삶은 달걀이다
post-custom-banner

0개의 댓글