[HVT_Project] Generic 메서드를 통한 Dynamic Projection 구현하기

JUNHYUK CHANG·2024년 2월 16일
1

TIL

목록 보기
20/33

이번 프로젝트에서 다룰 Entity 는 서울시의 인터넷 쇼핑몰 업체에 대한 정보를 담고 있는데, 생각보다 필드가 너무나 많고 일반적인 상황에선 필요하지 않은 정보들도 너무 많이 포함되어 있었다. 매번 이런 정보들까지 포함하여 호출하는 것이 비효율적이라고 판단되어 처음 csv 를 열어보자마자 아, Projection 무조건 해야겠다.. 하고 느꼈다.

Projection 이 무엇인지는 여기를 누르면 도움이 될지도ㅎ..?

33개의 필드 중 일반적으로 필요할 것 같은 정보를 추려 Projection 용 data class 를 생성하였다.

상대적으로 아담해진 Simple Store Entity.

Projection 적용하기 전 기본형의 GET 메서드는 페이징을 적용하여 다음과 같이 구현하였다.

그리고 아래 코드가 Projection을 적용한 메서드. DTO 의 불변성을 지키기 위해 constructor() 방식으로 적용하였다. 생성자의 순서를 지키기 위해 DTO 의 생성자를 그대로 복사하여 불필요한 내용만 지우는 것으로 작업하였다.


완성을 해놓고 보니 중간의 Projection 만 빼면 완전히 동일한 메서드인데 굳이 2개로 나눠둘 이유가 있을까? 고민해보았다.

Projection 적용 여부를 판단할 수 있는 플래그만 주어진다면 하나로 합칠 수 있을 것 같은데 그럼 반환형이 문제인가.. 어떨 땐 Store Entity 를 반환하고 어떨 땐 Simple Store Entity 를 반환할 수는 없을까??

그래서 사용한 것이 바로 Generic 메서드를 이용한 Dynamic Projection !

Generic 메서드

제네릭 메서드는 다양한 유형의 매개변수와 반환 유형을 가질 수 있는 메서드이다. 이를 통해 동일한 메서드를 여러 유형의 데이터에 대해 사용할 수 있다. 이를 통해 코드의 재사용성과 유연성이 향상된다.

fun < T > methodName(parameter1: T, parameter2: T): T {
// 메서드 본문
}

여기에서 사용된 <T> 는 제네릭 프로그래밍에서 사용되는 일종의 플레이스홀더(특정한 타입을 대신하여 사용되는 기호나 표현. T는 Type 을 뜻함) 인데 일반적인 표현인 것으로 실제론 fun <myMind> methodName( param1: myMind ) : myMind 라고 해도 상관 없이 작동한다. 한마디로 내맴.

쉽게 설명하자면 Generic 메서드는 <T> 에 어떤 타입이 사용될지를 나중에 결정하는 메서드이다. 처음 이 메서드를 공부할 땐 갑자기 자유로워진 기분이었지만 반대로 생각해보면 무궁한 에러의 가능성을 품고 있다는 뜻이니 섬뜩해지기도 했다.

Dynamic Projection은 QueryDSL을 사용하지 않는 JPA 환경이라면 아주 간단하게 적용이 가능하다.

@Repository
interface StoreRepository : JpaRepository<Store, Long> {
		fun <T> findByEmail(email: String, type: Class<T>): T?
}


memberRepository.findByEmail(email, Store::class.java)
memberRepository.findByEmail(email, SimpleStore::class.java)
memberRepository.findByEmail(email, DetailStore::class.java)

위와 같이 Repository 메서드를 Generic 으로 설정해준 뒤 type 에 DTO 클래스를 설정해주면 JPA 가 알아서 Projection 해주기 때문이다.

하지만 이번엔 모든 업체들의 정보를 출력해야하기 때문에 Paging 을 적용해야 하니 QueryDSL 에서 구현해보았다.

기존 코드에서 달라진 점은
1. 메서드 원형이 fun <T> getStores(pageable: Pageable, type: Class<T>): Page<T>? 로 바뀐 것이다. Service 로 부터 어떤 클래스를 처리할 지 type 으로 받아온 뒤 Page형태로 반환하는 것.
2. type 의 내용을 확인하여 Projection 적용 여부를 확인하고, 쿼리문에 Select 구문을 미리 설정해준다.
3. contents 에서 .fetch() 로 내용들을 가져올 때 as List<T> 로 설정해주어 메서드의 반환형<T> 와 맞춰주도록 하였다.

  • 이때 as List<T> 로의 변환이 없는 경우 아래와 같은 에러가 발생할 수 있다.
Type mismatch.
  Required:
    Page<T>
  Found:
    Page<SimpleStore>?
  • Generic 메서드가 자유롭게 반환이 가능하다 하더라도 <T>의 타입은 맞춰주어야 하는 것이다.
  • 여기서 쓰인 as 는 타입 캐스팅을 수행하는 데 사용되는 키워드로 어느 타입을 다른 타입으로 변환할 때 사용된다. 하지만 안전한 캐스팅을 보낭하는 것은 아니기 때문에 as? 를 사용하여 캐스팅 실패 시 null 을 반환하도록 설정하는 것도 가능하다.

위의 방법을 통해 Repository 의 Store 전체 조회 메서드를 1개로 줄일 수 있었고, API 메서드에 따라 Projection이 제대로 적용 되는 것도 확인할 수 있었다.

어라..? 그럼 아예 Controller 와 Service 의 메서드도 각 1개씩으로 줄일 수 있지 않을까??
Projection 여부 체크하는 방식으로..?

To Be Continued . . .

1개의 댓글

comment-user-thumbnail
2024년 2월 16일

와 너무 어렵내요 감탄하고 갑니다 -프론트엔드

답글 달기