본문 바로가기
컴퓨터 프로그래밍/Spring

[Spring] Projection 및 예시코드 설명

by 한33 2024. 10. 7.
  • entity 전체의 값을 가져오는 것이 아닌 조회 대상을 지정해 원하는 값만 조회할 수 있도록 지원하는 클래스
  • DB조회 시 select * from table 간에 사용되는 * 가 모든 필드를 가져오므로, 불필요한 메모리 낭비가 될 수 있음.

 

Projections 사용법 4가지

  • bean: setter로 주입하며, 필드명이 일치해야함
  • field: setter 없이 주입 가능하며, 필드명이 일치해야함
  • constructor
    • 생성자 기반으로 삽입하여, 네이밍 상관 없이 생성자 필드순으로 바인딩
    • 생성자에 입력한 값 순서로 확인하기 때문에 컴파일 시점에 오류 확인이 불가하여, 런타임 간에 발견될 수 있음
    • 바인딩 DTO에 @QueryProjection가 있으면 컴파일 시점에 entity와 마찬가지로 Q클래스를 생성
    • 미리 생성된 Q클래스에 의해 컴파일 시점에 오류 확인 가능
    • QueryDSL을 사용하기 위한 클래스를 별도 관리 필요annotation

     


constuctor 방식

TodoProjectionDto

@Getter
@RequiredArgsConstructor
public class TodoProjectionDto {

    private final String title;
    private final int mamnagerCount;
    private final int commentCount;

 

출력하고자 하는 데이터 형식 Dto 를 만들어준다.

 

TodoQueryRepositoryImpl

.select(
        Projections.constructor(
                TodoProjectionDto.class,
                todo.title,             // 할일 제목
                todo.managers.size(),   // 담당자 수
                todo.comments.size()    // 댓글 수
        )
)

 

Projections.constructor 을 이용해서 위와 같이 필요한 정보들을 매칭시켜준다.


@annotation 방식

 

TodoProjectionDto

@Getter
public class TodoProjectionDto {

    private final String title;
    private final int mamnagerCount;
    private final int commentCount;

    @QueryProjection
    public TodoProjectionDto(String title, int mamnagerCount, int commentCount) {
        this.title = title;
        this.mamnagerCount = mamnagerCount;
        this.commentCount = commentCount;
    }

}

 

생성자 앞에 @QueryProjection 어노테이션을 달아주고 build 를 해서 Q클래스를 만들어준다.

 

 

그럼 위와 같이 Q 클래스가 나온 것을 볼 수 있다.

 

TodoQueryRepositoryImpl

.select(
        new QTodoProjectionDto(
                todo.title,             // 할일 제목
                todo.managers.size(),   // 담당자 수
                todo.comments.size()    // 댓글 수
        )
)

 

그런 다음 QTodoProjectionDto 를 이용해서 매칭시켜준다.


예시코드2

@Override
public Page<TodoProjectionDto> findByIdFromProjection(String title, String nickname, LocalDate start, LocalDate end, Pageable pageable) {

    BooleanBuilder builder = new BooleanBuilder();

    // 제목 검색
    if (title != null && !title.isEmpty()) {
        builder.and(titleContains(title));
    }

    // 닉네임 검색
    if (nickname != null && !nickname.isEmpty()) {
        builder.and(nicknameContains(nickname));
    }

    // 생성일 범위 검색
    if (start != null && end != null) {
        builder.and(createdDateBetween(start, end));
    } else if (start != null) {
        builder.and(createdDateAfter(start));
    } else if (end != null) {
        builder.and(createdDateBefore(end));
    }

    List<TodoProjectionDto> content = queryFactory
            .select(
                    new QTodoProjectionDto(
                            todo.title,             // 할일 제목
                            todo.managers.size(),   // 담당자 수
                            todo.comments.size()    // 댓글 수
                    )
            )
            .from(todo)
            .leftJoin(todo.managers)
            .leftJoin(todo.comments)
            .where(builder)
            .orderBy(todo.createdAt.desc())     // 생성일 최신순 조회
            .offset(pageable.getOffset())       // 페이지 넘버
            .limit(pageable.getPageSize())      // 페이지 크기 ( 페이지당 할일 표시 수 )
            .fetch();

    long total = Optional.ofNullable(queryFactory
            .select(todo.count())
            .from(todo)
            .where(builder)
            .fetchOne())
            .orElse(0L);

    return new PageImpl<>(content, pageable, total);
}

// todoId 로 조회 메서드
private BooleanExpression todoIdEq(Long todoId) {
    return todoId != null ? todo.id.eq(todoId) : null;
}

// 제목 검색 포함 조회 메서드
private BooleanExpression titleContains(String title) {
    return title != null ? todo.title.contains(title) : null ;
}

// 닉네임 검색 포함 조회 메서드
private BooleanExpression nicknameContains(String nickname) {
    return nickname != null ? todo.managers.any().user.nickname.contains(nickname) : null ;
}

// 생성일 범위 검색 메서드
private BooleanExpression createdDateBetween(LocalDate start, LocalDate end) {
    return todo.createdAt.between(start.atStartOfDay(), end.atTime(LocalTime.MAX));
}

// start 날짜 이상 범위 검색 메서드
private BooleanExpression createdDateAfter(LocalDate start) {
    return todo.createdAt.goe(start.atStartOfDay());
}

// end 날짜 이하 범위 검색 메서드
private BooleanExpression createdDateBefore(LocalDate end) {
    return todo.createdAt.loe(end.atTime(LocalTime.MAX));
}

 

todo.managers.size(),   // 담당자 수
todo.comments.size()    // 댓글 수

todo.managers.size(), todo.comments.size() 를 이용해서 몇 개의 데이터가 있는지 확인할 수 있음.

.leftJoin(todo.managers)
.leftJoin(todo.comments)

leftJoin 을 사용한 이유는 todo 는 있는데 comments, managers 가 없는 경우에 null 값으로라도 들어가서 조회시켜야하기 때문에 leftJoin 을 사용함.

 

.orderBy(todo.createdAt.desc())     // 생성일 최신순 조회

createdAt 순으로 내림차순 정렬한다.

 

.offset(pageable.getOffset())       // 페이지 넘버
.limit(pageable.getPageSize())      // 페이지 크기 ( 페이지당 할일 표시 수 )

페이지네이션을 위한 offset, limit 설정

 

long total = Optional.ofNullable(queryFactory
        .select(todo.count())
        .from(todo)
        .where(builder)
        .fetchOne())
        .orElse(0L);

페이지네이션을 위해 total 값을 얻기 위해서 todo.count() 호출

 

// 닉네임 검색 포함 조회 메서드
private BooleanExpression nicknameContains(String nickname) {
    return nickname != null ? todo.managers.any().user.nickname.contains(nickname) : null ;
}

nickname 을 받고 QueriyDSL 에서 조건을 표현하는데에 사용하는 객체인 BooleanExpression 으로 반환.

 

먼저 입력된 nickname 이 null 인지 확인. null 이라면 조건을 생성하고, null 이라면 null 그대로 반환

 

조건

todo.managers.any().user.nickname.contains(nickname)

 

  • todo.managers.any(): Todo 엔티티에서 managers 컬렉션의 어떤 항목이라도 선택하는 부분임.
  • 이때 any()는 managers 리스트에 하나 이상의 Manager 객체가 있을 때 조건을 적용함.
  • user.nickname.contains(nickname): 선택된 Manager의 User 엔티티에서 nickname 속성을 참조함. contains(nickname)는 nickname이 User의 nickname에 포함되어 있는지를 체크