코드/dev

@async와 @transactional

미로처럼 2024. 4. 1. 11:55
728x90

재직 중인 회사에서 작업 후 잘못 작업 한 부분이 있어  이를 확인하고 정리 할려고 한다. 

spring @Transaction, @Async 2가지  어노테이션을 사용법이나 주의 사항을 정리하고자 한다.

 

 

@Transactional, @Async  흔하게 많이 쓰는 어노테이션이다. 

 

 

 

Trasction 이란 데이터베이스의 상태를 변화시키기 위해서 수행하는 작업의 단위를 뜻한다. 

 

Spring에서 트랜잭션 관련된 3가지 핵심 

1. 동기화

2. 추상화

3. AOP이용한 트랜잭션 분리 

 

 

동기화 

 

트랜잭션을 시작하기 위한  connection 객체를 특별한 저장소에 보관해두고 필요할때 꺼내쓸 수 있도록 하는 기술 

 

//트랜잭션 매니저  시작 

TransactionSynchronizeManager.initSynchronization();
Connection c = DataSourceUtils.getConnection(dataSource);



//종료
DataSourceUtils.releaseConnection(c, dataSource);
TransactionSynchronizeManager.unbindResource(dataSource);
TransactionSynchronizeManager.clearSynchronization();

 

트랜잭션 동기화 저장소는 작업 쓰레드마다 Connection 객체를 독립적으로 관리, 멀티쓰레드 환경에서도 충돌이 발생할 여지가 없다. 

*Hibernate 에서는 Connection이 아닌 Session 을씀 이로 인한 JDBC  종속적인 트랜잭션 동기화 코드들을 문제를 유발

이를 해결하기 위해 아래 기술할 추상화 기술 제공.

 


 

추상화

 

Spring은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술 제공.  이를 통해  JDBC, JPA, Hobernate 등 종속적인 코들르 이용하지 않고 일관되게 트랜잭션을 처리 가능.

 

 

트랜잭션 경계설정을 위한 추상 인터페이스는 Platform TransactionManager 이다.

 


 

AOP 

 

흔히 우리가 쓰는 @Transactional 어노테이션이다. 이 어노테이션을 사용 함으로써 마치 트랜젹션 코드가 존재하지 않는 것 처럼 보이지만 실제로 트랜잭션이 적용이 되는것이다.

 

 


 

트랜잭션 세부 설정

 

Spring의 DefaultTrasactionDefinition이 구현하고 있는 TrasctionDefinition 인터페이스는 트랜잭션의 동작방식에 영향을 줄 수 있는 네 가지 속성을 정의 하고 있다. 아래 4가지 속성은 세부적으로 이용할 수 있게 도와주며, @Transactional 어노테이션에도 공통적으로 적용할 수 있다. 

 

1. 트랜잭션 전파

2. 격리수준

3. 제한시간

4. 일기전용

 


트랜잭션 전파

 

트랜잭션 전파란 트랜잭션의 경계에서 이미 진행중인 트랜잭션이 있거나 없을 때 어떻게 동작할 것인가를 결정하는 방식을 의미.

예를 들어 A 작업에 대한 트랜잭션이 진행중이고 B 작업에 대한 트랜잭션을 어떻게 처리할까에 대한 부분이다.

 

A의 트랜잭션 참여 (PROPAGATION_REQUIRED)

B의 코드는 새로운 트랜잭션을 만들지 않고 A에서 진행중인 트랜잭션에 참여할 수 있다. 이 경우 B의 작업이 마무리 되고 나서, 남은 A의 작업(2)를 처리할 때 예외가 발생하면 A와 B의 작업이 모두 취소된다. 왜냐하면 A와 B의 트랜잭션이 하나로 묶여있기 떄문이다.

 

독립적인 트랜잭션 생성(PROGATION_REQUIRES_NEW)

반대로 B의 트랜잭션은 A의 트랜잭션과 무관하게 만들 수 있다. 이 경우 B의 트랜잭션 경계를 빠져나오는 순간 B의 트랜잭션은 독자적으로 커밋 또는 롤백되고, 이것은 A에 어떠한 영향도 주지 않는다. 즉 이후 A가 (2)번 작ㅇ버을 하면서 예외가 발생해 롤백되어도 B의 작업에는 영향을 주지 못한다.

 

 

 

트랜잭션 없이 동작(PROGATION_NOT_SUPPORTED)

B의 작업에 대해 트랜잭션을 걸지 않을 수 있다. B의 작업이 단순 데이터 조회라면 굳이 트랜잭션이 필요 없을 것이다.

 


 

격리 수준

 

