✍️
dodeon
  • 개발왕, 도던
  • 스프링 시큐리티 인 액션
    • 오늘날의 보안
    • 안녕! 스프링 시큐리티
    • 사용자 관리
    • 암호 처리
    • 인증 구현
    • 실전: 작고 안전한 웹 애플리케이션
    • 권한 부여 구성: 액세스 제한
    • 권한 부여 구성: 제한 적용
    • 필터 구현
    • CSRF 보호와 CORS 적용
    • 실전: 책임의 분리
    • OAuth 2가 동작하는 방법
    • OAuth 2: 권한 부여 서버 구현
  • 스프링 고급편
    • 스레드 로컬
    • 템플릿 메서드 패턴과 콜백 패턴
    • 프록시 패턴과 데코레이터 패턴
  • 스프링 입문
    • 프로젝트 환경설정
    • 스프링 웹 개발 기초
    • 회원 관리 예제 - 백엔드
    • 스프링 빈과 의존 관계
    • 회원 관리 예제 - MVC
    • 스프링 DB 접근 기술
      • JDBC
      • JPA
    • AOP
  • 스프링 핵심 원리
    • 객체 지향 설계와 스프링
      • 스프링의 탄생
      • 객체 지향 프로그래밍
      • 좋은 객체 지향 설계의 원칙
      • 객체 지향 설계와 스프링
    • 스프링 핵심 원리 이해
      • 회원 도메인 개발
      • 주문 도메인 개발
    • 객체 지향 원리 적용
      • 관심사의 분리
      • 새로운 구조와 정책 적용
      • 정리
      • IoC, DI, 컨테이너
      • 스프링으로 전환하기
    • 스프링 컨테이너와 스프링 빈
      • 스프링 빈 기본 조회
      • 동일 타입이 둘 이상일 때 조회
      • 상속일 때 조회
      • BeanFactory와 ApplicationContext
      • 다양한 설정 형식
      • 스프링 빈 설정 메타 데이터
    • 싱글턴 컨테이너
      • @Configuration과 싱글턴
    • 컴포넌트 스캔
      • 탐색 위치와 기본 탐색 대상
      • 필터와 중복 등록
    • 의존 관계 자동 주입
      • 롬복과 최신 트렌드
      • 조회 빈이 2개 이상일 때
      • 애너테이션 직접 만들기
      • 조회한 빈이 모두 필요할 때
      • 올바른 실무 운영 기준
    • 빈 생명 주기 콜백
      • 인터페이스 방식
      • 메서드 지정 방식
      • 애너테이션 방식
    • 빈 스코프
      • 프로토타입 스코프
      • Provider
      • 웹 스코프
  • 스프링 MVC
    • 웹 애플리케이션 이해
      • 서버
      • 서블릿
      • 멀티 스레드
      • HTML, HTTP API, CSR, SSR
      • 자바 백엔드 웹 기술 역사
    • 서블릿
      • HttpServletRequest
      • HTTP 요청 데이터
      • HttpServletResponse
      • HTTP 응답 데이터
    • 서블릿, JSP, MVC 패턴
      • 서블릿으로 만들기
      • JSP로 만들기
      • MVC 패턴
    • MVC 프레임워크 만들기
      • 프론트 컨트롤러 패턴
      • View 분리
      • Model 추가
      • 단순하고 실용적인 컨트롤러
      • 유연한 컨트롤러
      • 정리
    • 스프링 MVC의 구조 이해
      • 스프링 MVC 전체 구조
      • 핸들러 매핑과 핸들러 어댑터
      • 뷰 리졸버
      • 스프링 MVC 시작하기
      • 스프링 MVC 컨트롤러 통합
      • 스프링 MVC 실용적인 방식
    • 스프링 MVC 기본 기능
      • 프로젝트 생성
      • 로깅
      • 요청 매핑
      • HTTP 요청의 기본 및 헤더 조회
      • HTTP 요청 파라미터
      • HTTP 요청 메시지
      • HTTP 응답
      • HTTP 메시지 컨버터
      • 요청 매핑 핸들러 어댑터
    • 스프링 MVC 웹 페이지 만들기
    • 메시지, 국제화
      • 스프링 메시지 소스
    • Validation
      • BindingResult
      • FieldError, ObjectError
      • 오류 코드와 메시지 처리
      • Validator 분리
    • Bean Validation
      • Form 전송 객체 분리
      • HTTP 메시지 컨버터
    • 로그인
      • 쿠키
      • 세션
      • 서블릿 HTTP 세션
      • 서블릿 필터
      • 스프링 인터셉터
      • ArgumentResolver 활용
    • 예외 처리와 오류 페이지
      • 오류 화면 제공
      • 필터
      • 인터셉터
      • 스프링 부트 오류 페이지
    • API 예외 처리
      • 스프링 부트 기본 오류 처리
      • HandlerExceptionResolver
      • ExceptionResolver
      • ControllerAdvice
    • 스프링 타입 컨버터
      • Converter
      • ConversionService
      • 뷰 템플릿에 적용하기
      • Formatter
    • 파일 업로드
      • 서블릿과 파일 업로드
      • 스프링과 파일 업로드
      • 파일 업로드 및 다운로드 예제
  • 자바 ORM 표준 JPA 프로그래밍
    • JPA 소개
    • JPA 시작하기
    • 영속성 관리
      • 영속성 컨텍스트
      • 플러시
      • 준영속 상태
    • Entity 매핑
      • 객체와 테이블 매핑
      • 데이터베이스 스키마 자동 생성
      • 필드와 칼럼 매핑
      • 기본 키 매핑
      • 실전 예제
    • 연관 관계 매핑
      • 단방향 연관 관계
      • 양방향 연관 관계
      • 실전 예제
    • 다양한 연관 관계 매핑
      • 다대일
      • 일대다
      • 일대일
      • 다대다
      • 실전 예제
    • 고급 매핑
      • 상속 관계 매핑
      • 매핑 정보 상속
      • 실전 예제
    • 프록시와 연관관계 관리
      • 프록시
      • 즉시 로딩과 지연 로딩
      • 영속성 전이와 고아 객체
      • 실전 예제
    • 값 타입
      • 기본값 타입
      • 임베디드 타입
      • 값 타입과 불변 객체
      • 값 타입의 비교
      • 값 타입 컬렉션
      • 실전 예제
    • 객체 지향 쿼리 언어 - 기본
      • 기본 문법과 쿼리 API
      • 프로젝션
      • 페이징
      • 조인
      • 서브 쿼리
      • JPQL 타입 표현과 기타 식
      • 조건식
      • JPQL 함수
    • 객체 지향 쿼리 언어 - 중급
      • 경로 표현식
      • fetch join
      • 다형성 쿼리
      • Entity 직접 사용
      • Named 쿼리
      • 벌크 연산
  • 스프링 부트와 JPA 활용 - 웹 애플리케이션 개발
    • 프로젝트 환경설정
    • 도메인 분석 설계
      • 도메인 분석 설계
      • Entity 클래스 개발
      • Entity 설계 시 주의점
    • 애플리케이션 아키텍처
    • 회원 도메인 개발
    • 상품 도메인 개발
    • 주문 도메인 개발
      • Entity, 리포지토리, 서비스 개발
      • 주문 기능 테스트
      • 주문 검색 기능 개발
    • 웹 계층 개발
      • 변경 감지와 병합
  • 스프링 부트와 JPA 활용 - API 개발과 성능 최적화
    • API 개발 기본
      • 회원 등록 API
      • 회원 수정 API
      • 회원 조회 API
    • 지연 로딩과 조회 성능 최적화
      • Entity 직접 노출
      • Entity를 DTO로 변환
      • JPA에서 DTO 직접 조회
    • 컬렉션 조회 최적화
      • Entity 직접 노출
      • Entity를 DTO로 변환: 페치 조인
      • Entity를 DTO로 변환: 페이징과 한계 돌파
      • DTO 직접 조회
      • DTO 직접 조회: 컬렉션 조회 최적화
      • DTO 직접 조회: 플랫 데이터 최적화
      • 정리
    • OSIV와 성능 최적화
  • 스프링 데이터 JPA
    • 예제 도메인 모델
    • 공통 인터페이스 기능
      • 순수 JPA 기반 리포지토리
      • 공통 인터페이스 설정
    • 쿼리 메서드 기능
      • JPA Named Query
      • @Query
      • 파라미터 바인딩
      • 반환 타입
      • 페이징과 정렬
      • 벌크성 수정 쿼리
      • @EntityGraph
      • JPA Hint & Lock
    • 확장 기능
      • 사용자 정의 리포지토리
      • Auditing
      • Web 확장
    • 스프링 데이터 JPA 분석
    • 나머지 기능
      • Specifications
      • Query By Example
      • Projections
      • Native Query
  • Querydsl
    • 프로젝트 환경 설정
    • 예제 도메인 모델
    • 기본 문법
      • JPQL vs Querydsl
      • Q-Type 활용
      • 검색 조건
      • 결과 조회
      • 정렬
      • 페이징
      • 집합 함수
      • 조인
      • 서브 쿼리
      • Case 문
      • 상수, 문자 더하기
    • 중급 문법
      • 프로젝션과 결과 반환
      • 동적 쿼리
      • 수정, 삭제 벌크 연산
      • SQL Function 호출
    • 순수 JPA와 Querydsl
      • 순수 JPA 리포지토리와 Querydsl
      • 동적 쿼리와 성능 최적화 조회
      • 조회 API 컨트롤러 개발
    • 스프링 데이터 JPA와 Querydsl
      • 스프링 데이터 페이징 활용
      • 스프링 데이터 JPA가 제공하는 Querydsl 기능
  • 데이터 접근 핵심 원리
    • JDBC 이해
      • JDBC와 최신 데이터 접근 기술
      • 데이터베이스 연결
      • JDBC 개발
  • 백엔드 시스템 실무
    • CPU bound 애플리케이션
      • CPU를 극단적으로 사용하는 애플리케이션
      • 스트레스 테스트 툴로 성능 측정
      • Dockerized 애플리케이션 GCP 배포
      • Jenkins를 이용한 배포
    • CPU bound 애플리케이션 무중단 배포
      • nginx를 통한 로드밸런싱 구성
      • 서버를 늘려서 성능 측정
    • 배포 자동화와 협업을 위한 Git
      • GitHub Webhook과 jenkins로 배포 자동화
      • 머지할 때 발생하는 충돌 해결하기
      • 실무에서 유용한 Git 꿀팁
    • I/O bound 애플리케이션
    • Message Queue를 도입하여 데이터 유실 방지
      • 스트레스 테스트
    • 검색과 분석을 위한 저장소 ElasticSearch
    • Kubernetes
  • 모든 개발자를 위한 HTTP 웹 기본 지식
    • 인터넷 네트워크
      • IP
      • TCP, UDP
      • PORT
      • DNS
    • URI와 웹 브라우저 요청 흐름
    • HTTP 기본
      • 클라이언트-서버 구조
      • stateful, stateless
      • 비 연결성
      • HTTP 메시지
    • HTTP 메서드
    • HTTP 메서드 활용
    • HTTP 상태 코드
    • HTTP 헤더 - 일반
      • 표현
      • 콘텐츠 협상
      • 전송 방식
      • 정보
      • Authorization
      • 쿠키
    • HTTP 헤더 - 캐시
      • 검증 헤더와 조건부 요청
      • 조건부 요청 헤더
      • 프록시 캐시
      • 캐시 무효화
  • 김영한의 실전 자바
    • 제네릭
  • 예제로 배우는 스프링 입문
    • 예제로 배우는 스프링 입문(개정판)
      • PetClinic 예제
      • 스프링 IoC
      • 스프링 AOP
      • 스프링 PSA
  • 스프링 프레임워크 핵심 기술
    • 스프링 프레임워크 핵심 기술
      • IoC 컨테이너와 빈
        • 스프링 IoC 컨테이너와 빈
        • ApplicationContext와 빈 설정
        • @Autowired
        • @Component와 컴포넌트 스캔
        • 빈의 스코프
        • Environment
        • MessageSource
        • ApplicationEventPublisher
        • ResourceLoader
      • Resource/Validation
        • Resource 추상화
        • validation 추상화
      • 데이터 바인딩 추상화
      • SpEL
      • 스프링 AOP
      • Null-Safety
  • 스프링 부트 개념과 활용
    • 스프링 부트 원리
      • 자동 설정
      • 내장 서버
        • 컨테이너와 서버 포트
        • HTTPS와 HTTP2
      • 독립적으로 실행 가능한 JAR
    • 스프링 부트 활용
      • Spring Application
      • 외부 설정
      • 프로파일
      • 로깅
      • 테스트
      • Spring Boot Devtools
    • 스프링 웹 MVC
      • 소개
      • HttpMessageConverters
      • ViewResolver
      • 정적 리소스
      • 웹 JAR
      • index 페이지와 파비콘
      • ExceptionHandler
      • Spring HATEOAS
      • CORS
  • THE JAVA
    • JVM 이해하기
      • 자바, JVM, JDK, JRE
      • JVM 구조
      • 클래스 로더
      • Heap
      • Garbage Collector
    • 리플렉션
      • 클래스 정보 조회
  • The Java - Test
    • JUnit 5
      • JUnit 시작하기
      • JUnit 시작하기
    • Mockito
