springframework/Spring Data JPA

Spring Data Common : Async Query Method

Jungsoomin :) 2020. 11. 16. 00:14

권장되지 않는 방법이다.

  • 비동기 식으로 쿼리를 날려 받아오는 식으로 동작하며 이는 백그라운드에 있는 Thread Pool 에 Task 를 맡긴 후 콜백으로 결과 값을 받는 방식이다.
  • 테스트 코드 작성이 힘들며 테스트 코드가 지저분해 진다.
  • 컴퓨팅 파워가 받쳐주는 현재 시점에서는 DB 튜닝이 어플리케이션의 속도를 판가름한다.
  • 쓰레드가 분리되면 다른 스레드에서 조작하는 데이터를 감지하지 못한다.
  • 즉 기존의 데이터는 가져올 수 있으나, insert와 select 동작이 언제 완료될지 모르는 상황에서 서로 조작하는 데이터를 감지할 수 없어 문제가 일어나는 것이다.

 


Async 설정방법

  • @Configuration 클래스에 @EnableAsync 어노테이션을 붙인다.
  • 백그라운드에서 돌게 되는 Thread Pool 을 위해 Executor 빈을 만든다.
  • 프록시를 사용하기 때문에 public 메서드를 선언하며 메서드에 @Async 어노테이션을 붙이고 백그라운드 쓰레드풀의 이름을 속성 값으로 준다.
@SpringBootApplication
@EnableJpaRepositories(queryLookupStrategy = QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)
@Import(SoominRegistrar.class)
@EnableAsync
public class DatajpaApplication {

    public static void main(String[] args) {
        SpringApplication.run(DatajpaApplication.class, args);
    }

    @Bean(name = "threadPoolTaskExecutor")
    public Executor threadPoolTaskExecutor() {
        return new ThreadPoolTaskExecutor();
    }

}

 @Async("threadPoolTaskExecutor")
    Future<List<Comment>> findByCommentContainsIgnoreCase(String keyword, Pageable pageable);

Future

  • Future<T> 는 Java5 : 논블로킹 콜을 할수 있으나 get()을 호출시 블로킹 된다.
  • CompletableFuture<T> 는 Java 8
  •  ListenableFuture 는 Spring 에서 제공 : addCallback() 메서드로 콜백함수를 직접 정의할 수 있다.
@Test
    @Rollback(false)
    public void testAsyncQuery() throws InterruptedException {
        this.createComment("Spring Data JPA", 100);
        this.createComment("Hibernate Spring", 55);

        PageRequest pageRequest = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "likeCount"));

        ListenableFuture<List<Comment>> future = asyncQueryRepository.findByCommentContainsIgnoreCase("Spring", pageRequest);

        System.out.println("==========================");
        System.out.println("is Done?"+ future.isDone());
        System.out.println("==========================");

        future.addCallback(new ListenableFutureCallback<List<Comment>>() {
            @Override
            public void onFailure(Throwable ex) {
                System.out.println(ex);
            }

            @Override
            public void onSuccess(@Nullable List<Comment> result) {
                System.out.println("--------------------------------------");
                List<Comment> comments = null;
                try {
                    comments = comments = future.get();
                } catch (Exception e) {
                    e.printStackTrace();
                }

                System.out.println(comments.size());
                comments.forEach(System.out::println);
                System.out.println("--------------------------------------");
            }
        });
        Thread.sleep(5000);

    }

    private void createComment(String comment, int likeCount){
        Comment newComment = new Comment();
        newComment.setComment(comment);
        newComment.setLikeCount(likeCount);
        asyncQueryRepository.save(newComment);
    }

select 쿼리가 먼저 돌아가고 있다..

Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    call next value for hibernate_sequence
==========================
is Done?false
==========================
Hibernate: 
    select
        comment0_.id as id1_1_,
        comment0_.comment as comment2_1_,
        comment0_.like_count as like_cou3_1_,
        comment0_.post_id as post_id4_1_ 
    from
        comment comment0_ 
    where
        upper(comment0_.comment) like upper(?) escape ? 
    order by
        comment0_.like_count desc limit ?