모든 DB 트랜잭션은 격리 수준을 가지고 있어야한다. 서버에는 여러 개의 트랜잭션이 동시에 진행 될 수 있는데, 모든 트랜잭션을 독립적으로 만들고 순차 진행 한다면 안전하지만 성능이 떨어짐. 따라서 적절하게 격리수준을 조정해서 가능한 많은 트랜잭션을 동시에 진행시키면서 문제가 발생하지 않도록 제어해야 한다. 이는 JDBC  드라이버나 DataSource  등에서 재설정하고 트랜잭션 단위 로 격리 수준을 조정할 수 있다.

DefualtTrasactionDefinition에 설정된 격리 수준은 ISOLATION_DEFAULT로 DataSource에 정의된 격리 수준을 따른다는 것이다.

기본적으로는  기본 격리 수준을 따르는 것이 좋지만. 특별한 작업을 수행하는 메소드라면 독자적으로 지정해줄 필요가 있다.

 

 


 

제한시간

 

트랜잭션을 수행하는 제한시간을 설정할 수 있다.  제한시간의 설정은 트랜잭션을 직접 시작하는 PROGATION_REQUIRED나 PROGATION_REQUIRES_NEW의 경우에 사용해야만 의미가 있다.

 


 

읽기전용

읽기전용으로 설정해두면 트랜잭션 내에서 데이터를 조작하는 시도를 막아주며 데이터 엑세스 기술에 따라 성능 향상이 된다.

(재직중인 곳이 아니더라도 보통 SELECT 문 을 처리할떄 slaveDB에 읽기전용으로 붙어서 쓰고 있다.)

 

 

 

 

 


 

@Async

 

@Async는 spring 에서 제공하는 Thread Pool을 활용하는 비동기 메소드 지원 어노테이션이다. 

@Async로 annotationg된 bean 의 메소드는 별도의 스레드에서 실행

기존 JAVA 에서 비동기 방식으로 메서드를 구현할때  java.util.concurrent.ExecutorService을 활용해서 비동기 방식의 method를 정의 할 때마다, Runnable의 run()을 재구현해야 하는 등 동일한 작업들의 반복이 잦았다.

 

그러나 @Async 사용 시  손쉽게 비동기 메서드 작성 가능

 

spring의 Async 지원은 JDK Proxy 또는 CGlib와 같은 객체를 사용하여 Async가 정의된 객체를 생성

그 후 Spring은 메소드의 로직을 별도의 실행 경로로 제출하기 위해 컨텍스트와 연결된 스레드 풀을 찾으려고 합니다. 구체적으로는, 고유한 TaskExcutor 빈이나 taskExcutor로  이름 지정된 빈을 검색

이러한 빈이 없으면 기본적으로 SimpleAsyncTaskExcutor를 사용

 

SimpleAsyncTaskExcutor는 스레드 풀이 아닙니다. 그렇기 때문에 스레드를 관리하고 재사용하는 것이 아니라 계속 만들어 냅니다.

스레드 자원이 많이 들기 때문에 SimpleAsyncTaskExcutor를 쓰지 말아야 합니다.

 

따라서 TaskExecutor를 빈으로 등록하여 사용하는것이 옮은 방법.

 

아래는 async config 중 일부이다.

@Configuration
@EnableAsync
public class AsyncThreadConfiguration {
	@Bean
	public Executor asyncThreadTaskExecutor() {
		ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
		threadPoolTaskExecutor.setCorePoolSize(5);
		threadPoolTaskExecutor.setMaxPoolSize(10);
		threadPoolTaskExecutor.setThreadNamePrefix("Executor-");
		return threadPoolTaskExecutor;
	}
}

 

 

스레드 생성 하는 경우 

Rules for creating Threads internally by SUN:

  • the number of threads 가 corePoolSize보다 작으면 새로운 스레드를 생성
  • the number of threads 가 corePoolSize 같거나(또는 크다면), task를 큐에 넣습니다.
  • 큐가 다 찼을 때 the number of threads가 maxPoolSize보다 작으면 새로운 스레드를 생성
  • 큐가  the number of treads가 maxpoolsize보다 같거나(또는 크다면) 해당 task를 reject 한다.(RejectExcutionException)

 


 

 

주의사항

 

1. private 메서드 적용 불가 proxy로 만들 수 없기 떄문에

 

2. self-invocation 해서는 안됨 proxy로 만ㄷ루 서 없기 떄문에

 

3. return 값은 void 혹은 CompleteableFuture<>

 

 

 

 

간략하게나마 트랜잭션과 async 처리에 대해서 정리해보았는데  앞으로는 좀더 알고 사용 알 수 있을것 같다.

728x90