Powered by GitBook
On this page
  • 프록시 기초
  • em.find()
  • em.getReference()
  • 프록시의 특징
  • 프록시 객체의 초기화
  • 주의 사항
  • 준영속 상태의 프록시
  • 프록시 유틸리티 메서드

Was this helpful?

  1. 자바 ORM 표준 JPA 프로그래밍
  2. 프록시와 연관관계 관리

프록시

Previous프록시와 연관관계 관리Next즉시 로딩과 지연 로딩

Last updated 3 years ago

Was this helpful?

Member와 Team이 연관 관계가 맺어져 있을 때 Member를 조회하면 Team도 매번 함께 조회해야 할까?

public class App {
    public void printUserAndTeam(String memberId) {
        ...

        Member member = em.find(Member.class, memberId);
        Team team = member.getTeam();

        System.out.println("회원 이름: " + member.getUsername());
        System.out.println("소속팀: " + team.getName());
    }
}
public class App {
    public void printUser(String memberId) {
        ...

        Member member = em.find(Member.class, memberId);
        Team team = member.getTeam();

        System.out.println("회원 이름: " + member.getUsername());
    }
}
  • 첫 번째처럼 소속 팀이 필요하다면 한 방에 가져오는 게 좋다.

  • 두 번째처럼 회원 정보만 필요하다면 team을 가져올 필요도 없다.

    • 이 경우 team을 매번 가져오면 리소스를 낭비하게 된다.

  • 이 문제를 해결하려면 프록시를 이해해야 한다.

