프로젝션과 프로시저

뾰족머리삼돌이·2024년 9월 6일

Spring Data JPA

목록 보기
3/9

데이터베이스 릴레이션에서 원하는 속성만을 뽑아내어 새로운 릴레이션을 생성해내는 작업을 Projection이라 부른다.
SQL에서 SELECT문만 생각해봐도 테이블에서 원하는 특정 속성값만 얻어내는 작업이 가능하고, 그 과정에 프로젝션이 사용된다고 볼 수 있다.

Spring Data JPA 에서의 Projection

Spring Data JPA에서 SELECT를 수행하면 기본적으로 해당 도메인 클래스의 모든 정보를 가져온다.
Proejction을 수행하기 위해서는 크게 3가지의 방법 중에서 하나를 선택해야한다.

  • 인터페이스 기반의 프로젝션
  • 클래스 기반의 프로젝션
  • 동적 프로젝션
@Entity
@Getter
@NoArgsConstructor
public class Person {

    @Id
    private UUID id;

    private String firstname;
    private String lastname;

    @Embedded
    private Address address;

    public Person(String firstname, String lastname, String zipCode, String city, String street){
        this.id = UUID.randomUUID();
        this.firstname = firstname;
        this.lastname = lastname;
        this.address = new Address(zipCode, city, street);
    }

    @Embeddable
    @NoArgsConstructor
    @AllArgsConstructor
    static class Address{
        String zipCode;
        String city;
        String street;
    }
}

대략 이런 형태의 도메인 클래스에서 Projection을 수행하는 모습을 살펴보자

인터페이스 기반의 프로젝션

가장 쉬운 접근방법은 원하는 속성에 대한 접근자를 포함하는 인터페이스를 이용하는 것이다.

public interface NamesOnly {
    String getFirstname();
    String getLastname();
}

public interface PersonRepository extends JpaRepository<Person, UUID> {

    Collection<NamesOnly> findByLastname(String lastname);
}
Collection<NamesOnly> last = personRepository.findByLastname("last");
for (NamesOnly namesOnly : last) {
	log.info(namesOnly.getFirstname());
	log.info(namesOnly.getLastname());
}

// 발생 쿼리
select p1_0.firstname,p1_0.lastname from person p1_0 where p1_0.lastname=?

// 출력 결과
first
last

예를들어, 위 코드의 NamesOnly 처럼 도메인 클래스의 속성과 정확히 일치하는 접근자 메서드를 가지는 인터페이스를 이용할 수 있다. 이렇게 작성하면 원하는 속성만을 골라서 조회하는 모습을 확인할 수 있다.

만약, 여기에 Address 정보까지 포함하고 싶다면 아래와 같은 방식으로 작성하는 것도 가능하다.

public interface PersonSummary {
    String getFirstname();
    String getLastname();
    AddressSummary getAddress();

    interface AddressSummary{
        String getCity();
    }
}
select p1_0.firstname,p1_0.lastname,p1_0.city,p1_0.street,p1_0.zip_code from person p1_0 where p1_0.lastname=?

하지만 이 방법을 사용하면 Address에 해당하는 모든 속성을 읽어오는 쿼리가 발생한다.
따라서, 원하는 속성만을 얻어오기 위해서는 아래 방식을 이용할 수도 있다.

public interface PersonSummary {
    String getFirstname();
    String getLastname();
    String getAddressCity();
}
select p1_0.firstname,p1_0.lastname,p1_0.city from person p1_0 where p1_0.lastname=?

폐쇄형 프로젝션

public interface NamesOnly {
    String getFirstname();
    String getLastname();
}

NamesOnly와 같이 도메인 클래스의 속성과 완벽하게 일치하는 접근자 메서드 만을 가지는 경우, 폐쇄형 프로젝션으로 간주된다.
이 경우, Spring에서 projection proxy 지원에 필요한 정보를 모두 가지고 있으므로 쿼리 실행을 최적화할 수 있다.

개방형 프로젝션

public interface NamesOnly {

  @Value("#{target.firstname + ' ' + target.lastname}")
  String getFullName();}

@Value를 이용하여 새로운 값을 계산해내는 접근자 메서드를 가지는 경우를 개방형 프로젝션이라고 부른다.

select p1_0.id,p1_0.city,p1_0.street,p1_0.zip_code,p1_0.firstname,p1_0.lastname from person p1_0 where p1_0.lastname=?

SpEL 표현식에 도메인에 관련된 모든 정보를 사용할 가능성이 있기때문에, 도메인 클래스에 대한 모든 정보를 가져오는 쿼리가 발생한다.

@Value를 이용하면 문자열로 작성해야 하기때문에 실수가 발생할 가능성이 크다.
따라서, 아래의 방식으로 Java8 문법을 이용한 폐쇄형 프로젝션을 이용하여 표현하는 것이 더 유용할 수 있다

