Spring Data JPA: DB 님, 안 쓰는 컬럼은 빼고 주세요 (JPA Projection)

Spring Data JPA: DB 님, 안 쓰는 컬럼은 빼고 주세요 (JPA Projection)

🗄 Spring Data JPA - Projection

JPA Repository로 조회하기

👩‍🎓 이런 글을 보시는 분들은 대부분 JPA Repository의 기본 조회 요령을 아실 것 같습니다만, 그래도 기본 전제는 공유해야 글에 좋겠죠.

JPA Repository는 기본적으로 몇 가지 조회 메서드를 제공하고, 또 네이밍 규칙만 따르면 알맞게 동작하는 커스텀 조회 메서드를 추가할 수 있습니다. 다음은 네이밍 규칙의 간단한 예시입니다.

findImSoCoolByUsername(String username);
  • find로 시작: 조회하겠단 뜻입니다.

  • ImSoCool: 몇 가지 키워드 또는 구분할 아무 말을 적는 영역입니다.

  • By부터는 SQLWHERE절에 가까운 역할입니다.

쉽죠? 정리하면 이런 느낌이에요.

find + 몇_가지_키워드_또는_구분할_아무_말 + By + 필드_이름_등 + (필드_목록 등 매개변수);

그러면 조회 동작을 알아서 수행해 줍니다! 아주 편리하죠.

// 예시 코드
public interface ExampleRepository extends JpaRepository<ExampleEntity, Long> {
    // 페이징을 하고, List 타입으로 목록 조회하는 메서드 예시
    List<ExampleEntity> findByStatus(Status status, Pageable pageable);
}

반환하는 행 수에 관한 제어는 이런 정도만 먼저 알아 둡시다.

  • 단건 조회 반환 타입은 Optional<엔티티타입> 사용을 권합니다.

  • 목록 조회 반환 타입은 List<엔티티타입>, Page<엔티티타입> 등을 권합니다.
    (Page<엔티티타입> 반환은 조회 구간 퍼포먼스에 두 배 이상 안 좋을 수 있어요.)

  • Page<엔티티타입>으로 반환하는 게 아니어도 인자에 Pageable을 추가하면 페이징이 됩니다.


🎦 Projection(프로젝션)이란?

프로젝션이라는 것은 수학이나 프로그래밍 등에서도 아래 그림처럼 일면(一面)이 투영되는 것을 뜻하죠.

엑스축과 와이축으로 된 좌표평면에서 벡터가 화살표 모양으로 하나 표현되어 있고, 이 벡터가 엑스 축에 대해서 수직으로 그림자를 내리고 있습니다. 이 그림자 쪽에는 "엑스에 대한 부분적인 투영"이라고 쓰여 있어요. 영어로는 파셜 프로젝션 투 엑스입니다. 표기는 영어로 되어 있어요.

위 그림은 벡터(방향과 크기가 있는 선)에서 x축에 대해서만 값이 궁금할 때x축 관점에서 본 벡터를 표현했습니다. 그리고 이 해석을 수학에서 '프로젝션(projection)'이라고 불러요. 그러면 보통 벡터의 원래 길이보다 짧은, '부분적인' 길이가 됩니다.

우리에게 필요한 부분에 대해서만 값을 산출한 셈이죠.

데이터베이스에서의 프로젝션도 비슷하게 생각해 볼까요? "우리에게 필요한 부분에 대해서만" 바라보겠다! 테이블의 전체가 아니라, 필요한 일부 컬럼만 바라보고 싶다! 이것을 프로젝션이라는 이름으로 부르고 있습니다. 아래 그림처럼요.

