목차

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을 이용하여 비동기 처리 개발할 때 ThreadPoolTaskExecutorThreadPoolTaskScheduler를 사용하였다. 쉬운 비동기 처리를 위해 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

스케줄러는 말마따나 별도의 쓰레드가 백그라운드에서 주기적으로 작업을 해주는 것이다. 그 이상.. 그 이하.. 설명할 것이 없다.. ThreadPoolTaskSchedulerThreadPoolTaskExecutor와 같이 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를 통해 스케줄링 시간을 설정할 수 있다.
  • 설정파일 값을 파싱하여 스케줄링 가동 여부를 설정할 수 있다.
  • 위의 인터페이스와 추상 클래스를 통해 통일된 스케줄러 클래스를 개발할 수 있다.

참고문헌

sqlite.org/atomiccommit.html

www.quora.com/What-is-the-difference-between-a-journaling-vs-a-log-structured-file-system

www.baeldung.com/spring-async

www.baeldung.com/spring-task-scheduler

 

Posted by 동팡

서두

  • CURRVAL을 잘못 사용할 경우, 끔찍한 상황이 도래될 수 있다.
  • 정말 기본적인 사항을 간과했습니다...

오류

WARN  [org.jboss.jca.adapters.jdbc.local.LocalManagedConnectionFactory] (default task-10) IJ030027: Destroying connection that is not valid, due to the following exception: …

 중략

java.sql.SQLException: JDBC-6003: Sequence "AUDIT_REQUEST_ID_SEQ" has not been accessed in this session: no CURRVAL available.

위의 오류 상황은 다음과 같다.

  • API 요청에 대해 감사 로깅을 진행한다.
  • API 요청은 3개의 작업을 1개 트랜잭션 으로 작업을 진행한다.
  • 감사로그는 3개의 작업에 대해 각각 감사로그를 진행한다. 즉, 총 3개의 감사로깅을 진행한다.
  • 첫 번째 감사로그는 NEXTVAL을 사용한다.
  • 두 번째, 세 번째 감사로그는 CURRVAL을 사용한다.

Tibero-JDBC-6003오류의 경우, 현재 접속 세션(Connection)SEQUENCE.NEXTVAL 없이 SEQUENCE.CURRVAL을 사용했을 때 발생한다.

 

암호키 등록에 대한 감사로그 입력은 총 3회 실시한다. 첫 번째 감사로그를 입력할 때 NEXTVAL을 호출한다. 두 번째부터는 CURRVAL을 사용한다. 두 번째 감사로그 입력 후, DB Connection Reset이 발생하였다. 해당 사항으로 인해 CURRVAL의 값을 보관했던 커넥션(Session)은 소멸되었다. 그 후, 세 번째 감사로그 입력할 때 CURRVAL을 찾지 못한다.

그러나 위의 상황 분석을 통해 다음과 같이 통찰할 수 있다(다음의 통찰을 통해 쪽팔림 1을 획득할 수 있다.).

 

  • CURRVALNEXTVAL을 호출한 세션에서 호출해야 한다.
  • CURRVAL의 값은 Thread-Safe하지 않다.

(은연중에 CURRVAL이 Thread-Safe하지 않은 것을 알고 있었지만, 연계해서 생각을 하지 못 했다......)

 

그림으로 표현하면 아래와 같다.

  • 1개 트랜잭션 내부에서의 NEXTVAL->CURRVAL->CURRVAL은 상관없다고 생각한다(트랜잭션 과정 중 DB 세션이 안죽는다는 가정 하에(트랜잭션 중 DB 세션이 죽으면 그건 또 다른 문제이니까 상관 없을 듯...)).
  • 그러나, 위와 같이 별개의 트랜잭션에서의 NEXTVAL->CURRVAL->CURRVAL은 문제가 상당하다. 심각하다. 자세한 사항은 아래 "위의 사항으로 인한 추가 위험"을 참고한다.