프록시 기초

em.find()

  • 데이터베이스를 통해서 실제 Entity 객체를 조회한다.

public class App {
    public class App {
        public void printUserAndTeam(String memberId) {
            ...

            Member member = em.find(Member.class, memberId);
            // 출력 등 사용하는 로직 없음
        }
    }
}
  • 데이터를 사용하지 않고 find()만 했을 때도 select 쿼리를 실행한다.

em.getReference()

  • 데이터베이스 조회를 미루는 가짜(프록시) Entity 객체를 조회한다.

  • DB에 쿼리가 안 날아가는데 객체가 조회된다.

public class App {
    public void printUserAndTeam(String memberId) {
      ...

        Member member = em.getReference(Member.class, memberId);
        // 출력 등 사용하는 로직 없음
    }
}
  • select 쿼리가 나가지 않았다.

public class App {
    public void printUserAndTeam(String memberId) {
        Member member = em.getReference(Member.class, memberId);
        // 데이터 사용
        System.out.println("회원 이름: " + member.getUsername());
    }
}
  • member 데이터를 실제 호출하는 순간에 select 쿼리가 출력된다.

public class App {
    public void printUserAndTeam(String memberId) {
        Member member = em.getReference(Member.class, memberId);
        // 클래스 정보 조회
        System.out.println("회원 이름: " + member.getClass());
    }
}
  • 클래스를 출력해보면 Proxy라는 글자가 보인다.

  • 하이버네이트가 강제로 만든 가짜 클래스라는 뜻이다.

  • em.getReference()는 프록시를 사용한다.

    • 진짜 객체를 주는 게 아니라 프록시라는 가짜 객체를 준다.

    • 껍데기만 있고 안은 텅텅 빈 상태다.

