[데브코스] Spring Boot REST API 실습 (28강) - ResponseAspect 도입 RsData 기반으로 HTTP 응답코드 자동 설정하기 (AOP)

zuno·2026년 1월 4일

이번 강의는 지금까지 배운 내용들이 한 번에 정리되는 느낌의 강의였다.

핵심 키워드는 딱 세 가지다.

  • ResponseEntity 제거
  • RsData.resultCode 기준으로 HTTP 상태 코드 설정
  • AOP(ResponseAspect)로 후처리

📌 왜 이 작업을 하게 되었을까?

27강에서 우리는 이런 코드를 사용했다.

@PostMapping
@Transactional
public ResponseEntity<RsData<PostWriteResBody>> write(@Valid @RequestBody PostWriteReqBody form) {
    Post post = postService.write(form.title, form.content);

    return new ResponseEntity<>(
            new RsData<>(
                    "201-1",
                    "%d번 글이 작성되었습니다.".formatted(post.getId()),
                    new PostWriteResBody(
                            postService.count(),
                            new PostDto(post)
                    )
            ),
            HttpStatus.CREATED
    );
}

이 방식의 문제점은 명확했다.

  • HTTP 응답코드를 설정하려고
  • 매번 ResponseEntity를 감싸야 한다
  • 코드가 길어지고 가성비가 나빠진다

👉 “HTTP 응답코드 하나 바꾸려고 이 정도 코드가 필요한가?”

그래서 나온 결론이 바로 AOP 다.


🔄 목표

  • 컨트롤러는 RsData만 반환
  • HTTP 응답코드는 AOP에서 후처리
  • resultCode(예: 201-1)를 기준으로 상태 코드 자동 설정

✅ 컨트롤러 코드 (최종 형태)

@PostMapping
@Transactional
public RsData<PostWriteResBody> write(@Valid @RequestBody PostWriteReqBody form) {
    Post post = postService.write(form.title, form.content);

    return new RsData<>(
            "201-1",
            "%d번 글이 작성되었습니다.".formatted(post.getId()),
            new PostWriteResBody(
                    postService.count(),
                    new PostDto(post)
            )
    );
}

이제 컨트롤러는 RsData만 신경 쓴다.


🔧 RsData 구조 변경

public record RsData<T>(String resultCode, int statusCode, String msg, T data) {

    public RsData(String resultCode, String msg) {
        this(resultCode, msg, null);
    }

    public RsData(String resultCode, String msg, T data) {
        this(
            resultCode,
            Integer.parseInt(resultCode.split("-", 2)[0]),
            msg,
            data
        );
    }
}

🔍 this(...) 문법은 뭐지?

this(resultCode, msg, null);
  • 같은 record 안에 있는 다른 생성자를 호출
  • 기본 생성자를 호출하는 게 아님
  • record는 컴파일 시점에 모든 필드를 받는 canonical constructor가 항상 존재

즉 우리가 직접 정의하지 않아도

RsData(String resultCode, int statusCode, String msg, T data)

이 생성자는 항상 존재하고,
우리는 그걸 호출하고 있는 것이다.


🎯 statusCode 자동 계산 원리

Integer.parseInt(resultCode.split("-", 2)[0])
  • "201-1" → 201
  • "404-1" → 404

resultCode 규칙만 지키면
HTTP 상태 코드도 자동으로 결정된다.


🧠 핵심: ResponseAspect (AOP) — 초보자 관점 설명

@Aspect
@Component
public class ResponseAspect {

    private final HttpServletResponse response;

    public ResponseAspect(HttpServletResponse response) {
        this.response = response;
    }

1️⃣ @Aspect란?

“이 클래스는 AOP용 클래스입니다” 라고 스프링에게 알려주는 표시

즉,

