프록시와 연관관계 관리

원종서·2022년 2월 12일
1

JPA

목록 보기
8/13

프록시

연관된 객체가 항상 사용되는 것은 아니다. 사용되지도 않은 연관된 엔티티를 함께 쿼리해서 데이터를 가져오는 것은 효율적이지 못하다.

JPA는 이런 문제를 해결하기 위해 엔티티가 실제 사용할 떄까지 데이터베이스 조회를 지연하는 방법 (지연로딩) 을 사용한다.

즉 team.getName() 처럼 엔티티의 값을 실제 사용하는 시점에 연관된 엔티티를 데베에서 조회한다.

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

프록시란

엔티티를 실제 사용하는 시점까지 데베 조회를 미루고 싶으면

EntityManager.getReference(ENTITY.CLASS, ENTITY.ID) // <- 메서드를 사용하면 된다.

해당 메서드는 JPA는 데베를 조회하지 않고, 실제 엔티티 객체를 생성하지 않는다. 대신 데베 접근을 위임한 프록시 객체를 반환한다.

프록시의 특징.
1. 프록시 객체는 실제 객체에 대한 참조(target)를 보관한다. 그 후 프록시 객체의 메서드를 호출하면 프록시는 실제 객체의 메서드를 호출한다.

프록시 객체 초기화

team.getName() 처럼 실제 사용될 때 데베를 죄해서 실제 엔티티 객체를 생성하는 것을 프록시 초기화라고 한다.

초기화 과정
1. 프록시 객체에서 getName() 을 호출해 실제 데이터 조회.
2. 프록시 객체는 실제 엔티티가 생성돼 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요구함 (프록시 초기화)
3. 영속성 컨텍스트는 데베를 조회해 실제 엔티티 객체 생성
4. 생성된 실제 엔티티 객체의 참조를 프록시 Team target 멤버변수에 보관.
5. 프록시는 실제 엔티티 객체의 getName() 을 호출해 결과를 반환.

프록시의 특징.
1. 프록시 객체는 처음 사용할 때 한번만 초기화한다. 프록시 내 target 필드에..
2. 초기화 한다고 프록시가 실제 엔티티가 되는 것은 아니다.
3. 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 getReference()를 해도 데베에서 찾는 것이 아니니. 실제 엔티티를 반환함.
4. 초기화는 준영속 상태의 프록시를 초기화하면 문제가 발생함.

프록시와 식별자.

엔티티를 프록시로 조회할때 식별자 값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 저장한다.

Team team = em.getReferenec(Team.class, "team1"); // 실별자 보간
team.getId(); // 를 호출해도 초기화하지 않는다. 왜냐 식별자는 이미 있거든

단 엔티티 접근 방식을 프로퍼티로 설정한 경우에만 초기화하지않음.

엔티티 접근 방식을 필드로 설정하면 getId() 메소드가 id만 조회하는 메소드인지 다른 필드까지 활용하는지 알지 못하기때문에 프록시 객체를 초기화한다.

그러므로 프록시는 연관관계를 설정할 떄 유용하게 쓸 수 있다.

Member member = em.find(Member.class, "member1");
Team member = em.getReference(Team.class, "team1"); //SQL 실행 안함
member.setTeam(team); 

연관관계를 설정할 때는 식별자 값만 사용함으로 푸록시를 사용하면 데베 접근 회수를 줄일 수 있다.

즉시로딩과 지연로딩.

프록시는 주로 연관된 데이터를 지연로딩 할 때 사용된다.

연관된 엔티티의 조회 시점을 선택하는 두가지 방법
1. 즉시로딩 : 엔티티 조회 시 연관된 엔티티도 함께 조회. @ManyToOne(fetch = FetchType.EAGER)
2. 지연로딩: 연관된 엔티티를 실제 사용할떄 조회 @ManyToOne(fetch = FetchType.LAZY)

  • 즉시로딩을 해도, 하나의 쿼리문 (조인) 으로 데이터를 연관된 엔티티의 값들까지 한번에 가져온다.

!. NULL제약조건과 JPA 조인 전략

즉시 로딩 시 , 외부조인으로 연관객체를 가져온다.
외부조인을 사용하는 이유는 연관객체가 null을 허용하기 때문에, 내부조인으로 가져오면 관계가 null 객체를 가져오지 않기 때문이다.
하지만 외부조인은 내부조인보다 성능이 떨어진다.
두 문제를 해결하기 위해선 내부조인으로 데이터를 가져오고 연관객체가 null (외래키에 NOT NULL 제약 조건) 을 허용하지 못하게 하면 된다.

