스프링

TIL 20240211 - Projection이란?

개발자 백구 블로그 2024. 2. 11. 02:39

 

저번 주에 강의를 쭉 듣고 첨들어본 개념들을 다시 정리하기 위해서

복습하는 느낌으로 공부하고 있다,,

설날연휴 와중에도 열중하는 나,, 좀 멋있을지도,,?

 

 

 

 

 

 


projection?

 

사전적 의미로는 투사, 예상을 나타내는 것으로 사용하고 있고,

jpa에서는 데이터베이스에서 테이블을 출력할 때 Column을 제한적으로 출력하는 것을 의미한다.

 

SELECT * FROM Team;          // Non-Projection
SELECT name, age FROM Team;  // Projection

 

이처럼 *는 전체를 뜻하는 것으로 위부분은 projection이 아니지만

아래는 Team의 name, age를 가져오는 것으로 특정된 것만 가져오는 쿼리문을 작성하였으므로 projection에 해당된다.

 

 

그나저나 왜일까?

그저 Entity로만 단순히 조회하면 된다고 생각했는데 추가적으로 이런것이 왜 생겼을까?

 

위 예시로 간단히 짐작해보면 특정 컬럼을 조회하고 싶은 경우도 분명히 있을 텐데 이를 위해 항상 전체 컬럼을 가져올 필요성은 없을 테니까.,,? 이지 않을까라는 생각이 든다.

즉, 불필요한 컬럼이 제외된 꼭 필요한 데이터를 조회하기 위해서 같다. 그러면 좀 더 데이터를 찾기 편하지 않을까?

 

 

그 외에도, 필요한 이유를 찾아보자면

=> 조회된 모든 Entity는 강제적으로 영속성 컨텍스트의 관리를 받게 된다.

이러면 성능 및 메모리에도 영향을 주므로 만약에 한 꺼번에 많은 양의 Entity를 조회하여 각 Entity 들이 영속성 컨텍스트의 관리를 받게 된다면 성능부하가 무수하게 커질 가능성이 있다.

 

 

=> projection을 이용해 커버링 인덱스를 활용 할 수 있다.

 

커버링 인덱스

 

 

이 때, 커버링 인덱스란, 원하는 데이터를 인덱스에서만 추출 할 수 있는 인덱스를 의미하며, 성능이 굉장히 빠른 탐색이 가능하게 해주는 인덱스라고 보면 된다..

 

 

 

Projection 같은 경우 언제 사용하는 것이 알맞는 것일까?

음.. 우선 쓰기 작업을 할 경우 Entity로 조회하여 1차 캐시, 쓰기 지연, 변경 감지 등의 JPA의 지원을 충분히 받고!

읽기 작업 할 시에만 필요한 정보만 얻도록 projection을 조회하면 된다. 꼭 해야한다! 까진 아니더라도 대량으로 한 번에 조회가 될 경우에는 성능개선을 위해서라도 projection을 이용하는 것이 좋다고 한다.

 

 

 

그렇다면 DTO에서의 projeciton의 역할은 어떻게 될까?

DTO은 직역하자면 데이터 전송 객체라는 말로 데이터를 옮기기 위해 존재하는 객체를 뜻한다.

여기서 projection 을 통해 데이터를 조회하는 것은 반환 자료형이므로 Entity를 사용하지 않는다! 는 것이기 때문에 데이터를 담을 별도의 객체가 필요하게 되고 projection을 통해 데이터를 조회할 경우에 반환 자료형이 DTO가 되게 된다.

interface UserRepository : JpaRepository<User, Long> {
    fun findByEmail(email: String): SimpleUser?  
}

 

이는 레포지토리에 작성해준 것인데, 이처럼 simpleUser는 반환 자료형이므로 DTO가 된다는 것을 볼 수 있다.

 

 

 

JPA Repository에서 Projection은 Closed Projection, Open Projection으로 구분 할 수 있는데,

 

Closed Projection은 데이터베이스에 있는 컬럼을 그대로 매핑하여 Projection을 해주는 것이지만 

(예를 들어 name, age의 컬럼만 하는 경우!)

 

Open Projection은 데이터베이스에 있는 컬럼을 이용해 다양한 연산을 한 결과로 Projection을 하는 것이라고 생각하면 된다. 예를 들어 남성과 여성을 더해 '성별' 을 조회하고 싶을 경우 사용한다.

 

 

 

Repository에서 projection을 구현하는 방법으로는,

 

Interface-Based Projection

