시작하며..
최근에 추천받아서 주니어 백엔드 개발자가 반드시 알아야 할 실무 지식 이란 책을 읽기 시작했다!!

책을 읽다 보니 동기/비동기 파트가 이전에 프로젝트 때 아쉬웠던 부분을 떠오르게 해서 재밌게 일고 있었습니다!! 비동기 방식을 하기 위해선 3가지 방식이 있다고 하는데..!!
1.Thread 이용하기 (ThreadPool과 연관)
2.MessageQueue 이용하기 (카프카 , Rabbit MQ , Redis...)
3. CDN 이용하기
였다..!!!
기존 프로젝트 때 우승 했던 팀이 Thread를 활용해서 비동기를 구현했던 것 같아서 공부하기로 했다!!
가장 먼저 든 의문..(사용법)
이전에 DB Connection Pool (CP)의 중요성을 학습한 적이 있었고 , 평소에 학부시절 Thread를 이용해 많은 프로그래밍을 했기 때문에 개념 자체는 어렵지 않았지만 궁금한 점은 이걸 Spring에서 어떻게 사용하지?! 였다!!
공식 문서와 인터넷 자료를 찾아보니 다음과 같은 코드로 ThreadPool을 활용해 비동기를 구현하고 있었다!
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 기본 스레드
executor.setMaxPoolSize(30); // 최대 스레드
executor.setQueueCapacity(100); // 대기 큐
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy()
);
executor.initialize();
return executor;
}
}
사용하는 방법은
new AsyncConfig().asyncExecutor
가 아닌
@Service
public class PushService {
@Async("asyncExecutor")
public void sendPush(){
...
}
}
이런 식으로 사용하고자 하는 위치에서 Async 어노테이션을 이용해 호출을 하면 된다
Spring에서 ThreadPoolTaskExecutor를 활용해서 기본 스레드 수 , 최대 스레드 수 , 대기큐 크기.. 등등을 설정할 수 있었다!
공부하면서 느끼는 건데 게임에서 초반에 캐릭터를 만들 때 커스터 마이징 하는 것처럼 프레임워크들도 개발자의 편의에 맞춰서 다양하게 쓸 수 있게 (=커스터 마이징 할 수 있게) 많은 것들을 만들어 둔 것 같다
조금 더 공부해보자!!!
@Async 어노테이션은 어떻게 동작하기에.. 비동기로 동작할 수 있는 걸까??
정답은!! 현재 호출한 스레드와 분리해서 다른 스레드풀에서 실행되도록 한다
실제로 어떻게 동작하는지 내부 동작을 살펴보면 다음과 같다
Bean 호출 -> Spring 프록시 -> TaskExecutor에 작업 제출 -> 스레드풀 실행
여기서 위에서 언급한 "현재 호출한 스레드와 분리해서.."에서 호출한 스레드란 Tomcat과 같은 WebServer에서의 Thread Pool이다
(참고 : Tomcat의 ThreadPool 은 한 번에 몇 개의 요청을 Application으로 넘기는지 결정을 한다 )
Spring의 ThreadPool 종류
여기까지 공부하면서 "어..? ThreadPool의 종류가 여러 개 있구나!! "가 자연스럽게 생각 이 났다!!
그래서!!! 대표적인 ThreadPool의 종류가 뭐가 있을지 궁금해졌다...
- Schedular Thread Pool
- Batch Thread Pool
- ThreadPoolTaskExecutor (여기가 Async!!)
- Main Thread Pool
그렇군..!!
ThreadPool과 DB 커넥션 관계
다음으로 알아볼 것이 ThreadPool과 DB 커넥션의 관계다!
ThreadPool을 이용하려는 목적이 비동기 작업으로 성능 향상인데 여기서 중요한 관계가 있다 바로..!!! ThreadPool과 DB 커넥션 풀의 관계다!
비동기 스레드 풀의 크기 <= DB 커넥션 풀의 크기
위의 관계를 유지해야 한다 왜.. 그럴까??
개발자 A 씨가 Application의 응답 시간을 향상하기 위해서 Application에서 동작할 여러 작업을 비동기 만들고 싶다..
많은 방법이 있지만 Thread를 이용하려고 했고 ThreadPool의 크기를 (1->10) 늘렸다
그러나 DB connection Pool의 크기를 늘리는 법을 깜빡해서 여전히 CP의 크기가 5 인 상태를 가정해 보자!!
개발자 A 씨가 많은 노력은 했지만 성능이 오히려 나빠질 수 있다..
개발자 A 씨의 의도는 한 번에 10개 까지 비동기 작업을 처리 하고 싶었지만 까먹은 CP의 크기 때문에 성능이 저하가 된다
10개를 아니 100개를 아무리 늘려도 DB가 한번에 연결할 수 있는 크기는 5이기 때문이다.. CP의 크기 이상의 요청은 Connection을 얻기 위해서 기다리게 되고 그만큼 응답시간이 늦춰진다..!!
응답시간을 개선하기 위해 CP에 Timeout 을 평소보다 짧게 가져 가도 성능 저하는 조금이라도 생기게 된다
가장 중요한 건 개발자 A 씨가 의도한 ThreadPool을 늘린 효과를 보기가 어렵다...
그래서 항상 스레드 풀의 크기와 DB커넥션 풀의 크기를 꼭 신경 써줘야 한다!
@Transaction과 @Async
주의할 게 또 있다 이건 진짜 진짜 중요하다
보통 우리가 @Transaction을 왜 쓸까?!?
Transaction으로 묶은 여러 작업 중을 하나가 실패하게 되면 모두 Rollback 하는 등 Transaction은 엄청 유용하게 쓰인다
그러면 @Async 어노테이션이 걸려있는 비동기 메서드를 Transaction으로 묶으면 좋지 않을까??
그래서 흔히 “@Transactional 메서드 안에서 @Async 호출하면 트랜잭션 유지되겠지?”를 생각해 다음과 같이 코드를 작성한다
@Transactional
public void order() {
saveOrder();
asyncService.sendPush();
}
하지만 이렇게 하면 👉 100% 끊긴다
왜?
- @Async는 다른 스레드
- 트랜잭션은 ThreadLocal → 새 스레드에는 트랜잭션 없음
그러면 Transaction으로 묶으면서 Async를 적용할 방법이 없을까..?? 크게 2가지 방법이 있는데 정석 방식은 후자 다!
방법 1️⃣ 비동기 메서드에 트랜잭션
@Async
@Transactional
public void sendPush() { ... }
방법 2️⃣ 이벤트 기반 (정석 ⭐)
@Transactional
public void order() {
saveOrder();
eventPublisher.publish(new OrderEvent(id));
}
@Async
@TransactionalEventListener(phase = AFTER_COMMIT)
public void handle(OrderEvent e) {
...
}
느낌을 알 것 같다 publish가 event를 발생시키면 -> handler가 그 이벤트를 감지해 동작을 한다 근데... 어떻게 Spring이 이걸 수행하는지 궁금해졌다
EventPublisher와 EventHandler를 예시로 통해 알아보자
먼저 publish 코드를 보면 새로운 객체인 OrderEvent를 주입하고 있다 이건 뭘까??
이건 일반적인 POJO 클래스다!!
Spring 이벤트는
“객체 하나 = 이벤트”
이기 때문에 객체를 하나 넣어 줘야 한다
이 클래스의 예시는 다음과 같다
public class OrderEvent {
private final Long orderId;
public OrderEvent(Long orderId) {
this.orderId = orderId;
}
public Long getOrderId() {
return orderId;
}
}
이제 어떻게 동작하는지 진짜 흐름을 통해서 보자!!
① 이벤트 발행
eventPublisher.publishEvent(new OrderEvent(id));
- Spring Context에
- **“OrderEvent 타입 이벤트 발생”**이라고 알림
② 스프링 내부에서 하는 일 (개념적)
현재 컨텍스트에 등록된 모든 Bean 조회
↓
@EventListener / @TransactionalEventListener 붙은 메서드들 스캔
↓
메서드 파라미터 타입 확인
↓
OrderEvent를 받을 수 있으면 실행
@TransactionalEventListener(phase = AFTER_COMMIT)
publishEvent()
↓
(트랜잭션 큐에 쌓임)
↓
commit 성공
↓
리스너 실행
정리를 해보면!!
publish 된 이벤트 객체의 타입을 기준으로, 그 타입(또는 상위 타입)을 파라미터로 받는 리스너 메서드를 전부 실행한다!
'🖥️ 컴퓨터 공부 > 주니어 백엔드 개발자가 반드시 알아야 할 실무 지식' 카테고리의 다른 글
| ThreadPool(2) (1) | 2026.01.04 |
|---|