8. 프록시와 연관관계 관리
프록시란?
- JPA의 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데, 이것을 프록시 객체라고 함
- JPA의 지연 로딩 : 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연시키는 방법(실제 데이터를 사용할 때 조회)
- JPA의 표준 명세는 지연 로딩의 구현 방법을 JPA 구현체에 위임함
- 하이버네이트 기준으로 아래 내용 작성
- 하이버네이트가 지연 로딩을 지원하는 방법
- 프록시
- 바이트코드 수정 : 복잡해서 다루지 않음
1. 프록시 기초
- EntityManager.find()
- JPA에서 데이터베이스로부터 엔티티를 조회할 때 사용
- 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 조회하고 엔티티(객체) 반환
- 조회한 엔티티를 실제 사용 여부와 상관 없이 일단 데이터베이스 조회
- EntityManager.getReference()
- 엔티티를 실제 사용하는 시점까지 조회를 미루고 싶을 때 사용
- JPA는 해당 메소드 호출 시 바로 데이터베이스르 조회하지 않고 실제 엔티티 객체도 생성하지 않음
- 데이터베이스 접근을 위임한 프록시 객체를 반환
Member member = em.getReference(Member.class, "member1");

- 프록시의 특징
- 실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉 모양이 같음
- 따라서 사용하는 입장애서 프록시인지, 객체인지 구분할 필요 없음

- 아래 그림과 같이 프록시는 실제 객체에 대한 참조(target)을 보관함
- 프록시 객체의 메소드를 호출하면 프록시 객체가 실제 객체의 메소드를 호출

- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없음
- 따라서 프록시가 아닌 실제 엔티티 객체가 반환됨
- 프록시 초기화는 영속성 컨텍스트를 통해서 진행됨
- 따라서 준영속 상태의 프록시 초기화는 에러 발생함 (아래 코드 참조)
Member member = em.getReference(Member.class, "member1"); tx.commit(); em.close(); member.getName(); // 준영속 상태에서 호출
- 프록시 객체의 초기화
- 실제 데이터를 필요로 하는 메소드 호출 시 데이터베이스를 조회해서 실제 엔티티 객체를 생성함 (프록시 객체의 초기화라고 함)

// 프록시 클래스의 예상 코드 class MemberProxy extends Member { Member target = null; public String getName() { if(target == null){ // 2. 초기화 요청 // 3. DB 조회 // 4. 실제 엔티티 생성 및 참조 보관 this.target = ...; } return target.getName(); } }
- 실제 데이터를 필요로 하는 메소드 호출 시 데이터베이스를 조회해서 실제 엔티티 객체를 생성함 (프록시 객체의 초기화라고 함)
- 프록시와 식별자
- 엔티티를 프록시로 조회할 때 식별자(PK) 값을 파라미터로 전달, 프록시는 이 식별자를 보관함
- 식별자를 가지고 있는 상태에서 getId() 메소드를 호출하면?
- @Access(AccessType.PROPERTY)
- 이미 id 필드가 채워져 있으므로 프록시 초기화(DQ 쿼리) 하지 않음
- @Access(AccessType.FIELD)
- 이미 id 필드가 채워져 있지만 프록시를 초기화 함
- @Access(AccessType.PROPERTY)
Member member = em.find(Member.class, "member1"); Team team = em.getReference(Team.class, "team1"); // SQL 실행하지 않음 member.setTeam(team);- 연관관계 설정은 식별자 값만 사용하므로 위에 코드처럼 프록시를 사용하면 데이터베이스 접근 횟수를 줄일 수 있음
- 이 상황에서는 엔티티 접근 방식이 필드로 설정해도 프록시 초기화 하지 않음
- 프록시 확인
- 프록시 인스턴스가 초기화 됐는지 확인하는 방법?
PersistenceUnitUtil.isLoaded(Object entity);
- 조회한 엔티티가 프록시로 조회한 것인지 진짜 엔티티로 조회한 것인지 확인 방법?
- 클래스 명을 출력해봤을 때 이름 뒷부분에 “javassist”가 있으면 프록시
- 프록시 인스턴스가 초기화 됐는지 확인하는 방법?
2. 즉시 로딩과 지연 로딩
- 즉시 로딩 : 엔티티 조회 시 연관 엔티티도 함께 조회
@ManyToOne(fetch = FetchType.EAGER)
- 지연 로딩 : 연관된 엔티티를 실제 사용할 때 조회
@ManyToOne(fetch = FetchType.LAZY)
- 즉시 로딩
- @ManyToOne(fetch = FetchType.EAGER)

- em.find(Member.class, “member1”) 호출과 동시에 팀도 함께 조회를 함
- 회원과 팀 두 테이블을 조회해야 하므로 쿼리를 2번 실행하나?
- 대부분의 JPA 구현체가 조인 쿼리를 이용해서 한번에 처리
- JPA는 그럼 조인 전략을 어떤 전략을 사용하냐?
- LEFT OUTER JOIN을 상요함
- 왜 INNER JOIN을 안할까?
- 외래키가 null인 경우 INNER JOIN을 하면 아무 데이터도 조회가 안되는 참사가..
- 그래서 JPA는 기본적으로 OUTER JOIN을 사용함
- 만약 외래키에
nullable = false설정을 해주면 JPA는 INNER JOIN을 사용함 @JoinColumn(name = "TEAM_ID", nullable = false) private Team team;
- 지연 로딩
- @ManyToOne(fetch = FetchType.LAZY)

