JPA | projections

DoItDev·2021년 9월 4일
1
post-thumbnail

Overview

지속적인 계층을 구현하기 위해서 JPA의 경우 사용을 할때 일반적으로 하나 이상의 인스턴스를 반환을 하지만 대부분의 경우 반환된 객체를 전반적으로 다 사용을 안할때가 있습니다.

이럴경우 데이터를 사용자가 지정하여서 사용이 가능합니다. 정의된 유형에 따라 객체를 검색하여 필요한 부분만 사용을 하는것이 올바르다고 생각이됩니다.

또한 필요없는 데이터를 객체를 반환을 할때 리소스가 발생이 합니다

DAO(레파지토리)에서 일반적으로 데이터를 가지고 오는데 그때 서비스 계층에서 또 한번의 소스 코드를 작성을 하는데 이럴때 리소스를 발생이됩니다.

인스턴스에서 받은것을 java dto 또는 vo 로 반환을 하는데 이것은 현업에서 사용을 할때 안티 로직이라고 볼 수 있습니다

관심유형별로 인스턴스를 구성이 가능하며 이러한 방식을 통하여 좀더 프로적인 jpa 방법을 설명하고 자합니다.

Basic Entity

@Data
@Entity
@DynamicInsert
@DynamicUpdate
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "app_job")
public class Job extends BaseEntity implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "job_title", nullable = false)
    private String title;

    @Column(name = "content", nullable = false)
    private String content;

    @ManyToMany(targetEntity = User.class)
    @JoinTable(name = "app_job_user",
            joinColumns = @JoinColumn(name = "job_id", referencedColumnName = "id"),
            inverseJoinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id")
    )
    @JsonIgnoreProperties({"groups", "gender"})
    private Set<User> users = new HashSet<>();

    @Builder
    public Job(String title, String content, Set<User> users) {
        this.title = title;
        this.content = content;
        this.users = users;
    }

}

Job 이라는 엔티티의 경우 User 도메인과 릴레이션쉽을 가지고 있습니다.

Job 과 User 그리고 두 도메인 사이에 연결을 시켜주는 Job&User 라는 도메인 까지 3가지를 이용하려고 힙니다.

스크린샷 2021-11-30 오후 3 52 19

projectsions usage

기본적으로 우리는 하나의 엔티티를 통하여 jpa 를 사용을 합니다.

  List<Job> findAll();

위와 같이 메소드를 레파지토리에서 가지고 오면 Job에 도메인에 대한 모든것을 반환을 합니다.

그리고 이것을 우리는 Server Layer 에서 Java Map 또는 For 문으로 모든 것을 다시 또다른 인스턴스에 다가 담아 넣습니다.

이런식으로 Jpa 로직을 짠다는 것은 제가 보았을 때에는 Jpa 를 잘 이용을 못한다고 볼 수 있습니다. (상황별로 다르지만 불가피한 상황이 아닌이상)

그렇다면 이렇게 짜는 것이 아니라 어떻게 짜야하는가..? 라는 것인데

   @Transactional
   <T> Optional<T> findById(Long id, Class<T> className);

레파지토리에서 Collection, List, Set, Optional ... 등 제네릭을 사용하는 java에서 제공을 해주는 객체(인스턴스)들이 있습니다.

그렇게 위와 같은 코드로 제네릭 + collection 을 이용을 해서 사용이 가능합니다.

단순한게 array(배열) 뿐만 아니라 Optional 을 이용을 해서 객체를 가지고 오는 것이 가능합니다.

    @Transactional
    <T> List<T> findAllProjectedBy(Class<T> className);

위의 코드를 읽어 보면 List 배열로 전체 select 를 해온다 그리고 className 이라는 class 로 return 을 받는다는 것이다.

findAll의 경우 projectsions 을 사용을 할때 Rename을 시켜주어야 된다.

public interface IOnlyTitle {
    String getTitle();
}

위의 IOnlyTitle 의 경우 위의 job 도메인에서 title 만 select한다는 의미가 된다.

스크린샷 2021-11-30 오후 3 14 49 스크린샷 2021-11-30 오후 3 14 55

그러면 테스트를 했을 때 위의 쿼리 처럼 title 만 항목으로 가지고 올 수 있다.

위의 로직에서 sql에서 사용하는 concat을 사용을 해보려고한다.

public interface IOnlyTitle {
        String getTitle();

        @Value("#{target.title} = #{target.content}")
        String getValue();
}

@Value

위에서 interface에서 @value 어노테이션을 사용을 해서 concat을 구현을 해보려고한다.

target의 키워드는 타겟 엔티티라고 생각하시면 됩니다. 엔티티에 변수명을 적어주시면 targeting 됩니다.

스크린샷 2021-11-30 오후 3 19 02 스크린샷 2021-11-30 오후 3 18 51

테스트 케이스로 확인을 해보니 concat이라는 기능은 정확하게 작동이 됩니다.

하지만 sql구문 자체 도메인 전체를 불러오는 것을 볼 수 있습니다.

이루어 보았을때 db 에서 sql구문을 날려줄때 concat 으로 변형이 되는 것이 아니라 jpa layer 에서 변형을 시켜준는 것으로 유추가 가능합니다.

Relation Domain

이렇게 @Value 를 사용을 해서 사용이 가능하는 것을 알아보았습니다. 그렇다면 연관관계가 있는 도메인의 경우 어떻게 컨트롤이 가능한것인가..? 라는 궁금증을 가지게 되었습니다.

    @Transactional
    @EntityGraph(attributePaths = {"users"})
    <T> Optional<T> findById(Long id, Class<T> className);

위의 로직을 레파지로리에서 작성을 해주었습니다.

Job 도메이에서 직접 Lazy 로딩을 해주는것이 아니라 @EntityGraph 을 통해서 Lazy 로딩을 시켜줌으로써 N+1 에 대한 에러를 방지하고자 위와 같이 로직을 만들었습니다.

그렇다면 우리가 사용하는 DTO interface는 어떻게 작성을 해야한는가 ..?

    public interface IUserIds {
        Long getId();

        String getTitle();

        String getContent();

        @Value("#{target.users}")
        Set<Ids> getUserIds();

        interface Ids {
            Long getId();
        }

    }

interface 안에 inner interface를 작성을 해주었습니다.

그리고 @Value를 통하여 target을 relation domain 으로 작성을 해주었습니다.

스크린샷 2021-11-30 오후 3 43 03 스크린샷 2021-11-30 오후 3 43 15

sql 구문 자체는 전체를 불러오고있습니다 전에 보았던과는 다르게 @EntityGrap 을 통하여 User 도메인을 불러오기 때문에 join을 하는 것을 볼 수 있습니다.

그렇게 inner interface 에서 id 값만 불러오기 때문에 jpa layer에서 가공을 해주어서 사용이 가능합니다.

이렇게 위와 같이 jpa 로 entity 만 불러오는 것이 아니라 다른 방식으로도 불러오는 것을 확인 하실 수 있습니다.

profile
Back-End Engineer

0개의 댓글