서론
앞 번에 개발한 소스에는 두 가지 문제점이 있었다
- Network I/O를 순차 실행
- O(n) 시간이 걸림 : timeout * attachment 갯수
- Async로 O(1)만에 끝내도록 튜닝 필요
- Failover
- attachment는 단순 부가 정보임에도 불구하고 attachmentService에서 exception이 발생하면, 아무 정보도 내려줄 수 없음
- attach는 실패해도 Board 정보와 나머지 성공한 attachment는 보여줘야 함
이번에는 위 이슈들을 reactor를 사용해서 해결해보려 한다.
Reactor
우선 왜 reactor를 사용하는 가에 대해서 간략하게 정리해보자
- Rx(Reactive Extension)를 구현하여 쉽게 비동기 프로그래밍 가능
- 또 다른 Rx 구현체인 RxJava와 비교했을 때, 다음의 장점이 있음
- Spring5에 통합하기 쉬움
- Java8에 대한 지원
- rxJava는 1.6버전부터 쓸 수 있으며 자체적으로
Function
을 구현해서 사용 - Reactor는 Java8부터 쓸 수 있으며 Java8 Api와 Optional 등을 지원
- rxJava는 1.6버전부터 쓸 수 있으며 자체적으로
여기서 Reactor API에 대한 기초적인 사항들은 다루지 않으려 한다. 참고로 reactor는 Java 버전에 영향을 받는다. 본문의 소스는 Spring 5에서 작성되었지만 Java8을 사용한다면 아래 소스를 Spring4에 적용해도 문제없이 동작해야 한다.
AttachmentWrapperItem
본격적으로 이슈를 해결하기 전에 먼저 한가지 리팩토링을 해야한다. 이전의 내용을 잠깐 살펴보자. BoardDto
는 아래와 같이 AttachmentWrapper
를 가지고 있다
1 |
|
AttachmentWrapper
는 AttachmentType
과 Attachment
를 따로 받고 있다.
1 |
|
reactor를 사용하게 되면 Mono<T>
, Flux<T>
와 같이 Generic으로 표현할 수 있도록 AttachmentType
과 Attachment
를 하나로 묶는 AttachmentWrapperItem
클래스를 작성하고 이를 AttachmentWrapper
에 반영해야한다.
AttachmentWrapperItem
1 |
|
AttachmentWrapper 적용
1 |
|
Attachable interface 변경
기존의 두 개의 파라미터를 받던 것을 하나의 파라미터를 받도록 변경한다.
1 | // 변경 전 |
AttachService 변경
getAttachment
의 반환값을 AttachmentWrapperItem
으로 바꾸자.
1 | AttachmentWrapperItem getAttachment(Attachable attachable); |
AttachWriterToBoardService 변경
AttachService
의 변경된 로직을 반영한다.
1 | // 변경 전 |
AttachmentAspect 변경
1 | // 변경 전 |
reactor로 비동기 프로그래밍 적용
attachService.getAttachment()
를 호출할 때 Network I/O가 발생하고 있다.
문제는 이 메서드가 attachment해야할 갯수만큼 실행이 된다는 점이다.
이를 비동기 프로그래밍을 적용해서 해결해보자.
의존성 설정
1 | compile('io.projectreactor:reactor-core:3.1.5.RELEASE') |
AttachService 수정
getAttachment
의 반환형을 Mono<AttachmentWrapperInfo>
로 수정한다.
1 | public interface AttachService<T extends Attachable> { |
AttachWriterToBoardService 수정
수정한 AttachService
의 구현체인 AttachWriterToBoardService
에 변경된 내용을 반영하자.
1 |
|
AttachmetAspect 수정
Attachable
의 구현체의 타입에 맞추어 service를 실행하고
얻은 Mono
의 List를 각각 비동기로 실행시키고, block()
을 호출해 동기화합니다
1 | private void executeAttach(Attachable attachable) { |
실행
이제 비동기로 돌아가는 것을 확인해볼 차례다.
테스트 코드를 짜서 확인하는 것이 가장 좋겠으나…
간편히 Thread.sleep(3000)
을 줘서 확인해보도록 한다.
1 | // 댓글 서비스에 3초 슬립 |
3초 이상, 6초 이내에 요청이 오면 성공이다
reactor로 실패 극복
reactor로 실패를 극복하는 방법은 간단하다. 오류가 발생하면 앞서 작성했던 AttachmentWrapperItem.ON_ERROR
를 반환하도록 하면 된다. Rx는 이러한 상황을 위한 API들이 모두 정의하고 있다.
AttachService에서 예외 발생시 처리
AttachWriterToBoardService
에서 Writer
를 가져오는 중에 Exception이 발생하면 AttachmentWrapperItem.ON_ERROR
를 보내도록 변경한다.
1 | 4j |
AttachmentAspect에서 ON_ERROR를 거르도록 로직 변경
앞서 AttachmentAspect
에서 List<Mono>
를 비동기로 실행시키고 결과 값들을 List<AttachmentWrapperItem>
에 모아서 Attachable
에 넣어줬다.
간단히 비동시 실행결과가 ON_ERROR
인 경우를 필터링하면 성공한 결과만을 모아 List<AttachmentWrapperItem>
을 만들 수 있다.
1 | 4j |
실행
이전에 100번 게시판을 불러올 때, 작성자 정보를 가져오려고 하면 FeignClient
에서 404를 던지고 아래와 같이 API 자체가 실패했었다.
GET /boards/100?attachment=comments,writer
1 | { |
1 | feign.FeignException: status 404 reading WriterClient#getWriter(long); content: {} |
하지만 장애 극복을 적용한 후에는 예외는 warn
으로 로그를 남기되, 성공한 부분까지는 응답을 할 수 있게 되었다.
1 | { |
1 | 2018-03-08 19:59:12.056 WARN 64890 --- [ elastic-5] c.p.s.s.a.s.w.AttachWriterToBoardService : status 404 reading WriterClient#getWriter(long); content: |
마무리
Reactor를 사용해서 비동기 프로그래밍을 하고, 장애에 대처해 극복할 수 있게 해봤다.
중간에 reactor에 timeout()
을 사용했는 데,
이 부분은 client를 FeignClient
를 사용해서 application.yml
로 빼서 따로 관리할 수 있다.
이전에 공유했던 Hystrix
와도 연계해서 fallback을 구현할 수도 있어서, 강력한 장애 대응을 할 수 있다.
여전히 현재의 코드는 큰 단점이 있다.AttachmentAspect
에서 reactor의 block()
을 호출한다는 점이다.
이것이 단점인 이유는 reactor learn 페이지에서 가져온 사진 3장으로 설명할 수 있을 것 같다.
출처 : https://projectreactor.io/learn |
즉 Non-Blocking을 사용해서 자원을 효율적으로
사용하지 않았다는 것이다. 100만원짜리 서버를 써야 하던 일을, 50만원짜리 서버로 처리할 수 있다면 그렇게 해야한다. 때문에 SpringFramework 5에서는 webflux를 사용하여 netty기반(기본설정)으로 Non-Blocking + Async를 사용할 수 있도록 했다.