개발자로 후회없는 삶 살기
[문법] 프록시 메커니즘과 부모 자식 life cycle 본문
서론
※ 과거에 기록한 내용에서 중요한 부분만 발췌하여 모두가 이해하기 쉽게 다시 서술한다.
본론
- 프록시
프록시가 뭔지 알아보고 활용해보자, 예를들어서 멤버에 팀이 있는데 멤버를 조회할 때 팀도 DB에서 무조건 조회해야 할까? 고민하는 상황이다.
멤버와 팀을 같이 출력하는 경우라면, 회원과 연관된 팀을 불러와서 출력하고, 이런 비즈니스 로직에서는 멤버를 가져오고 팀도 한 번에 한 방 쿼리로 가져오면 좋을 것이다.
근데, 팀은 출력하지 않고 회원만 출력하려고 하면? DB에서 연관됐다고 해서 사용하지 않는 팀도 가져오면 손해이다. 언제는 멤버와 팀을 같이 사용하고 언제는 멤버만 사용하고 하면, 낭비를 고려해야 한다. JPA는 이걸 프록시로 해결한다.
⇒ 프록시 기초
em.find : DB에서 진짜 객체를 가져온다.
em.getReference : DB에서 가짜 프록시 객체를 가져온다.
find는 DB에 쿼리가 나가는데 refer는 쿼리가 안나가는데 객체가 조회가 되긴 된다.
1) find
find를 하면 회원과 팀의 조인 쿼리가 실제 DB로 날라간다.
2) reference
refer를 하면 쿼리가 날라가지 않지만 객체가 조회되고,
가져온 객체를 사용할 때 쿼리가 나간다.
getRefer를 호출하는 시점에 쿼리를 안하고 실제 사용하는 시점에 쿼리를 날린다.
가져온 객체를 실행해보면 하이버네이트의 프록시 객체이다.
⇒ 매커니즘
refer은 실제 객체를 가져오는 것이 아니라, 프록시 객체를 가져오고 빈 껍대기에 내부에 target을 실제 객체를 가지고 있다. 초기에 target은 null 값이다.
→ 특징
프록시는 실제 객체를 상속받아서, 진짜인지 가짜인지 구분하지 않고 사용해도 된다.
프록시 객체의 메서드를 호출하면 가지고 있는 target 실제 객체의 메서드를 대신 호출한다. 처음엔 target이 null이다.
→ 프록시 객체의 초기화
1) 최초 프록시 객체의 target은 null로 초기화 되어있다.
2) 프록시의 메서드를 호출하면 프록시가 컨텍스트에서 초기화를 요청한다.
3) 컨텍스트가 요청을 받아서 실제 객체를 영속화한다.
4) 영속화된 실제 객체의 메서드를 호출한다.
프록시의 메서드를 실제로 사용할 때 컨텍스트를 통해서 초기화한다. 초기화란 컨텍스트에 실제 객체를 가져오는 것이다.
- 프록시의 특징
1. 처음 사용할 때 한 번만 초기화
여러번 호출해도 첫 번째 초기화한 진짜 객체를 계속 사용한다.
2. 초기화하면 프록시가 실제 엔터티로 바뀌는게 아니라 접근만 가능
교체되는 것이 아니고, 내부에 타겟에만 채워지는 것이다.
3. 타입 체크 시 유의
프록시 객체는 원본 엔터티를 상속 받아서 타입 체크 시 유의해야 한다. jpa에서 엔터티의 타입 비교 시 == 비교가 아닌, instanceof를 사용해야 한다.
멤버를 find하고 class 타입을 == 비교해보면 같은 class 타입이라서 true가 나온다.
refer를 하면 == 비교가 false가 나온다. instanceof는 상속 관계를 포함해서, 프록시와 타겟의 관계를 검사할 수 있다.
🚨 눈으로 봐도 알지 않아요?
같은 메서드에 있으면 알 수 있겠지만, 파라미터로 들어오면 알 수가 없다.
따라서, jpa를 쓰면 타입 비교를 절대로 == 말고 instanceof를 써야한다. 프록시가 교체되는게 아니고, target만 채워지는 것이기 때문이다.
4. getRefer에서 실제 엔터티를 반환하는 경우
컨텍스트에 이미 실제 엔터티가 있으면 refer를 해도 실제 엔터티를 반환한다.
find 이후, refer를 하면 프록시 객체가 아닌, 실제 객체를 가져온다.
이렇게 하는 이유 🚨
1) 한 번 가져온 실제 객체를 두고 프록시를 가져오는 것은 성능상 이점이 없다.
2) jpa에서는 같은 트랜잭션에서 같은 pk를 가지는 엔터티는 반드시 같음을 보장한다. 이를 보장하기 위해 같은 pk로 가져온 객체는 == 비교가 가능하도록, refer을 해도 실제 객체를 가져온다.(같은 트랜잭션에서 find를 먼저 하고 refer를 했을 때 == 비교가 가능하다는 거지, 그게 아니면 instance 비교를 해야한다.)
→ ref 이후 find
ref 이후 find를 하면 당연히 실제 객체를 가져와야 할 것 같다. 하지만 프록시 객체가 나온다. 이 또한 jpa에서 == 비교를 보장하기 위함이다. 프록시 객체를 가져온다고 해도, DB에 쿼리는 날라가고, 데이터는 가져온다. 프록시 객체만 반환하는 것일 뿐이지, 데이터 조회는 일어난다.
핵심 ✅
find를 해도 프록시가 나올 수 있기에 프록시 객체가 들어가도 아무런 문제가 없도록 개발을 해야한다.
5. 준영속 상태에서 프록시 초기화
프록시는 컨택스트가 관리하는 객체이다. 프록시가 컨텍스트에서 해제되고 초기화를 하면 무슨 일이 일어나나 보자
deteach로 프록시를 해제하고 메서드를 호출하여 실제 객체를 초기화하려고 하면, 프록시의 도움을 받아서 초기화를 해야하는데 그렇지 못해 예외가 발생한다.
세션이 연결되지 않았다는 예외가 발생한다. 이 예외는 진짜 많이 만나는 예외로, 준영속의 프록시가 있다는 것을 알 수 있어야 한다.
- 즉시 로딩과 지연 로딩
회원을 사용할 때 팀도 반드시 같이 사용하면 함께 조회하는 것이 좋지만, 회원만 조회하면 회원만 가져오는 것이 성능상 좋다.
→ 지연 로딩
JPA는 이를 지연 로딩으로 제공한다. 멤버의 팀은 없는 프록시이다. 멤버 클래스만 db에서 조회한다.
멤버의 팀을 출력해보면 프록시 객체가 출력되고,
원래는 멤버를 가져올 때 조인으로 팀도 같이 가져왔는데, 멤버만 가져온 sql 모습이다.
프록시 팀의 메서드를 호출했을 때, 팀을 컨텍스트에 초기화하고 쿼리가 나간다.
=== 사이에 쿼리가 나가면서 초기화한다.
→ 내부 매커니즘
멤버를 로딩할 때 팀 참조는 지연로딩이라서 프록시를 가져오고, 팀을 사용할 때 초기화된다. 멤버만 사용하면 지연로딩을 해야한다. 근데 반대로 로직에서 팀과 회원을 무조건 같이 쓰면 멤버, 팀이 따로 쿼리가 나가면 성능상 손해이다.
→ 멤버와 팀을 같이 쓰면? ✅
대부분 팀과 회원을 같이 쓰면 한 방 쿼리로 네트워크를 타지 않고, 같이 가져오는 것이 좋다. 즉시 로딩으로 함께 조회할 수 있다. eager로 바꾸면 한번에 조인해서 가져오고, 프록시를 사용하지 않고 실제 객체를 사용한다.
- 프록시와 즉시로딩 주의사항
실무에서는 즉시 로딩을 절대로 사용하면 안 된다.
→ jpql에서 n+1 문제 발생
즉시 로딩은 JPA의 find를 직접 사용하면, 쿼리가 한 번 날라가지만, JPQL을 사용하면 회원을 조회 시 즉시 로딩이라도, 각각 쿼리가 2번 나간다. em.find는 pk로 찾아오는 것을 jpa가 내부적으로 다 최적화되어있는데 jpql은 sql을 그대로 날리는 것이라서 멤버만 가져온다.
만약 회원이 3개 있다면 3개의 회원을 가져오지만 추가로 1번 더 쿼리가 날라가는 n+1 문제가 발생한다.
- 영속성 전이 cascade
부모를 저장할 때 연관된 자식 엔터티도 같이 저장하고 싶을 때 사용한다. 지연 로딩과는 상관없는 내용이다.
노예인 다측에서 List를 가지고, 자식을 List에 편의 메서드로 넣어서, 주인의 객체를 가지고 노예에서 자신과 주인 객체를 넣는 상황이다.
이를 전부 다 저장하려면 3개의 persist가 필요하다.
실행해보면 3개의 쿼리가 나간다.
하지만, 부모가 저장될 때 자식도 같이 저장되면 좋을 것이다.
이럴 때 List에 cascade를 붙인다. 1측에 cascade.ALL을 하면
persist는 1측만 했는데, 3개의 insert 쿼리가 날라가고 3개가 저장된다. persist를 할 때 cascade를 표시한 객체도 같이 저장하는게 cascade이다.
⇒ 옵션
ALL : 모든 life cycle을 부모와 함께 수행
PERSIST : 부모를 저장할 때만 자식도 같이 저장
REMOVE : 부모를 삭제할 때만 자식도 같이 삭제
ALL, PERSIST, REMOVE만 사용하게 된다.
✅ 언제 쓰나요?
1대다에 다 거는게 아니다. 하나의 부모가 자식들을 관리할 때 의미가 있다. 게시판, 첨부파일처럼 하나의 부모만이 자식을 관리할 때 사용한다. 파일 여러개를 하나의 게시물이 관리하기 때문에 사용할 수 있다.
🚨 언제 안 쓰나요?
하나의 부모 외에 다른 곳에서도, 자식을 참조하면 사용하면 안 된다. 다른 데랑 연관이 있으면 쓰면 안되고, 소유자가 하나 일때만 써야한다. 단일 엔터티에 완전히 종속된다면 라이플 싸이클이 완전히 똑같아서 써도 된다.
- 고아 객체
부모의 List에서 자식을 제거할 때, 자식이 자동으로 삭제되는 기능을 제공하고, 이를 고아라고 한다.
부모의 List에서 remove를 하면 고아가 되어 삭제된다.
자동으로 delete 쿼리가 나간다.
🚨 주의점
함부로 쓰면 큰일 나며, 참조하는 기능이 하나 (게시판, 파일)일 때만 사용해야 한다.
cascade remove와 차이점은 cascade는 부모가 삭제되면 자식도 삭제되는 건데, 고아 객체는 부모는 살아있고, 부모의 List에서 자식을 제거하면 delete 쿼리가 나가는 것이다. 부모가 삭제되어도 자식은 고아가 되어 cascade remove 처럼 동작한다.
부모를 제거하면, 자식도 고아가 되기 때문에 자식 2개도 같이 제거된다.
이 둘은 같이 사용할 수 있다. 이를 다 키면 부모 엔터티를 통해서 자식의 생명주기를 관리할 수 있다.
'[백엔드] > [JPA | 학습기록]' 카테고리의 다른 글
[문법] 스프링 데이터 JPA 기능 (0) | 2024.08.12 |
---|---|
[문법] JPQL과 페치 조인 성능 최적화 (0) | 2024.08.11 |
[문법] 연관관계 매핑 시 고려사항 (0) | 2024.08.11 |
[문법] 다양한 연관관계 매핑 (3) | 2024.08.10 |
[문법] JPA 동작 원리 (0) | 2024.08.09 |