에러 해결을 통해 공부하는 지연로딩과 프록시

2023. 1. 18. 00:57프로젝트/[Sleeper] 수면관리 어플리케이션

728x90

 

프로젝트를 어느정도 마무리하고 실제 어떤 쿼리들이 실행되고 있는 지 확인하고 있는 도중 아래와 같은 상황을  목격했다.

 

 

문제 발생

 

현재, 회원테이블에서 회원의 아이디로 사용자를 찾으려고 한다.

내가 예상한 쿼리는 회원을 찾는 쿼리 1개인데 그 외의 쿼리들이 발생하고 있다.

 


select
        user0_.id as id1_4_,
        user0_.character_fk as characte9_4_,
        user0_.deal_fk as deal_fk10_4_,
        user0_.user_goal_sleep_time as user_goa2_4_,
        user0_.user_goal_wake_time as user_goa3_4_,
        user0_.user_role_type as user_rol4_4_,
        user0_.user_age as user_age5_4_,
        user0_.user_id as user_id6_4_,
        user0_.user_nick_name as user_nic7_4_,
        user0_.user_password as user_pas8_4_ 
    from
        user user0_ 
    where
        user0_.user_id=?



    select
        character0_.id as id1_0_0_,
        character0_.character_color as characte2_0_0_,
        character0_.character_experience as characte3_0_0_,
        character0_.character_level as characte4_0_0_,
        character0_.character_status as characte5_0_0_ 
    from
        character character0_ 
    where
        character0_.id=?
        
        
                      
    select
        user0_.id as id1_4_2_,
        user0_.character_fk as characte9_4_2_,
        user0_.deal_fk as deal_fk10_4_2_,
        user0_.user_goal_sleep_time as user_goa2_4_2_,
        user0_.user_goal_wake_time as user_goa3_4_2_,
        user0_.user_role_type as user_rol4_4_2_,
        user0_.user_age as user_age5_4_2_,
        user0_.user_id as user_id6_4_2_,
        user0_.user_nick_name as user_nic7_4_2_,
        user0_.user_password as user_pas8_4_2_,
        character1_.id as id1_0_0_,
        character1_.character_color as characte2_0_0_,
        character1_.character_experience as characte3_0_0_,
        character1_.character_level as characte4_0_0_,
        character1_.character_status as characte5_0_0_,
        deal2_.id as id1_1_1_,
        deal2_.money_date as money_da2_1_1_,
        deal2_.money_now as money_no3_1_1_,
        deal2_.money_change as money_ch4_1_1_ 
    from
        user user0_ 
    left outer join
        character character1_ 
            on user0_.character_fk=character1_.id 
    left outer join
        deal deal2_ 
            on user0_.deal_fk=deal2_.id 
    where
        user0_.character_fk=?
        
           
    
    select
        user0_.id as id1_4_2_,
        user0_.character_fk as characte9_4_2_,
        user0_.deal_fk as deal_fk10_4_2_,
        user0_.user_goal_sleep_time as user_goa2_4_2_,
        user0_.user_goal_wake_time as user_goa3_4_2_,
        user0_.user_role_type as user_rol4_4_2_,
        user0_.user_age as user_age5_4_2_,
        user0_.user_id as user_id6_4_2_,
        user0_.user_nick_name as user_nic7_4_2_,
        user0_.user_password as user_pas8_4_2_,
        character1_.id as id1_0_0_,
        character1_.character_color as characte2_0_0_,
        character1_.character_experience as characte3_0_0_,
        character1_.character_level as characte4_0_0_,
        character1_.character_status as characte5_0_0_,
        deal2_.id as id1_1_1_,
        deal2_.money_date as money_da2_1_1_,
        deal2_.money_now as money_no3_1_1_,
        deal2_.money_change as money_ch4_1_1_ 
    from
        user user0_ 
    left outer join
        character character1_ 
            on user0_.character_fk=character1_.id 
    left outer join
        deal deal2_ 
            on user0_.deal_fk=deal2_.id 
    where
        user0_.deal_fk=?

 

각각의 쿼리를 분석해보자!

 

첫 번째 쿼리는 회원의 아이디를 사용해 회원 엔티티를 조회하고 있다.

 

두 번째 쿼리는 케릭터 PK를 사용해 케릭터 엔티티를 조회하고 있다.

세 번째 쿼리는 유저 엔티티와 케릭터 엔티티 그리고 거래 엔티티를 join을 2번이나 써가면서 조회하고 있다.

네 번째 쿼리도 유저 엔티티와 케릭터 엔티티 그리고 거래 엔티티를 join을 2번이나 써가면서 조회하고 있다.

 

두 번째부터 네 번째 쿼리는 내가 의도하지 않는 쿼리이다.