인터페이스에 Projection을 할 컬럼들에 Getter 추상 메소드를 만들어줌으로써 JPA가 추론하도록 만드는 것이다. 따라서 정확한 컬럼명을 Getter로 만들어줘야 하는 불편함을 감수해야 한다. 사실 별도로 Proxy 객체를 통해 결과를 받아와서 비교적 성능이 좋지 않기 때문에 잘 사용하지 않으므로 그저 이런것도 있구나 하고 넘어가면 될 것 같다!

 

 

Class-Based Projection

구조 자체는 Interface-Based Projection 과 동일하지만 인터페이스를 통해 Getter 추상 메소드를 만드는지? 아니면 클래스에 컬럼명과 동일한 필드를 만들 것인지에 따라 차이가 있다고 한다. 또한 JPA 가 컬럼명을 추론하는 근거는 필드의 이름이 아니라 생성자의 파라미터 이름이다.

 

 

 

아 그런데 여기서 잠깐,,, 조회해야 하는 API가 한 개가 아니라 여러 개일 경우,, 게다가 다른 형태로 Projection을 해야 하는 경우엔 어떻게 해야 할까?

interface UserRepository : JpaRepository<User, Long> {
    fun findByEmail(email: String): SimpleUser1?
    fun findByEmail(email: String): SimpleUser2?
    fun findByEmail(email: String): SimpleUser3?
    fun findByEmail(email: String): SimpleUser4?
    fun findByEmail(email: String): SimpleUser5?
    fun findByEmail(email: String): SimpleUser6?
}

 

그저 이런식으로 연속으로 작성해주면 되는 것인가??

아니다. 이는 결코 좋은 방법이 아니다.

 

 

이런 상황에서는 Dynamic Projection을 사용하면 된다고 한다!!!

interface UserRepository : JpaRepository<User, Long> {
    fun <T> findByEmail(email: String, type: Class<T>): T?
}

 

Dynamic Projection 은 이처럼  제네릭을 이용해 쉽게 구성할 수 있다!
이 이후부터는 type 에 들어오는 DTO 클래스를 이용하여 JPA 가 Projection 해준다.

UserRepository.findByEmail(email, SimpleUser1::class.java)
UserRepository.findByEmail(email, SimpleUser2::class.java)

 

참고로  앞서 구현하는 방법으로 소개한 것( Interface-Based Projection, Class-Based Projection ) 모두 사용 가능하다!

 

 

 

JPQL 의 Projection

여기서 JPQL은 무엇일까? JPA에서 SQL을 추상화한 객체 지향 쿼리 언어를 제공하는데 이를 JPQL이라고 한다.

마찬가지로 Interface-Based Projection, Class-Based Projection 의 두 가지 방법이 있는데,

 

 Interface-Based Projection

interface SimpleUser {
    fun getName(): String
    fun getAge(): Int
}

 

Getter 를 인터페이스에 생성하는데, 이때 이름이 반드시 동일해야 한다는 점! 명심하자.

 

interface UserRepository : JpaRepository<User, Long>, UserQueryDslRepository {

    @Query("SELECT m.name as name, m.age as age FROM User m WHERE m.email = :email")
    fun findByEmail(email: String): SimpleUser?
}

 

@Query을 통해 JPQL를 작성하고 SELECT 절에 Projection을 할 컬럼들을 직접 명시해줘야 하는 하는데,

반드시 Alias(별칭)을 이용해 컬럼명을 재작성해줘야 하는 불편함이 발생한다.

 

 

Class-Based Projection

data class SimpleUser (
    val name: String,
    val age: Int
)

 

interface UserRepository : JpaRepository<User, Long>, UserQueryDslRepository {
    @Query("SELECT new com.example.usersample.domain.user.entity.projection.SimpleUser(m.name, m.age) FROM User m WHERE m.email = :email")
    fun findByEmail(email: String): SimpleUser?
}

 

JPQL 쿼리의 SELECT 절에 new DTO()와 같은 형태로 생성자를 호출해주면 되고

이때는 패키지 경로를 전부 입력해줘야 한다 😂

 

 

 

아 그리고 마지막으로 QueryDSL의 Projection이 있는데

이는 아직 공부가 덜 돼서 더 해보고 정리할 생각이다!!

이제 마저 강의 듣고 자자 ㅎㅎ


흐흐 그래도 뭔가 알아낸 기분이라 뿌듯 ㅎㅎ

아!

마지막으로 CQRS 구현이 무엇인지 추가적으로 살펴보면 좋다고 한다!

흐흐 자기 전에 한 번 살펴보고 자야지 ㅠ

오늘도 고생많았당!!