Spring

SpringData JPA야, 나는 엔티티 일부만 필요한데? (Projection)

sehunbang 2024. 3. 6. 18:34

조심하세요! 별(*) 은 점점 커져요!

 

select * 은 날이 갈수록 커집니다.

JPARepository 로 find() 또는 findAll() 메소드를 사용할때 Entity 단위로 조회해오고 있죠?

이걸 속칭 ⭐별쿼리라고 합니다.

 

 

🚨 이렇게 조회하면 당연히 필드가 많아질수록 느려지겠죠? 🚨

 

그럼 일부 필드만 조회해서 성능을 최적화 하려면 어떻게 해야할까요?

 

Projection의 기능

  1. 원하는 필드만 지정해서 조회 가능
  2. 여러필드 합쳐서 재정의 필드(Alias) 조회 가능 (Nested 프로젝션)
  3. 조회 필드 그룹을 인터페이스 또는 클래스로 만들어놓고 재사용 가능

Porjection 필드 사용방법

 

 

1. get필드() 메소드로 정의

  • 정의한/원하는 필드만 조회하기 때문에 Closed 프로젝션 이라고 한다.
  • 쿼리를 줄이므로 최적화 할 수 있다.
  • 메소드 형태이기 때문에 Java 8의 메소드를 사용해서 연산을 할 수 있다.
public interface UserProfile {

  String getUsername();

  String getProfileImageUrl();

 
}

 

 select username, profileImageUrl from user; // closed projection //이 나간다 라고 보면 됩니다.

 

@Value 로 정의 (이게 있는 순간 * 로 바뀜

  • 전체 필드를 조회할 수 밖에 없어서 Open 프로젝션 이라고 한다.
  • @Value(SpEL)을 사용해서 연산을 할 수 있다. (https://www.baeldung.com/spring-expression-language)
  • 스프링 빈 들의 메소드도 호출 가능하다.
    • 스프링 빈 메소드 호출 예시
@Value("#{target.profileImageUrl != null}")
boolean hasProfileImage();

 

 

 

쿼리 최적화를 할 수 없다. SpEL을 엔티티 대상으로 사용하기 때문에.

 

 

SqEL 샘플 보기 // 출처 : Spring Expression Language Guide // https://www.baeldung.com/spring-expression-language 

Type       

Operators Arithmetic  +, -, *, /, %, ^, div, mod

Relational  <, >, ==, !=, <=, >=, lt, gt, eq, ne, le, ge

Logical      and, or, not, &&, ||, !

Conditional  ?:

Regex        matches 

 

 

 

Projection 구현체 정의방법

 

1. 인터페이스 기반 Projection

  • Projection 을 Interface 처럼 사용하는 방법
public interface UserRepository extends JpaRepository<User, Long>, QueryByExampleExecutor<User> {

  List<UserProfile> findByUsername(String username);
public interface UserProfile {

  String getUsername();

  String getProfileImageUrl();

  @Value("#{target.profileImageUrl != null}")
  boolean hasProfileImage();

  default String getUserInfo() {
    return getUsername() + " " + (hasProfileImage() ? getProfileImageUrl() : "");
  }

 

 

2. 클래스 기반 Projection (@getter 랑 @AllArgsConstructor 같이 메소드 필요)

  • Projection 을 DTO 클래스 처럼 사용하는 방법

 

public interface UserRepository extends JpaRepository<User, Long>, QueryByExampleExecutor<User> {
  List<UserInfo> findByPassword(String password);

  List<UserProfile> findByUsername(String username);

 

Nested 처럼 하고 싶으면 메소드로 

아래 getUserInfo() 처럼

@Getter
@AllArgsConstructor
public class UserInfo {

  private String username;

  private String password;

  public String getUserInfo() {
    return username + " " + password;
  }
}

 

테스트

@Test
void projectionTest() {
  // given
  var newUser = User.builder().username("user").profileImageUrl("http://").password("pass")
      .build();
  var newChannel = Channel.builder().name("new-channel").build();

  // when
  var savedChannel = channelRepository.save(newChannel);
  var savedUser = userRepository.save(newUser);
  var newUserChannel = newChannel.joinUser(newUser);

  // then interface projection
  var userProfiles = userRepository.findByUsername("user");
  System.out.println("interface projection : ");
  userProfiles.forEach(userProfile -> System.out.println(userProfile.hasProfileImage()));
  assert !userProfiles.isEmpty();

  // then class projection
  var userInfos = userRepository.findByPassword("pass");
  System.out.println("class projection : ");
  userInfos.forEach(userInfo -> System.out.println(userInfo.getUserInfo()));
  assert !userInfos.isEmpty();

  // then dynamic projection
  var userProfiles2 = userRepository.findByProfileImageUrlStartingWith("http", UserProfile.class);
  System.out.println("dynamic projection : ");
  userProfiles2.forEach(userInfo -> System.out.println(userInfo.getProfileImageUrl()));
  assert !userProfiles2.isEmpty();
}