본문 바로가기

JPA

[JPA] JPA 프록시와 연관관계 관리

 

 

 

프록시(Proxy)는 JPA와 Hibernate에서 자주 사용되는 개념으로, 실제 엔티티 객체를 대리하여 동작하는 가짜 객체입니다. 프록시를 통해 데이터베이스 조회를 지연시키고, 성능을 최적화할 수 있습니다.

 

프록시의 주요 특징

  1. 데이터베이스 조회 지연:
    • em.find() 메소드는 데이터베이스를 즉시 조회하여 실제 엔티티 객체를 반환합니다.
    • em.getReference() 메소드는 데이터베이스 조회를 미루고, 프록시 객체를 반환합니다. 실제 데이터는 프록시 객체의 메소드가 처음 호출될 때 조회됩니다.
  2. 프록시 객체의 구조:
    • 프록시 객체는 실제 엔티티 클래스를 상속받아 만들어집니다.
    • 프록시 객체는 실제 엔티티 객체를 참조하는 방식으로 동작합니다.
  3. 프록시 객체의 초기화:
    • 프록시 객체는 처음 사용할 때 한 번만 초기화됩니다.
    • 초기화 시 프록시 객체는 실제 엔티티 객체의 데이터에 접근할 수 있게 됩니다.
  4. 프록시와 타입 비교:
    • 프록시 객체는 실제 엔티티 객체와 동일한 인터페이스를 구현하므로, 대부분의 경우 프록시 객체와 실제 객체를 구분할 필요가 없습니다.
    • 그러나 타입 비교를 할 때 == 연산자는 실패할 수 있으므로, instanceof 연산자를 사용해야 합니다.
  5. 영속성 컨텍스트와 프록시:
    • 영속성 컨텍스트에 이미 로드된 엔티티가 있다면, em.getReference()는 프록시가 아닌 실제 엔티티 객체를 반환합니다.
    • 준영속 상태에서 프록시를 초기화하면 org.hibernate.LazyInitializationException 예외가 발생할 수 있습니다.

프록시의 장단점

장점:

  • 데이터베이스 조회를 지연시켜 성능을 최적화할 수 있습니다.
  • 초기에는 프록시 객체만 메모리에 로드되므로 메모리 사용량을 줄일 수 있습니다.

단점:

  • 프록시 초기화 시 성능 저하가 발생할 수 있습니다.
  • 잘못된 사용으로 인해 LazyInitializationException 예외가 발생할 수 있습니다.
  • 프록시 객체와 실제 객체를 혼동할 수 있으므로, 코드 작성 시 주의가 필요합니다.

 

연관관계에서 fetch 전략을 LAZY로 설정하면, 해당 연관된 엔티티는 프록시 객체로 조회됩니다. 이는 성능 최적화와 메모리 사용량 감소를 위해 자주 사용되는 방법입니다. 기본적으로 JPA에서는 @ManyToOne과 @OneToOne 연관관계는 EAGER로, @OneToMany와 @ManyToMany 연관관계는 LAZY로 설정됩니다.

LAZY 로딩과 프록시

  1. 프록시 생성:
    • fetch = FetchType.LAZY로 설정된 연관관계는 처음에는 실제 엔티티가 아닌 프록시 객체로 로드됩니다.
    • 이 프록시 객체는 실제 엔티티 객체를 대리하여 동작하며, 실제 데이터를 요청할 때 데이터베이스 조회가 이루어집니다.
  2. 지연로딩 예시
@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

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

    // getters and setters
}

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

    private String name;

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

    // getters and setters
}

 

위 예제에서 Member 엔티티의 team 연관관계는 fetch = FetchType.LAZY로 설정되어 있습니다. 따라서, Member 객체를 조회할 때 team 속성은 프록시 객체로 로드됩니다.

 

EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();

Member member = em.find(Member.class, memberId);
Team team = member.getTeam(); // 이 시점에는 프록시 객체가 로드됨
String teamName = team.getName(); // 이 시점에 실제 DB 조회 발생

tx.commit();
em.close();

 

프록시 초기화 확인

프록시 객체가 실제 엔티티로 초기화되었는지 확인하는 방법은 다음과 같습니다:

 

프록시 초기화 여부 확인

PersistenceUnitUtil persistenceUnitUtil = emf.getPersistenceUnitUtil();
boolean isInitialized = persistenceUnitUtil.isLoaded(member.getTeam());

 

프록시 강제 초기화

org.hibernate.Hibernate.initialize(member.getTeam());

 

 

  • fetch = FetchType.LAZY로 설정하면 프록시 객체가 생성됩니다.
  • 프록시 객체는 실제 데이터베이스 조회를 지연시켜 성능을 최적화합니다.
  • 프록시 객체는 실제 사용 시점에 데이터베이스를 조회하여 초기화됩니다.
  • 프록시 초기화 여부를 확인하고, 강제 초기화하는 방법도 제공합니다.

 

