Spring
SpringData JPA야, 나는 엔티티 일부만 필요한데? (Projection)
sehunbang
2024. 3. 6. 18:34
조심하세요! 별(*) 은 점점 커져요!
select * 은 날이 갈수록 커집니다.
JPARepository 로 find() 또는 findAll() 메소드를 사용할때 Entity 단위로 조회해오고 있죠?
이걸 속칭 ⭐별쿼리라고 합니다.
🚨 이렇게 조회하면 당연히 필드가 많아질수록 느려지겠죠? 🚨
그럼 일부 필드만 조회해서 성능을 최적화 하려면 어떻게 해야할까요?
Projection의 기능
- 원하는 필드만 지정해서 조회 가능
- 여러필드 합쳐서 재정의 필드(Alias) 조회 가능 (Nested 프로젝션)
- 조회 필드 그룹을 인터페이스 또는 클래스로 만들어놓고 재사용 가능
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();
}