프록시의 특징

  • 실제 클래스를 상속 받아서 만들어진다.

    • 따라서 실제 클래스와 겉모습이 같다.

    • 내가 직접 상속하는 게 아니라 하이버네이트가 내부적으로 라이브러리를 사용해 상속한다.

  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용한다.

  • 프록시 객체는 실제 객체의 참조(target)을 보관한다.

  • 프록시 객체에 있는 메서드를 호출하면, 프록시 객체는 실제 객체의 메서드를 호출한다.

    • getId()를 호출하면 프록시는 target에 있는 getId()를 대신 호출한다.

    • 하지만 맨 처음에는 DB 조회가 되지 않은 상태이므로 target이 비어있을 것이다. 이때는 어떻게 될까?

프록시 객체의 초기화

public class App {
    public void getMemberName() {
        // 실제 객체가 아닌 프록시 객체를 가져온다.
        Member member = em.getReference(Member.class, "id");
        // 처음에 getName()을 호출하면 target에 값이 없는 상태다.
        // 그럼 영속성 컨텍스트에 데이터를 요청해 그 값을 반환한다.
        member.getName();
    }
}
  1. 데이터를 요청했는데 Member의 target에 데이터가 없다.

  2. JPA가 진짜 Member 객체를 가져오라고 영속성 컨텍스트에 요청한다.

  3. 영속성 컨텍스트는 DB를 조회해서 실제 Entity 객체를 생성해 보내준다.

  4. target과 진짜 객체인 Entity를 연결한다.

  5. target의 진짜 getName()을 통해서 값을 반환한다.