  • 이 클래스는
  • 다른 코드에 끼어들어서
  • 실행 전/후에 뭔가를 하겠다는 뜻이다.

2️⃣ HttpServletResponse는 왜 주입받을까?

HTTP 응답코드(200, 201, 404 등)는
컨트롤러 리턴값이 아니라 응답 객체(HttpServletResponse) 에 설정된다.

그래서 상태 코드를 바꾸려면

response.setStatus(201);

를 직접 호출해야 하고,
이를 위해 HttpServletResponse를 주입받는다.


3️⃣ @Around는 언제 실행될까?

@Around("""
    (
        within(@RestController *) &&
        (
            @annotation(GetMapping) ||
            @annotation(PostMapping) ||
            @annotation(PutMapping) ||
            @annotation(DeleteMapping) ||
            @annotation(RequestMapping)
        )
    )
""")

이 조건의 의미는 딱 하나다.

👉 “모든 RestController의 요청 메서드를 가로채라”

즉,

  • @GetMapping
  • @PostMapping
  • @DeleteMapping

이 붙은 메서드는
실행 전·후로 이 AOP가 자동 실행된다.


4️⃣ joinPoint.proceed()의 의미

Object proceed = joinPoint.proceed();

이 줄이 의미하는 바는 아주 중요하다.

  • 실제 컨트롤러 메서드를 실행하라
  • 이 줄이 없으면 컨트롤러 자체가 실행되지 않는다

즉,

@PostMapping
public RsData write(...) { ... }

이 메서드가
이 시점에서 실행되고 그 결과가 proceed에 담긴다.


5️⃣ RsData인지 검사하는 이유

if (proceed instanceof RsData<?> rsData) {
    response.setStatus(rsData.statusCode());
}
  • 모든 컨트롤러가 RsData를 반환하는 건 아님
  • RsData인 경우에만
    • resultCode → statusCode 꺼내서
    • HTTP 응답코드를 설정

JSON 본문은 건드리지 않는다.


6️⃣ 최종 반환

return proceed;
  • 컨트롤러가 반환한 객체 그대로 클라이언트로 전달
  • 단, HTTP 상태 코드만 AOP에서 바뀜

📸 여기에 Postman 201 Created 결과 이미지 추가


🔄 전체 흐름 한 번에 정리

1️⃣ 컨트롤러 메서드 실행
2️⃣ RsData 반환
3️⃣ ResponseAspect가 실행됨
4️⃣ resultCode → statusCode 계산
5️⃣ HttpServletResponse에 상태 코드 설정
6️⃣ JSON 응답 전송


🧩 이 구조의 장점

  • 컨트롤러 코드가 매우 단순해짐
  • ResponseEntity 제거
  • HTTP 응답 정책을 한 곳에서 관리
  • resultCode 규칙만 지키면 자동 처리

🧠 AOP는 이미 우리가 쓰고 있다

@Transactional  → AOP
@Validated      → AOP
@ResponseAspect → AOP

즉, 스프링은 이미 AOP 기반 프레임워크다.


❓ @Valid는 왜 붙였을까?

public RsData<PostWriteResBody> write(@Valid @RequestBody PostWriteReqBody form)
  • @NotBlank, @Size 같은 유효성 검사를 실제로 실행시키는 트리거
  • 없으면 어노테이션이 있어도 검사 안 됨

REST API에서는

  • BindingResult 없이
  • 예외를 바로 터뜨리고
  • GlobalExceptionHandler에서 처리하는 방식이 일반적이다.

❓ @RequestBody는 필수인가?

  • JSON 요청을 받을 때는 필수
  • 없으면 JSON은 매핑되지 않는다

👉 REST API + JSON = @RequestBody 필수


✍️ 마무리 정리

  • ResponseEntity로 상태 코드를 관리하는 건 가성비가 나쁠 수 있다
  • RsData + AOP 조합으로 깔끔한 구조 가능
  • record 생성자 체이닝은 canonical constructor 덕분에 가능
  • @Valid는 보안과 안정성을 위한 최소한의 방어선

💬 개인적인 느낀 점

이번 강의는
단순히 AOP 문법을 알려준 게 아니라,

“왜 이런 구조를 쓰는지”
“언제 단순하게 가도 되는지”

를 같이 보여준 강의였다.

지금까지 배운 RsData, HTTP 상태 코드, AOP가
하나의 구조로 딱 맞아떨어진 느낌이다.

0개의 댓글