위의 사항으로 인한 추가 위험

위의 원인 분석을 토대로 추가 위험사항을 분석한다. 해당 분석은 시나리오 정의와 검증을 토대로 분석을 진행한다. 대표적인 시나리오는 총 두 개,아래와 같다.

  • Thread-Safe 하지 않는 값의 사용으로 인해 의도하지 않는 값을 테이블에 적재
  • CURRVAL의 잘못된 사용으로 인해 어플리케이션 오류 발생

Time-1) Session1

 

Time-2) Session2

Time-3) Sesseion1

  • 두 개의 SessionCURRVAL 값이 다르다.
  • 만약, Time-3) Session1의 CURRVAL 값은 2가 아닌, 1이다.

[시나리오2 검증 : 잘못된 사용으로 인한 오류 발생]

현재의 오류가 시나리오2와 같다. 위의 오류는 DBCP 커넥션(세션)의 소멸로 인해 발생하였다. 그러나 많은 동접이 쇄도할 때 아래 그림과 같이 오류가 발생할 수 있다. 모든 요청을 Session1에서 처리하는 것으로 예상하였지만 두 번째 CURRVAL 요청은 Session2에서 처리할 경우 오류가 발생할 수 있다.

 

부록

  • 현재 DBMS Tibero이다. 그러나, Oracle 또한 위와 같은 상황이 있을 수 있다 [1].
  • 오라클 오류는 "ORA-08002: sequence string. CURRVAL is not yet defined in this session"이다.

 

 


[1] http://nimishgarg.blogspot.com/2014/07/ora-08002-sequence-stringcurrval-is-not.html

Posted by 동팡

스프링에서 빈을 생성할 때 빈의 Scope(생명주기)은 싱글톤이다. 해당 빈 생성은 디폴트이다.

Prototype, request, session 등 다양하게 존재한다.

 

여기서 잠깐 싱글톤이 뭔지 지고 넘어가자.

싱글톤 패턴은 생성자가 여러 차례 호출되더라도 결국 실제로 생성되는 객체는 하나이고, 최초 생성한 객체를 계속 리턴한다. 보통 Java 에서의 getInstace() 메소드는 보통싱글톤을 의미한다(다 그렇지는 않다).

 

, Spring에서 @Components, @Service, @Controller, @Repository 등의 모든 빈들은 WAS가 가동할 때 싱글톤으로 객체를 인스턴스화 후, Spring Container에 보관한다(IoC 행위). 그 다음은 우리는 Inject를 통해 사용한다(DI 행위).

여기서 질문은 다음과 같다.

 

아니, Controller Service 객체가 1개인데 멀티 쓰레드 환경에서 Thread-Safe하게 어플리케이션을 어떻게 운용하냐? 그리고 Spring BeanThread-Safe 하냐??

 

일단 답은 No이다. Spring 빈은 Thread-Safe하지 않다.

Thread-Safe하게 사용하면 Yes이고, 그렇지 않으면 No이다. 다음과 같이 맴버 변수를 사용하면 Thread-Safe하지 않다.

그럼 어떻게 사용해야하는가?

빈을 불변하도록 한다(Immutable).

1. Constructor Inject 사용한다.

2. Builder 패턴을 활용한다.

 

3. 빈에 대해 Setter를 허용하지 않는다.

빈은 무상태여야 한다(Stateless).

빈의 특정 상태를 나타내는 변수를 계속 힙메모리에 상주시키면 안된다(위의 Calculator와 같은 행동).

 

참고

https://stackoverflow.com/questions/15745140/are-spring-objects-thread-safe

https://alwayspr.tistory.com/11

https://beyondj2ee.wordpress.com/2013/02/28/멀티-쓰레드-환경에서-스프링빈-주의사항/

https://stackoverflow.com/questions/25617962/how-does-the-singleton-bean-serve-the-concurrent-request

https://javacan.tistory.com/entry/ThreadLocalUsage

 

Posted by 동팡