public class App {
    public void printUserAndTeam(String memberId) {
        Member member = em.getReference(Member.class, memberId);
        // 실제 레퍼런스 조회
        System.out.println("회원 이름: " + member.getUsername());
    }
}
  • userName을 실제 가져다 쓰는 시점에 영속성 컨텍스트로 Member를 요청해서 실제 레퍼런스를 가지게 된다.

public class App {
    public void printUserAndTeam(String memberId) {
        Member member = em.getReference(Member.class, memberId);
        // 실제 레퍼런스 조회
        System.out.println("회원 이름: " + member.getUsername());
        // 다시 하면 프록시에서 조회
        System.out.println("회원 이름: " + member.getUsername());
    }
}
  • 이제 target에 값이 있기 때문에 다음에 다시 조회해도 DB에 쿼리를 다시 날리지 않는다.

주의 사항

  • 프록시 객체는 처음 사용할 때 한 번만 초기화 된다.

    • 한 번 초기화하면 그 내용을 그대로 사용한다.

public class App {
    public void printUserAndTeam(String memberId) {
        Member member = em.getReference(Member.class, memberId);

        System.out.println("before: " + member.getClass());
        System.out.println("회원 이름: " + member.getUsername());
        System.out.println("after: " + member.getClass());  // before와 같은 값 출력
    }
}
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 Entity로 바뀌는 것은 아니다.

    • 초기화 되면 프록시 객체를 통해 실제 엔티티에 접근할 수 있을 뿐이다.

    • 그래서 프록시를 통해 데이터를 가져온 뒤에도 getClass()의 값은 $Proxy...로 동일하다.

public class App {
    public void printUserAndTeam(String memberId) {
        Member member1 = new Member();
        member1.setUsername("member1");
        em.persist(member1);

        Member member2 = new Member();
        member2.setUsername("member2");
        em.persist(member2);

        em.flush();
        em.clear();

        Member m1 = em.find(Member.class, member1.getId());
        Member m2 = em.find(Member.class, member2.getId());

        // find()로 가져왔고 타입을 정확하게 비교하는 것이므로 true를 출력한다.
        System.out.println("m1 == m2: " + (m1.getClass() == m2.getClass()));

        Member m3 = em.getReference(Member.class, member1.getId());
        Member m4 = em.find(Member.class, member2.getId());

        // m3는 getReference()이므로 false를 출력한다.
        System.out.println("m1 == m2: " + (m1.getClass() == m2.getClass()));

        tx.commit();
    }
}
public class App {
    public static void main(String[] args) {
        Member m1 = em.find(Member.class, member1.getId());
        Member m2 = em.find(Member.class, member2.getId());
        // true
        System.out.println("m1 == Member: " + (m1.getClass() == m2.getClass()));

        Member m1 = em.find(Member.class, member1.getId());
        Member m2 = em.getReference(Member.class, member2.getId());
        // false
        System.out.println("m1 == Member: " + (m1.getClass() == m2.getClass()));
    }