약간 입체화된 그림입니다. 이 그림에서 뒤쪽 패널에는 오리진 테이블이라는 텍스트가 위에 떠 있고, 패널은 전체적으로 회색 계열로 옅게 표현돼 있습니다. 거기에는 다섯 개의 컬럼으로 된 테이블이 있어요. 그 패널의 앞쪽으로는 색채를 띤 세 컬럼이 앞으로 복사돼 있어요. 그러니까 오리진 테이블의 다섯 개의 컬럼 중 왼쪽 세 컬럼만 앞쪽으로 색채를 띤 채 복사되어 있죠. 이 색채를 띤 세 컬럼이 프로젝션입니다. 프로젝션이라는 글씨가 세 컬럼의 아래에 쓰여 있어요.

JPA Projection으로 조회하기 (녯날 버죤)

자바 16 미만(또는 14 Preview 미만)에서는 record라는 MZ한 클래스 유형 대신 일반 클래스나 interface를 사용했습니다. 게터(getter) 양식을 그대로 적용했죠.

// 별도 애노테이션 없어도 됩니다.
public interface ExampleProjection {
    Long getId(); // id
    String getName(); // name
    String getNickname(); // nickname
    ExampleStatus getStatus(); // status (enum)
}

이런 표기는 작은 예시에선 편하고 쉬워 보여도 많은 컬럼 표기에는 불편했을 거예요. 그래도 만들고 나면, 사용할 땐 이렇게 간단하게 반환 타입으로 쓰기만 하면 알아서 부분적인 조회가 됐습니다.

public interface ExampleRepository extends JpaRepository<ExampleEntity, Long> {
    List<ExampleProjection> findAllExamplesByStatus(ExampleStatus status, Pageable pageable);
}

Optional<Projection>이든 List<Projection>이든 Page<Projection>이든 잘 작동합니다!

요즘 세대의 JPA Projection

JDK 16 공식 릴리스 이상 또는 JDK 14~15에서 Preview로 사용할 수 있는 record를 쓸 수 있죠.

정말 이걸로 완성?! 🥳 이것은 나를 놀라게 하다.

@Builder
public record ExampleProjection(Long id, String name, String nickname) {
}

😲 엥, 다 만들었어요 🥳 이거 정말 엔티티 클래스에서 복붙만 해도 금방 만들겠네요? MZ 세대의 인텔리제이와 함께라면 왠지 휠 클릭이나 다중 키보드 커서로 더 빨리 만들 것 같아요. 이것은 쉬운! 나는 이제 프로젝션을 쉽게 만들다!

📦 프로젝션 모아 두기

스타일에 따라선 파일을 낱개로 여러 개 만드는 것보다 어느 정도 단위로 모아서 관리하는 분들도 있어요.

public final class ExampleProjections {
    // not open any constructors
    private ExampleProjections() {}

    @Builder
    public record ExampleListViewProjection( ... ) {}

    @Builder
    public record ExampleDetailViewProjection( ... ) {}
}

이때 탑레벨 클래스는 final 클래스로 상속이 안 되게 하고, 생성자는 private으로 선언해서 객체 생성이 없게 하면 좋을 것 같아요.

대신 그렇게 중요한 조치는 아니기 때문에 편의상 (아이콘 바꾸려고라도) 탑레벨 record 안에 내부 record 구성으로 하기도 하죠. record는 어차피 final 클래스고 빈 레코드 객체 생성은 (함수도 안 만들었을 테니까) 특별한 의미가 없어서 안 할 거거든요.

public record ExampleProjections() {
    @Builder
    public record ExampleListViewProjection( ... ) {}

    @Builder
    public record ExampleDetailViewProjection( ... ) {}
}

이런 건 모두 취향 차이니까, 팀 내에서 스타일을 정해서 해 보면 좋겠습니다!

🕶 프로젝션 만들기, 생각보다 쉽죠?


자료는 모두 DALL·E, excalidraw, Power Point 등을 통해 스스로 만들었습니다. (그래서 영어가 어색할 수 있습니다.) 어색한 영어 문장은 남대문 열렸다고 알려 주듯 살짝 말씀해 주시면 얼른 고쳐 보겠습니다. 그러면 다들 모쪼록 착한 개발 하세요.