Spring Web-Back 실전 훈련북

이 문서의 사용법

이 문서는 Spring Web-Back 문서들의 요약본이 아니다. 이미 정리된 개념 문서와 실전 가이드북을 바탕으로, 직접 판단을 써보고 해설과 비교하는 훈련 문서다.

읽기만 하면 효과가 약하다. 각 Lab, Incident, Review에서 먼저 써보기 또는 질문 항목을 실제로 채운 뒤 해설을 본다. 해설을 본 뒤에는 연결 문서로 돌아가서 내가 놓친 판단 기준을 다시 확인한다.

훈련 방식

  1. 상황을 먼저 읽는다.
  2. 제공된 evidence만 보고 원인이나 설계 결론을 바로 단정하지 않는다.
  3. 빈 답안 공간에 내 판단 순서, 확인할 증거, 선택한 조치를 쓴다.
  4. 힌트를 보고 답을 보완한다.
  5. 해설과 비교한다.
  6. 연결 문서로 돌아가 내가 약한 개념을 확인한다.
  7. 반복 기록표에 틀린 이유와 다음 회독 기준을 남긴다.

반복 학습 루프

  • 1회독: 답을 맞히려 하지 말고, 어떤 경계를 먼저 봐야 하는지 적는다.
  • 2회독: 증거를 보고 원인 후보를 2~4개로 분류한다.
  • 3회독: 하면 안 되는 조치와 완료 검증 기준까지 쓴다.
  • 4회독: 연결 문서를 보지 않고 판단 기준 한 문장만으로 설명한다.

훈련 목록

구분훈련핵심 판단
Lab 1Controller에 도착하지 않는 요청 추적Filter, Security, DispatcherServlet 경계
Lab 2JSON/form binding과 validation 응답 설계입력 위치, converter, binding, error contract
Lab 3@Transactional 적용 실패와 rollback 설계proxy, self-invocation, rollback rule
Lab 4MockMvc와 WebMvcTest 계약 테스트 작성slice 범위, security, validation, exception
Incident 1API가 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 문제로 나누어 본다.

연결 문서

가져갈 판단 기준

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 경로와 오류 처리 방식이 다르다.

연결 문서

가져갈 판단 기준

입력 오류를 보기 전에 값이 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 판단을 할 수 없으므로, 실패 의도와 응답 의도를 분리해야 한다.

연결 문서

가져갈 판단 기준

트랜잭션은 애너테이션 위치가 아니라 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로 남긴다.

연결 문서

가져갈 판단 기준

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 authenticated
http
    .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 문자열을 확인하지 않고 권한을 추가한다.

복구 절차

  1. Security debug 로그로 현재 요청이 어떤 SecurityFilterChain에 매칭되는지 확인한다.
  2. preflight 요청 허용과 CORS 설정 위치를 확인한다.
  3. API 요청의 인증 실패 응답이 redirect가 아니라 401 JSON이 되도록 entry point 정책을 확인한다.
  4. JWT filter가 Authentication을 만든 뒤 AuthorizationFilter가 읽을 수 있는 위치인지 확인한다.
  5. 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를 자동 테스트로 고정했는가?

연결 문서

가져갈 판단 기준

보안 장애는 Controller가 아니라 현재 인증 상태와 필터 체인의 결정으로 설명해야 한다.

Incident 2. 결제 기록은 남았는데 주문 생성은 실패했다

Incident 상황

주문 API에서 결제 기록은 DB에 남았지만 주문 상태는 FAILED이고 재고도 차감되지 않았다. 고객에게는 500 응답이 반환되었다.

현재 보이는 증상

  • payment row exists
  • order row status failed
  • stock unchanged
  • 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을 추가한다.

복구 절차

  1. 영향 주문과 결제 row 범위를 조회한다.
  2. 중복 결제 가능성을 먼저 차단한다.
  3. 주문 상태, 결제 상태, 재고 상태의 source of truth를 정한다.
  4. 보상 처리 또는 재처리를 idempotency key 기준으로 수행한다.
  5. Service transaction 경계와 외부 호출 위치를 재설계한다.

