Book Review

자바 ORM 표준 JPA 프로그래밍 - 프록시와 연관관계

빈코 2022. 12. 3. 15:57

이전 포스팅에서는 JPA의 고급 매핑에 대해 알아보았다. 이번 포스팅에서는 프록시와 즉시 로딩, 지연 로딩 및 영속성 전이에 대해 알아보려고 한다😄

 

프록시 & 연관관계

 

개요

객체는 객체 그래프로 연관된 객체들을 탐색한다. 그런데 객체가 데이터베이스에 저장되어 있으므로 연관된 객체를 마음껏 탐색하기는 어렵다. JPA 구현체들은 이 문제를 해결하려고 프록시라는 기술을 사용한다.

 

프록시를 사용하면 연관된 객체를 처음부터 데이터베이스에 조회하는 것이 아니라, 실제 사용하는 시점에 데이터베이스에서 조회한다. 하지만 자주 함께 사용하는 객체들을 조인을 사용해서 함께 조회하는 것이 효과적이다. JPA는 즉시 로딩지연 로딩이라는 방법으로 둘을 모두 지원한다.

 

또한, JPA는 연관된 객체를 함께 저장하거나 함께 삭제할 수 있는 영속성 전이고아 객체 제거라는 편리한 기능을 제공한다.

 

다음은 예제에 사용할 엔티티 클래스이다.

@Entity
public class Member {
	
    private String username;
    
    @ManyToOne
    private Team team;
    
    public Team getTeam() {
    	return team;
    }
    
    public String getUsername() {
    	return username;
    }
}

@Entity
public class Team {
	
    private String name;
    
    public String getName() {
    	return name;
    }
}

 

프록시📗

public void printUserAndTeam(String memberId) {
    Member member = em.find(Member.class, memberId);
    // Team 조회 X
    Team team = member.getTeam();
    // Team 조회 O
}

위 코드와 같이 회원과 팀이 연관관계가 설정이 되어 있더라도, getTeam() 메서드를 호출하기 전까지는 Team 객체를 조회하지 않는다. 개발자가 필요한 건 회원 객체뿐인데, 자동으로 팀 객체까지 조회해오는 것은 효율적이지 않기 때문이다.

 

JPA는 이런 문제를 해결하려고 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이것을 지연 로딩이라고 한다. 쉽게 이야기해서 team.getName() 처럼 팀 엔티티의 값을 실제 사용하는 시점에 데이터베이스에서 팀 엔티티에 필요한 데이터를 조회하는 것이다.

 

이처럼 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라 한다😀

 

상속

프록시 클래스는 실제 클래스를 상속받아서 만들어지므로 실제 클래스와 겉모양이 같다. 따라서 사용하는 입장에서는 이것이 진짜 객체인지 프록시 객체인지 구분하고 않고 사용하면 된다.

 

위임

또한, 프록시 객체는 실제 객체에 대한 참조를 보관한다. 그리고 프록시 객체의 메서드를 호출하면 프록시 객체는 실제 객체의 메서드를 호출한다. 

 

프록시 객체의 초기화📘

프록시 객체는 member.getName()처럼 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라 한다. 

 

Member member = em.getReference(Member.class, "id");
// getReference는 프록시 객체를 반환한다.(실제 객체X)
member.getName();

 

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

📌 특 징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
  • 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
  • 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환한다.
  • 초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 따라서 영속성 콘텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제가 발생한다.

 

프록시 확인📒

boolean isLoad = em.getEntityManagerFactory()
			.getPersistenceUnitUtil().isLoaded(entity);
            
System.out.println("isLoad = " + isLoad); // 초기화 여부 확인

 

JPA가 제공하는 PersistenceUnitUtil.isLoaded(Object Entity) 메소드를 사용하면 프록시 인스턴스의 초기화 여부를 확인할 수 있다, 아직 초기화되지 않은 프록시 인스턴스는 false를 반환한다. 이미 초기화되었거나 프록시 인스턴스가 아니면 true를 반환한다.

 

