프록시(Proxy)란?
프록시란, 간단하게 말해 누군가를 대신하여 권한을 받아 수행하는 주체를 말한다. 해당 글에서는 JPA에서 사용되는 프록시 객체에 대한 이야기를 하겠다.
프록시 클래스는 실제 클래스(엔티티)를 상속받아 만들어진다. 따라서 비즈니스 로직에서는 이것이 프록시 객체인지, 실제 엔티티인지 구분없이 사용 가능하다.
JPA에서 프록시 패턴의 사용
비지니스 로직이 실행될 때, 어떤 엔티티 객체를 조회하는데 그 엔티티와 관계를 맺고 잇는 다른 엔티티들의 조회도 함께 이루어져야 할 때가 있다. 그런데 그 중에서 한 개의 엔티티만 조회가 이루어져도 상관이 없는 상황에서는 성능상의 손해가 발생할 것이다. 이러한 성능 저하가 발생하지 않게 하기 위하여 '실제 사용되는 시점'에 DB에서 조회하도록 하는 기술이 지연 로딩이고, 이 지연 로딩을 위해 프록시(가짜 엔티티)가 사용된다.
프록시 사용 - 지연 로딩
데이터베이스를 통해서 실제 엔티티 객체를 조회하는 EntityManager.find()와 다르게 데이터베이스 조회를 미루는 가짜 엔티티 객체를 조회하는 함수이다. 아래 코드만 실행시켜보면 DB에 쿼리문이 나가지 않는 모습을 볼 수 있다.
...
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear(); //영속성 콘텍스트 초기화
// Member findMember = em.find(Member.class, member.getId()); //엔티티 객체 조회
Member findMember = em.getReference(Member.class, member.getId());
하지만 여기서 findMember객체를 사용해보면
System.out.println("findMember.id = " + findMember.getId()); //식별자값은 알고있음
System.out.println("findMember.username = " + findMember.getUsername()); //DB에서 가져오지 않으면 알 수 없다
그제서야 쿼리문이 나가서 print문이 출력되는 모습을 볼 수 있다. 이것은 프록시가 엔티티가 사용될 때까지 조회하지 않고 필요한 시점에 조회하기 때문에 그렇다.
getReference()함수를 사용하면 실제 엔티티를 상속받는 가짜 엔티티 객체가 hibernate에 의해 만들어지고, 그 가짜 객체, 즉 프록시 객체를 조회하게 된다. 이 프록시 객체는 실제 객체의 참조를 보관하고 있어서, 프록시 객체의 메소드를 호출하면 프록시 객체가 실제 객체의 메소드를 호출한다.
프록시 초기화
만약에 getReference를 통해서 실제 엔티티 객체의 메소드를 호출하려 했는데 프록시 객체에 실제 객체의 참조가 존재하지 않는다면, 영속성 콘텍스트를 통해서 DB를 조회하여 실제 엔티티를 생성하여 참조하고, 이후에는 프록시 객체를 통해서 엔티티에 접근할 수 있게 된다. 이것을 프록시 초기화라고 한다.
따라서 위의 코드에서도 getUsername()메소드를 호출받았을때 프록시 객체에 실제 객체의 참조가 존재하지 않는 경우, DB에 접근해서 실제 엔티티 객체를 만든 후 값을 가져온 것이다. 따라서 한번 호출해서 참조값을 가진 후에는 다시 초기화될 필요가 없게 된다. 그리고 만약 영속성 콘텍스트에 엔티티가 이미 존재해서 DB를 조회할 필요가 없는 경우에는 프록시의 존재 의미가 없기 때문에 getReferece를 호출하더라도 실제 엔티티를 반환해주게 된다.
또한, 초기화시 영속성 컨텍스트를 이용해서 초기화하기 때문에 도중에 준영속상태가 될 경우에 에러가 발생한다.
-> 추가예정
-> 추가) 프록시 초기화는 영속성 콘텍스트의 도움을 받아야 하므로, 영속성 콘텍스트의 도움을 받을 수 없는 상태인 준영속상태의 프록시를 초기화하면 예외(LazyInitializationException)가 터진다.
-> 추가2) 준영속 상태: https://eckrin.tistory.com/18, 엔티티를 영속성 컨텍스트에서 분리한 상태를 말하며, 더 이상 EntityManager가 관리하지 않는다.
프록시 특징 제대로 이해하기
1. 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
2. 프록시 객체를 초기화할 때, 프록시 객체를 실제 엔티티로 바꿔치기 하는것이 아니라, 프록시 객체를 통해서 실제 엔티티에 접근할 수 있게 된다.
3. 프록시 객체는 원본 엔티티를 상속받는다.
즉시 로딩과 지연 로딩
연관관계 매핑이 걸려있을 때, 연관관계가 있다는 이유만으로 모든 객체정보들을 가져오게 되면 실행시간과 성능에 손해를 볼 것이다. 이를 사용자가 효율적으로 컨트롤하기 위해 jpa는 지연로딩이라는 옵션을 제공한다.
//Member클래스
...
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
...
//Team클래스
...
@OneToMany(MappedBy = "team")
private List<Member> members = new ArrayList<>();
...
Member클래스가 fk를 가지고 있고, Team클래스가 역참조가 가능하게 양방향 다대일 매핑이 이루어진 상황이다. 이때 주인객체의 매핑스타일 어노테이션에
@ManyToOne(fetch = FetchType.LAZY)
와 같이 FetchType을 LAZY로 설정해주면 멤버클래스만 DB에서 조회한다.
FetchType.LAZY로 어노테이션 설정을 하게 되면 Member로만 조회하면 Team은 조회하지 않고, Team정보를 필요로 하면 그때 Team에 관한 쿼리가 나가는 것을 볼 수 있다. 즉, 지연 로딩을 사용하면 실제 사용하는 시점 이전에는 프록시가 초기화되지 않으며, 관련 메소드를 호출하는 시점에서 초기화되어 실제 엔티티가 프록시 객체의 참조변수에 할당된다.
반대로 즉시 로딩을 사용하면 Member만 조회시에도 연관관계가 설정된 Team까지 가져온다. @OneToMany 관계이거나 테이블이 복잡하게 얽혀있는 상황이라면 즉시 로딩을 사용하면 원하지 않는 쿼리문들이 join을 위해 나가게 되니, 큰 스케일의 프로젝트라면 지연 로딩만을 사용하자.
@ManyToOne 기본값: FetchType.EAGER //(매핑된 값이 하나이므로 연관 엔티티를 그냥 들고올게)
@OneToMany 기본값: FetchType.LAZY //(매핑된 엔티티가 몇개인지 모르니까 필요하면 알아서 들고올게)
영속성 전이
엔티티의 상태 변화가 있을 때, 연관된 엔티티에도 상태 변화를 전이시키는 옵션이다. 부모-자식관계에 있는 도메인에 적용할 수 있다.
다시말해서 특정 엔티티를 영속 상태로 만들때 연관된 엔티티도 모두 영속상태로 만드는 것. (연관관계 매핑이나 즉시로딩/지연로딩과는 관련이 없다). 원래는 부모객체와 자식객체를 모두 EntityManager.persist해주어야 하지만, 영속성 전이를 이용하면 부모객체와 자식객체 모두 영속상태로 만들 수 있다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) //밑에있는것들에 대해 영속성 전이한다.
private List<Child> children = new ArrayList<>();
단일 엔티티에 종속적인 경우에만 사용하도록 하자. (Parent-Child 엔티티관계에서 Child가 Parent에 의해서만 소유될 때)
+
이렇게 자식 엔티티와 연결되어있을 때 부모 엔티티를 삭제하려고 할 경우 cascade 설정을 해주지 않으면 위와 같이 오류가 뜨는데, cascade = CascadeType.REMOVE로 속성을 지정해주면 해결된다.
++1
그런데 REMOVE설정 없이 orphanRemoval=TRUE로만 지정해줘도 문제가 발생하지 않는다. CascadeType.REMOVE와 orphanRemoval속성 모두 부모 엔티티가 삭제되었을 경우 연관된 자식 엔티티를 제거한다. cascade를 REMOVE속성을 지정하는 것과의 차이는 부모 엔티티에서 자식 엔티티를 삭제하는 경우 CascadeType.REMOVE는 자식 엔티티가 유지되지만, orphanRemoval=TRUE의 경우 부모 엔티티에서 제거된 자식 엔티티를 제거한다(delete 쿼리가 나간다)
++2
cascade와 orphanRemoval 모두 부모-자식 관계가 1:1일때만 사용해야 한다. 그렇지 않으면 한쪽 부모가 삭제되었다고 해서 삭제되서는 안될 자식이 삭제될 수도 있다.
고아 객체
부모 엔티티와 관계가 끊어진 자식 엔티티를 말한다..
@OneToMany(mappedBy = "parent", CascadeType.ALL, orphanRemoval = TRUE)
private List<Child> children = new ArrayList<>();
위 @OneToMany 어노테이션을 보면 orphanRemoval옵션을 TRUE로 설정해서 고아 객체를 자동으로 제거하도록 설정할 수 있다. 영속성 전이와 같이 특정 엔티티가 단일 소유될때만 사용해야 한다.
스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화하고, em.remove()로 제거한다. 이 때 위와같이 설정해주면 부모 엔티티를 통해서 자식의 생명주기까지 같이 관리할 수 있게 된다.
em.persist(parent); //부모를 영속시키면 자식까지 영속
em.remove(parent); //부모를 삭제하면 자식의 생명주기 종료
CascadeType.ALL에 orphanRemoval=TRUE까지 지정해주면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.
'[ Backend ] > Spring DB, JPA' 카테고리의 다른 글
[JPA] 객체지향 쿼리 언어(JPQL) 이해하기 (0) | 2022.01.17 |
---|---|
[JPA] 값 타입 (0) | 2022.01.15 |
[JPA] 연관관계 매핑(3) - 상속관계 매핑 (0) | 2022.01.11 |
[JPA] 연관관계 매핑(2) - 다양한 연관관계 (0) | 2022.01.06 |
[JPA] 연관관계 매핑(1) - 객체와 테이블 (0) | 2022.01.04 |