✅ JPA 프록시란?
객체가 객체 그래프로 연관된 객체들을 탐색할때, 데이터베이스에 저장되어 있는 객체를 처음부터 데이터베이스에서 조회하는 것이 아니라 실제 사용하는 시점에 데이터베이스에서 조회(지연 로딩)할 수 있게 해주는 기술이다. 프록시 객체는 실제 클래스를 상속 받아서 만들어진 겉모양만 똑같은 가짜 객체다. 여기서 사용된 프록시는 타겟에 대한 접근 제어로 사용되었다. cross cutting concern으로 사용된 것은 아니다.
🔻프록시 구조
- 실제 클래스를 상속 받아서 만들어진다.
- 실제 클래스와 겉모양이 같다.
- 사용하는 입장(Client)에서 진짜 객체인지 프록시 객체인지 알지 못하고 항상 진짜 객체인 것처럼 사용한다.
🔻프록시 위임
- 프록시 객체는 실제 객체의 참조를 저장한다.
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다. 이를 통해 실제 사용하는 시점에 객체를 로딩하는 지연 로딩이 가능하다.
✅ 프록시 객체 동작과정
Member member = em.getReference(Member.class, "id"); // 프록시 객체 생성
member.getName(); // 초기화를 통해 실제 Entity 생성 및 호출 위임
getReference()
: 프록시 객체를 반환하는 메소드
- member 변수에는 프록시 객체가 참조된다.
getName()
메소드가 호출되면 실제 Entity 객체로 위임해야 한다. 프록시 객체 내부에 target은 실제 Entity 객체로 초기화되지 않았기 때문에 영속성 컨텍스트에 초기화를 요청한다.
- 영속성 컨텍스트는 DB를 조회해서 Entity 객체를 반환한다.
- 프록시 객체는 반환된 실제 엔티티 객체의 참조를 Member target 멤버변수에 보관한다.
- target을 통해서 위임하여 메소드를 수행하게 된다.
❗주의할 점
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 프록시 객체가 실제 엔티티 객체로 접근할 수 있게 된 것이다.
- 영속성 컨텍스트(1차 캐시)에서 찾은 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로
em.getReference()
를 호출해도 실제 엔티티를 반환한다.
✅ 즉시로딩 (fetch = FetchType.EAGER)
- Team
@Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name;
- Member
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String name; //@ManyToOne : default EAGER @ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩 @JoinColumn(name = "TEAM_ID") private Team team;
- Main
public class Main { public static void main(String[] args) { //엔티티 매니저 팩토리 생성 EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook"); EntityManager em = emf.createEntityManager(); //엔티티 매니저 생성 EntityTransaction tx = em.getTransaction(); //트랜잭션 기능 획득 try { tx.begin(); //트랜잭션 시작 //저장 Long[] ids = testSave(em); Long memberId = ids[0]; Long team1Id = ids[1]; Long team2Id = ids[2]; tx.commit();//트랜잭션 커밋 em.clear(); // Eager & Lazy Test printUserAndTeam(em, memberId); } catch (Exception e) { e.printStackTrace(); tx.rollback(); //트랜잭션 롤백 } finally { em.close(); //엔티티 매니저 종료 } emf.close(); //엔티티 매니저 팩토리 종료 }
Team 객체를 사용하기도 전에 Member을 조회할 때 연관된 Team 객체도 함께 조회되어서 미리 초기화된 것을 볼 수 있다. JPA 구현체는 즉시 로딩을 최적화 하기 위해 외부 조인(LEFT OUTER JOIN)을 사용해서 한 번에 조회했다.
✅ 지연로딩 (fetch = FetchType.LAZY)
- Member
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String name; //@ManyToOne : default EAGER @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "TEAM_ID") private Team team;
em.find()
로 Member을 호출할 때는 Member만 호출하고 Team에는 HibernateProxy 객체를 반환한다. 프록시 객체는 실제 사용될 때까지 데이터 로딩을 미룬다. 이를 지연 로딩이라 한다. Team 객체의 getName()
메소드를 호출할 때, 영속성 컨텍스트에서 DB로 쿼리를 발생시켜서 HibernateProxy객체의 target(Team) 객체를 초기화 시킨다.
Member을 호출할 때 프록시 객체가 반환되고, 프록시 객체 내부에 target이 실제 엔티티 객체인 Team으로 참조되고 있다. Clinet가 프록시 객체를 통해서 실제 객체의 메소드를 호출할 때 영속성 컨텍스트를 통해 target을 초기화 시키며 프록시 객체는 실제 엔티티 객체의 메소드로 호출을 위임한다.
🔹Null 제약 조건과 JPA 조인 전략
즉시 로딩 실행 SQL에서 JPA가 내부 조인(INNER JOIN)이 아닌 외부 조인(LEFT OUTER JOIN)을 사용했다. 회원 테이블에 TEAM_ID 외래 키는 NULL을 허용하고 있다. 따라서 팀에 소속되지 않은 회원이 있을 가능성이 있기 때문에, 팀에 소속되지 않은 회원과 팀을 내부조인하면 어떠한 데이터도 조회할 수 없다. JPA는 이런 상황을 고려해서 default로 외부 조인을 사용한다. 외래 키(FK)에 NotNull 설정을 통해 JPA에게 내부 조인을 사용할 수 있다는 것을 알리며 내부 조인을 사용할 수 있다.
- @JoinColumn(nullable = true) : NULL 허용(기본값), 외부 조인 사용
- @JoinColumn(nullable = false) : NULL 허용하지 않음, 내부 조인 사용
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String name; //@ManyToOne : default EAGER @ManyToOne @JoinColumn(name = "TEAM_ID", nullable = false) // Not NULL private Team team;
👉 nullable = false 제약 조건을 추가해 INNER JOIN으로 쿼리가 변경된 것을 확인할 수 있다.
- 전체 코드, Eager, Lazy 바꿔가면서 테스트
public class Main { public static void main(String[] args) { //엔티티 매니저 팩토리 생성 EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook"); EntityManager em = emf.createEntityManager(); //엔티티 매니저 생성 EntityTransaction tx = em.getTransaction(); //트랜잭션 기능 획득 try { tx.begin(); //트랜잭션 시작 //저장 Long[] ids = testSave(em); Long memberId = ids[0]; Long team1Id = ids[1]; Long team2Id = ids[2]; tx.commit();//트랜잭션 커밋 // Eager & Lazy Test printUserAndTeam(em, memberId); printUser(em, memberId); // Proxy Test proxyTest(em, memberId); // Mapping Test referenceMapping(em, memberId, team2Id); // Check Proxy checkProxy(em, memberId); } catch (Exception e) { e.printStackTrace(); tx.rollback(); //트랜잭션 롤백 } finally { em.close(); //엔티티 매니저 종료 } emf.close(); //엔티티 매니저 팩토리 종료 } public static Long[] testSave(EntityManager em) { Team team1 = new Team("team1"); em.persist(team1); // .persist()하면 영속성 컨텍스트로 관리되기 위해 식별자 ID인 시퀀스 값을 할당 받음 // DB에 저장은 트랜잭션이 commit될 때임 Team team2 = new Team("team2"); em.persist(team2); Member member1 = new Member("member1"); member1.setTeam(team1); // 연관관계 설정 em.persist(member1); Long[] result = new Long[5]; result[0] = member1.getId(); result[1] = team1.getId(); result[2] = team2.getId(); return result; } public static void printUserAndTeam(EntityManager em, Long id) { em.clear(); System.out.println("====================== print Member & Team =========================="); Member findMember = em.find(Member.class, id); Team findTeam = findMember.getTeam(); System.out.println("Team 객체의 초기화 여부: " + Hibernate.isInitialized(findTeam)); System.out.println("Member Name: " + findMember.getName()); System.out.println("Team Name: " + findTeam.getName()); System.out.println("Team 객체의 초기화 여부: " + Hibernate.isInitialized(findTeam)); } public static void printUser(EntityManager em, Long id) { em.clear(); System.out.println("======================== print Member =============================="); Member findMember = em.find(Member.class, id); System.out.println("Member Name: " + findMember.getName()); } public static void proxyTest(EntityManager em, Long id) { em.clear(); System.out.println("======================== Proxy Test ================================"); Member proxyMember = em.getReference(Member.class, id); System.out.println("객체 이름: " + proxyMember.getClass().getName()); System.out.println("Proxy 객체의 초기화 여부: " + Hibernate.isInitialized(proxyMember)); proxyMember.getName(); System.out.println("Proxy 객체의 초기화 여부: " + Hibernate.isInitialized(proxyMember)); } public static void referenceMapping(EntityManager em, Long memberId, Long team2Id) { em.clear(); System.out.println("======================== Mapping Test ================================"); Member getMember = em.find(Member.class, memberId); Team team = em.getReference(Team.class, team2Id); // SQL을 실행하지 않는다 getMember.setTeam(team); } public static void checkProxy(EntityManager em, Long memberId) { em.clear(); System.out.println("========================== Check Test ================================"); Member memberProxy = em.getReference(Member.class, memberId); boolean isLoad = em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(memberProxy); System.out.println("memberProxy: " + memberProxy.getClass().getName()); System.out.println("isLoad: " + isLoad); } }
Uploaded by N2T