본문 바로가기
Framework and Tool/JPA

JPA - 프록시

by ocwokocw 2021. 7. 14.

- 참조: 자바 ORM 표준 JPA 프로그래밍

- 프록시와 즉시로딩, 지연로딩

객체가 데이터베이스에 저장되어있으면 연관된 객체를 탐색하기 난해하다. JPA 구현체들은 이런 문제 해결을 위해 프록시를 이용한다. 프록시를 이용하면 실제 이용하는 시점에 데이터베이스에서 조회할 수 있다. JPA 에서는 즉시로딩 혹은 지연로딩으로 시점을 정할 수 있다.


- 프록시

엔티티를 사용할 때 연관된 엔티티의 사용 유무는 비즈니스 로직에 따라 다르다.

 

@Entity
public class Member {

	@Id @GeneratedValue
	@Column(name = "USER_ID")
	private Long id;
	
	private String userName;
	
	@ManyToOne
	@JoinColumn(name = "TEAM_ID", referencedColumnName = "TEAM_ID")
	private Team team;

.............

@Entity
public class Team {

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

 

Member 와 Team 의 관계가 N : 1 이라고 가정하자. JPA 로 위와 같이 단방향 맵핑을 하였다.

 

public static void find(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();

	Member member = em.find(Member.class, 2L);
	System.out.println("Member name: " + member.getUserName());

	System.out.println("User's team name: " + member.getTeam().getName());
	
	tx.commit();
	em.close();
}

 

이때 비즈니스 로직에서 단순하게 회원의 이름만 조회할 수도 있고, 회원이 속한 팀의 이름까지도 조회할 수 있다. 만약 회원의 이름만 출력한다면 팀 엔티티는 전혀 사용하지 않게 된다.

 

하지만 위 코드에서 User's team name: 으로 출력하는 부분을 주석처리 하여도 하이버네이트가 실행하는 쿼리는 member 를 기준으로 하는 left outer join 을 이용하여 team 테이블까지 조회한다. 회원 엔티티 사용시 팀 엔티티까지 무조건 사용된다는 보장이 있으면 한번에 join 하여 조회하는것이 효율적이지만 꼭 그렇다는 보장이 없을 경우 비효율적인 경우가 있다.

 

Hibernate: 
    select
        member0_.user_id as user_id1_3_0_,
        member0_.team_id as team_id3_3_0_,
        member0_.user_name as user_nam2_3_0_,
        team1_.team_id as team_id1_6_1_,
        team1_.name as name2_6_1_ 
    from
        member member0_ 
    left outer join
        team team1_ 
            on member0_.team_id=team1_.team_id 
    where
        member0_.user_id=?

 

JPA 에서는 위와 같은 문제를 해결하기 위해 지연로딩 기법을 지원한다. 회원이 속한 팀을 실제로 사용하면 그 시점에 팀을 조회하는 것이다. 하지만 실제 조회시점은 지연시키더라도 실제 팀 엔티티대신 사용할 객체가 필요한데 이때 프록시 객체가 사용된다.


- 프록시의 특징

엔티티 매니저의 find() 메소드를 이용할 때 영속성 컨텍스트에서 엔티티를 찾고 없으면 데이터베이스에서 조회한다고 하였다. find() 메소드를 사용하면 실제 해당 엔티티 이용여부에 관계없이 일단 데이터베이스에서 조회를 하게 된다. 이때 실제 엔티티를 사용하기 전까지 조회를 미룰 수 있는데 getReference() 를 이용하면 된다.

 

public static void find(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();

	System.out.println("find() method start=============");
	
	Member member = em.find(Member.class, 2L);

	System.out.println("find() method end=============");		
	
	tx.commit();
	em.close();
}

 

위와 같이 테스트코드를 작성하고 결과를 보면 member 엔티티를 실제 사용하진 않지만 아래처럼 SQL 이 실행된다.

 

fine() method start=============
Hibernate: 
    select
        member0_.user_id as user_id1_3_0_,
        member0_.team_id as team_id3_3_0_,
        member0_.user_name as user_nam2_3_0_,
        team1_.team_id as team_id1_6_1_,
        team1_.name as name2_6_1_ 
    from
        member member0_ 
    left outer join
        team team1_ 
            on member0_.team_id=team1_.team_id 
    where
        member0_.user_id=?
fine() method end=============

 

반면 테스트 코드를 getReference 로 아래와 같이 변경하면 조회 SQL 이 수행되지 않는다.

 

public static void find(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();

	System.out.println("getReference() method start=============");
	
	Member member = em.getReference(Member.class, 2L);

	System.out.println("getReference() method end=============");		
	
	tx.commit();
	em.close();
}
getReference() method start=============
getReference() method end=============

 

이때 아래와 같이 member 엔티티의 이름을 조회하는 코드를 추가해주면 실제로 데이터베이스를 조회해야 이름을 알 수 있기 때문에 SQL이 실행된다.

 

System.out.println("getUserName : " + member.getUserName());
getReference() method start=============
Hibernate: 
    select
        member0_.user_id as user_id1_3_0_,
        member0_.team_id as team_id3_3_0_,
        member0_.user_name as user_nam2_3_0_,
        team1_.team_id as team_id1_6_1_,
        team1_.name as name2_6_1_ 
    from
        member member0_ 
    left outer join
        team team1_ 
            on member0_.team_id=team1_.team_id 
    where
        member0_.user_id=?
getUserName : UesrName#1
getReference() method end=============

 

프록시 객체는 실제 클래스를 상속받아 만들어지며 실제 객체를 참조하고 있다. 그러다가 실제 객체를 사용해야하는 상황이 생기면(위에서는 회원의 이름 조회) 실제 객체에 이 행동을 위임한다. 이를 프록시 객체의 초기화라고 한다.

 

프록시 객체가 초기화 되었다는것은 실제 객체 참조를 갖고 있다는 말이 된다. 이렇게 되면 프록시 객체가 실제 객체 앞단에 위치하면서 사용자의 어떤 요청을 받아도 실제 객체에 위임해버리면 되기 때문에 또 초기화할 필요는 없다. 

 

한 가지 사용시 주의사항이 있는데 초기화시 영속성 컨텍스트의 도움을 받아야 하기 때문에 준영속 상태의 프록시를 초기화 할 수 없다. 만약 em.close() 메소드 뒤에 회원의 이름을 호출하면 Exception 이 발생한다.


- 프록시와 식별자, 초기화 확인

getReference() 로 엔티티를 프록시 조회할 때 식별자 값을 파라미터로 전달하므로 프록시 객체는 식별자 값을 보관하고 있다.

 

System.out.println("getReference() method start=============");

Member member = em.getReference(Member.class, 2L);
System.out.println("getId : " + member.getId());

System.out.println("getReference() method end=============");

 

만약 위와 같이 회원의 식별자 값만을 출력한다면 member 엔티티를 사용하긴 하지만 아래처럼 SQL 은 수행되지 않는다.

 

getReference() method start=============
getId : 2
getReference() method end=============

 

프록시는 위처럼 조회뿐만 아니라 연관관계를 설정할때에도 사용할 수 있다. 만약 아래와 같이 Team 과 Member 를 각각 영속시키는 테스트코드가 있다고 가정하자.

 

public static void save(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();
	
	Team team = new Team();
	
	team.setName("Team#1");
	em.persist(team);
	
	Member member = new Member();
	
	member.setUserName("UesrName#1");
	em.persist(member);
	
	tx.commit();
	em.close();
}

 

그리고 회원을 팀에 종속시키는 테스트 코드를 따로 작성하여 두 메소드를 수행한다.

 

public static void setRelationship(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();

	Team team = em.getReference(Team.class, 1L);
	Member member = em.find(Member.class, 2L);
	member.setTeam(team);
	em.persist(member);
	
	tx.commit();
	em.close();
}

 

위의 코드에서 만약 team 과 member 조회를 둘다 find 로 하면 조회 SQL 을 2 번 수행한다. 하지만 Team 엔티티를 조회할 때 getReference 를 이용하면 SQL 을 회원검색 1 번만 이용된다. 그리고 연관관계도 정상적으로 맵핑된다.

JPA 에서는 프록시 인스턴스가 초기화되었는지를 확인해주는 메소드도 제공하는데 아래처럼 사용할 수 있다.

 

Team team = em.getReference(Team.class, 1L);
boolean teamIsLoaded = em.getEntityManagerFactory()
		.getPersistenceUnitUtil()
		.isLoaded(team);
System.out.println("teamIsLoaded: " + teamIsLoaded);

댓글