2020-11-15 23:44:18.012 TRACE 11368 --- [lTaskExecutor-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [%Spring%]
2020-11-15 23:44:18.013 TRACE 11368 --- [lTaskExecutor-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [CHAR] - [\]
--------------------------------------
0
--------------------------------------
Hibernate: 
    insert 
    into
        comment
        (comment, like_count, post_id, id) 
    values
        (?, ?, ?, ?)
2020-11-15 23:44:22.460 TRACE 11368 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [Spring Data JPA]
2020-11-15 23:44:22.462 TRACE 11368 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [INTEGER] - [100]
2020-11-15 23:44:22.465 TRACE 11368 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [null]
2020-11-15 23:44:22.466 TRACE 11368 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [1]
Hibernate: 
    insert 
    into
        comment
        (comment, like_count, post_id, id) 
    values
        (?, ?, ?, ?)
2020-11-15 23:44:22.471 TRACE 11368 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [Hibernate Spring]
2020-11-15 23:44:22.471 TRACE 11368 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [INTEGER] - [55]
2020-11-15 23:44:22.471 TRACE 11368 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [null]
2020-11-15 23:44:22.471 TRACE 11368 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [2]

바로 insert 작업을 flushing 시키려면 JpaRepository 가 제공하는 flush()를 사용해야한다.

  • 쿼리 순서는 정확하나 별도 쓰레드에서는 데이터 조작을 감지하지 못하기 때문에 결과 값은 0 이다.
public interface AsyncQueryRepository extends JpaRepository<Comment,Long> {

    @Async("threadPoolTaskExecutor")
    ListenableFuture<List<Comment>> findByCommentContainsIgnoreCase(String keyword, Pageable pageable);
}


////TEST

@Test
    @Rollback(false)
    public void testAsyncQuery() throws InterruptedException {
        this.createComment("Spring Data JPA", 100);
        this.createComment("Hibernate Spring", 55);
        asyncQueryRepository.flush();

        PageRequest pageRequest = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "likeCount"));

        ListenableFuture<List<Comment>> future = asyncQueryRepository.findByCommentContainsIgnoreCase("Spring", pageRequest);

        System.out.println("==========================");
        System.out.println("is Done?"+ future.isDone());
        System.out.println("==========================");

        future.addCallback(new ListenableFutureCallback<List<Comment>>() {
            @Override
            public void onFailure(Throwable ex) {
                System.out.println(ex);
            }

            @Override
            public void onSuccess(@Nullable List<Comment> result) {
                System.out.println("--------------------------------------");
                List<Comment> comments = null;
                try {
                    comments = comments = future.get();
                } catch (Exception e) {
                    e.printStackTrace();
                }

                System.out.println(comments.size());
                comments.forEach(System.out::println);
                System.out.println("--------------------------------------");
            }
        });
        Thread.sleep(5000);

    }

    private void createComment(String comment, int likeCount){
        Comment newComment = new Comment();
        newComment.setComment(comment);
        newComment.setLikeCount(likeCount);
        asyncQueryRepository.save(newComment);
    }

insert 후 select 쿼리를 쏘지만 역시 결과 값은 0 이다.

Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        comment
        (comment, like_count, post_id, id) 
    values
        (?, ?, ?, ?)
2020-11-15 23:50:29.794 TRACE 17164 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [Spring Data JPA]
2020-11-15 23:50:29.795 TRACE 17164 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [INTEGER] - [100]
2020-11-15 23:50:29.797 TRACE 17164 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [null]
2020-11-15 23:50:29.798 TRACE 17164 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [1]
Hibernate: 
    insert 
    into
        comment
        (comment, like_count, post_id, id) 
    values
        (?, ?, ?, ?)
2020-11-15 23:50:29.803 TRACE 17164 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [Hibernate Spring]
2020-11-15 23:50:29.804 TRACE 17164 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [INTEGER] - [55]
2020-11-15 23:50:29.805 TRACE 17164 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [null]
2020-11-15 23:50:29.805 TRACE 17164 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [2]
==========================
is Done?false
==========================
Hibernate: 
    select
        comment0_.id as id1_1_,
        comment0_.comment as comment2_1_,
        comment0_.like_count as like_cou3_1_,
        comment0_.post_id as post_id4_1_ 
    from
        comment comment0_ 
    where
        upper(comment0_.comment) like upper(?) escape ? 
    order by
        comment0_.like_count desc limit ?
2020-11-15 23:50:30.263 TRACE 17164 --- [lTaskExecutor-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [%Spring%]
2020-11-15 23:50:30.263 TRACE 17164 --- [lTaskExecutor-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [CHAR] - [\]
--------------------------------------
0
--------------------------------------

어플리케이션 성능 최적화는 DB 에 달려있기 때문에 이렇게 하나 저렇게하나 결국 DB 관련 문제에 봉착하게 된다.

  • 메인쓰레드가 놀지않게 되고, 자원을 효율적으로 관리하는 것은 좋은 방법이다. 그러나 다이나믹한 성능향상을 보여주지는 못한다.
  • 어플리케이션 성능에 큰 영향을 주는 것은 DB 쿼리에서 발견되는 것 이기 때문에 성능 최적화는 SQL 질의 횟수를 줄이는 것이고, 필요한 데이터만을 가져오는 것이다.


쓴다면 Spring5에 들어온 WebFlux 기능을 사용해야한다.

  • Spring ReactiveWebFlux 를 지원하는 JDBC 가 존재하지 않는다.
  • MongoDB 같은 NoSQL 은 Reactive 를 지원하기 때문에 비동기 기능을 십분 활용할 수 있게 된다.

NoSQL 도 배우고, Spring Reactive 나중에 함 봐야겠다. 비동적으로 접근하는 걸 보면 너무너무 경탄할 것 같다.