방법은
@JoinColumn(name="TEAM_ID", nullable = false) // 널을 허용하지 않는다. 내부조인 사용
또는
@ManyToOne(fetch =FetchType.EAGER, optional= false) // 널을 허용하지 않는다, 내부조인 사용 .

Member m = em.find(Member.class, "member1");
Team t = m.getTeam(); // t 는 프록시,,
t.getName(); // 팀 객체 실제 사용 (객체 초기화)

프록시와 컬랙션 래퍼

하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는데 이를 '컬랙션 래퍼' 라고 한다.

엔티티를 지연 로딩하면 프록시 객체를 사용해 지연로딩을 수행하지만 , 컬렉션은 컬랙션 래퍼가 지연로딩처리를 한다. 컬렉션 퍼도 프록시다.

List orders = member.getOrders() 를 호출하면 컬렉션은 초기화되지 않는다.
orders.get(0) 처럼 컬렉션에서 실제 데이터를 조회할 때 데이터베이스를 조회해서 초기화한다.

기본 패치 절략

연관된 엔티티가 하나면 즉시로딩, 컬렉션이면 지연로딩을 사용함. 단 모든 연관관계를 지연로딩 하는 것이 좋다.

컬렉션에 EAGER 사용 시 주의점

  1. 컬렉션을 하나 이상 즉시 로딩하는 것 권장 안함
  2. 컬렉션 즉시 로딩은 항상 외부조인을 사용한다.

영속성 전이 :CASCADE

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 영속상태로 만드려면 영속성 전이기능을 사용하면 된다,
쉽게 말해 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장한다.

@Entity
public class Parent {
    @ID
    Long id;

    @OneToMany(mappedBy="parent")
    List<Child> child = new ArrayList<>();
}

@Entity
public class Child {
    @id
    //

    @ManyToOne
    private Parent parent;
    ...
}

만약 부모 한명에 자식 2명를 저장하라면
부모엔티티를 영속 상태로 만들고 자식들도 영속 상태로 만들어야한다. 이때 영속성 전이를 사용하면 부모만 영속 상태로 만들면 연관된 자식까지 영속상태로 만들 수 있다.

    @OneToMany(mappedBy="parent", cascade = CascadeType.PERSIST)
    List<Child> child = new ArrayList<>();

    ...

    Child child = new Child();
    Child child2 = new Child();

    Parent p = new P();

    child.setParent(p); // 연관관계 추가.
    child2.setParent(p); // 연관관계 추가.

    parent.getChild().add(child);
    parent.getChild().add(child1);

    em.persist(parent); // 부모 저장, 연관된 자식들 저장.

영속성 전이는 연관관계를 매핑하는 것과 연관이 없다. 단지 엔티티를 영속화할때 연관된 엔티티도 같이 영속화하는 편리함을 제공하는 것 뿐인다.

엔티티를 삭제할 때는 CascadeType.REMOVE 로 설정함.

CASCADE 종류
ALL, PERSIST, MERGE, REMOVE, REFRESH , DETACH

cascase ={CascadeType.PERSIST , CascadeType.REMOVE} 처럼 여러 속성을 함께 사용할 수 있다.

PERSIST, REMOVE 는 em.flush() 를 호출 할 때 전이가 발생한다.

8.5 고아객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공한다. 이것을 "고아 객체 제거" 라고 한다.
부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제된다.

@OneToMany(mapped = "parent" , orphanRemoval = true)
private List children = new ArrayList<>();

Parent parent = em.find(Parent.class, id);
parent.getChildren().remove(0);
DELETE FROM CHILD WHERE ID = ?

처번째 자식을 제거했다. 오펀리무벌 = 트루 옵션으로 인해 컬렉션에서 엔티티를 제거하면 데베의 데이터도 삭제된다.
영속성 컨텍스트가 플러시할 때 적용된다.

영속성 전이 + 고아객체 , 생명주기

Cascade.ALL + orphanRemoval =true 를 동시에 사용하면 ?
부모 엔티티를 통해 자식의 생명주기가 관리된다.

자식을 저장하려면 부모에 등록만 하면 된다. (CASCADE)

Parent parent = em.find(Parent.class ,id);
parent.addChild(child);

삭제시..
parent.getChildren().remove(removeObject);

0개의 댓글