System.out.println("memberProxy = " + member.getClass().getName());
// 결과 : memberProxy = binco.domain.Member_$$_javassist_O

 

조회한 엔티티가 진짜 엔티티인지 프록시로 조회한 것인지 확인하려면 클래스명을 직접 출력해보면 된다. 다음 예를 보면 클래스 명 뒤에.. javassist.. 라 되어 있는데 이것으로 프록시인 것을 확인할 수 있다. 

 

즉시 로딩과 지연 로딩📙

프록시 객체는 주로 연관된 엔티티를 지연 로딩할 때 사용한다. 앞선 예처럼, 회원 엔티티를 조회할 때 연관된 팀 엔티티도 함께 데이터베이스에서 조회하는 것이 좋을까? 아니면 회원 엔티티만 조회해 두고 팀 엔티티는 실제 사용하는 시점에 데이터베이스에서 조회하는 것이 좋을까? 이것은 상황마다 다르기 때문에, JPA는 개발자가 연관된 엔티티의 조회 시점을 선택할 수 있도록 다음 두 가지 방법을 제공한다.

 

📌 즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.

- 예 : em.find(Member.class, "member1")를 호출할 때 회원 엔티티와 연관된 팀 엔티티도 함께 조회한다.

- 설정 방법 : @ManyToOne(fetch = FetchType.EAGER)

 

📌 지연 로딩 : 연관된 엔티티를 실제 사용할 때 조회한다.

- 예 : member.getTeam(). getName()처럼 조회한 팀 엔티티를 실제 사용하는 시점에 JPA가 SQL을 호출해서 팀 엔티티를 조회한다.

- 설정 방법 : @ManyToOne(fetch = FetchType.LAZY)

 

즉시 로딩📔

@Entity
public class Member {
	...
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    ...
}

// 실제 사용
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); //객체 그래프 탐색

 

회원과 팀을 즉시 로딩으로 설정했기 때문에 em.find()를 이용하여 회원을 조회하는 순간 팀도 함께 조회한다. 이때 회원과 팀 두 테이블을 조회해야 하므로 쿼리를 2번 실행할 것 같지만, 대부분의 JPA 구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용한다.

 

SELECT
    M.MEMBER_ID AS MEMBER_ID,
    M.TEAM_ID AS TEAM_ID,
    M.USERNAME AS USERNAME,
    T.TEAM_ID AS TEAM_ID,
    T.NAME AS NAME
FROM
	MEMBER M LEFT OUTER JOIN TEAM T
         ON M.TEAM_ID=T.TEAM_ID
WHERE
	M.MEMBER_ID='member1'

 

실행된 SQL을 분석해보면 회원과 팀을 조인해서 쿼리 한 번으로 조회한 것을 알 수 있다. 이후 member.getTeam()을 호출하면 이미 로딩된 팀 엔티티를 반환한다.


NULL 제약조건과 JPA 조인 전략

 

  • 회원 테이블에 TEAM_ID 외래 키는 NULL 값을 허용, 따라서 JPA는 외부 조인(OUTER)을 사용한다.
  • 회원은 팀에 소속되지 않은 회원이 있을 가능성이 존재 그런 경우 내부 조인하면 팀은 물론이고 회원 데이터도 조회할 수 없는 상황이 발생할 수 있다.
  • 외부 조인보다 내부 조인이 성능과 최적화에서 유리하다. (내부 조인이 외부 조인보다 속도가 빠름)
  • 외래 키에 NOT NULL 제약 조건을 설정하면 내부 조인만 사용해도 가능하다. (nullable 설정)
    • @JoinColumn(nullable = true) : NULL 허용(기본값), 외부 조인 사용
    • @JoinColumn(nullable = false) : NULL 허용 안 함, 내부 조인 사용
    //nullable 속성 사용
    @Entity
    public class Member {
       ...
       @ManyToOne(fetch = FetchType.EAGER)
       @JoinColumn(name = "TEAM_ID", nullable = false)
       private Team team;
       ...
    }
    
    //@ManyToOne.optional 속성 사용
    @Entity
    public class Member {
       ...
       @ManyToOne(fetch = FetchType.EAGER, optional = false)
       @JoinColumn(name = "TEAM_ID")
       private Team team;
       ...
    }

 

