객체그래프 탐색
- 객체는 객체 그래프로 연관된 객체를 탐색한다.
- Entity는 객체가 데이터베이스(RDB)와 매핑되어 있어서 자유롭개 객체를 탐색하는데 제한이 있다(필요할 때마다 쿼리를 다 날려야 하니).
- // order.getMember() → orders 테이블과 member 테이블의 정보를 모두 가져와야 하는상태
- JPA는 프록시객체라는 기술을 사용하여 연관된 객체를 처음부터 데이터베이스에서 조회하지 않고, 실제 사용하는 시점에 조회할 수 있다.
- order.getMember().getNam() 등을 호출했을 때, 해당 멤버 데이터를 join 해서 가져오는 것
프록시 객체
@Entity @Table(name = "member") public class Member extends BaseEntity { ... @OneToMany(mappedBy = "member", fetch = FetchType.LAZY) private List<Order> orders = new ArrayList<>(); // proxy ... }
@Entity @Table(name = "orders") public class Order extends BaseEntity { ... @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id", referencedColumnName = "id") private Member member; ... }
@Test void proxy() { EntityManager entityManager = emf.createEntityManager(); // 회원 조회 -> 회원의 주문 까지 조회 Member findMember = entityManager.find(Member.class, 1L); log.info("orders is loaded : {}", entityManager.getEntityManagerFactory() .getPersistenceUnitUtil().isLoaded(findMember.getOrders())); // isLoaded 가 false이면 proxy 객체 라는 의미임 log.info("-------"); log.info("{}" ,findMember.getOrders().get(0).getMemo()); log.info("orders is loaded : {}", entityManager.getEntityManagerFactory() .getPersistenceUnitUtil().isLoaded(findMember.getOrders())); } // 실행결과 Hibernate: select member0_.id as id1_1_0_, member0_.created_at as created_2_1_0_, member0_.created_by as created_3_1_0_, member0_.address as address4_1_0_, member0_.age as age5_1_0_, member0_.description as descript6_1_0_, member0_.name as name7_1_0_, member0_.nickName as nickname8_1_0_ from member member0_ where member0_.id=? 2021-09-11 14:11:52.453 INFO 7960 --- [ main] com.kdt.lecture.domain.order.ProxyTest : orders is loaded : false 2021-09-11 14:11:52.454 INFO 7960 --- [ main] com.kdt.lecture.domain.order.ProxyTest : ------- Hibernate: select orders0_.member_id as member_i7_3_0_, orders0_.id as id1_3_0_, orders0_.id as id1_3_1_, orders0_.created_at as created_2_3_1_, orders0_.created_by as created_3_3_1_, orders0_.member_id as member_i7_3_1_, orders0_.memo as memo4_3_1_, orders0_.order_datetime as order_da5_3_1_, orders0_.orderStatus as ordersta6_3_1_ from orders orders0_ where orders0_.member_id=? 2021-09-11 14:11:52.459 INFO 7960 --- [ main] com.kdt.lecture.domain.order.ProxyTest : 부재시 전화주세요. 2021-09-11 14:11:52.459 INFO 7960 --- [ main] com.kdt.lecture.domain.order.ProxyTest : orders is loaded : true
프록시의 특징
- 프록시 객체는 처음 사용할 때 한번만 초기화 된다.
- 프록시 객체의 초기화 : 프록시 객체는 member.getName() 처럼 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라 함
- 프록시 객체가 초기화되면, 프록시 객체를 통해서 실제 엔티티에 접근 할 수 있다.
- 필요할 때 그 엔티티를 가지고 와서
프록시 객체
→엔티티
로 변경
- 초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 따라서 준영속 상태의 프록시를 초기화하면 LazyInitializationException 예외가 발생한다.
프록시와 컬렉션 래퍼
- 하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는데 이것을 컬렉션 래퍼라 함(PersistentBag)
- 컬렉션 래퍼도 컬렉션에 대한 프록시 역할을 함
지연로딩(LAZY) & 즉시로딩(EAGER)
지연로딩
연관된 엔티티를 실제 사용할 때 조회한다.

- member.getOrders() 로 얻어온 Order를 사용하려고 할 때, 초기화 요청이 시작됨
- 사용 전에는 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라 함
- 프록시 객체는 실제 객체에 대한 참조(target)를 보관. 그리고 프록시 객체의 메서드 호출 시, 프록시 객체는 실제 객체의 메소드를 호출
- 엔티티를 프록시로 조회할 때 식별자(PK) 값을 파라미터로 전달하는 데 프록시 객체는 이 식별자 값을 보관함. 프록시 객체가 식별자 값을 갖고 있기에 getId() 메서드를 호출한다고 해서 엔티티가 초기화 되지는 않음
즉시로딩
엔티티를 조회할때, 연관된 엔티티를 함께 조회한다.
- EagerFetch 사용시 주의점
- 컬렉션 하나 이상 즉시 로딩 하지 않기. 카르테시안 곱 만큼의 데이터가 반환될 수 있음
EagerFetch
인 경우, JPQL 을 사용하여 연관된 엔티티를 제외하고 조회하여도, 결국 엔티티를 구성하기 위해 SQL 쿼리를 다시 실행해 연관된 엔티티를 조회하게 됨

@Test @Transactional void 이메일로_계정조회() { //given Organization organization = organizationRepository.findById(createdOrganizationId).orElseThrow(); Department department = departmentRepository.findById(createdDepartmentId).orElseThrow(); Room room = roomRepository.save(RoomFixture.createTownHall("townhall", 50, organization)); Account account = new Account("user1@test.com", "abcd1234!@", "user1", "nickname", "010-1234-1234", department, room); Account savedAccount = accountRepository.save(account); entityManager.flush(); entityManager.clear(); //when Optional<Account> optionalAccount = accountRepository.findByEmail(savedAccount.getEmail()); //then assertThat(optionalAccount).isPresent(); Account fetchAccount = optionalAccount.get(); int organizationUid = fetchAccount.organizationUid(); Organization organization1 = fetchAccount.getDepartment().getOrganization(); String name = organization1.getName(); System.out.println(name); System.out.println(fetchAccount.getRoom().getName()); assertThat(fetchAccount.getEmail()).isEqualTo(account.getEmail()); // teardown accountRepository.deleteById(fetchAccount.getUid()); }
- 위 코드에서 entityManager.clear() 를 통해 Persistence Context 가 비워짐
- findByEmail에서는 Account와 Relation관계인 Department만 가져옴
- 그러므로 then 밑에서 fetchAccount에서 organization과 room 을 조회할 시, sql이 다시 나가게 됨
- 만약 Persistence Context에 given 절에서 불러온 organization과 room이 있다면 then 절에서 sql이 다시 발생하지는 않음. Persistence Context에서 찾아서 바로 가져올 수 있기 때문에(id 기반으로)