프록시 객체의 작동 방식

  1. 프록시 객체 생성:
    • member.getTeam()을 호출할 때, 실제로 Team 엔티티가 아닌 Team 엔티티의 프록시 객체가 반환됩니다.
    • 이 프록시 객체는 실제 데이터를 가지고 있지 않으며, 데이터베이스에서 데이터를 로드하는 로직만 포함하고 있습니다.
  2. 초기화 전 상태:
    • team 객체는 프록시 객체로, 이 시점에서는 실제 Team 엔티티의 데이터는 데이터베이스에서 로드되지 않습니다.
    • 프록시 객체는 데이터베이스 접근을 지연시키기 위한 것이므로, 데이터가 실제로 필요할 때까지 로드되지 않습니다.
  3. 실제 데이터 접근 시점:
    • team.getName()을 호출하면 프록시 객체는 데이터베이스에서 실제 데이터를 로드합니다.
    • 이 시점에서 데이터베이스 쿼리가 실행되고, Team 엔티티의 데이터가 프록시 객체에 로드됩니다. 이를 '초기화'라고 합니다.
  4. 초기화 후 상태:
    • 초기화가 완료된 프록시 객체는 실제 데이터를 가지고 있으며, 이후의 데이터 접근은 이미 로드된 데이터를 사용합니다.
public class ProxyExample {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        
        try {
            tx.begin();

            // ID가 1인 멤버 조회
            Member member = em.find(Member.class, 1L);

            // 이 시점에서는 Team 객체가 프록시 객체로 로드됨
            Team team = member.getTeam(); // 프록시 객체

            // 아직 데이터베이스에서 데이터를 로드하지 않음
            // team.getName() 호출 전까지는 실제 데이터를 로드하지 않음

            // 프록시 객체의 실제 데이터를 요청하는 시점에서 DB에서 데이터를 조회
            String teamName = team.getName(); // 여기서 DB 쿼리 실행 (프록시 초기화)

            System.out.println("Team Name: " + teamName); // 실제 데이터 출력

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

 

 

 

프록시와 즉시로딩 주의

- 가급적 지연 로딩만 사용(특히 실무에서)

- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생

- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.

- @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 설정

- @OntToMany, @ManyToMany는 기본이 지연 로딩

 

 

즉시 로딩(Eager Loading)

즉시 로딩은 엔티티와 연관된 모든 데이터를 즉시 데이터베이스에서 가져오는 방식입니다. 즉, 연관된 엔티티의 데이터까지 한 번의 쿼리로 모두 가져옵니다. 이 경우 프록시 객체가 사용되지 않습니다.

지연 로딩(Lazy Loading)

지연 로딩은 연관된 엔티티의 데이터를 실제로 필요할 때까지 데이터베이스에서 가져오지 않는 방식입니다. 지연 로딩을 설정하면, 연관된 엔티티는 프록시 객체로 로드되며, 이 프록시 객체는 실제 데이터를 가지고 있지 않습니다. 데이터가 실제로 필요할 때(프록시 객체가 사용될 때) 데이터베이스에서 데이터를 가져옵니다.

 

즉시로딩

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

    private String name;

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

    // getters and setters
}

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

    private String name;

    // getters and setters
}

 

즉시 로딩의 경우, Member 엔티티를 조회할 때 Team 엔티티도 함께 조회됩니다.

Member foundMember = em.find(Member.class, memberId);
// 이 시점에서 Team 데이터도 이미 로드됨
Team team = foundMember.getTeam(); // 실제 Team 객체
String teamName = team.getName(); // 데이터베이스 접근 없음

 

위의 코드에서 em.find(Member.class, memberId)를 호출하면, JPA는 즉시 Team 엔티티의 데이터도 함께 로드합니다. 따라서 team.getName()을 호출할 때 추가적인 데이터베이스 쿼리가 실행되지 않습니다.

 

 

지연로딩

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

    private String name;

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

    // getters and setters
}

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

    private String name;

    // getters and setters
}

 

지연 로딩의 경우, Member 엔티티를 조회할 때 Team 엔티티는 프록시 객체로 로드됩니다.

 

Member foundMember = em.find(Member.class, memberId);
// 이 시점에서 Team 객체는 프록시 객체로 로드됨
Team team = foundMember.getTeam(); // 프록시 객체
String teamName = team.getName(); // 여기서 DB 쿼리 실행

 

위의 코드에서 em.find(Member.class, memberId)를 호출하면, JPA는 Team 엔티티를 프록시 객체로 로드합니다. team.getName()을 호출할 때 프록시 객체가 초기화되며, 이 시점에 데이터베이스에서 Team 엔티티의 데이터를 가져오는 쿼리가 실행됩니다.

 