지연 로딩📕

//즉시 로딩 설정
@Entity
public class Member {
   ...
   @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "TEAM_ID")
   private Team team;
   ...
}

//사용 예시
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); //객체 그래프 탐색
team.getName(); //팀 객체 실제 사용
//em.find(Member.class, "member1") 호출시
SELECT * FROM MEMBER
WHERE MEMBER_ID = 'member1'

//team.getName() 호출시
SELECT * FROM TEAM
WHERE TEAM_ID = 'team1'
  • em.find(Member.class, "member1") 회원만 조회 team변수에는 프록시 객체로 대체한다.
  • 프록시 객체는 실제 사용될 때까지 데이터 로딩을 미룸, 그것이 지연 로딩이다.
  • 조회 대상이 영속성 콘텍스트에 이미 있으면 프록시가 아닌 실제 객체를 사용한다.

 


👨‍👩‍👦‍👦 오픈채팅방 운영

취업을 준비하는 예비 개발자분들을 위한 질문&답변할 수 있는 공간을 만들었습니다. 취업과 이직을 하기 위해서 어떤 걸 중점적으로 준비해야 하는지부터 포트폴리오&이력서 작성법 등 다양한 질문들을 받고 답변을 드립니다. 참여하셔서 다양한 정보 얻고 가시면 좋을 것 같네요😁

 

참여코드 : 456456

https://open.kakao.com/o/gVHZP8dg

 

비전공 개발자 취업 준비방(질문&답변)

#비전공 #개발자 #취업 #멘토링 #부트캠프 #국비지원 #백엔드 #프론트엔드 #중소기업 #중견기업 #자바 #Java #sql

open.kakao.com

 


👨‍💻 전자책 출간

아울러 제가  🌟비전공자에서 2년만에 보안 전문 중견기업으로 이직 한 방법들을 정리한 전자책을 출간 하게 되었습니다. 어떤 걸 공부해야 하는지, 이직을 위해서 무엇을 준비해야 하는지, 제가 받았던 기술 면접 리스트 등 다양한 목차로 구성되어 있습니다. 또한, 구매 시 1:1 채팅을 이용하여 포트폴리오 첨삭을 도와드리고 있습니다. 🐕전자책으로 얻은 모든 수익은 유기견 센터 '팅*벨 입양센터'에 후원될 예정입니다. 관심 있으신 분들은 아래 링크를 참고해주세요😁

https://kmong.com/gig/480954

 

비전공개발자 2년만에 중견기업 들어간 방법 | 14000원부터 시작 가능한 총 평점 0점의 전자책, 취

0개 총 작업 개수 완료한 총 평점 0점인 Binco의 전자책, 취업·이직 전자책 서비스를 0개의 리뷰와 함께 확인해 보세요. 전자책, 취업·이직 전자책 제공 등 14000원부터 시작 가능한 서비스

kmong.com


 

마치며

지금까지 프록시와 연관관계를 알아보았다. 지난번에 Eager와 LAZY에 대해 포스팅한 적이 있었는데, 이 번 책을 읽으면서 다시 한번 정리할 수 있어서 좋았다. 조금 더 자세한 사항은 아래 링크를 통해 확인해보시면 될 것 같다😁

 

 

JPA Eager Loading VS Lazy Loading

JPA를 사용하다 보면 즉시로딩(Eager)과 지연로딩(Lazy)을 자연스레 접하게 됩니다. 사실 실무에서 모든 연관관계는 지연 로딩으로 설정하는 것이 좋지만, 이렇게 하는 설정하는 법이 왜 좋은지에

binco.tistory.com

 

반응형