    public void logic(Member m1, Member m2) {
        // 실제 로직 상에서는 실제 인티티가 넘어올지 프록시가 넘어올지 모르기 때문에
        // 비교할 때는 instance of를 사용해야 한다.
        // false
        System.out.println("m1 == m2: " + (m1 == m2));
        // true
        System.out.println("m1 == m2: " + (m1 instanceof m2));
        System.out.println("m2 == m2: " + (m2 instanceof m2));

    }
}
  • 프록시 객체는 원본 Entity를 상속받는다.

    • 즉, 프록시인 멤버와 아닌 멤버가 타입이 맞지 않을 수 있어 주의해야 한다.

      • 타입 비교 시 ==를 쓰면 실패하고, instance of를 사용해야 한다.

      • 프록시를 쓸지 안 쓸지 모르므로 JPA에서는 웬만하면 instance of를 사용하는 것이 좋다.

public class App {
    public void printUserAndTeam(String memberId) {
        Member member1 = new Member();
        member1.setUsername("member1");
        em.persist(member1);

        em.flush();
        em.clear();

        Member m1 = em.find(Member.class, member1.getId());
        // find()로 진짜 객체를 가져오기 때문에 'Member'라고 출력된다.
        System.out.println("m1 = " + m1.getClass());

        // find()한 뒤에 getReference()로 가져오게 되면
        Member m2 = em.getReference(Member.class, member1.getId());
        // 'Proxy'가 아니라 'Member'로 출력된다.
        System.out.println("m2 = " + m2.getClass());

        // 프록시든 아니면 한 영속성 컨텍스트에서 가져오고 PK가 같다면 항상 true가 된다.
        System.out.println("m1 == reference = " + (m1 == reference));

        tx.commit();
    }
}
  • find() 후 getReference()를 호출하면 실제 Entity를 반환한다.

    • 이미 멤버를 영속성 컨텍스트 1차 캐시에 넣어뒀기 때문에 굳이 프록시로 가져올 필요가 없다.

      • getReference()로 수행할 수 있는 성능 최적화에 대한 이점이 없다.

  • 실제 객체와 레퍼런스로 가져온 객체를 같다고 취급한다.

    • JPA는 한 트랜잭션 안에서 PK가 같다면 같은 객체임을 보장한다.

    • 원본과 레퍼런스를 == 비교 시 항상 true가 나온다.

public class App {
    public void printUserAndTeam(String memberId) {
        Member member1 = new Member();
        member1.setUsername("member1");
        em.persist(member1);

        em.flush();
        em.clear();

        Member m1 = em.getReference(Member.class, member1.getId());
        System.out.println("m1 = " + m1.getClass());

        Member reference = em.getReference(Member.class, member1.getId());
        System.out.println("reference = " + reference.getClass());

        // true
        System.out.println("a == a: " + (m1 == reference));

        tx.commit();
    }
}
  • 둘 다 레퍼런스로 받을 때도 같은 프록시로 출력된다.

  • 하나의 영속성 컨텍스트에 있을 때 같다는 걸 보장해줘야 하기 때문이다.