완료 검증

  • 영향 주문 count와 결제 count가 일치한다.
  • 중복 결제 row가 없다.
  • 실패 주문의 상태 전이가 기록되어 있다.
  • 같은 요청 replay가 안전하다.
  • transaction test와 failure path test가 추가되었다.

회고 질문

  • 예외를 잡는 코드가 rollback 의도를 숨기고 있지 않은가?
  • 외부 API 호출은 commit 전/후 어느 위치가 맞는가?
  • 실패 상태와 재처리 계약이 API와 운영 문서에 남아 있는가?

연결 문서

가져갈 판단 기준

예외를 삼키는 순간 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만 추가한다.

복구 절차

  1. query log와 실행 횟수로 N+1 여부를 확인한다.
  2. API 응답에 필요한 데이터 shape을 정의한다.
  3. fetch join, EntityGraph, projection 중 하나를 선택해 조회 의도를 코드에 드러낸다.
  4. transaction 경계와 DTO 변환 위치를 조정한다.
  5. Repository 테스트나 통합 테스트로 query 동작을 고정한다.

완료 검증

  • 동일 API의 query count가 기대 범위로 줄었다.
  • p95 latency가 회복되었다.
  • lazy loading 예외가 재현되지 않는다.
  • 응답 DTO가 Entity를 직접 노출하지 않는다.
  • DataJpaTest 또는 통합 테스트가 조회 경로를 보호한다.

회고 질문

  • Repository method 이름이나 @Query가 fetch 의도를 드러내는가?
  • 조회 API마다 필요한 데이터 shape이 문서화되어 있는가?
  • N+1을 찾는 테스트 또는 로깅 기준이 있는가?

연결 문서

가져갈 판단 기준

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 기준으로 결론낸다.

복구 절차

  1. conditions report에서 관련 auto configuration과 negative match 이유를 확인한다.
  2. 직접 Bean이 필요한지 property나 customizer로 충분한지 판단한다.
  3. 필요하면 Boot가 제공하는 builder/customizer 확장 지점을 사용한다.
  4. profile별 실제 Environment 값을 확인한다.
  5. JSON 계약 테스트로 날짜 형식과 naming 전략을 고정한다.

완료 검증

  • conditions report에서 의도한 설정 경로가 확인된다.
  • local/test/prod의 JSON 계약이 일관된다.
  • 사용자 Bean 등록이 필요한 이유가 문서화되어 있다.
  • slice test와 통합 test에서 같은 API 계약이 검증된다.

회고 질문

  • back-off 지점을 변경하기 전에 property 조정 가능성을 봤는가?
  • 자동 설정 Bean을 대체할 때 영향 범위를 리뷰했는가?
  • conditions report를 장애 runbook에 포함했는가?

연결 문서

가져갈 판단 기준

자동 설정 문제는 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가 있다.

연결 문서

가져갈 판단 기준

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이 좁고 테스트로 검증된다.

연결 문서

가져갈 판단 기준

공통 처리의 위치는 기술 취향이 아니라 필요한 컨텍스트가 결정한다.

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이 명확하다.
  • 정상/실패/초과 용량/권한 테스트가 있다.

연결 문서

가져갈 판단 기준

파일 업로드는 들어오는 길, 저장되는 곳, 다시 나가는 길을 함께 설계해야 한다.

최종 종합 훈련

신규 주문 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로 나눈다.

연결 문서

가져갈 판단 기준

운영 가능한 API는 성공 흐름이 아니라 실패 위치와 검증 방법까지 설명할 수 있어야 한다.

훈련 커버리지 지도

