안전한 비동기 처리 전략(Feat. Spring)
목차
1부. 안전한 비동기 처리 설계 경험
- 개요
- 비동기
- 저널링
- 비동기 설계
2부. Spring을 통한 비동기 처리 개발
- 개요
- ThreadPoolTaskExecutor
- ThreadPoolTaskScheduler
1부. 안전한 비동기 처리 설계 경험
개요
TiberoDBMS를 사용하는 상태, 이 DBMS는 너무 느리다. DBA 또는 DB 엔지니어가 설정을 어떻게 했는지 모르는데 진짜 느리다. 아무리 DBMS 동접자가 많다한들 이런식으로 느린거는 진짜 선을 넘은 것이다.
각설하고.. 고객님께서 삭제 API가 느리다고 콜이 왔다. 이래저래 DB 쪽에 문제가 많았는데.. 많은 과정을 해결하고 비동기 처리 적용을 하는 방향이 나왔음.
CRU(Create, Read, Update) 부분의 경우, 해당 작업 후, 사용의 여지가 있기 때문에 비동기 처리하기에 적합하지 않다. Delete의 경우, 비동기 처리를 해도 무방하였다. 그러면 비동기를 처리해보자.
비동기
비동기를 잘 설명할 수 있는 생활의 예는 우리는 커피 주문 후 주문대에서 계속 기다리지 않고 벨을 받고 다른 곳에서 핸드폰을 하거나 일행과 대화를 하며 "다른 행위"를 한다. 이게 바로 비동기닼ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
동기의 경우, 커피 주문 후 주문대에서 아무것도 안하고 커피만 나올 때 까지 기다리는 것이다.
필자의 몇몇 프로젝트는 비동기를 넣은 용례(아래 그림의 작업을 비동기로 처리함)가 존재한다. 아래 그림의 설명은 다음과 같다.
- A 프로세스 시작
- B 프로세스 시작 요청
- B 프로세스 시작 요청 후 대기하는 것이 아닌 A 프로세스 재개
- A 프로세스 완료 후, B 프로세스 완료 상태 확인 후, 반환
비동기 처리할 때 조심해야할 사항은 무엇일까? 제일 주의해야할 사항은 작업의 무결한 처리이다. 동기 처리의 경우 눈으로 확인이 가능하지만, 비동기 처리의 경우, 눈으로 확인하기 힘들다. 그래서 우리는 "비동기는 실패할 수 있다." 라는 가정하에 개발을 하였다.
저널링
비동기 처리 기술은 아니지만, 파일 시스템의 변경사항을 반영하기 전에, 저널안에 변경 로그(추적할 수 있는 기록)들을 저널에 기입한다. 파일 시스템 변경사항을 복구할 때 해당 저널들을 참고하여 복구한다.
저널링을 잘 이해하기 좋은 것은 Sqlite 트랜잭션 개념 중, "저널 모드"를 이해하면 좋다. Sqlite는 1개 프로세스만 Write 작업(CRU)을 할 수 있다. Sqlite의 트랜잭션을 진행할 때 1개 프로세스가 DB 파일에 Write 작업을 실시한다. 이 때 생성되는 임시 파일이 JOURNAL 파일이다. 반영사항은 DB파일에 기록하고 복구할 파일? 기록들은 JOURNAL에 기록하는 것이다. 만약 Commit이 아닌 Rollback을 진행할 때 JOURNAL 파일을 통해 Rollback을 실시한다. 이런 저널링은 리눅스 파일 시스템 뿐만아니라 많은 곳에서 활용한다.
저널링을 하는 이유는 다음과 같다. 작업 중, 얘기치 못한 이유로 작업을 실패했을 때 해당 지점을 찾아서 복구 또는 작업의 완료를 진행하기 위해서이지 않을까 생각이든다. 또는 시점 복구 기법을 사용할 때 이것을 쓰지 않을까? 생각한다.
(리눅스 파일 시스템에서 사용된다.) <<- 이 부분 분석/공부 후 추가 기재 필요
(번외로 면접에서 비동기 처리 관련 답변을 했었는데 내가 비동기 개발할 때 저널링 기법을 사용했다. 근데 그 때 당시 저널링 방식이 뭔지 몰랐다. 면접관이 묻더라.. "그런 방식을 뭐라하는지 아세요~?" 생각하다 잘 모르겠습니다. 했는데, 저널링이라고 한번 알아보라고 말씀해주셨다. 정말 고마웠다. 이런 피드백... 근데 Sqlite 분석할 때 분석했던 내용이더라... 에휴... 근데 몰랐었음... 그리고 이 글을 작성하는 동기도 주셨지... )
저널링의 발전이 DB redo log가 아닐까 생각이 든다...
비동기 설계
위에서의 설명과 같이 우리는 완벽한 저널링은 아니더라도 저널링 기법을 본 받아서 비동기 처리 프로세스를 설계해야한다.
(아래 그림은 기존 설계 사항에서 몇개 삭제하였다.)
순서는 다음과 같다.
- 클라이언트의 삭제 요청이 들어온다.
- Server A는 삭제하고자하는 정보들의 식별정보와 삭제 요청한 주체에 대한 정보들을 특정 디렉토리에 기재한다(저널링, 간단한 저널링이다.).
- 파일 입력 완료 후, 비동기 쓰레드 풀에 notify를 진행한다.
- Cleint에 삭제 성공을 반환한다(202 Accepted).
- 비동기 쓰레드 풀은 특정 쓰레드를 지정하여 로직을 진행한다.
- 삭제하고자하는 식별정보는 파일에 입력했지만 메모리에 보관하고 있기 때문에 조회를 실시하지 않아도 된다.
- 삭제를 진행한다.
- 삭제 트랜잭션은 Server A, B에서 일어나기 때문에 DB A, B 를 접근하여 삭제를 실시한다(이것도 비동기 로직을 태운다 ㅋㅋㅋ).
- 삭제 완료 후, 결과 감사로깅을 진행한다.
- 삭제 트랜잭션을 성공적으로 완료하였다.
- 파일에 입력한 것(JOURNAL 파일)을 삭제한다.
위의 사항이 기본 순서이다. 그러나 위의 작업 중 얘기치 못한 시스템 오류로 서버가 종료 되었을 경우, 다음과 같이 진행한다.
- Destroy 스케줄러는 주기적으로 디렉토리에 있는 파일들을 검사한다.
- 파일이 있는 경우, 정상적으로 삭제를 하지 못한 것이다.
- 또한 이 때 주의할 사항이 Thread-Safe이다.
- 비동기 쓰레드가 삭제 하고 있을 때 스케줄러가 접근할 수 있다.
- Lock 방식으로 Thread-Safe하게 개발한다. 먼저 파일을 획득하고 선점하면 아무도 접근하지 못한다.
- 스케줄러는 Lock을 획득하여 삭제를 시도한다.
- 스케줄러는 삭제 트랜잭션을 성공적으로 완료한다.
- 스케줄러에 대한 결과 값을 감사로깅을 한다.
위의 사항에서 "API 감사로그", "스케줄러 감사로그"를 통해 작업의 완료 여부를 확인할 수 있다.
더 나아가 발전하면 Admin 툴을 이용하여 비동기 실패 여부를 모니터링 기능을 제공할 수 있다.
2부. Spring을 통한 비동기 처리 개발
개요
Spring을 이용하여 비동기 처리 개발할 때 ThreadPoolTaskExecutor와 ThreadPoolTaskScheduler를 사용하였다. 쉬운 비동기 처리를 위해 ThreadPooltaskExecutor를 사용했으며, 비동기 처리의 실패를 보완하기 위해 ThreadPoolTaskScheduler를 통해 실패한 요청에 대해 재시도하는 로직을 개발하였다. 간략하게 위의 두 가지 사항에 대해 알아보자.
ThreadPoolTaskExecutor
Spring에서 설정과 어노테이션의 설정으로 간략하게 비동기 처리를 개발할 수 있다. 스프링의 AOP 전략으로 비동기 처리를 깔끔하게 한다 생각해도 좋다(트랜잭션과 비슷한 원리). 트랜잭션 또한 @Transactional 어노테이션을 통해 트랜잭션 관련 코드가 내 비지니스 로직을 침투를하지 않는다. 이것을 가능하게 하는 것이 SpringAOP이며 Spring 비동기도 마찬가지이다. @Async 어노테이션을 통해 비동기 처리 관련 코드가 내 비지니스 로직을 침투하지 않는다.
@Async 어노테이션은 다음과 같이 지정해야한다.
- 메소드 레벨
- public 메소드
- Async는 또다른 Async를 호출하면 안된다.
비동기 반환 타입은 다음과 같이 진행할 수 있다.
- void
- Future<$너의타입>
java.utils.concurrent.Future을 통해 비동기 반환을 받을 수 있다. 그러나 비동기의 반환의 경우 아래와 같이 기다림이 필요하다.
Future<String> future = asyncAnnotationExample.asyncMethodWithReturnType();
while (true) {
if (future.isDone()) {
System.out.println("Result from asynchronous process - " + future.get());
break;
}
System.out.println("Continue doing something else. ");
Thread.sleep(1000);
}
Future를 사용할 경우, 다음의 이점이 존재한다.
- Future<>.get()을 통해 Exception을 메인 쓰레드(비동기를 호출한 부모 쓰레드)로 전파시켜 Exception 핸들링을 할 수 있다.
- 그러나 AsyncUncaughtExceptionHandler를 통해 해결할 수 있다.
SimpleAsyncTaskExecutor를 통해 바로 설정을 할 수 있지만 이런 방법을 통해 설정을 할 수 있다.
ThreadPoolTaskScheduler
스케줄러는 말마따나 별도의 쓰레드가 백그라운드에서 주기적으로 작업을 해주는 것이다. 그 이상.. 그 이하.. 설명할 것이 없다.. ThreadPoolTaskScheduler는 ThreadPoolTaskExecutor와 같이 TaskExecutor 인터페이스를 구현하였다. 즉, 비동기 쓰레들을 별도 관리 해준다.
@Scheduled 어노테이션을 통해 간단하게 구현할 수 있지만 좀 복잡하게 설정이 필요한 경우가 존재하여 해당 어노테이션으로 구현하지 않고 일부는 수동으로 설정하였다.
예제는 다음과 같다.
[SchedulerConfig.class]
@Bean(name="scheduler")
public ThreadPoolTaskScheduler threadPoolTaskScheduler() throws IOException {
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize( 2 );
threadPoolTaskScheduler.setThreadNamePrefix( "ThreadPoolTaskScheduler" );
return threadPoolTaskScheduler;
}
@Bean
public ISchedulerOperation intValidOperation( SchedulerAuditLogService schedulerAuditLogService, Invoker invoker, SchedulerDao dao ) {
if ( settings.getSchedulerAuditMode() ) {
return new AuditSchedulerOperation( new IntValidSchedulerOperation( dao, invoker ), schedulerAuditLogService );
} else {
return new IntValidSchedulerOperation( dao, invoker );
}
}
첫 번째 Bean은 ThreadPoolTaskScheduler이다. 가볍게 보고 넘어가도 괜찮다.
두 번째 Bean은 프록시, 데코레이터 방식을 통해 빈을 상황에 따라 선택적 주입을 실시한다. 위의 코드를 말로 풀면 스케줄러에 대해 감사로그를 실시할 것 인가 안할 것인가 이다.
[Scheduler.class]
public abstract class Scheduler {
boolean state = false;
@Autowired
private SchedulerDao dao;
@Autowired
private ThreadPoolTaskScheduler threadPoolTaskScheduler;
@PostConstruct
public void scheduleRunnableWithCronTrigger() throws IOException {
if ( isEnabled() ) {
threadPoolTaskScheduler.schedule( runner(), getTrigger() );
}
}
public SchedulerDao getDao() { return this.dao; }
public abstract Runnable runner();
public abstract Trigger getTrigger();
public abstract boolean isEnabled();
}
Scheduler를 확장한 구현체의 runner메소드에서 ISchedulerOperation인터페이스의 run메소드를 실행한다. 구현체는 별도 첨부를 하지 않았다.
위의 코드와 설정 파일을 통해 구현체를 아래와 같이 구현할 수 있다.
- 설정파일에 값을 파싱하여 CronTrigger를 통해 스케줄링 시간을 설정할 수 있다.
- 설정파일 값을 파싱하여 스케줄링 가동 여부를 설정할 수 있다.
- 위의 인터페이스와 추상 클래스를 통해 통일된 스케줄러 클래스를 개발할 수 있다.
참고문헌
www.quora.com/What-is-the-difference-between-a-journaling-vs-a-log-structured-file-system
www.baeldung.com/spring-task-scheduler
'개발관련 > 삽질' 카테고리의 다른 글
Servlet/Spring에서 web.xml은 어떻게 사라졌을까? (0) | 2021.04.28 |
---|---|
Apache Commons-DBCP / Hikari-DBCP 정리 (0) | 2021.04.05 |
Oracle/Tibero CURRVAL 사용 주의사항 (0) | 2021.03.29 |
LoRa 1.0 취약점 분석 (0) | 2021.02.04 |
LoRaWAN 1.1 보안 부분 분석(LoRa Spec 6장 일부분 번역) (0) | 2021.02.04 |