이 쿼리가 왜 발생하고, 이 상황을 어떻게 해결할 수 있을 까??

 

현재 상황

 

현재 요청은 Controller를 거쳐 loginService.login() 메서드 그리고 userRepository의 findById() 메서드를 거치고 있다.

 

Service

 

public User login(LoginRequest loginRequest){
        return userRepository.findById(loginRequest.getUserId())
                .filter(u -> u.getUserPassword().equals(loginRequest.getUserPassword()))
                .orElse(null);
    }

 

Repository

public Optional<User> findById(String userId){
       return em.createQuery("select u from User u where u.userId = :userId ",User.class)
                .setParameter("userId",userId)
               .getResultStream().findFirst();
    }

 

제일 중요한 엔티티들의 연관관계를 봐주길 바란다!!

 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "USER")
public class User {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;
	
   ...

   @OneToMany(mappedBy = "user")
   private List<Diary> diaries = new ArrayList<>();
   
   @OneToMany(mappedBy = "user")
   private List<Sleep> sleeps = new ArrayList<>();
   
   @OneToOne()
   @JoinColumn(name = "DEAL_FK")
   private Deal deal;
   
   @OneToOne()
   @JoinColumn(name = "CHARACTER_FK")
   private Character character;

   ...


}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "CHARACTER")
public class Character {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
  	...
    
    @OneToOne(mappedBy = "character", fetch = FetchType.LAZY)
    private User user;
   
...
}

 

User 엔티티와 Deal 엔티티 그리고 User 엔티티와 Character 엔티티는 즉시 로딩을 사용하고 있다.

(@xxToOne은 fetch = FetchType.Eager이 Default값이다.)

 

 

문제 해결 + 관련 개념 정리

 

문제 해결은 간단하다. 엔티티간들간의 관계를 지연 로딩으로 바꾸면 된다.

 

먼저 문제의 원인부터 말하자면, 즉시 로딩을 사용한 것이 문제였다.

그렇다면, 즉시 로딩이 무엇일까??

 

 

즉시 로딩 vs 지연 로딩

 

 

먼저, 즉시 로딩이란 엔티티를 조회할 때, 연관된 엔티티도 함께 조회하는 것이다.

반대로, 지연 로딩이란 연관된 엔티티는 실제 사용할 때 조회하는 전략이다.

 

위 sql 코드에서 join 쿼리를 봤을 것이다.

 

대부분 JPA 구현체에서는 조회 쿼리를 최적화하기 위해서 join 쿼리를 사용한다.

 

아!! 그러면, 즉시 로딩을 지연 로딩으로 바꾸면 위 문제가 해결되는 것을 알았다.

 

그렇다면 지연 로딩이 일어나는 원리는 무엇일까???

이를 알기 위해서는 프록시라는 개념을 알아야한다.

 

먼저 이것의 원인에 대해서 알기 위해서는 '프록시'라는 개념을 알아야한다.

 

 

프록시

 

프록시란 지연 로딩 기능을 사용하기 위해 필요한 가짜 객체이다.

 

 

위에서 User 엔티티와 Character 엔티티는 1:1관계였다.

 

User user = em.find(User.class, userId);
Character character = user.getCharacter();
System.out.println("회원 이름: " + user.getUsername());

 

현재 시점까지는 user 엔티티만 DB에서 조회해 사용하고, character 엔티티는 DB에서 조회하지 않았다. -> 이를 지연 로딩이라한다.

이때 사용되는 것이 가짜 객체이다.

 

User user = em.getReference(User.class, "user1");
// 이 객체는 프록시 객체이다.

String name = user.getCharacter().getCharacterName();

 

 

실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다. 이를  프록시 객체의 초기화라고 한다.

 

프록시 객체는 실제 객체에 대한 참조를 보관하는 데 프록시 객체가 실제 객체에게 데이터를 요청하는 과정은 아래와 같다.

 

 

1. getCharacterName() 

2-1) 프록시 객체의 실제 엔티티 참조가 없을 경우에는 영속성 컨텍스트에 요청 -> DB에 조회 하여 실제 엔티티 객체를 생성한 후에 데이터를 찾아온다.

  • 이때 DB에 조회할 수 있는 이유는 프록시 객체에는 해당 객체의 PK값이 있다!

2-2) 실제 엔티티가 있을 경우, 즉 프록시 객체가 초기화 되어 있을 경우에는 실제 엔티티에 접근하여 데이터를 가져온다.

 

 

 

그러면, 즉시 로딩과 지연 로딩은 언제 사용해야되는 거야??

 

기본 전략은 지연 로딩을 사용하고, 비즈니스 상황에 따라 조회시 연관된 엔티티까지 가져와야할 경우에는 Fetch Join을 사용하여 가져오도록 하자!!

 

 

 

 

 

 

 

 

728x90