init: 클린 기반 auth 서버 설계
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
# Async / Scheduler / Retry 기준
|
||||
|
||||
## 1. 목적
|
||||
|
||||
이 문서는 Spring의 비동기 실행(@Async), 스케줄 실행(@Scheduled), 재시도(@Retryable / RetryTemplate)를 프로젝트에서 언제, 어디에, 어떤 방식으로 사용할지 정의한다.
|
||||
|
||||
이 문서의 목적은 다음과 같다.
|
||||
|
||||
- 실행 경계를 명확히 한다.
|
||||
- 프록시 기반 동작의 함정을 피한다.
|
||||
- 스레드 풀/스케줄러를 암묵적 기본값에 맡기지 않는다.
|
||||
- 재시도를 “일시적 실패”에만 제한한다.
|
||||
- 핵심 비즈니스 로직이 비동기/스케줄/재시도 애노테이션 뒤에 숨지 않게 한다.
|
||||
|
||||
## 2. 근거 수준
|
||||
|
||||
- Official: Spring Framework / Spring Boot / Spring Retry 공식 문서에서 직접 확인되는 내용
|
||||
- Official + Practice: 공식 제약 위에 일반적인 실무 운영 원칙을 결합한 내용
|
||||
- Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙
|
||||
|
||||
## 3. 기본 원칙
|
||||
|
||||
### 3.1 동기 실행을 기본값으로 둔다
|
||||
|
||||
비동기, 스케줄, 재시도는 기본 선택지가 아니라 명시적 필요가 있을 때만 도입한다.
|
||||
|
||||
- @Async는 호출자가 즉시 반환되어도 되는 후속 작업에만 사용한다.
|
||||
- @Scheduled는 요청-응답 흐름이 아닌 주기성/지연 실행 작업에만 사용한다.
|
||||
- retry는 외부 시스템 호출 등 일시적 실패 가능성이 있는 작업에만 사용한다.
|
||||
- 검증 오류, 도메인 규칙 위반, 매핑 오류, 프로그래밍 오류에 retry를 걸지 않는다.
|
||||
|
||||
Spring Retry 공식 문서도 “항상 같은 예외가 발생하는 결정적 실패는 재시도해도 도움이 되지 않으며, 모든 예외 타입에 대해 재시도하지 말라”고 설명한다.
|
||||
|
||||
### 3.2 애노테이션은 도메인 규칙이 아니라 실행 메커니즘이다
|
||||
|
||||
@Async, @Scheduled, @Retryable은 모두 실행 방식에 대한 인프라 성격의 도구다. 따라서 이 프로젝트에서는 다음을 기본으로 한다.
|
||||
|
||||
- domain 레이어에는 사용하지 않는다.
|
||||
- 주 사용 위치는 application / infrastructure 레이어로 제한한다.
|
||||
- controller, entity, value object, mapper에 실행 메커니즘 애노테이션을 붙여 책임을 섞지 않는다.
|
||||
|
||||
이 규칙은 프로젝트 아키텍처 정렬을 위한 Project Recommendation이다.
|
||||
|
||||
## 4. @Async 표준
|
||||
|
||||
### 4.1 사용 기준
|
||||
|
||||
@Async는 호출 즉시 반환해도 되는 작업에만 사용한다.
|
||||
|
||||
허용 예:
|
||||
|
||||
- 알림 전송
|
||||
- 감사 로그 전송
|
||||
- 비핵심 후속 연산
|
||||
- 핵심 트랜잭션 완료 뒤의 독립 작업
|
||||
|
||||
비허용 예:
|
||||
|
||||
- 핵심 비즈니스 결과를 결정하는 로직
|
||||
- 호출자가 반드시 성공/실패를 알아야 하는 로직
|
||||
- 트랜잭션 경계를 우회하려는 용도
|
||||
- 초기화 콜백(@PostConstruct)에 직접 붙이는 방식
|
||||
|
||||
Spring 공식 문서상 @Async는 호출 시 작업을 TaskExecutor에 제출해 비동기로 실행하며, @PostConstruct 같은 lifecycle callback과 함께 사용할 수 없다.
|
||||
|
||||
### 4.2 프록시 경계 규칙
|
||||
|
||||
@Async는 기본적으로 proxy mode로 처리되므로 같은 클래스 내부 호출(self-invocation) 에는 적용되지 않는다. 따라서:
|
||||
|
||||
- this.someAsyncMethod() 형태를 금지한다.
|
||||
- @Async가 필요한 로직은 별도 bean 으로 분리한다.
|
||||
- 프록시 경계가 드러나게 설계한다.
|
||||
|
||||
Spring 공식 문서도 @Async의 기본 advice mode는 proxy이며 같은 클래스 내부 로컬 호출은 가로채지 못한다고 명시한다.
|
||||
|
||||
### 4.3 메서드 시그니처 규칙
|
||||
|
||||
Spring 공식 기준에서 @Async 메서드는 파라미터는 자유롭지만 반환형은 void 또는 Future 계열이어야 하며, CompletableFuture 사용이 가능하다. 또한 @Configuration 클래스 내부 메서드에는 지원되지 않는다.
|
||||
|
||||
프로젝트 규칙은 다음과 같다.
|
||||
|
||||
- 호출자가 결과를 관찰해야 하면 CompletableFuture<T>를 사용한다.
|
||||
- 단순 fire-and-forget이면 void를 사용할 수 있다.
|
||||
- 새 코드에서 구식 Future는 특별한 호환 요구가 없으면 사용하지 않는다.
|
||||
- @Configuration 내부 메서드에 @Async를 사용하지 않는다.
|
||||
|
||||
### 4.4 예외 처리 규칙
|
||||
|
||||
void 반환 @Async 메서드의 예외는 호출자에게 전달되지 않고, 기본적으로는 로깅만 된다. 호출자가 실패를 알아야 하는 경우 CompletableFuture를 사용하고, void를 쓰는 경우에는 AsyncUncaughtExceptionHandler 또는 메서드 내부 명시적 예외 처리 전략을 둔다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- void @Async는 실패를 호출자에게 전달할 필요가 없는 작업에만 사용한다.
|
||||
- void @Async를 도입하면 예외 처리/로그/모니터링 전략을 같이 정의한다.
|
||||
- 실패가 비즈니스적으로 중요하면 @Async로 숨기지 않는다.
|
||||
|
||||
## 5. Executor / Scheduler 구성 표준
|
||||
|
||||
### 5.1 기본 자동 구성을 무심코 공유하지 않는다
|
||||
|
||||
Spring Boot는 AsyncTaskExecutor를 자동 구성하고, 그 실행기는 @EnableAsync뿐 아니라 MVC 비동기 요청 처리, WebFlux blocking 지원, GraphQL, JPA bootstrap, background initialization 등 여러 통합 지점에서 사용될 수 있다. 또한 스케줄러도 자동 구성되며, 가상 스레드를 쓰지 않을 때 기본 ThreadPoolTaskScheduler는 기본 스레드 수 1로 동작한다.
|
||||
|
||||
따라서 프로젝트 규칙은 다음과 같다.
|
||||
|
||||
- @Async용 executor와 @Scheduled용 scheduler를 개념적으로 분리한다.
|
||||
- 무거운 업무성 비동기 작업을 web 요청 처리와 우연히 같은 executor에 태우지 않는다.
|
||||
- 스레드 이름 prefix를 명시한다.
|
||||
- pool size / queue capacity / rejection 정책을 의도적으로 설정한다.
|
||||
- 운영에서 식별 가능한 bean 이름을 사용한다.
|
||||
|
||||
### 5.2 가상 스레드 사용 시 주의
|
||||
|
||||
Spring Framework는 SimpleAsyncTaskScheduler가 가상 스레드 정렬 옵션으로 동작할 수 있지만, fixed-delay 작업은 단일 scheduler thread에서 동작하므로 이 경우 fixed-rate나 cron을 권장한다고 설명한다. Spring Boot도 가상 스레드 활성화 시 scheduler가 SimpleAsyncTaskScheduler가 되며 pooling 관련 설정을 무시한다고 설명한다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- 가상 스레드를 켠다고 해서 scheduler 설계를 생략하지 않는다.
|
||||
- fixed-delay 중심 작업이 많다면 가상 스레드 scheduler를 무비판적으로 선택하지 않는다.
|
||||
|
||||
## 6. @Scheduled 표준
|
||||
|
||||
### 6.1 사용 기준
|
||||
|
||||
@Scheduled는 주기 작업/지연 작업의 진입점으로만 사용한다.
|
||||
|
||||
Spring 공식 기준에서:
|
||||
|
||||
- 주기 작업에는 cron, fixedDelay, fixedRate 중 정확히 하나를 지정해야 한다.
|
||||
- initialDelay는 선택 사항이다.
|
||||
- 메서드는 인자를 받을 수 없다.
|
||||
- 반환값은 일반적으로 무시된다.
|
||||
- 같은 메서드에 여러 스케줄 선언을 둘 수 있으며, 이 경우 서로 독립적으로 실행되어 겹칠 수 있다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- 이 프로젝트의 @Scheduled 메서드는 반드시 void 로 작성한다.
|
||||
- @Scheduled 메서드는 얇은 트리거(thin trigger) 로 유지하고 실제 업무는 application service/use case로 위임한다.
|
||||
- 하나의 메서드에 여러 @Scheduled를 붙이지 않는다.
|
||||
- 각 스케줄 작업은 재실행 가능(idempotent) 하고 중복 실행/겹침에 안전해야 한다.
|
||||
|
||||
### 6.2 fixedDelay와 fixedRate 선택 기준
|
||||
|
||||
Spring 공식 문서 기준:
|
||||
|
||||
- fixedDelay는 이전 실행 완료 시점 기준
|
||||
- fixedRate는 이전 실행 시작 시점 기준 으로 간격이 계산된다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- 이전 실행이 끝난 뒤 다음 실행을 시작해야 하면 fixedDelay
|
||||
- 일정 간격 기준으로 계속 트리거되어도 괜찮으면 fixedRate
|
||||
- 업무 시간이 분명한 배치성 작업은 cron
|
||||
- timezone 의미가 중요한 cron은 zone을 명시하는 방향을 우선 검토한다
|
||||
|
||||
### 6.3 스케줄러 풀 크기 규칙
|
||||
|
||||
Spring Boot의 기본 scheduler는 단일 스레드일 수 있다. 따라서 둘 이상의 작업이 있거나, 하나라도 오래 걸리는 작업이 있으면 pool size를 명시적으로 설계한다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- scheduler bean 또는 spring.task.scheduling.* 설정을 명시한다.
|
||||
- “기본값 1개 스레드”에 의존한 채 운영에 올리지 않는다.
|
||||
- 긴 작업과 짧은 작업이 섞이면 분리 가능성도 검토한다.
|
||||
|
||||
## 7. Retry 표준
|
||||
|
||||
### 7.1 retry를 사용할 수 있는 위치
|
||||
|
||||
Retry는 다음 같은 경우에만 사용한다.
|
||||
|
||||
- 외부 HTTP/gRPC/API 호출
|
||||
- 메시지 브로커 일시 실패
|
||||
- 네트워크 일시 장애
|
||||
- 잠깐 후 재시도하면 회복될 수 있는 외부 의존성 오류
|
||||
|
||||
Spring Retry는 @EnableRetry로 @Retryable bean에 프록시를 만들며, RetryTemplate 기반의 프로그래밍 방식도 제공한다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- retry는 외부 경계(adapter/client/gateway) 에 가깝게 둔다.
|
||||
- controller / domain / entity / mapper / validation 로직에는 두지 않는다.
|
||||
- “왜 재시도 가능한가?”를 설명할 수 없는 경우 retry를 두지 않는다.
|
||||
|
||||
### 7.2 기본값을 그대로 쓰지 않는다
|
||||
|
||||
Spring Retry의 현재 API 기준:
|
||||
|
||||
- maxAttempts 기본값은 3
|
||||
- retryFor와 noRetryFor를 비워두면 기본적으로 모든 예외가 재시도 대상이 될 수 있다
|
||||
- backoff는 지정 가능하며 기본은 단순 Backoff 사양이다
|
||||
- noRetryFor, notRecoverable 같은 세밀한 제어가 가능하다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- @Retryable에는 반드시 retryFor를 명시한다.
|
||||
- 필요하면 noRetryFor 또는 notRecoverable도 함께 명시한다.
|
||||
- maxAttempts를 명시한다.
|
||||
- backoff 전략을 명시한다.
|
||||
- “기본적으로 모든 예외 재시도” 형태를 금지한다.
|
||||
|
||||
### 7.3 @Recover 사용 기준
|
||||
|
||||
Spring Retry 공식 문서상 recovery method는:
|
||||
|
||||
- @Retryable 메서드와 같은 클래스 에 있어야 하고
|
||||
- @Recover 로 표시해야 하며
|
||||
- 반환형이 @Retryable 메서드와 맞아야 한다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- 재시도 소진 뒤 대체 경로가 의미 있을 때만 @Recover를 둔다.
|
||||
- recover는 “실패를 조용히 삼키는 메서드”가 아니라, 대체 동작 또는 명시적 실패 변환 역할이어야 한다.
|
||||
- recover가 있어도 관측 가능성(log/metric/alert)을 잃지 않는다.
|
||||
|
||||
### 7.4 @Retryable vs RetryTemplate
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
애노테이션 기반이 더 읽기 쉬운 경우: @Retryable
|
||||
|
||||
다음 경우에는 RetryTemplate을 우선 검토:
|
||||
|
||||
- 한 메서드 전체가 아니라 일부 코드 블록만 재시도해야 하는 경우
|
||||
- 루프 내부 각 항목마다 다른 retry 문맥이 필요한 경우
|
||||
- 정책을 동적으로 조합해야 하는 경우
|
||||
- 테스트에서 retry 경계를 더 명시적으로 다루고 싶은 경우
|
||||
|
||||
이 구분은 @Retryable이 bean method 경계의 선언적 방식이고, RetryTemplate은 임의 코드 블록의 프로그래밍 방식이라는 공식 구조에 맞춘 Official + Practice 규칙이다.
|
||||
|
||||
## 8. 조합 규칙
|
||||
|
||||
### 8.1 한 메서드에 다 몰아넣지 않는다
|
||||
|
||||
프로젝트 기본 규칙:
|
||||
|
||||
- @Scheduled + @Async + @Retryable를 같은 메서드에 겹쳐 붙이는 것을 기본 금지한다.
|
||||
- 트리거, 업무 오케스트레이션, 외부 재시도, 후속 비동기 작업은 서로 다른 bean / 메서드 경계 로 나눈다.
|
||||
|
||||
이 규칙의 이유는 @Async와 @Retryable 모두 프록시 기반이며, 경계가 흐려질수록 self-invocation·예외 전파·관측 가능성 문제가 커지기 때문이다.
|
||||
|
||||
권장 구조:
|
||||
|
||||
- @Scheduled → application use case
|
||||
- application use case → retry가 붙은 external gateway/client
|
||||
- 비핵심 후속 작업 → 별도 async bean 또는 event listener
|
||||
|
||||
### 8.2 트랜잭션과의 결합
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- 핵심 트랜잭션 오케스트레이션은 application service에 둔다.
|
||||
- retry 대상 외부 호출과 async 후속 처리까지 같은 메서드에 한꺼번에 섞지 않는다.
|
||||
- 프록시 경계가 필요한 경우 메서드 분리가 아니라 bean 분리를 우선한다.
|
||||
|
||||
이 항목은 기존 transaction 문서와 맞물리는 Project Recommendation이다.
|
||||
|
||||
## 9. 체크리스트
|
||||
|
||||
다음 질문에 “예”로 답할 수 있어야 도입 가능하다.
|
||||
|
||||
@Async:
|
||||
|
||||
- 호출자가 즉시 반환되어도 되는가?
|
||||
- 실패가 호출자에게 반드시 전달될 필요가 없는가, 또는 CompletableFuture로 전달되는가?
|
||||
- 같은 클래스 내부 호출이 아닌가?
|
||||
- 전용 executor와 예외 처리 전략이 있는가?
|
||||
|
||||
@Scheduled:
|
||||
|
||||
- 요청 흐름이 아닌 주기 작업인가?
|
||||
- 메서드가 얇은 트리거인가?
|
||||
- 중복 실행/겹침에 안전한가?
|
||||
- scheduler pool 설정이 의도적으로 잡혀 있는가?
|
||||
|
||||
Retry:
|
||||
|
||||
- 실패가 일시적이라는 근거가 있는가?
|
||||
- retryFor, maxAttempts, backoff가 명시되어 있는가?
|
||||
- validation/domain/programming 오류는 제외되어 있는가?
|
||||
- recover 또는 최종 실패 경로가 분명한가?
|
||||
Reference in New Issue
Block a user