기존 문서연결된 훈련역할
00. 학습 문서 개선 기준.mdFinal문서 구조와 반복 회독 기준
00. AOP 압축 정리.mdLab 3, Review 2AOP 전체 회수
01. AOP 기본 개념.mdReview 2AOP 적용 판단
02. AOP 주요 용어.mdLab 3, Review 2Proxy와 Aspect 용어
03. Spring AOP 프록시 동작 원리.mdLab 3proxy와 내부 호출
04. Pointcut 작성 가이드.mdReview 2pointcut 범위
05. Advice 타입 비교.mdReview 2advice 선택
00. 통합 목차.mdLab 1, Lab 2, Lab 4, Review 3레거시 MVC 진입점
01. 횡단 관심사의 모듈화Review 2공통 관심사 분리
02. Target Aspect Advice Pointcut JoinPointReview 2AOP 구성 요소
03. 비즈니스 로직과 시스템 서비스 분리Review 2비즈니스 로직 분리
01. Configuration 프록시 객체 생성을 통한 싱글톤 유지 보장Incident 4Configuration proxy
02. CGLIB 설정 클래스를 상속받아 빈 중복 생성 방지Incident 4CGLIB 설정 클래스
03. Bean 메서드 호출 시 기존 등록 빈 반환Incident 4Bean method 호출
01. Async 별도 ThreadPool 생성이 필요 없는 비동기 처리Finalcommit 이후 작업 판단
02. Cacheable 복잡한 코드 없는 메모리 기반 데이터 캐싱Review 2공통 정책 적용
03. EnableAsync EnableCaching 필수 설정Final활성화 설정
01. 프록시 메커니즘 외부 호출 기준Lab 3proxy 외부 호출
02. 내부 호출 문제 Self invocationLab 3내부 호출 문제
03. 구조 설계 변경을 통한 프록시 외부 노출Lab 3구조 변경
01. 모든 HTTP 요청의 중앙 수신 및 분산 처리Lab 1MVC 중앙 입구
02. 공통 작업 통합 관리 및 컨트롤러 구현 단순화Lab 1Controller 단순화
01. HandlerMapping URL 기반 핸들러 검색Lab 1, Lab 2mapping 검색
02. HandlerAdapter 핸들러 실행 표준 인터페이스Lab 2argument resolver
03. ViewResolver 논리적 뷰 이름을 물리 경로로 변환Lab 2view와 body 구분
01. 클라이언트 요청 수신 및 핸들러 결정Lab 1요청 수신
02. 핸들러 어댑터를 통한 컨트롤러 로직 실행Lab 1, Lab 2Controller 실행
03. 서비스 데이터 처리 및 Model 데이터 바인딩Lab 2Model과 Service
04. 뷰 리졸버를 통한 화면 렌더링 및 응답Lab 2화면 응답
01. RequestMapping URL 경로와 메서드 연결Lab 1, Lab 2URL mapping
02. HTTP Method 제약 GetMapping PostMappingLab 1HTTP method
01. RequestParam 쿼리 파라미터 및 폼 데이터 매핑Lab 2query/form binding
02. Primitive Wrapper 타입 자동 변환Lab 2type conversion
03. Model 객체를 활용한 Request Scope 데이터 공유Lab 2form 오류 화면
01. Forward 논리적 뷰 경로 문자열 리턴Lab 2view forward
02. Redirect redirect 접두어를 이용한 클라이언트 재요청Lab 2redirect
03. ResponseBody JSON 데이터 직접 전송 JacksonLab 2JSON response
01. static 정적 웹 리소스 CSS JS ImageReview 3정적 리소스
02. templates 템플릿 엔진 파일 Thymeleaf MustacheLab 2template
01. tomcat embed jasper 라이브러리 수동 추가 필요Lab 2JSP dependency
02. application properties prefix suffix 설정Lab 2JSP resolver
01. MockMvc WAS 미가동 가상 Servlet 환경Lab 4MockMvc 환경
02. WebMvcTest MVC 관련 설정만 로드하는 슬라이스 테스트Lab 4WebMvcTest 범위
03. MockBean 컨텍스트 내 실제 빈을 가짜 객체로 대체Lab 4mock bean
01. RequestBuilder 가상 요청 구성Lab 4request builder
02. perform DispatcherServlet을 통한 요청 실행Lab 4perform
03. ResultActions 실행 결과에 대한 체이닝 접근Lab 4ResultActions
01. ResultMatcher 상태 코드 뷰 모델 데이터 검증Lab 4상태와 model 검증
02. jsonPath JSON 응답 구조 및 리프 노드 값 추출Lab 4JSON 검증
03. Content Type 및 인코딩 상태 확인Lab 4content type
01. Servlet API HttpServletRequest Response 직접 제어Lab 2Servlet API
02. ArgumentResolver 파라미터 타입별 자동 매핑 메커니즘Lab 2ArgumentResolver
01. RequestParam 쿼리 파라미터 및 기본 타입 매핑Lab 2RequestParam
02. ModelAttribute DTO 생성 및 Setter 기반 프로퍼티 주입Lab 2ModelAttribute
03. CookieValue HTTP 쿠키 정보 자동 추출 및 형변환Review 1CookieValue
01. RedirectAttributes 리다이렉트 시 데이터 소실 방지Lab 2redirect data
02. addAttribute URL 쿼리 스트링을 통한 데이터 노출Lab 2query 노출
03. addFlashAttribute 세션 기반 일회성 데이터 Flash ScopeLab 2flash attribute
01. ControllerAdvice 컨트롤러 계층 공통 관심사 분리Lab 2, Lab 4ControllerAdvice
02. ExceptionHandler 특정 예외 발생 시 핸들링 로직 정의Lab 2, Lab 4ExceptionHandler
01. BasicErrorController 미처리 예외에 대한 최종 처리기Lab 2기본 에러 처리
02. Whitelabel Error Page 기본 뷰 매핑 및 에러 속성 제공Lab 2HTML error
01. 우선순위 Advice 기반 처리와 부트 기본 처리Lab 4handler 우선순위
02. 응답 분기 HTML 요청과 JSON AJAX 요청의 분리 처리Lab 2HTML/JSON 분기
03. ErrorAttributes 에러 응답 필드 커스터마이징Lab 2error attributes
01. MultipartFile 멀티파트 요청 캡슐화 인터페이스Review 3MultipartFile
02. application.properties 임계값 및 임시 저장소 설정Review 3multipart 설정
03. transferTo 메모리 임시파일의 실제 물리 경로 저장Review 3파일 저장
01. WebMvcConfigurer 가상 경로와 물리 경로의 매핑 ResourceHandlerReview 3ResourceHandler
02. HTML5 download 속성 브라우저 기본 다운로드 동작 제어Review 3다운로드
01. Base64 Encoding 바이너리의 텍스트화 및 인라인 삽입Review 3Base64
02. FormData API Ajax 기반 비동기 멀티파트 전송 구현Review 3FormData
03. 트레이드오프 HTTP 요청 수 감소와 데이터 크기 증가Review 3Base64 trade-off
00. 순서.mdReview 1Security 학습 순서
0. 최종목표 문서.mdReview 1MVC 로그인 전환
1. FilterChain 문서.mdIncident 1Controller 이전 흐름
세션 JWT 쿠키 문서.mdReview 1세션 JWT 쿠키
00. IoC DI Bean 압축 정리.mdIncident 4IoC/DI 전체
01. IoC Container와 Bean.mdIncident 4Bean 경계
02. 의존성 주입과 생성자 주입.mdIncident 4DI 실패
03. Bean 등록 방식과 Component Scan.mdIncident 4Bean 등록
04. Configuration과 Bean 메서드 프록시.mdIncident 4설정 프록시
00. 압축 정리.mdIncident 4Container 전체
01. Container 부팅과 BeanDefinition.mdIncident 4BeanDefinition
02. Bean 생성 초기화 소멸 흐름.mdIncident 4Bean lifecycle
03. BeanPostProcessor와 확장 포인트.mdLab 3, Incident 4BPP와 proxy
04. Scope와 Lazy 초기화.mdIncident 4Scope와 lazy
00. 압축 정리.mdLab 1MVC 흐름 전체
01. DispatcherServlet과 Front Controller.mdLab 1DispatcherServlet
02. HandlerMapping과 HandlerAdapter.mdLab 1, Lab 2HandlerMapping/Adapter
03. Controller 실행과 Request Response Cycle.mdLab 1, Lab 2Request cycle
04. ViewResolver와 MessageConverter.mdLab 2View와 converter
00. 압축 정리.mdLab 2Binding 전체
01. RequestMapping과 Handler Method.mdLab 1, Lab 2mapping 조건
02. RequestParam PathVariable ModelAttribute.mdLab 2parameter binding
03. RequestBody와 HttpMessageConverter.mdLab 2JSON body
04. ResponseBody와 ResponseEntity.mdLab 2response contract
00. Validation 압축 정리.mdLab 2, FinalValidation 전체
01. Bean Validation과 제약 조건.mdLab 2제약 조건
02. @Valid @Validated와 검증 흐름.mdLab 2검증 흐름
03. BindingResult와 에러 응답 설계.mdLab 2, Lab 4오류 응답
04. Method Validation과 계층별 검증.mdLab 3, Finalmethod validation
00. 압축 정리.mdLab 2, FinalException 전체
01. 예외 처리 흐름과 우선순위.mdIncident 2예외 흐름
02. ExceptionHandler와 ControllerAdvice.mdLab 2, Lab 4Advice
03. BasicErrorController와 ErrorAttributes.mdLab 2Boot error
04. API 에러 응답 설계.mdLab 2, Review 1API 오류 계약
00. Transaction 압축 정리.mdLab 3, Incident 2, FinalTransaction 전체
01. PlatformTransactionManager와 트랜잭션 추상화.mdIncident 2트랜잭션 추상화
02. @Transactional 프록시와 적용 범위.mdLab 3적용 범위
03. Propagation Isolation Rollback.mdLab 3, Incident 2rollback
04. 읽기 전용 트랜잭션과 경계 설계.mdIncident 2, Incident 3경계 설계
00. Data Access 압축 정리.mdIncident 3, FinalData Access 전체
01. DataSource Connection JdbcTemplate.mdIncident 3Connection/JDBC
02. DataAccessException과 예외 변환.mdIncident 2, Incident 3예외 변환
03. Spring Data Repository와 쿼리 메서드.mdIncident 3Repository
04. JPA 영속성 컨텍스트와 N+1.mdIncident 3JPA/N+1
00. 압축 정리.mdIncident 4Auto Config 전체
01. Starter와 Auto Configuration 동작.mdIncident 4Starter
02. Conditional과 Back Off.mdIncident 4Conditional/back-off
03. Configuration Properties와 Profile.mdIncident 4properties/profile
04. Actuator Conditions와 자동 설정 디버깅.mdIncident 4conditions debug
00. Spring Security 압축 정리.mdIncident 1, Review 1, FinalSecurity 전체
01. Security FilterChain과 Controller 이전 흐름.mdLab 1, Incident 1Security filter
02. AuthenticationManager Provider SecurityContext.mdReview 1Authentication
03. AuthorizationFilter와 권한 규칙.mdLab 1, Incident 1Authorization
04. Session JWT Cookie CSRF 설계.mdIncident 1, Review 1Session/JWT/CSRF
00. 압축 정리.mdReview 2위치 비교 전체
01. 요청 처리 체인에서 위치 비교.mdReview 2위치 비교
02. Servlet Filter와 Security FilterChain.mdLab 1, Incident 1, Review 2Filter
03. HandlerInterceptor와 MVC 경계.mdReview 2Interceptor
04. AOP 프록시와 선택 기준.mdReview 2AOP 선택
00. Testing 압축 정리.mdLab 4, FinalTesting 전체
01. 테스트 전략과 Spring TestContext.mdLab 4TestContext
02. 단위 테스트와 테스트 더블.mdLab 4, FinalUnit test
03. WebMvcTest MockMvc와 Controller 테스트.mdLab 4WebMvcTest
04. SpringBootTest DataJpaTest Testcontainers.mdIncident 3, FinalIntegration/DataJpaTest
17. Spring Web-Back 실전 가이드북.md모든 훈련상황별 문서 길찾기

반복 기록표

날짜훈련내 판단의 약점다시 볼 문서다음 회독 기준
Lab 1
Lab 2
Lab 3
Lab 4
Incident 1
Incident 2
Incident 3
Incident 4
Review 1
Review 2
Review 3
Final