영속성 전이(CASCADE)는 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용하는 기능입니다. 예를 들어, 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장되도록 할 수 있습니다. 하지만 영속성 전이는 연관관계를 매핑하는 것과는 관련이 없고, 단지 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공하는 기능입니다.

CASCADE의 종류

  • ALL: 모든 영속성 전이 옵션을 적용합니다.
  • PERSIST: 영속 상태로 전이합니다.
  • REMOVE: 삭제 상태로 전이합니다.
  • MERGE: 병합 상태로 전이합니다.
  • REFRESH: 새로 고침 상태로 전이합니다.
  • DETACH: 준영속 상태로 전이합니다.
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

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

    private String name;

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

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

    // getters and setters
}

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

    private String name;

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

    // getters and setters
}

public class CascadeExample {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("example-unit");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        try {
            tx.begin();

            Parent parent = new Parent();
            parent.setName("Parent");

            Child child1 = new Child();
            child1.setName("Child1");

            Child child2 = new Child();
            child2.setName("Child2");

            parent.addChild(child1);
            parent.addChild(child2);

            em.persist(parent); // parent와 child1, child2도 함께 저장

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

        emf.close();
    }
}

 

주의 사항

  • 영속성 전이는 단순히 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐입니다. 연관관계를 매핑하는 것과는 직접적인 관련이 없습니다.
  • 특정 상황에서만 영속성 전이를 사용하는 것이 좋습니다. 모든 상황에서 무조건 사용하면 예상치 못한 부작용이 발생할 수 있습니다. 예를 들어, REMOVE를 남용하면 불필요한 데이터 삭제가 발생할 수 있습니다.

 

고아 객체(Orphan Removal)

개념

고아 객체는 부모 엔티티와의 연관 관계가 끊어진 자식 엔티티를 말하며, JPA에서는 이런 고아 객체를 자동으로 삭제하는 기능을 제공합니다. 이를 통해 부모 엔티티에서 자식 엔티티를 제거하면, 데이터베이스에서도 해당 자식 엔티티가 자동으로 삭제됩니다.

 

orphanRemoval = true를 사용하여 고아 객체 제거 기능을 활성화할 수 있습니다.

 

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

    private String name;

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

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

    public void removeChild(Child child) {
        children.remove(child);
        child.setParent(null);
    }

    // getters and setters
}

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

    private String name;

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

    // getters and setters
}

 

부모 엔티티에서 자식 엔티티를 제거하면 해당 자식 엔티티가 고아 객체가 되어 삭제됩니다.

 

public class OrphanRemovalExample {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("example-unit");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        try {
            tx.begin();

            Parent parent = new Parent();
            parent.setName("Parent");

            Child child1 = new Child();
            child1.setName("Child1");

            Child child2 = new Child();
            child2.setName("Child2");

            parent.addChild(child1);
            parent.addChild(child2);

            em.persist(parent);
            tx.commit();

            tx.begin();

            Parent foundParent = em.find(Parent.class, parent.getId());
            foundParent.removeChild(child1); // 자식 엔티티를 컬렉션에서 제거 -> 고아 객체 삭제

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

        emf.close();
    }
}

 

주의사항

  1. 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 간주되어 삭제:
    • 고아 객체 제거 기능을 사용하면, 자식 엔티티가 다른 곳에서 참조되지 않을 때 자동으로 삭제됩니다.
  2. 참조하는 곳이 하나일 때 사용:
    • 고아 객체 제거 기능은 자식 엔티티가 하나의 부모 엔티티에만 의존할 때 적합합니다.
  3. 특정 엔티티가 개인 소유할 때 사용:
    • 고아 객체 제거 기능은 엔티티가 특정 부모 엔티티에만 속할 때 사용됩니다.
  4. @OneToOne, @OneToMany만 가능:
    • 고아 객체 제거 기능은 @OneToOne과 @OneToMany 관계에서만 사용할 수 있습니다.
  5. 부모를 제거하면 자식도 함께 제거됨:
    • 고아 객체 제거 기능을 활성화하면 부모를 제거할 때 자식도 함께 제거됩니다. 이는 CascadeType.REMOVE와 유사하게 동작합니다.

'JPA' 카테고리의 다른 글

[JPA] JPA 활용1 - 프로젝트 환경설정  (0) 2024.06.30
[JPA] JPA 기본값 타입  (0) 2024.06.30
[JPA] JPA 고급 매핑  (0) 2024.06.29
[JPA] JPA 연관관계 매핑 (2)  (0) 2024.06.29
[JPA] JPA 연관관계 매핑 (1)  (0) 2024.06.29