public class App {
    public void printUserAndTeam(String memberId) {
        Member member1 = new Member();
        member1.setUsername("member1");
        em.persist(member1);

        em.flush();
        em.clear();

        // 프록시로 불러온 다음
        Member refMember = em.getReference(Member.class, member1.getId());
        // 출력을 위해 실제 값으로 초기화 한다.
        // 클래스 값은 프록시로 출력된다.
        System.out.println("refMember = " + refMember.getClass());

        Member findMember = em.find(Member.class, member1.getId());
        // 프록시가 초기화 되었으니 당연히 Member 타입이 출력되어야 하는 것 아닌가? 할 수 있지만
        // JPA는 PK가 같으면 무조건 같음을 보장해줘야 하기 때문에 프록시로 나온다.
        System.out.println("findMember = " + findMember.getClass());

        // JPA에서는 무조건 이게 참이 되도록 맞춘다!
        System.out.println("a == a: " + (refMember == findMember));

        tx.commit();
    }
}
  • 프록시가 초기화 된 상태에서 find()를 하면 어떻게 될까?

  • JPA는 기본적으로 refMember == findMember의 값이 true임을 보장해야 한다.

  • 따라서 refMember, findMember 모두 프록시로 출력된다.

  • find()를 했기 때문에 실제 DB를 조회하면서 select 쿼리는 찍힌다.

  • 하지만 프록시를 한 번 조회한 뒤에는 find()를 한 객체에도 프록시로 반환한다.

    • 그래야 JPA의 룰을 보장할 수 있기 때문이다.

    • 처음에 엔티티로 반환하면 엔티티로, 프록시로 반환하면 계속 프록시로 반환한다.

  • 중요한 건 프록시든 아니든 개발에 문제가 없도록 짜는 것이다. instance of를 기억하자.

준영속 상태의 프록시

  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태이면, 프록시를 초기화할 때 문제가 발생한다.

    • 하이버네이트는 org.hibernate.LazyInitializationException 예외를 터뜨린다.

public class App {
    public void printUserAndTeam(String memberId) {
        Member member1 = new Member();
        member1.setUsername("member1");
        em.persist(member1);

        em.flush();
        em.clear();

        // 프록시 생성
        Member refMember = em.getReference(Member.class, member1.getId());
        // 프록시로 출력한다.
        System.out.println("refMember = " + refMember.getClass());

        // 실제 DB를 조회하면서 쿼리를 날리고 프록시를 초기화 한다.
        refMember.getUsername();
        // 타입은 여전히 프록시로 출력된다.
        System.out.println("refMember = " + refMember.getClass());

        tx.commit();
    }
}
  • 같은 영속성 컨텍스트 안에서는 같은 프록시를 출력한다.

public class App {
    public void printUserAndTeam(String memberId) {
        Member member1 = new Member();
        member1.setUsername("member1");
        em.persist(member1);

        em.flush();
        em.clear();

        // 프록시 생성
        Member refMember = em.getReference(Member.class, member1.getId());
        // 프록시로 출력
        System.out.println("refMember = " + refMember.getClass());

        // detach(), clear(), close()로 영속성 컨텍스트를 준영속으로 만든다.
        em.close();

        // 실제 데이터로 초기화 하면서 데이터를 가져와야 하지만
        // 영속성 컨텍스트로 관리하지 않게 되면서 exception이 떨어진다.
        refMember.getUsername();
        System.out.println("refMember = " + refMember.getClass());

        tx.commit();
    }
}
  • detach(), clear(), close()로 준영속 상태가 되면 LazyInitializationException을 던진다.

  • 프록시를 초기화 할 때 영속성 컨텍스트를 통하기 때문이다.

  • 트랜잭션이 끝났는데 조회 하려고 할 때 많이 만나는 에러이므로 기억해두도록 하자.

프록시 유틸리티 메서드

프록시 인스턴스의 초기화 여부 확인

public class App {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory();
        ...
        // 앞에서 초기화 했다면 true, 아니라면 false
        System.out.println("isLoaded: " + emf.getPersistenceUnitUtil().isLoaded(refMember));
    }
}
  • PersistenceUnitUtil.isLoaded(Object entity)

    • 해당 프록시가 초기화 됐는지 확인하는 메서드

프록시 클래스 확인

  • entity.getClass().getName()

프록시 강제 초기화

public class App {
    public void printUserAndTeam(String memberId) {
        Member member1 = new Member();
        member1.setUsername("member1");
        em.persist(member1);

        em.flush();
        em.clear();

        Member refMember = em.getReference(Member.class, member1.getId());
        System.out.println("refMember = " + refMember.getClass());

        // 이런 방식으로 강제 호출하는 것 보다는
        refMember.getUsername();
        // 이 방식을 더 권유한다.
        Hibernate.initialize(refMember);

        tx.commit();
    }
}
  • org.hibernate.Hibernate.initialize(entity)

    • Hibernate에서 제공하는 것이며, JPA 표준은 강제 초기화가 없다.

      • member.getName()처럼 강제 호출 해야 한다.