본문 바로가기

Back-End/JPA

JPA 8장 - 프록시와 연관관계 정리

객체는 객체 그래프로 연관된 객체들을 탐색한다. 그런데 객체가 데이터베이스에 저장되어 있으므로 연관된 객체를 마음껏 탐색하기 어렵다. JPA 구현체들은 이 문제를 해결하려고 프록시라는 기술을 사용한다. 이를 이용하면 실제 사용되는 시점에 데이터베이스에서 조회할 수 있다. 하지만 자주 사용하는 객체는 조인을 사용해서 함께 조회하는 것이 더 효과적이다. JPA는 즉시 로딩 ( eager )과 지연 로딩 ( lazy ) 를 지원 한다.

 

프록시

지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이를 프록시 객체라 한다.

JPA 표준 명세는 지연로딩의 구현방법을 JPA 구현체에 위임했다. 따라서 지금부터 나오는 내용은 하이버네이트 구현체에 대한 내용이다. 하이버네이트는 지연 로딩을 사용하기 위해 프록시를 사용하는 방법과 바이트코드를 사용하는 방법이 있다.

 

프록시 기초

// Persistence Context 에 Entity가 없으면 데이터베이스를 조회
// 엔티티를 사용하지 않아도 데이터베이스를 조회함
Member member = em.find(Member.class, "member1");

// 엔티티를 직접 사용하는 시점까지 데이터베이스 조회를 미룸
Member member = em.getReference(Member.class, "member2");

 

  • 프록시의 특징
    • 실제 클래스를 상속받아서 만들어지므로 겉 모양은 같다. 따라서 사용하는 입장에서는 이것이 진짜 객체인지 프록시 객체인지 구분하지 않고 사용함
    • 프록시 객체는 실제 객체에 대한 참조(target)을 보관함. 프록시 객체의 메소드를 호출하면 실제 엔티티에 위임(delegate)함.
    • 초기화는 영속성 컨텍스트의 도움을 받아야 가능함. 준영속 상태의 프록시를 초기화하면 org.hibernate.LazyInitializationException 예외 발생 ( 지연 로딩에 대한 구현은 JPA 구현체에 맡겼기 때문에 에러 메시지는 구현체게 나온다. )

// MemberProxy 반환
Member member = em.getReference(Member.class, "id1");
member.getName(); // 1. getName()

class MemberProxy extends Member {

	Member target = null; // 실제 엔티티 참조
    
    public String getName() {
    
    	if( target == null ) {
        
        	// 2. 초기화 요청
            // 3. DB 조회
            // 4. 실제 엔티티 생성 및 참조 보관
            this.target = ...;
        }
        
        // 5
        return tagert.getName();
    }
}

 

  1. 프록시 객체에 member.getName()을 호출해서 실제 데이터 조회
  2. 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청 ( 초기화 )
  3. 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성
  4. target에 보관
  5. 실제 엔티티 객체의 getName()을 호출해서 결과 반환

준영속 상태의 초기화

Member member = em.getReference...

transaction.commit();
em.close(); // 영속성 컨텍스트 종료

member.getName();

프록시와 식별자

Team team = em.getReference(Team.class, "team1"); // 식별자 보관
team.getId(); // 초기화되지 않음
  • 프록시 객체는 식별자(id)를 가지고 있으므로 Id를 조회해도 프록시를 초기화하지 않는다. ( @Access(AccessType.PROPERTY )
  • @Access(AccessType.FIELD) 로 설정하면 getId()를 해도 id만 조회하는지 다른필드까지 활용하는지 모르기 때문에 프록시 객체를 초기화 한다.

연관 관계를 설정할 때는 식별자 값만 사용하므로 프록시를 사용하면 데이터베이스 접근 횟수를 줄일 수 있다.

 

boolean isLoad = PersistenceUnitUtil.isLoaded(entity);
// 프록시 인스턴스 초기화 여부 확인

즉시 로딩

@Entity
public class Member {

	@ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name= "TEAM_ID")
    private Team team;
    
    // ...
}
Member member = em.find(Member.class,"member1");
Team team = member.getTeam();

// SQL
SELECT *
FROM
	MEMBER M LEFT OUTER JOIN TEAM T
    ON M.TEAM_ID=T.TEAM_ID
WHERE
	M.MEMBER_ID = 'member1';

쿼리를 살펴보면 LEFT OUTER JOIN을 사용하고 있는 걸 발견할 수 있다. 왜 LEFT OUTER JOIN을 사용했을까? 일단, 팀이 없는 회원이 있다면? 만약 내부조인이라면 팀이 없는회원은 조회조차 되지 않았을 것이다. JPA는 이런상황을 고려해서 외부 조인을 사용한 것이다.

NULL 제약 조건과 JPA 조인 전략
외부 조인보다는 내부조인이 성능이 최적화에 더 유리하다. 내부조인을 사용하기 위해서는 외래 키에 NOT NULL 제약 조건을 설정하면 값이 있는 것을 보장한다.

@JoinColumn(name="TEAM_ID", nullable=false)

처럼 명시해주면 내부조인을 사용한다.

또는
@ManyToOne(fetch = FetchType.EAGER, optional = false )
@JoinColum(name ="TEAM_ID")
private Team team;

지연 로딩

Member member = em.find(Member.class,"member1");
Team team = member.getTeam();	// 프록시 객체
team.getName(); // 팀 객체 실제 사용
  • 컬렉션인 경우 - @ManyToOne, @OneToOne : 즉시로딩
  • @OneToMany, @ManyToMany : 지연 로딩 - 무조건 외부 조인을 사용
컬렉션에 FetchType.EAGER 사용 시 주의점
1. 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.
2. 컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않는다.

영속성 전이 : CASCADE

1. 저장 코드를 살펴보자.

@Entity
public class Parent {
	@OneToMany( mappedBy = "parent", cascade = CascadeType.PERSIST )
    private List<Child> children = Lists.newArrayList();
}

private static void save(EntityManager em) {

	Parent parent = new Parent();
    child1.setParent(parent);
    child2.setParent(parent);
    parent.getChildren().addAll(child1,child2);
    
    em.persist(parent);
}

2. 삭제 cascade 를 안걸고 부모 entity를 삭제하면, 외래 키 제약조건으로 인해 외래키 무결성 예외가 발생한다.

 

고아 객체 - orphanRemoval

부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제 된다.

Parent paren = em.find(Parent.class, parentId);
parent.getChildren().remove(removeObject);

 

영속성 전이는 DDD의 Aggregate Root 개념을 구현할 때 사용하면 편리하다.