- em.find(Member.class, “member1”) 호출 시 회원만 조회
- 팀은 조회하지 않고 프록시 객체만 넣어둠
Member member = em.find(Member.class, "member1") Team team = em.getTeam(); // 프록시 객체 사용 team.getName(); // 팀 객체 실제 사용 - SQL 쿼리
- 프록시와 즉시로딩 정리
- 지연 로딩(LAZY)
- 연관된 엔티티를 프록시로 조회
- 프록시를 실제 사용할 때 초기화하면서 데이터베이스 조회
- 즉시 로딩(EAGER)
- 연관된 엔티티 즉시 조회
- 하이버네이트트 가능하면 SQL 조인으로 한 번에 조회함
- 지연 로딩(LAZY)
- 프록시와 즉시로딩 주의
- 즉시 로딩 갖다 버리고 지연 로딩만 써라..
- JPQL의 fetch 조인이나 엔티티 그래프 기능을 사용해야 함
- @ManyToOne, @OneToOne은 기본이 즉시 로딩을 사용하도록 되어있음
- 즉시 로딩은 상상하지 못한 쿼리가 나감
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다
- 이게 무슨소리냐면.. JPA는 조인으로 한 쿼리로 연관관계를 가져오지만 JPQL은 SQL과 같기 때문에 직접 조인을 해주지 않는 이상 그러지 못함
List<Memebr> members = em.createQuery("select m from Member m", Member.class) .getResultList(); // SQL1 : select * from Member // SQL2 : select * from Team where Member.TEAM_ID = TEAM_ID
- 즉시 로딩 갖다 버리고 지연 로딩만 써라..
3. 영속성 전이 : CASCADE
- 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티는 어떻게 되나?
- 영속성 전이(transitive persistence) 기능을 사용하면 연관된 엔티티도 영속 상태로 만들 수 있음
연관관계 매핑과는 전혀 관련이 없고, 엔티티를 영속화할 때 연관된 엔티티도 같이 영속화 할 수 있는 편의를 제공하는 기능
- 영속성 전이 : 저장
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST) private List<Child> children = new ArrayList<Child>();- 영속성 전이 설정을 통해 부모만 영속 상태로 만들어도 자식도 함께 영속 상태로 됨

- 영속성 전이 : 삭제
- 부모와 자식 엔티티를 삭제하려면..?
Parent parent = em.find(Parent.class, 1L); Child child1 = em.find(Child.class, 1L); Child child2 = em.find(Child.class, 2L); em.remove(child1); em.remove(child2); em.remove(parent);- 연관관계 엔티티들을 모두 조회해서 일일이 remove 해줘야 함..
- 영속성 전이를 통해 연관관계 엔티티도 함께 삭제가 가능함
@OneToMany(mappedBy = "parent", cascade = { // 여러 속성 동시 사용 CascadeType.PERSIST, CascadeType.REMOVE, }) private List<Child> children = new ArrayList<Child>(); ... Parent findParent = em.find(Parent.class, 1L); em.remove(findParent); // 자식도 함께 삭제 Cascading- 삭제 순서는 외래키 제약조건을 고려해서 자식 먼저 삭제 후 부모 삭제됨
- 만약 Cascade 속성 없이 부모만 삭제하면?
- 자식 테이블에 걸려있는 외래키 제약조건에 의해 외래키 무결성 예외가 발생함..
- 전이의 발생 시점?
- em.persist(), em.remove() 호출 시 전이가 되는게 아님!!
- flush() 호출 시 전이됨!!
- CASCADE의 종류
public enum CascadeType { ALL, // 모두 적용 PERSIST, // 영속 MERGE, // 병합 REMOVE, // 삭제 REFRESH, // REFRESH DETACH //DETACH }
4. 고아 객체
- 고아 객체 삭제 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능
- 부모 엔티티 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제
@Entity public class Parent { ... @OneToMany(mappedBy = "parent", orphanRemoval = true) private List<Child> children = new ArrayList<Child>(); ... }orphanRemoval = true설정 시 컬렉션에서 삭제된 엔티티는 자동으로 삭제됨- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 것
- 따라서 참조하는 곳이 하나일 때만 이 기능을 적용해야 함(특정 엔티티가 개인 소유할 때)
- 따라서 @OneToOne, @OneToMany에서만 사용
5. 영속성 전이 + 고아 객체, 생명주기
CascadeType.ALL + orphanRemoval = true를 동시에 사용하면?- 원래의 엔티티는 EntityManager를 통해 자신의 생명주기를 스스로 관리함
- em.persist(), em.remove()를 통해 스스로 영속, 비영속 되는 것과 같이
- 위와 같은 옵션을 사용할 경우 부모가 자식의 생명주기를 완전히 관리 가능
- 자식을 부모에 등록할 때(CASCADE)
Parent parent = em.find(Parent.class, parentId); parent.addChild(child1);- 자식을 부모에서 삭제할 때(orphanRemoval)
Parent parent = em.find(Parent.class, parentId); parent.getChildren().remove(removeObject);
- 원래의 엔티티는 EntityManager를 통해 자신의 생명주기를 스스로 관리함
정리하면?
- JPA 구현체는 객체 그래프 탐색 기능 지원을 위해 프록시 기술을 사용함
- 객체를 조회할 때 연관된 객체를 즉시 로딩, 지연 로딩하는 방법을 지원함
- 객체를 저장하거나 삭제할 때 연관된 객체도 함께 저장하거나 삭제할 수 있고 이를 영속성 전이라고 함
- 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하려면 고아 객체 제거 기능을 사용
