[ERP프로젝트 창고 기능 구현 중]
인터페이스 (Interface)가 Spring Data JPA의 @Query와 함께 어떻게 작동하는지:
Spring Data JPA에서 @Query 어노테이션으로 데이터베이스에서 직접 SQL 쿼리를 실행할 때, 그 결과를 특정 형태의 자바 객체로 받아야 합니다. 이때, 일반적으로는 완전한 엔티티(@Entity 붙은 클래스)를 받거나, 모든 필드를 가진 일반적인 DTO 클래스를 만들어서 받으려고 합니다.
하지만, 네이티브 쿼리(nativeQuery = true)를 사용하거나, 엔티티의 모든 필드가 아닌 특정 필드만 선택해서 가져오고 싶을 때가 있습니다. 이럴 때 **인터페이스 (Projection)**를 사용하면 아주 편리하고 강력해집니다.
핵심 원리:
- "나는 이런 정보가 필요해"라고 정의하는 계약서:
- WhmstDto를 class가 아닌 interface로 만들었죠? 이 인터페이스는 실제 데이터가 들어갈 공간을 만드는 게 아니라, "나는 getWhIdx(), getWhCd(), getWhNm() 같은 메서드를 가지고 있을 거야"라고 필요한 데이터의 '형태'와 '이름'만 약속(정의)하는 계약서와 같습니다.
- 데이터베이스 쿼리와 이름 맞추기:
- WhmstRepository의 @Query 안에서 SELECT w.WH_IDX as whIdx, w.WH_NM as whNm, ... 이렇게 as 키워드를 써서 별칭을 붙였죠?
- 이 whIdx, whNm, whUserNm 같은 별칭들이 바로 WhmstDto 인터페이스의 getWhIdx(), getWhNm(), getWhUserNm() 메서드 이름과 정확히 일치하도록 만들어주는 겁니다. (자바의 카멜케이스 규칙에 따라 맞춰줍니다. WH_IDX는 whIdx로, WH_USER_NM은 whUserNm으로요).
- Spring의 마법: "내가 대신 만들어 줄게!"
- Spring Data JPA는 @Query가 실행되어 데이터베이스에서 결과를 가져오면, 이 결과를 보고 WhmstDto 인터페이스의 약속에 맞춰서 실시간으로 이 인터페이스를 구현하는 가짜 클래스(프록시 객체)를 자동으로 만들어줍니다.
- 예를 들어, 데이터베이스에서 WH_IDX가 101이고 WH_NM이 'A창고'인 데이터가 오면, Spring은 마치 new WhmstDto() { @Override public Long getWhIdx() { return 101L; } @Override public String getWhNm() { return "A창고"; } ... }와 같은 객체를 즉석에서 만들어서 우리에게 넘겨주는 겁니다.
- 우리는 그냥 사용하기만 하면 돼:
- 우리는 이 실제 구현된 객체가 어떻게 만들어졌는지 알 필요 없이, 그냥 WhmstDto 타입으로 받아서 dto.getWhIdx(), dto.getWhNm() 처럼 원래 클래스를 사용하듯이 호출하면 됩니다. Spring이 알아서 데이터베이스에서 온 값을 해당 getter 메서드로 매핑해 줍니다.
간단히 말해, 인터페이스 DTO (Projection) 방식은:
"데이터베이스에서 필요한 필드만 뽑아서, 그 필드에 맞는 getter 메서드를 가진 인터페이스를 정의하면, Spring이 알아서 그 인터페이스를 구현한 객체를 만들어서 데이터를 채워주는 똑똑한 방식"이라고 생각하시면 됩니다. 이렇게 하면 불필요한 데이터를 가져오지 않아 효율적이고, 코드도 더 깔끔해집니다.
네, 뷰(View)를 사용해서 여러 테이블의 데이터를 조인하여 가져올 때, 해당 뷰의 결과를 자바 애플리케이션의 DTO로 매핑하려면 인터페이스(Projection)로 구현하는 것이 가장 좋습니다.
이유는 다음과 같습니다:
- 객체 매핑의 유연성 (Projection):
- 뷰는 여러 테이블의 컬럼을 조합하여 새로운 논리적인 테이블처럼 보여줍니다. 이 뷰의 컬럼들을 Spring Data JPA 엔티티(@Entity)에 1:1로 매핑하기는 어렵거나 불가능할 때가 많습니다. (예: 뷰에는 USER_NM이 있지만 실제 Whmst 엔티티에는 USER_NM 필드가 없음).
- 인터페이스는 엔티티와 독립적으로, 뷰에서 필요한 컬럼만 선택하여 정의할 수 있는 "투영(Projection)" 기능을 제공합니다. 즉, "나는 이 뷰에서 whIdx, whCd, whNm, whUserNm 같은 컬럼만 필요해" 라고 명확하게 선언하는 것입니다.
- 자동 매핑의 편리함 (nativeQuery = true 또는 JPQL):
- Spring Data JPA는 @Query와 함께 인터페이스를 반환 타입으로 사용하면, SQL 쿼리 결과의 컬럼명(혹은 AS로 지정한 별칭)과 인터페이스의 getter 메서드 이름을 자동으로 매핑해줍니다. (예: SQL의 WH_USER_NM as whUserNm은 인터페이스의 getWhUserNm()과 연결됨).
- 특히 nativeQuery = true를 사용할 때, JPA는 튜플(Tuple) 형태의 결과를 반환하는데, 인터페이스 프로젝션은 이 튜플을 원하는 인터페이스 타입으로 변환하는 과정을 Spring이 자동으로 처리해줍니다. DTO 클래스에 생성자를 추가하는 방식보다 훨씬 간편하고 유지보수하기 좋습니다.
- Lombok 의존성 감소 (조회용 DTO):
- 인터페이스는 필드나 생성자가 필요 없기 때문에, @Getter, @Setter, @NoArgsConstructor, @AllArgsConstructor, @Builder와 같은 Lombok 어노테이션이 필요 없습니다. 순수 Java 인터페이스로만 구성되어 코드가 더 간결해집니다. 이는 특히 조회 전용 DTO를 만들 때 장점입니다.
- 불필요한 필드 로딩 방지:
- 엔티티를 직접 조회하면 해당 엔티티에 매핑된 모든 필드를 가져오려 시도합니다. 하지만 뷰는 엔티티에 없는 추가 정보를 포함할 수 있습니다. 인터페이스 프로젝션을 사용하면 애플리케이션에 필요한 데이터만 정확히 지정하여 가져오므로, 데이터 전송량과 메모리 사용량을 줄일 수 있습니다.
결론적으로,
뷰를 사용한다는 것은 데이터베이스 계층에서 이미 특정 목적에 맞게 데이터를 가공해 놓았다는 의미가 강합니다. 이 가공된 데이터를 애플리케이션 계층에서 그대로 활용하려면, 뷰의 컬럼 구조에 맞춰 데이터를 받아줄 자바 객체가 필요합니다. 이때, 데이터를 읽기 전용으로 특정 형태에 맞춰 가져오는 "Projection"의 역할에 가장 적합한 것이 바로 인터페이스입니다.
따라서, VW_WAREHOUSE_USER_INFO와 같이 조인된 복합 정보를 제공하는 뷰의 경우, WhmstDto와 같은 인터페이스를 사용하여 해당 뷰의 컬럼을 프로젝션하는 것이 가장 권장되는 모범 사례입니다.