interface NamesOnly {

  String getFirstname();
  String getLastname();

  default String getFullName() {
    return getFirstname().concat(" ").concat(getLastname());
  }
}

복잡한 표현식의 경우에는 전체 도메인 정보를 받아온다는 점을 이용,
도메인 인스턴스를 인수로 받는 메서드를 포함한 클래스를 Bean으로 등록하여 사용하는 방법도 있다.

@Component
class MyBean {

  String getFullName(Person person) {}
}

interface NamesOnly {

  @Value("#{@myBean.getFullName(target)}")
  String getFullName();}

접근자 메서드 반환타입으로 Null 안정성 향상을 위한 Optional<T> 사용도 가능하다

클래스 기반의 프로젝션

record NamesOnly(String firstname, String lastname) {
}

public interface PersonRepository extends JpaRepository<Person, UUID> {

    Collection<NamesOnly> findByFirstname(String firstname);
    
}

DTO 클래스를 이용하면 중첩된 프로젝션이 불가능하다는 점과 프록시가 사용되지 않는다는 점을 제외하고는 인터페이스 기반의 프로젝션과 동일하게 사용 가능하다.


인터페이스 기반 프로젝션의 결과인 last와 다르게, 프록시 없이 바로 해당 타입의 인스턴스가 생성된다

Java 14의 Record는 자동으로 private final/equals/hashCode/toString을 생성해주기 때문에 DTO로 사용하기 적합하다

동적 프로젝션

interface PersonRepository extends Repository<Person, UUID> {

  <T> Collection<T> findByLastname(String lastname, Class<T> type);
}

동적 프로젝션은 상황에 따라 필요한 속성을 골라 프로젝션을 하는 경우를 의미한다.
이를 위해서는 위 형태와 같이 Class<T>를 이용하여 타입을 지정할 수 있는 쿼리메서드가 필요하다.

void someMethod(PersonRepository people) {

  Collection<Person> aggregates =
    people.findByLastname("Matthews", Person.class);

  Collection<NamesOnly> aggregates =
    people.findByLastname("Matthews", NamesOnly.class);
}

메서드 호출에서 type에 원하는 반환타입을 함께 넘겨주면 위 예시와 같이 사용할 수 있다.

Procedure

DB에서 프로시저는 반복적으로 동일한 동작을 수행하는 작업을 묶어서 하나의 함수와 유사한 형태로 관리하는 것을 의미한다.
함수와는 다르게 명확한 리턴 키워드가 존재하지 않기때문에 값의 계산과 반환보다는 작업의 수행에 좀 더 의의를 둔 기능이다.
OUT 키워드를 이용하여 값을 반환할 수는 있지만, 필수가 아니기 때문에 함수와는 차이가 있다.

Spring Data JPA에서의 프로시저 사용은 몇가지 애노테이션을 통해 이뤄진다.

@Entity
@NamedStoredProcedureQuery(name = "User.plus1", procedureName = "plus1inout", parameters = {
  @StoredProcedureParameter(mode = ParameterMode.IN, name = "arg", type = Integer.class),
  @StoredProcedureParameter(mode = ParameterMode.OUT, name = "res", type = Integer.class) })
public class User {}

위와 같이 Entity 클래스에 @NamedStoredProcedureQuery 설정을 하여 프로시저에 대한 메타데이터를 작성할 수 있다.
이 중에서 procedureName 속성은 실제 DB의 프로시저 명칭과 동일해야한다.

실제 프로시저를 호출하는 메서드를 작성하는 방법은 약 4가지가 존재한다
기본적으로 OUT에 해당하는 데이터가 하나라면 해당 타입으로 받을 수 있지만, 여러개인 경우 Map으로 받아야한다.


@Procedure("plus1inout")
Integer explicitlyNamedPlus1inout(Integer arg);

name속성에 프로시저 명칭을 작성하는 메서드 작성방법 이다.
이 방법은 앞서 작성한 @NamedStoredProcedureQuery의 구성을 무시한다


@Procedure(procedureName = "plus1inout")
Integer callPlus1InOut(Integer arg);

procedureName 속성에 프로시저 명칭을 작성하는 메서드 작성방법 이다.
이 방법은 앞서 작성한 @NamedStoredProcedureQuery의 구성을 무시한다


@Procedure
Integer plus1inout(@Param("arg") Integer arg);

프로시저 이름을 메서드 명칭으로 대체하는 작성방법이다.


@Procedure(name = "User.plus1IO")
Integer entityAnnotatedCustomNamedProcedurePlus1IO(@Param("arg") Integer arg);

@NamedStoredProcedureQuery의 name 속성을 이용한 작성방법이다.

0개의 댓글