init: 클린 기반 auth 서버 설계
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
# AOP 사용 기준
|
||||
|
||||
## 목적
|
||||
|
||||
AOP는 비즈니스 로직을 숨기는 우회 수단이 아니라,
|
||||
여러 타입과 객체를 가로지르는 **cross-cutting concern** 을 모듈화할 때만 사용한다.
|
||||
|
||||
## 공식 의미
|
||||
|
||||
- Spring AOP는 OOP를 보완하는 방식이다.
|
||||
- AOP의 핵심 단위는 class가 아니라 aspect다.
|
||||
- Spring AOP는 proxy-based다.
|
||||
- proxy-based AOP에서는 proxy를 통과하는 외부 호출만 advice가 적용된다.
|
||||
- self-invocation은 기본적으로 advice가 적용되지 않는다.
|
||||
- Spring은 AOP의 대표적 용도로 declarative transaction 같은 cross-cutting concern을 든다.
|
||||
- pointcut은 더 작은 named pointcut으로 조합하는 것이 권장된다.
|
||||
- 대부분의 경우 static pointcut이 충분하고 더 낫다.
|
||||
|
||||
## 기본 규칙
|
||||
|
||||
### 1. AOP는 cross-cutting concern에만 사용
|
||||
다음은 AOP 후보가 된다.
|
||||
|
||||
- 공통 로깅
|
||||
- 메트릭/트레이싱
|
||||
- 권한 체크의 반복 경계
|
||||
- 재시도/타이밍 측정
|
||||
- 공통 감사(audit)
|
||||
- 선언적 트랜잭션
|
||||
|
||||
다음은 AOP로 풀지 않는다.
|
||||
|
||||
- 핵심 비즈니스 흐름
|
||||
- 상태 전이 정책
|
||||
- 도메인 규칙
|
||||
- 복잡한 분기 로직
|
||||
- 외부 API orchestration
|
||||
|
||||
### 2. 기본값은 “명시적 코드”, 예외적으로 AOP
|
||||
같은 기능을 명시적 코드로 더 잘 읽을 수 있으면 AOP를 쓰지 않는다.
|
||||
|
||||
기본:
|
||||
- use case/service 안에서 명시적으로 보이는 흐름 우선
|
||||
- 반복되는 횡단 관심사만 AOP로 추출
|
||||
|
||||
### 3. proxy-based 한계를 항상 전제로 한다
|
||||
Spring AOP는 proxy 기반이므로 다음을 전제로 설계한다.
|
||||
|
||||
- proxy를 통과하는 외부 호출만 interception
|
||||
- self-invocation은 적용되지 않음
|
||||
- “같은 클래스 안에서 호출되면 aspect가 붙겠지”를 금지
|
||||
|
||||
### 4. self-invocation 해결을 위해 AOP 남용 금지
|
||||
self-invocation 문제를 해결하려고 다음을 기본 금지한다.
|
||||
|
||||
- 자기 자신 proxy 주입
|
||||
- `AopContext.currentProxy()` 의존
|
||||
- 구현을 proxy semantics에 강하게 묶는 설계
|
||||
|
||||
기본 대응:
|
||||
- 경계를 다시 분리
|
||||
- 클래스를 분리
|
||||
- 더 명시적인 구조로 변경
|
||||
|
||||
### 5. AOP는 경계가 뚜렷한 곳에만 적용
|
||||
좋은 적용 지점:
|
||||
- service/use-case public method
|
||||
- controller 경계
|
||||
- repository 경계
|
||||
- 명시된 package/bean naming convention
|
||||
|
||||
지양:
|
||||
- 너무 넓은 전체 패키지
|
||||
- “일단 다 잡고 보자” 식 표현식
|
||||
- private/internal 세부 구현까지 얽는 pointcut
|
||||
|
||||
### 6. pointcut은 작고 이름 있게 조합
|
||||
공식 권장대로 pointcut은 작은 named pointcut을 조합해 만든다.
|
||||
|
||||
기본:
|
||||
- package 범위 pointcut
|
||||
- role 기반 pointcut
|
||||
- public method pointcut
|
||||
- bean naming 기반 pointcut
|
||||
|
||||
를 분리하고 조합한다.
|
||||
|
||||
### 7. 대부분 static pointcut 우선
|
||||
동적 조건보다 static pointcut이 충분하면 static 쪽을 우선한다.
|
||||
성능/이해도/예측 가능성이 더 좋다.
|
||||
|
||||
### 8. `@Around`는 최소화
|
||||
`@Around`는 가장 강력하지만 가장 위험하다.
|
||||
반환값/예외/호출 자체를 제어할 수 있으므로 꼭 필요할 때만 쓴다.
|
||||
|
||||
기본 우선순위:
|
||||
- 단순 전처리 -> `@Before`
|
||||
- 정상 반환 후 후처리 -> `@AfterReturning`
|
||||
- 예외 기록/번역 -> `@AfterThrowing`
|
||||
- 무조건 정리 -> `@After`
|
||||
- 호출 제어/타이밍/재시도 등 정말 필요할 때만 `@Around`
|
||||
|
||||
### 9. advice 안에서 비즈니스 의미를 새로 만들지 않는다
|
||||
advice는 보조 concern을 수행해야 한다.
|
||||
|
||||
금지:
|
||||
- 상태 전이 결정
|
||||
- 비즈니스 실패를 성공처럼 바꾸기
|
||||
- 핵심 정책 우회
|
||||
- controller/service가 해야 할 결정을 aspect에서 대신하기
|
||||
|
||||
### 10. 예외를 숨기지 않는다
|
||||
AOP에서 예외를 잡더라도 기본은:
|
||||
- 기록
|
||||
- 문맥 추가
|
||||
- 그대로 전파
|
||||
중 하나다.
|
||||
|
||||
금지:
|
||||
- 예외 삼키기
|
||||
- 정상값으로 은폐
|
||||
- 실패를 조용히 무시
|
||||
|
||||
### 11. 트랜잭션 대체 수단으로 일반 AOP를 남용하지 않는다
|
||||
선언적 트랜잭션은 Spring이 제공하는 표준 메커니즘을 우선 사용한다.
|
||||
일반 custom aspect로 transaction semantics를 흉내 내지 않는다.
|
||||
|
||||
### 12. AOP는 observability / policy enforcement에 더 적합
|
||||
프로젝트에서 AOP는 아래 유형에 더 적합하다.
|
||||
|
||||
- 실행 시간 측정
|
||||
- 공통 로깅
|
||||
- 감사 기록
|
||||
- annotation 기반 정책 강제
|
||||
- 공통 예외 기록
|
||||
|
||||
복잡한 use case orchestration에는 부적합하다.
|
||||
|
||||
### 13. pointcut 범위는 문서화 가능해야 한다
|
||||
pointcut을 보고 아래를 설명할 수 있어야 한다.
|
||||
|
||||
- 어디에 적용되는가
|
||||
- 왜 거기에만 적용되는가
|
||||
- 새 코드가 추가되면 어떤 naming/package 규칙으로 포함되는가
|
||||
|
||||
설명하기 어려우면 범위가 너무 넓거나 모호한 것이다.
|
||||
|
||||
### 14. bean naming / package convention을 설계와 함께 쓴다
|
||||
Spring 공식 문서가 bean PCD나 package 기반 pointcut 예시를 드는 것처럼,
|
||||
AOP를 쓸 거면 package 구조나 bean naming convention이 일정해야 한다.
|
||||
|
||||
즉:
|
||||
- `*Service`
|
||||
- `..application..`
|
||||
- `..infrastructure..`
|
||||
같은 규칙은 pointcut과 함께 관리한다.
|
||||
|
||||
## 프로젝트 기준 요약
|
||||
|
||||
- AOP는 cross-cutting concern에만 사용
|
||||
- 기본값은 명시적 코드
|
||||
- Spring AOP는 proxy-based라는 점을 전제로 설계
|
||||
- self-invocation 기대 금지
|
||||
- pointcut은 작고 이름 있게 조합
|
||||
- 대부분 static pointcut 우선
|
||||
- `@Around` 최소화
|
||||
- advice에서 비즈니스 의미를 만들지 않음
|
||||
- 예외를 숨기지 않음
|
||||
@@ -0,0 +1,175 @@
|
||||
# ApplicationEvent 사용 기준
|
||||
|
||||
## 목적
|
||||
|
||||
Application Event는 같은 애플리케이션 내부에서 **느슨하게 결합된 후속 반응**을 분리하기 위해 사용한다.
|
||||
핵심 비즈니스 오케스트레이션을 숨기는 수단으로 사용하지 않는다.
|
||||
|
||||
## 공식 의미
|
||||
|
||||
- `ApplicationEventPublisher`는 이벤트 발행 기능을 제공한다.
|
||||
- `publishEvent(Object)`는 일반 객체도 이벤트로 발행할 수 있으며, 필요 시 `PayloadApplicationEvent`로 감싸진다.
|
||||
- 이벤트 발행은 multicaster로의 hand-off일 뿐, 그 자체로 synchronous/asynchronous 또는 immediate execution을 보장하지 않는다.
|
||||
- 리스너는 가능한 한 효율적이어야 하며, 오래 걸리거나 blocking 가능한 작업은 개별적으로 비동기 실행을 고려한다.
|
||||
- 트랜잭션 결과와 묶어 처리해야 하면 `@TransactionalEventListener`를 사용한다.
|
||||
- `@TransactionalEventListener`의 기본 phase는 `AFTER_COMMIT`이다.
|
||||
- 트랜잭션 밖에서 발행된 이벤트는 기본적으로 discard되며, `fallbackExecution = true`일 때만 예외적으로 처리된다.
|
||||
|
||||
## 기본 규칙
|
||||
|
||||
### 1. Application Event는 “후속 반응”에만 사용
|
||||
다음은 이벤트 후보가 된다.
|
||||
|
||||
- 감사 로그 기록
|
||||
- 메트릭/알림 발행
|
||||
- 후속 캐시 정리
|
||||
- 읽기 모델 갱신
|
||||
- 부가적인 notification
|
||||
- core use case 이후의 느슨한 반응
|
||||
|
||||
다음은 이벤트로 풀지 않는다.
|
||||
|
||||
- 핵심 비즈니스 흐름 자체
|
||||
- 반드시 순서대로 수행되어야 하는 오케스트레이션
|
||||
- 즉시 실패/성공 여부가 핵심인 주 경로
|
||||
- 도메인 규칙 판정
|
||||
- controller/service가 직접 보여줘야 하는 결과 계산
|
||||
|
||||
### 2. 이벤트는 같은 애플리케이션 내부 경계로 본다
|
||||
기본적으로 Spring Application Event는 in-process 이벤트다.
|
||||
|
||||
즉:
|
||||
- 다른 시스템과의 통합 이벤트 브로커 대체제가 아니다
|
||||
- Kafka/RabbitMQ 같은 외부 메시징과 같은 의미로 쓰지 않는다
|
||||
- 프로세스 내부의 느슨한 반응 분리에 한정한다
|
||||
|
||||
### 3. 발행은 “hand-off”일 뿐, 실행 모델을 가정하지 않는다
|
||||
`publishEvent(...)`를 호출했다고 해서 아래를 가정하지 않는다.
|
||||
|
||||
- 반드시 동기적으로 끝난다
|
||||
- 반드시 즉시 실행된다
|
||||
- 반드시 같은 스레드에서 다 처리된다
|
||||
|
||||
즉 발행자(publisher)는 listener의 실행 방식에 의존하지 않는다.
|
||||
|
||||
### 4. listener는 짧고 효율적으로 유지
|
||||
공식 문서 취지대로 listener는 가능한 한 짧고 효율적으로 유지한다.
|
||||
|
||||
기본 금지:
|
||||
- 긴 블로킹 작업
|
||||
- 대규모 외부 API 호출
|
||||
- 무거운 batch 처리
|
||||
- 여러 단계 오케스트레이션
|
||||
|
||||
정말 오래 걸리면 별도 비동기/후속 처리 구조를 검토한다.
|
||||
|
||||
### 5. 트랜잭션 결과가 중요하면 `@TransactionalEventListener`
|
||||
다음은 `@TransactionalEventListener`를 우선 검토한다.
|
||||
|
||||
- DB commit 성공 후에만 실행되어야 하는 후속 처리
|
||||
- rollback되면 수행하면 안 되는 반응
|
||||
- 저장 완료 이후에만 의미가 있는 알림/감사/후속 처리
|
||||
|
||||
기본 phase:
|
||||
- 특별한 이유가 없으면 `AFTER_COMMIT`
|
||||
|
||||
### 6. `fallbackExecution = true`는 예외적으로만
|
||||
트랜잭션이 없을 때도 listener를 실행해야 하는 경우가 정말 명확할 때만 사용한다.
|
||||
|
||||
기본값:
|
||||
- 트랜잭션 경계가 없는 발행은 discard되어도 괜찮다고 본다
|
||||
|
||||
### 7. 이벤트 payload는 처리에 필요한 상태를 포함
|
||||
공식 문서상 reactive/async hand-off에서는 thread-local 상태를 기대하면 안 된다.
|
||||
따라서 이벤트 객체에는 listener가 처리하는 데 필요한 최소 상태를 자체적으로 담는다.
|
||||
|
||||
금지:
|
||||
- listener가 `SecurityContext`, MDC, thread-local만 믿고 동작
|
||||
- payload 없이 “가서 다시 다 조회해라” 식으로 과도하게 빈약한 이벤트
|
||||
|
||||
기본:
|
||||
- 식별자
|
||||
- 필요한 시점 정보
|
||||
- 필요한 타입/상태
|
||||
를 명시적으로 포함
|
||||
|
||||
### 8. payload는 작고 안정적으로
|
||||
이벤트는 무거운 객체 그래프 전체보다, listener가 필요한 최소 데이터만 담는다.
|
||||
|
||||
기본:
|
||||
- entity 전체보다 id/필수 상태 우선
|
||||
- JPA lazy proxy를 payload로 넘기지 않음
|
||||
- 직렬화/로그에 취약한 대형 객체를 그대로 넘기지 않음
|
||||
|
||||
### 9. 이벤트 이름은 business fact 또는 completed action으로 짓는다
|
||||
좋은 방향:
|
||||
- `UserRegisteredEvent`
|
||||
- `LoginSucceededEvent`
|
||||
- `PublicKeyRotatedEvent`
|
||||
|
||||
지양:
|
||||
- `DoSomethingEvent`
|
||||
- `CommonEvent`
|
||||
- `UserProcessEvent`
|
||||
|
||||
이름만 보고 무슨 일이 일어났는지 보여야 한다.
|
||||
|
||||
### 10. 발행자는 listener 존재를 몰라야 한다
|
||||
publisher는 listener가 몇 개인지, 누가 듣는지, 어떤 순서인지에 기대지 않는다.
|
||||
|
||||
금지:
|
||||
- “이 이벤트를 쏘면 저 listener가 반드시 먼저 실행된다”는 설계
|
||||
- 이벤트 발행으로 핵심 결과를 암묵적으로 완성하는 구조
|
||||
|
||||
### 11. listener 순서 의존 최소화
|
||||
`@Order`를 줄 수는 있지만, 가능하면 listener 간 순서 의존을 설계하지 않는다.
|
||||
|
||||
정말 필요할 때만:
|
||||
- 같은 phase 안에서 우선순위 조정
|
||||
- 매우 명확한 부가 처리 순서
|
||||
|
||||
기본은 서로 독립적으로 동작해야 한다.
|
||||
|
||||
### 12. listener 안에서 핵심 business decision 금지
|
||||
listener는 후속 반응을 수행해야 한다.
|
||||
|
||||
금지:
|
||||
- 핵심 상태 전이 결정
|
||||
- 메인 use case 성공/실패를 뒤집는 판단
|
||||
- 여러 하위 흐름을 연결한 복잡한 오케스트레이션
|
||||
|
||||
### 13. listener 예외는 의도를 분명히
|
||||
listener에서 예외가 나면 어떤 영향을 기대하는지 명확해야 한다.
|
||||
|
||||
기본:
|
||||
- 주 흐름과 강결합이면 이벤트보다 명시적 호출이 더 적합
|
||||
- 후속 반응이면 실패 처리/재시도/로그 정책을 분리해서 설계
|
||||
- 예외를 조용히 삼키지 않는다
|
||||
|
||||
### 14. 이벤트는 남발하지 않는다
|
||||
“느슨하게 연결하고 싶다”는 이유만으로 이벤트를 남발하지 않는다.
|
||||
|
||||
다음 질문 중 여러 개가 “예”일 때만 검토한다.
|
||||
- 발행자와 반응자를 분리할 가치가 큰가?
|
||||
- 반응자가 하나가 아닐 수 있는가?
|
||||
- 후속 반응이 핵심 흐름이 아닌가?
|
||||
- 트랜잭션 완료 후 처리로 분리하는 이점이 큰가?
|
||||
|
||||
### 15. 테스트에서 이벤트를 검증할 수 있어야 한다
|
||||
Spring 테스트는 `ApplicationEvents`를 기록하고 검증할 수 있다.
|
||||
|
||||
기본:
|
||||
- 이벤트를 발행하는 use case는 발행 여부를 테스트 가능하게 설계
|
||||
- listener 동작도 별도 테스트 가능하게 유지
|
||||
- “이벤트가 어딘가에서 되겠지”를 금지
|
||||
|
||||
## 프로젝트 기준 요약
|
||||
|
||||
- Application Event는 내부 후속 반응 분리 수단
|
||||
- 핵심 오케스트레이션에는 기본 금지
|
||||
- 발행은 hand-off일 뿐 실행 모델을 가정하지 않음
|
||||
- listener는 짧고 효율적으로
|
||||
- commit 결과가 중요하면 `@TransactionalEventListener`
|
||||
- payload는 작고 필요한 상태를 명시적으로 포함
|
||||
- publisher는 listener 순서/존재를 몰라야 함
|
||||
- 이벤트 남발 금지
|
||||
@@ -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 또는 최종 실패 경로가 분명한가?
|
||||
@@ -0,0 +1,166 @@
|
||||
# bean registration 기준
|
||||
|
||||
## 목적
|
||||
|
||||
Spring bean 등록은 “컨테이너가 생명주기와 의존성을 관리해야 하는 객체”에만 사용한다.
|
||||
아무 객체나 bean으로 올리지 않고, stereotype scanning과 `@Configuration` + `@Bean`을 역할에 따라 구분한다.
|
||||
|
||||
## 공식 의미
|
||||
|
||||
- Spring IoC container는 configuration metadata를 읽어 bean definition을 만들고 객체를 관리한다.
|
||||
- 설정 메타데이터는 주로 annotation-based component class, `@Configuration` + `@Bean`, 또는 외부 설정으로 표현할 수 있다.
|
||||
- `@Component`와 그 특수화(`@Repository`, `@Service`, `@Controller`)는 classpath scanning 대상이다.
|
||||
- `@Bean`은 객체를 생성·설정·초기화하는 factory method를 bean definition으로 등록한다.
|
||||
- `@Bean`은 `@Configuration` 클래스에서 사용하는 것이 기본 권장 방식이다.
|
||||
- bean overriding은 일반적으로 권장되지 않으며 설정 가독성을 해친다.
|
||||
- bean/runtime registration은 초기 단계가 아니라 live access 중에는 공식적으로 지원되지 않는다.
|
||||
- 일반적으로 fine-grained domain object는 Spring container에 등록하지 않는다.
|
||||
|
||||
## 기본 규칙
|
||||
|
||||
### 1. bean은 “컨테이너 관리 가치”가 있는 객체만 등록
|
||||
다음은 bean 등록 후보이다.
|
||||
|
||||
- application service / use case entry object
|
||||
- repository adapter
|
||||
- external API client
|
||||
- configuration / security / filter / interceptor
|
||||
- shared infrastructure object
|
||||
- framework integration object
|
||||
|
||||
다음은 기본적으로 bean 등록하지 않는다.
|
||||
|
||||
- domain entity
|
||||
- value object
|
||||
- request / response DTO
|
||||
- command / result DTO
|
||||
- 단순 임시 helper object
|
||||
- 매 요청/매 호출마다 새로 만들어도 되는 순수 data object
|
||||
|
||||
### 2. 애플리케이션 주 컴포넌트는 stereotype annotation 우선
|
||||
다음은 stereotype을 우선 검토한다.
|
||||
|
||||
- `@Service`
|
||||
- `@Repository`
|
||||
- `@Controller` / `@RestController`
|
||||
- generic component면 `@Component`
|
||||
|
||||
즉 프로젝트 코드 안의 “주된 역할 객체”는 scanning 기반 등록을 기본값으로 한다.
|
||||
|
||||
### 3. `@Bean`은 명시적 조립이 필요할 때 사용
|
||||
다음은 `@Configuration` + `@Bean`을 우선 검토한다.
|
||||
|
||||
- 외부 라이브러리 타입 등록
|
||||
- 생성자가 복잡하거나 factory method가 필요한 경우
|
||||
- 조건부 조립이 필요한 경우
|
||||
- 여러 collaborator를 엮어 명시적으로 wiring해야 하는 경우
|
||||
- infrastructure object / client / encoder / formatter / strategy bean 생성
|
||||
- 같은 config 안에서 inter-bean wiring을 명시적으로 보여주고 싶은 경우
|
||||
|
||||
### 4. `@Bean`은 기본적으로 `@Configuration` 안에서만
|
||||
`@Bean` method는 기본적으로 `@Configuration` 클래스 안에 둔다.
|
||||
|
||||
이유:
|
||||
- full configuration mode가 inter-bean dependency를 더 안전하게 다룬다
|
||||
- lite mode의 subtle bug 가능성을 줄일 수 있다
|
||||
|
||||
기본 금지:
|
||||
- 일반 `@Component` 안에 습관적으로 `@Bean` method 두기
|
||||
|
||||
예외:
|
||||
- 아주 제한된 factory-style component가 필요하고, inter-bean dependency 호출을 하지 않는 경우
|
||||
|
||||
### 5. bean 등록 이유가 이름만 보고 드러나야 한다
|
||||
- scanning bean이면 stereotype이 역할을 드러내야 한다
|
||||
- `@Configuration` 클래스는 조립 목적이 이름에 드러나야 한다
|
||||
|
||||
좋은 방향:
|
||||
- `SecurityConfiguration`
|
||||
- `WebConfiguration`
|
||||
- `VaultClientConfiguration`
|
||||
- `OAuth2SecurityConfiguration`
|
||||
|
||||
지양:
|
||||
- `CommonConfig`
|
||||
- `AppBeans`
|
||||
- `GeneralConfiguration`
|
||||
|
||||
### 6. domain object를 bean으로 등록하지 않는다
|
||||
Spring 공식 문서도 fine-grained domain object는 보통 container가 아니라 repository/business logic이 만들고 로드한다고 설명한다.
|
||||
|
||||
기본 금지:
|
||||
- `User`, `Money`, `UserEmail`, `CreateUserCommand`를 bean으로 등록
|
||||
- domain 생성 책임을 container로 넘기기
|
||||
|
||||
### 7. bean 이름은 기본 규칙을 따르고, 명시적 이름은 정말 필요할 때만
|
||||
Spring은 scanning bean의 이름을 일반적으로 decapitalize된 simple class name으로 만든다.
|
||||
|
||||
기본:
|
||||
- 이름 충돌이 없으면 기본 이름 사용
|
||||
- qualifier/alias/explicit name은 실제 필요가 있을 때만 사용
|
||||
|
||||
무분별한 명시적 이름 지정 지양:
|
||||
- `"mySpecialUserServiceBean"`
|
||||
- `"appMainPrimaryService"`
|
||||
|
||||
### 8. bean overriding 기본 금지
|
||||
같은 이름의 bean을 덮어쓰는 방식으로 조립하지 않는다.
|
||||
|
||||
이유:
|
||||
- 설정 가독성이 나빠진다
|
||||
- 어떤 bean이 실제로 쓰이는지 추적이 어려워진다
|
||||
|
||||
테스트에서만 예외적으로 필요하면 별도 테스트 설정/지원 메커니즘을 사용한다.
|
||||
|
||||
### 9. 런타임 동적 bean 등록 금지
|
||||
애플리케이션 실행 중 live container에 새 bean을 동적으로 등록하는 방식은 기본 금지한다.
|
||||
|
||||
기본:
|
||||
- bean definition은 startup 시점에 확정
|
||||
- 동적 확장이 필요하면 registry/plugin/factory 전략을 따로 설계
|
||||
|
||||
### 10. bean은 역할 단위로 등록하고, 잡동사니 helper를 bean으로 올리지 않는다
|
||||
container가 관리할 필요가 없는 순수 helper는 bean 대신:
|
||||
- static utility
|
||||
- package-private helper
|
||||
- mapper instance
|
||||
- plain object 생성
|
||||
을 우선 검토한다.
|
||||
|
||||
### 11. configuration class는 “조립”만 하고 business logic은 넣지 않는다
|
||||
`@Configuration` 클래스는 bean wiring과 설정 소유만 담당한다.
|
||||
|
||||
금지:
|
||||
- 비즈니스 흐름
|
||||
- 상태 전이
|
||||
- 외부 호출 오케스트레이션
|
||||
- 의미 있는 계산 로직
|
||||
|
||||
### 12. stereotype는 의미에 맞게 쓴다
|
||||
- persistence adapter면 `@Repository`
|
||||
- use case/application service면 `@Service`
|
||||
- web adapter면 `@Controller` / `@RestController`
|
||||
- 그 외 일반 Spring-managed component면 `@Component`
|
||||
|
||||
의미 없는 전부 `@Component` 관성 사용은 지양한다.
|
||||
|
||||
### 13. public API 역할이 없는 내부 구현은 과도한 bean 분해를 피한다
|
||||
container bean 수를 늘리는 것이 곧 좋은 설계는 아니다.
|
||||
|
||||
질문:
|
||||
- lifecycle 관리가 필요한가?
|
||||
- 외부에서 주입받아야 하는가?
|
||||
- 테스트 seam 가치가 있는가?
|
||||
- 명시적 wiring으로 읽기 쉬워지는가?
|
||||
|
||||
아니면 plain object가 더 낫다.
|
||||
|
||||
## 프로젝트 기준 요약
|
||||
|
||||
- bean은 컨테이너 관리 가치가 있는 객체만 등록
|
||||
- application 주 컴포넌트는 stereotype scanning 우선
|
||||
- 외부 라이브러리/명시적 조립은 `@Configuration` + `@Bean`
|
||||
- `@Bean`은 기본적으로 `@Configuration` 안에서만
|
||||
- domain object / DTO / value object는 bean 등록 금지
|
||||
- bean overriding, 런타임 동적 등록 기본 금지
|
||||
- configuration class는 조립만 담당
|
||||
@@ -0,0 +1,180 @@
|
||||
# @ConfigurationProperties 사용 기준
|
||||
|
||||
## 목적
|
||||
|
||||
외부 설정은 산발적인 문자열 주입이 아니라, **의미 있는 설정 객체**로 묶어 관리한다.
|
||||
설정은 business object가 아니라 **configuration contract** 로 취급한다.
|
||||
|
||||
## 공식 의미
|
||||
|
||||
- `@ConfigurationProperties`는 externalized configuration을 타입 안전하게 바인딩하기 위한 애노테이션이다.
|
||||
- 클래스 또는 `@Configuration` 안의 `@Bean` 메서드에 붙일 수 있다.
|
||||
- 바인딩은 setter 또는 생성자 인자를 통해 수행될 수 있다.
|
||||
- `@EnableConfigurationProperties` 또는 `@ConfigurationPropertiesScan`으로 등록할 수 있다.
|
||||
- `@ConfigurationProperties`는 `@Value`보다 relaxed binding, metadata 지원에 유리하다.
|
||||
- `@ConfigurationProperties`는 SpEL을 평가하지 않는다.
|
||||
- `@ConfigurationPropertiesScan`은 `@Component`가 붙은 클래스를 스캔 대상으로 잡지 않는다.
|
||||
- `@Validated`로 properties validation을 수행할 수 있다.
|
||||
- `Optional`은 `@ConfigurationProperties`에서 권장되지 않는다.
|
||||
|
||||
## 기본 규칙
|
||||
|
||||
### 1. 의미 있는 설정 그룹은 `@ConfigurationProperties` 우선
|
||||
다음은 `@ConfigurationProperties`를 우선 검토한다.
|
||||
|
||||
- 같은 prefix 아래 여러 설정값이 함께 움직임
|
||||
- 계층형/nested 설정이 있음
|
||||
- 설정 검증이 중요함
|
||||
- 여러 bean이 같은 설정 집합을 참조함
|
||||
- 운영 문서와 IDE metadata 지원이 중요함
|
||||
|
||||
예:
|
||||
- Vault 설정
|
||||
- OAuth client 설정
|
||||
- JWT 설정
|
||||
- scheduler/retry 설정
|
||||
- feature toggle 묶음
|
||||
|
||||
### 2. 단발성 한두 값만 필요하면 `@Value`를 제한적으로 허용
|
||||
다음은 `@Value`를 허용할 수 있다.
|
||||
|
||||
- 단일 상수성 설정값
|
||||
- 로컬 config class 안에서만 쓰는 매우 작은 값
|
||||
- SpEL이 실제로 필요한 경우
|
||||
|
||||
단, 애플리케이션 자체 설정 키 집합이라면 `@ConfigurationProperties`를 우선한다.
|
||||
|
||||
### 3. properties class는 설정 계약만 표현
|
||||
`@ConfigurationProperties` 클래스의 책임은:
|
||||
- 설정값 구조 표현
|
||||
- 타입 안전 바인딩
|
||||
- 검증
|
||||
- 합리적 기본값 표현
|
||||
|
||||
다음은 넣지 않는다.
|
||||
- business logic
|
||||
- 외부 API 호출
|
||||
- repository/service 호출
|
||||
- 큰 계산 로직
|
||||
- runtime mutable state
|
||||
|
||||
### 4. prefix는 명확하고 안정적으로 설계
|
||||
prefix는 기능/도메인 경계를 드러내야 한다.
|
||||
|
||||
좋은 방향:
|
||||
- `auth.jwt`
|
||||
- `auth.oauth.google`
|
||||
- `vault.transit`
|
||||
- `app.retry`
|
||||
|
||||
지양:
|
||||
- `config`
|
||||
- `common`
|
||||
- `misc`
|
||||
- 의미가 너무 넓은 prefix
|
||||
|
||||
### 5. 등록 방식은 스캔과 명시 등록을 구분
|
||||
기본 선택:
|
||||
- 애플리케이션 내부 일반 설정 타입 -> `@ConfigurationPropertiesScan`
|
||||
- 조건부 등록/auto-configuration/명시적 wiring 필요 -> `@EnableConfigurationProperties` 또는 `@Bean` + `@ConfigurationProperties`
|
||||
|
||||
### 6. `@Component`와 `@ConfigurationProperties`를 습관적으로 같이 쓰지 않는다
|
||||
properties class는 설정 바인딩 타입이지 일반 component가 아니다.
|
||||
|
||||
기본:
|
||||
- scanning 대상 properties -> `@ConfigurationPropertiesScan`
|
||||
- 명시 등록이 필요하면 `@EnableConfigurationProperties`
|
||||
|
||||
`@Component`를 붙여 일반 bean처럼 다루는 패턴은 지양한다.
|
||||
|
||||
### 7. 가능한 한 immutable 구조를 선호
|
||||
설정은 보통 startup 후 바뀌지 않는 계약이다.
|
||||
|
||||
기본 방향:
|
||||
- 생성자 기반 바인딩 또는 immutable한 구조 선호
|
||||
- 변경 가능한 setter-only bag object를 기본값으로 삼지 않음
|
||||
- 필수 설정은 생성 시점에 확정되게 설계
|
||||
|
||||
### 8. `Optional` 필드 금지
|
||||
Spring Boot 공식 문서상 `Optional`은 `@ConfigurationProperties`에서 권장되지 않는다.
|
||||
|
||||
기본:
|
||||
- nullable field
|
||||
- 기본값
|
||||
- nested object
|
||||
- 명시적 default object
|
||||
중 하나로 표현한다.
|
||||
|
||||
### 9. validation은 startup에서 최대한 실패하게
|
||||
설정이 잘못되면 런타임 깊은 지점에서 터지지 않게, 바인딩 시점 검증을 우선한다.
|
||||
|
||||
기본:
|
||||
- `@Validated`
|
||||
- Bean Validation annotation (`@NotNull`, `@Min`, `@Pattern` 등)
|
||||
- nested properties는 필요 시 `@Valid`
|
||||
|
||||
### 10. 기본값 정책을 숨기지 않는다
|
||||
기본값은 다음 중 하나로 명시한다.
|
||||
|
||||
- 필드 기본값
|
||||
- 생성자 기본값
|
||||
- 명시적 nested default object
|
||||
- 문서화된 운영 기본값
|
||||
|
||||
“값이 없으면 나중에 어딘가에서 처리”를 금지한다.
|
||||
|
||||
### 11. Environment 직접 조회보다 properties bean 주입 우선
|
||||
application/service/infrastructure 코드에서 `Environment#getProperty(...)`를 흩뿌리지 않는다.
|
||||
|
||||
기본:
|
||||
- 관련 설정은 properties 객체로 묶고
|
||||
- 필요한 bean에 주입한다
|
||||
|
||||
예외:
|
||||
- truly dynamic property lookup
|
||||
- framework/bootstrap 초기화 특수 상황
|
||||
|
||||
### 12. 설정 객체는 소유 모듈 가까이에 둔다
|
||||
properties class는 그것을 사용하는 기능/모듈 옆에 둔다.
|
||||
|
||||
예:
|
||||
- `vault` 설정은 vault adapter/config 근처
|
||||
- `jwt` 설정은 jwt/token 모듈 근처
|
||||
|
||||
`CommonProperties`, `AppProperties`처럼 전역 잡동사니 설정 객체는 지양한다.
|
||||
|
||||
### 13. third-party bean 바인딩도 가능하지만, 범위를 제한
|
||||
외부 라이브러리 객체를 `@Bean` 메서드 + `@ConfigurationProperties`로 바인딩할 수 있다.
|
||||
단, 그 경우도:
|
||||
- 명확한 prefix
|
||||
- 명확한 config class
|
||||
- 외부 라이브러리 설정 범위 제한
|
||||
을 지킨다.
|
||||
|
||||
### 14. 설정 계약과 business meaning을 혼동하지 않는다
|
||||
예:
|
||||
- `token-expiration-seconds`는 설정 계약
|
||||
- `TokenTtl`은 도메인 의미일 수 있다
|
||||
|
||||
필요하면 properties -> domain config/value object 변환 단계를 둔다.
|
||||
설정 타입을 그대로 domain everywhere에 흘려보내지 않는다.
|
||||
|
||||
### 15. 문서와 metadata를 함께 고려
|
||||
`@ConfigurationProperties`의 장점 중 하나는 metadata/IDE 지원이다.
|
||||
애플리케이션이 제공하는 설정 키는:
|
||||
- prefix 일관성
|
||||
- 이름 일관성
|
||||
- 설명/문서화
|
||||
를 함께 고려한다.
|
||||
|
||||
## 프로젝트 기준 요약
|
||||
|
||||
- 의미 있는 설정 집합은 `@ConfigurationProperties` 우선
|
||||
- 한두 개 단발성 값만 `@Value` 제한 허용
|
||||
- properties class는 설정 계약만 표현
|
||||
- `@Component`와 습관적 결합 금지
|
||||
- immutable 구조 선호
|
||||
- `Optional` 필드 금지
|
||||
- validation은 startup에서 최대한 실패하게
|
||||
- `Environment` 직접 조회보다 properties bean 주입 우선
|
||||
- 설정 객체는 owning module 가까이에 둔다
|
||||
@@ -0,0 +1,166 @@
|
||||
# dependency injection 기준
|
||||
|
||||
## 목적
|
||||
|
||||
의존성 주입은 객체가 자신의 협력 객체를 직접 생성하거나 찾지 않게 하여,
|
||||
- 결합도를 낮추고
|
||||
- 테스트를 쉽게 하며
|
||||
- 초기화 상태를 더 명확하게 만드는 데 사용한다.
|
||||
|
||||
## 공식 의미
|
||||
|
||||
- Spring DI는 객체가 의존성을 생성자 인자, factory method 인자, 또는 setter/config method로 선언하면 컨테이너가 주입하는 방식이다.
|
||||
- Spring은 constructor-based DI와 setter-based DI를 지원한다.
|
||||
- Spring 팀은 일반적으로 constructor injection을 권장한다.
|
||||
- constructor injection은 필수 의존성의 non-null 보장과 fully initialized state를 더 쉽게 만든다.
|
||||
- setter injection은 주로 optional dependency 또는 재설정 가능한 dependency에 적합하다.
|
||||
- field injection은 production code에서는 권장되지 않는다.
|
||||
- 생성자가 하나뿐인 경우 Spring은 `@Autowired` 없이도 그 생성자를 사용할 수 있다.
|
||||
|
||||
## 기본 규칙
|
||||
|
||||
### 1. 기본값은 constructor injection
|
||||
application code의 기본 DI 방식은 생성자 주입이다.
|
||||
|
||||
이유:
|
||||
- 필수 의존성이 명확하다
|
||||
- 객체가 생성 직후 완전한 상태가 된다
|
||||
- final field 사용이 가능하다
|
||||
- 테스트에서 plain constructor 호출이 쉽다
|
||||
|
||||
### 2. 필수 의존성은 생성자로만 받는다
|
||||
다음은 생성자로만 주입한다.
|
||||
|
||||
- business collaborator
|
||||
- repository / port
|
||||
- external client
|
||||
- policy / strategy
|
||||
- configuration object
|
||||
- mapper / validator / assembler 중 필수 협력 객체
|
||||
|
||||
필수 의존성을 setter/field로 받지 않는다.
|
||||
|
||||
### 3. 선택 의존성만 setter/config method injection 검토
|
||||
setter 또는 config method injection은 아래일 때만 검토한다.
|
||||
|
||||
- optional dependency
|
||||
- reasonable default가 있는 경우
|
||||
- 재설정/reconfiguration 가능성이 실제로 필요한 경우
|
||||
- legacy / third-party class 구조상 생성자 주입이 적합하지 않은 경우
|
||||
|
||||
기본값은 아니다.
|
||||
|
||||
### 4. production code field injection 금지
|
||||
production code에서는 field injection을 사용하지 않는다.
|
||||
|
||||
이유:
|
||||
- 의존성이 시그니처에 드러나지 않는다
|
||||
- plain unit test가 불편해진다
|
||||
- final field 사용이 어렵다
|
||||
- partially initialized state 위험을 키운다
|
||||
|
||||
예외:
|
||||
- 테스트 클래스
|
||||
- framework가 직접 관리하는 극히 제한적 특수 케이스
|
||||
|
||||
### 5. 단일 생성자면 `@Autowired` 생략 가능
|
||||
생성자가 하나뿐인 bean class는 `@Autowired`를 굳이 붙이지 않아도 된다.
|
||||
|
||||
기본:
|
||||
- single constructor -> annotation 생략 가능
|
||||
- 여러 생성자면 의도를 분명히 해야 한다
|
||||
|
||||
### 6. 의존성은 lookup하지 않는다
|
||||
bean은 자신의 dependency를 직접 찾지 않는다.
|
||||
|
||||
금지:
|
||||
- `applicationContext.getBean(...)`
|
||||
- service locator 패턴
|
||||
- static holder 통해 bean 가져오기
|
||||
|
||||
예외:
|
||||
- 아주 제한된 framework integration
|
||||
- truly dynamic lookup이 필요한 infrastructure 경계
|
||||
|
||||
기본은 constructor/setter 주입이다.
|
||||
|
||||
### 7. 생성자 인자가 많으면 DI 스타일이 아니라 책임 분해 문제를 먼저 본다
|
||||
Spring 공식 문서도 constructor parameter가 많으면 code smell로 본다.
|
||||
|
||||
기본 판단:
|
||||
- 5~7개 이상으로 커지면 책임 과다를 의심
|
||||
- 하위 collaborator 분리
|
||||
- policy object 분리
|
||||
- orchestration 분리
|
||||
- config object 묶기
|
||||
를 먼저 검토한다
|
||||
|
||||
“setter로 바꿔서 숨기기”로 해결하지 않는다.
|
||||
|
||||
### 8. final field 우선
|
||||
constructor injection을 쓴다면 의존성 필드는 가능한 한 final로 둔다.
|
||||
|
||||
이유:
|
||||
- 불변성 강화
|
||||
- 초기화 상태 명확화
|
||||
- 재주입/변경 여지 축소
|
||||
|
||||
### 9. optional dependency는 명시적으로 표현
|
||||
optional dependency는 다음 방식 중 하나를 명시적으로 선택한다.
|
||||
|
||||
- setter injection
|
||||
- `ObjectProvider<T>`
|
||||
- nullable/optional parameter를 가진 config method
|
||||
- reasonable default를 가진 생성자/팩토리 구성
|
||||
|
||||
필수와 선택을 섞어 모호하게 만들지 않는다.
|
||||
|
||||
### 10. 컬렉션/다중 구현 주입은 의도를 분명히
|
||||
여러 bean이 한 인터페이스를 구현할 때는:
|
||||
- `List<T>`
|
||||
- `Map<String, T>`
|
||||
- `@Qualifier`
|
||||
- `@Primary`
|
||||
등을 통해 의도를 분명히 한다.
|
||||
|
||||
“우연히 하나만 있으니까 된다”에 기대지 않는다.
|
||||
|
||||
### 11. configuration properties는 raw value보다 객체로 주입
|
||||
관련 설정값이 여러 개면 primitive/string 여러 개를 직접 주입하지 말고, configuration properties 객체로 묶어 주입하는 쪽을 우선 검토한다.
|
||||
|
||||
### 12. bean 간 순환 의존은 기본 금지
|
||||
Spring은 constructor circular dependency를 문제로 보고, setter injection으로 우회는 가능하지만 권장하지 않는다.
|
||||
|
||||
기본:
|
||||
- 순환 구조를 리팩터링으로 제거
|
||||
- 책임 재배치
|
||||
- 이벤트/포트/분리된 collaborator 도입 검토
|
||||
|
||||
setter로 억지 우회하지 않는다.
|
||||
|
||||
### 13. framework 관리 대상과 plain object를 구분
|
||||
모든 객체가 DI 대상은 아니다.
|
||||
|
||||
기본:
|
||||
- Spring bean 협력은 DI
|
||||
- value object / domain entity / DTO / 단순 계산 객체는 plain object 생성 유지
|
||||
|
||||
### 14. 테스트도 같은 원칙을 따르되, 테스트 클래스 field injection은 허용 가능
|
||||
Spring 공식 문서는 테스트에서는 field injection이 자연스러울 수 있다고 설명한다.
|
||||
|
||||
기본:
|
||||
- production code -> constructor injection
|
||||
- test class -> Spring test fixture에서는 field injection 허용 가능
|
||||
- 하지만 application code 자체는 계속 constructor injection 유지
|
||||
|
||||
## 프로젝트 기준 요약
|
||||
|
||||
- 기본 DI 방식은 constructor injection
|
||||
- 필수 의존성은 생성자
|
||||
- optional dependency만 setter/config method 검토
|
||||
- production code field injection 금지
|
||||
- single constructor면 `@Autowired` 생략 가능
|
||||
- dependency lookup 금지
|
||||
- 생성자 인자 과다는 책임 분해 신호
|
||||
- final field 우선
|
||||
- circular dependency 우회보다 구조 수정 우선
|
||||
@@ -0,0 +1,317 @@
|
||||
# Filter / Interceptor / Resolver / Advice 기준
|
||||
|
||||
## 1. 목적
|
||||
|
||||
이 문서는 Spring MVC 기반 서버에서 요청/응답 경계의 공통 처리 로직을 어디에 둘지 정의한다.
|
||||
|
||||
대상은 다음 네 가지다.
|
||||
|
||||
- Servlet Filter
|
||||
- Spring MVC HandlerInterceptor
|
||||
- HandlerExceptionResolver
|
||||
- @ControllerAdvice / @RestControllerAdvice / @ExceptionHandler / ResponseBodyAdvice
|
||||
|
||||
목표는 다음과 같다.
|
||||
|
||||
- HTTP/Servlet 수준 관심사와 MVC/controller 수준 관심사를 분리한다.
|
||||
- 예외 처리와 응답 포맷 표준화를 한 곳에 모은다.
|
||||
- business rule, validation, transaction, domain mapping이 web infrastructure 훅 안으로 새어 들어가지 않게 한다.
|
||||
- 같은 문제를 filter / interceptor / advice 어디에든 중복 구현하는 일을 막는다.
|
||||
|
||||
## 2. 근거 수준
|
||||
|
||||
- Official: Spring Framework / Spring Boot 공식 문서에서 직접 확인되는 내용
|
||||
- Official + Practice: 공식 제약 위에 일반적인 실무 운영 원칙을 결합한 내용
|
||||
- Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙
|
||||
|
||||
## 3. 기본 원칙
|
||||
|
||||
### 3.1 계층이 낮을수록 더 일반적인 HTTP 관심사만 둔다
|
||||
|
||||
기본 원칙:
|
||||
|
||||
- Filter: Servlet/HTTP 인프라 수준 공통 처리
|
||||
- Interceptor: handler/controller 실행 전후 공통 처리
|
||||
- @RestControllerAdvice: controller 계층의 예외 응답/공통 응답 규약 처리
|
||||
- HandlerExceptionResolver: 정말 낮은 수준의 resolver chain 커스터마이징이 필요할 때만 사용
|
||||
|
||||
Spring 공식 문서 기준으로 Filter는 filter chain과 target Servlet 전후에 interception-style logic을 적용하는 용도이고, HandlerInterceptor는 handler 실행 전후 callback이며, 예외는 DispatcherServlet이 HandlerExceptionResolver 체인으로 위임합니다. @ControllerAdvice / @RestControllerAdvice는 전역 @ExceptionHandler 적용 지점입니다.
|
||||
|
||||
### 3.2 더 위 레벨 도구로 해결 가능한 일은 아래 레벨로 내리지 않는다
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- controller 예외 응답은 우선 @RestControllerAdvice
|
||||
- 응답 body 표준화는 우선 ResponseBodyAdvice
|
||||
- handler 관련 공통 전처리는 우선 HandlerInterceptor
|
||||
- Servlet container 전체에 걸친 공통 처리만 Filter
|
||||
|
||||
즉, 아래 레벨 훅을 “더 강력하니까” 먼저 선택하지 않는다.
|
||||
|
||||
### 3.3 이 프로젝트의 에러 응답 기본 포맷은 ApiResult다
|
||||
|
||||
Spring은 @ExceptionHandler 또는 @RequestMapping에서 ProblemDetail이나 ErrorResponse를 반환해 RFC 9457 응답을 렌더링할 수 있고, ResponseEntityExceptionHandler도 공식 제공한다. 하지만 이 프로젝트는 공식 확장 지점은 따르되, 응답 본문 포맷의 기본값은 custom ApiResult 로 둔다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- 에러 응답의 프로젝트 기본 표준은 ProblemDetail이 아니라 ApiResult
|
||||
- 전역 예외 처리의 기본 위치는 @RestControllerAdvice
|
||||
- built-in MVC 예외와 business exception 모두 프로젝트 공통 ApiResult 규약으로 변환
|
||||
- 외부 표준 계약이나 특정 연동에서 RFC 9457이 명시적으로 필요할 때만 ProblemDetail 사용을 예외적으로 허용
|
||||
|
||||
이 규칙은 Official + Practice + Project Recommendation 이다.
|
||||
즉, 공식 문서가 제공하는 entry point는 사용하되, 실제 payload shape은 프로젝트 표준으로 통일한다.
|
||||
|
||||
## 4. Filter 표준
|
||||
|
||||
### 4.1 Filter의 책임
|
||||
|
||||
Filter는 Servlet 체인 수준의 공통 처리에 사용한다. Spring 공식 문서에서 Filter는 processing chain과 target Servlet 전후에 interception-style logic을 적용하는 용도다.
|
||||
|
||||
허용 예:
|
||||
|
||||
- request/response wrapping
|
||||
- forwarded header 처리
|
||||
- correlation id / trace id의 very-early binding
|
||||
- MDC 진입/해제
|
||||
- Spring Security filter chain에 통합되는 보안 전처리
|
||||
- controller mapping 이전에 처리되어야 하는 공통 HTTP concern
|
||||
|
||||
비허용 예:
|
||||
|
||||
- 도메인 검증
|
||||
- use case 오케스트레이션
|
||||
- transaction 시작/종료
|
||||
- repository/JPA 직접 호출을 전제로 한 핵심 업무 처리
|
||||
- DTO ↔ domain 변환
|
||||
- 공통 API 에러 응답 본문 구성의 주 책임
|
||||
|
||||
### 4.2 custom filter는 OncePerRequestFilter를 우선 검토한다
|
||||
|
||||
Spring 공식 문서 기준으로 OncePerRequestFilter는 request 시작 시 단일 호출을 지원하고, ASYNC/ERROR dispatch 관여 여부 제어를 제공한다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- 새 custom filter는 기본적으로 OncePerRequestFilter를 우선 사용한다.
|
||||
- shouldNotFilterAsyncDispatch, shouldNotFilterErrorDispatch 필요 여부를 명시적으로 검토한다.
|
||||
- “왜 filter여야 하는가?”를 설명할 수 없으면 interceptor 또는 advice로 올린다.
|
||||
|
||||
### 4.3 Filter에는 무거운 의존성을 직접 물지 않는다
|
||||
|
||||
Spring Boot 공식 문서는 filter bean이 application lifecycle 초기에 설치되므로 너무 많은 bean의 eager initialization을 유발하지 않게 주의하라고 하며, DataSource나 JPA configuration 의존은 좋지 않다고 설명한다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- filter에서 repository, entity manager, transaction-heavy service 직접 의존을 기본 금지한다.
|
||||
- filter가 복잡한 서비스 계층을 호출해야 한다면 설계를 다시 검토한다.
|
||||
- 인증/인가 체계는 개별 custom filter 남발보다 security/filter chain 구조에 맞춘다.
|
||||
|
||||
### 4.4 Filter 등록과 순서는 명시적 필요가 있을 때만 건드린다
|
||||
|
||||
Spring Boot는 Filter bean을 자동 등록하고, 필요하면 FilterRegistrationBean으로 매핑과 order를 제어할 수 있다. dispatcher type 미지정 시 기본은 REQUEST다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- 순서 의존이 없으면 순서 지정 최소화
|
||||
- 순서가 필요하면 이유를 주석 또는 문서에 남긴다
|
||||
- request body를 읽거나 wrapping하는 filter는 더 이른 순서 배치를 신중히 검토한다
|
||||
|
||||
## 5. HandlerInterceptor 표준
|
||||
|
||||
### 5.1 Interceptor의 책임
|
||||
|
||||
HandlerInterceptor는 handler/controller 실행 전후의 가벼운 공통 처리에 사용한다. Spring 공식 문서도 interceptor를 handler 관련 fine-grained preprocessing 용도로 설명한다.
|
||||
|
||||
허용 예:
|
||||
|
||||
- 인증 완료 이후의 요청자 정보 추출
|
||||
- audit context 세팅/해제
|
||||
- handler 기반 lightweight access policy
|
||||
- locale/theme 같은 MVC handler 관련 전처리
|
||||
- controller 호출 전후의 가벼운 메타데이터 기록
|
||||
|
||||
비허용 예:
|
||||
|
||||
- 보안의 주 진입점
|
||||
- request/response wrapping
|
||||
- body 읽기/변형
|
||||
- transaction 제어
|
||||
- 핵심 비즈니스 검증
|
||||
- domain object 생성/조립
|
||||
|
||||
### 5.2 Interceptor를 보안 레이어의 중심으로 사용하지 않는다
|
||||
|
||||
Spring 공식 문서는 interceptor가 annotated controller path matching과 mismatch 가능성이 있어 security layer로는 이상적이지 않으며, 일반적으로 Spring Security 또는 Servlet filter chain에 통합된 접근을 더 이르게 적용하라고 권장한다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- 인증/인가의 중심은 interceptor가 아니라 security/filter chain
|
||||
- interceptor는 보안 체계가 끝난 뒤 controller 실행에 가까운 공통 처리에만 사용
|
||||
|
||||
### 5.3 @ResponseBody / ResponseEntity 응답 변경 지점으로 쓰지 않는다
|
||||
|
||||
Spring 공식 문서 기준으로 @ResponseBody와 ResponseEntity 응답은 HandlerAdapter 내부에서 body가 쓰이고 커밋된 뒤 postHandle이 호출되므로, 이 시점은 응답 변경 지점으로 늦다. 이 경우 ResponseBodyAdvice를 사용해야 한다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- JSON 응답 envelope 적용
|
||||
- 공통 body 필드 삽입
|
||||
- 에러 응답 본문 공통 보강
|
||||
- ApiResult 응답 래핑/정규화
|
||||
|
||||
이런 작업은 interceptor가 아니라 ResponseBodyAdvice 또는 @RestControllerAdvice에 둔다.
|
||||
|
||||
### 5.4 async controller가 있으면 interceptor lifecycle을 단순 가정하지 않는다
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- thread-local 정리 책임이 interceptor에 있다면 async request 존재 여부를 반드시 점검한다
|
||||
- async endpoint가 있다면 interceptor cleanup이 sync 요청과 동일하게 호출된다고 가정하지 않는다
|
||||
- async lifecycle이 중요하면 별도 async 처리 훅 설계를 검토한다
|
||||
|
||||
이 항목은 Spring MVC async 처리 모델을 반영한 Official + Practice 규칙이다.
|
||||
|
||||
## 6. HandlerExceptionResolver 표준
|
||||
|
||||
### 6.1 Resolver는 저수준 exception chain 확장 지점이다
|
||||
|
||||
Spring 공식 문서 기준으로 여러 HandlerExceptionResolver를 체인으로 둘 수 있고, order에 따라 순서가 정해지며, resolver는 ModelAndView, empty ModelAndView, 또는 null을 반환할 수 있다. null이면 다음 resolver가 계속 처리한다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- 일반 애플리케이션 예외 응답의 기본 수단으로 custom HandlerExceptionResolver를 만들지 않는다
|
||||
- 기본 선택은 @RestControllerAdvice + @ExceptionHandler
|
||||
- custom resolver는 framework integration이나 아주 낮은 수준의 예외 변환이 필요할 때만 허용
|
||||
|
||||
### 6.2 resolver는 ApiResult 표준화의 주 도구가 아니다
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- business exception → HTTP 응답 매핑은 @RestControllerAdvice
|
||||
- ApiResult.fail(...) 생성도 기본적으로 advice에서 수행
|
||||
- resolver는 “정말 advice보다 아래 레벨에서 처리해야 하는 상황”에만 사용
|
||||
|
||||
즉, ApiResult를 도입한다고 해서 resolver 쪽으로 내려가지 않는다.
|
||||
응답 포맷 표준화의 중심은 여전히 advice다.
|
||||
|
||||
## 7. @ControllerAdvice / @RestControllerAdvice 표준
|
||||
|
||||
### 7.1 전역 예외 처리는 @RestControllerAdvice를 기본으로 한다
|
||||
|
||||
Spring 공식 문서 기준으로 @RestControllerAdvice는 @ControllerAdvice + @ResponseBody의 shortcut이며, 전역 @ExceptionHandler는 local controller의 @ExceptionHandler 뒤에 적용된다. 기본적으로 모든 controller에 적용된다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- REST API 서버의 전역 예외 처리 기본값은 @RestControllerAdvice
|
||||
- controller별 local @ExceptionHandler 남발을 피한다
|
||||
- 공통 에러 정책은 소수의 advice에 집중시킨다
|
||||
|
||||
### 7.2 이 프로젝트의 전역 에러 응답은 ApiResult로 통일한다
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- business exception, validation exception, built-in MVC exception 모두 가능한 한 ApiResult 규약으로 변환한다
|
||||
- 에러 응답 구조는 프로젝트 전역에서 일관되게 유지한다
|
||||
- controller가 직접 에러 body를 조립하지 않는다
|
||||
- advice가 HTTP status와 ApiResult body를 함께 결정한다
|
||||
|
||||
권장 방향 예시:
|
||||
|
||||
- HTTP status는 표준 의미를 유지
|
||||
- body는 ApiResult의 실패 형식으로 통일
|
||||
- 내부 stack trace, framework class name, 구현 세부사항은 노출 금지
|
||||
- 외부 노출용 에러 코드와 메시지는 분리 가능하게 설계
|
||||
|
||||
이 규칙은 Official + Practice 에 가깝다.
|
||||
Spring은 ProblemDetail을 공식 지원하지만, 실제 프로젝트에서는 별도의 공통 envelope를 유지하는 경우가 많고, Spring의 공식 advice/exception handler 확장 지점은 그런 custom body에도 그대로 사용할 수 있다.
|
||||
|
||||
### 7.3 ProblemDetail은 기본이 아니라 예외적 옵션이다
|
||||
|
||||
Spring은 ProblemDetail, ErrorResponse, ResponseEntityExceptionHandler를 공식 지원한다. 또한 Spring Boot는 spring.mvc.problemdetails.enabled를 통해 built-in exception의 problem details 처리도 자동 구성할 수 있다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- 프로젝트 기본 에러 포맷은 ApiResult
|
||||
- ProblemDetail은 다음 경우에만 예외적으로 허용
|
||||
- 외부 표준 계약이 RFC 9457을 요구하는 경우
|
||||
- 특정 API만 공개 표준 준수가 더 중요한 경우
|
||||
- 타 시스템과의 호환성 때문에 application/problem+json이 필요한 경우
|
||||
- 프로젝트 내부/일반 REST API에는 ProblemDetail과 ApiResult를 혼용하지 않는다
|
||||
|
||||
### 7.4 built-in MVC 예외도 프로젝트 포맷으로 흡수한다
|
||||
|
||||
Spring 공식 문서 기준으로 ResponseEntityExceptionHandler는 Spring MVC 예외와 ErrorResponseException을 다루는 편의 base class다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- built-in MVC 예외가 많고 이를 일관된 ApiResult로 바꿔야 하면 ResponseEntityExceptionHandler 확장을 검토한다
|
||||
- built-in 예외를 ProblemDetail 그대로 노출하는 방향은 프로젝트 기본값이 아니다
|
||||
- business exception과 framework exception이 서로 다른 응답 형식을 가지지 않게 한다
|
||||
|
||||
## 8. ResponseBodyAdvice 표준
|
||||
|
||||
### 8.1 응답 body 공통 가공은 ResponseBodyAdvice를 사용한다
|
||||
|
||||
Spring 공식 문서 기준으로 ResponseBodyAdvice는 @ResponseBody 또는 ResponseEntity controller method 실행 후, HttpMessageConverter가 body를 쓰기 전에 응답을 커스터마이징하는 확장 지점이다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- 성공 응답의 공통 envelope 적용
|
||||
- ApiResult.success(...) 형태의 일관화
|
||||
- 에러 응답 body의 공통 필드 보강
|
||||
- trace id, timestamp, request id 같은 공통 값 삽입
|
||||
|
||||
이런 책임은 ResponseBodyAdvice에 둘 수 있다.
|
||||
|
||||
### 8.2 다만 전역 래핑은 “명확한 규약”이 있을 때만 사용한다
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- 모든 성공 응답을 ApiResult로 래핑할지 여부는 프로젝트 API 계약에 맞춰 일관되게 정한다
|
||||
- file download, streaming, SSE, 이미 포맷이 고정된 응답에는 전역 wrapping을 피한다
|
||||
- supports(...) 조건을 좁게 잡아 surprise를 줄인다
|
||||
- controller가 이미 ApiResult를 반환하는 프로젝트라면 ResponseBodyAdvice로 이중 래핑하지 않는다
|
||||
|
||||
즉, ResponseBodyAdvice는 강력하지만 “마법처럼 몰래 바꾸는 곳”이 아니라 공식적인 응답 규약 적용 지점이어야 한다.
|
||||
|
||||
## 9. 위치별 금지 규칙
|
||||
|
||||
다음은 기본 금지다.
|
||||
|
||||
Filter:
|
||||
|
||||
- transaction
|
||||
- repository/JPA 직접 호출
|
||||
- business validation
|
||||
- DTO/domain mapping
|
||||
- ApiResult 에러 본문 생성의 주 수단
|
||||
|
||||
Interceptor:
|
||||
|
||||
- 인증/인가의 주 구현
|
||||
- body wrapping
|
||||
- @ResponseBody 응답 변형
|
||||
- transaction
|
||||
- 핵심 use case 호출
|
||||
|
||||
HandlerExceptionResolver:
|
||||
|
||||
- 일반 business exception 처리의 기본 수단
|
||||
- 팀 공통 ApiResult 포맷의 주 진입점
|
||||
|
||||
ControllerAdvice / ResponseBodyAdvice:
|
||||
|
||||
- domain 규칙 실행
|
||||
- repository 접근
|
||||
- 핵심 오케스트레이션
|
||||
- endpoint별 business branching 누적
|
||||
|
||||
## 10. 선택 기준 요약
|
||||
|
||||
다음 질문으로 결정한다.
|
||||
|
||||
- request/response를 Servlet 수준에서 감싸거나 아주 이른 시점에 처리해야 하는가? Filter
|
||||
- 특정 handler/controller 실행 전후의 공통 처리인가? HandlerInterceptor
|
||||
- controller 예외를 HTTP 응답으로 바꾸는가? @RestControllerAdvice + @ExceptionHandler
|
||||
- 성공/실패 body를 프로젝트 공통 ApiResult 규약에 맞게 가공해야 하는가? ResponseBodyAdvice 또는 @RestControllerAdvice
|
||||
- 정말 resolver chain 자체를 커스터마이징해야 하는가? HandlerExceptionResolver
|
||||
@@ -0,0 +1,182 @@
|
||||
# @Transactional 위치 기준
|
||||
|
||||
## 목적
|
||||
|
||||
트랜잭션은 “아무 데나 붙이는 애노테이션”이 아니라,
|
||||
**하나의 비즈니스 작업 단위를 원자적으로 끝내야 하는 경계**에 둔다.
|
||||
|
||||
이 문서의 핵심은:
|
||||
- 트랜잭션을 어디에 둘지
|
||||
- 어디에 두면 안 되는지
|
||||
- 프록시 기반 동작 때문에 어떤 함정이 있는지
|
||||
를 명확히 하는 것이다.
|
||||
|
||||
## 공식 의미
|
||||
|
||||
- Spring의 선언적 트랜잭션은 기본적으로 AOP proxy 기반이다.
|
||||
- proxy mode에서는 프록시를 통해 들어오는 외부 메서드 호출만 interception 된다.
|
||||
- self-invocation은 기본적으로 transactional interception을 일으키지 않는다.
|
||||
- `@Transactional` 기본값은 `PROPAGATION_REQUIRED`, `ISOLATION_DEFAULT`, read-write, 기본 timeout, unchecked exception rollback이다.
|
||||
- Spring 팀은 인터페이스보다 구체 클래스의 메서드에 `@Transactional`을 두는 것을 권장한다.
|
||||
- `PROPAGATION_REQUIRED`는 같은 스레드에서 service facade가 여러 repository 호출을 묶는 일반적인 호출 구조에 적절한 기본값이다.
|
||||
- `REQUIRES_NEW`는 별도 물리 트랜잭션/자원을 더 잡으므로 커넥션 풀 고갈 위험이 있을 수 있다.
|
||||
|
||||
## 기본 규칙
|
||||
|
||||
### 1. 기본 위치는 application service / use case
|
||||
트랜잭션 경계의 기본 위치는 application 계층의 service/use case 메서드다.
|
||||
|
||||
이유:
|
||||
- 하나의 비즈니스 작업 단위를 가장 잘 표현한다
|
||||
- 여러 repository/adapter 호출을 하나의 경계로 묶기 쉽다
|
||||
- controller / repository / adapter에 흩어지는 것을 막는다
|
||||
|
||||
기본 예:
|
||||
- 회원 가입
|
||||
- 로그인 완료 처리
|
||||
- 사용자 상태 변경
|
||||
- 토큰 발급 + 저장
|
||||
- 주문 생성
|
||||
- 재시도 가능한 import 단위
|
||||
|
||||
### 2. controller / filter / interceptor / resolver에는 기본 금지
|
||||
web adapter는 HTTP 경계를 담당한다.
|
||||
트랜잭션 경계를 web layer에 두지 않는다.
|
||||
|
||||
금지 대상 기본값:
|
||||
- `@Controller`
|
||||
- `@RestController`
|
||||
- `Filter`
|
||||
- `HandlerInterceptor`
|
||||
- `HandlerMethodArgumentResolver`
|
||||
- exception handler
|
||||
|
||||
이유:
|
||||
- HTTP concern과 transaction concern이 섞인다
|
||||
- web boundary가 persistence 세부를 과도하게 끌어안게 된다
|
||||
- 요청 전체를 너무 넓은 transaction으로 감쌀 위험이 커진다
|
||||
|
||||
### 3. repository / adapter에는 기본 금지
|
||||
repository나 infrastructure adapter는 “트랜잭션 경계 소유자”가 아니라
|
||||
상위 경계 안에서 참여하는 collaborator를 기본값으로 한다.
|
||||
|
||||
기본 금지:
|
||||
- repository 메서드마다 습관적으로 `@Transactional`
|
||||
- external API client에 `@Transactional`
|
||||
- mapper/assembler/helper에 `@Transactional`
|
||||
|
||||
예외:
|
||||
- 그 컴포넌트가 독립적인 transaction boundary를 실제로 소유해야 하는 경우
|
||||
- 프레임워크/기술 통합상 별도 transaction semantics가 정말 필요한 경우
|
||||
|
||||
### 4. 읽기 전용 use case는 `readOnly = true` 검토
|
||||
순수 조회 유스케이스라면 `@Transactional(readOnly = true)`를 우선 검토한다.
|
||||
|
||||
적용 후보:
|
||||
- 상세 조회
|
||||
- 목록 조회
|
||||
- 검색
|
||||
- 통계용 읽기 작업
|
||||
|
||||
단, readOnly 안에서 실제 write가 섞이면 안 된다.
|
||||
|
||||
### 5. 쓰기 작업은 기본 read-write
|
||||
상태 변경, 저장, 삭제, 발급, 전이 같은 작업은 기본 read-write transaction으로 둔다.
|
||||
readOnly를 습관적으로 붙이지 않는다.
|
||||
|
||||
### 6. self-invocation을 믿지 않는다
|
||||
같은 클래스 안에서 `this.someTransactionalMethod()`처럼 호출하면
|
||||
proxy mode에서는 transactional interception이 일어나지 않는다.
|
||||
|
||||
기본 대응:
|
||||
- transaction boundary를 public entry method로 둔다
|
||||
- 필요하면 클래스를 분리한다
|
||||
- self-invocation을 전제로 설계하지 않는다
|
||||
|
||||
### 7. `@PostConstruct` / 초기화 코드에서 transaction에 기대지 않는다
|
||||
Spring 공식 문서상 proxy가 완전히 준비되기 전에는 기대한 동작이 보장되지 않는다.
|
||||
초기화 코드에서 transactional behavior를 전제로 하지 않는다.
|
||||
|
||||
### 8. 인터페이스보다 구체 클래스 메서드에 둔다
|
||||
Spring 팀 권장에 따라 `@Transactional`은 기본적으로 구체 클래스의 메서드에 둔다.
|
||||
|
||||
이유:
|
||||
- AspectJ weaving 등에서 interface annotation이 조용히 무시될 수 있는 함정을 줄인다
|
||||
- annotation 위치가 더 직접적이고 명확하다
|
||||
|
||||
### 9. 클래스 레벨보다 메서드 레벨 우선 검토
|
||||
클래스 전체가 거의 같은 transaction semantics를 가지면 클래스 레벨 선언을 허용할 수 있다.
|
||||
하지만 readOnly/read-write, propagation, timeout이 섞이면 메서드별로 명시한다.
|
||||
|
||||
기본:
|
||||
- 모든 public method가 같은 semantics면 class-level 가능
|
||||
- 그렇지 않으면 method-level로 명확히 분리
|
||||
|
||||
### 10. 트랜잭션은 가능한 한 짧게 유지
|
||||
트랜잭션은 DB 자원/잠금/연결을 붙잡을 수 있으므로 가능한 짧게 유지한다.
|
||||
|
||||
기본 금지:
|
||||
- 긴 계산
|
||||
- sleep
|
||||
- 재시도 루프
|
||||
- 사용자 입력 대기
|
||||
- 네트워크 왕복 다수 포함
|
||||
- 파일 업로드/다운로드 전체를 transaction 안에 유지
|
||||
|
||||
### 11. 외부 API 호출을 긴 DB transaction 안에 넣지 않는다
|
||||
공식 문서가 직접 “외부 API 호출 금지”라고 쓰지는 않지만, transaction 자원과 propagation semantics 설명을 보면
|
||||
DB transaction이 열려 있는 동안 원격 네트워크 호출까지 길게 포함시키는 것은 프로젝트 기본값으로 금지하는 것이 안전하다.
|
||||
|
||||
기본 방향:
|
||||
- DB update 전/후로 외부 호출을 분리
|
||||
- 정말 필요하면 outbox/event/후속 작업 구조 검토
|
||||
- 원격 호출 때문에 DB connection/lock을 오래 잡지 않는다
|
||||
|
||||
### 12. `REQUIRES_NEW`는 예외적으로만
|
||||
`REQUIRES_NEW`는 기본값이 아니다.
|
||||
|
||||
허용 후보:
|
||||
- 독립 감사 로그 저장
|
||||
- 실패해도 본 작업과 분리되어야 하는 후속 기록
|
||||
- 별도 확정 단위가 명확한 경우
|
||||
|
||||
주의:
|
||||
- 별도 물리 트랜잭션/자원을 사용한다
|
||||
- 커넥션 풀 크기와 deadlock 가능성을 고려해야 한다
|
||||
|
||||
### 13. propagation/isolation/timeout은 필요할 때만 명시
|
||||
기본값은 `REQUIRED`, `ISOLATION_DEFAULT`, 시스템 timeout이다.
|
||||
즉 의미가 명확할 때만 덮어쓴다.
|
||||
|
||||
금지:
|
||||
- 습관적으로 모든 메서드에 propagation/isolation 지정
|
||||
- 이유 없는 `REQUIRES_NEW`
|
||||
- 이유 없는 custom timeout
|
||||
|
||||
### 14. rollback 규칙은 예외 설계와 함께 본다
|
||||
기본적으로 unchecked exception만 rollback된다.
|
||||
checked exception도 rollback해야 하면 `rollbackFor` 등을 명시한다.
|
||||
|
||||
즉:
|
||||
- 예외 타입 설계
|
||||
- rollback 정책
|
||||
- business failure 의미
|
||||
를 같이 설계한다.
|
||||
|
||||
### 15. method visibility와 proxy 제약을 의식
|
||||
proxy mode에서는 인터페이스 기반 프록시의 transactional 메서드는 public이어야 한다.
|
||||
class-based proxy에서는 protected/package-visible 메서드도 가능하지만,
|
||||
프로젝트 기본값은 **외부 진입 public method를 transaction boundary로 두는 것**이다.
|
||||
|
||||
## 프로젝트 기준 요약
|
||||
|
||||
- 기본 위치는 application service / use case
|
||||
- controller / filter / resolver / interceptor에는 기본 금지
|
||||
- repository / external client / mapper에는 기본 금지
|
||||
- 순수 조회는 `readOnly = true` 검토
|
||||
- self-invocation 믿지 않음
|
||||
- `@Transactional`은 구체 클래스 메서드에 우선
|
||||
- transaction은 짧게 유지
|
||||
- 외부 API 호출을 긴 DB transaction 안에 넣지 않음
|
||||
- `REQUIRES_NEW`는 예외적으로만
|
||||
- rollback 규칙은 예외 타입과 함께 설계
|
||||
@@ -0,0 +1,246 @@
|
||||
# Validation Location 기준
|
||||
|
||||
## 1. 목적
|
||||
|
||||
이 문서는 “무엇을 어디서 검증할 것인가”를 정의한다.
|
||||
|
||||
이 문서의 목표는 다음과 같다.
|
||||
|
||||
- request shape 검증과 business rule 검증을 구분한다.
|
||||
- web validation, application validation, domain invariant enforcement의 위치를 명확히 한다.
|
||||
- Bean Validation 애노테이션을 어디에 써야 하고 어디에 의존하면 안 되는지 정한다.
|
||||
- validation이 filter, interceptor, advice 같은 잘못된 지점으로 새어 나가지 않게 한다.
|
||||
|
||||
## 2. 근거 수준
|
||||
|
||||
- Official: Spring Framework 공식 문서에서 직접 확인되는 내용
|
||||
- Official + Practice: 공식 문서의 제약 위에 실무적으로 널리 쓰이는 구조를 결합한 내용
|
||||
- Project Recommendation: 이 프로젝트 구조에 맞춘 규칙
|
||||
|
||||
## 3. 기본 원칙
|
||||
|
||||
### 3.1 검증은 웹 계층 전용이 아니다
|
||||
|
||||
Spring은 validation이 웹 계층에 묶여 있어서는 안 되며, Spring Validator는 애플리케이션의 모든 계층에서 사용할 수 있다고 설명한다. 따라서 이 프로젝트에서도 validation을 “controller에서 끝나는 일”로 보지 않는다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- web validation은 입력 경계 검증
|
||||
- application validation은 use case 실행 전제 검증
|
||||
- domain validation은 도메인 불변식 보장
|
||||
- 외부 연동 validation은 외부 계약 준수 확인
|
||||
|
||||
으로 나눈다.
|
||||
|
||||
### 3.2 한 곳의 검증만 믿지 않는다
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- controller validation은 잘못된 HTTP 입력을 빨리 거르는 역할이다.
|
||||
- 그러나 domain invariant를 controller validation에 의존하지 않는다.
|
||||
- 핵심 규칙은 domain 또는 application에서 다시 보장한다.
|
||||
|
||||
즉, “controller에서 @Valid 붙였으니 충분하다”를 금지한다.
|
||||
|
||||
### 3.3 request model과 domain model을 분리한다
|
||||
|
||||
Spring 공식 문서는 웹 데이터 바인딩에서 전용 model object를 사용하는 것이 좋고, JPA/Hibernate entity 같은 domain model을 직접 노출하지 말라고 권장한다. 또한 constructor binding 또는 allowedFields 같은 바인딩 통제를 검토하라고 안내한다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- request binding 대상은 전용 request DTO / form / command model이다.
|
||||
- entity, aggregate, domain object를 @RequestBody / @ModelAttribute 바인딩 대상으로 쓰지 않는다.
|
||||
- “웹 입력용 객체”와 “도메인 객체”는 분리한다.
|
||||
|
||||
## 4. 위치별 검증 규칙
|
||||
|
||||
### 4.1 Presentation(Web) 레이어
|
||||
|
||||
무엇을 여기서 검증하나
|
||||
|
||||
controller에서는 다음을 검증한다.
|
||||
|
||||
- 필수값 존재 여부
|
||||
- 문자열 길이, blank 여부, 숫자 범위
|
||||
- enum/format/basic pattern
|
||||
- request DTO의 필드 단위 구조 검증
|
||||
- 단일 요청 객체 안에서 해결 가능한 단순 cross-field 검증
|
||||
- path variable / request param / header의 기본 제약
|
||||
|
||||
Spring MVC는 @RequestBody, @ModelAttribute, @RequestPart에 @Valid 또는 @Validated를 붙여 개별 객체 검증을 수행할 수 있고, 메서드 파라미터에 직접 @NotBlank, @Min 같은 constraint를 두면 method validation이 적용된다. 상황에 따라 MethodArgumentNotValidException 또는 HandlerMethodValidationException이 발생할 수 있다.
|
||||
|
||||
무엇을 여기서 검증하지 않나
|
||||
|
||||
controller에서는 다음을 검증하지 않는다.
|
||||
|
||||
- DB 조회가 필요한 business rule
|
||||
- 인증 이후 사용자 상태 기반 정책
|
||||
- aggregate 간 일관성
|
||||
- 도메인 불변식의 최종 보장
|
||||
- 외부 시스템 현재 상태에 의존하는 검증
|
||||
|
||||
이런 검증은 application 또는 domain으로 올린다.
|
||||
|
||||
controller 규칙
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- @RequestBody DTO에는 Bean Validation을 적극 사용한다.
|
||||
- request param / path variable / request header의 단순 제약은 메서드 파라미터에 직접 건다.
|
||||
- controller는 validation 실패를 ApiResult 규약으로 변환만 하고, 검증 정책 자체를 많이 품지 않는다.
|
||||
- request DTO는 transport schema를 표현하는 객체이지 domain object가 아니다.
|
||||
|
||||
### 4.2 Application 레이어
|
||||
|
||||
무엇을 여기서 검증하나
|
||||
|
||||
application service / use case에서는 다음을 검증한다.
|
||||
|
||||
- use case 실행 전제조건
|
||||
- 여러 입력 조합에 대한 정책 검증
|
||||
- repository 조회나 외부 상태 조회가 필요한 검증
|
||||
- “이 요청을 지금 수행해도 되는가”에 대한 오케스트레이션 수준 판단
|
||||
|
||||
Spring은 validation이 웹 계층에만 속하지 않는다고 설명하므로, 이런 종류의 검증을 application 계층으로 올리는 것은 Spring 철학과도 맞다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- application validation은 보통 명시적 코드로 작성한다.
|
||||
- 복잡한 business rule을 Bean Validation 애노테이션으로 숨기지 않는다.
|
||||
- application validation 실패는 domain/application 예외로 표현하고, web layer는 이를 ApiResult로 번역한다.
|
||||
|
||||
@Validated on service 사용 기준
|
||||
|
||||
Spring은 Bean Validation의 method validation을 MethodValidationPostProcessor와 @Validated를 통해 Spring bean에 적용할 수 있으며, 이는 AOP proxy에 의존한다고 설명한다. 따라서 proxy 한계가 있다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- service의 @Validated는 재사용 가능한 bean 경계 검증에 한해 제한적으로 허용한다.
|
||||
- self-invocation, proxy bypass, 내부 private method 호출에 기대지 않는다.
|
||||
- 핵심 business rule enforcement를 service method validation 하나에만 맡기지 않는다.
|
||||
- 단순 null/size/precondition 보조 수단으로는 쓸 수 있지만, 복잡한 도메인 정책의 주 수단으로는 쓰지 않는다.
|
||||
|
||||
### 4.3 Domain 레이어
|
||||
|
||||
무엇을 여기서 검증하나
|
||||
|
||||
domain에서는 다음을 보장한다.
|
||||
|
||||
- value object 생성 조건
|
||||
- entity/aggregate invariant
|
||||
- 도메인 행위 수행 가능 조건
|
||||
- “이 상태의 도메인 객체가 존재해도 되는가”에 대한 규칙
|
||||
|
||||
이 항목은 프로젝트 아키텍처 규칙이다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- domain invariant는 생성자, factory method, 도메인 메서드 안에서 보장한다.
|
||||
- “controller에서 이미 검증했으니 domain에서는 생략”을 금지한다.
|
||||
- domain rule 실패는 명시적 domain exception 또는 명시적 실패 모델로 표현한다.
|
||||
- domain 객체에 web concern(BindingResult, @RequestBody, @ModelAttribute)를 들이지 않는다.
|
||||
|
||||
Bean Validation 애노테이션 사용 기준
|
||||
|
||||
Spring은 Bean Validation이 domain model 속성에도 선언될 수 있다고 설명하지만, 이 프로젝트에서는 domain의 핵심 규칙을 애노테이션만으로 표현하는 방식을 기본값으로 두지 않는다. Bean Validation은 보조 표현일 수 있으나, 최종 규칙 enforcement는 도메인 코드가 담당한다. 이는 Official + Practice + Project Recommendation 이다.
|
||||
|
||||
### 4.4 Infrastructure 레이어
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- 외부 API client, messaging adapter 등에서는 외부 계약에 필요한 검증을 할 수 있다.
|
||||
- 다만 infrastructure validation은 외부 포맷/프로토콜/계약 확인용이지, 핵심 business rule의 본진이 아니다.
|
||||
- 외부 요청 DTO와 내부 domain object 사이의 매핑 전에 필요한 최소 검증은 허용한다.
|
||||
- domain 규칙을 infrastructure adapter에 중복 구현하지 않는다.
|
||||
|
||||
## 5. Bean Validation / Spring Validator 사용 규칙
|
||||
|
||||
### 5.1 @Valid / @Validated in controller
|
||||
|
||||
Spring MVC는 @Valid / @Validated로 command object 검증을 수행할 수 있고, method parameter constraint가 있으면 method validation이 개별 parameter validation을 대체한다. 또한 @Valid 자체는 constraint annotation이 아니며, @NotNull 같은 constraint와 함께 있을 때 method validation으로 이어질 수 있다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- request DTO 내부 필드 검증에는 @Valid
|
||||
- validation group이 실제로 필요한 경우에만 @Validated
|
||||
- scalar parameter 제약은 파라미터에 직접 constraint 부여
|
||||
- controller 클래스 레벨 @Validated는 기본 금지
|
||||
|
||||
### 5.2 controller 클래스 레벨 @Validated 금지
|
||||
|
||||
Spring Framework 6.1+에서 MVC built-in method validation을 제대로 쓰려면 controller 클래스 레벨 @Validated를 제거해야 한다고 공식 문서가 명시한다. 클래스 레벨 @Validated를 두면 AOP proxy 방식이 적용된다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- controller 클래스에는 @Validated를 붙이지 않는다.
|
||||
- 필요한 경우 메서드 파라미터에 직접 constraint를 선언해 MVC built-in method validation을 사용한다.
|
||||
|
||||
### 5.3 Spring Validator와 @InitBinder
|
||||
|
||||
Spring은 global validator를 MVC config로, local validator를 @InitBinder를 통해 controller 또는 @ControllerAdvice에 등록할 수 있다고 설명한다. @InitBinder는 binder 초기화, 변환, 포맷팅, controller-local customization에 쓰인다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- Bean Validation으로 충분하면 custom Spring Validator를 추가하지 않는다.
|
||||
- 특정 web input에만 필요한 로컬 검증은 @InitBinder + custom Validator를 검토할 수 있다.
|
||||
- @InitBinder는 web binding 전용 커스터마이징 지점으로 사용한다.
|
||||
- application/domain business rule을 @InitBinder validator에 넣지 않는다.
|
||||
|
||||
### 5.4 BindingResult 사용 기준
|
||||
|
||||
Spring MVC는 Errors / BindingResult를 method parameter 바로 뒤에 두면 일부 validation error를 controller 안에서 직접 다룰 수 있다고 설명한다. 그러나 다른 파라미터에 validation error가 있으면 HandlerMethodValidationException이 발생할 수 있다.
|
||||
|
||||
프로젝트 규칙:
|
||||
|
||||
- 일반 REST API에서는 BindingResult를 광범위하게 사용하지 않는다.
|
||||
- 기본 경로는 예외 발생 → @RestControllerAdvice에서 ApiResult 변환이다.
|
||||
- HTML form 처리처럼 controller가 오류를 직접 조합해야 하는 경우에만 제한적으로 사용한다.
|
||||
|
||||
## 6. 이 프로젝트의 기본 배치
|
||||
|
||||
이 프로젝트의 기본 배치는 다음과 같다.
|
||||
|
||||
presentation:
|
||||
|
||||
- request DTO 구조 검증
|
||||
- request param/path/header 기본 제약
|
||||
- transport-level parsing/format validation
|
||||
|
||||
application:
|
||||
|
||||
- use case 전제조건
|
||||
- 조회/상태 의존 정책
|
||||
- 여러 입력의 조합 규칙
|
||||
|
||||
domain:
|
||||
|
||||
- value object/entity/aggregate invariant
|
||||
- 생성/변경 가능 조건
|
||||
- 핵심 business rule
|
||||
|
||||
infrastructure:
|
||||
|
||||
- 외부 시스템 계약 검증
|
||||
- protocol/adapter 수준 포맷 검증
|
||||
|
||||
## 7. 금지 규칙
|
||||
|
||||
다음은 기본 금지다.
|
||||
|
||||
- filter / interceptor / resolver / advice 에 business validation 작성
|
||||
- entity를 @RequestBody / @ModelAttribute로 직접 바인딩
|
||||
- controller validation만 믿고 domain invariant 생략
|
||||
- service method validation proxy에 business rule enforcement를 전부 위임
|
||||
- ApiResult 에러 메시지 생성을 validator 안에서 직접 수행
|
||||
- validation group을 명확한 이유 없이 남발
|
||||
- 단순 request DTO 검증까지 repository 조회 기반 custom validator로 만드는 것
|
||||
|
||||
## 8. 체크리스트
|
||||
|
||||
다음 질문으로 위치를 결정한다.
|
||||
|
||||
- HTTP 입력 형식, nullability, 길이, 범위, 단순 필드 검증인가? presentation
|
||||
- 여러 입력 조합, 현재 상태, 조회 결과에 따라 use case 수행 가능 여부를 판단하는가? application
|
||||
- 이 객체가 존재하거나 이 행위를 수행할 수 있는지의 핵심 규칙인가? domain
|
||||
- 외부 API 계약이나 adapter 포맷 문제인가? infrastructure
|
||||
- 특정 request binding 방식, formatter, web-only validator가 필요한가? @InitBinder / local validator
|
||||
Reference in New Issue
Block a user