프록시와 연관관계 관리

Proxy

객체는 객체 그래프로 연관된 객체들을 탐색한다.

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

엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것이 아니다. 예를 들어 회원 엔티티를 조회할 때 연관된 팀 엔티티는 비지니스 로직에 따라 사용될 때도 있지만 그렇지 않을 때도 있다.

private static void printUserAndTeam(EntityManager entityManager) {
  Member member = entityManager.find(Member.class, 3L);
  Team team = member.getTeam();
  System.out.println("회원명 : " + member.getName());
  System.out.println("소속팀 : " + team.getName());
}

위 코드는 회원과 팀 모두 조회하는 비지니스 로직이다. 회원 엔티티를 찾아서 회원은 정보는 물론이고 연관된 팀 엔티티도 조회한다.

private static void printUser(EntityManager entityManager) {
  Member member = entityManager.find(Member.class, 3L);
  System.out.println("회원명 : " + member.getName());
}

위 코드는 회원 정보만 조회하는 비지니스 로직이다. 회원 엔티티만 찾고 팀 정보는 조회 하지 않는다. 만약 위의 코드중 회원 엔티티만 조회 하고 팀 엔티티는 조회 하지 않는다면 printUserAndTeam 메소드는 비효율적이다. 이 문제를 해결하고자 JPA는 엔티티를 사용할때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이 방법을 지연로딩이라 부른다.

참고) JPA 표준 명세는 지연로딩의 구현 방법을 JPA 구현체에 위임했다. 그래서 어떤 구현체를 쓰는 것에 따라서 다를 수도 있다. 일반적으로 하이버네이트를 많이 쓰기 때문에 하이버네이트를 구현체로 생각하면 되겠다. JPA 구현체 > eclipselink, hibernate, openJPA

private static void proxy(EntityManager entityManager) {
  Member member = entityManager.getReference(Member.class, 3L);
  System.out.println(member.getName());
}

public class MemberProxy extends Member{
  Member target = null;
  public String getName(){
    if(target == null){

      // 초기화 요청
      // DB 조회
      // 실제 엔티티 생성 및 참조 보관
      this.target = ...;
    }
    return target.getName();
  }
}

위 코드는 프록시 클래스의 예상 코드이다.

프록시 객체에 member.getName()을 호출해서 실제 데이터를 조회한다. 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데 이것을 초기화라 한다. 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다. 프록시 객체는 생성된 실제 엔티티 객체의 참조를 Member target 변수에 보관한다. 프록시 객체는 실제 엔티티 객체의 getName() 을 호출하여 결과를 반환한다.

만약 영속 상태가 끝나고 준 영속 상태에서 초기화를 시도 하면 에러가 발생한다. 하이버 네이트 기준으로 LazyInitializationException

영속성 전이: CASCADE

CascadeType의 종류에는 다음과 같은 것들이 있다.

CascadeType.RESIST – 엔티티를 생성하고, 연관 엔티티를 추가하였을 때 persist() 를 수행하면 연관 엔티티도 함께 persist()가 수행된다. 만약 연관 엔티티가 DB에 등록된 키값을 가지고 있다면 detached entity passed to persist Exception이 발생한다. CascadeType.MERGE – 트랜잭션이 종료되고 detach 상태에서 연관 엔티티를 추가하거나 변경된 이후에 부모 엔티티가 merge()를 수행하게 되면 변경사항이 적용된다.(연관 엔티티의 추가 및 수정 모두 반영됨) CascadeType.REMOVE – 삭제 시 연관된 엔티티도 같이 삭제됨 CascadeType.DETACH – 부모 엔티티가 detach()를 수행하게 되면, 연관된 엔티티도 detach() 상태가 되어 변경사항이 반영되지 않는다. CascadeType.ALL – 모든 Cascade 적용

orphanRemoval = true vs DDL의 On Delete Cascade vs CascadeType.REMOVE 차이점

@Entity
class Emp {
@OneToOne(orphanRemoval=true)
    private Addr addr;
}

Emp 엔티티가 삭제될 때 참조가 끊어진 연관된 Addr 엔티티도 삭제하라는 의미이며 DB에서도 삭제되는데 참조(연결)가 끊어진 Addr 객체는 DB에서도 삭제된다는 뜻이다.

@Entity
class Emp {
@OneToOne(cascade=CascadeType.REMOVE)
    private Addr addr;
}

Emp 엔티티가 삭제될 때 연관된 Addr 엔티티도 삭제하라는 의미이며 DB에서도 삭제된다.

orphanRemoval은 JPA2.0 이상에서 지원하는 것으로 ORM 스펙, JPA 레벨에서의 정의이고 On Delete Cascade는 DBMS 레벨에서 작동되며 하는 일은 같다.

orphanRemoval은 @OneToMany 연관에서 부모 엔티티의 컬렉션 등에서 자식 엔티티가 삭제될 때 참조가 끊어지므로 DB 레벨에서도 삭제되고 @OneToOne연관에서 엔티티가 삭제될 때 연관된 엔티티가 참조가 끊어지므로 DB에서 삭제된다. 즉 참조, 연결이 끊어진(Disconnected된) 엔티티를 같이 삭제하라는 의미로 Owner 객체와 참조가 끊어진 객체들을 정리할 때 유용하다.

cascade=CascadeType.REMOVE는 연결이 끊어진다고 해서 자동 삭제되는 것은 아니고 명시적으로 연관 엔티티가 삭제될 때 같이 삭제하라는 영속성 전이와 관련된 옵션이다.

반면 On Delete Cascade는 DB레벨에서 부모 테이블의 레코드가 삭제될 때 자식레코드도 같이 삭제하라는 의미이다.

CascadeType.REMOVE와 orphanRemoval = true는 같은 기능으로 보인다. 사실 같은 기능이라고 해도 된다. 둘다 객체 내에 다른 객체를 가리키는 aggregation관계에 있는 변수를 가질 경우, 그 객체를 삭제할 때 내부에 포함된 객체들도 삭제를 할지에 대한 여부를 지정하는 내용이다.

CascadeType은 사실 부모객체와 그 내부의 인스턴스로 포함된 자녀객체의 ORM동작에 대한 설정이다. PERSIST는 부모객체가 저장되면 자녀객체도 자동저장되게 하는 설정이다. save(), persist()가 해당된다.

MERGE, REFRESH는 객체가 persistent상태가 아닌 경우에 entity객체가 수정되었을 경우에 다시 DB와 데이터 동기화를 시키는 경우에 사용되는 것인데, MERGE는 객체 --> DB, REFRESH는 DB --> 객체로 반영한다. 그 때 자녀객체도 자동으로 반영할 것인지에 대한 설정이다.

REMOVE는 말그대로 부모객체에 대한 삭제가 자동으로 자녀에게 영향을 미칠 것인지 여부다. remove()가 해당된다.

DETACH역시 부모객체가 detach상태가 될 경우 자녀도 그렇게 되는 것인지 설정한다. detach를 사용하는 경우는 보통 DB영향을 주지 않고 객체를 변경하여 다른 곳에서 사용하기 위한 것이다.

결론적으로 질문에 대한 답을 하면, 명시적 remove() 호출의 경우는 동일한 동작을 한다. 하지만, 부모객체를 a, 자식객체를 b라고 할 때, a.setB(null)의 경우에 다른 동작을 한다. 명식적인 remove 호출이 아니므로 Cascade.REMOVE설정의 경우 기존의 자식객체 b는 DB에서 삭제되지 않는다. 하지만 orphanRemoval의 경우는 객체의 레퍼런스만 확인하기 때문에 DB에서도 삭제가 일어난다.

results matching ""

    No results matching ""