Spring Web-Back 실전 훈련북
이 문서의 사용법
이 문서는 Spring Web-Back 문서들의 요약본이 아니다. 이미 정리된 개념 문서와 실전 가이드북을 바탕으로, 직접 판단을 써보고 해설과 비교하는 훈련 문서다.
읽기만 하면 효과가 약하다. 각 Lab, Incident, Review에서 먼저 써보기 또는 질문 항목을 실제로 채운 뒤 해설을 본다. 해설을 본 뒤에는 연결 문서로 돌아가서 내가 놓친 판단 기준을 다시 확인한다.
훈련 방식
- 상황을 먼저 읽는다.
- 제공된 evidence만 보고 원인이나 설계 결론을 바로 단정하지 않는다.
- 빈 답안 공간에 내 판단 순서, 확인할 증거, 선택한 조치를 쓴다.
- 힌트를 보고 답을 보완한다.
- 해설과 비교한다.
- 연결 문서로 돌아가 내가 약한 개념을 확인한다.
- 반복 기록표에 틀린 이유와 다음 회독 기준을 남긴다.
반복 학습 루프
- 1회독: 답을 맞히려 하지 말고, 어떤 경계를 먼저 봐야 하는지 적는다.
- 2회독: 증거를 보고 원인 후보를 2~4개로 분류한다.
- 3회독: 하면 안 되는 조치와 완료 검증 기준까지 쓴다.
- 4회독: 연결 문서를 보지 않고 판단 기준 한 문장만으로 설명한다.
훈련 목록
| 구분 | 훈련 | 핵심 판단 |
|---|---|---|
| Lab 1 | Controller에 도착하지 않는 요청 추적 | Filter, Security, DispatcherServlet 경계 |
| Lab 2 | JSON/form binding과 validation 응답 설계 | 입력 위치, converter, binding, error contract |
| Lab 3 | @Transactional 적용 실패와 rollback 설계 | proxy, self-invocation, rollback rule |
| Lab 4 | MockMvc와 WebMvcTest 계약 테스트 작성 | slice 범위, security, validation, exception |
| Incident 1 | API가 302/403/CORS로 막힌 장애 | Security filter chain과 브라우저 보안 |
| Incident 2 | 결제 저장은 됐는데 재고 차감이 누락된 장애 | transaction boundary와 예외 처리 |
| Incident 3 | 조회 API 지연과 N+1 장애 | fetch plan, transaction, query 검증 |
| Incident 4 | 배포 후 Bean이 사라진 자동 설정 장애 | conditional, back-off, profile, conditions |
| Review 1 | 로그인/JWT/Cookie/CSRF 설계 리뷰 | 인증 상태 저장과 운반 수단 |
| Review 2 | 공통 로깅을 어디에 둘지 리뷰 | Filter, Interceptor, AOP 선택 |
| Review 3 | 파일 업로드 API 설계 리뷰 | multipart, 저장 경로, 정적 서빙, 테스트 |
| Final | 신규 주문 API 운영 준비 종합 훈련 | 전체 요청-검증-보안-트랜잭션-테스트 흐름 |
Part 1. 실습 랩
Lab 1. Controller에 도착하지 않는 요청 추적표 만들기
상황
GET /api/orders/42 요청이 Controller breakpoint에 걸리지 않는다. 개발자는 Controller mapping이 잘못됐다고 생각하지만, 실제로는 요청이 Controller 앞에서 멈췄을 가능성이 있다.
제공된 Evidence
Request
GET /api/orders/42
Origin: http://localhost:3000
Authorization: Bearer eyJ...
Response
HTTP/1.1 403
Body: {"error":"Forbidden"}
Application log
SecurityFilterChain matched request GET /api/orders/42
AuthorizationFilter denied access
OrderController log 없음해야 할 일
- 요청이 멈춘 위치를 3단계 이상으로 좁히는 추적표를 작성한다.
- 401, 403, 302, CORS, 404, 405가 각각 어느 경계에서 의심되는지 분류한다.
- Controller 코드를 보기 전에 확인할 로그와 설정을 적는다.
먼저 써보기
내 답안
여기에 직접 작성한다.
힌트
- Controller breakpoint가 안 걸렸다는 사실 자체가 evidence다.
- 먼저 Servlet FilterChain, Spring Security FilterChain, DispatcherServlet, HandlerMapping의 순서를 떠올린다.
403은 보통 인증 여부보다 권한 판단 쪽을 먼저 본다.
해설
좋은 답안은 Controller mapping부터 고치지 않는다. 먼저 요청이 SecurityFilterChain에 매칭되었고 AuthorizationFilter에서 거절되었다는 증거를 잡는다. 그 다음 현재 Authentication이 있는지, authority가 어떤 문자열인지, authorizeHttpRequests의 matcher 순서가 어떤지 본다.
401은 인증 정보 없음 또는 인증 실패, 403은 인증은 되었지만 권한 부족, 302는 API 요청이 form login으로 redirect되는 흐름, CORS는 브라우저 preflight와 security/cors 설정, 404/405는 Security를 통과한 뒤 MVC mapping 또는 HTTP method 문제로 나누어 본다.
연결 문서
- Spring Web-Back 실전 가이드북: Case 4와 Case 9에서 요청 위치 추적 순서를 확인한다.
- Security FilterChain과 Controller 이전 흐름: Controller 이전 보안 흐름을 확인한다.
- AuthorizationFilter와 권한 규칙: 401과 403, matcher 순서를 확인한다.
- Servlet Filter와 Security FilterChain: Filter 중단과
chain.doFilter()의미를 확인한다. - DispatcherServlet과 Front Controller: Security를 통과한 뒤 MVC 입구를 확인한다.
가져갈 판단 기준
Controller가 조용하면 Controller 앞의 경계부터 본다.
Lab 2. JSON, form, query binding과 validation 에러 응답 설계하기
상황
회원 가입 API와 HTML form 화면이 같은 DTO 이름을 공유하고 있다. JSON API에서는 415가 나고, form 화면에서는 검증 실패 메시지가 화면에 남지 않는다.
제공된 Evidence
@PostMapping("/api/users")
public ResponseEntity<?> create(@ModelAttribute @Valid SignupRequest request) {
userService.create(request);
return ResponseEntity.ok().build();
}
@PostMapping("/users")
public String createForm(@RequestBody @Valid SignupRequest request, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "users/new";
}
userService.create(request);
return "redirect:/users";
}API request
Content-Type: application/json
{"email":"bad", "password":""}
Observed
API: 415 Unsupported Media Type 또는 DTO 값 비어 있음
Form: 검증 실패 시 예외 처리로 이동해야 할 일
- API와 HTML form의 입력 바인딩 방식을 분리해 고친다.
- 검증 실패가 API에서는 어떤 예외 응답으로, HTML form에서는 어떤 화면 흐름으로 가야 하는지 쓴다.
Content-Type,Accept,@RequestBody,@ModelAttribute,BindingResult의 역할을 표로 정리한다.
먼저 써보기
내 답안
여기에 직접 작성한다.
힌트
- JSON body는
HttpMessageConverter가 읽는다. - HTML form은 request parameter 바인딩으로 보는 편이 자연스럽다.
BindingResult는 검증 대상 바로 뒤에 있어야 한다.
해설
API Controller는 @RequestBody @Valid SignupRequest request로 JSON body를 읽고, 실패는 @RestControllerAdvice에서 MethodArgumentNotValidException 등을 안정적인 오류 DTO로 바꾸는 편이 좋다. HTML form Controller는 @ModelAttribute @Valid SignupRequest request, BindingResult bindingResult 순서를 유지하고, 실패하면 같은 view로 돌아가 필드 오류를 보여준다.
Content-Type은 클라이언트가 보낸 body 형식이고, Accept는 원하는 응답 형식이다. JSON API와 HTML form은 같은 HTTP POST라도 binding 경로와 오류 처리 방식이 다르다.
연결 문서
- Spring Web-Back 실전 가이드북: Case 5, Case 6, Case 7을 함께 본다.
- RequestBody와 HttpMessageConverter: JSON body 변환을 확인한다.
- RequestParam PathVariable ModelAttribute: form/query/path 바인딩을 확인한다.
- @Valid @Validated와 검증 흐름: 바인딩 이후 검증 흐름을 확인한다.
- BindingResult와 에러 응답 설계: HTML form과 API 오류 응답 차이를 확인한다.
- API 에러 응답 설계: API 오류 계약을 정리한다.
가져갈 판단 기준
입력 오류를 보기 전에 값이 HTTP의 어느 칸에서 왔는지 먼저 분류한다.
Lab 3. @Transactional self-invocation과 rollback 판단표 만들기
상황
주문 생성 서비스에서 결제 기록 저장 후 재고 차감이 실패했는데 결제 기록이 rollback되지 않았다. 코드에는 @Transactional이 붙어 있어 팀원이 혼란스러워한다.
제공된 Evidence
@Service
public class OrderService {
public void order(OrderCommand command) {
savePayment(command);
decreaseStock(command);
}
@Transactional
void savePayment(OrderCommand command) {
paymentRepository.save(command.toPayment());
}
@Transactional
void decreaseStock(OrderCommand command) throws StockShortageException {
stockRepository.decrease(command.itemId(), command.quantity());
throw new StockShortageException();
}
}Observed
- payment row inserted
- stock update failed
- no rollback
- exception is checked exception해야 할 일
- 왜
@Transactional이 기대대로 적용되지 않았는지 원인 후보를 분류한다. - 유스케이스 경계로 적절한 메서드를 고르고, rollback 정책을 적는다.
- checked exception에서 rollback이 필요한 경우의 설정을 쓴다.
먼저 써보기
내 답안
여기에 직접 작성한다.
힌트
- 애너테이션 위치보다 호출이 Spring proxy를 통과했는지가 중요하다.
- 트랜잭션 경계는 Repository 작업 단위가 아니라 유스케이스 일관성 경계다.
- checked exception은 기본 rollback 규칙에서 주의가 필요하다.
해설
order()가 같은 클래스 내부에서 savePayment()와 decreaseStock()을 호출하면 self-invocation 문제가 생길 수 있다. 선언적 트랜잭션은 주로 proxy를 통과하는 외부 Bean 호출에서 적용된다. 또한 두 내부 메서드를 각각 트랜잭션 경계로 나누는 것보다, order()처럼 하나의 유스케이스를 나타내는 공개 메서드에 트랜잭션 경계를 두는 편이 자연스럽다.
StockShortageException이 checked exception이라면 기본 rollback 대상이 아닐 수 있으므로 rollbackFor 또는 예외 정책을 명확히 해야 한다. 예외를 잡아 삼키면 Spring이 rollback 판단을 할 수 없으므로, 실패 의도와 응답 의도를 분리해야 한다.
연결 문서
- Spring Web-Back 실전 가이드북: Case 11과 Case 12의 proxy와 transaction 판단을 확인한다.
- @Transactional 프록시와 적용 범위: self-invocation과 적용 범위를 확인한다.
- Propagation Isolation Rollback: rollback 정책을 확인한다.
- 읽기 전용 트랜잭션과 경계 설계: 유스케이스 경계를 확인한다.
- Spring AOP 프록시 동작 원리: proxy 호출과 내부 호출을 다시 본다.
가져갈 판단 기준
트랜잭션은 애너테이션 위치가 아니라 proxy를 통과한 유스케이스 경계에서 시작된다.
Lab 4. Controller 계약 테스트 작성 범위 정하기
상황
팀의 Controller 테스트가 느리고 불안정하다. 대부분 @SpringBootTest로 작성되어 있고, 실패가 Controller 문제인지 Service 문제인지 Security 문제인지 구분하기 어렵다.
제공된 Evidence
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired MockMvc mockMvc;
@Test
void create_user() throws Exception {
mockMvc.perform(post("/api/users")
.content("{\"email\":\"bad\"}"))
.andExpect(status().isOk());
}
}Missing checks
- Content-Type 없음
- validation 실패 기대 없음
- ControllerAdvice 포함 여부 불명확
- Security 사용자/CSRF 조건 없음
- Service 내부 분기까지 한 테스트에서 보려 함해야 할 일
- 이 테스트를
@WebMvcTest로 바꿀 때 포함해야 할 검증 항목을 적는다. - 정상 요청, validation 실패, 예외 변환, 권한 실패를 각각 어떤 assertion으로 볼지 쓴다.
- Service 로직과 Repository query는 어디에서 검증할지 분리한다.
먼저 써보기
내 답안
여기에 직접 작성한다.
힌트
- Controller 테스트는 Service 내부 분기가 아니라 HTTP 계약을 본다.
- JSON API는 status, content type, body shape, field error, security 조건을 함께 검증한다.
- 실제 DB 쿼리나 transaction은 다른 테스트 계층이 더 적합할 수 있다.
해설
@WebMvcTest(UserController.class)는 MVC 계층을 작게 띄우고, Service는 mock이나 fake로 대체한다. 정상 요청은 contentType(application/json), expected status, response DTO field를 확인한다. 검증 실패는 400과 field error 구조를 확인하고, 예외 변환은 @RestControllerAdvice가 실제 API 오류 DTO를 만드는지 본다. Security가 적용된 API라면 인증 사용자와 CSRF 의도를 테스트에 명시한다.
Service 분기는 단위 테스트로, Repository query는 @DataJpaTest 또는 필요하면 Testcontainers로 검증한다. 전체 wiring과 보안 필터, transaction까지 함께 보는 대표 흐름만 @SpringBootTest로 남긴다.
연결 문서
- Spring Web-Back 실전 가이드북: Case 14와 Case 18의 테스트 판단을 확인한다.
- WebMvcTest MockMvc와 Controller 테스트: MVC slice 테스트 기준을 확인한다.
- 테스트 전략과 Spring TestContext: context caching과 테스트 계층을 확인한다.
- perform DispatcherServlet을 통한 요청 실행: MockMvc 실행 흐름을 확인한다.
- jsonPath JSON 응답 구조 및 리프 노드 값 추출: JSON 계약 검증을 확인한다.
가져갈 판단 기준
Controller 테스트는 HTTP 계약을 좁고 반복 가능하게 고정하는 테스트다.
Part 2. 장애 대응 훈련
Incident 1. 로그인 이후 API가 302, 403, CORS 오류로 흔들린다
Incident 상황
프론트엔드에서 로그인 후 /api/me를 호출하면 환경에 따라 302, 403, CORS 오류가 번갈아 나타난다. 백엔드 Controller에는 요청 로그가 거의 남지 않는다.
현재 보이는 증상
- local: CORS preflight 실패
- dev:
302 Location: /login - staging:
403 Forbidden - Controller breakpoint 미동작
로그/지표/코드 단서
OPTIONS /api/me -> 403
GET /api/me -> 302 /login
GET /api/me with Bearer token -> 403
Security filter chain: any request authenticatedhttp
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());5분 안에 확인할 것
- 요청이 Controller까지 도달했는가?
- preflight
OPTIONS요청이 허용되는가? - API인데 form login redirect가 켜져 있는가?
- Bearer token 인증 filter가 AuthorizationFilter보다 앞에 있는가?
- 실제 authority 문자열과
hasRole/hasAuthority기대값이 맞는가?
원인 후보 분류
- CORS 설정 누락 또는 Security CORS 통합 누락
- API 요청에 form login redirect가 적용됨
- JWT 인증 filter 위치 문제
ROLE_prefix mismatch- matcher 순서 문제
하면 안 되는 조치
- Controller mapping부터 바꾼다.
- CORS 오류를 없애려고 모든 origin과 method를 무조건 허용한다.
- 403을 보고 인증 filter를 삭제한다.
hasRole과 저장된 authority 문자열을 확인하지 않고 권한을 추가한다.
복구 절차
- Security debug 로그로 현재 요청이 어떤
SecurityFilterChain에 매칭되는지 확인한다. - preflight 요청 허용과 CORS 설정 위치를 확인한다.
- API 요청의 인증 실패 응답이 redirect가 아니라 401 JSON이 되도록 entry point 정책을 확인한다.
- JWT filter가 Authentication을 만든 뒤 AuthorizationFilter가 읽을 수 있는 위치인지 확인한다.
hasRole,hasAuthority, 저장 authority 문자열을 맞춘다.
완료 검증
OPTIONS /api/me가 기대한 CORS 응답을 반환한다.- 토큰 없음은 401, 권한 부족은 403, 유효 토큰은 200으로 구분된다.
- Controller log가 정상 요청에서만 남는다.
- 보안 실패 응답이 HTML redirect가 아니라 API 오류 계약을 따른다.
회고 질문
- API와 form login entry point를 분리했는가?
- CORS/CSRF/Security matcher 기준이 문서화되어 있는가?
- 401/403/302/CORS를 자동 테스트로 고정했는가?
연결 문서
- Spring Web-Back 실전 가이드북: Case 4, Case 8, Case 9를 다시 본다.
- Security FilterChain과 Controller 이전 흐름
- AuthorizationFilter와 권한 규칙
- Session JWT Cookie CSRF 설계
- 클라이언트 요청은 Controller로 바로 가지 않는다
가져갈 판단 기준
보안 장애는 Controller가 아니라 현재 인증 상태와 필터 체인의 결정으로 설명해야 한다.
Incident 2. 결제 기록은 남았는데 주문 생성은 실패했다
Incident 상황
주문 API에서 결제 기록은 DB에 남았지만 주문 상태는 FAILED이고 재고도 차감되지 않았다. 고객에게는 500 응답이 반환되었다.
현재 보이는 증상
paymentrow existsorderrow status failedstockunchanged- retry 시 duplicate payment 위험
로그/지표/코드 단서
OrderService.createOrder started
PaymentRepository.save success
StockClient timeout
OrderService caught exception: SocketTimeoutException
Response 500
Transaction committed@Transactional
public OrderResult createOrder(Command command) {
paymentRepository.save(command.payment());
try {
stockClient.decrease(command.itemId());
} catch (Exception e) {
log.warn("stock failed", e);
}
orderRepository.saveFailed(command.orderId());
return OrderResult.failed();
}5분 안에 확인할 것
- 외부 API 호출이 transaction 안에서 실행되는가?
- 예외를 잡은 뒤 rollback 의도가 사라졌는가?
- 결제 저장과 재고 차감이 반드시 같은 DB transaction 안에 있어야 하는가?
- retry나 duplicate 방어 키가 있는가?
- commit 이후 작업으로 분리할 수 있는가?
원인 후보 분류
- 예외 삼킴으로 인한 commit
- 외부 API 호출을 transaction 내부에 둔 설계 문제
- rollback 정책 누락
- idempotency key 부재
- 결제/재고/주문 상태의 일관성 경계 미정의
하면 안 되는 조치
- DB row만 수동 삭제하고 장애 종료한다.
- duplicate safety 없이 전체 요청을 재실행한다.
- timeout을 무작정 늘린다.
- 모든 메서드에
@Transactional을 추가한다.
복구 절차
- 영향 주문과 결제 row 범위를 조회한다.
- 중복 결제 가능성을 먼저 차단한다.
- 주문 상태, 결제 상태, 재고 상태의 source of truth를 정한다.
- 보상 처리 또는 재처리를 idempotency key 기준으로 수행한다.
- Service transaction 경계와 외부 호출 위치를 재설계한다.
완료 검증
- 영향 주문 count와 결제 count가 일치한다.
- 중복 결제 row가 없다.
- 실패 주문의 상태 전이가 기록되어 있다.
- 같은 요청 replay가 안전하다.
- transaction test와 failure path test가 추가되었다.
회고 질문
- 예외를 잡는 코드가 rollback 의도를 숨기고 있지 않은가?
- 외부 API 호출은 commit 전/후 어느 위치가 맞는가?
- 실패 상태와 재처리 계약이 API와 운영 문서에 남아 있는가?
연결 문서
- Spring Web-Back 실전 가이드북: Case 12와 Case 15를 확인한다.
- PlatformTransactionManager와 트랜잭션 추상화
- Propagation Isolation Rollback
- 읽기 전용 트랜잭션과 경계 설계
- 예외 처리 흐름과 우선순위
가져갈 판단 기준
예외를 삼키는 순간 transaction도 실패를 모를 수 있다.
Incident 3. 주문 목록 API가 갑자기 느려졌다
Incident 상황
배포 후 /api/orders 응답 시간이 300ms에서 4초로 증가했다. DB CPU가 올라가고, query log에는 비슷한 select가 반복된다.
현재 보이는 증상
- p95 latency 4s
- DB select count 급증
- API response size는 크게 변하지 않음
- 일부 요청에서 lazy loading 예외 발생
로그/지표/코드 단서
select * from orders limit 50;
select * from order_items where order_id = ?;
select * from order_items where order_id = ?;
select * from order_items where order_id = ?;
...@GetMapping("/api/orders")
public List<OrderResponse> orders() {
return orderRepository.findRecent()
.stream()
.map(OrderResponse::from)
.toList();
}5분 안에 확인할 것
- 같은 select가 parent row 수만큼 반복되는가?
- response DTO 변환 시 lazy association을 건드리는가?
- transaction 경계 밖에서 lazy loading이 일어나는가?
- fetch join, EntityGraph, projection 중 어떤 해결책이 적합한가?
- 테스트가 query count 또는 대표 fetch plan을 검증하는가?
원인 후보 분류
- N+1 query
- DTO 변환 시 lazy association 접근
- transaction boundary 밖 lazy loading
- projection 미사용
- 조회 목적에 맞지 않는 Repository method
하면 안 되는 조치
- 모든 association을 eager로 바꾼다.
- Open Session in View로 증상만 덮는다.
- Controller에서 Entity를 그대로 반환한다.
- query log 없이 감으로 index만 추가한다.
복구 절차
- query log와 실행 횟수로 N+1 여부를 확인한다.
- API 응답에 필요한 데이터 shape을 정의한다.
- fetch join, EntityGraph, projection 중 하나를 선택해 조회 의도를 코드에 드러낸다.
- transaction 경계와 DTO 변환 위치를 조정한다.
- Repository 테스트나 통합 테스트로 query 동작을 고정한다.
완료 검증
- 동일 API의 query count가 기대 범위로 줄었다.
- p95 latency가 회복되었다.
- lazy loading 예외가 재현되지 않는다.
- 응답 DTO가 Entity를 직접 노출하지 않는다.
- DataJpaTest 또는 통합 테스트가 조회 경로를 보호한다.
회고 질문
- Repository method 이름이나
@Query가 fetch 의도를 드러내는가? - 조회 API마다 필요한 데이터 shape이 문서화되어 있는가?
- N+1을 찾는 테스트 또는 로깅 기준이 있는가?
연결 문서
- Spring Web-Back 실전 가이드북: Case 13과 Case 14를 본다.
- Spring Data Repository와 쿼리 메서드
- JPA 영속성 컨텍스트와 N+1
- 읽기 전용 트랜잭션과 경계 설계
- SpringBootTest DataJpaTest Testcontainers
가져갈 판단 기준
N+1은 성능 문제이기 전에 조회 의도가 코드에 드러나지 않은 문제다.
Incident 4. 배포 후 자동 설정 Bean이 사라졌다
Incident 상황
새 설정 클래스를 추가한 뒤 배포했더니 기존에 자동 설정으로 만들어지던 ObjectMapper 커스터마이징과 일부 converter가 적용되지 않는다.
현재 보이는 증상
- 날짜 응답 형식이 바뀜
- JSON property naming이 바뀜
- 일부 테스트에서만 통과
- local과 prod profile 결과가 다름
로그/지표/코드 단서
Conditions report
JacksonAutoConfiguration matched
JacksonObjectMapperConfiguration did not match:
@ConditionalOnMissingBean(ObjectMapper) found user-defined bean 'objectMapper'@Configuration
class JsonConfig {
@Bean
ObjectMapper objectMapper() {
return new ObjectMapper();
}
}5분 안에 확인할 것
- 내가 직접 제공한 Bean 때문에 자동 설정이 back-off되었는가?
- property로 조정할 수 있는 일을 Bean override로 처리했는가?
- profile별로 같은 Bean이 다르게 등록되는가?
- conditions report의 negative match 이유를 읽었는가?
원인 후보 분류
- 사용자 Bean 등록으로 인한 back-off
@ConfigurationProperties대신 직접 Bean 생성- profile 조건 차이
- test slice에서 다른 ObjectMapper 사용
- 설정 source 우선순위 혼동
하면 안 되는 조치
- 자동 설정 전체를 exclude한다.
- 모든 테스트에 같은 ObjectMapper Bean을 복사한다.
- conditions report를 보지 않고 Bean 이름만 바꾼다.
- 운영 profile 값을 확인하지 않고 local 기준으로 결론낸다.
복구 절차
- conditions report에서 관련 auto configuration과 negative match 이유를 확인한다.
- 직접 Bean이 필요한지 property나 customizer로 충분한지 판단한다.
- 필요하면 Boot가 제공하는 builder/customizer 확장 지점을 사용한다.
- profile별 실제 Environment 값을 확인한다.
- JSON 계약 테스트로 날짜 형식과 naming 전략을 고정한다.
완료 검증
- conditions report에서 의도한 설정 경로가 확인된다.
- local/test/prod의 JSON 계약이 일관된다.
- 사용자 Bean 등록이 필요한 이유가 문서화되어 있다.
- slice test와 통합 test에서 같은 API 계약이 검증된다.
회고 질문
- back-off 지점을 변경하기 전에 property 조정 가능성을 봤는가?
- 자동 설정 Bean을 대체할 때 영향 범위를 리뷰했는가?
- conditions report를 장애 runbook에 포함했는가?
연결 문서
- Spring Web-Back 실전 가이드북: Case 3을 다시 푼다.
- Starter와 Auto Configuration 동작
- Conditional과 Back Off
- Configuration Properties와 Profile
- Actuator Conditions와 자동 설정 디버깅
가져갈 판단 기준
자동 설정 문제는 Bean 이름보다 조건 평가 결과로 설명한다.
Part 3. 설계 리뷰 훈련
Review 1. JWT를 Cookie에 담는 로그인 설계 리뷰
제안된 설계안
로그인 성공 시 access token과 refresh token을 모두 HttpOnly Cookie로 내려준다. API 서버는 JWT를 검증하므로 stateless하고, CSRF는 JWT를 쓰므로 비활성화한다. 로그아웃은 프론트에서 cookie를 지우는 것으로 처리한다.
겉보기 장점
- 브라우저 저장소에 token을 직접 저장하지 않는다.
- 서버 session 저장소가 필요 없어 보인다.
- API 요청에 cookie가 자동으로 붙어 구현이 단순해 보인다.
숨은 리스크
- Cookie 자동 전송 구조는 CSRF 판단이 필요하다.
- JWT access token은 만료 전 강제 무효화가 어렵다.
- refresh token 저장소, rotation, 탈취 대응이 빠져 있다.
- 로그아웃을 client cookie 삭제만으로 처리하면 이미 탈취된 token 대응이 약하다.
리뷰 질문
- 인증 상태를 서버가 어디까지 관리하는가?
- access token 만료 전 강제 무효화가 필요한 요구가 있는가?
- Cookie를 쓰는 요청에서 CSRF 방어를 어떻게 할 것인가?
- refresh token 저장소와 tokenVersion 또는 denylist 전략이 있는가?
- 401, 403, refresh 실패 응답 계약이 있는가?
빠진 계약
- access token TTL
- refresh token 저장 위치와 rotation 정책
- logout/탈취/비밀번호 변경 시 무효화 전략
- CSRF token 전달 방식
- 권한 문자열과
hasRole/hasAuthority규칙 - API error response 계약
개선된 설계 방향
JWT를 쓰더라도 “완전 무상태”라고 단정하지 않는다. access token 검증은 stateless하게 할 수 있지만, refresh token과 강제 만료 요구는 서버 상태가 필요할 수 있다. Cookie 자동 전송을 선택하면 CSRF 대응을 함께 설계한다.
승인 조건
- Cookie 운반 방식과 CSRF 정책이 명시되어 있다.
- refresh token 저장소와 rotation 정책이 있다.
- logout과 탈취 대응이 서버 측 전략까지 포함한다.
- Security filter, authentication, authorization test가 있다.
연결 문서
- Spring Web-Back 실전 가이드북: Case 8과 Case 9를 본다.
- AuthenticationManager Provider SecurityContext
- Session JWT Cookie CSRF 설계
- 세션, JWT, 토큰, 쿠키 역할 구분
- API 에러 응답 설계
가져갈 판단 기준
JWT와 Cookie는 같은 층위의 선택지가 아니라 값의 형식과 운반 수단이다.
Review 2. 모든 공통 로깅을 AOP 하나로 처리하는 설계 리뷰
제안된 설계안
CommonLoggingAspect 하나를 만들고 Controller, Service, Repository의 모든 메서드 실행 전후에 request id, user id, payload, latency, error를 남긴다. Filter와 Interceptor는 쓰지 않는다.
겉보기 장점
- 파일 하나에 공통 로깅이 모인다.
- 메서드 실행 시간 측정이 쉽다.
- 비즈니스 코드에 로그가 줄어든다.
숨은 리스크
- raw request/response, CORS, status, body wrapper는 AOP보다 Filter가 직접적이다.
- HandlerMethod나 path pattern 기반 정보는 Interceptor가 더 자연스럽다.
- Service method timing과 audit log는 AOP가 적합하지만 범위를 넓히면 디버깅이 어려워진다.
- payload 로깅은 개인정보와 보안 위험이 있다.
리뷰 질문
- 필요한 정보는 Servlet request인가, MVC Handler인가, Bean method인가?
- 요청이 Controller에 도착하지 않는 보안 실패도 로깅해야 하는가?
- user id는 SecurityContext가 만들어진 뒤에 필요한가?
- payload를 어디까지 마스킹할 것인가?
- AOP pointcut이 너무 넓지 않은가?
빠진 계약
- request id 생성 위치
- 보안 실패 access log 기준
- handler 기반 로그와 service timing 로그의 분리
- 개인정보 마스킹 규칙
- AOP pointcut 범위와 예외 조건
- log field naming과 trace id 계약
개선된 설계 방향
access log와 request id는 Filter에서, HandlerMethod 기반 로그는 Interceptor에서, Service timing이나 audit policy는 AOP에서 처리한다. 같은 로그를 세 계층에서 중복 남기지 않도록 목적을 나눈다.
승인 조건
- Filter, Interceptor, AOP 각각의 책임이 문서화되어 있다.
- 보안 실패와 Controller 성공 요청이 모두 추적 가능하다.
- 개인정보 마스킹 기준이 있다.
- AOP pointcut이 좁고 테스트로 검증된다.
연결 문서
- Spring Web-Back 실전 가이드북: Case 10과 Case 16을 확인한다.
- Filter, Interceptor, AOP 압축 정리
- 요청 처리 체인에서 위치 비교
- HandlerInterceptor와 MVC 경계
- Pointcut 작성 가이드
가져갈 판단 기준
공통 처리의 위치는 기술 취향이 아니라 필요한 컨텍스트가 결정한다.
Review 3. Multipart 파일 업로드 API 설계 리뷰
제안된 설계안
이미지 업로드 API는 MultipartFile을 받아 원본 파일명으로 서버 디렉터리에 저장한다. 저장 경로는 /uploads/{filename}으로 노출하고, 클라이언트는 FormData로 파일을 올린다. 테스트는 정상 업로드 한 개만 둔다.
겉보기 장점
- 구현이 빠르다.
- 브라우저 FormData와 Spring
MultipartFile이 잘 맞는다. - URL로 바로 파일을 확인할 수 있다.
숨은 리스크
- 파일명 충돌과 경로 조작 위험이 있다.
- multipart 크기 제한과 임시 저장소 정책이 없다.
- 정적 리소스 서빙 경로와 물리 저장 경로의 계약이 불명확하다.
- 파일 다운로드 UX, cache header, 권한 검사가 빠질 수 있다.
- Base64 inline 대안과 multipart 선택 기준이 없다.
리뷰 질문
- 파일명을 서버에서 새로 생성하는가?
- 허용 확장자와 content type 검증이 있는가?
- multipart max file size와 임시 저장소 기준이 있는가?
- 저장 경로와 공개 URL mapping이 분리되어 있는가?
- 파일 업로드 실패, 초과 용량, 다운로드 권한 실패 테스트가 있는가?
빠진 계약
- 파일 크기 제한
- 임시 저장소와 최종 저장소
- 파일명 생성 규칙
- content type/extension 검증
- ResourceHandler mapping
- 다운로드 응답 header
- MockMvc multipart test
개선된 설계 방향
파일 업로드는 MultipartFile 수신만으로 끝내지 않는다. 서버 생성 파일명, 저장소 경계, ResourceHandler mapping, 다운로드 정책, 크기 제한, 예외 응답, 테스트를 함께 설계한다. Base64는 요청 수 감소와 payload 증가의 trade-off가 있는 선택지로만 검토한다.
승인 조건
- 저장 파일명이 사용자 입력에 직접 의존하지 않는다.
- multipart 설정과 초과 용량 오류 응답이 있다.
- 정적 서빙 경로와 물리 경로 mapping이 명확하다.
- 정상/실패/초과 용량/권한 테스트가 있다.
연결 문서
- Spring Web-Back 실전 가이드북: Case 19를 본다.
- MultipartFile 멀티파트 요청 캡슐화 인터페이스
- application.properties 임계값 및 임시 저장소 설정
- WebMvcConfigurer 가상 경로와 물리 경로의 매핑 ResourceHandler
- 트레이드오프 HTTP 요청 수 감소와 데이터 크기 증가
가져갈 판단 기준
파일 업로드는 들어오는 길, 저장되는 곳, 다시 나가는 길을 함께 설계해야 한다.
최종 종합 훈련
신규 주문 API를 운영 배포 전 리뷰한다. 요구사항은 다음과 같다.
POST /api/orders
- Authorization Bearer token 필요
- JSON body로 주문 요청 수신
- 요청 검증 실패 시 field error 반환
- 주문 생성과 결제 기록 저장은 transaction으로 처리
- 결제 승인 후 알림 발송
- 주문 상세 조회에서 item 목록 포함
- Controller 테스트와 Repository 테스트 필요해야 할 일
- 요청이 Controller까지 도달하기 전 확인할 Security 항목을 쓴다.
@RequestBody, validation, error response 계약을 설계한다.- transaction 경계와 외부 알림 발송 위치를 정한다.
- 조회 API의 N+1 방지 전략을 쓴다.
- 테스트를 unit, WebMvcTest, DataJpaTest, SpringBootTest 중 어디에 둘지 나눈다.
- 배포 전 운영 체크리스트를 작성한다.
먼저 써보기
내 종합 답안
여기에 직접 작성한다.
해설
좋은 설계는 Security FilterChain에서 Bearer token을 검증하고, AuthorizationFilter가 권한을 판단한 뒤 Controller로 보낸다. Controller는 JSON body를 @RequestBody로 받고 DTO validation을 수행한다. 검증 실패와 비즈니스 실패는 공통 API 오류 응답으로 변환한다.
트랜잭션은 주문 생성과 결제 기록 저장처럼 DB 일관성이 필요한 Service 유스케이스 경계에 둔다. 외부 알림 발송은 DB transaction 내부에서 오래 붙잡지 않도록 commit 이후 이벤트나 별도 후속 처리로 분리하는 편이 안전하다. 주문 상세 조회는 필요한 fetch plan을 Repository에 드러내고, N+1이 생기지 않게 projection, fetch join, EntityGraph를 검토한다.
테스트는 Controller HTTP 계약은 WebMvcTest, 도메인 분기는 단위 테스트, Repository query와 fetch plan은 DataJpaTest, 보안과 transaction이 함께 맞물리는 대표 happy path와 failure path는 SpringBootTest로 나눈다.
연결 문서
- Spring Web-Back 실전 가이드북: 전체 Case를 상황별 지도처럼 사용한다.
- Spring Security 압축 정리
- HTTP 요청 응답 바인딩 압축 정리
- Validation 압축 정리
- Exception Handling 압축 정리
- Transaction 압축 정리
- Data Access 압축 정리
- Testing 압축 정리
가져갈 판단 기준
운영 가능한 API는 성공 흐름이 아니라 실패 위치와 검증 방법까지 설명할 수 있어야 한다.
훈련 커버리지 지도
반복 기록표
| 날짜 | 훈련 | 내 판단의 약점 | 다시 볼 문서 | 다음 회독 기준 |
|---|---|---|---|---|
| Lab 1 | ||||
| Lab 2 | ||||
| Lab 3 | ||||
| Lab 4 | ||||
| Incident 1 | ||||
| Incident 2 | ||||
| Incident 3 | ||||
| Incident 4 | ||||
| Review 1 | ||||
| Review 2 | ||||
| Review 3 | ||||
| Final |