[아이티센 부트캠프] Spring MVC REST 요청 처리 응용예제

이언덕·2026년 5월 14일

아이티센 부트캠프

목록 보기
97/115
post-thumbnail

1. Filter로 요청 전후 흐름 확인하기 응용예제

이 예제는 Filter가 요청이 실제 Controller에 도착하기 전과, 요청 처리가 끝난 뒤 응답이 돌아오는 시점에 실행될 수 있다는 흐름을 보여 준다.
Filter는 웹 요청이 Spring MVC의 Controller로 바로 들어가기 전에 먼저 거치는 앞단 처리 영역이다.
쉽게 말하면 요청이 컨트롤러에 들어가기 전에 한 번 걸러 보거나, 공통 작업을 먼저 처리하는 문지기 같은 역할을 한다.


이번 예제에서는 /home 요청을 보낼 때 Filter에서 요청 전 로그를 출력하고, HomeController가 요청을 처리한 뒤 다시 Filter로 돌아와 요청 후 로그를 출력하는 흐름을 확인한다.
여기서 가장 중요한 코드는 chain.doFilter(request, response)이다.
이 코드를 기준으로 앞에 있는 코드는 요청이 다음 단계로 가기 전에 실행되고, 뒤에 있는 코드는 요청 처리가 끝난 뒤 응답이 돌아올 때 실행된다.


결과물은 브라우저 화면과 IntelliJ 콘솔을 함께 확인해서 뽑는다.
브라우저 화면에서는 /home 요청의 응답 문자열을 확인하고, IntelliJ 콘솔에서는 Filter 요청 전 로그, HomeController 수행, Filter 요청 후 로그가 어떤 순서로 출력되는지 확인한다.


이 예제의 핵심은 Filter가 chain.doFilter()를 기준으로 요청 전 작업과 응답 후 작업을 나누어 처리할 수 있다는 점이다.


예제 전체 코드

// TestFilter1.java
package com.example.springrestedu.filter; // 클래스가 속한 패키지 경로

import jakarta.servlet.Filter; // `Filter` 기능 구현 인터페이스
import jakarta.servlet.FilterChain; // 다음 단계로 요청을 넘기는 객체
import jakarta.servlet.ServletException; // `Servlet` 처리 중 예외 표현
import jakarta.servlet.ServletRequest; // 요청 정보를 담는 객체
import jakarta.servlet.ServletResponse; // 응답 정보를 담는 객체
import jakarta.servlet.annotation.WebFilter; // `Web Filter` 등록 애노테이션
import jakarta.servlet.http.HttpServletRequest; // `HTTP` 요청 정보를 다루는 객체
import lombok.extern.slf4j.Slf4j; // 로그 출력을 위한 `Lombok` 애노테이션
import org.springframework.core.annotation.Order; // `Filter` 실행 순서 지정 애노테이션
import org.springframework.stereotype.Component; // `Spring Bean` 등록 애노테이션
import java.io.IOException; // 입출력 처리 중 예외 표현

@Slf4j // `log.info()` 사용 설정
//@WebFilter(urlPatterns = {"/*"}) // 현재는 주석 처리되어 `Filter` 등록이 비활성화된 상태
public class TestFilter1 implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                        throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request; // `HTTP` 요청 객체로 변환
        String path = req.getRequestURI(); // 요청 `URI` 추출

        // 시스템성 요청은 로그를 남기지 않고 바로 다음 단계로 넘긴다.
        if (path.startsWith("/.well-known") || path.equals("/favicon.ico")) {
            chain.doFilter(request, response); // 다음 `Filter` 또는 요청 처리 대상으로 이동
            return; // 아래 로그를 실행하지 않고 종료
        }

        log.info("[필터1] 요청 자원 수행 전"); // 다음 단계로 가기 전 실행
        chain.doFilter(request, response); // 다음 `Filter` 또는 `Controller` 흐름으로 요청 전달
        log.info("[필터1] 요청 자원 수행 후"); // 요청 처리가 끝난 뒤 응답이 돌아올 때 실행
    }
}
// TestFilter2.java
package com.example.springrestedu.filter; // 클래스가 속한 패키지 경로

import jakarta.servlet.*; // `Filter`, `FilterChain`, 요청, 응답 관련 클래스 사용
import jakarta.servlet.annotation.WebFilter; // `Web Filter` 등록 애노테이션
import lombok.extern.slf4j.Slf4j; // 로그 출력을 위한 `Lombok` 애노테이션
import org.springframework.core.annotation.Order; // `Filter` 실행 순서 지정 애노테이션
import org.springframework.stereotype.Component; // `Spring Bean` 등록 애노테이션
import java.io.IOException; // 입출력 처리 중 예외 표현

@Slf4j // `log.info()` 사용 설정
//@WebFilter(urlPatterns = {"/home"}) // 현재는 주석 처리되어 `Filter` 등록이 비활성화된 상태
public class TestFilter2 implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                        throws IOException, ServletException {
        log.info("[필터2] 요청 자원 수행 전"); // 다음 단계로 가기 전 실행
        chain.doFilter(request, response); // 다음 `Filter` 또는 `Controller` 흐름으로 요청 전달
        log.info("[필터2] 요청 자원 수행 후"); // 요청 처리가 끝난 뒤 응답이 돌아올 때 실행
    }
}
// HomeController.java
package com.example.springrestedu.controller; // 클래스가 속한 패키지 경로

import org.springframework.web.bind.annotation.GetMapping; // `GET` 요청을 메서드와 연결
import org.springframework.web.bind.annotation.RestController; // `REST` 방식 `Controller` 등록

@RestController // 반환값을 응답 본문으로 바로 전달하는 `Controller`
public class HomeController {
    @GetMapping("/home") // `/home` `GET` 요청을 이 메서드와 연결
    public String corstest() {
        System.out.println("HomeController 수행"); // `Controller` 실행 확인용 콘솔 출력
        return "CORS 설정을 하지 않았어요ㅜㅜ"; // 브라우저에 반환할 문자열
    }
}

이 코드는 크게 세 파일로 나누어 볼 수 있다.
첫 번째 TestFilter1.java는 전체 요청을 대상으로 사용할 수 있는 Filter 구조이다.
두 번째 TestFilter2.java는 /home 요청을 대상으로 사용할 수 있는 Filter 구조이다.
세 번째 HomeController.java는 /home 요청을 실제로 처리하는 Controller이다.


TestFilter1과 TestFilter2는 모두 Filter 인터페이스를 구현한다.
인터페이스는 어떤 기능을 만들기 위해 반드시 갖춰야 할 메서드 규칙이라고 이해하면 된다.
Filter를 구현하면 요청이 필터를 지나갈 때 doFilter() 메서드가 실행된다.


HomeController는 /home 요청이 실제로 도착했을 때 실행된다.
브라우저에서 /home 주소로 요청을 보내면 corstest() 메서드가 실행되고, "CORS 설정을 하지 않았어요ㅜㅜ"라는 문자열이 응답으로 반환된다.


다만 현재 보여준 코드만 기준으로 보면 TestFilter1과 TestFilter2의 @WebFilter가 주석 처리되어 있다.
또 @Component도 클래스 위에 붙어 있지 않다.
따라서 현재 코드 그대로는 Filter가 실제 요청 흐름에 등록되지 않은 상태이다.
실행 결과에서 Filter 로그까지 확인하려면 @WebFilter 주석을 해제하거나 @Component를 붙이는 방식으로 등록을 활성화해야 한다.


이 예제는 Filter의 실행 흐름을 이해하기 위한 코드이며, 실제 Filter 실행 결과물을 뽑으려면 먼저 Filter 등록을 활성화해야 한다.


이 예제에서 확인할 핵심

  • Filter는 Controller에 요청이 도착하기 전후에 실행될 수 있다.
  • doFilter()는 Filter에서 실제 요청과 응답 흐름을 처리하는 메서드이다.
  • chain.doFilter(request, response)는 다음 Filter 또는 최종 요청 처리 대상으로 요청을 넘긴다.
  • chain.doFilter() 앞의 코드는 요청이 다음 단계로 가기 전에 실행된다.
  • chain.doFilter() 뒤의 코드는 요청 처리가 끝나고 응답이 돌아올 때 실행된다.
  • TestFilter1은 요청 주소를 확인해서 시스템성 요청은 로그 없이 넘긴다.
  • TestFilter2는 /home 요청을 대상으로 사용할 수 있는 단순한 필터 구조이다.
  • HomeController는 /home 요청을 받아 문자열을 응답으로 반환한다.
  • 현재 코드에서는 @WebFilter가 주석 처리되어 있고 @Component도 없으므로, 그대로 실행하면 Filter 로그가 찍히지 않는다.

이 예제는 Filter가 요청을 막는 코드가 아니라, 요청 흐름 중간에서 공통 작업을 실행할 수 있는 구조라는 점을 보여 준다.


Filter란 무엇인가

Filter는 웹 요청이 Controller에 도착하기 전에 먼저 실행될 수 있는 처리 단계이다.
또 요청 처리가 끝난 뒤 응답이 돌아올 때도 다시 실행 흐름을 이어갈 수 있다.


예를 들어 모든 요청마다 로그를 남기고 싶다고 해 보자.
이 작업을 각 Controller 메서드마다 작성하면 코드가 반복된다.
GET, POST, PUT, DELETE 요청을 처리하는 메서드가 많아질수록 같은 로그 코드가 계속 중복된다.


이럴 때 Filter를 사용하면 공통 작업을 앞단에 모아 둘 수 있다.
요청이 어떤 Controller로 가든 먼저 Filter를 지나가게 만들 수 있기 때문이다.


Filter는 다음과 같은 작업에 자주 사용된다.

  • 요청 로그 남기기
  • 인코딩 처리
  • 인증이나 권한 확인
  • 특정 요청 차단
  • 공통 보안 처리

여기서 중요한 점은 Filter가 특정 Controller 내부 로직 자체를 대신하는 것이 아니라는 점이다.
Filter는 요청이 Controller에 들어가기 전후의 공통 흐름을 다룬다.


Filter는 여러 요청에 반복되는 공통 작업을 Controller 앞단에서 처리하기 위해 사용한다.


TestFilter1은 전체 요청 흐름을 대상으로 사용할 수 있는 Filter이다

// TestFilter1.java
@Slf4j // `log.info()` 사용 설정
//@WebFilter(urlPatterns = {"/*"}) // 현재는 주석 처리되어 `Filter` 등록이 비활성화된 상태
public class TestFilter1 implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                        throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request; // `HTTP` 요청 객체로 변환
        String path = req.getRequestURI(); // 요청 `URI` 추출

        if (path.startsWith("/.well-known") || path.equals("/favicon.ico")) {
            chain.doFilter(request, response); // 다음 단계로 요청 전달
            return; // 아래 로그를 실행하지 않음
        }

        log.info("[필터1] 요청 자원 수행 전"); // 요청 처리 전 로그
        chain.doFilter(request, response); // 다음 단계로 요청 전달
        log.info("[필터1] 요청 자원 수행 후"); // 요청 처리 후 로그
    }
}

TestFilter1은 Filter를 구현한 클래스이다.
implements Filter는 이 클래스가 Filter 역할을 하겠다는 뜻이다.
그래서 요청이 이 필터를 지나가면 doFilter() 메서드가 실행된다.


원래 @WebFilter(urlPatterns = {"/*"})를 사용하면 모든 요청에 이 필터를 적용하겠다는 의미로 볼 수 있다.
여기서 /*는 전체 경로를 의미한다.
즉, /home, /boards, /restapi/hello 같은 여러 요청을 모두 대상으로 삼을 수 있는 패턴이다.


하지만 현재 코드에서는 @WebFilter(urlPatterns = {"/*"})가 주석 처리되어 있다.
주석 처리된 코드는 실행되지 않는다.
또 클래스 위에 @Component도 붙어 있지 않다.
따라서 현재 보여준 코드만 기준으로는 이 필터가 실제 요청 흐름에 등록되지 않는다.


TestFilter1의 특징은 요청 주소를 직접 확인한다는 점이다.
HttpServletRequest req = (HttpServletRequest) request로 요청 객체를 HTTP 요청 객체로 변환한다.
그리고 req.getRequestURI()로 요청 주소를 꺼낸다.


그다음 /.well-known으로 시작하는 요청이나 /favicon.ico 요청이면 로그를 남기지 않고 바로 다음 단계로 넘긴다.
이런 요청은 브라우저나 도구가 자동으로 보내는 시스템성 요청일 수 있다.
예제에서 핵심으로 보고 싶은 요청 로그와 섞이면 헷갈릴 수 있으므로 제외한 것이다.


TestFilter1은 전체 요청에 적용할 수 있는 구조를 가지고 있고, 불필요한 시스템성 요청은 로그 없이 넘기도록 조건을 둔 필터이다.


TestFilter2는 /home 요청을 대상으로 사용할 수 있는 Filter이다

// TestFilter2.java
@Slf4j // `log.info()` 사용 설정
//@WebFilter(urlPatterns = {"/home"}) // 현재는 주석 처리되어 `Filter` 등록이 비활성화된 상태
public class TestFilter2 implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                        throws IOException, ServletException {
        log.info("[필터2] 요청 자원 수행 전"); // 요청 처리 전 로그
        chain.doFilter(request, response); // 다음 단계로 요청 전달
        log.info("[필터2] 요청 자원 수행 후"); // 요청 처리 후 로그
    }
}

TestFilter2도 Filter를 구현한 클래스이다.
구조는 TestFilter1보다 단순하다.
요청 주소를 따로 확인하지 않고, 요청 전 로그와 요청 후 로그만 출력한다.


원래 @WebFilter(urlPatterns = {"/home"})를 사용하면 /home 요청에 이 필터를 적용하겠다는 의미로 볼 수 있다.
즉, 전체 요청이 아니라 /home 요청만 대상으로 삼는 구조이다.


현재는 이 애노테이션도 주석 처리되어 있다.
또 클래스 위에 @Component도 붙어 있지 않다.
따라서 현재 보여준 코드만 기준으로는 TestFilter2도 실제 요청 흐름에 등록되지 않는다.


TestFilter2의 흐름은 아래처럼 단순하다.

  • 요청이 들어오면 [필터2] 요청 자원 수행 전 로그를 출력한다.
  • chain.doFilter(request, response)로 다음 단계로 요청을 넘긴다.
  • 요청 처리가 끝나고 응답이 돌아오면 [필터2] 요청 자원 수행 후 로그를 출력한다.

TestFilter2는 /home 요청 흐름에서 chain.doFilter() 전후 로그를 확인하기 위한 단순한 필터 예제이다.


chain.doFilter가 요청 흐름의 기준점이다

// TestFilter2.java
log.info("[필터2] 요청 자원 수행 전"); // 요청 처리 전 로그
chain.doFilter(request, response); // 다음 단계로 요청 전달
log.info("[필터2] 요청 자원 수행 후"); // 요청 처리 후 로그

Filter에서 가장 중요한 코드는 chain.doFilter(request, response)이다.
이 코드는 현재 Filter에서 다음 단계로 요청을 넘긴다.


다음 단계는 다른 Filter일 수도 있고, 더 이상 필터가 없다면 실제 요청 처리 흐름으로 넘어갈 수도 있다.
Spring MVC 프로젝트에서는 결국 요청이 DispatcherServlet을 거쳐 Controller로 전달된다.
DispatcherServlet은 Spring MVC에서 들어온 요청을 어떤 Controller가 처리할지 찾아주는 핵심 진입점이다.


chain.doFilter() 앞에 있는 코드는 요청이 다음 단계로 가기 전에 실행된다.
그래서 [필터2] 요청 자원 수행 전 로그가 먼저 찍힌다.


그다음 chain.doFilter()가 실행되면 요청이 다음 단계로 넘어간다.
이 과정에서 /home 요청은 HomeController로 전달되어 corstest() 메서드가 실행된다.


요청 처리가 끝나고 응답이 돌아오면 chain.doFilter() 뒤에 있는 코드가 실행된다.
그래서 [필터2] 요청 자원 수행 후 로그가 마지막에 찍힌다.


chain.doFilter()는 요청을 다음 단계로 보내는 코드이면서, 요청 전 작업과 응답 후 작업을 나누는 기준점이다.


chain.doFilter를 호출하지 않으면 요청이 다음 단계로 가지 못한다

chain.doFilter()는 단순히 로그 사이에 끼어 있는 코드가 아니다.
요청 흐름을 계속 진행시키는 핵심 코드이다.


만약 chain.doFilter(request, response)를 호출하지 않으면 요청이 다음 단계로 넘어가지 않는다.
그러면 /home 요청이 HomeController까지 도착하지 못할 수 있다.


예를 들어 아래처럼 작성하면 요청 전 로그만 찍히고, 다음 단계로 요청을 넘기지 않는다.

// TestFilterWrongExample.java
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                    throws IOException, ServletException {
    log.info("[필터] 요청 자원 수행 전"); // 요청 전 로그만 출력
    // chain.doFilter(request, response)를 호출하지 않으면 다음 단계로 가지 못함
}

이 코드는 특별히 요청을 막아야 하는 상황이 아니라면 잘못된 흐름이다.
요청이 Controller까지 가지 못하면 브라우저는 정상 응답을 받지 못할 수 있다.


따라서 일반적인 로그 확인용 Filter에서는 요청 전 작업을 한 뒤 chain.doFilter()를 호출해야 한다.
그래야 다음 Filter나 Controller가 실행될 수 있다.


특별히 요청을 차단하려는 목적이 아니라면 Filter 안에서 chain.doFilter()를 호출해야 요청 처리가 계속 진행된다.


HomeController는 /home 요청을 실제로 처리한다

// HomeController.java
@RestController // 반환값을 응답 본문으로 바로 전달하는 `Controller`
public class HomeController {
    @GetMapping("/home") // `/home` `GET` 요청을 이 메서드와 연결
    public String corstest() {
        System.out.println("HomeController 수행"); // `Controller` 실행 확인용 콘솔 출력
        return "CORS 설정을 하지 않았어요ㅜㅜ"; // 브라우저에 반환할 문자열
    }
}

HomeController는 /home 요청을 처리하는 Controller이다.
@RestController는 이 클래스가 요청을 처리하는 Controller이고, 메서드의 반환값을 응답 본문으로 바로 보내겠다는 뜻이다.


@GetMapping("/home")은 GET /home 요청이 들어오면 corstest() 메서드를 실행하겠다는 뜻이다.
브라우저 주소창에서 /home으로 요청하면 기본적으로 GET 요청이 보내진다.
그래서 이 메서드가 실행된다.


System.out.println("HomeController 수행")은 서버 콘솔에서 Controller가 실행되었는지 확인하기 위한 출력이다.
필터 로그 사이에서 이 문장이 보이면 요청이 실제로 Controller까지 도착했다는 것을 알 수 있다.


return "CORS 설정을 하지 않았어요ㅜㅜ"는 브라우저에 보여 줄 응답 문자열이다.
@RestController가 붙어 있기 때문에 이 문자열은 화면 이름이 아니라 응답 본문으로 그대로 반환된다.


HomeController는 Filter가 요청을 다음 단계로 넘긴 뒤 실제 /home 요청을 처리하는 대상이다.


Filter와 Controller 실행 순서 이해하기

Filter가 정상 등록되어 있다면 /home 요청 흐름은 아래처럼 볼 수 있다.

  • 브라우저에서 /home 요청을 보낸다.
  • 요청이 먼저 Filter에 도착한다.
  • Filter에서 요청 전 로그가 출력된다.
  • chain.doFilter(request, response)가 실행된다.
  • 요청이 다음 단계로 넘어가고 HomeController가 실행된다.
  • HomeController 수행 문장이 IntelliJ 콘솔에 출력된다.
  • HomeController가 문자열 응답을 반환한다.
  • 응답이 다시 Filter로 돌아온다.
  • Filter에서 요청 후 로그가 출력된다.
  • 브라우저에는 응답 문자열이 보인다.

이 흐름을 결과물로 확인하려면 브라우저 화면과 IntelliJ 콘솔을 함께 봐야 한다.
브라우저 화면에서는 HomeController가 반환한 응답 문자열을 확인한다.
IntelliJ 콘솔에서는 Filter 요청 전 로그, HomeController 수행, Filter 요청 후 로그가 어떤 순서로 찍히는지 확인한다.


흐름을 한 줄로 정리하면 다음과 같다.


/home 요청 → Filter 요청 전 로그 → chain.doFilter() → HomeController 실행 → 응답 반환 → Filter 요청 후 로그


이 흐름에서 Filter 요청 후 로그가 Controller 실행 뒤에 찍히는 이유가 중요하다.
chain.doFilter()가 요청을 다음 단계로 보내고, 그 요청 처리가 끝난 뒤 다시 원래 필터 코드로 돌아오기 때문이다.


Filter의 실행 여부는 브라우저 화면이 아니라 IntelliJ 콘솔 로그 순서로 확인해야 한다.


현재 코드에서 주의해야 할 점

현재 TestFilter1과 TestFilter2의 @WebFilter는 주석 처리되어 있다.
주석 처리된 애노테이션은 실행되지 않는다.
또 현재 코드에는 @Component도 클래스 위에 붙어 있지 않다.
따라서 현재 보여준 코드만 기준으로 보면 TestFilter1과 TestFilter2는 실제 요청 흐름에 등록되지 않은 상태이다.


결과물을 뽑으려면 먼저 필터 등록 방식을 활성화해야 한다.
방법은 크게 두 가지로 볼 수 있다.


첫 번째는 @WebFilter 방식이다.
이 경우 @WebFilter 주석을 해제해야 한다.
그리고 Spring Boot 프로젝트에서는 @ServletComponentScan 설정이 필요할 수 있다.


두 번째는 Spring Bean 방식이다.
이 경우 클래스 위에 @Component를 붙여 Filter를 Bean으로 등록한다.
여러 Filter의 실행 순서가 필요하면 @Order를 함께 사용할 수 있다.


중요한 것은 이 예제의 핵심이 필터 등록 방식 자체가 아니라는 점이다.
이 예제의 핵심은 등록된 Filter가 요청 전후에 어떻게 실행되는지 확인하는 것이다.
따라서 결과를 확인할 때는 아래를 봐야 한다.

  • 요청 전 로그가 먼저 찍히는지
  • HomeController 수행이 중간에 찍히는지
  • 요청 후 로그가 마지막에 찍히는지

현재 코드 그대로는 Filter 로그가 나오지 않으므로, 결과물 확인 전 Filter 등록을 먼저 활성화해야 한다.


실행 흐름 정리

이 예제의 실행 흐름은 아래 순서로 진행된다.

  • Filter 등록을 활성화한다.
  • 서버를 실행한다.
  • 브라우저에서 /home 요청을 보낸다.
  • 등록된 Filter가 먼저 요청을 받는다.
  • Filter에서 요청 전 로그를 출력한다.
  • chain.doFilter(request, response)가 다음 단계로 요청을 넘긴다.
  • HomeController의 corstest() 메서드가 실행된다.
  • HomeController 수행 문장이 서버 콘솔에 출력된다.
  • HomeController가 문자열 응답을 반환한다.
  • 응답이 다시 Filter로 돌아온다.
  • Filter에서 요청 후 로그를 출력한다.
  • 브라우저 화면에 "CORS 설정을 하지 않았어요ㅜㅜ"가 표시된다.

흐름을 한 줄로 정리하면 다음과 같다.


Filter 등록 활성화 → /home 요청 → Filter 요청 전 → chain.doFilter() → HomeController 실행 → 응답 반환 → Filter 요청 후


Filter는 요청이 들어갈 때 한 번, 응답이 돌아올 때 한 번 흐름을 나누어 공통 작업을 처리할 수 있다.


결과물 뽑기

이 예제의 결과물은 브라우저 응답 화면과 IntelliJ 콘솔 로그로 확인한다.
브라우저 화면에서는 /home 요청을 보냈을 때 HomeController가 반환한 응답 문자열을 확인한다.
IntelliJ 콘솔에서는 Filter 요청 전 로그, HomeController 수행, Filter 요청 후 로그가 순서대로 출력되는지 확인한다.


먼저 현재 코드 그대로 실행하면 @WebFilter가 주석 처리되어 있고 @Component도 없기 때문에 Filter는 등록되지 않는다.
그래서 브라우저에서는 /home 응답이 보이지만, 콘솔에는 HomeController 수행만 출력된다.
이 결과는 HomeController는 정상 실행되지만, Filter는 아직 실행되지 않았다는 뜻이다.

// 현재 코드 그대로 실행했을 때 콘솔 결과
// HomeController 수행

현재 코드 그대로 실행했을 때 브라우저 응답은 아래처럼 확인된다.

// 브라우저 응답
// CORS 설정을 하지 않았어요ㅜㅜ

현재 코드 그대로 실행해도 /home 요청 자체는 HomeController까지 도착한다.
그래서 브라우저에는 HomeController가 반환한 문자열이 그대로 출력된다.


콘솔에는 HomeController 수행만 출력된다.
즉, 이 결과는 Filter가 실행된 결과가 아니라, Filter가 등록되지 않은 상태에서 /home 요청이 HomeController로 바로 처리된 결과이다.


Filter 실행 결과를 뽑으려면 먼저 Filter 등록을 활성화해야 한다.
이번 결과에서는 TestFilter1이 실행되도록 등록을 활성화한 뒤 다시 /home 요청을 보냈다.
그러면 콘솔에서 [필터1] 요청 자원 수행 전, HomeController 수행, [필터1] 요청 자원 수행 후 순서가 확인된다.

// `Filter` 등록 후 콘솔 결과
// [필터1] 요청 자원 수행 전
// HomeController 수행
// [필터1] 요청 자원 수행 후

[필터1] 요청 자원 수행 전은 chain.doFilter() 전에 실행된 로그이다.
HomeController 수행은 요청이 Controller까지 도착했다는 로그이다.
[필터1] 요청 자원 수행 후는 Controller 처리가 끝난 뒤 응답이 다시 Filter로 돌아왔다는 로그이다.


위 순서가 보이면 Filter가 요청 전후에 정상적으로 동작한 것이다.
브라우저 결과만 보면 Controller 응답만 확인할 수 있다.
Filter의 실행 여부와 실행 순서는 반드시 IntelliJ 콘솔 로그로 확인해야 한다.


이 예제의 최종 결과물은 /home 브라우저 응답 화면과, Filter 등록 후 Filter 요청 전 로그 → HomeController 수행 → Filter 요청 후 로그가 순서대로 보이는 IntelliJ 콘솔 로그이다.


핵심 정리

이 예제의 핵심은 Filter의 요청 전후 실행 흐름이다.
Filter는 요청이 Controller로 가기 전에 먼저 실행될 수 있고, 요청 처리가 끝난 뒤 응답이 돌아올 때도 다시 이어서 실행될 수 있다.


이 흐름은 아래처럼 정리할 수 있다.

  • Filter는 Controller에 요청이 도착하기 전후에 실행될 수 있는 공통 처리 영역이다.
  • doFilter()는 Filter에서 실제 요청 흐름을 처리하는 메서드이다.
  • chain.doFilter(request, response)는 다음 단계로 요청을 넘기는 코드이다.
  • chain.doFilter() 앞의 코드는 요청 전 작업이다.
  • chain.doFilter() 뒤의 코드는 응답이 돌아온 뒤 실행되는 작업이다.
  • TestFilter1은 전체 요청에 적용할 수 있는 구조를 가지고 있다.
  • TestFilter1은 시스템성 요청을 로그 없이 넘기는 조건을 가지고 있다.
  • TestFilter2는 /home 요청에 적용할 수 있는 단순한 필터 구조이다.
  • HomeController는 /home 요청을 실제로 처리하고 문자열을 반환한다.
  • 현재 코드 그대로는 Filter가 등록되지 않은 상태이므로 실행 전 등록을 활성화해야 한다.
  • 현재 코드 그대로 실행하면 브라우저 응답과 HomeController 수행만 확인된다.
  • Filter 등록 후에는 요청 전 로그, HomeController 수행, 요청 후 로그가 순서대로 출력된다.
  • 결과물은 /home 브라우저 응답 화면과 IntelliJ 콘솔 로그 순서로 확인한다.

따라서 이 예제가 알려 주는 핵심은 이것이다.
Filter는 chain.doFilter()를 기준으로 요청이 다음 단계로 가기 전 작업과, 요청 처리가 끝나고 응답이 돌아온 뒤 작업을 나누어 처리할 수 있다.
다음 예제에서는 Controller 실행 전후에 동작하는 Interceptor 흐름을 확인한다.



2. Interceptor 등록과 /home 요청 흐름 확인하기 응용예제

이 예제는 Interceptor가 Controller 실행 전후에 어떤 순서로 동작하는지 확인하는 흐름이다.
앞의 예제에서 본 Filter는 Controller에 요청이 도착하기 전보다 더 앞단에서 동작한다.
반면 Interceptor는 Spring MVC 내부에서 Controller가 실행되기 전후에 동작한다.


이번 예제에서는 /home 요청을 보낼 때 TestInterceptor가 실행되도록 설정한다.
TestInterceptor는 preHandle(), postHandle(), afterCompletion() 세 메서드를 가지고 있다.
이 세 메서드는 실행 시점이 서로 다르기 때문에, 로그 순서를 보면 Interceptor가 언제 동작하는지 확인할 수 있다.


결과물은 브라우저 화면과 IntelliJ 콘솔을 함께 확인해서 뽑는다.
브라우저 화면에서는 /home 요청의 응답 문자열을 확인하고, IntelliJ 콘솔에서는 Interceptor 로그와 HomeController 수행 로그가 어떤 순서로 출력되는지 확인한다.


이 예제의 핵심은 Interceptor가 Controller 실행 전, 실행 후, 요청 완료 후 시점을 나누어 공통 작업을 처리할 수 있다는 점이다.


예제 전체 코드

// TestInterceptor.java
package com.example.springrestedu.interceptor; // 클래스가 속한 패키지 경로

import jakarta.servlet.http.HttpServletRequest; // `HTTP` 요청 정보를 다루는 객체
import jakarta.servlet.http.HttpServletResponse; // `HTTP` 응답 정보를 다루는 객체
import lombok.extern.slf4j.Slf4j; // 로그 출력을 위한 `Lombok` 애노테이션
import org.springframework.web.servlet.HandlerInterceptor; // `Interceptor` 기능 구현 인터페이스
import org.springframework.web.servlet.ModelAndView; // `Model`과 `View` 정보를 담는 객체

@Slf4j // `log.info()` 사용 설정
public class TestInterceptor implements HandlerInterceptor {
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        log.info("[인터셉터] preHandle 수행"); // `Controller` 실행 전 로그 출력
        System.out.println(handler); // 실행될 `Controller` 메서드 정보 출력
        return true; // 다음 단계로 요청 진행
    }

    public void postHandle(
            HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {
        log.info("[인터셉터] postHandle 수행"); // `Controller` 실행 후 로그 출력
        System.out.println(handler); // 실행된 `Controller` 메서드 정보 출력
        System.out.println(modelAndView); // `ModelAndView` 정보 출력
    }

    public void afterCompletion(
            HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        log.info("[인터셉터] afterCompletion 수행"); // 요청 완료 후 로그 출력
    }
}
// WebMvcConfig.java
package com.example.springrestedu.config; // 클래스가 속한 패키지 경로

import com.example.springrestedu.interceptor.TestInterceptor; // 등록할 `Interceptor` 클래스
import org.springframework.context.annotation.Configuration; // 설정 클래스 등록 애노테이션
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; // `Interceptor` 등록 객체
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; // `Spring MVC` 설정 확장 인터페이스

//@Configuration // 현재는 주석 처리되어 설정 클래스 등록이 비활성화된 상태
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TestInterceptor()) // `TestInterceptor` 등록
                .addPathPatterns("/home"); // `/home` 요청에만 `Interceptor` 적용

        /*
        registry.addInterceptor(인터셉터객체)
                .addPathPatterns("/*")
                .excludePathPatterns("/sample");
        */
    }
}
// HomeController.java
package com.example.springrestedu.controller; // 클래스가 속한 패키지 경로

import org.springframework.web.bind.annotation.GetMapping; // `GET` 요청을 메서드와 연결
import org.springframework.web.bind.annotation.RestController; // `REST` 방식 `Controller` 등록

@RestController // 반환값을 응답 본문으로 바로 전달하는 `Controller`
public class HomeController {
    @GetMapping("/home") // `/home` `GET` 요청을 이 메서드와 연결
    public String corstest() {
        System.out.println("HomeController 수행"); // `Controller` 실행 확인용 콘솔 출력
        return "CORS 설정을 하지 않았어요ㅜㅜ"; // 브라우저에 반환할 문자열
    }
}

이 코드는 크게 세 파일로 나누어 볼 수 있다.
첫 번째 TestInterceptor.java는 Interceptor가 실제로 어떤 시점에 실행되는지 로그로 확인하는 클래스이다.
두 번째 WebMvcConfig.java는 TestInterceptor를 /home 요청에 등록하는 설정 클래스이다.
세 번째 HomeController.java는 /home 요청을 실제로 처리하는 Controller이다.


TestInterceptor는 HandlerInterceptor 인터페이스를 구현한다.
인터페이스는 어떤 기능을 만들기 위해 반드시 갖춰야 할 메서드 규칙이라고 이해하면 된다.
HandlerInterceptor를 구현하면 preHandle(), postHandle(), afterCompletion() 같은 요청 처리 전후 메서드를 사용할 수 있다.


WebMvcConfig는 Spring MVC 설정을 추가하는 클래스이다.
여기서는 addInterceptors() 메서드 안에서 TestInterceptor를 등록한다.
그리고 .addPathPatterns("/home")을 사용해 /home 요청에만 Interceptor가 적용되도록 설정한다.


다만 현재 보여준 코드만 기준으로 보면 WebMvcConfig의 @Configuration이 주석 처리되어 있다.
주석 처리된 설정 클래스는 Spring 설정으로 등록되지 않는다.
따라서 현재 코드 그대로는 TestInterceptor가 실제 요청 흐름에 적용되지 않을 수 있다.


이 예제는 Interceptor 실행 흐름을 이해하기 위한 코드이며, 실제 결과물을 뽑으려면 먼저 WebMvcConfig의 설정 등록을 활성화해야 한다.


이 예제에서 확인할 핵심

  • Interceptor는 Spring MVC 내부에서 Controller 실행 전후에 동작한다.
  • TestInterceptor는 HandlerInterceptor를 구현한 클래스이다.
  • preHandle()은 Controller 실행 전에 동작한다.
  • postHandle()은 Controller 실행 후에 동작한다.
  • afterCompletion()은 요청 처리가 끝난 뒤 마지막에 동작한다.
  • preHandle()에서 return true를 반환해야 요청이 다음 단계로 계속 진행된다.
  • WebMvcConfig는 TestInterceptor를 요청 경로에 등록하는 설정 클래스이다.
  • .addPathPatterns("/home")은 /home 요청에 Interceptor를 적용한다는 뜻이다.
  • 현재 코드에서는 @Configuration이 주석 처리되어 있으므로, 그대로 실행하면 Interceptor 로그가 찍히지 않을 수 있다.

이 예제는 Controller 실행 전후에 공통 작업을 끼워 넣고 싶을 때 Interceptor를 어떻게 등록하고 확인하는지 보여 준다.


Interceptor란 무엇인가

Interceptor는 Spring MVC에서 Controller 실행 전후에 공통 작업을 처리할 수 있게 해 주는 기능이다.
이름 그대로 요청 처리 흐름 중간을 가로채서 필요한 작업을 실행할 수 있다.


예를 들어 로그인 여부를 확인해야 한다고 해 보자.
이 작업을 모든 Controller 메서드마다 직접 작성하면 코드가 반복된다.
또 어떤 Controller에는 검사 코드를 넣고, 어떤 Controller에는 빠뜨리는 실수가 생길 수 있다.


이럴 때 Interceptor를 사용하면 특정 요청이 Controller에 도착하기 전에 공통 검사를 먼저 할 수 있다.
요청 처리 후에는 공통 로그를 남기거나 후처리 작업도 할 수 있다.


Interceptor는 다음과 같은 작업에 자주 사용된다.

  • 로그인 여부 확인
  • 권한 검사
  • 요청 처리 전 로그 남기기
  • 요청 처리 후 로그 남기기
  • 공통 데이터 준비
  • 요청 처리 완료 후 정리 작업

Filter와 비슷해 보이지만 실행 위치가 다르다.
Filter는 Spring MVC에 들어오기 전 더 앞단에서 동작한다.
Interceptor는 Spring MVC 내부에서 Controller 실행 전후에 동작한다.


Interceptor는 Controller 실행 전후의 공통 작업을 Spring MVC 흐름 안에서 처리하기 위해 사용한다.


TestInterceptor는 HandlerInterceptor를 구현한다

// TestInterceptor.java
@Slf4j // `log.info()` 사용 설정
public class TestInterceptor implements HandlerInterceptor {
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        log.info("[인터셉터] preHandle 수행"); // `Controller` 실행 전 로그 출력
        System.out.println(handler); // 실행될 `Controller` 메서드 정보 출력
        return true; // 다음 단계로 요청 진행
    }

    public void postHandle(
            HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {
        log.info("[인터셉터] postHandle 수행"); // `Controller` 실행 후 로그 출력
        System.out.println(handler); // 실행된 `Controller` 메서드 정보 출력
        System.out.println(modelAndView); // `ModelAndView` 정보 출력
    }

    public void afterCompletion(
            HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        log.info("[인터셉터] afterCompletion 수행"); // 요청 완료 후 로그 출력
    }
}

TestInterceptor는 HandlerInterceptor를 구현한 클래스이다.
HandlerInterceptor는 Spring MVC에서 Controller 실행 전후 흐름에 끼어들 수 있게 해 주는 인터페이스이다.


이 클래스에는 세 메서드가 있다.
preHandle()은 Controller 실행 전에 호출된다.
postHandle()은 Controller 실행 후에 호출된다.
afterCompletion()은 요청 처리가 완료된 뒤 호출된다.


코드에는 @Override가 붙어 있지 않지만, 메서드 모양이 HandlerInterceptor의 규칙과 맞으면 오버라이딩된 메서드로 동작한다.
다만 실제 코드에서는 @Override를 붙이면 “이 메서드는 부모 타입의 메서드를 재정의한다”는 의미가 더 분명해진다.


System.out.println(handler)는 현재 실행될 Controller 메서드 정보를 콘솔에 출력한다.
여기서 handler는 요청을 실제로 처리할 대상에 대한 정보이다.
/home 요청에서는 HomeController의 corstest() 메서드 정보가 출력될 수 있다.


TestInterceptor는 preHandle(), postHandle(), afterCompletion() 로그를 통해 Interceptor 실행 시점을 확인하는 클래스이다.


preHandle은 Controller 실행 전에 동작한다

// TestInterceptor.java
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {
    log.info("[인터셉터] preHandle 수행"); // `Controller` 실행 전 로그 출력
    System.out.println(handler); // 실행될 `Controller` 메서드 정보 출력
    return true; // 다음 단계로 요청 진행
}

preHandle()은 Controller가 실행되기 전에 호출된다.
이름의 pre는 앞이라는 뜻으로 이해하면 된다.
즉, 요청이 Controller 메서드로 들어가기 직전에 실행되는 단계이다.


이 메서드에서는 요청을 계속 진행할지 막을지 결정할 수 있다.
그 기준이 반환값이다.
preHandle()의 반환 타입은 boolean이다.
boolean은 true 또는 false만 가질 수 있는 타입이다.


return true를 하면 요청이 계속 진행된다.
그래서 HomeController의 corstest() 메서드가 실행된다.
반대로 return false를 하면 요청이 더 이상 진행되지 않는다.
그러면 Controller가 실행되지 않을 수 있다.


이 예제에서는 return true를 하고 있다.
따라서 preHandle() 로그가 출력된 뒤 요청은 HomeController까지 계속 진행된다.


preHandle()은 Controller 실행 전에 동작하며, return true를 해야 다음 단계로 요청이 계속 진행된다.


postHandle은 Controller 실행 후에 동작한다

// TestInterceptor.java
public void postHandle(
        HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
        throws Exception {
    log.info("[인터셉터] postHandle 수행"); // `Controller` 실행 후 로그 출력
    System.out.println(handler); // 실행된 `Controller` 메서드 정보 출력
    System.out.println(modelAndView); // `ModelAndView` 정보 출력
}

postHandle()은 Controller가 실행된 뒤 호출된다.
이름의 post는 뒤라는 뜻으로 이해하면 된다.
즉, Controller 메서드가 실행된 다음에 후처리 작업을 넣을 수 있는 단계이다.


이 예제에서는 postHandle()에서 두 가지를 출력한다.
첫 번째는 handler이다.
handler는 어떤 Controller 메서드가 요청을 처리했는지에 대한 정보이다.


두 번째는 modelAndView이다.
ModelAndView는 화면 이름과 화면에 전달할 데이터를 함께 담을 수 있는 객체이다.
하지만 현재 HomeController는 @RestController를 사용한다.
@RestController는 문자열을 화면 이름으로 해석하지 않고 응답 본문으로 바로 반환한다.


그래서 이 예제에서는 modelAndView가 null로 출력될 수 있다.
이것은 오류라기보다, View를 렌더링하는 방식이 아니라 응답 본문을 바로 반환하는 흐름이기 때문에 자연스러운 결과로 볼 수 있다.


postHandle()은 Controller 실행 후 동작하며, @RestController 응답에서는 modelAndView가 null일 수 있다.


afterCompletion은 요청 처리가 끝난 뒤 동작한다

// TestInterceptor.java
public void afterCompletion(
        HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
        throws Exception {
    log.info("[인터셉터] afterCompletion 수행"); // 요청 완료 후 로그 출력
}

afterCompletion()은 요청 처리가 끝난 뒤 호출된다.
Controller 실행도 끝나고, 후처리 단계도 지난 뒤 마지막 정리 단계에 가깝다.


이 메서드는 요청 처리 완료 후 로그를 남기거나, 예외 정보를 확인하거나, 사용한 자원을 정리할 때 사용할 수 있다.
매개변수 Exception ex는 요청 처리 중 예외가 발생했을 때 그 정보를 받을 수 있는 자리이다.
예외가 없으면 null일 수 있다.


이 예제에서는 별도 조건 없이 [인터셉터] afterCompletion 수행 로그만 출력한다.
이 로그가 콘솔에 보이면 요청 처리 마지막 단계까지 Interceptor 흐름이 진행되었다고 볼 수 있다.


afterCompletion()은 요청 처리가 끝난 뒤 마지막 정리 작업을 넣기 좋은 메서드이다.


WebMvcConfig는 Interceptor를 요청 경로에 등록한다

// WebMvcConfig.java
// @Configuration // 현재는 주석 처리되어 설정 클래스 등록이 비활성화된 상태
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TestInterceptor()) // `TestInterceptor` 등록
                .addPathPatterns("/home"); // `/home` 요청에만 `Interceptor` 적용
    }
}

TestInterceptor 클래스만 만들어서는 요청 흐름에 자동으로 적용되지 않는다.
어떤 요청에 이 Interceptor를 적용할지 등록해야 한다.
그 등록을 담당하는 클래스가 WebMvcConfig이다.


WebMvcConfig는 WebMvcConfigurer를 구현한다.
WebMvcConfigurer는 Spring MVC 설정을 추가로 조정할 수 있게 해 주는 인터페이스이다.
여기서는 addInterceptors() 메서드를 사용해 Interceptor를 등록한다.


registry.addInterceptor(new TestInterceptor())는 등록할 Interceptor 객체를 지정하는 코드이다.
즉, TestInterceptor를 Spring MVC 요청 처리 흐름에 끼워 넣겠다는 뜻이다.


.addPathPatterns("/home")은 이 Interceptor를 /home 요청에만 적용하겠다는 뜻이다.
그래서 /home 요청을 보낼 때는 TestInterceptor가 실행되지만, 다른 요청에는 적용되지 않을 수 있다.


다만 현재 코드에서는 @Configuration이 주석 처리되어 있다.
@Configuration은 이 클래스가 설정 클래스라는 것을 Spring에게 알려 주는 애노테이션이다.
이 애노테이션이 주석 처리되어 있으면 WebMvcConfig가 설정 클래스로 등록되지 않을 수 있다.
그러면 addInterceptors()도 실행되지 않고, TestInterceptor도 요청 흐름에 적용되지 않을 수 있다.


Interceptor는 클래스만 만든다고 실행되는 것이 아니라, WebMvcConfig에서 요청 경로에 등록해야 실행된다.


addPathPatterns는 Interceptor 적용 대상을 정한다

// WebMvcConfig.java
registry.addInterceptor(new TestInterceptor()) // `TestInterceptor` 등록
        .addPathPatterns("/home"); // `/home` 요청에만 `Interceptor` 적용

addPathPatterns()는 Interceptor를 어떤 요청 경로에 적용할지 정하는 메서드이다.
이 예제에서는 "/home"을 지정했다.
그래서 /home 요청에 TestInterceptor가 적용된다.


만약 여러 경로에 적용하고 싶다면 패턴을 바꿀 수 있다.
예를 들어 "/*"는 한 단계 경로를 의미하고, "/**"는 여러 단계 하위 경로까지 포함하는 전체 경로 패턴으로 이해할 수 있다.


코드 아래 주석에는 excludePathPatterns("/sample") 예시도 있다.
excludePathPatterns()는 특정 경로를 Interceptor 적용 대상에서 제외할 때 사용한다.
예를 들어 전체 요청에 Interceptor를 적용하면서 /sample만 제외하고 싶을 때 사용할 수 있다.


이 예제에서는 제외 경로를 사용하지 않는다.
/home 요청 하나에만 TestInterceptor를 적용하는 구조이다.


요청 경로를 잘못 지정하면 Interceptor 클래스가 있어도 원하는 요청에서 실행되지 않을 수 있다.
그래서 결과물을 확인할 때는 요청 주소와 addPathPatterns()에 지정한 주소가 일치하는지 확인해야 한다.


Interceptor와 HomeController 실행 순서 이해하기

WebMvcConfig가 정상 등록되어 있고 /home 요청에 TestInterceptor가 적용되면 실행 흐름은 아래처럼 진행된다.

  • 브라우저에서 /home 요청을 보낸다.
  • Spring MVC가 /home 요청을 처리할 HomeController를 찾는다.
  • Controller 실행 전에 preHandle()이 먼저 실행된다.
  • preHandle()에서 [인터셉터] preHandle 수행 로그가 출력된다.
  • preHandle()이 return true를 반환한다.
  • HomeController의 corstest() 메서드가 실행된다.
  • 콘솔에 HomeController 수행이 출력된다.
  • Controller 실행 후 postHandle()이 실행된다.
  • 요청 처리가 끝난 뒤 afterCompletion()이 실행된다.
  • 브라우저에는 "CORS 설정을 하지 않았어요ㅜㅜ"가 응답으로 보인다.

이 흐름을 결과물로 확인하려면 브라우저 화면과 IntelliJ 콘솔을 함께 봐야 한다.
브라우저 화면에서는 HomeController가 반환한 응답 문자열을 확인한다.
IntelliJ 콘솔에서는 preHandle(), HomeController 수행, postHandle(), afterCompletion() 로그 순서를 확인한다.


흐름을 한 줄로 정리하면 다음과 같다.


/home 요청 → preHandle() → HomeController 실행 → postHandle() → afterCompletion() → 브라우저 응답


Interceptor의 실행 여부는 브라우저 화면만으로는 확인하기 어렵고, IntelliJ 콘솔 로그 순서로 확인해야 한다.


현재 코드에서 주의해야 할 점

현재 WebMvcConfig의 @Configuration이 주석 처리되어 있다.
주석 처리된 애노테이션은 실행되지 않는다.
따라서 현재 보여준 코드만 기준으로 보면 WebMvcConfig가 Spring 설정 클래스로 등록되지 않을 수 있다.


설정 클래스가 등록되지 않으면 addInterceptors()가 실행되지 않는다.
addInterceptors()가 실행되지 않으면 TestInterceptor도 /home 요청에 적용되지 않는다.
이 상태에서는 브라우저에서 /home 요청을 보내도 HomeController만 실행되고, Interceptor 로그는 보이지 않을 수 있다.


결과물을 뽑으려면 먼저 @Configuration 주석을 해제해야 한다.
그다음 서버를 다시 실행하고 /home 요청을 다시 보내야 한다.


또 하나 주의할 점은 preHandle()의 반환값이다.
현재 코드는 return true를 반환한다.
그래서 요청이 HomeController까지 계속 진행된다.
만약 return false를 반환하면 Controller가 실행되지 않을 수 있다.


현재 코드 그대로는 Interceptor가 적용되지 않을 수 있으므로, 결과물 확인 전 WebMvcConfig의 @Configuration을 활성화해야 한다.


실행 흐름 정리

이 예제의 실행 흐름은 아래 순서로 진행된다.

  • WebMvcConfig의 @Configuration 주석을 해제한다.
  • 서버를 실행한다.
  • 브라우저에서 /home 요청을 보낸다.
  • WebMvcConfig의 addInterceptors() 설정에 따라 TestInterceptor가 /home 요청에 적용된다.
  • Controller 실행 전에 preHandle()이 실행된다.
  • preHandle()에서 [인터셉터] preHandle 수행 로그와 handler 정보가 출력된다.
  • preHandle()이 return true를 반환해서 요청이 계속 진행된다.
  • HomeController의 corstest() 메서드가 실행된다.
  • HomeController 수행 문장이 서버 콘솔에 출력된다.
  • Controller 실행 후 postHandle()이 실행된다.
  • postHandle()에서 [인터셉터] postHandle 수행, handler, modelAndView 정보가 출력된다.
  • 요청 처리가 끝난 뒤 afterCompletion()이 실행된다.
  • 브라우저 화면에 "CORS 설정을 하지 않았어요ㅜㅜ"가 표시된다.

흐름을 한 줄로 정리하면 다음과 같다.


@Configuration 활성화 → /home 요청 → preHandle() → HomeController 실행 → postHandle() → afterCompletion() → 브라우저 응답


Interceptor는 Controller 실행 전후의 흐름을 나누어 공통 작업을 처리할 수 있다.


결과물 뽑기

이 예제의 결과물은 두 단계로 나누어 확인한다.
첫 번째는 현재 코드 그대로 실행했을 때의 결과이다.
두 번째는 WebMvcConfig의 @Configuration을 활성화한 뒤 다시 실행했을 때의 최종 결과이다.


현재 코드 그대로 실행하면 @Configuration이 주석 처리되어 있기 때문에 WebMvcConfig가 설정 클래스로 등록되지 않을 수 있다.
그래서 /home 요청은 HomeController까지는 정상적으로 도착하지만, Interceptor 로그는 출력되지 않을 수 있다.
이 상태에서는 브라우저 화면에 "CORS 설정을 하지 않았어요ㅜㅜ"가 보이고, IntelliJ 콘솔에는 HomeController 수행만 출력될 수 있다.

// 현재 코드 그대로 실행했을 때 콘솔 결과
// HomeController 수행

현재 코드 그대로 실행하면 Interceptor 로그 없이 HomeController 수행만 출력된다.
이 결과는 /home 요청이 HomeController까지 도착했지만, 아직 Interceptor가 적용되지 않았다는 뜻이다.


현재 코드 그대로 실행했을 때 브라우저 응답은 아래처럼 확인된다.

// 브라우저 응답
// CORS 설정을 하지 않았어요ㅜㅜ

브라우저에는 HomeController가 반환한 문자열이 그대로 출력된다.
브라우저 화면만 보면 Controller 응답은 확인할 수 있지만, Interceptor 실행 여부는 확인할 수 없다.


Interceptor 실행 결과를 뽑으려면 먼저 WebMvcConfig의 @Configuration 주석을 해제해야 한다.
그다음 서버를 다시 실행하고 브라우저에서 /home 요청을 다시 보낸다.


Interceptor가 정상 등록된 뒤 다시 /home 요청을 보내면 콘솔 결과는 아래 흐름으로 나온다.

// Interceptor 등록 후 콘솔 결과
// [인터셉터] preHandle 수행
// com.example.springrestedu.controller.HomeController#corstest()
// HomeController 수행
// [인터셉터] postHandle 수행
// com.example.springrestedu.controller.HomeController#corstest()
// null
// [인터셉터] afterCompletion 수행

[인터셉터] preHandle 수행은 Controller 실행 전에 출력된다.
그다음 handler 정보로 HomeController#corstest()가 출력된다.
HomeController 수행은 실제 /home 요청을 처리하는 Controller 메서드가 실행되었다는 뜻이다.


[인터셉터] postHandle 수행은 Controller 실행 후에 출력된다.
그 아래의 HomeController#corstest()는 실행된 Controller 메서드 정보이고, null은 modelAndView 출력 결과이다.
현재 HomeController는 @RestController이기 때문에 화면 이름과 데이터를 담는 ModelAndView를 사용하지 않고, 문자열을 응답 본문으로 바로 반환한다.
그래서 modelAndView가 null로 출력되는 것은 자연스러운 결과이다.


마지막으로 [인터셉터] afterCompletion 수행이 출력된다.
이 로그는 요청 처리가 끝난 뒤 마지막 정리 단계까지 Interceptor 흐름이 진행되었다는 뜻이다.


이 예제의 최종 결과물은 /home 브라우저 응답 화면과, Interceptor 등록 후 preHandle() → HomeController 수행 → postHandle() → afterCompletion() 순서가 보이는 IntelliJ 콘솔 로그이다.


핵심 정리

이 예제의 핵심은 Interceptor의 등록과 실행 순서이다.
Interceptor는 Controller 실행 전후에 공통 작업을 처리할 수 있지만, 클래스만 만든다고 자동으로 실행되지는 않는다.
WebMvcConfig에서 어떤 요청에 적용할지 등록해야 한다.


이 흐름은 아래처럼 정리할 수 있다.

  • TestInterceptor는 HandlerInterceptor를 구현한 클래스이다.
  • preHandle()은 Controller 실행 전에 동작한다.
  • preHandle()에서 return true를 해야 요청이 계속 진행된다.
  • postHandle()은 Controller 실행 후에 동작한다.
  • afterCompletion()은 요청 처리가 끝난 뒤 동작한다.
  • WebMvcConfig는 TestInterceptor를 요청 경로에 등록하는 설정 클래스이다.
  • .addPathPatterns("/home")은 /home 요청에 TestInterceptor를 적용한다는 뜻이다.
  • 현재 코드의 @Configuration이 주석 처리되어 있으면 설정이 적용되지 않을 수 있다.
  • HomeController는 /home 요청을 실제로 처리하고 문자열을 반환한다.
  • @RestController 응답에서는 modelAndView가 null로 출력될 수 있다.
  • 결과물은 /home 브라우저 응답 화면과 IntelliJ 콘솔 로그 순서로 확인한다.

따라서 이 예제가 알려 주는 핵심은 이것이다.
Interceptor는 WebMvcConfig에 등록되어야 실행되며, preHandle()로 Controller 실행 전 작업을 처리하고, postHandle()과 afterCompletion()으로 실행 후 작업과 마무리 작업을 처리할 수 있다.
다음 예제에서는 Filter와 Interceptor가 함께 있을 때 요청 흐름의 실행 순서를 비교한다.



3. Filter와 Interceptor 실행 순서 비교 정리 응용예제

이 예제는 Filter와 Interceptor가 함께 적용되었을 때 요청 처리 흐름이 어떤 순서로 진행되는지 비교하는 흐름이다.
앞에서 Filter는 Controller보다 더 앞단에서 요청 전후 작업을 처리한다고 정리했다.
그리고 Interceptor는 Spring MVC 내부에서 Controller 실행 전후 작업을 처리한다고 정리했다.


이번 예제에서는 /home 요청 하나를 기준으로 Filter, Interceptor, HomeController가 어떤 순서로 실행되는지 확인한다.
Filter는 chain.doFilter()를 기준으로 요청 전 작업과 응답 후 작업을 나눈다.
Interceptor는 preHandle(), postHandle(), afterCompletion()으로 Controller 실행 전후 흐름을 나눈다.


결과물은 브라우저 화면보다 IntelliJ 콘솔 로그 순서가 더 중요하다.
브라우저에는 HomeController가 반환한 문자열만 보이지만, 콘솔에서는 Filter, Interceptor, Controller 실행 순서를 한 번에 확인할 수 있다.


이 예제의 핵심은 Filter가 Interceptor보다 더 앞단에서 실행되고, 응답이 돌아올 때는 Interceptor 후처리가 끝난 뒤 다시 Filter 후처리로 돌아온다는 점이다.


예제 전체 코드

// TestFilter1.java
package com.example.springrestedu.filter; // 클래스가 속한 패키지 경로

import jakarta.servlet.Filter; // `Filter` 기능 구현 인터페이스
import jakarta.servlet.FilterChain; // 다음 단계로 요청을 넘기는 객체
import jakarta.servlet.ServletException; // `Servlet` 처리 중 예외 표현
import jakarta.servlet.ServletRequest; // 요청 정보를 담는 객체
import jakarta.servlet.ServletResponse; // 응답 정보를 담는 객체
import jakarta.servlet.annotation.WebFilter; // `Web Filter` 등록 애노테이션
import jakarta.servlet.http.HttpServletRequest; // `HTTP` 요청 정보를 다루는 객체
import lombok.extern.slf4j.Slf4j; // 로그 출력을 위한 `Lombok` 애노테이션
import org.springframework.core.annotation.Order; // `Filter` 실행 순서 지정 애노테이션
import org.springframework.stereotype.Component; // `Spring Bean` 등록 애노테이션
import java.io.IOException; // 입출력 처리 중 예외 표현

@Slf4j // `log.info()` 사용 설정
//@WebFilter(urlPatterns = {"/*"}) // 현재는 주석 처리되어 `Filter` 등록이 비활성화된 상태
public class TestFilter1 implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                        throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request; // `HTTP` 요청 객체로 변환
        String path = req.getRequestURI(); // 요청 `URI` 추출

        // 시스템성 요청은 로그를 남기지 않고 바로 다음 단계로 넘긴다.
        if (path.startsWith("/.well-known") || path.equals("/favicon.ico")) {
            chain.doFilter(request, response); // 다음 단계로 요청 전달
            return; // 아래 로그를 실행하지 않고 종료
        }

        log.info("[필터1] 요청 자원 수행 전"); // 요청이 다음 단계로 가기 전 실행
        chain.doFilter(request, response); // 다음 `Filter`, `Interceptor`, `Controller` 흐름으로 요청 전달
        log.info("[필터1] 요청 자원 수행 후"); // 요청 처리가 끝난 뒤 응답이 돌아올 때 실행
    }
}
// TestInterceptor.java
package com.example.springrestedu.interceptor; // 클래스가 속한 패키지 경로

import jakarta.servlet.http.HttpServletRequest; // `HTTP` 요청 정보를 다루는 객체
import jakarta.servlet.http.HttpServletResponse; // `HTTP` 응답 정보를 다루는 객체
import lombok.extern.slf4j.Slf4j; // 로그 출력을 위한 `Lombok` 애노테이션
import org.springframework.web.servlet.HandlerInterceptor; // `Interceptor` 기능 구현 인터페이스
import org.springframework.web.servlet.ModelAndView; // `Model`과 `View` 정보를 담는 객체

@Slf4j // `log.info()` 사용 설정
public class TestInterceptor implements HandlerInterceptor {
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        log.info("[인터셉터] preHandle 수행"); // `Controller` 실행 전 로그 출력
        System.out.println(handler); // 실행될 `Controller` 메서드 정보 출력
        return true; // 다음 단계로 요청 진행
    }

    public void postHandle(
            HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {
        log.info("[인터셉터] postHandle 수행"); // `Controller` 실행 후 로그 출력
        System.out.println(handler); // 실행된 `Controller` 메서드 정보 출력
        System.out.println(modelAndView); // `ModelAndView` 정보 출력
    }

    public void afterCompletion(
            HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        log.info("[인터셉터] afterCompletion 수행"); // 요청 완료 후 로그 출력
    }
}
// WebMvcConfig.java
package com.example.springrestedu.config; // 클래스가 속한 패키지 경로

import com.example.springrestedu.interceptor.TestInterceptor; // 등록할 `Interceptor` 클래스
import org.springframework.context.annotation.Configuration; // 설정 클래스 등록 애노테이션
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; // `Interceptor` 등록 객체
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; // `Spring MVC` 설정 확장 인터페이스

//@Configuration // 현재는 주석 처리되어 설정 클래스 등록이 비활성화된 상태
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TestInterceptor()) // `TestInterceptor` 등록
                .addPathPatterns("/home"); // `/home` 요청에만 `Interceptor` 적용
    }
}
// HomeController.java
package com.example.springrestedu.controller; // 클래스가 속한 패키지 경로

import org.springframework.web.bind.annotation.GetMapping; // `GET` 요청을 메서드와 연결
import org.springframework.web.bind.annotation.RestController; // `REST` 방식 `Controller` 등록

@RestController // 반환값을 응답 본문으로 바로 전달하는 `Controller`
public class HomeController {
    @GetMapping("/home") // `/home` `GET` 요청을 이 메서드와 연결
    public String corstest() {
        System.out.println("HomeController 수행"); // `Controller` 실행 확인용 콘솔 출력
        return "CORS 설정을 하지 않았어요ㅜㅜ"; // 브라우저에 반환할 문자열
    }
}

이 코드는 크게 네 파일로 나누어 볼 수 있다.
첫 번째 TestFilter1.java는 요청이 Spring MVC 내부로 들어가기 전후에 실행되는 Filter이다.
두 번째 TestInterceptor.java는 Controller 실행 전후에 실행되는 Interceptor이다.
세 번째 WebMvcConfig.java는 TestInterceptor를 /home 요청에 등록하는 설정 클래스이다.
네 번째 HomeController.java는 /home 요청을 실제로 처리하는 Controller이다.


이 예제에서 흐름을 제대로 확인하려면 Filter와 Interceptor가 모두 활성화되어 있어야 한다.
Filter는 @WebFilter 주석을 해제하거나 @Component 방식으로 등록해야 한다.
Interceptor는 WebMvcConfig의 @Configuration 주석을 해제해서 설정 클래스가 등록되도록 해야 한다.


Filter와 Interceptor는 둘 다 공통 작업을 처리하지만, 실행되는 위치가 다르기 때문에 콘솔 로그 순서도 다르게 나타난다.


이 예제에서 확인할 핵심

  • Filter는 Spring MVC 요청 처리 흐름보다 더 앞단에서 동작한다.
  • Interceptor는 Spring MVC 내부에서 Controller 실행 전후에 동작한다.
  • Filter의 요청 전 로그는 Interceptor의 preHandle()보다 먼저 출력된다.
  • Interceptor의 preHandle()은 Controller 실행 전에 출력된다.
  • HomeController 수행은 실제 Controller 메서드가 실행되었다는 로그이다.
  • Interceptor의 postHandle()은 Controller 실행 후에 출력된다.
  • Interceptor의 afterCompletion()은 요청 처리가 끝난 뒤 출력된다.
  • Filter의 요청 후 로그는 응답이 다시 Filter로 돌아올 때 출력된다.
  • 결과물은 브라우저 응답 화면보다 IntelliJ 콘솔 로그 순서로 확인하는 것이 중요하다.

이 예제는 요청이 들어갈 때와 응답이 돌아올 때 Filter, Interceptor, Controller가 어떤 순서로 실행되는지 비교하는 예제이다.


Filter와 Interceptor는 실행 위치가 다르다

Filter와 Interceptor는 모두 요청 처리 흐름 중간에서 공통 작업을 처리할 수 있다.
그래서 처음 보면 둘이 비슷해 보일 수 있다.
하지만 실행 위치는 다르다.


Filter는 Spring MVC가 요청을 본격적으로 처리하기 전, 더 앞단에서 동작한다.
즉, 요청이 DispatcherServlet이나 Controller 흐름으로 들어가기 전에 먼저 실행될 수 있다.


반면 Interceptor는 Spring MVC 내부에서 동작한다.
Spring MVC가 어떤 Controller가 요청을 처리할지 찾은 뒤, 그 Controller 실행 전후에 Interceptor가 실행된다.


쉽게 정리하면 아래처럼 볼 수 있다.

  • Filter는 요청이 Spring MVC 내부로 들어가기 전후의 공통 작업을 처리한다.
  • Interceptor는 Controller 실행 전후의 공통 작업을 처리한다.

Filter는 더 바깥쪽 흐름이고, Interceptor는 Controller와 더 가까운 안쪽 흐름이다.


요청이 들어올 때는 Filter가 Interceptor보다 먼저 실행된다

/home 요청이 들어오면 요청은 먼저 Filter를 지난다.
그래서 Filter의 요청 전 로그가 가장 먼저 출력된다.


TestFilter1에서는 아래 코드가 요청 전 작업이다.

// TestFilter1.java
log.info("[필터1] 요청 자원 수행 전"); // 요청이 다음 단계로 가기 전 실행
chain.doFilter(request, response); // 다음 단계로 요청 전달

[필터1] 요청 자원 수행 전 로그는 chain.doFilter()보다 앞에 있다.
따라서 요청이 다음 단계로 넘어가기 전에 먼저 출력된다.


그다음 chain.doFilter(request, response)가 실행된다.
이 코드가 실행되어야 요청이 다음 단계로 넘어갈 수 있다.
이후 요청은 Spring MVC 내부로 들어가고, /home 요청에 등록된 Interceptor가 실행된다.


Interceptor에서는 preHandle()이 Controller 실행 전에 먼저 실행된다.

// TestInterceptor.java
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {
    log.info("[인터셉터] preHandle 수행"); // `Controller` 실행 전 로그 출력
    System.out.println(handler); // 실행될 `Controller` 메서드 정보 출력
    return true; // 다음 단계로 요청 진행
}

그래서 요청이 들어갈 때의 순서는 아래처럼 볼 수 있다.


Filter 요청 전 로그 → Interceptor preHandle() → Controller 실행


요청이 들어갈 때는 바깥쪽에 있는 Filter가 먼저 실행되고, 그다음 안쪽에 있는 Interceptor가 실행된다.


Controller 실행 후에는 Interceptor 후처리가 먼저 진행된다

preHandle()이 return true를 반환하면 요청은 HomeController까지 진행된다.
그러면 HomeController의 corstest() 메서드가 실행된다.

// HomeController.java
@GetMapping("/home") // `/home` `GET` 요청을 이 메서드와 연결
public String corstest() {
    System.out.println("HomeController 수행"); // `Controller` 실행 확인용 콘솔 출력
    return "CORS 설정을 하지 않았어요ㅜㅜ"; // 브라우저에 반환할 문자열
}

HomeController 수행 로그가 출력되면 요청이 실제 Controller까지 도착했다는 뜻이다.


Controller 실행이 끝나면 Interceptor의 postHandle()이 실행된다.
postHandle()은 Controller 실행 후에 실행되는 후처리 단계이다.
그다음 요청 처리가 끝난 뒤 afterCompletion()이 실행된다.


이 순서는 아래처럼 볼 수 있다.


Controller 실행 → Interceptor postHandle() → Interceptor afterCompletion()


여기서 중요한 점은 Filter의 요청 후 로그가 아직 출력되지 않았다는 것이다.
아직 응답이 Filter 위치까지 완전히 돌아간 것이 아니기 때문이다.


Controller 실행이 끝난 직후에는 먼저 Interceptor의 후처리 메서드들이 실행된다.


응답이 돌아올 때는 Filter의 요청 후 코드가 마지막에 실행된다

Interceptor의 후처리 흐름이 끝나면 응답은 다시 바깥쪽으로 돌아간다.
이때 요청을 넘겼던 Filter의 chain.doFilter() 뒤 코드가 실행된다.


TestFilter1에서는 아래 코드가 요청 후 작업이다.

// TestFilter1.java
chain.doFilter(request, response); // 다음 단계로 요청 전달
log.info("[필터1] 요청 자원 수행 후"); // 요청 처리가 끝난 뒤 응답이 돌아올 때 실행

[필터1] 요청 자원 수행 후 로그는 chain.doFilter() 뒤에 있다.
그래서 Controller와 Interceptor 처리가 끝난 뒤 응답이 다시 Filter로 돌아올 때 실행된다.


따라서 전체 흐름은 아래처럼 정리할 수 있다.


Filter 요청 전 → Interceptor preHandle() → Controller 실행 → Interceptor postHandle() → Interceptor afterCompletion() → Filter 요청 후


응답이 돌아올 때는 안쪽 흐름이 먼저 끝나고, 마지막에 바깥쪽 Filter의 요청 후 코드가 실행된다.


Filter와 Interceptor가 모두 활성화되어야 비교할 수 있다

이 예제는 Filter와 Interceptor의 실행 순서를 비교하는 예제이다.
그래서 둘 중 하나만 활성화되어 있으면 전체 비교가 되지 않는다.


Filter만 활성화되어 있으면 Filter 요청 전 로그, HomeController 수행, Filter 요청 후 로그만 확인된다.
이 경우 Interceptor의 preHandle(), postHandle(), afterCompletion() 흐름은 보이지 않는다.


반대로 Interceptor만 활성화되어 있으면 preHandle(), HomeController 수행, postHandle(), afterCompletion() 흐름만 확인된다.
이 경우 Filter가 더 앞단에서 먼저 실행되고, 마지막에 다시 돌아오는 흐름은 확인할 수 없다.


따라서 이 예제의 결과물을 제대로 뽑으려면 아래 두 가지가 모두 필요하다.

  • Filter 등록 활성화
  • WebMvcConfig의 @Configuration 활성화

Filter와 Interceptor 실행 순서 비교 결과물은 두 기능이 모두 등록된 상태에서만 확인할 수 있다.


실행 흐름 정리

이 예제의 실행 흐름은 아래 순서로 진행된다.

  • Filter 등록을 활성화한다.
  • WebMvcConfig의 @Configuration 주석을 해제한다.
  • 서버를 실행한다.
  • 브라우저에서 /home 요청을 보낸다.
  • 요청이 먼저 Filter에 도착한다.
  • Filter에서 [필터1] 요청 자원 수행 전 로그가 출력된다.
  • chain.doFilter(request, response)가 다음 단계로 요청을 넘긴다.
  • Spring MVC 내부에서 TestInterceptor의 preHandle()이 실행된다.
  • preHandle()에서 [인터셉터] preHandle 수행 로그와 handler 정보가 출력된다.
  • preHandle()이 return true를 반환해서 요청이 계속 진행된다.
  • HomeController의 corstest() 메서드가 실행된다.
  • HomeController 수행 문장이 콘솔에 출력된다.
  • Controller 실행 후 postHandle()이 실행된다.
  • postHandle()에서 [인터셉터] postHandle 수행, handler, modelAndView 정보가 출력된다.
  • 요청 처리가 끝난 뒤 afterCompletion()이 실행된다.
  • 응답이 다시 Filter로 돌아온다.
  • Filter에서 [필터1] 요청 자원 수행 후 로그가 출력된다.
  • 브라우저 화면에 "CORS 설정을 하지 않았어요ㅜㅜ"가 표시된다.

흐름을 한 줄로 정리하면 다음과 같다.


Filter 요청 전 → Interceptor preHandle() → HomeController 수행 → Interceptor postHandle() → Interceptor afterCompletion() → Filter 요청 후


전체 요청 흐름은 바깥쪽 Filter에서 시작하고, 안쪽 Interceptor와 Controller 처리가 끝난 뒤 다시 바깥쪽 Filter로 돌아오는 구조이다.


결과물 뽑기

이 예제의 결과물은 Filter와 Interceptor를 모두 활성화한 뒤 확인한다.
브라우저 화면에서는 /home 요청을 보냈을 때 HomeController가 반환한 응답 문자열을 확인한다.
IntelliJ 콘솔에서는 Filter, Interceptor, HomeController 로그가 어떤 순서로 출력되는지 확인한다.


결과물을 뽑기 전에 아래 상태를 먼저 확인한다.

  • Filter가 등록되어 있어야 한다.
  • WebMvcConfig의 @Configuration이 활성화되어 있어야 한다.
  • WebMvcConfig에서 TestInterceptor가 /home 요청에 등록되어 있어야 한다.
  • HomeController가 /home 요청을 처리하고 있어야 한다.

브라우저 응답은 아래처럼 확인된다.

// 브라우저 응답
// CORS 설정을 하지 않았어요ㅜㅜ

브라우저 화면은 Controller 응답이 정상적으로 반환되었는지만 보여 준다.
Filter와 Interceptor의 실행 순서는 브라우저 화면이 아니라 IntelliJ 콘솔에서 확인해야 한다.


Filter와 Interceptor가 모두 정상 등록된 뒤 /home 요청을 보내면 콘솔 결과는 아래 흐름으로 나와야 한다.

// Filter와 Interceptor 등록 후 콘솔 결과
// [필터1] 요청 자원 수행 전
// [인터셉터] preHandle 수행
// com.example.springrestedu.controller.HomeController#corstest()
// HomeController 수행
// [인터셉터] postHandle 수행
// com.example.springrestedu.controller.HomeController#corstest()
// null
// [인터셉터] afterCompletion 수행
// [필터1] 요청 자원 수행 후

위 결과에서 가장 먼저 [필터1] 요청 자원 수행 전이 출력된다.
이 로그는 요청이 Spring MVC 내부로 들어가기 전에 Filter에서 먼저 실행된 것이다.


그다음 [인터셉터] preHandle 수행이 출력된다.
이 로그는 Controller 실행 직전에 Interceptor가 실행되었다는 뜻이다.
바로 아래의 HomeController#corstest()는 이번 요청을 처리할 Controller 메서드 정보이다.


그다음 HomeController 수행이 출력된다.
이 로그는 실제 /home 요청을 처리하는 Controller 메서드가 실행되었다는 뜻이다.


이후 [인터셉터] postHandle 수행이 출력된다.
이 로그는 Controller 실행 후 Interceptor의 후처리 단계가 실행되었다는 뜻이다.
그 아래의 HomeController#corstest()는 실행된 Controller 메서드 정보이고, null은 modelAndView 출력 결과이다.
현재 HomeController는 @RestController이기 때문에 화면 이름과 데이터를 담는 ModelAndView를 사용하지 않고, 문자열을 응답 본문으로 바로 반환한다.
그래서 modelAndView가 null로 출력되는 것은 자연스러운 결과이다.


그다음 [인터셉터] afterCompletion 수행이 출력된다.
이 로그는 요청 처리가 끝난 뒤 Interceptor의 마지막 정리 단계가 실행되었다는 뜻이다.


마지막으로 [필터1] 요청 자원 수행 후가 출력된다.
이 로그는 Controller와 Interceptor 처리가 모두 끝난 뒤 응답이 다시 Filter로 돌아왔다는 뜻이다.


이 예제의 최종 결과물은 /home 브라우저 응답 화면과, Filter 요청 전 → Interceptor preHandle() → HomeController 수행 → Interceptor postHandle() → Interceptor afterCompletion() → Filter 요청 후 순서가 보이는 IntelliJ 콘솔 로그이다.


핵심 정리

이 예제의 핵심은 Filter, Interceptor, Controller의 실행 순서를 비교하는 것이다.
Filter와 Interceptor는 모두 공통 작업을 처리할 수 있지만, 실행 위치가 다르다.
그래서 같은 /home 요청을 처리하더라도 콘솔 로그 순서가 다르게 나타난다.


이 흐름은 아래처럼 정리할 수 있다.

  • Filter는 Spring MVC 내부로 들어가기 전 더 앞단에서 실행된다.
  • Interceptor는 Spring MVC 내부에서 Controller 실행 전후에 실행된다.
  • 요청이 들어갈 때는 Filter 요청 전 로그가 가장 먼저 출력된다.
  • 그다음 Interceptor의 preHandle()이 실행된다.
  • preHandle()이 return true를 반환하면 Controller가 실행된다.
  • Controller 실행 후 Interceptor의 postHandle()이 실행된다.
  • 요청 처리가 끝난 뒤 Interceptor의 afterCompletion()이 실행된다.
  • 응답이 다시 바깥쪽으로 돌아오면 Filter 요청 후 로그가 마지막에 출력된다.
  • @RestController 응답에서는 modelAndView가 null로 출력될 수 있다.
  • 실행 순서 비교 결과물은 브라우저 화면이 아니라 IntelliJ 콘솔 로그로 확인해야 한다.

따라서 이 예제가 알려 주는 핵심은 이것이다.
Filter는 요청 흐름의 바깥쪽에서 먼저 실행되고 마지막에 다시 돌아오며, Interceptor는 그 안쪽에서 Controller 실행 전후를 나누어 처리한다.
이 흐름을 이해하면 요청이 들어와서 응답으로 돌아가기까지 공통 처리 코드가 어느 위치에서 실행되는지 구분할 수 있다.



4. GET 요청으로 문자열, 경로값, 요청 파라미터 받기 응용예제

이 예제는 GET 요청으로 서버에 값을 전달하고, Controller가 그 값을 어떻게 받는지 확인하는 흐름이다.
GET은 주로 데이터를 조회할 때 사용하는 요청 방식이다.
브라우저 주소창에 주소를 입력해서도 테스트할 수 있지만, 이번 응용예제 결과물은 Talend API Tester 기준으로 확인한다.


이번 예제에서는 /restapi로 시작하는 여러 GET 요청을 확인한다.
단순 문자열 응답, 경로에 들어간 값 받기, 주소 뒤에 붙는 요청 파라미터 받기, 요청 파라미터를 MemberDTO 객체로 받기, ResponseEntity로 응답 상태와 본문을 함께 반환하는 흐름까지 확인한다.


결과물은 각 요청 설명 바로 아래에서 Talend API Tester 화면으로 확인한다.
Talend API Tester를 사용하면 요청 방식, 요청 URL, 응답 본문, 응답 상태 코드, 응답 헤더를 한 화면에서 함께 확인할 수 있다.


이 예제의 핵심은 GET 요청에서 값이 들어오는 위치가 경로값인지, 요청 파라미터인지, 객체 바인딩인지에 따라 Controller 메서드의 받는 방식이 달라진다는 점이다.


예제 전체 코드

// GetController.java
package com.example.springrestedu.controller; // 클래스가 속한 패키지 경로

import java.nio.charset.Charset; // 문자 인코딩 설정에 사용하는 클래스
import java.util.Map; // 여러 요청 파라미터를 key-value 형태로 받기 위한 클래스
import lombok.extern.slf4j.Slf4j; // 로그 출력을 위한 `Lombok` 애노테이션
import org.springframework.http.HttpHeaders; // 응답 헤더를 설정하기 위한 클래스
import org.springframework.http.HttpStatus; // 응답 상태 코드를 표현하는 클래스
import org.springframework.http.MediaType; // 응답 데이터 형식을 표현하는 클래스
import org.springframework.http.ResponseEntity; // 응답 본문, 헤더, 상태 코드를 함께 반환하는 클래스
import org.springframework.web.bind.annotation.*; // 요청 매핑과 요청값 처리를 위한 애노테이션 모음
import com.example.springrestedu.domain.MemberDTO; // 요청 파라미터를 객체로 담기 위한 `DTO`

@RestController // 반환값을 응답 본문으로 바로 전달하는 `Controller`
@RequestMapping("/restapi") // 이 `Controller`의 공통 요청 경로
@Slf4j // `log.info()` 사용 설정
public class GetController {
    @RequestMapping(value = "/hello", method = RequestMethod.GET) // `/restapi/hello` `GET` 요청 처리
    public String getHello() {
        log.info("getHello 메소드가 호출되었습니다."); // 메서드 호출 로그 출력
        return "안녕하세요?"; // 문자열 응답 반환
    }

    @GetMapping(value = "/name") // `/restapi/name` `GET` 요청 처리
    public String getName() {
        log.info("getName 메소드가 호출되었습니다."); // 메서드 호출 로그 출력
        return "<h1>둘리</h1><hr><img src='/images/dooly.png'>"; // `HTML` 형태의 문자열 응답 반환
    }

    @GetMapping(value = "/var1/{variable}") // `/restapi/var1/값` 형태의 요청 처리
    public String getVariable1(@PathVariable String variable) {
        log.info("@PathVariable을 통해 들어온 값 : {}", variable); // 경로값 로그 출력
        return variable; // 경로로 받은 값 반환
    }

    @GetMapping(value = "/var2/{variable}") // `/restapi/var2/값` 형태의 요청 처리
    public String getVariable2(@PathVariable("variable") String var) {
        return "내 친구 " + var; // 경로로 받은 값을 문장에 붙여 반환
    }

    @GetMapping(value = "/req1") // `/restapi/req1` 요청 처리
    public String getRequestParam1(String name, String email, String phone) {
        return name + " " + email + " " + phone; // 요청 파라미터를 각각 받아 문자열로 반환
    }

    @GetMapping(value = "/req2") // `/restapi/req2` 요청 처리
    public String getRequestParam2(@RequestParam Map<String, String> param) {
        StringBuilder sb = new StringBuilder(); // 응답 문자열을 누적할 객체 생성
        param.entrySet().forEach(map -> {
            sb.append(map.getKey() + " - " + map.getValue() + "<br>"); // key-value 형태로 문자열 누적
        });
        return sb.toString(); // 완성된 문자열 반환
    }

    @GetMapping(value = "/req3") // `/restapi/req3` 요청 처리
    public String getRequestParam3(MemberDTO memberDTO) {
        return memberDTO.toString(); // 요청 파라미터가 담긴 객체를 문자열로 반환
    }

    @GetMapping(value = "/req4") // `/restapi/req4` 요청 처리
    public MemberDTO getRequestParam4(MemberDTO memberDTO) {
        return memberDTO; // 요청 파라미터가 담긴 객체를 `JSON` 형태로 반환
    }

    @GetMapping(value = "/req5") // `/restapi/req5` 요청 처리
    public ResponseEntity getRequestParam5(String name) {
        MemberDTO dto = new MemberDTO(); // 응답으로 보낼 `DTO` 객체 생성
        dto.setName(name); // 요청 파라미터로 받은 이름 저장
        dto.setEmail("aaa@naver.com"); // 고정 이메일 저장
        dto.setPhone("010-3333-4444"); // 고정 전화번호 저장

        HttpHeaders header = new HttpHeaders(); // 응답 헤더 객체 생성
        header.setContentType(new MediaType("application", "json", Charset.forName("UTF-8"))); // 응답 형식 설정
        return new ResponseEntity<>(dto, header, HttpStatus.OK); // 본문, 헤더, 상태 코드 반환
    }
}
// MemberDTO.java
package com.example.springrestedu.domain; // 클래스가 속한 패키지 경로

import lombok.Getter; // 필드 값을 읽는 메서드 자동 생성
import lombok.Setter; // 필드 값을 저장하거나 수정하는 메서드 자동 생성

@Getter // getter 메서드 자동 생성
@Setter // setter 메서드 자동 생성
public class MemberDTO {
    private String name; // 이름 저장
    private String email; // 이메일 저장
    private String phone; // 전화번호 저장

    @Override
    public String toString() {
        return "MemberDTO 객체 {" +
            "name='" + name + '\'' +
            ", email='" + email + '\'' +
            ", phone='" + phone + '\'' +
            '}'; // 객체 안의 값을 문자열로 반환
    }
}

이 코드는 크게 두 파일로 나누어 볼 수 있다.
첫 번째 GetController.java는 여러 GET 요청을 처리하는 Controller이다.
두 번째 MemberDTO.java는 요청 파라미터를 객체로 묶어 담을 때 사용하는 데이터 객체이다.


GetController에는 @RestController가 붙어 있다.
@RestController는 메서드가 반환하는 값을 화면 이름으로 해석하지 않고, 응답 본문으로 바로 반환한다.
그래서 문자열을 반환하면 문자열이 응답으로 나가고, 객체를 반환하면 JSON 형태로 변환되어 응답될 수 있다.


@RequestMapping("/restapi")는 이 Controller의 공통 주소를 정한다.
따라서 메서드에 /hello가 붙어 있으면 실제 요청 주소는 /restapi/hello가 된다.
/req1이 붙어 있으면 실제 요청 주소는 /restapi/req1이 된다.


이 예제는 하나의 Controller 안에서 GET 요청값을 받는 여러 방식을 한 번에 비교하는 예제이다.


이 예제에서 확인할 핵심

  • GET 요청은 주소에 값을 실어 서버로 전달할 수 있다.
  • @RestController는 반환값을 응답 본문으로 바로 보낸다.
  • @RequestMapping("/restapi")는 공통 요청 경로를 정한다.
  • @RequestMapping(method = RequestMethod.GET)은 GET 요청을 처리한다.
  • @GetMapping은 GET 요청을 더 간단하게 연결하는 애노테이션이다.
  • @PathVariable은 주소 경로에 들어간 값을 받는다.
  • 요청 파라미터는 주소 뒤의 ?name=값&email=값 형태로 전달된다.
  • 단순 매개변수로 요청 파라미터를 각각 받을 수 있다.
  • @RequestParam Map으로 요청 파라미터 전체를 한 번에 받을 수 있다.
  • MemberDTO로 요청 파라미터를 객체에 묶어 받을 수 있다.
  • 객체를 반환하면 JSON 형태로 응답될 수 있다.
  • ResponseEntity를 사용하면 응답 본문, 헤더, 상태 코드를 함께 정할 수 있다.
  • 결과물은 Talend API Tester에서 요청 방식, 요청 URL, 응답 본문, 응답 상태 코드를 기준으로 확인한다.

이 예제는 GET 요청에서 값이 어디에 들어오고, 그 값을 Controller에서 어떤 형태로 받을 수 있는지 확인하는 예제이다.


GET 요청은 조회 흐름을 확인하기 좋다

GET은 주로 서버에서 데이터를 조회할 때 사용하는 요청 방식이다.
브라우저 주소창에 URL을 입력하면 기본적으로 GET 요청이 보내진다.
그래서 GET 예제는 브라우저만으로도 확인할 수 있다.


하지만 이번 응용예제 결과물은 Talend API Tester로 확인한다.
Talend API Tester를 사용하면 요청 방식이 GET인지 확인할 수 있고, 요청 URL, 응답 본문, 응답 상태 코드까지 한 화면에서 같이 확인할 수 있다.


예를 들어 아래 주소로 GET 요청을 보내면 /restapi/hello 요청이 서버로 간다.

// GET 요청 주소
// http://localhost:9000/restapi/hello

서버는 이 주소와 연결된 Controller 메서드를 찾는다.
이 예제에서는 GetController의 getHello() 메서드가 실행된다.


GET 요청은 데이터를 보낼 수도 있다.
단, 보통 요청 본문에 담기보다는 주소에 값을 붙여 보낸다.
주소 경로에 값을 넣을 수도 있고, 주소 뒤에 ?name=둘리 같은 요청 파라미터를 붙일 수도 있다.


GET 요청은 주소를 통해 어떤 데이터를 요청하는지 드러나기 때문에, 조회 요청과 간단한 테스트에 자주 사용된다.


공통 경로는 @RequestMapping("/restapi")로 정한다

// GetController.java
@RestController // 반환값을 응답 본문으로 바로 전달하는 `Controller`
@RequestMapping("/restapi") // 이 `Controller`의 공통 요청 경로
@Slf4j // `log.info()` 사용 설정
public class GetController {
}

GetController에는 @RequestMapping("/restapi")가 붙어 있다.
이 애노테이션은 이 Controller 안의 모든 요청 주소 앞에 /restapi를 붙이겠다는 뜻이다.


예를 들어 메서드에 /hello가 붙어 있으면 실제 주소는 /restapi/hello가 된다.
메서드에 /req1이 붙어 있으면 실제 주소는 /restapi/req1이 된다.


이렇게 공통 경로를 클래스 위에 붙이면 메서드마다 /restapi를 반복해서 쓰지 않아도 된다.
요청 주소를 기능 단위로 묶어 관리할 수 있다.


클래스 위의 @RequestMapping("/restapi")는 이 Controller에 들어 있는 요청들의 공통 시작 주소를 정한다.


기본 GET 요청으로 문자열 반환하기

// GetController.java
@RequestMapping(value = "/hello", method = RequestMethod.GET) // `/restapi/hello` `GET` 요청 처리
public String getHello() {
    log.info("getHello 메소드가 호출되었습니다."); // 메서드 호출 로그 출력
    return "안녕하세요?"; // 문자열 응답 반환
}

getHello()는 /restapi/hello 주소로 들어오는 GET 요청을 처리한다.
여기서는 @RequestMapping에 method = RequestMethod.GET을 지정했다.
즉, 같은 /hello 주소라도 GET 방식일 때만 이 메서드가 실행된다.


이 메서드는 문자열 "안녕하세요?"를 반환한다.
GetController가 @RestController이기 때문에 이 문자열은 화면 이름이 아니라 응답 본문으로 바로 나간다.


Talend API Tester에서 요청 방식은 GET으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/restapi/hello

예상 응답은 아래와 같다.

// 응답 본문
// 안녕하세요?

Talend API Tester 결과를 보면 요청 방식이 GET이고, 요청 URL이 /restapi/hello로 들어간다.
응답 상태는 200이고, 응답 본문에는 "안녕하세요?"가 출력된다.
따라서 /restapi/hello 요청이 getHello() 메서드와 정상적으로 연결된 것을 확인할 수 있다.


@RestController에서 문자열을 반환하면 그 문자열 자체가 응답 본문으로 전달된다.


@GetMapping으로 GET 요청을 더 간단하게 연결한다

// GetController.java
@GetMapping(value = "/name") // `/restapi/name` `GET` 요청 처리
public String getName() {
    log.info("getName 메소드가 호출되었습니다."); // 메서드 호출 로그 출력
    return "<h1>둘리</h1><hr><img src='/images/dooly.png'>"; // `HTML` 형태의 문자열 응답 반환
}

@GetMapping은 GET 요청을 처리하는 메서드에 붙이는 애노테이션이다.
@RequestMapping(method = RequestMethod.GET)을 더 짧게 쓴 형태라고 이해하면 된다.


위 메서드는 /restapi/name 주소로 들어오는 GET 요청을 처리한다.
반환값은 HTML 태그처럼 생긴 문자열이다.


중요한 점은 이 메서드도 화면 파일을 찾는 것이 아니라, 문자열을 응답 본문으로 반환한다는 것이다.
@RestController에서는 반환한 문자열이 응답으로 나간다.
Talend API Tester에서 응답 본문을 보면 HTML 문자열이 그대로 반환되는지 확인할 수 있다.


요청 주소는 아래와 같다.

// 요청 주소
// http://localhost:9000/restapi/name

예상 응답 내용은 아래와 같다.

// 응답 본문
// <h1>둘리</h1><hr><img src='/images/dooly.png'>

Talend API Tester 결과를 보면 /restapi/name 요청의 응답 상태는 200이다.
응답 본문에는 <h1>둘리</h1><hr><img src='/images/dooly.png'> 문자열이 그대로 출력된다.
즉, @RestController에서는 이 값을 화면 파일 이름으로 보지 않고 응답 본문 문자열로 반환한다.


@GetMapping은 GET 요청을 메서드와 연결할 때 가장 자주 쓰는 방식이다.


@PathVariable은 주소 경로에 들어간 값을 받는다

// GetController.java
@GetMapping(value = "/var1/{variable}") // `/restapi/var1/값` 형태의 요청 처리
public String getVariable1(@PathVariable String variable) {
    log.info("@PathVariable을 통해 들어온 값 : {}", variable); // 경로값 로그 출력
    return variable; // 경로로 받은 값 반환
}

@PathVariable은 주소 경로에 들어간 값을 메서드 매개변수로 받을 때 사용한다.
여기서 매개변수는 메서드가 외부에서 값을 받기 위해 열어 둔 자리이다.


/var1/{variable}에서 {variable}은 고정된 글자가 아니다.
그 자리에 실제 값이 들어올 수 있다는 뜻이다.
예를 들어 /restapi/var1/둘리로 요청하면 둘리가 variable에 들어간다.


Talend API Tester에서는 요청 방식은 GET으로 선택하고, 아래 주소를 입력한다.

// 요청 주소
// http://localhost:9000/restapi/var1/둘리

예상 응답은 아래와 같다.

// 응답 본문
// 둘리

Talend API Tester 결과를 보면 요청 URL의 마지막 경로에 둘리가 들어가 있다.
응답 본문에도 둘리가 그대로 출력된다.
따라서 /var1/{variable}의 {variable} 자리에 들어간 값이 @PathVariable을 통해 메서드 매개변수로 전달된 것을 확인할 수 있다.


@PathVariable은 주소의 일부를 값으로 받아야 할 때 사용한다.


@PathVariable 이름을 다르게 받을 수도 있다

// GetController.java
@GetMapping(value = "/var2/{variable}") // `/restapi/var2/값` 형태의 요청 처리
public String getVariable2(@PathVariable("variable") String var) {
    return "내 친구 " + var; // 경로로 받은 값을 문장에 붙여 반환
}

이번에는 주소 경로의 이름과 메서드 매개변수 이름이 다르다.
주소에는 {variable}이라고 되어 있지만, 메서드 안에서는 var라는 이름으로 값을 받는다.


이럴 때는 @PathVariable("variable")처럼 주소 경로의 변수 이름을 직접 지정한다.
그러면 /restapi/var2/또치에서 또치가 var에 들어간다.


Talend API Tester에서는 요청 방식은 GET으로 선택하고, 아래 주소를 입력한다.

// 요청 주소
// http://localhost:9000/restapi/var2/또치

예상 응답은 아래와 같다.

// 응답 본문
// 내 친구 또치

Talend API Tester 결과를 보면 /restapi/var2/또치 요청의 응답 본문이 내 친구 또치로 출력된다.
주소 경로의 {variable} 값이 메서드 안의 var 매개변수로 연결되었기 때문에 이런 응답이 만들어진다.


주소의 변수 이름과 메서드 매개변수 이름이 같으면 @PathVariable String variable처럼 쓸 수 있다.
하지만 이름이 다르면 @PathVariable("variable") String var처럼 연결 기준을 명확히 적어야 한다.


경로 변수 이름과 매개변수 이름이 다를 때는 @PathVariable("경로변수명")으로 어떤 값을 받을지 지정한다.


요청 파라미터를 각각의 변수로 받기

// GetController.java
@GetMapping(value = "/req1") // `/restapi/req1` 요청 처리
public String getRequestParam1(String name, String email, String phone) {
    return name + " " + email + " " + phone; // 요청 파라미터를 각각 받아 문자열로 반환
}

요청 파라미터는 주소 뒤에 ?를 붙이고 전달하는 값이다.
여러 값을 보낼 때는 &로 이어 붙인다.


예를 들어 아래 주소를 보면 name, email, phone이라는 값이 전달된다.

// 요청 주소
// http://localhost:9000/restapi/req1?name=둘리&email=dooly@test.com&phone=010-1111-2222

이 요청이 들어오면 name에는 둘리, email에는 dooly@test.com, phone에는 010-1111-2222가 들어간다.
메서드 매개변수 이름과 요청 파라미터 이름이 같기 때문에 자동으로 연결된다.


Talend API Tester에서는 주소에 요청 파라미터를 직접 붙여서 보내도 되고, 파라미터 입력 영역이 있다면 name, email, phone을 각각 입력해서 보내도 된다.
결국 서버로 전달되는 요청 주소는 같은 의미가 된다.


예상 응답은 아래와 같다.

// 응답 본문
// 둘리 dooly@test.com 010-1111-2222

Talend API Tester 결과를 보면 QUERY PARAMETERS 영역에 name, email, phone 값이 각각 들어가 있다.
응답 본문에는 둘리 dooly@test.com 010-1111-2222가 출력된다.
따라서 요청 파라미터 이름과 메서드 매개변수 이름이 일치하면 각각의 변수로 값이 전달되는 것을 확인할 수 있다.


여기서는 @RequestParam을 직접 붙이지 않았지만, 단순 타입 매개변수는 요청 파라미터와 이름이 맞으면 값이 들어올 수 있다.
다만 코드의 의도를 더 분명히 보여 주고 싶다면 @RequestParam String name처럼 애노테이션을 붙여 쓸 수도 있다.


요청 파라미터 이름과 메서드 매개변수 이름이 같으면 주소 뒤의 값이 각 변수에 연결될 수 있다.


@RequestParam Map으로 요청 파라미터 전체 받기

// GetController.java
@GetMapping(value = "/req2") // `/restapi/req2` 요청 처리
public String getRequestParam2(@RequestParam Map<String, String> param) {
    StringBuilder sb = new StringBuilder(); // 응답 문자열을 누적할 객체 생성
    param.entrySet().forEach(map -> {
        sb.append(map.getKey() + " - " + map.getValue() + "<br>"); // key-value 형태로 문자열 누적
    });
    return sb.toString(); // 완성된 문자열 반환
}

요청 파라미터가 여러 개일 때 하나씩 변수로 받지 않고, Map으로 한 번에 받을 수도 있다.
Map은 key와 value를 한 쌍으로 저장하는 구조이다.
여기서는 요청 파라미터 이름이 key가 되고, 요청 파라미터 값이 value가 된다.


예를 들어 아래 요청을 보낸다고 해 보자.

// 요청 주소
// http://localhost:9000/restapi/req2?name=둘리&email=dooly@test.com&phone=010-1111-2222

이 요청은 Map 안에 대략 아래처럼 담긴다.

// Map에 담기는 값 예시
// name - 둘리
// email - dooly@test.com
// phone - 010-1111-2222

코드에서는 param.entrySet()으로 Map 안의 값을 하나씩 꺼낸다.
그리고 StringBuilder에 key - value<br> 형태로 누적한다.


StringBuilder는 문자열을 여러 번 이어 붙일 때 사용하는 객체이다.
문자열을 계속 더하는 것보다 누적 흐름을 표현하기 좋다.


예상 응답은 아래처럼 볼 수 있다.

// 응답 본문 예시
// name - 둘리<br>email - dooly@test.com<br>phone - 010-1111-2222<br>

Talend API Tester 결과를 보면 요청 파라미터 name, email, phone이 모두 전달되었다.
응답 본문에는 name - 둘리<br>email - dooly@test.com<br>phone - 010-1111-2222<br> 형태로 출력된다.
Talend API Tester에서는 응답 문자열 안의 <br> 태그가 그대로 보인다.
브라우저에서 보면 <br>이 줄바꿈처럼 렌더링될 수 있지만, 요청 도구에서는 응답 본문에 포함된 문자열로 확인된다.


요청 파라미터 개수가 많거나 이름을 한 번에 확인하고 싶을 때는 @RequestParam Map 방식이 유용하다.


요청 파라미터를 MemberDTO 객체로 묶어 받기

// GetController.java
@GetMapping(value = "/req3") // `/restapi/req3` 요청 처리
public String getRequestParam3(MemberDTO memberDTO) {
    return memberDTO.toString(); // 요청 파라미터가 담긴 객체를 문자열로 반환
}

요청 파라미터는 객체로도 받을 수 있다.
이 예제에서는 MemberDTO 객체로 요청 파라미터를 받는다.


MemberDTO에는 name, email, phone 필드가 있다.
요청 파라미터 이름도 name, email, phone이면 Spring이 이 값을 객체에 넣어 준다.
이때 MemberDTO에는 값을 저장할 수 있는 setter 메서드가 필요하다.
현재 코드는 @Setter를 사용해서 setter 메서드를 자동으로 만든다.


요청 주소는 아래와 같다.

// 요청 주소
// http://localhost:9000/restapi/req3?name=둘리&email=dooly@test.com&phone=010-1111-2222

예상 응답은 아래와 같다.

// 응답 본문
// MemberDTO 객체 {name='둘리', email='dooly@test.com', phone='010-1111-2222'}

Talend API Tester 결과를 보면 name, email, phone 요청 파라미터가 전달되었고, 응답 본문에는 MemberDTO 객체 {name='둘리', email='dooly@test.com', phone='010-1111-2222'}가 출력된다.
이는 요청 파라미터가 MemberDTO 객체에 담긴 뒤, toString() 결과로 반환되었다는 뜻이다.


이 방식은 요청값이 여러 개일 때 특히 편하다.
매개변수를 String name, String email, String phone처럼 여러 개 나열하지 않아도 된다.
대신 관련 있는 값을 하나의 객체로 묶어서 받을 수 있다.


요청 파라미터 이름과 DTO 필드 이름이 맞으면 여러 요청값을 하나의 객체로 묶어 받을 수 있다.


MemberDTO는 요청값을 담는 객체이다

// MemberDTO.java
@Getter // getter 메서드 자동 생성
@Setter // setter 메서드 자동 생성
public class MemberDTO {
    private String name; // 이름 저장
    private String email; // 이메일 저장
    private String phone; // 전화번호 저장

    @Override
    public String toString() {
        return "MemberDTO 객체 {" +
            "name='" + name + '\'' +
            ", email='" + email + '\'' +
            ", phone='" + phone + '\'' +
            '}'; // 객체 안의 값을 문자열로 반환
    }
}

MemberDTO는 회원 한 명의 요청값을 담기 위한 객체이다.
DTO는 데이터를 옮기기 위한 객체라고 이해하면 된다.
여기서는 클라이언트가 보낸 name, email, phone 값을 Controller에서 하나의 객체로 받기 위해 사용한다.


@Getter는 값을 읽는 메서드를 자동으로 만든다.
@Setter는 값을 저장하거나 수정하는 메서드를 자동으로 만든다.
요청 파라미터를 객체에 넣으려면 각 필드에 값을 저장할 수 있어야 하므로 setter가 필요하다.


toString()은 객체 안의 값을 문자열로 확인하기 위해 작성한 메서드이다.
req3에서는 memberDTO.toString()을 반환하기 때문에 MemberDTO 객체 {name='...', email='...', phone='...'} 형태로 응답이 나온다.


MemberDTO는 요청 파라미터 여러 개를 하나의 의미 있는 단위로 묶어 받기 위한 객체이다.


객체를 그대로 반환하면 JSON 응답으로 변환된다

// GetController.java
@GetMapping(value = "/req4") // `/restapi/req4` 요청 처리
public MemberDTO getRequestParam4(MemberDTO memberDTO) {
    return memberDTO; // 요청 파라미터가 담긴 객체를 `JSON` 형태로 반환
}

req4도 요청 파라미터를 MemberDTO로 받는다.
하지만 req3과 차이가 있다.
req3은 memberDTO.toString()을 반환한다.
그래서 응답이 문자열이다.


반면 req4는 MemberDTO 객체 자체를 반환한다.
@RestController에서 객체를 반환하면 Spring이 객체를 JSON 형태로 변환해서 응답할 수 있다.
JSON은 데이터를 key-value 형태로 표현하는 형식이다.


요청 주소는 아래와 같다.

// 요청 주소
// http://localhost:9000/restapi/req4?name=둘리&email=dooly@test.com&phone=010-1111-2222

예상 응답은 아래와 같다.

// 응답 본문
// {"name":"둘리","email":"dooly@test.com","phone":"010-1111-2222"}

Talend API Tester 결과를 보면 응답 상태는 200이고, 응답 본문은 JSON 형태로 출력된다.
화면에서는 email, name, phone 순서로 보이지만, 중요한 것은 순서가 아니다.
name에 둘리, email에 dooly@test.com, phone에 010-1111-2222 값이 모두 들어왔는지를 확인해야 한다.


여기서 req3과 req4의 차이를 정리하면 다음과 같다.

  • req3은 MemberDTO를 문자열로 바꿔 반환한다.
  • req4는 MemberDTO 객체를 그대로 반환한다.
  • req3 응답은 toString() 결과이다.
  • req4 응답은 JSON 형태이다.

@RestController에서 객체를 반환하면 문자열 설명이 아니라 데이터 응답인 JSON으로 변환될 수 있다.


ResponseEntity로 응답 본문, 헤더, 상태 코드를 함께 반환한다

// GetController.java
@GetMapping(value = "/req5") // `/restapi/req5` 요청 처리
public ResponseEntity getRequestParam5(String name) {
    MemberDTO dto = new MemberDTO(); // 응답으로 보낼 `DTO` 객체 생성
    dto.setName(name); // 요청 파라미터로 받은 이름 저장
    dto.setEmail("aaa@naver.com"); // 고정 이메일 저장
    dto.setPhone("010-3333-4444"); // 고정 전화번호 저장

    HttpHeaders header = new HttpHeaders(); // 응답 헤더 객체 생성
    header.setContentType(new MediaType("application", "json", Charset.forName("UTF-8"))); // 응답 형식 설정
    return new ResponseEntity<>(dto, header, HttpStatus.OK); // 본문, 헤더, 상태 코드 반환
}

req5는 ResponseEntity를 사용한다.
ResponseEntity는 응답 본문뿐 아니라 응답 헤더와 상태 코드까지 함께 정할 수 있는 객체이다.


이 메서드는 요청 파라미터로 name만 받는다.
그리고 서버 안에서 MemberDTO 객체를 새로 만든다.
name에는 요청으로 받은 값을 넣고, email과 phone은 코드 안에서 고정값으로 넣는다.


그다음 HttpHeaders를 만들어 응답 데이터 형식을 설정한다.
header.setContentType(...)은 응답의 Content-Type을 정하는 코드이다.
여기서는 application/json과 UTF-8을 지정한다.


마지막으로 new ResponseEntity<>(dto, header, HttpStatus.OK)를 반환한다.
이 코드는 응답 본문으로 dto, 응답 헤더로 header, 응답 상태 코드로 200 OK를 반환한다는 뜻이다.


요청 주소는 아래와 같다.

// 요청 주소
// http://localhost:9000/restapi/req5?name=둘리

예상 응답 본문은 아래와 같다.

// 응답 본문
// {"name":"둘리","email":"aaa@naver.com","phone":"010-3333-4444"}

상태 코드는 아래처럼 확인한다.

// 응답 상태
// 200 OK

Talend API Tester 결과를 보면 응답 상태는 200이다.
응답 본문은 JSON 형태로 출력되고, name에는 요청 파라미터로 보낸 둘리가 들어간다.
email과 phone은 요청으로 보낸 값이 아니라 코드에서 직접 넣은 고정값이다.
따라서 email은 aaa@naver.com, phone은 010-3333-4444로 출력된다.


또 응답 헤더의 Content-Type을 보면 application/json;charset=UTF-8로 확인된다.
이 결과를 통해 ResponseEntity가 응답 본문, 응답 헤더, 응답 상태 코드를 함께 구성한다는 점을 확인할 수 있다.


ResponseEntity는 응답 데이터뿐 아니라 상태 코드와 헤더까지 직접 정해야 할 때 사용한다.


GET 요청값을 받는 방식 비교하기

이번 예제에서는 GET 요청값을 여러 방식으로 받았다.
각 방식은 값이 들어오는 위치와 받는 형태가 다르다.


@PathVariable은 주소 경로의 일부를 값으로 받는다.
예를 들어 /restapi/var1/둘리에서 둘리가 값이다.
이 방식은 특정 자원을 주소로 구분할 때 자주 사용한다.


요청 파라미터는 주소 뒤에 ?로 붙는 값이다.
예를 들어 /restapi/req1?name=둘리에서 name=둘리가 요청 파라미터이다.
간단한 검색 조건이나 조회 조건을 전달할 때 자주 사용한다.


MemberDTO 객체 바인딩은 여러 요청 파라미터를 하나의 객체로 묶어 받는 방식이다.
name, email, phone처럼 서로 관련 있는 값이 함께 들어올 때 사용하기 좋다.


ResponseEntity는 요청값을 받는 방식이라기보다 응답을 만드는 방식이다.
응답 본문, 응답 헤더, 응답 상태 코드를 직접 제어해야 할 때 사용한다.


정리하면 아래처럼 볼 수 있다.

  • @PathVariable은 주소 경로의 값을 받는다.
  • 단순 매개변수는 요청 파라미터를 각각 받는다.
  • @RequestParam Map은 요청 파라미터 전체를 한 번에 받는다.
  • DTO는 관련 있는 요청 파라미터를 객체로 묶어 받는다.
  • ResponseEntity는 응답 본문, 헤더, 상태 코드를 함께 구성한다.

요청값을 받는 방식은 하나만 있는 것이 아니라, 값이 들어오는 위치와 응답 처리 방식에 따라 다르게 선택한다.


실행 흐름 정리

이 예제의 실행 흐름은 아래 순서로 진행된다.

  • 서버를 실행한다.
  • Talend API Tester에서 요청 방식을 GET으로 선택한다.
  • 요청 URL에 /restapi/hello를 입력하고 요청을 보낸다.
  • GetController의 getHello() 메서드가 실행된다.
  • 문자열 "안녕하세요?"가 응답 본문으로 반환된다.
  • /restapi/var1/둘리 요청을 보내면 @PathVariable로 둘리가 전달된다.
  • /restapi/req1?name=둘리&email=...&phone=... 요청을 보내면 요청 파라미터가 각각의 변수에 들어간다.
  • /restapi/req2?... 요청을 보내면 요청 파라미터 전체가 Map에 담긴다.
  • /restapi/req3?... 요청을 보내면 요청 파라미터가 MemberDTO 객체에 담기고 toString() 결과가 반환된다.
  • /restapi/req4?... 요청을 보내면 요청 파라미터가 MemberDTO 객체에 담기고 객체가 JSON으로 반환된다.
  • /restapi/req5?name=둘리 요청을 보내면 ResponseEntity가 본문, 헤더, 상태 코드를 함께 반환한다.

흐름을 한 줄로 정리하면 다음과 같다.


GET 요청 → 요청 주소 또는 요청 파라미터 전달 → Controller 메서드 실행 → 값 바인딩 → 문자열, 객체, ResponseEntity 응답 반환


GET 요청은 주소를 통해 값을 전달하고, Controller는 그 값을 경로값, 파라미터, 객체 형태로 받아 응답을 만든다.


핵심 정리

이 예제의 핵심은 GET 요청에서 값을 받는 여러 방식을 비교하는 것이다.
GetController는 /restapi 공통 경로 아래에서 여러 GET 요청을 처리한다.
각 메서드는 요청값을 받는 방식과 응답을 만드는 방식이 조금씩 다르다.


이 흐름은 아래처럼 정리할 수 있다.

  • GET 요청은 주소를 통해 값을 전달할 수 있다.
  • @RestController는 반환값을 응답 본문으로 바로 보낸다.
  • @RequestMapping("/restapi")는 공통 요청 경로를 정한다.
  • @RequestMapping(method = RequestMethod.GET)은 GET 요청을 처리한다.
  • @GetMapping은 GET 요청을 더 간단하게 연결한다.
  • @PathVariable은 주소 경로에 포함된 값을 받는다.
  • 요청 파라미터는 주소 뒤에 ?name=값 형태로 전달된다.
  • 단순 매개변수로 요청 파라미터를 각각 받을 수 있다.
  • @RequestParam Map으로 요청 파라미터 전체를 한 번에 받을 수 있다.
  • MemberDTO로 요청 파라미터 여러 개를 객체에 묶어 받을 수 있다.
  • MemberDTO를 문자열로 반환하면 toString() 결과가 응답된다.
  • MemberDTO 객체를 그대로 반환하면 JSON 형태로 응답될 수 있다.
  • ResponseEntity는 응답 본문, 헤더, 상태 코드를 함께 정할 수 있다.
  • 이번 응용예제 결과물은 Talend API Tester에서 요청 방식, 요청 URL, 응답 본문, 응답 상태 코드를 기준으로 확인한다.

따라서 이 예제가 알려 주는 핵심은 이것이다.
GET 요청에서는 주소 경로와 요청 파라미터로 값을 전달할 수 있고, Controller는 그 값을 단순 변수, Map, DTO 객체로 받아 응답을 만들 수 있다.
다음 예제에서는 POST 요청으로 JSON과 form 데이터를 받는 흐름을 확인한다.



5. POST 요청으로 JSON과 form 데이터 받기 응용예제

이 예제는 POST 요청으로 클라이언트가 보낸 데이터를 서버가 어떻게 받는지 확인하는 흐름이다.
POST는 주로 데이터를 새로 등록하거나, 요청 본문에 데이터를 담아 서버로 보낼 때 사용하는 요청 방식이다.
GET 요청은 보통 주소에 값을 붙여 보내지만, POST 요청은 요청 본문인 Body에 데이터를 담아 보내는 경우가 많다.


이번 예제에서는 /restapi로 시작하는 POST 요청을 확인한다.
단순 POST 문자열 응답, JSON 요청 본문을 Map으로 받기, JSON 요청 본문을 MemberDTO 객체로 받기, @RequestBody 없이 form 데이터를 MemberDTO 객체로 받고 201 Created 상태를 반환하는 흐름까지 확인한다.


결과물은 각 요청 설명 바로 아래에서 Talend API Tester 화면으로 확인한다.
POST 요청은 요청 방식, 요청 URL, 요청 데이터 전달 방식, 응답 본문, 응답 상태 코드를 함께 봐야 한다.
특히 JSON 본문을 읽는 방식과 @RequestBody 없이 form 데이터를 객체로 묶는 방식은 다르므로 구분해서 봐야 한다.


이 예제의 핵심은 POST 요청에서 데이터를 어떤 형식으로 보내느냐와 @RequestBody를 사용하는지에 따라 Controller에서 값을 받는 방식이 달라진다는 점이다.


예제 전체 코드

// PostController.java
package com.example.springrestedu.controller; // 클래스가 속한 패키지 경로

import java.util.Map; // `JSON` 데이터를 `key-value` 형태로 받기 위한 클래스

import org.springframework.http.HttpStatus; // 응답 상태 코드를 표현하는 클래스
import org.springframework.http.ResponseEntity; // 응답 본문과 상태 코드를 함께 반환하는 클래스
import org.springframework.web.bind.annotation.PostMapping; // `POST` 요청을 메서드와 연결하는 애노테이션
import org.springframework.web.bind.annotation.RequestBody; // 요청 본문 데이터를 받는 애노테이션
import org.springframework.web.bind.annotation.RequestMapping; // 요청 경로와 요청 방식을 연결하는 애노테이션
import org.springframework.web.bind.annotation.RequestMethod; // 요청 방식을 지정하는 `enum`
import org.springframework.web.bind.annotation.RestController; // `REST` 방식 `Controller` 등록
import com.example.springrestedu.domain.MemberDTO; // 요청 데이터를 객체로 담기 위한 `DTO`

@RestController // 반환값을 응답 본문으로 바로 전달하는 `Controller`
@RequestMapping("/restapi") // 이 `Controller`의 공통 요청 경로
public class PostController {
    @RequestMapping(value = "/hello", method = RequestMethod.POST) // `/restapi/hello` `POST` 요청 처리
    public String post1(){
        return "안녕? POST 방식 요청 했네~~~"; // 문자열 응답 반환
    }

    @PostMapping(value = "/member1") // `JSON` 형식 데이터를 `Map`으로 받는 요청 처리
    public String post2(@RequestBody Map<String, Object> postData) {
        StringBuilder sb = new StringBuilder(); // 응답 문자열을 누적할 객체 생성
        postData.entrySet().forEach(map -> {
            sb.append(map.getKey() + " - " + map.getValue() + "\n"); // `key-value` 형태로 문자열 누적
        });
        return sb.toString(); // 완성된 문자열 반환
    }

    @PostMapping(value = "/member2") // `JSON` 형식 데이터를 `DTO`로 받는 요청 처리
    public MemberDTO post3(@RequestBody MemberDTO memberDTO) {
        return memberDTO; // 요청 본문이 담긴 `DTO`를 `JSON` 응답으로 반환
    }

    @PostMapping(value = "/member3") // `form` 데이터를 `DTO`로 바인딩하는 요청 처리
    public ResponseEntity<MemberDTO> post4(MemberDTO memberDTO) {
        return ResponseEntity
                .status(HttpStatus.CREATED) // `201 Created` 상태 코드 설정
                .body(memberDTO); // 응답 본문에 `DTO` 객체 설정
    }
}
// MemberDTO.java
package com.example.springrestedu.domain; // 클래스가 속한 패키지 경로

import lombok.Getter; // 필드 값을 읽는 메서드 자동 생성
import lombok.Setter; // 필드 값을 저장하거나 수정하는 메서드 자동 생성

@Getter // `getter` 메서드 자동 생성
@Setter // `setter` 메서드 자동 생성
public class MemberDTO {
    private String name; // 이름 저장
    private String email; // 이메일 저장
    private String phone; // 전화번호 저장

    @Override
    public String toString() {
        return "MemberDTO 객체 {" +
            "name='" + name + '\'' +
            ", email='" + email + '\'' +
            ", phone='" + phone + '\'' +
            '}'; // 객체 안의 값을 문자열로 반환
    }
}

이 코드는 크게 두 파일로 나누어 볼 수 있다.
첫 번째 PostController.java는 여러 POST 요청을 처리하는 Controller이다.
두 번째 MemberDTO.java는 요청 데이터를 객체로 묶어 담을 때 사용하는 데이터 객체이다.


PostController에는 @RestController가 붙어 있다.
@RestController는 메서드가 반환하는 값을 화면 이름으로 해석하지 않고, 응답 본문으로 바로 반환한다.
그래서 문자열을 반환하면 문자열이 응답으로 나가고, 객체를 반환하면 JSON 형태로 변환되어 응답될 수 있다.


@RequestMapping("/restapi")는 이 Controller의 공통 주소를 정한다.
따라서 메서드에 /hello가 붙어 있으면 실제 요청 주소는 /restapi/hello가 된다.
/member1이 붙어 있으면 실제 요청 주소는 /restapi/member1이 된다.


이 예제는 하나의 Controller 안에서 POST 요청 데이터를 받는 여러 방식을 비교하는 예제이다.


이 예제에서 확인할 핵심

  • POST 요청은 요청 본문에 데이터를 담아 보낼 수 있다.
  • @RestController는 반환값을 응답 본문으로 바로 보낸다.
  • @RequestMapping("/restapi")는 공통 요청 경로를 정한다.
  • @RequestMapping(method = RequestMethod.POST)는 POST 요청을 처리한다.
  • @PostMapping은 POST 요청을 더 간단하게 연결하는 애노테이션이다.
  • @RequestBody는 요청 본문에 담긴 데이터를 읽을 때 사용한다.
  • @RequestBody Map은 JSON 요청 본문을 Map 구조로 받는다.
  • @RequestBody MemberDTO는 JSON 요청 본문을 객체로 받는다.
  • @RequestBody 없이 MemberDTO만 매개변수로 두면 application/x-www-form-urlencoded 형식의 form 데이터를 객체로 받을 수 있다.
  • ResponseEntity를 사용하면 응답 본문과 상태 코드를 함께 정할 수 있다.
  • HttpStatus.CREATED는 201 Created 상태를 의미한다.
  • 결과물은 Talend API Tester에서 요청 방식, 요청 URL, 요청 데이터, 응답 본문, 응답 상태 코드를 기준으로 확인한다.

이 예제는 POST 요청에서 데이터가 서버로 들어오고, 서버가 그 데이터를 Map, DTO, ResponseEntity로 처리하는 흐름을 확인하는 예제이다.


POST 요청은 요청 본문에 데이터를 담을 수 있다

POST는 서버에 데이터를 보내는 요청 방식이다.
대표적으로 회원 등록, 게시글 등록, 상품 등록처럼 새로운 데이터를 만들 때 자주 사용한다.


GET 요청은 보통 주소 뒤에 값을 붙여 보낸다.
예를 들어 ?name=둘리처럼 요청 파라미터를 주소에 붙인다.
반면 POST 요청은 데이터를 요청 본문인 Body에 담아 보낼 수 있다.


요청 본문에 담는 데이터 형식은 하나만 있는 것이 아니다.
이번 예제에서는 크게 두 가지 흐름을 확인한다.

  • JSON 본문을 @RequestBody로 읽는 방식
  • @RequestBody 없이 application/x-www-form-urlencoded 형식의 form 데이터를 객체로 바인딩하는 방식

JSON은 데이터를 { "name": "둘리" }처럼 key-value 구조로 표현하는 형식이다.
application/x-www-form-urlencoded는 name=둘리&email=dooly@test.com처럼 form 데이터를 전송할 때 자주 사용하는 형식이다.


POST 요청은 요청 본문에 데이터를 담을 수 있고, 데이터 형식에 따라 Controller에서 받는 방식도 달라진다.


공통 경로는 @RequestMapping("/restapi")로 정한다

// PostController.java
@RestController // 반환값을 응답 본문으로 바로 전달하는 `Controller`
@RequestMapping("/restapi") // 이 `Controller`의 공통 요청 경로
public class PostController {
}

PostController에는 @RequestMapping("/restapi")가 붙어 있다.
이 애노테이션은 이 Controller 안의 모든 요청 주소 앞에 /restapi를 붙이겠다는 뜻이다.


예를 들어 메서드에 /hello가 붙어 있으면 실제 요청 주소는 /restapi/hello가 된다.
메서드에 /member1이 붙어 있으면 실제 요청 주소는 /restapi/member1이 된다.


이렇게 공통 경로를 클래스 위에 붙이면 요청 주소를 기능 단위로 묶어 관리할 수 있다.
GET 예제에서도 /restapi를 공통 경로로 사용했고, 이번 POST 예제에서도 같은 공통 경로를 사용한다.


클래스 위의 @RequestMapping("/restapi")는 이 Controller에 들어 있는 요청들의 공통 시작 주소를 정한다.


기본 POST 요청으로 문자열 반환하기

// PostController.java
@RequestMapping(value = "/hello", method = RequestMethod.POST) // `/restapi/hello` `POST` 요청 처리
public String post1(){
    return "안녕? POST 방식 요청 했네~~~"; // 문자열 응답 반환
}

post1()은 /restapi/hello 주소로 들어오는 POST 요청을 처리한다.
여기서는 @RequestMapping에 method = RequestMethod.POST를 지정했다.
즉, 같은 /hello 주소라도 POST 방식일 때 이 메서드가 실행된다.


이 메서드는 요청 본문을 따로 받지 않는다.
그냥 POST 방식으로 요청이 들어왔는지만 확인하고 문자열을 반환한다.


Talend API Tester에서 요청 방식은 POST로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/restapi/hello

예상 응답은 아래와 같다.

// 응답 본문
// 안녕? POST 방식 요청 했네~~~

Talend API Tester 결과를 보면 요청 방식이 POST이고, 요청 URL이 /restapi/hello로 들어간다.
응답 상태는 200이고, 응답 본문에는 "안녕? POST 방식 요청 했네~~~"가 출력된다.
따라서 /restapi/hello 요청이 POST 방식으로 들어왔을 때 post1() 메서드와 정상적으로 연결된 것을 확인할 수 있다.


@RequestMapping(method = RequestMethod.POST)는 해당 메서드가 POST 요청을 처리한다는 뜻이다.


@PostMapping은 POST 요청을 더 간단하게 연결한다

@PostMapping은 POST 요청을 처리하는 메서드에 붙이는 애노테이션이다.
@RequestMapping(method = RequestMethod.POST)를 더 짧게 쓴 형태라고 이해하면 된다.


아래 세 메서드는 모두 @PostMapping을 사용한다.

// PostController.java
@PostMapping(value = "/member1") // `JSON` 형식 데이터를 `Map`으로 받는 요청 처리
public String post2(@RequestBody Map<String, Object> postData) {
    // 코드 생략
}

@PostMapping(value = "/member2") // `JSON` 형식 데이터를 `DTO`로 받는 요청 처리
public MemberDTO post3(@RequestBody MemberDTO memberDTO) {
    // 코드 생략
}

@PostMapping(value = "/member3") // `form` 데이터를 `DTO`로 바인딩하는 요청 처리
public ResponseEntity<MemberDTO> post4(MemberDTO memberDTO) {
    // 코드 생략
}

/member1, /member2, /member3은 모두 POST 요청을 처리한다.
하지만 요청 데이터를 받는 방식은 서로 다르다.


member1은 @RequestBody Map으로 받는다.
member2는 @RequestBody MemberDTO로 받는다.
member3은 @RequestBody 없이 MemberDTO로 받는다.


@PostMapping은 POST 요청을 메서드와 연결하는 가장 대표적인 방식이다.


@RequestBody Map으로 JSON 요청 본문 받기

// PostController.java
@PostMapping(value = "/member1") // `JSON` 형식 데이터를 `Map`으로 받는 요청 처리
public String post2(@RequestBody Map<String, Object> postData) {
    StringBuilder sb = new StringBuilder(); // 응답 문자열을 누적할 객체 생성
    postData.entrySet().forEach(map -> {
        sb.append(map.getKey() + " - " + map.getValue() + "\n"); // `key-value` 형태로 문자열 누적
    });
    return sb.toString(); // 완성된 문자열 반환
}

member1은 요청 본문에 담긴 JSON 데이터를 Map으로 받는다.
여기서 중요한 애노테이션은 @RequestBody이다.


@RequestBody는 요청 본문에 들어 있는 데이터를 읽어서 메서드 매개변수에 넣어 준다.
POST 요청에서 JSON 데이터를 보낼 때 자주 사용한다.


Map<String, Object>는 key-value 구조로 데이터를 담는다.
요청 본문에 아래와 같은 JSON을 보내면 name, email, phone이 Map의 key가 된다.
각 값인 둘리, dooly@test.com, 010-1111-2222는 Map의 value가 된다.

// 요청 본문 JSON
// {
//   "name": "둘리",
//   "email": "dooly@test.com",
//   "phone": "010-1111-2222"
// }

post2()는 Map에 담긴 값을 하나씩 꺼내서 key - value 형태의 문자열로 만든다.
그래서 응답 본문에는 요청으로 보낸 값들이 줄 단위 문자열로 반환된다.


Talend API Tester에서는 요청 방식은 POST로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/restapi/member1

요청 본문은 JSON 형식으로 작성한다.

// 요청 본문
// {
//   "name": "둘리",
//   "email": "dooly@test.com",
//   "phone": "010-1111-2222"
// }

예상 응답은 아래처럼 볼 수 있다.

// 응답 본문 예시
// name - 둘리
// email - dooly@test.com
// phone - 010-1111-2222

Talend API Tester 결과를 보면 요청 방식은 POST이고, Content-Type은 application/json으로 설정되어 있다.
요청 본문에는 name, email, phone을 가진 JSON 데이터가 들어간다.
응답 본문에는 name - 둘리, email - dooly@test.com, phone - 010-1111-2222가 줄 단위로 출력된다.


Map 응답은 출력 순서가 실행 환경에 따라 달라질 수 있다.
따라서 순서보다 name, email, phone 값이 모두 들어왔는지를 확인해야 한다.


@RequestBody Map은 요청 본문에 들어온 JSON 데이터를 이름과 값의 묶음으로 받을 때 사용한다.


@RequestBody MemberDTO로 JSON 요청 본문 받기

// PostController.java
@PostMapping(value = "/member2") // `JSON` 형식 데이터를 `DTO`로 받는 요청 처리
public MemberDTO post3(@RequestBody MemberDTO memberDTO) {
    return memberDTO; // 요청 본문이 담긴 `DTO`를 `JSON` 응답으로 반환
}

member2는 요청 본문에 담긴 JSON 데이터를 MemberDTO 객체로 받는다.
member1은 Map으로 받았지만, member2는 객체로 받는다는 점이 다르다.


요청 본문 JSON의 key 이름과 MemberDTO의 필드 이름이 맞으면 Spring이 값을 객체에 넣어 준다.
예를 들어 JSON의 name은 MemberDTO의 name 필드에 들어간다.
email은 email 필드에 들어가고, phone은 phone 필드에 들어간다.


이때 MemberDTO에는 값을 넣을 수 있는 setter가 필요하다.
현재 MemberDTO에는 @Setter가 붙어 있으므로 setter 메서드가 자동으로 만들어진다.


post3()은 받은 memberDTO를 그대로 반환한다.
@RestController에서 객체를 반환하면 JSON 응답으로 변환될 수 있다.


Talend API Tester에서는 요청 방식은 POST로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/restapi/member2

요청 본문은 JSON 형식으로 작성한다.

// 요청 본문
// {
//   "name": "둘리",
//   "email": "dooly@test.com",
//   "phone": "010-1111-2222"
// }

예상 응답은 아래와 같다.

// 응답 본문
// {"name":"둘리","email":"dooly@test.com","phone":"010-1111-2222"}

Talend API Tester 결과를 보면 요청 방식은 POST이고, 요청 본문에는 name, email, phone을 가진 JSON 데이터가 들어간다.
응답 본문도 JSON 형태로 출력된다.


응답의 JSON 필드 순서는 화면에서 다르게 보일 수 있다.
중요한 것은 name, email, phone 값이 요청 본문과 같은 값으로 들어왔는지 확인하는 것이다.


@RequestBody MemberDTO는 요청 본문에 들어온 JSON 데이터를 의미 있는 객체로 묶어 받을 때 사용한다.


MemberDTO는 POST 요청 데이터를 담는 객체이다

// MemberDTO.java
@Getter // `getter` 메서드 자동 생성
@Setter // `setter` 메서드 자동 생성
public class MemberDTO {
    private String name; // 이름 저장
    private String email; // 이메일 저장
    private String phone; // 전화번호 저장

    @Override
    public String toString() {
        return "MemberDTO 객체 {" +
            "name='" + name + '\'' +
            ", email='" + email + '\'' +
            ", phone='" + phone + '\'' +
            '}'; // 객체 안의 값을 문자열로 반환
    }
}

MemberDTO는 요청 데이터를 담기 위한 객체이다.
DTO는 데이터를 옮기기 위한 객체라고 이해하면 된다.
여기서는 클라이언트가 보낸 name, email, phone 값을 Controller에서 하나의 객체로 받기 위해 사용한다.


@Getter는 값을 읽는 메서드를 자동으로 만든다.
@Setter는 값을 저장하거나 수정하는 메서드를 자동으로 만든다.
JSON 요청 본문이나 form 데이터가 객체에 들어가려면 각 필드에 값을 저장할 수 있어야 하므로 setter가 필요하다.


GET 예제에서도 같은 MemberDTO를 사용했다.
GET에서는 주소 뒤의 요청 파라미터를 객체로 묶어 받았다.
이번 POST 예제에서는 JSON 요청 본문이나 form 데이터로 전달된 값을 객체로 묶어 받는다.


같은 MemberDTO를 사용하더라도 GET에서는 주소의 파라미터가 들어오고, POST에서는 요청 본문 데이터나 form 데이터가 객체로 바인딩될 수 있다.


RequestBody 없이 DTO로 form 데이터 받기

// PostController.java
@PostMapping(value = "/member3") // `form` 데이터를 `DTO`로 바인딩하는 요청 처리
public ResponseEntity<MemberDTO> post4(MemberDTO memberDTO) {
    return ResponseEntity
            .status(HttpStatus.CREATED) // `201 Created` 상태 코드 설정
            .body(memberDTO); // 응답 본문에 `DTO` 객체 설정
}

member3은 @RequestBody 없이 MemberDTO로 데이터를 받는다.
이 점이 member1, member2와 다르다.


@RequestBody가 없으면 JSON 요청 본문을 직접 읽는 방식이 아니다.
대신 Spring은 application/x-www-form-urlencoded 형식으로 전달된 form 데이터를 객체의 필드에 연결할 수 있다.
요청 데이터의 이름이 MemberDTO의 필드 이름과 같으면 값이 자동으로 들어간다.


이번 테스트에서는 Talend API Tester에서 요청 방식은 POST로 선택하고, Body의 Form 영역에 email, name, phone 값을 입력했다.
그리고 Content-Type은 application/x-www-form-urlencoded로 설정했다.
그래서 Spring이 이 값을 MemberDTO 객체에 바인딩한다.


이 메서드는 ResponseEntity<MemberDTO>를 반환한다.
응답 본문에는 memberDTO를 넣고, 응답 상태 코드는 HttpStatus.CREATED로 지정한다.
HttpStatus.CREATED는 201 Created 상태를 의미한다.
데이터가 새로 생성되었거나 등록되었다는 의미를 줄 때 사용하는 상태 코드이다.


Talend API Tester에서는 요청 방식은 POST로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/restapi/member3

Body의 Form 영역에는 아래 값을 입력한다.

// form 데이터
// email=dooly@test.com
// name=둘리
// phone=010-1111-2222

예상 응답은 아래와 같다.

// 응답 본문
// {"email":"dooly@test.com","name":"둘리","phone":"010-1111-2222"}

응답 상태는 아래처럼 확인한다.

// 응답 상태
// 201 Created

Talend API Tester 결과를 보면 요청 방식은 POST이고, 요청 URL은 /restapi/member3이다.
Body의 Form 영역에 email, name, phone 값이 들어가 있으며, Content-Type은 application/x-www-form-urlencoded로 설정되어 있다.
응답 상태는 201이고, 응답 본문에는 전달한 값들이 JSON 형태로 다시 반환된다.
이는 ResponseEntity.status(HttpStatus.CREATED)가 적용되었기 때문이다.


이 결과에서는 form 데이터가 MemberDTO 객체로 들어갔는지 확인한다.
또 응답 상태가 200 OK가 아니라 201 Created로 반환되는지도 확인해야 한다.


member3은 @RequestBody 없이 application/x-www-form-urlencoded 형식의 form 데이터를 MemberDTO 객체로 바인딩하고, ResponseEntity로 201 Created 상태를 반환하는 예제이다.


JSON 방식과 RequestBody 없는 form 바인딩 방식 비교하기

이번 예제에서 가장 헷갈리기 쉬운 부분은 member2와 member3의 차이이다.
둘 다 최종적으로는 MemberDTO 객체에 값이 담긴다.
하지만 클라이언트가 데이터를 보내는 방식과 서버가 받는 방식이 다르다.


member2는 JSON 요청 본문을 받는다.
그래서 매개변수 앞에 @RequestBody가 붙어 있다.
@RequestBody는 요청 본문을 읽어서 객체로 변환하는 역할을 한다.


반면 member3은 @RequestBody가 없다.
그래서 JSON 요청 본문을 읽는 방식이 아니라, application/x-www-form-urlencoded 형식으로 전달된 form 데이터를 MemberDTO 객체로 바인딩한다.


정리하면 아래처럼 볼 수 있다.

  • member1은 JSON 요청 본문을 Map으로 받는다.
  • member2는 JSON 요청 본문을 MemberDTO로 받는다.
  • member3은 @RequestBody 없이 form 데이터를 MemberDTO로 받는다.
  • member2는 @RequestBody가 필요하다.
  • member3은 @RequestBody 없이 객체 바인딩을 사용한다.
  • member3은 ResponseEntity로 201 Created 상태를 반환한다.

같은 POST 요청이어도 JSON 본문을 읽는 방식과 @RequestBody 없이 form 데이터를 객체로 묶는 방식은 다르다.


실행 흐름 정리

이 예제의 실행 흐름은 아래 순서로 진행된다.

  • 서버를 실행한다.
  • Talend API Tester에서 요청 방식을 POST로 선택한다.
  • /restapi/hello 요청을 보내서 기본 POST 응답을 확인한다.
  • /restapi/member1 요청에 JSON 본문을 담아 보낸다.
  • post2()가 @RequestBody Map으로 요청 본문을 받는다.
  • /restapi/member2 요청에 JSON 본문을 담아 보낸다.
  • post3()이 @RequestBody MemberDTO로 요청 본문을 객체에 담는다.
  • /restapi/member3 요청에서 Body의 Form 영역에 application/x-www-form-urlencoded 형식으로 값을 담아 보낸다.
  • post4()가 MemberDTO로 form 데이터를 받고 ResponseEntity를 반환한다.
  • member3 응답 상태가 201 Created인지 확인한다.

흐름을 한 줄로 정리하면 다음과 같다.


POST 요청 → 요청 데이터 전달 → Map 또는 DTO 바인딩 → 문자열, JSON, ResponseEntity 응답 반환


POST 요청은 데이터를 서버로 보내고, Controller는 그 데이터를 형식에 맞게 읽거나 바인딩해서 응답을 만든다.


핵심 정리

이 예제의 핵심은 POST 요청에서 데이터를 받는 방식을 비교하는 것이다.
PostController는 /restapi 공통 경로 아래에서 여러 POST 요청을 처리한다.
각 메서드는 요청 데이터를 받는 방식과 응답을 만드는 방식이 조금씩 다르다.


이 흐름은 아래처럼 정리할 수 있다.

  • POST 요청은 요청 본문에 데이터를 담아 보낼 수 있다.
  • @RestController는 반환값을 응답 본문으로 바로 보낸다.
  • @RequestMapping("/restapi")는 공통 요청 경로를 정한다.
  • @RequestMapping(method = RequestMethod.POST)는 POST 요청을 처리한다.
  • @PostMapping은 POST 요청을 더 간단하게 연결한다.
  • @RequestBody는 요청 본문 데이터를 읽어 매개변수에 연결한다.
  • @RequestBody Map은 JSON 데이터를 Map으로 받는다.
  • @RequestBody MemberDTO는 JSON 데이터를 객체로 받는다.
  • @RequestBody 없이 DTO를 매개변수로 두면 application/x-www-form-urlencoded 형식의 form 데이터를 객체로 받을 수 있다.
  • ResponseEntity는 응답 본문과 상태 코드를 함께 정할 수 있다.
  • HttpStatus.CREATED는 201 Created 상태를 의미한다.
  • 이번 응용예제 결과물은 Talend API Tester에서 요청 방식, 요청 URL, 요청 데이터, 응답 본문, 응답 상태 코드를 기준으로 확인한다.

따라서 이 예제가 알려 주는 핵심은 이것이다.
POST 요청에서는 데이터를 서버로 보낼 수 있고, Controller는 그 데이터를 Map, DTO, ResponseEntity로 처리할 수 있다.
다음 예제에서는 PUT 요청으로 데이터 수정 흐름을 확인한다.



6. PUT 요청으로 데이터 수정 흐름 확인하기 응용예제

이 예제는 PUT 요청으로 클라이언트가 보낸 데이터를 서버가 어떻게 받는지 확인하는 흐름이다.
PUT은 주로 기존 데이터를 수정할 때 사용하는 요청 방식이다.
POST가 새 데이터를 등록하는 흐름에 자주 쓰인다면, PUT은 이미 존재하는 데이터를 바꾸는 흐름에 자주 쓰인다.


이번 예제에서는 /restapi로 시작하는 PUT 요청을 확인한다.
단순 PUT 문자열 응답, JSON 요청 본문을 Map으로 받기, JSON 요청 본문을 MemberDTO 객체로 받기, ResponseEntity로 202 Accepted와 205 Reset Content 상태를 반환하는 흐름까지 확인한다.


결과물은 각 요청 설명 바로 아래에서 Talend API Tester 화면으로 확인한다.
PUT 요청은 요청 방식, 요청 URL, 요청 데이터 전달 방식, 응답 본문, 응답 상태 코드를 함께 봐야 한다.
특히 같은 JSON 요청 본문을 보내더라도 응답 상태 코드가 200, 202, 205로 달라질 수 있다는 점을 구분해서 봐야 한다.


이 예제의 핵심은 PUT 요청에서 요청 본문 데이터를 Map 또는 DTO로 받고, ResponseEntity를 통해 수정 요청에 맞는 응답 상태를 반환할 수 있다는 점이다.


예제 전체 코드

// PutController.java
package com.example.springrestedu.controller; // 클래스가 속한 패키지 경로

import java.util.Map; // `JSON` 데이터를 `key-value` 형태로 받기 위한 클래스
import org.springframework.http.HttpStatus; // 응답 상태 코드를 표현하는 클래스
import org.springframework.http.ResponseEntity; // 응답 본문과 상태 코드를 함께 반환하는 클래스
import org.springframework.web.bind.annotation.PutMapping; // `PUT` 요청을 메서드와 연결하는 애노테이션
import org.springframework.web.bind.annotation.RequestBody; // 요청 본문 데이터를 받는 애노테이션
import org.springframework.web.bind.annotation.RequestMapping; // 공통 요청 경로를 지정하는 애노테이션
import org.springframework.web.bind.annotation.RestController; // `REST` 방식 `Controller` 등록
import com.example.springrestedu.domain.MemberDTO; // 요청 데이터를 객체로 담기 위한 `DTO`

@RestController // 반환값을 응답 본문으로 바로 전달하는 `Controller`
@RequestMapping("/restapi") // 이 `Controller`의 공통 요청 경로
public class PutController {
    @PutMapping(value = "/hello") // `/restapi/hello` `PUT` 요청 처리
    public String put1() {
        return "안녕? PUT 방식 요청 했네~~~"; // 문자열 응답 반환
    }

    @PutMapping(value = "/member1") // `JSON` 형식 데이터를 `Map`으로 받는 요청 처리
    public String put2(@RequestBody Map<String, Object> putData) {
        StringBuilder sb = new StringBuilder(); // 응답 문자열을 누적할 객체 생성
        putData.entrySet().forEach(map -> {
            sb.append(map.getKey() + " - " + map.getValue() + "\n"); // `key-value` 형태로 문자열 누적
        });
        return sb.toString(); // 완성된 문자열 반환
    }

    @PutMapping(value = "/member2") // `JSON` 형식 데이터를 `DTO`로 받는 요청 처리
    public MemberDTO put3(@RequestBody MemberDTO memberDTO) {
        return memberDTO; // 요청 본문이 담긴 `DTO`를 `JSON` 응답으로 반환
    }

    @PutMapping(value = "/member3") // `JSON` 형식 데이터를 받고 `202 Accepted` 반환
    public ResponseEntity<MemberDTO> put4(@RequestBody MemberDTO memberDTO) {
        return ResponseEntity
                .status(HttpStatus.ACCEPTED) // `202 Accepted` 상태 코드 설정
                .body(memberDTO); // 응답 본문에 `DTO` 객체 설정
    }

    @PutMapping(value = "/member4") // `JSON` 형식 데이터를 받고 `205 Reset Content` 반환
    public ResponseEntity<MemberDTO> put5(@RequestBody MemberDTO memberDTO) {
        return ResponseEntity
            .status(HttpStatus.RESET_CONTENT) // `205 Reset Content` 상태 코드 설정
            .body(null); // 응답 본문 없이 반환
    }
}
// MemberDTO.java
package com.example.springrestedu.domain; // 클래스가 속한 패키지 경로

import lombok.Getter; // 필드 값을 읽는 메서드 자동 생성
import lombok.Setter; // 필드 값을 저장하거나 수정하는 메서드 자동 생성

@Getter // `getter` 메서드 자동 생성
@Setter // `setter` 메서드 자동 생성
public class MemberDTO {
    private String name; // 이름 저장
    private String email; // 이메일 저장
    private String phone; // 전화번호 저장

    @Override
    public String toString() {
        return "MemberDTO 객체 {" +
            "name='" + name + '\'' +
            ", email='" + email + '\'' +
            ", phone='" + phone + '\'' +
            '}'; // 객체 안의 값을 문자열로 반환
    }
}

이 코드는 크게 두 파일로 나누어 볼 수 있다.
첫 번째 PutController.java는 여러 PUT 요청을 처리하는 Controller이다.
두 번째 MemberDTO.java는 요청 데이터를 객체로 묶어 담을 때 사용하는 데이터 객체이다.


PutController에는 @RestController가 붙어 있다.
@RestController는 메서드가 반환하는 값을 화면 이름으로 해석하지 않고, 응답 본문으로 바로 반환한다.
그래서 문자열을 반환하면 문자열이 응답으로 나가고, 객체를 반환하면 JSON 형태로 변환되어 응답될 수 있다.


@RequestMapping("/restapi")는 이 Controller의 공통 주소를 정한다.
따라서 메서드에 /hello가 붙어 있으면 실제 요청 주소는 /restapi/hello가 된다.
/member1이 붙어 있으면 실제 요청 주소는 /restapi/member1이 된다.


이 예제는 하나의 Controller 안에서 PUT 요청 데이터를 받는 여러 방식과 응답 상태 코드를 비교하는 예제이다.


이 예제에서 확인할 핵심

  • PUT 요청은 기존 데이터를 수정하는 요청에 자주 사용한다.
  • @RestController는 반환값을 응답 본문으로 바로 보낸다.
  • @RequestMapping("/restapi")는 공통 요청 경로를 정한다.
  • @PutMapping은 PUT 요청을 메서드와 연결하는 애노테이션이다.
  • @RequestBody는 요청 본문에 담긴 데이터를 읽을 때 사용한다.
  • @RequestBody Map은 JSON 요청 본문을 Map 구조로 받는다.
  • @RequestBody MemberDTO는 JSON 요청 본문을 객체로 받는다.
  • ResponseEntity를 사용하면 응답 본문과 상태 코드를 함께 정할 수 있다.
  • HttpStatus.ACCEPTED는 202 Accepted 상태를 의미한다.
  • HttpStatus.RESET_CONTENT는 205 Reset Content 상태를 의미한다.
  • member4는 응답 본문 없이 상태 코드만 확인하는 흐름이다.
  • 결과물은 Talend API Tester에서 요청 방식, 요청 URL, 요청 데이터, 응답 본문, 응답 상태 코드를 기준으로 확인한다.

이 예제는 PUT 요청에서 데이터가 서버로 들어오고, 서버가 그 데이터를 Map, DTO, ResponseEntity로 처리하는 흐름을 확인하는 예제이다.


PUT 요청은 수정 요청에 자주 사용한다

PUT은 기존 데이터를 수정할 때 자주 사용하는 요청 방식이다.
예를 들어 회원 정보를 바꾸거나, 게시글 내용을 수정하거나, 상품 정보를 수정하는 요청에서 사용할 수 있다.


다만 이번 예제는 실제 데이터베이스를 수정하는 코드가 아니다.
요청으로 보낸 데이터를 서버가 어떻게 받는지 확인하고, 수정 요청에 어울리는 응답 상태를 어떻게 반환하는지 확인하는 예제이다.


POST 예제와 비슷하게 PUT도 요청 본문에 데이터를 담아 보낼 수 있다.
그래서 member1, member2, member3, member4는 모두 JSON 요청 본문을 사용한다.
그리고 서버에서는 @RequestBody를 사용해 그 본문 데이터를 읽는다.


이번 PUT 예제의 목적은 실제 수정 저장이 아니라, 수정 요청에서 요청 본문을 받고 응답 상태를 다르게 반환하는 흐름을 확인하는 것이다.


공통 경로는 @RequestMapping("/restapi")로 정한다

// PutController.java
@RestController // 반환값을 응답 본문으로 바로 전달하는 `Controller`
@RequestMapping("/restapi") // 이 `Controller`의 공통 요청 경로
public class PutController {
}

PutController에도 @RequestMapping("/restapi")가 붙어 있다.
이 애노테이션은 이 Controller 안의 모든 요청 주소 앞에 /restapi를 붙이겠다는 뜻이다.


예를 들어 메서드에 /hello가 붙어 있으면 실제 요청 주소는 /restapi/hello가 된다.
메서드에 /member3이 붙어 있으면 실제 요청 주소는 /restapi/member3이 된다.


GET, POST, PUT 예제 모두 /restapi를 공통 경로로 사용한다.
그래서 요청 방식은 달라도 기본 주소 구조는 비슷하게 비교할 수 있다.


클래스 위의 @RequestMapping("/restapi")는 이 Controller에 들어 있는 요청들의 공통 시작 주소를 정한다.


기본 PUT 요청으로 문자열 반환하기

// PutController.java
@PutMapping(value = "/hello") // `/restapi/hello` `PUT` 요청 처리
public String put1() {
    return "안녕? PUT 방식 요청 했네~~~"; // 문자열 응답 반환
}

put1()은 /restapi/hello 주소로 들어오는 PUT 요청을 처리한다.
@PutMapping은 PUT 요청을 메서드와 연결하는 애노테이션이다.
@PostMapping이 POST 요청을 처리했다면, @PutMapping은 PUT 요청을 처리한다.


이 메서드는 요청 본문을 따로 받지 않는다.
그냥 PUT 방식으로 요청이 들어왔는지만 확인하고 문자열을 반환한다.
캡처에서는 공통 테스트용으로 JSON 본문이 함께 들어가 있지만, 이 메서드에는 @RequestBody 매개변수가 없기 때문에 요청 본문 값은 사용되지 않는다.


Talend API Tester에서 요청 방식은 PUT으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/restapi/hello

예상 응답은 아래와 같다.

// 응답 본문
// 안녕? PUT 방식 요청 했네~~~

Talend API Tester 결과를 보면 요청 방식은 PUT이고, 요청 URL은 /restapi/hello이다.
응답 상태는 200이고, 응답 본문에는 "안녕? PUT 방식 요청 했네~~~"가 출력된다.
따라서 /restapi/hello 요청이 PUT 방식으로 들어왔을 때 put1() 메서드와 정상적으로 연결된 것을 확인할 수 있다.


@PutMapping은 해당 메서드가 PUT 요청을 처리한다는 뜻이다.


@RequestBody Map으로 JSON 요청 본문 받기

// PutController.java
@PutMapping(value = "/member1") // `JSON` 형식 데이터를 `Map`으로 받는 요청 처리
public String put2(@RequestBody Map<String, Object> putData) {
    StringBuilder sb = new StringBuilder(); // 응답 문자열을 누적할 객체 생성
    putData.entrySet().forEach(map -> {
        sb.append(map.getKey() + " - " + map.getValue() + "\n"); // `key-value` 형태로 문자열 누적
    });
    return sb.toString(); // 완성된 문자열 반환
}

member1은 요청 본문에 담긴 JSON 데이터를 Map으로 받는다.
POST 예제의 member1과 구조가 거의 같다.
다만 요청 방식이 POST가 아니라 PUT이라는 점이 다르다.


@RequestBody는 요청 본문에 들어 있는 데이터를 읽어서 메서드 매개변수에 넣어 준다.
여기서는 JSON 요청 본문을 Map<String, Object>로 받는다.


Map은 key-value 구조로 데이터를 담는다.
요청 본문에 아래와 같은 JSON을 보내면 name, email, phone이 Map의 key가 된다.
각 값인 둘리, dooly@test.com, 010-1111-2222는 Map의 value가 된다.

// 요청 본문 JSON
// {
//   "name": "둘리",
//   "email": "dooly@test.com",
//   "phone": "010-1111-2222"
// }

put2()는 Map에 담긴 값을 하나씩 꺼내서 key - value 형태의 문자열로 만든다.
그래서 응답 본문에는 요청으로 보낸 값들이 줄 단위 문자열로 반환된다.


Talend API Tester에서는 요청 방식은 PUT으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/restapi/member1

요청 본문은 JSON 형식으로 작성한다.

// 요청 본문
// {
//   "name": "둘리",
//   "email": "dooly@test.com",
//   "phone": "010-1111-2222"
// }

예상 응답은 아래처럼 볼 수 있다.

// 응답 본문 예시
// name - 둘리
// email - dooly@test.com
// phone - 010-1111-2222

Talend API Tester 결과를 보면 요청 방식은 PUT이고, Content-Type은 application/json으로 설정되어 있다.
요청 본문에는 name, email, phone을 가진 JSON 데이터가 들어간다.
응답 본문에는 name - 둘리, email - dooly@test.com, phone - 010-1111-2222가 줄 단위로 출력된다.


Map 응답은 출력 순서가 실행 환경에 따라 달라질 수 있다.
따라서 순서보다 name, email, phone 값이 모두 들어왔는지를 확인해야 한다.


@RequestBody Map은 PUT 요청 본문에 들어온 JSON 데이터를 이름과 값의 묶음으로 받을 때 사용한다.


@RequestBody MemberDTO로 JSON 요청 본문 받기

// PutController.java
@PutMapping(value = "/member2") // `JSON` 형식 데이터를 `DTO`로 받는 요청 처리
public MemberDTO put3(@RequestBody MemberDTO memberDTO) {
    return memberDTO; // 요청 본문이 담긴 `DTO`를 `JSON` 응답으로 반환
}

member2는 요청 본문에 담긴 JSON 데이터를 MemberDTO 객체로 받는다.
member1은 Map으로 받았지만, member2는 객체로 받는다는 점이 다르다.


요청 본문 JSON의 key 이름과 MemberDTO의 필드 이름이 맞으면 Spring이 값을 객체에 넣어 준다.
예를 들어 JSON의 name은 MemberDTO의 name 필드에 들어간다.
email은 email 필드에 들어가고, phone은 phone 필드에 들어간다.


이때 MemberDTO에는 값을 넣을 수 있는 setter가 필요하다.
현재 MemberDTO에는 @Setter가 붙어 있으므로 setter 메서드가 자동으로 만들어진다.


put3()은 받은 memberDTO를 그대로 반환한다.
@RestController에서 객체를 반환하면 JSON 응답으로 변환될 수 있다.


Talend API Tester에서는 요청 방식은 PUT으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/restapi/member2

요청 본문은 JSON 형식으로 작성한다.

// 요청 본문
// {
//   "name": "둘리",
//   "email": "dooly@test.com",
//   "phone": "010-1111-2222"
// }

예상 응답은 아래와 같다.

// 응답 본문
// {"name":"둘리","email":"dooly@test.com","phone":"010-1111-2222"}

Talend API Tester 결과를 보면 요청 방식은 PUT이고, 요청 본문에는 name, email, phone을 가진 JSON 데이터가 들어간다.
응답 상태는 200이고, 응답 본문은 JSON 형태로 출력된다.


응답의 JSON 필드 순서는 화면에서 다르게 보일 수 있다.
중요한 것은 name, email, phone 값이 요청 본문과 같은 값으로 들어왔는지 확인하는 것이다.


@RequestBody MemberDTO는 PUT 요청 본문에 들어온 JSON 데이터를 의미 있는 객체로 묶어 받을 때 사용한다.


MemberDTO는 PUT 요청 데이터를 담는 객체이다

// MemberDTO.java
@Getter // `getter` 메서드 자동 생성
@Setter // `setter` 메서드 자동 생성
public class MemberDTO {
    private String name; // 이름 저장
    private String email; // 이메일 저장
    private String phone; // 전화번호 저장

    @Override
    public String toString() {
        return "MemberDTO 객체 {" +
            "name='" + name + '\'' +
            ", email='" + email + '\'' +
            ", phone='" + phone + '\'' +
            '}'; // 객체 안의 값을 문자열로 반환
    }
}

MemberDTO는 요청 데이터를 담기 위한 객체이다.
DTO는 데이터를 옮기기 위한 객체라고 이해하면 된다.
여기서는 클라이언트가 보낸 name, email, phone 값을 Controller에서 하나의 객체로 받기 위해 사용한다.


@Getter는 값을 읽는 메서드를 자동으로 만든다.
@Setter는 값을 저장하거나 수정하는 메서드를 자동으로 만든다.
JSON 요청 본문이 객체에 들어가려면 각 필드에 값을 저장할 수 있어야 하므로 setter가 필요하다.


PUT 예제에서는 member2, member3, member4에서 MemberDTO를 사용한다.
각 메서드는 모두 JSON 요청 본문을 MemberDTO로 받지만, 응답 상태 코드는 서로 다르게 반환할 수 있다.


같은 MemberDTO를 사용하더라도 응답을 문자열로 만들지, JSON으로 반환할지, 상태 코드를 함께 반환할지는 Controller 메서드가 결정한다.


ResponseEntity로 202 Accepted 상태와 본문 반환하기

// PutController.java
@PutMapping(value = "/member3") // `JSON` 형식 데이터를 받고 `202 Accepted` 반환
public ResponseEntity<MemberDTO> put4(@RequestBody MemberDTO memberDTO) {
    return ResponseEntity
            .status(HttpStatus.ACCEPTED) // `202 Accepted` 상태 코드 설정
            .body(memberDTO); // 응답 본문에 `DTO` 객체 설정
}

member3은 JSON 요청 본문을 MemberDTO로 받은 뒤 ResponseEntity를 반환한다.
member2와 똑같이 @RequestBody MemberDTO로 요청 데이터를 받지만, 반환 방식이 다르다.


member2는 MemberDTO 객체를 그대로 반환한다.
그래서 일반적으로 응답 상태는 200 OK로 볼 수 있다.
반면 member3은 ResponseEntity를 사용해서 응답 상태 코드를 직접 지정한다.


HttpStatus.ACCEPTED는 202 Accepted 상태를 의미한다.
202 Accepted는 요청이 접수되었다는 의미를 표현할 때 사용할 수 있다.
이번 예제에서는 PUT 요청을 받았고, 그 결과로 202 상태와 요청 데이터 본문을 함께 반환하는 흐름을 확인한다.


Talend API Tester에서는 요청 방식은 PUT으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/restapi/member3

요청 본문은 JSON 형식으로 작성한다.

// 요청 본문
// {
//   "name": "둘리",
//   "email": "dooly@test.com",
//   "phone": "010-1111-2222"
// }

예상 응답은 아래와 같다.

// 응답 본문
// {"name":"둘리","email":"dooly@test.com","phone":"010-1111-2222"}

응답 상태는 아래처럼 확인한다.

// 응답 상태
// 202 Accepted

Talend API Tester 결과를 보면 요청 방식은 PUT이고, 요청 본문에는 JSON 데이터가 들어간다.
응답 상태는 202이고, 응답 본문에는 요청으로 보낸 name, email, phone 값이 JSON 형태로 다시 반환된다.
따라서 ResponseEntity를 사용해 응답 본문과 상태 코드를 함께 지정한 것을 확인할 수 있다.


ResponseEntity.status(HttpStatus.ACCEPTED).body(memberDTO)는 응답 본문과 202 Accepted 상태를 함께 반환한다.


ResponseEntity로 205 Reset Content 상태와 빈 본문 반환하기

// PutController.java
@PutMapping(value = "/member4") // `JSON` 형식 데이터를 받고 `205 Reset Content` 반환
public ResponseEntity<MemberDTO> put5(@RequestBody MemberDTO memberDTO) {
    return ResponseEntity
        .status(HttpStatus.RESET_CONTENT) // `205 Reset Content` 상태 코드 설정
        .body(null); // 응답 본문 없이 반환
}

member4도 JSON 요청 본문을 MemberDTO로 받는다.
하지만 응답 본문에 받은 객체를 다시 반환하지 않는다.
대신 body(null)을 사용해 응답 본문을 비운다.


응답 상태 코드는 HttpStatus.RESET_CONTENT로 지정한다.
HttpStatus.RESET_CONTENT는 205 Reset Content 상태를 의미한다.
이 상태는 요청 처리가 끝났고, 클라이언트가 입력 내용을 초기화해도 된다는 의미로 이해할 수 있다.


이번 예제에서는 실제 화면 입력값을 초기화하는 코드를 작성하는 것이 아니다.
ResponseEntity로 응답 본문 없이 205 상태를 반환할 수 있다는 점을 확인하는 예제이다.


Talend API Tester에서는 요청 방식은 PUT으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/restapi/member4

요청 본문은 JSON 형식으로 작성한다.

// 요청 본문
// {
//   "name": "둘리",
//   "email": "dooly@test.com",
//   "phone": "010-1111-2222"
// }

예상 응답 본문은 비어 있다.

// 응답 본문
// 비어 있음

응답 상태는 아래처럼 확인한다.

// 응답 상태
// 205 Reset Content

Talend API Tester 결과를 보면 요청 방식은 PUT이고, 요청 본문에는 JSON 데이터가 들어간다.
응답 상태는 205이고, 응답 본문 영역에는 No Content가 표시된다.
또 응답 헤더의 Content-Length가 0 byte로 확인된다.
이는 body(null)로 응답 본문을 비웠기 때문이다.


ResponseEntity.status(HttpStatus.RESET_CONTENT).body(null)은 응답 본문 없이 205 Reset Content 상태를 반환한다.


PUT 요청에서 응답 상태 코드 비교하기

이번 예제에서 가장 헷갈리기 쉬운 부분은 member2, member3, member4의 차이이다.
세 메서드는 모두 JSON 요청 본문을 MemberDTO로 받는다.
하지만 응답을 만드는 방식과 상태 코드가 다르다.


member2는 MemberDTO 객체를 그대로 반환한다.
이 경우 응답 본문에는 MemberDTO가 JSON 형태로 들어가고, 일반적으로 상태 코드는 200 OK로 확인된다.


member3은 ResponseEntity를 사용한다.
응답 본문에는 MemberDTO를 넣고, 상태 코드는 202 Accepted로 지정한다.
즉, 본문도 있고 상태 코드도 직접 정한다.


member4도 ResponseEntity를 사용한다.
하지만 응답 본문은 null이고, 상태 코드는 205 Reset Content로 지정한다.
즉, 상태 코드는 있지만 본문은 비어 있는 응답이다.


정리하면 아래처럼 볼 수 있다.

  • member2는 MemberDTO 객체를 그대로 반환한다.
  • member3은 MemberDTO 본문과 202 Accepted 상태를 함께 반환한다.
  • member4는 본문 없이 205 Reset Content 상태를 반환한다.
  • ResponseEntity를 사용하면 상태 코드를 직접 지정할 수 있다.

같은 PUT 요청과 같은 DTO를 사용해도 응답 상태와 본문 구성은 ResponseEntity 사용 여부에 따라 달라질 수 있다.


실행 흐름 정리

이 예제의 실행 흐름은 아래 순서로 진행된다.

  • 서버를 실행한다.
  • Talend API Tester에서 요청 방식을 PUT으로 선택한다.
  • /restapi/hello 요청을 보내서 기본 PUT 응답을 확인한다.
  • /restapi/member1 요청에 JSON 본문을 담아 보낸다.
  • put2()가 @RequestBody Map으로 요청 본문을 받는다.
  • /restapi/member2 요청에 JSON 본문을 담아 보낸다.
  • put3()이 @RequestBody MemberDTO로 요청 본문을 객체에 담는다.
  • /restapi/member3 요청에 JSON 본문을 담아 보낸다.
  • put4()가 MemberDTO 본문과 202 Accepted 상태를 반환한다.
  • /restapi/member4 요청에 JSON 본문을 담아 보낸다.
  • put5()가 응답 본문 없이 205 Reset Content 상태를 반환한다.

흐름을 한 줄로 정리하면 다음과 같다.


PUT 요청 → JSON 요청 본문 전달 → Map 또는 DTO 바인딩 → 문자열, JSON, ResponseEntity 응답 반환


PUT 요청은 수정 요청에 자주 쓰이며, Controller는 요청 본문 데이터를 받아 수정 요청에 맞는 응답 상태를 반환할 수 있다.


핵심 정리

이 예제의 핵심은 PUT 요청에서 데이터를 받는 방식과 응답 상태 코드를 비교하는 것이다.
PutController는 /restapi 공통 경로 아래에서 여러 PUT 요청을 처리한다.
각 메서드는 요청 데이터를 받는 방식과 응답을 만드는 방식이 조금씩 다르다.


이 흐름은 아래처럼 정리할 수 있다.

  • PUT 요청은 기존 데이터를 수정하는 요청에 자주 사용한다.
  • @RestController는 반환값을 응답 본문으로 바로 보낸다.
  • @RequestMapping("/restapi")는 공통 요청 경로를 정한다.
  • @PutMapping은 PUT 요청을 처리한다.
  • @RequestBody는 요청 본문 데이터를 읽어 매개변수에 연결한다.
  • @RequestBody Map은 JSON 데이터를 Map으로 받는다.
  • @RequestBody MemberDTO는 JSON 데이터를 객체로 받는다.
  • MemberDTO 객체를 그대로 반환하면 JSON 형태로 응답될 수 있다.
  • ResponseEntity는 응답 본문과 상태 코드를 함께 정할 수 있다.
  • HttpStatus.ACCEPTED는 202 Accepted 상태를 의미한다.
  • HttpStatus.RESET_CONTENT는 205 Reset Content 상태를 의미한다.
  • 이번 응용예제 결과물은 Talend API Tester에서 요청 방식, 요청 URL, 요청 데이터, 응답 본문, 응답 상태 코드를 기준으로 확인한다.

따라서 이 예제가 알려 주는 핵심은 이것이다.
PUT 요청에서는 수정할 데이터를 요청 본문에 담아 보낼 수 있고, Controller는 그 데이터를 Map, DTO, ResponseEntity로 처리하면서 응답 상태 코드까지 직접 정할 수 있다.
다음 예제에서는 DELETE 요청으로 데이터 삭제 흐름을 확인한다.



7. DELETE 요청으로 데이터 삭제 흐름 확인하기 응용예제

이 예제는 DELETE 요청으로 서버에 삭제 요청을 보내는 흐름을 확인하는 예제이다.
DELETE는 주로 기존 데이터를 삭제할 때 사용하는 요청 방식이다.
GET은 조회, POST는 등록, PUT은 수정에 자주 사용한다면, DELETE는 삭제 요청에 자주 사용한다.


이번 예제에서는 /restapi로 시작하는 여러 DELETE 요청을 확인한다.
단순 DELETE 문자열 응답, 주소 경로값 받기, 요청 파라미터 받기, ResponseEntity로 204 No Content와 202 Accepted 상태를 반환하는 흐름까지 확인한다.


결과물은 각 요청 설명 바로 아래에서 Talend API Tester 화면으로 확인한다.
DELETE 요청은 요청 방식, 요청 URL, 요청 파라미터, 응답 본문, 응답 상태 코드를 함께 봐야 한다.
특히 삭제 요청에서는 응답 본문을 돌려주는 경우도 있고, 본문 없이 상태 코드만 반환하는 경우도 있으므로 이 차이를 구분해야 한다.


이 예제의 핵심은 DELETE 요청에서 삭제할 대상을 경로값이나 요청 파라미터로 전달하고, 삭제 결과를 상태 코드와 응답 본문으로 표현할 수 있다는 점이다.


예제 전체 코드

// DeleteController.java
package com.example.springrestedu.controller; // 클래스가 속한 패키지 경로

import org.springframework.http.HttpStatus; // 응답 상태 코드를 표현하는 클래스
import org.springframework.http.ResponseEntity; // 응답 본문과 상태 코드를 함께 반환하는 클래스
import org.springframework.web.bind.annotation.*; // 요청 매핑과 요청값 처리를 위한 애노테이션 모음

@RestController // 반환값을 응답 본문으로 바로 전달하는 `Controller`
@RequestMapping("/restapi") // 이 `Controller`의 공통 요청 경로
public class DeleteController {
    @DeleteMapping(value = "/hello") // `/restapi/hello` `DELETE` 요청 처리
    public String delete() {
        return "안녕? DELETE 방식 요청 했네~~~"; // 문자열 응답 반환
    }

    @DeleteMapping(value = "/{variable}") // `/restapi/값` 형태의 `DELETE` 요청 처리
    public String delete1(@PathVariable String variable) {
        return variable; // 경로로 받은 값 반환
    }

    @DeleteMapping(value = "/deletetest1") // 요청 파라미터 `email`을 받는 `DELETE` 요청 처리
    public String delete2(@RequestParam String email) {
        return "e-mail : " + email; // 요청 파라미터로 받은 이메일 반환
    }

    @DeleteMapping(value = "/deletetest2") // 요청 파라미터 `id`를 받고 `204 No Content` 반환
    public ResponseEntity<Object> delete3(int id) {
        return ResponseEntity
                .status(HttpStatus.NO_CONTENT) // `204 No Content` 상태 코드 설정
                .body(null); // 응답 본문 없이 반환
    }

    @DeleteMapping(value = "/deletetest3") // 요청 파라미터 `id`를 받고 `204 No Content` 반환
    public ResponseEntity<Object> delete4(int id) {
        return ResponseEntity.noContent().build(); // `204 No Content` 응답 생성
    }

    @DeleteMapping(value = "/deletetest4") // 요청 파라미터 `id`를 받고 `202 Accepted`와 메시지 반환
    public ResponseEntity<String> delete5(int id) {
        return ResponseEntity
                .status(HttpStatus.ACCEPTED) // `202 Accepted` 상태 코드 설정
                .body(id + "번 글이 삭제되었어요"); // 삭제 결과 메시지 반환
    }
}

이 코드는 하나의 DeleteController.java 파일로 구성되어 있다.
DeleteController는 여러 DELETE 요청을 처리하는 Controller이다.


@RestController가 붙어 있으므로 메서드가 반환하는 값은 화면 이름으로 해석되지 않는다.
문자열을 반환하면 문자열이 응답 본문으로 나가고, ResponseEntity를 반환하면 상태 코드와 응답 본문을 함께 정할 수 있다.


@RequestMapping("/restapi")는 이 Controller의 공통 주소를 정한다.
따라서 메서드에 /hello가 붙어 있으면 실제 요청 주소는 /restapi/hello가 된다.
/deletetest1이 붙어 있으면 실제 요청 주소는 /restapi/deletetest1이 된다.


이 예제는 하나의 Controller 안에서 DELETE 요청값을 받는 방식과 삭제 응답 상태 코드를 비교하는 예제이다.


이 예제에서 확인할 핵심

  • DELETE 요청은 기존 데이터를 삭제하는 요청에 자주 사용한다.
  • @RestController는 반환값을 응답 본문으로 바로 보낸다.
  • @RequestMapping("/restapi")는 공통 요청 경로를 정한다.
  • @DeleteMapping은 DELETE 요청을 메서드와 연결하는 애노테이션이다.
  • @PathVariable은 주소 경로에 들어간 값을 받는다.
  • @RequestParam은 요청 파라미터 값을 받는다.
  • 단순 타입 매개변수도 요청 파라미터 이름과 맞으면 값을 받을 수 있다.
  • ResponseEntity를 사용하면 응답 본문과 상태 코드를 함께 정할 수 있다.
  • HttpStatus.NO_CONTENT는 204 No Content 상태를 의미한다.
  • ResponseEntity.noContent().build()도 204 No Content 응답을 만든다.
  • HttpStatus.ACCEPTED는 202 Accepted 상태를 의미한다.
  • 삭제 요청은 본문 없이 상태 코드만 반환할 수도 있고, 삭제 결과 메시지를 본문으로 반환할 수도 있다.

이 예제는 DELETE 요청에서 삭제 대상을 어떻게 전달하고, 삭제 결과를 어떤 상태 코드로 표현하는지 확인하는 예제이다.


DELETE 요청은 삭제 요청에 자주 사용한다

DELETE는 기존 데이터를 삭제할 때 자주 사용하는 요청 방식이다.
예를 들어 게시글 삭제, 댓글 삭제, 회원 탈퇴 같은 기능에서 사용할 수 있다.


다만 이번 예제는 실제 DB에서 데이터를 삭제하는 코드가 아니다.
요청으로 삭제할 대상을 전달하고, 서버가 그 요청에 대해 어떤 응답을 반환하는지 확인하는 예제이다.


삭제할 대상은 여러 방식으로 전달할 수 있다.
이번 예제에서는 주소 경로에 값을 넣는 방식과 요청 파라미터로 값을 전달하는 방식을 확인한다.


예를 들어 /restapi/둘리처럼 주소 경로에 값을 넣으면 @PathVariable로 받을 수 있다.
또 /restapi/deletetest1?email=dooly@test.com처럼 주소 뒤에 요청 파라미터를 붙이면 @RequestParam이나 단순 매개변수로 받을 수 있다.


이번 DELETE 예제의 목적은 실제 삭제 저장 로직이 아니라, 삭제 요청값을 받고 삭제 결과를 상태 코드로 표현하는 흐름을 확인하는 것이다.


공통 경로는 @RequestMapping("/restapi")로 정한다

// DeleteController.java
@RestController // 반환값을 응답 본문으로 바로 전달하는 `Controller`
@RequestMapping("/restapi") // 이 `Controller`의 공통 요청 경로
public class DeleteController {
}

DeleteController에도 @RequestMapping("/restapi")가 붙어 있다.
이 애노테이션은 이 Controller 안의 모든 요청 주소 앞에 /restapi를 붙이겠다는 뜻이다.


예를 들어 메서드에 /hello가 붙어 있으면 실제 요청 주소는 /restapi/hello가 된다.
메서드에 /deletetest4가 붙어 있으면 실제 요청 주소는 /restapi/deletetest4가 된다.


GET, POST, PUT, DELETE 예제 모두 /restapi를 공통 경로로 사용한다.
그래서 요청 방식은 달라도 기본 주소 구조는 비슷하게 비교할 수 있다.


클래스 위의 @RequestMapping("/restapi")는 이 Controller에 들어 있는 요청들의 공통 시작 주소를 정한다.


기본 DELETE 요청으로 문자열 반환하기

// DeleteController.java
@DeleteMapping(value = "/hello") // `/restapi/hello` `DELETE` 요청 처리
public String delete() {
    return "안녕? DELETE 방식 요청 했네~~~"; // 문자열 응답 반환
}

delete()는 /restapi/hello 주소로 들어오는 DELETE 요청을 처리한다.
@DeleteMapping은 DELETE 요청을 메서드와 연결하는 애노테이션이다.


이 메서드는 삭제할 대상 값을 따로 받지 않는다.
그냥 DELETE 방식으로 요청이 들어왔는지만 확인하고 문자열을 반환한다.


Talend API Tester에서 요청 방식은 DELETE로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/restapi/hello

예상 응답은 아래와 같다.

// 응답 본문
// 안녕? DELETE 방식 요청 했네~~~

Talend API Tester 결과를 보면 요청 방식은 DELETE이고, 요청 URL은 /restapi/hello이다.
응답 상태는 200이고, 응답 본문에는 "안녕? DELETE 방식 요청 했네~~~"가 출력된다.
따라서 /restapi/hello 요청이 DELETE 방식으로 들어왔을 때 delete() 메서드와 정상적으로 연결된 것을 확인할 수 있다.


@DeleteMapping은 해당 메서드가 DELETE 요청을 처리한다는 뜻이다.


@PathVariable로 삭제 대상 값을 받기

// DeleteController.java
@DeleteMapping(value = "/{variable}") // `/restapi/값` 형태의 `DELETE` 요청 처리
public String delete1(@PathVariable String variable) {
    return variable; // 경로로 받은 값 반환
}

delete1()은 주소 경로에 들어간 값을 받는다.
여기서 사용하는 애노테이션은 @PathVariable이다.


/{variable}에서 {variable}은 고정된 글자가 아니다.
그 자리에 실제 값이 들어올 수 있다는 뜻이다.
예를 들어 /restapi/둘리로 요청하면 둘리가 variable에 들어간다.


삭제 기능에서는 보통 삭제할 대상의 번호나 식별자를 주소 경로에 넣는 경우가 많다.
이번 예제에서는 값이 제대로 들어오는지 확인하기 위해 받은 값을 그대로 반환한다.


Talend API Tester에서 요청 방식은 DELETE로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/restapi/둘리

예상 응답은 아래와 같다.

// 응답 본문
// 둘리

Talend API Tester 결과를 보면 요청 방식은 DELETE이고, 요청 URL의 마지막 경로에 둘리가 들어가 있다.
응답 상태는 200이고, 응답 본문에도 둘리가 그대로 출력된다.
따라서 /restapi/{variable}의 {variable} 자리에 들어간 값이 @PathVariable로 전달된 것을 확인할 수 있다.


@PathVariable은 삭제할 대상 값을 주소 경로에 포함해 전달할 때 사용할 수 있다.


@RequestParam으로 요청 파라미터 받기

// DeleteController.java
@DeleteMapping(value = "/deletetest1") // 요청 파라미터 `email`을 받는 `DELETE` 요청 처리
public String delete2(@RequestParam String email) {
    return "e-mail : " + email; // 요청 파라미터로 받은 이메일 반환
}

deletetest1은 요청 파라미터로 email 값을 받는다.
요청 파라미터는 주소 뒤에 ?를 붙여 전달하는 값이다.


예를 들어 아래 주소를 보면 email이라는 이름으로 값이 전달된다.

// 요청 주소
// http://localhost:9000/restapi/deletetest1?email=dooly@test.com

이 요청이 들어오면 @RequestParam String email 부분에 dooly@test.com 값이 들어간다.
그리고 메서드는 "e-mail : " 문자열 뒤에 받은 이메일 값을 붙여 반환한다.


예상 응답은 아래와 같다.

// 응답 본문
// e-mail : dooly@test.com

Talend API Tester 결과를 보면 요청 방식은 DELETE이고, Query Parameters에 email=dooly@test.com 값이 들어가 있다.
응답 상태는 200이고, 응답 본문에는 e-mail : dooly@test.com이 출력된다.
이 결과에서는 DELETE 요청에서도 요청 파라미터를 받을 수 있다는 점을 확인한다.


@RequestParam은 주소 뒤의 요청 파라미터 값을 메서드 매개변수로 받을 때 사용한다.


ResponseEntity로 204 No Content 상태 반환하기

// DeleteController.java
@DeleteMapping(value = "/deletetest2") // 요청 파라미터 `id`를 받고 `204 No Content` 반환
public ResponseEntity<Object> delete3(int id) {
    return ResponseEntity
            .status(HttpStatus.NO_CONTENT) // `204 No Content` 상태 코드 설정
            .body(null); // 응답 본문 없이 반환
}

deletetest2는 요청 파라미터로 id 값을 받는다.
여기서는 @RequestParam을 직접 붙이지 않았지만, 단순 타입 매개변수 이름이 요청 파라미터 이름과 같으면 값을 받을 수 있다.


요청 주소는 아래처럼 작성한다.

// 요청 주소
// http://localhost:9000/restapi/deletetest2?id=100

이 요청이 들어오면 id 매개변수에 100이 들어간다.
다만 이 메서드는 id 값을 응답 본문에 사용하지 않는다.
대신 삭제 요청이 처리되었다는 의미로 204 No Content 상태를 반환한다.


204 No Content는 요청 처리는 성공했지만 응답 본문은 없다는 의미이다.
삭제 요청에서는 삭제 결과 데이터를 굳이 다시 보내지 않고 상태 코드만 반환할 때 사용할 수 있다.


예상 응답은 아래와 같다.

// 응답 상태
// 204 No Content
// 응답 본문
// 비어 있음

Talend API Tester 결과를 보면 요청 방식은 DELETE이고, Query Parameters에 id=100 값이 들어가 있다.
응답 상태는 204이고, 응답 본문 영역에는 No Content가 표시된다.
즉, 요청은 정상 처리되었지만 응답 본문은 없는 결과이다.


HttpStatus.NO_CONTENT는 요청은 성공했지만 응답 본문은 없다는 의미의 204 No Content 상태를 반환할 때 사용한다.


ResponseEntity.noContent().build()로 204 No Content 반환하기

// DeleteController.java
@DeleteMapping(value = "/deletetest3") // 요청 파라미터 `id`를 받고 `204 No Content` 반환
public ResponseEntity<Object> delete4(int id) {
    return ResponseEntity.noContent().build(); // `204 No Content` 응답 생성
}

deletetest3도 요청 파라미터로 id 값을 받는다.
요청 주소는 아래처럼 작성한다.

// 요청 주소
// http://localhost:9000/restapi/deletetest3?id=100

이 메서드는 ResponseEntity.noContent().build()를 반환한다.
이 코드는 204 No Content 응답을 더 간단하게 만드는 방식이다.


앞의 deletetest2는 ResponseEntity.status(HttpStatus.NO_CONTENT).body(null)로 작성했다.
이번 deletetest3은 ResponseEntity.noContent().build()로 작성했다.
둘 다 결과적으로 응답 본문 없이 204 No Content 상태를 반환한다.


예상 응답은 아래와 같다.

// 응답 상태
// 204 No Content
// 응답 본문
// 비어 있음

Talend API Tester 결과를 보면 요청 방식은 DELETE이고, Query Parameters에 id=100 값이 들어가 있다.
응답 상태는 204이고, 응답 본문 영역에는 No Content가 표시된다.
deletetest2와 결과는 같지만, 코드 작성 방식이 더 간단하다.


정리하면 두 방식의 차이는 코드 표현이다.

  • ResponseEntity.status(HttpStatus.NO_CONTENT).body(null)은 상태 코드와 빈 본문을 직접 지정한다.
  • ResponseEntity.noContent().build()는 204 No Content 응답을 더 짧게 만든다.

ResponseEntity.noContent().build()는 응답 본문 없이 204 No Content 상태를 반환할 때 자주 쓰는 간단한 표현이다.


ResponseEntity로 202 Accepted 상태와 삭제 메시지 반환하기

// DeleteController.java
@DeleteMapping(value = "/deletetest4") // 요청 파라미터 `id`를 받고 `202 Accepted`와 메시지 반환
public ResponseEntity<String> delete5(int id) {
    return ResponseEntity
            .status(HttpStatus.ACCEPTED) // `202 Accepted` 상태 코드 설정
            .body(id + "번 글이 삭제되었어요"); // 삭제 결과 메시지 반환
}

deletetest4도 요청 파라미터로 id 값을 받는다.
요청 주소는 아래처럼 작성한다.

// 요청 주소
// http://localhost:9000/restapi/deletetest4?id=100

이 요청이 들어오면 id 매개변수에 100이 들어간다.
그리고 응답 본문에는 "100번 글이 삭제되었어요"라는 문자열이 반환된다.


응답 상태 코드는 HttpStatus.ACCEPTED로 지정한다.
HttpStatus.ACCEPTED는 202 Accepted 상태를 의미한다.
이번 예제에서는 삭제 요청을 받아들였고, 그 결과 메시지를 함께 반환하는 흐름을 확인한다.


예상 응답은 아래와 같다.

// 응답 상태
// 202 Accepted
// 응답 본문
// 100번 글이 삭제되었어요

Talend API Tester 결과를 보면 요청 방식은 DELETE이고, Query Parameters에 id=100 값이 들어가 있다.
응답 상태는 202이고, 응답 본문에는 100번 글이 삭제되었어요라는 메시지가 출력된다.
즉, 삭제 요청을 받아들였다는 상태 코드와 삭제 결과 메시지를 함께 반환한 것이다.


ResponseEntity.status(HttpStatus.ACCEPTED).body(...)는 202 Accepted 상태와 응답 메시지를 함께 반환할 때 사용할 수 있다.


DELETE 요청에서 응답 상태 코드 비교하기

이번 예제에서 가장 헷갈리기 쉬운 부분은 deletetest2, deletetest3, deletetest4의 차이이다.
세 메서드는 모두 요청 파라미터로 id 값을 받는다.
하지만 응답을 만드는 방식과 상태 코드가 다르다.


deletetest2는 ResponseEntity.status(HttpStatus.NO_CONTENT).body(null)을 반환한다.
상태 코드는 204 No Content이고 응답 본문은 없다.


deletetest3은 ResponseEntity.noContent().build()를 반환한다.
이것도 상태 코드는 204 No Content이고 응답 본문은 없다.
차이는 같은 결과를 더 간단한 코드로 만든다는 점이다.


deletetest4는 ResponseEntity.status(HttpStatus.ACCEPTED).body(...)를 반환한다.
상태 코드는 202 Accepted이고 응답 본문에는 삭제 메시지가 들어간다.


정리하면 아래처럼 볼 수 있다.

  • deletetest2는 204 No Content와 빈 본문을 직접 지정한다.
  • deletetest3은 204 No Content 응답을 간단한 코드로 만든다.
  • deletetest4는 202 Accepted와 삭제 메시지를 함께 반환한다.
  • 삭제 요청은 꼭 응답 본문을 반환해야 하는 것은 아니다.
  • 삭제 결과를 메시지로 보여 주고 싶다면 응답 본문을 함께 반환할 수 있다.

삭제 요청에서는 응답 본문을 비우고 상태 코드만 반환할 수도 있고, 삭제 결과 메시지를 본문으로 반환할 수도 있다.


실행 흐름 정리

이 예제의 실행 흐름은 아래 순서로 진행된다.

  • 서버를 실행한다.
  • Talend API Tester에서 요청 방식을 DELETE로 선택한다.
  • /restapi/hello 요청을 보내서 기본 DELETE 응답을 확인한다.
  • /restapi/둘리 요청을 보내서 @PathVariable 값이 들어오는지 확인한다.
  • /restapi/deletetest1?email=dooly@test.com 요청을 보내서 @RequestParam 값이 들어오는지 확인한다.
  • /restapi/deletetest2?id=100 요청을 보내서 204 No Content와 빈 본문을 확인한다.
  • /restapi/deletetest3?id=100 요청을 보내서 ResponseEntity.noContent().build() 결과를 확인한다.
  • /restapi/deletetest4?id=100 요청을 보내서 202 Accepted와 삭제 메시지를 확인한다.

흐름을 한 줄로 정리하면 다음과 같다.


DELETE 요청 → 경로값 또는 요청 파라미터 전달 → Controller 메서드 실행 → 문자열 또는 ResponseEntity 응답 반환


DELETE 요청은 삭제할 대상을 전달하고, Controller는 삭제 요청 결과를 상태 코드와 응답 본문으로 표현할 수 있다.


핵심 정리

이 예제의 핵심은 DELETE 요청에서 삭제 대상을 전달하는 방식과 삭제 응답 상태 코드를 비교하는 것이다.
DeleteController는 /restapi 공통 경로 아래에서 여러 DELETE 요청을 처리한다.
각 메서드는 요청값을 받는 방식과 응답을 만드는 방식이 조금씩 다르다.


이 흐름은 아래처럼 정리할 수 있다.

  • DELETE 요청은 기존 데이터를 삭제하는 요청에 자주 사용한다.
  • @RestController는 반환값을 응답 본문으로 바로 보낸다.
  • @RequestMapping("/restapi")는 공통 요청 경로를 정한다.
  • @DeleteMapping은 DELETE 요청을 처리한다.
  • @PathVariable은 주소 경로에 들어간 값을 받는다.
  • @RequestParam은 요청 파라미터 값을 받는다.
  • 단순 타입 매개변수도 요청 파라미터 이름과 같으면 값을 받을 수 있다.
  • ResponseEntity는 응답 본문과 상태 코드를 함께 정할 수 있다.
  • HttpStatus.NO_CONTENT는 204 No Content 상태를 의미한다.
  • ResponseEntity.noContent().build()는 204 No Content 응답을 간단하게 만든다.
  • HttpStatus.ACCEPTED는 202 Accepted 상태를 의미한다.
  • 이번 응용예제 결과물은 Talend API Tester에서 요청 방식, 요청 URL, 요청 파라미터, 응답 본문, 응답 상태 코드를 기준으로 확인한다.

따라서 이 예제가 알려 주는 핵심은 이것이다.
DELETE 요청에서는 삭제할 대상을 경로값이나 요청 파라미터로 전달할 수 있고, Controller는 삭제 결과를 문자열, 204 No Content, 202 Accepted 같은 응답으로 표현할 수 있다.
다음 예제에서는 실제 게시글 목록을 다루는 REST API 흐름을 확인한다.



8. 실제 게시글 목록을 다루는 REST API 흐름 확인하기 응용예제

이 예제는 앞에서 확인한 GET, POST, PUT, DELETE 요청을 하나의 게시글 기능에 적용해 보는 흐름이다.
앞의 예제들은 요청 방식 하나씩 따로 확인했다.
이번 예제는 게시글 목록을 기준으로 조회, 등록, 단건 조회, 삭제, 수정 요청을 한 Controller 안에서 함께 확인한다.


이번 예제에서는 /boards로 시작하는 요청을 사용한다.
GET /boards는 게시글 전체 목록 조회, POST /boards는 게시글 등록, GET /boards/{boardNo}는 게시글 한 개 조회, DELETE /boards/{boardNo}는 게시글 삭제, PUT /boards/{boardNo}는 게시글 수정 흐름이다.


결과물은 각 요청 설명 바로 아래에서 Talend API Tester 기준으로 확인한다.
각 요청마다 요청 방식, 요청 URL, 요청 본문, 응답 상태, 응답 본문을 함께 봐야 한다.
특히 같은 /boards 주소를 사용하더라도 요청 방식이 달라지면 실행되는 기능도 달라진다.


이 예제의 핵심은 REST API에서 같은 /boards 주소를 사용하더라도 요청 방식과 경로값에 따라 조회, 등록, 수정, 삭제 기능이 나뉜다는 점이다.


예제 전체 코드

// BoardController.java
package com.example.springrestedu.controller; // 클래스가 속한 패키지 경로

import java.time.DayOfWeek; // 요일 정보를 다루기 위한 클래스
import java.time.LocalDate; // 현재 날짜를 구하기 위한 클래스
import java.time.LocalDateTime; // 게시글 등록 시간을 저장하기 위한 클래스
import java.time.format.TextStyle; // 요일 표시 형식을 정하기 위한 클래스
import java.util.ArrayList; // 게시글 목록을 저장할 리스트 구현체
import java.util.List; // 게시글 목록을 다루기 위한 인터페이스
import java.util.Locale; // 한국어 요일 표시를 위한 지역 정보

import com.example.springrestedu.domain.BoardDTO; // 게시글 데이터를 담는 DTO
import org.springframework.http.HttpStatus; // 응답 상태 코드를 표현하는 클래스
import org.springframework.http.ResponseEntity; // 응답 본문과 상태 코드를 함께 반환하는 클래스
import org.springframework.web.bind.annotation.*; // 요청 매핑과 요청값 처리를 위한 애노테이션 모음
import lombok.extern.slf4j.Slf4j; // 로그 출력을 위한 Lombok 애노테이션

@Slf4j // log.info() 사용 설정
@RestController // 반환값을 응답 본문으로 바로 전달하는 Controller
@RequestMapping("/boards") // 이 Controller의 공통 요청 경로
@CrossOrigin(origins = "*") // 모든 출처의 요청 허용
public class BoardController {

    List<BoardDTO> boardList = new ArrayList<>(); // 게시글 데이터를 임시로 저장할 목록

    public BoardController() {
        BoardDTO board = new BoardDTO(); // 첫 번째 게시글 객체 생성
        board.setBoardNo(1); // 게시글 번호 저장
        board.setTitle("아기공룡 둘리 한자대탐험"); // 게시글 제목 저장
        board.setContent("둘리 학습만화 시리즈"); // 게시글 내용 저장
        board.setWriter("김수정"); // 작성자 저장
        board.setRegDate(LocalDateTime.now()); // 현재 시간을 등록일로 저장

        boardList.add(board); // 첫 번째 게시글을 목록에 추가

        board = new BoardDTO(); // 두 번째 게시글 객체 생성
        board.setBoardNo(2); // 게시글 번호 저장
        board.setTitle("고래 도서관"); // 게시글 제목 저장
        board.setContent("바다 도서관 이야기"); // 게시글 내용 저장
        board.setWriter("지드루"); // 작성자 저장
        board.setRegDate(LocalDateTime.now()); // 현재 시간을 등록일로 저장

        boardList.add(board); // 두 번째 게시글을 목록에 추가
    }

    @GetMapping // GET /boards 요청 처리
    public ResponseEntity<List<BoardDTO>> list() {
        log.info("list 요청"); // 목록 조회 요청 로그 출력

        if (boardList.isEmpty()) { // 게시글 목록이 비어 있는지 확인
            return ResponseEntity.noContent().build(); // 목록이 없으면 204 상태 반환
        }

        ResponseEntity<List<BoardDTO>> entity =
                new ResponseEntity<>(boardList, HttpStatus.OK); // 목록과 200 상태 반환
        return entity; // 응답 반환
    }

    @PostMapping // POST /boards 요청 처리
    public ResponseEntity<String> register(@RequestBody BoardDTO board) {
        log.info("register 요청"); // 등록 요청 로그 출력

        boardList.add(board); // 요청 본문으로 받은 게시글을 목록에 추가
        ResponseEntity<String> entity =
                new ResponseEntity<>("성공적으로 삽입했어용", HttpStatus.CREATED); // 등록 메시지와 201 상태 반환

        return entity; // 응답 반환
    }

    @GetMapping("/{boardNo}") // GET /boards/{boardNo} 요청 처리
    public ResponseEntity<BoardDTO> read(@PathVariable("boardNo") int boardNo) {
        log.info("read 요청"); // 단건 조회 요청 로그 출력

        BoardDTO board = new BoardDTO(); // 검색 기준으로 사용할 게시글 객체 생성
        board.setBoardNo(boardNo); // 경로값으로 받은 게시글 번호 저장

        int index = boardList.indexOf(board); // 같은 boardNo를 가진 게시글 위치 검색
        if (index >= 0) { // 게시글을 찾은 경우
            board = boardList.get(index); // 목록에서 실제 게시글 객체 꺼내기
        }

        ResponseEntity<BoardDTO> entity =
                new ResponseEntity<>(board, HttpStatus.OK); // 조회된 게시글과 200 상태 반환

        return entity; // 응답 반환
    }

    @DeleteMapping("/{boardNo}") // DELETE /boards/{boardNo} 요청 처리
    public ResponseEntity<String> remove(@PathVariable("boardNo") int boardNo) {
        log.info("remove 요청"); // 삭제 요청 로그 출력

        BoardDTO board = new BoardDTO(); // 삭제 기준으로 사용할 게시글 객체 생성
        board.setBoardNo(boardNo); // 경로값으로 받은 게시글 번호 저장

        boardList.remove(board); // 같은 boardNo를 가진 게시글 삭제

        ResponseEntity<String> entity =
                new ResponseEntity<>("성공적으로 삭제했어용", HttpStatus.OK); // 삭제 메시지와 200 상태 반환

        return entity; // 응답 반환
    }

    @PutMapping("/{boardNo}") // PUT /boards/{boardNo} 요청 처리
    public ResponseEntity<String> modify(@PathVariable("boardNo") int boardNo, @RequestBody BoardDTO board) {
        log.info("modify 요청"); // 수정 요청 로그 출력

        BoardDTO board1 = new BoardDTO(); // 수정할 게시글을 찾기 위한 기준 객체 생성
        board1.setBoardNo(boardNo); // 경로값으로 받은 게시글 번호 저장

        int index = boardList.indexOf(board1); // 같은 boardNo를 가진 게시글 위치 검색
        if (index >= 0) { // 게시글을 찾은 경우
            board1 = boardList.get(index); // 목록에서 실제 게시글 객체 꺼내기
        }

        board1.setWriter(board.getWriter()); // 작성자 수정
        board1.setContent(board.getContent()); // 내용 수정
        board1.setTitle(board.getTitle()); // 제목 수정
        board1.setRegDate(board.getRegDate()); // 등록일 수정

        ResponseEntity<String> entity =
                new ResponseEntity<>("성공적으로 수정했어용", HttpStatus.OK); // 수정 메시지와 200 상태 반환

        return entity; // 응답 반환
    }

    @GetMapping("/day") // GET /boards/day 요청 처리
    public String day() {
        LocalDate ld = LocalDate.now(); // 현재 날짜 구하기
        DayOfWeek dow = ld.getDayOfWeek(); // 현재 날짜의 요일 구하기
        String korDay = dow.getDisplayName(TextStyle.SHORT, Locale.KOREAN); // 요일을 한국어 짧은 형식으로 변환
        return korDay; // 한국어 요일 반환
    }

    @GetMapping(value = "/friends") // GET /boards/friends 요청 처리
    public String getFriends() {
        log.info("getFriends 메소드가 호출되었습니다."); // 친구 목록 요청 로그 출력
        return "둘리 또치 도우너"; // 문자열 응답 반환
    }
}
// BoardDTO.java
package com.example.springrestedu.domain; // 클래스가 속한 패키지 경로

import java.time.LocalDateTime; // 게시글 등록일을 저장하기 위한 클래스
import lombok.*; // Getter, Setter, ToString, EqualsAndHashCode 사용

@Getter // getter 메서드 자동 생성
@Setter // setter 메서드 자동 생성
@ToString // 객체 내용을 문자열로 확인할 수 있게 자동 생성
@EqualsAndHashCode(of = "boardNo") // boardNo만 기준으로 같은 게시글인지 비교
public class BoardDTO {

    private long boardNo; // 게시글 번호
    private String title; // 게시글 제목
    private String content; // 게시글 내용
    private String writer; // 작성자
    private LocalDateTime regDate; // 등록일
}

이 코드는 크게 두 파일로 나누어 볼 수 있다.
첫 번째 BoardController.java는 게시글 관련 요청을 처리하는 Controller이다.
두 번째 BoardDTO.java는 게시글 한 개의 데이터를 담는 객체이다.


BoardController는 /boards를 공통 경로로 사용한다.
그래서 게시글 목록 조회는 /boards, 게시글 한 개 조회는 /boards/{boardNo}, 게시글 삭제도 /boards/{boardNo} 형태로 요청한다.
같은 주소처럼 보이더라도 요청 방식이 GET, POST, PUT, DELETE 중 무엇인지에 따라 실행되는 메서드가 달라진다.


이번 예제는 앞에서 배운 요청 방식을 게시글 기능 하나에 모아서 적용하는 종합 예제이다.


이 예제에서 확인할 핵심

  • @RequestMapping("/boards")는 게시글 기능의 공통 경로를 정한다.
  • @CrossOrigin(origins = "*")는 다른 출처의 요청을 허용한다.
  • boardList는 게시글 데이터를 임시로 저장하는 목록이다.
  • 생성자에서 기본 게시글 2개를 미리 넣어 둔다.
  • GET /boards는 전체 게시글 목록을 조회한다.
  • POST /boards는 요청 본문으로 받은 게시글을 등록한다.
  • GET /boards/{boardNo}는 게시글 번호로 게시글 한 개를 조회한다.
  • DELETE /boards/{boardNo}는 게시글 번호로 게시글을 삭제한다.
  • PUT /boards/{boardNo}는 게시글 번호로 게시글을 찾아 수정한다.
  • @RequestBody는 요청 본문에 들어온 JSON 데이터를 객체로 받는다.
  • @PathVariable은 주소 경로에 들어간 게시글 번호를 받는다.
  • ResponseEntity는 응답 본문과 상태 코드를 함께 반환한다.
  • @EqualsAndHashCode(of = "boardNo") 때문에 boardNo가 같으면 같은 게시글로 비교된다.

이 예제는 게시글 목록을 임시 저장소처럼 사용해서 REST API의 기본 요청 흐름을 확인하는 예제이다.


BoardDTO는 게시글 한 개의 데이터를 담는 객체이다

// BoardDTO.java
@Getter // getter 메서드 자동 생성
@Setter // setter 메서드 자동 생성
@ToString // 객체 내용을 문자열로 확인할 수 있게 자동 생성
@EqualsAndHashCode(of = "boardNo") // boardNo만 기준으로 같은 게시글인지 비교
public class BoardDTO {

    private long boardNo; // 게시글 번호
    private String title; // 게시글 제목
    private String content; // 게시글 내용
    private String writer; // 작성자
    private LocalDateTime regDate; // 등록일
}

BoardDTO는 게시글 한 개의 데이터를 담는 객체이다.
DTO는 데이터를 옮기기 위한 객체라고 이해하면 된다.
여기서는 게시글 번호, 제목, 내용, 작성자, 등록일을 하나로 묶어서 주고받기 위해 사용한다.


boardNo는 게시글을 구분하는 번호이다.
게시글 목록에서 어떤 게시글을 조회할지, 삭제할지, 수정할지 판단할 때 기준이 된다.


title은 게시글 제목이고, content는 게시글 내용이다.
writer는 작성자이고, regDate는 게시글 등록 시간이다.
regDate의 타입은 LocalDateTime이다.
LocalDateTime은 날짜와 시간을 함께 표현하는 클래스이다.


여기서 중요한 애노테이션은 @EqualsAndHashCode(of = "boardNo")이다.
이 애노테이션은 객체를 비교할 때 모든 필드를 보지 않고 boardNo만 기준으로 비교하게 만든다.


예를 들어 목록 안에 boardNo가 1인 게시글이 있다고 하자.
새로 만든 BoardDTO 객체에 boardNo만 1로 넣어도, indexOf()나 remove()에서 같은 게시글로 찾을 수 있다.


이 예제에서 게시글을 찾거나 삭제할 수 있는 이유는 BoardDTO가 boardNo를 기준으로 같은 객체인지 비교하도록 설정되어 있기 때문이다.


boardList는 임시 게시글 저장소 역할을 한다

// BoardController.java
List<BoardDTO> boardList = new ArrayList<>(); // 게시글 데이터를 임시로 저장할 목록

boardList는 게시글 데이터를 저장하는 목록이다.
여기서는 실제 DB를 사용하지 않는다.
대신 서버 메모리 안의 ArrayList에 게시글을 저장한다.


ArrayList는 여러 데이터를 순서대로 저장하는 자료구조이다.
이번 예제에서는 BoardDTO 객체 여러 개를 저장한다.
즉, boardList는 게시글 목록처럼 동작한다.


다만 이 방식은 실제 서비스의 영구 저장소가 아니다.
서버를 다시 실행하면 boardList에 런타임 중 추가하거나 수정한 데이터는 사라질 수 있다.
이번 예제에서는 REST API 요청 흐름을 확인하는 것이 목적이므로, 메모리 목록을 사용한다.


boardList는 실제 데이터베이스가 아니라 요청 흐름을 확인하기 위한 임시 저장소이다.


생성자에서 기본 게시글 2개를 미리 저장한다

// BoardController.java
public BoardController() {
    BoardDTO board = new BoardDTO(); // 첫 번째 게시글 객체 생성
    board.setBoardNo(1); // 게시글 번호 저장
    board.setTitle("아기공룡 둘리 한자대탐험"); // 게시글 제목 저장
    board.setContent("둘리 학습만화 시리즈"); // 게시글 내용 저장
    board.setWriter("김수정"); // 작성자 저장
    board.setRegDate(LocalDateTime.now()); // 현재 시간을 등록일로 저장

    boardList.add(board); // 첫 번째 게시글을 목록에 추가

    board = new BoardDTO(); // 두 번째 게시글 객체 생성
    board.setBoardNo(2); // 게시글 번호 저장
    board.setTitle("고래 도서관"); // 게시글 제목 저장
    board.setContent("바다 도서관 이야기"); // 게시글 내용 저장
    board.setWriter("지드루"); // 작성자 저장
    board.setRegDate(LocalDateTime.now()); // 현재 시간을 등록일로 저장

    boardList.add(board); // 두 번째 게시글을 목록에 추가
}

BoardController 생성자에서는 기본 게시글 2개를 만들어 boardList에 넣는다.
생성자는 객체가 만들어질 때 자동으로 실행되는 특별한 메서드이다.


BoardController가 생성되면 첫 번째 게시글과 두 번째 게시글이 미리 목록에 들어간다.
그래서 서버를 실행한 직후 GET /boards 요청을 보내면 빈 목록이 아니라 기본 게시글 2개가 조회된다.


LocalDateTime.now()는 현재 날짜와 시간을 구한다.
여기서는 게시글 등록일처럼 사용한다.


기본 게시글을 생성자에서 미리 넣어 두기 때문에 목록 조회 요청을 바로 테스트할 수 있다.


GET /boards로 전체 게시글 목록 조회하기

// BoardController.java
@GetMapping // GET /boards 요청 처리
public ResponseEntity<List<BoardDTO>> list() {
    log.info("list 요청"); // 목록 조회 요청 로그 출력

    if (boardList.isEmpty()) { // 게시글 목록이 비어 있는지 확인
        return ResponseEntity.noContent().build(); // 목록이 없으면 204 상태 반환
    }

    ResponseEntity<List<BoardDTO>> entity =
            new ResponseEntity<>(boardList, HttpStatus.OK); // 목록과 200 상태 반환
    return entity; // 응답 반환
}

list()는 전체 게시글 목록을 조회하는 메서드이다.
클래스 위에 /boards가 공통 경로로 붙어 있고, 메서드에는 따로 경로가 없다.
그래서 실제 요청 주소는 GET /boards이다.


먼저 boardList.isEmpty()로 게시글 목록이 비어 있는지 확인한다.
목록이 비어 있으면 ResponseEntity.noContent().build()를 반환한다.
이 응답은 204 No Content 상태를 의미한다.


목록에 게시글이 있으면 new ResponseEntity<>(boardList, HttpStatus.OK)를 반환한다.
응답 본문에는 게시글 목록이 들어가고, 응답 상태는 200 OK가 된다.


Talend API Tester에서는 요청 방식은 GET으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/boards

예상 응답 상태는 아래와 같다.

// 응답 상태
// 200 OK

예상 응답 본문은 게시글 배열 형태로 나온다.

// 응답 본문 예시
// [
//   {
//     "boardNo": 1,
//     "title": "아기공룡 둘리 한자대탐험",
//     "content": "둘리 학습만화 시리즈",
//     "writer": "김수정",
//     "regDate": "현재 날짜와 시간"
//   },
//   {
//     "boardNo": 2,
//     "title": "고래 도서관",
//     "content": "바다 도서관 이야기",
//     "writer": "지드루",
//     "regDate": "현재 날짜와 시간"
//   }
// ]

Talend API Tester 결과를 보면 요청 방식은 GET이고, 요청 URL은 /boards이다.
응답 상태는 200이고, 응답 본문에는 기본 게시글 2개가 JSON 배열로 반환된다.
각 게시글에는 boardNo, content, regDate, title, writer 값이 들어 있다.


GET /boards는 게시글 목록 전체를 조회하고, 목록이 있으면 200 OK와 게시글 배열을 반환한다.


POST /boards로 게시글 등록하기

// BoardController.java
@PostMapping // POST /boards 요청 처리
public ResponseEntity<String> register(@RequestBody BoardDTO board) {
    log.info("register 요청"); // 등록 요청 로그 출력

    boardList.add(board); // 요청 본문으로 받은 게시글을 목록에 추가
    ResponseEntity<String> entity =
            new ResponseEntity<>("성공적으로 삽입했어용", HttpStatus.CREATED); // 등록 메시지와 201 상태 반환

    return entity; // 응답 반환
}

register()는 새 게시글을 등록하는 메서드이다.
요청 방식은 POST이고, 요청 주소는 /boards이다.


@RequestBody BoardDTO board는 요청 본문에 들어온 JSON 데이터를 BoardDTO 객체로 받겠다는 뜻이다.
요청 본문의 boardNo, title, content, writer, regDate 이름이 BoardDTO 필드 이름과 맞으면 값이 객체에 들어간다.


그다음 boardList.add(board)로 요청받은 게시글을 목록에 추가한다.
응답으로는 "성공적으로 삽입했어용"이라는 문자열과 201 Created 상태를 반환한다.
201 Created는 새 데이터가 생성되었음을 표현할 때 사용할 수 있는 상태 코드이다.


Talend API Tester에서는 요청 방식은 POST로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/boards

요청 본문은 JSON 형식으로 작성한다.

// 요청 본문
// {
//   "boardNo": 3,
//   "title": "새 게시글",
//   "content": "새 게시글 내용",
//   "writer": "둘리",
//   "regDate": "2026-05-14T16:40:00"
// }

예상 응답 상태와 본문은 아래와 같다.

// 응답 상태
// 201 Created
// 응답 본문
// 성공적으로 삽입했어용

Talend API Tester 결과를 보면 요청 방식은 POST이고, 요청 URL은 /boards이다.
요청 본문에는 새 게시글 데이터가 JSON으로 들어가 있다.
응답 상태는 201이고, 응답 본문에는 성공적으로 삽입했어용이 출력된다.


등록 요청을 보낸 뒤 다시 GET /boards를 실행하면 방금 추가한 게시글이 목록에 포함되어야 한다.


POST /boards는 요청 본문의 게시글 데이터를 BoardDTO로 받아 boardList에 추가하고 201 Created 상태를 반환한다.


GET /boards/{boardNo}로 게시글 한 개 조회하기

// BoardController.java
@GetMapping("/{boardNo}") // GET /boards/{boardNo} 요청 처리
public ResponseEntity<BoardDTO> read(@PathVariable("boardNo") int boardNo) {
    log.info("read 요청"); // 단건 조회 요청 로그 출력

    BoardDTO board = new BoardDTO(); // 검색 기준으로 사용할 게시글 객체 생성
    board.setBoardNo(boardNo); // 경로값으로 받은 게시글 번호 저장

    int index = boardList.indexOf(board); // 같은 boardNo를 가진 게시글 위치 검색
    if (index >= 0) { // 게시글을 찾은 경우
        board = boardList.get(index); // 목록에서 실제 게시글 객체 꺼내기
    }

    ResponseEntity<BoardDTO> entity =
            new ResponseEntity<>(board, HttpStatus.OK); // 조회된 게시글과 200 상태 반환

    return entity; // 응답 반환
}

read()는 게시글 한 개를 조회하는 메서드이다.
요청 주소는 /boards/{boardNo}이다.
여기서 {boardNo}는 실제 게시글 번호가 들어가는 자리이다.


예를 들어 /boards/1로 요청하면 boardNo에는 1이 들어간다.
@PathVariable("boardNo")는 주소 경로에 들어간 boardNo 값을 메서드 매개변수로 받는다.


그다음 BoardDTO 객체를 하나 만들고, 그 객체에 boardNo만 저장한다.
그리고 boardList.indexOf(board)로 목록 안에서 같은 게시글 번호를 가진 객체를 찾는다.
이 동작이 가능한 이유는 BoardDTO에 @EqualsAndHashCode(of = "boardNo")가 붙어 있기 때문이다.
즉, boardNo가 같으면 같은 게시글로 비교된다.


게시글을 찾으면 boardList.get(index)로 실제 게시글 객체를 꺼낸다.
그리고 그 게시글을 응답 본문에 넣고 200 OK 상태로 반환한다.


Talend API Tester에서는 요청 방식은 GET으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/boards/1

예상 응답 상태는 아래와 같다.

// 응답 상태
// 200 OK

예상 응답 본문은 아래와 같다.

// 응답 본문 예시
// {
//   "boardNo": 1,
//   "title": "아기공룡 둘리 한자대탐험",
//   "content": "둘리 학습만화 시리즈",
//   "writer": "김수정",
//   "regDate": "현재 날짜와 시간"
// }

Talend API Tester 결과를 보면 요청 방식은 GET이고, 요청 URL은 /boards/1이다.
응답 상태는 200이고, 응답 본문에는 boardNo가 1인 게시글 데이터가 JSON 객체로 반환된다.
이 결과를 통해 주소 경로에 들어간 1이 @PathVariable로 전달되고, 해당 게시글 조회에 사용된 것을 확인할 수 있다.


GET /boards/{boardNo}는 주소 경로의 게시글 번호를 받아 해당 게시글을 찾아 반환한다.


DELETE /boards/{boardNo}로 게시글 삭제하기

// BoardController.java
@DeleteMapping("/{boardNo}") // DELETE /boards/{boardNo} 요청 처리
public ResponseEntity<String> remove(@PathVariable("boardNo") int boardNo) {
    log.info("remove 요청"); // 삭제 요청 로그 출력

    BoardDTO board = new BoardDTO(); // 삭제 기준으로 사용할 게시글 객체 생성
    board.setBoardNo(boardNo); // 경로값으로 받은 게시글 번호 저장

    boardList.remove(board); // 같은 boardNo를 가진 게시글 삭제

    ResponseEntity<String> entity =
            new ResponseEntity<>("성공적으로 삭제했어용", HttpStatus.OK); // 삭제 메시지와 200 상태 반환

    return entity; // 응답 반환
}

remove()는 게시글을 삭제하는 메서드이다.
요청 방식은 DELETE이고, 요청 주소는 /boards/{boardNo}이다.


예를 들어 /boards/1로 DELETE 요청을 보내면 boardNo에는 1이 들어간다.
그다음 삭제 기준으로 사용할 BoardDTO 객체를 만들고, boardNo를 저장한다.


boardList.remove(board)는 목록에서 같은 게시글을 삭제한다.
이때도 BoardDTO의 @EqualsAndHashCode(of = "boardNo") 설정이 중요하다.
boardNo가 같으면 같은 게시글로 비교되기 때문에, 번호만 넣은 객체로도 목록에서 삭제할 대상을 찾을 수 있다.


삭제 후에는 "성공적으로 삭제했어용"이라는 문자열과 200 OK 상태를 반환한다.


Talend API Tester에서는 요청 방식은 DELETE로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/boards/1

예상 응답 상태와 본문은 아래와 같다.

// 응답 상태
// 200 OK
// 응답 본문
// 성공적으로 삭제했어용

Talend API Tester 결과를 보면 요청 방식은 DELETE이고, 요청 URL은 /boards/1이다.
응답 상태는 200이고, 응답 본문에는 성공적으로 삭제했어용이 출력된다.
이 결과는 boardNo가 1인 게시글을 삭제하는 요청이 정상적으로 처리되었다는 뜻이다.


삭제 요청을 보낸 뒤 다시 GET /boards를 실행하면 삭제된 게시글이 목록에서 빠졌는지 확인할 수 있다.


DELETE /boards/{boardNo}는 주소 경로의 게시글 번호를 기준으로 게시글을 삭제한다.


PUT /boards/{boardNo}로 게시글 수정하기

// BoardController.java
@PutMapping("/{boardNo}") // PUT /boards/{boardNo} 요청 처리
public ResponseEntity<String> modify(@PathVariable("boardNo") int boardNo, @RequestBody BoardDTO board) {
    log.info("modify 요청"); // 수정 요청 로그 출력

    BoardDTO board1 = new BoardDTO(); // 수정할 게시글을 찾기 위한 기준 객체 생성
    board1.setBoardNo(boardNo); // 경로값으로 받은 게시글 번호 저장

    int index = boardList.indexOf(board1); // 같은 boardNo를 가진 게시글 위치 검색
    if (index >= 0) { // 게시글을 찾은 경우
        board1 = boardList.get(index); // 목록에서 실제 게시글 객체 꺼내기
    }

    board1.setWriter(board.getWriter()); // 작성자 수정
    board1.setContent(board.getContent()); // 내용 수정
    board1.setTitle(board.getTitle()); // 제목 수정
    board1.setRegDate(board.getRegDate()); // 등록일 수정

    ResponseEntity<String> entity =
            new ResponseEntity<>("성공적으로 수정했어용", HttpStatus.OK); // 수정 메시지와 200 상태 반환

    return entity; // 응답 반환
}

modify()는 게시글을 수정하는 메서드이다.
요청 방식은 PUT이고, 요청 주소는 /boards/{boardNo}이다.


주소 경로의 {boardNo}는 수정할 게시글 번호이다.
요청 본문에는 수정할 제목, 내용, 작성자, 등록일을 JSON으로 보낸다.


이 메서드는 값을 두 군데에서 받는다.
첫 번째는 @PathVariable("boardNo") int boardNo이다.
이 값은 어떤 게시글을 수정할지 찾는 기준이다.
두 번째는 @RequestBody BoardDTO board이다.
이 값은 무엇으로 수정할지 담고 있는 새 데이터이다.


먼저 boardNo만 담은 BoardDTO 객체를 만든다.
그리고 boardList.indexOf(board1)로 목록에서 수정할 게시글 위치를 찾는다.
게시글을 찾으면 boardList.get(index)로 실제 게시글 객체를 꺼낸다.


그 다음 요청 본문으로 받은 board의 값을 실제 게시글 객체 board1에 복사한다.
이 과정에서 작성자, 내용, 제목, 등록일이 수정된다.


Talend API Tester에서는 요청 방식은 PUT으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/boards/2

요청 본문은 JSON 형식으로 작성한다.

// 요청 본문
// {
//   "title": "수정된 제목",
//   "content": "수정된 내용",
//   "writer": "또치",
//   "regDate": "2026-05-14T16:50:00"
// }

예상 응답 상태와 본문은 아래와 같다.

// 응답 상태
// 200 OK
// 응답 본문
// 성공적으로 수정했어용

Talend API Tester 결과를 보면 요청 방식은 PUT이고, 요청 URL은 /boards/2이다.
요청 본문에는 수정할 제목, 내용, 작성자, 등록일이 JSON으로 들어가 있다.
응답 상태는 200이고, 응답 본문에는 성공적으로 수정했어용이 출력된다.


수정 요청을 보낸 뒤 GET /boards/2를 실행하면 제목, 내용, 작성자, 등록일이 바뀌었는지 확인할 수 있다.


PUT /boards/{boardNo}는 경로값으로 수정할 게시글을 찾고, 요청 본문의 값으로 게시글 내용을 바꾼다.


GET /boards/day로 현재 요일 문자열 반환하기

// BoardController.java
@GetMapping("/day") // GET /boards/day 요청 처리
public String day() {
    LocalDate ld = LocalDate.now(); // 현재 날짜 구하기
    DayOfWeek dow = ld.getDayOfWeek(); // 현재 날짜의 요일 구하기
    String korDay = dow.getDisplayName(TextStyle.SHORT, Locale.KOREAN); // 요일을 한국어 짧은 형식으로 변환
    return korDay; // 한국어 요일 반환
}

day()는 게시글 데이터와 직접 관련된 메서드는 아니다.
하지만 같은 BoardController 안에서 GET 요청으로 문자열을 반환하는 예시로 볼 수 있다.


LocalDate.now()는 현재 날짜를 구한다.
getDayOfWeek()는 현재 날짜의 요일을 구한다.
getDisplayName(TextStyle.SHORT, Locale.KOREAN)은 요일을 한국어 짧은 이름으로 바꾼다.
예를 들어 월요일이면 월, 화요일이면 화처럼 반환될 수 있다.


Talend API Tester에서는 요청 방식은 GET으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/boards/day

예상 응답은 실행 날짜에 따라 달라진다.

// 응답 본문 예시
// 목

Talend API Tester 결과를 보면 요청 방식은 GET이고, 요청 URL은 /boards/day이다.
응답 상태는 200이고, 응답 본문에는 실행 날짜에 해당하는 한국어 요일이 출력된다.
이 테스트에서는 목이 반환되었다.


GET /boards/day는 현재 날짜를 기준으로 한국어 요일 문자열을 반환한다.


GET /boards/friends로 문자열 반환하기

// BoardController.java
@GetMapping(value = "/friends") // GET /boards/friends 요청 처리
public String getFriends() {
    log.info("getFriends 메소드가 호출되었습니다."); // 친구 목록 요청 로그 출력
    return "둘리 또치 도우너"; // 문자열 응답 반환
}

getFriends()도 게시글 데이터와 직접 관련된 메서드는 아니다.
하지만 @RestController에서 문자열을 바로 응답 본문으로 반환하는 흐름을 다시 확인할 수 있다.


요청 주소는 /boards/friends이다.
이 요청을 보내면 "둘리 또치 도우너" 문자열이 응답 본문으로 반환된다.


Talend API Tester에서는 요청 방식은 GET으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/boards/friends

예상 응답은 아래와 같다.

// 응답 본문
// 둘리 또치 도우너

Talend API Tester 결과를 보면 요청 방식은 GET이고, 요청 URL은 /boards/friends이다.
응답 상태는 200이고, 응답 본문에는 둘리 또치 도우너 문자열이 출력된다.
이 결과를 통해 @RestController에서 문자열을 반환하면 그 문자열이 응답 본문으로 바로 나간다는 점을 확인할 수 있다.


GET /boards/friends는 문자열을 응답 본문으로 바로 반환하는 간단한 GET 요청 예시이다.


게시글 REST API 요청 흐름 비교하기

이번 예제에서 중요한 점은 주소와 요청 방식의 조합이다.
/boards라는 같은 공통 경로를 사용해도, 요청 방식과 경로값이 달라지면 처리되는 기능도 달라진다.


전체 목록 조회는 GET /boards이다.
새 게시글 등록은 POST /boards이다.
게시글 한 개 조회는 GET /boards/{boardNo}이다.
게시글 삭제는 DELETE /boards/{boardNo}이다.
게시글 수정은 PUT /boards/{boardNo}이다.


정리하면 아래처럼 볼 수 있다.

  • GET /boards는 게시글 목록을 조회한다.
  • POST /boards는 게시글을 등록한다.
  • GET /boards/{boardNo}는 게시글 한 개를 조회한다.
  • DELETE /boards/{boardNo}는 게시글 한 개를 삭제한다.
  • PUT /boards/{boardNo}는 게시글 한 개를 수정한다.

REST API에서는 주소는 자원을 나타내고, 요청 방식은 그 자원에 대해 어떤 작업을 할지 나타낸다.


실행 흐름 정리

이 예제의 실행 흐름은 아래 순서로 진행된다.

  • 서버를 실행한다.
  • Talend API Tester에서 GET /boards를 요청해 기본 게시글 2개가 조회되는지 확인한다.
  • POST /boards에 JSON 본문을 담아 새 게시글을 등록한다.
  • 다시 GET /boards를 요청해 새 게시글이 목록에 추가되었는지 확인한다.
  • GET /boards/{boardNo}로 게시글 한 개를 조회한다.
  • PUT /boards/{boardNo}에 수정할 JSON 본문을 담아 게시글을 수정한다.
  • 다시 GET /boards/{boardNo}로 수정 결과를 확인한다.
  • DELETE /boards/{boardNo}로 게시글을 삭제한다.
  • 다시 GET /boards를 요청해 삭제된 게시글이 목록에서 빠졌는지 확인한다.
  • GET /boards/day와 GET /boards/friends로 문자열 응답도 확인한다.

흐름을 한 줄로 정리하면 다음과 같다.


REST API 요청 → /boards 자원 선택 → 요청 방식에 따라 조회, 등록, 수정, 삭제 실행 → ResponseEntity로 상태 코드와 응답 본문 반환


이 예제는 게시글이라는 하나의 자원을 기준으로 GET, POST, PUT, DELETE 요청이 어떻게 나뉘어 동작하는지 보여 준다.


핵심 정리

이 예제의 핵심은 앞에서 배운 요청 방식을 실제 게시글 자원에 적용하는 것이다.
BoardController는 /boards 공통 경로 아래에서 게시글 목록을 관리한다.
실제 DB 대신 boardList를 사용하지만, 요청 방식별 흐름은 실제 REST API 구조와 비슷하게 볼 수 있다.


이 흐름은 아래처럼 정리할 수 있다.

  • BoardDTO는 게시글 한 개의 데이터를 담는 객체이다.
  • boardList는 게시글 목록을 임시로 저장하는 메모리 저장소이다.
  • 생성자에서 기본 게시글 2개를 미리 넣어 둔다.
  • GET /boards는 전체 게시글 목록을 반환한다.
  • POST /boards는 요청 본문으로 받은 게시글을 목록에 추가한다.
  • GET /boards/{boardNo}는 게시글 번호로 게시글 한 개를 조회한다.
  • DELETE /boards/{boardNo}는 게시글 번호로 게시글을 삭제한다.
  • PUT /boards/{boardNo}는 게시글 번호로 게시글을 찾아 내용을 수정한다.
  • @PathVariable은 주소 경로의 게시글 번호를 받는다.
  • @RequestBody는 요청 본문의 JSON 데이터를 객체로 받는다.
  • @EqualsAndHashCode(of = "boardNo")는 게시글 번호만 기준으로 같은 게시글인지 비교하게 만든다.
  • ResponseEntity는 응답 본문과 상태 코드를 함께 반환한다.

따라서 이 예제가 알려 주는 핵심은 이것이다.
REST API에서는 게시글 같은 자원을 주소로 표현하고, 요청 방식으로 조회, 등록, 수정, 삭제 작업을 구분한다.



9. RequestBody와 ResponseBody로 요청 본문 처리하기 응용예제

이 예제는 @Controller에서 요청 본문을 받고, 응답 본문을 직접 반환하는 흐름을 확인하는 예제이다.
앞의 예제들은 대부분 @RestController를 사용했다.
@RestController는 반환값을 자동으로 응답 본문에 넣어 주기 때문에 문자열이나 객체를 바로 반환할 수 있었다.


이번 예제는 @Controller를 사용한다.
@Controller는 원래 반환값을 화면 이름으로 해석할 수 있다.
그래서 메서드의 반환값을 응답 본문으로 직접 보내려면 메서드 위에 @ResponseBody를 붙여야 한다.


또 요청 데이터를 받을 때도 방식이 나뉜다.
@RequestParam은 요청 파라미터나 form 데이터를 받을 때 사용한다.
@RequestBody는 요청 본문 자체를 읽을 때 사용한다.


이 예제의 핵심은 @Controller에서 @ResponseBody를 붙이면 반환값이 응답 본문으로 나가고, @RequestBody를 붙이면 요청 본문 데이터를 읽을 수 있다는 점이다.


예제 전체 코드

// RequestBodyController.java
package com.example.springrestedu.controller; // 클래스가 속한 패키지 경로

import com.example.springrestedu.domain.PersonVO; // 요청 데이터를 객체로 받을 때 사용할 VO
import org.springframework.stereotype.Controller; // 일반 Controller 등록 애노테이션
import org.springframework.web.bind.annotation.*; // 요청 매핑과 요청값 처리를 위한 애노테이션 모음

import java.util.Map; // JSON 데이터를 key-value 구조로 받기 위한 클래스

@Controller // 일반 Controller로 등록
@CrossOrigin(origins = "*") // 모든 출처의 요청 허용
public class RequestBodyController {
    @PostMapping("/rb1") // POST /rb1 요청 처리
    @ResponseBody // 반환값을 응답 본문으로 직접 전달
    public String test1(@RequestParam("name") String name, @RequestParam("age") int age) {
        System.out.println(">>> " + name + ":" + age); // 전달받은 값 콘솔 출력
        return "폼태그로 전달된 파라미터 : " + name + ":" + age; // 문자열 응답 반환
    }

    @PostMapping(value = "/rb2", produces = "application/json; charset=utf-8") // POST /rb2 요청 처리
    @ResponseBody // 반환값을 응답 본문으로 직접 전달
    public String test2(@RequestBody String param) {
        System.out.println(">>> " + param); // 요청 본문 전체를 콘솔 출력
        return param; // 요청 본문을 그대로 응답으로 반환
    }

    @PostMapping(value = "/rb3", produces = "application/json; charset=utf-8") // POST /rb3 요청 처리
    @ResponseBody // 반환값을 응답 본문으로 직접 전달
    public PersonVO test3(@RequestBody PersonVO vo) {
        System.out.println(">>> " + vo.getName() + ":" + vo.getAge()); // 객체에 담긴 값 출력
        return vo; // 객체를 JSON 응답으로 반환
    }

    @PostMapping(value = "/rb4", produces = "application/json; charset=utf-8") // POST /rb4 요청 처리
    @ResponseBody // 반환값을 응답 본문으로 직접 전달
    public Map test4(@RequestBody Map<String, String> map) {
        System.out.println(">>> " + map); // Map에 담긴 값 출력
        return map; // Map을 JSON 응답으로 반환
    }
}
// PersonVO.java
package com.example.springrestedu.domain; // 클래스가 속한 패키지 경로

public class PersonVO {
    private String name; // 이름 저장
    private int age; // 나이 저장

    public PersonVO() {
        System.out.println("PersonVO 객체 생성"); // 객체 생성 확인용 출력
    }

    public String getName() {
        return name; // 이름 반환
    }

    public int getAge() {
        return age; // 나이 반환
    }

    @Override
    public String toString() {
        return "Person{name='" + name + '\'' + ", age=" + age + '}'; // 객체 정보를 문자열로 반환
    }
}

이 코드는 크게 두 파일로 나누어 볼 수 있다.
첫 번째 RequestBodyController.java는 요청 데이터를 받는 여러 방식을 확인하는 Controller이다.
두 번째 PersonVO.java는 요청 본문의 JSON 데이터를 객체로 받을 때 사용하는 클래스이다.


이 예제에서 RequestBodyController는 @RestController가 아니라 @Controller를 사용한다.
그래서 각 메서드에는 @ResponseBody가 붙어 있다.
@ResponseBody가 있어야 반환값이 화면 이름이 아니라 응답 본문으로 전달된다.


@Controller에서 문자열이나 객체를 응답 본문으로 직접 보내려면 @ResponseBody가 필요하다.


이 예제에서 확인할 핵심

  • @Controller는 일반 컨트롤러를 등록할 때 사용한다.
  • @Controller는 반환값을 화면 이름으로 해석할 수 있다.
  • @ResponseBody는 반환값을 응답 본문으로 직접 보내게 만든다.
  • @RequestParam은 요청 파라미터나 form 데이터를 받을 때 사용한다.
  • @RequestBody는 요청 본문 자체를 읽을 때 사용한다.
  • @RequestBody String은 요청 본문을 문자열 그대로 받는다.
  • @RequestBody PersonVO는 요청 본문 JSON을 객체로 받는다.
  • @RequestBody Map은 요청 본문 JSON을 key-value 구조로 받는다.
  • produces = "application/json; charset=utf-8"은 응답 데이터 형식과 문자 인코딩을 지정한다.
  • 결과물은 Talend API Tester에서 요청 방식, 요청 URL, 요청 데이터, 응답 본문, 응답 상태 코드를 기준으로 확인한다.

이 예제는 요청값을 받는 위치가 요청 파라미터인지 요청 본문인지에 따라 @RequestParam과 @RequestBody를 구분해서 사용하는 예제이다.


Controller와 ResponseBody를 함께 사용하는 이유

// RequestBodyController.java
@Controller // 일반 Controller로 등록
@CrossOrigin(origins = "*") // 모든 출처의 요청 허용
public class RequestBodyController {
}

RequestBodyController에는 @Controller가 붙어 있다.
@Controller는 Spring MVC에서 요청을 처리하는 클래스를 등록할 때 사용하는 애노테이션이다.


그런데 @Controller는 기본적으로 반환값을 화면 이름으로 해석할 수 있다.
예를 들어 어떤 메서드가 "home"을 반환하면, Spring은 이 값을 응답 문자열이 아니라 home이라는 화면을 찾는 이름으로 볼 수 있다.


이번 예제는 화면을 찾는 예제가 아니다.
요청을 보내면 문자열, 객체, Map을 응답 본문으로 바로 확인하는 예제이다.
그래서 각 메서드에 @ResponseBody를 붙인다.


@ResponseBody는 메서드 반환값을 응답 본문에 직접 넣어 준다.
문자열을 반환하면 문자열이 응답 본문으로 나간다.
객체나 Map을 반환하면 JSON 형태로 변환되어 응답될 수 있다.


@RestController는 @Controller와 @ResponseBody를 합쳐 놓은 형태로 이해할 수 있다.
이번 예제는 그 구조를 직접 확인하기 위해 @Controller와 @ResponseBody를 따로 사용한다.


rb1은 RequestParam으로 form 데이터를 받는다

// RequestBodyController.java
@PostMapping("/rb1") // POST /rb1 요청 처리
@ResponseBody // 반환값을 응답 본문으로 직접 전달
public String test1(@RequestParam("name") String name, @RequestParam("age") int age) {
    System.out.println(">>> " + name + ":" + age); // 전달받은 값 콘솔 출력
    return "폼태그로 전달된 파라미터 : " + name + ":" + age; // 문자열 응답 반환
}

rb1은 POST 요청으로 들어온 name, age 값을 받는다.
여기서는 @RequestBody가 아니라 @RequestParam을 사용한다.


@RequestParam은 요청 파라미터 값을 메서드 매개변수로 받을 때 사용한다.
요청 파라미터는 주소 뒤에 붙는 값일 수도 있고, form 형식으로 전달되는 값일 수도 있다.


이 메서드는 name을 String으로 받고, age를 int로 받는다.
String은 문자열을 의미하고, int는 정수를 의미한다.
따라서 age에는 숫자로 바꿀 수 있는 값이 들어와야 한다.


Talend API Tester에서는 요청 방식은 POST로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/rb1

Body의 Form 영역에는 아래 값을 입력한다.

// form 데이터
// name=둘리
// age=10

예상 응답은 아래와 같다.

// 응답 본문
// 폼태그로 전달된 파라미터 : 둘리:10

Talend API Tester 결과를 보면 요청 방식은 POST이고, 요청 URL은 /rb1이다.
Body의 Form 영역에 name=둘리, age=10이 들어가 있다.
응답 상태는 200이고, 응답 본문에는 폼태그로 전달된 파라미터 : 둘리:10이 출력된다.


콘솔에는 아래처럼 출력된다.

// 콘솔 출력
// >>> 둘리:10

이 결과에서는 form 데이터로 전달한 name, age가 @RequestParam으로 들어오는지 확인한다.


rb1은 요청 본문 전체를 읽는 예제가 아니라, form 데이터의 개별 값을 @RequestParam으로 받는 예제이다.


rb2는 RequestBody String으로 요청 본문을 그대로 받는다

// RequestBodyController.java
@PostMapping(value = "/rb2", produces = "application/json; charset=utf-8") // POST /rb2 요청 처리
@ResponseBody // 반환값을 응답 본문으로 직접 전달
public String test2(@RequestBody String param) {
    System.out.println(">>> " + param); // 요청 본문 전체를 콘솔 출력
    return param; // 요청 본문을 그대로 응답으로 반환
}

rb2는 요청 본문 전체를 문자열로 받는다.
여기서 중요한 부분은 @RequestBody String param이다.


@RequestBody는 요청 본문에 들어 있는 데이터를 읽는다.
그리고 String param은 읽은 요청 본문을 문자열 그대로 받겠다는 뜻이다.


예를 들어 요청 본문에 JSON을 넣어 보내면, param에는 그 JSON 문자열 전체가 들어간다.
이 메서드는 받은 param을 그대로 반환한다.
그래서 응답 본문에도 요청 본문으로 보낸 내용이 다시 출력된다.


produces = "application/json; charset=utf-8"은 응답의 형식을 application/json으로 지정한다.
charset=utf-8은 한글이 깨지지 않도록 문자 인코딩을 지정하는 설정이다.


Talend API Tester에서는 요청 방식은 POST로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/rb2

요청 본문은 JSON 형식으로 작성한다.

// 요청 본문
// {
//   "name": "둘리",
//   "age": 10
// }

예상 응답은 요청 본문과 같은 형태로 나온다.

// 응답 본문
// {
//   "name": "둘리",
//   "age": 10
// }

Talend API Tester 결과를 보면 요청 방식은 POST이고, 요청 URL은 /rb2이다.
요청 본문에는 JSON 데이터가 들어가 있다.
응답 상태는 200이고, 응답 본문에는 요청으로 보낸 JSON이 그대로 반환된다.
응답의 Content-Type이 application/json이기 때문에 화면에서는 JSON처럼 정리되어 보인다.


콘솔에는 요청 본문 문자열이 그대로 출력된다.

// 콘솔 출력
// >>> {
//      "name": "둘리",
//      "age": 10
//    }

@RequestBody String은 요청 본문을 분석해서 객체로 나누는 것이 아니라, 본문 전체를 문자열 그대로 받는다.


rb3은 RequestBody PersonVO로 JSON을 객체에 담는다

// RequestBodyController.java
@PostMapping(value = "/rb3", produces = "application/json; charset=utf-8") // POST /rb3 요청 처리
@ResponseBody // 반환값을 응답 본문으로 직접 전달
public PersonVO test3(@RequestBody PersonVO vo) {
    System.out.println(">>> " + vo.getName() + ":" + vo.getAge()); // 객체에 담긴 값 출력
    return vo; // 객체를 JSON 응답으로 반환
}

rb3은 요청 본문에 들어온 JSON 데이터를 PersonVO 객체로 받는다.
rb2가 요청 본문 전체를 문자열로 받았다면, rb3은 JSON의 값을 객체 필드에 나누어 담는다.


요청 본문 JSON에 name, age가 들어 있으면 이 값들이 PersonVO의 name, age에 연결된다.
그 결과 vo.getName()으로 이름을 읽고, vo.getAge()로 나이를 읽을 수 있다.


PersonVO에는 기본 생성자가 있다.
기본 생성자는 매개변수가 없는 생성자이다.
요청 본문으로 객체를 만들 때 먼저 PersonVO 객체가 생성되고, 그 안에 요청값이 채워진다고 이해하면 된다.

// PersonVO.java
public PersonVO() {
    System.out.println("PersonVO 객체 생성"); // 객체 생성 확인용 출력
}

그래서 rb3을 실행하면 콘솔에서 PersonVO 객체 생성 출력도 확인할 수 있다.
그 다음 객체에 들어간 값이 >>> 둘리:10처럼 출력된다.


Talend API Tester에서는 요청 방식은 POST로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/rb3

요청 본문은 JSON 형식으로 작성한다.

// 요청 본문
// {
//   "name": "둘리",
//   "age": 10
// }

예상 응답은 아래와 같다.

// 응답 본문
// {
//   "name": "둘리",
//   "age": 10
// }

Talend API Tester 결과를 보면 요청 방식은 POST이고, 요청 URL은 /rb3이다.
요청 본문에는 name, age를 가진 JSON 데이터가 들어가 있다.
응답 상태는 200이고, 응답 본문에는 PersonVO 객체가 JSON 형태로 반환된다.
응답 화면에서 필드 순서는 실행 환경이나 표시 방식에 따라 다르게 보일 수 있다.


콘솔에는 아래 흐름이 출력된다.

// 콘솔 출력
// PersonVO 객체 생성
// >>> 둘리:10

@RequestBody PersonVO는 요청 본문 JSON을 의미 있는 객체로 묶어 받을 때 사용한다.


PersonVO는 요청 본문 데이터를 담는 객체이다

// PersonVO.java
public class PersonVO {
    private String name; // 이름 저장
    private int age; // 나이 저장

    public PersonVO() {
        System.out.println("PersonVO 객체 생성"); // 객체 생성 확인용 출력
    }

    public String getName() {
        return name; // 이름 반환
    }

    public int getAge() {
        return age; // 나이 반환
    }

    @Override
    public String toString() {
        return "Person{name='" + name + '\'' + ", age=" + age + '}'; // 객체 정보를 문자열로 반환
    }
}

PersonVO는 사람 한 명의 데이터를 담는 객체이다.
VO는 값 객체라고 이해하면 된다.
여기서는 요청 본문으로 들어온 name, age를 하나의 객체로 묶어 담는 역할을 한다.


name은 이름을 저장한다.
age는 나이를 저장한다.
age의 타입은 int이므로 숫자 값이 들어온다.


getName()은 저장된 이름을 읽는다.
getAge()는 저장된 나이를 읽는다.
rb3 메서드에서 이 두 메서드를 호출해 콘솔에 값을 출력한다.


toString()은 객체 정보를 문자열로 확인할 때 사용한다.
이번 rb3 응답은 객체가 JSON으로 변환되어 나가므로 toString() 결과가 직접 응답되는 구조는 아니다.
하지만 콘솔이나 디버깅에서 객체 내용을 확인할 때 도움이 된다.


PersonVO는 요청 본문 JSON의 값을 객체 형태로 다루기 위해 사용하는 데이터 클래스이다.


rb4는 RequestBody Map으로 JSON을 key-value 구조로 받는다

// RequestBodyController.java
@PostMapping(value = "/rb4", produces = "application/json; charset=utf-8") // POST /rb4 요청 처리
@ResponseBody // 반환값을 응답 본문으로 직접 전달
public Map test4(@RequestBody Map<String, String> map) {
    System.out.println(">>> " + map); // Map에 담긴 값 출력
    return map; // Map을 JSON 응답으로 반환
}

rb4는 요청 본문에 들어온 JSON 데이터를 Map으로 받는다.
Map은 이름과 값을 묶어서 저장하는 구조이다.
여기서는 String 이름과 String 값을 가지는 Map<String, String>으로 받는다.


요청 본문에 "name": "둘리"가 들어오면 map 안에는 name이라는 이름과 둘리라는 값이 들어간다.
요청 본문에 "age": "10"이 들어오면 map 안에는 age라는 이름과 "10"이라는 값이 들어간다.


rb3은 PersonVO처럼 미리 정해 둔 객체에 값을 담았다.
반면 rb4는 Map으로 받기 때문에 정해진 클래스 없이 key-value 구조로 값을 확인할 수 있다.


Talend API Tester에서는 요청 방식은 POST로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/rb4

요청 본문은 JSON 형식으로 작성한다.

// 요청 본문
// {
//   "name": "둘리",
//   "age": "10"
// }

예상 응답은 아래와 같다.

// 응답 본문
// {
//   "name": "둘리",
//   "age": "10"
// }

Talend API Tester 결과를 보면 요청 방식은 POST이고, 요청 URL은 /rb4이다.
요청 본문에는 name, age를 가진 JSON 데이터가 들어가 있다.
이때 age는 "10"처럼 문자열로 전달했다.
응답 상태는 200이고, 응답 본문에는 Map이 JSON 형태로 반환된다.
응답에서도 age 값은 "10" 문자열로 확인된다.


콘솔에는 아래처럼 Map 형태로 출력된다.

// 콘솔 출력
// >>> {name=둘리, age=10}

@RequestBody Map은 요청 본문 JSON을 특정 객체가 아니라 이름과 값의 묶음으로 받을 때 사용한다.


String, 객체, Map으로 받는 방식 비교하기

이번 예제에서 가장 헷갈리기 쉬운 부분은 rb2, rb3, rb4의 차이이다.
세 메서드는 모두 @RequestBody를 사용한다.
하지만 요청 본문을 받는 형태가 다르다.


rb2는 String으로 받는다.
그래서 요청 본문 전체가 문자열 하나로 들어온다.
JSON의 name, age를 각각 나누어 쓰기보다는 본문 자체를 그대로 확인하는 흐름이다.


rb3은 PersonVO로 받는다.
요청 본문의 name, age가 객체의 필드에 들어간다.
그래서 vo.getName(), vo.getAge()처럼 의미 있는 이름으로 값을 사용할 수 있다.


rb4는 Map으로 받는다.
요청 본문 JSON을 key-value 구조로 받는다.
정해진 객체 클래스가 없어도 여러 값을 이름과 값으로 꺼내 볼 수 있다.
이번 예제처럼 Map<String, String>으로 받으면 값이 문자열 기준으로 처리된다.


정리하면 아래처럼 볼 수 있다.

  • @RequestBody String은 요청 본문 전체를 문자열로 받는다.
  • @RequestBody PersonVO는 요청 본문 JSON을 객체로 받는다.
  • @RequestBody Map은 요청 본문 JSON을 이름과 값의 묶음으로 받는다.
  • @RequestParam은 요청 본문 전체가 아니라 요청 파라미터나 form 데이터의 개별 값을 받는다.
  • @ResponseBody는 메서드 반환값을 응답 본문으로 직접 보낸다.

같은 POST 요청이어도 요청 데이터를 어디서 읽는지, 어떤 타입으로 받는지에 따라 코드 구조가 달라진다.


실행 흐름 정리

이 예제의 실행 흐름은 아래 순서로 진행된다.

  • 서버를 실행한다.
  • Talend API Tester에서 요청 방식을 POST로 선택한다.
  • /rb1 요청에서 Body의 Form 영역에 name, age를 넣어 보낸다.
  • test1()이 @RequestParam으로 name, age를 각각 받는다.
  • /rb2 요청에서 JSON 본문을 보낸다.
  • test2()가 @RequestBody String으로 요청 본문 전체를 문자열로 받는다.
  • /rb3 요청에서 JSON 본문을 보낸다.
  • test3()이 @RequestBody PersonVO로 요청 본문을 객체에 담는다.
  • /rb4 요청에서 JSON 본문을 보낸다.
  • test4()가 @RequestBody Map으로 요청 본문을 key-value 구조로 받는다.

흐름을 한 줄로 정리하면 다음과 같다.


POST 요청 → form 데이터 또는 JSON 본문 전달 → @RequestParam 또는 @RequestBody로 값 받기 → @ResponseBody로 응답 본문 반환


요청 데이터는 form 파라미터로 받을 수도 있고, 요청 본문 전체를 String, 객체, Map으로 받을 수도 있다.


핵심 정리

이 예제의 핵심은 @Controller에서 요청 본문과 응답 본문을 직접 다루는 방식이다.
RequestBodyController는 @Controller를 사용하므로, 응답 본문을 직접 반환하려면 각 메서드에 @ResponseBody를 붙여야 한다.


이 흐름은 아래처럼 정리할 수 있다.

  • @Controller는 일반 컨트롤러를 등록한다.
  • @ResponseBody는 반환값을 응답 본문으로 직접 보낸다.
  • @RequestParam은 요청 파라미터나 form 데이터 값을 받는다.
  • @RequestBody는 요청 본문 데이터를 읽는다.
  • @RequestBody String은 요청 본문 전체를 문자열로 받는다.
  • @RequestBody PersonVO는 요청 본문 JSON을 객체로 받는다.
  • @RequestBody Map은 요청 본문 JSON을 key-value 구조로 받는다.
  • produces = "application/json; charset=utf-8"은 응답 형식과 한글 인코딩을 지정한다.
  • 이번 응용예제 결과물은 Talend API Tester에서 요청 방식, 요청 URL, 요청 데이터, 응답 본문, 응답 상태 코드를 기준으로 확인한다.

따라서 이 예제가 알려 주는 핵심은 이것이다.
@RequestBody는 요청 본문을 읽는 역할이고, @ResponseBody는 반환값을 응답 본문으로 보내는 역할이다.



10. ResponseEntity로 응답 상태, 본문, 헤더 제어하기 응용예제

이 예제는 ResponseEntity를 사용해서 서버 응답을 더 자세하게 제어하는 흐름을 확인하는 예제이다.
앞의 예제들에서도 ResponseEntity를 사용했지만, 이번 예제는 응답 본문, 응답 상태 코드, 응답 헤더를 각각 다르게 반환하는 여러 방식을 한 번에 비교한다.


ResponseEntity는 응답을 직접 만들어 반환할 때 사용하는 객체이다.
단순히 문자열이나 객체만 반환하는 것이 아니라, 응답 상태 코드와 응답 헤더까지 함께 정할 수 있다.


이번 예제에서는 /test1부터 /test8까지 요청을 확인한다.
문자열 본문 반환, 객체 본문 반환, 오류 상태 반환, 빈 본문 반환, 헤더 반환, DTO 반환, 204 No Content 반환 흐름을 순서대로 확인한다.


이 예제의 핵심은 ResponseEntity를 사용하면 응답 본문뿐 아니라 상태 코드와 헤더까지 개발자가 직접 정할 수 있다는 점이다.


예제 전체 코드

// ResponseEntityController.java
package com.example.springrestedu.controller; // 클래스가 속한 패키지 경로

import org.springframework.http.HttpHeaders; // 응답 헤더를 만들기 위한 클래스
import org.springframework.http.HttpStatus; // 응답 상태 코드를 표현하는 클래스
import org.springframework.http.ResponseEntity; // 응답 본문, 상태 코드, 헤더를 함께 다루는 클래스
import org.springframework.web.bind.annotation.GetMapping; // `GET` 요청을 메서드와 연결하는 애노테이션
import org.springframework.web.bind.annotation.RestController; // `REST` 방식 `Controller` 등록
import com.example.springrestedu.domain.Message; // 메시지 응답 객체
import com.example.springrestedu.domain.MemberDTO; // 회원 응답 객체

@RestController // 반환값을 응답 본문으로 바로 전달하는 `Controller`
public class ResponseEntityController {
    @GetMapping("/test1") // `GET /test1` 요청 처리
    public ResponseEntity<String> work1(){
        return new ResponseEntity<>("*success*", HttpStatus.OK); // 문자열 본문과 `200` 상태 반환
    }

    @GetMapping(value="/test2") // `GET /test2` 요청 처리
    public ResponseEntity<Message> work2(){
        Message message = Message.builder() // `Builder` 방식으로 `Message` 객체 생성 시작
                .msg1("둘리") // 첫 번째 메시지 값 설정
                .msg2("또치") // 두 번째 메시지 값 설정
                .msg3("도우너") // 세 번째 메시지 값 설정
                .build(); // `Message` 객체 생성
        return new ResponseEntity<>(message, HttpStatus.OK); // `Message` 객체와 `200` 상태 반환
    }

    @GetMapping(value="/test3") // `GET /test3` 요청 처리
    public ResponseEntity work3(){
        return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR); // 본문 없이 `500` 상태 반환
    }

    @GetMapping(value="/test4") // `GET /test4` 요청 처리
    public ResponseEntity work4(){
        return new ResponseEntity("오류났슈!!", HttpStatus.INTERNAL_SERVER_ERROR); // 오류 메시지와 `500` 상태 반환
    }

    @GetMapping("/test5") // `GET /test5` 요청 처리
    public ResponseEntity work5(){
        return new ResponseEntity(HttpStatus.OK); // 본문 없이 `200` 상태 반환
    }

    @GetMapping("/test6") // `GET /test6` 요청 처리
    public ResponseEntity work6(){
        HttpHeaders headers = new HttpHeaders(); // 응답 헤더 객체 생성
        headers.add("AUTHCODE","xxxxxxx"); // `AUTHCODE` 헤더 추가
        headers.add("TOKEN", "yyyyyyy"); // `TOKEN` 헤더 추가
        return ResponseEntity.ok() // `200 OK` 응답 생성
                .headers(headers) // 응답 헤더 설정
                .build(); // 본문 없이 응답 생성
    }

    @GetMapping("/test7") // `GET /test7` 요청 처리
    public ResponseEntity<MemberDTO> work7(){
        MemberDTO dto = new MemberDTO(); // `MemberDTO` 객체 생성
        dto.setName("유니코"); // 이름 설정
        dto.setEmail("unico@naver.com"); // 이메일 설정
        dto.setPhone("010-3333-4444"); // 전화번호 설정
        return ResponseEntity
                .status(HttpStatus.OK) // `200 OK` 상태 설정
                .body(dto); // 응답 본문에 `DTO` 설정
    }

    @GetMapping("/test8") // `GET /test8` 요청 처리
    public ResponseEntity<MemberDTO> work8(){
        return ResponseEntity
                .noContent() // `204 No Content` 응답 생성
                .build(); // 본문 없이 응답 반환
    }
}
// Message.java
package com.example.springrestedu.domain; // 클래스가 속한 패키지 경로

import lombok.Builder; // `Builder` 패턴 메서드 자동 생성
import lombok.Getter; // `getter` 메서드 자동 생성
import lombok.NoArgsConstructor; // 기본 생성자 자동 생성

@Getter // 필드 값을 읽는 `getter` 메서드 자동 생성
@NoArgsConstructor // 매개변수 없는 기본 생성자 자동 생성
public class Message {
    private String message1; // 첫 번째 메시지 저장
    private String message2; // 두 번째 메시지 저장
    private String message3; // 세 번째 메시지 저장

    @Builder // 이 생성자를 기준으로 `Builder` 생성
    public Message(String msg1, String msg2, String msg3) {
        this.message1 = msg1; // `msg1` 값을 `message1`에 저장
        this.message2 = msg2; // `msg2` 값을 `message2`에 저장
        this.message3 = msg3; // `msg3` 값을 `message3`에 저장
        System.out.println("@Builder 가 설정된 생성자 호출"); // `Builder` 생성자 호출 확인
    }
}

이 코드는 크게 두 파일로 나누어 볼 수 있다.
첫 번째 ResponseEntityController.java는 /test1부터 /test8까지 여러 응답 방식을 처리하는 Controller이다.
두 번째 Message.java는 /test2 요청에서 응답 본문으로 반환할 메시지 객체이다.


ResponseEntityController에는 @RestController가 붙어 있다.
그래서 메서드가 반환하는 값은 화면 이름으로 해석되지 않고 응답 본문으로 바로 전달된다.
다만 이번 예제에서는 단순 문자열이나 객체를 바로 반환하지 않고, 대부분 ResponseEntity로 감싸서 반환한다.


ResponseEntity를 사용하면 응답 본문, 응답 상태 코드, 응답 헤더를 함께 다룰 수 있다.
그래서 성공 응답, 오류 응답, 빈 응답, 헤더만 있는 응답처럼 다양한 응답 형태를 만들 수 있다.


이번 예제는 ResponseEntity로 응답을 세밀하게 제어하는 방법을 확인하는 예제이다.


이 예제에서 확인할 핵심

  • ResponseEntity는 응답 본문, 상태 코드, 헤더를 함께 다룰 수 있다.
  • HttpStatus.OK는 200 OK 상태를 의미한다.
  • HttpStatus.INTERNAL_SERVER_ERROR는 500 Internal Server Error 상태를 의미한다.
  • ResponseEntity.ok()는 200 OK 응답을 만들 때 사용한다.
  • headers()는 응답 헤더를 설정할 때 사용한다.
  • body()는 응답 본문을 설정할 때 사용한다.
  • ResponseEntity.noContent().build()는 204 No Content 응답을 만들 때 사용한다.
  • Message.builder()는 Message 객체를 Builder 방식으로 생성한다.
  • @Builder는 객체 생성 코드를 읽기 쉽게 만드는 데 도움을 준다.
  • 결과물은 Talend API Tester에서 요청 방식, 요청 URL, 응답 상태, 응답 본문, 응답 헤더를 기준으로 확인한다.

이 예제는 ResponseEntity로 성공 응답, 오류 응답, 빈 응답, 헤더 응답, 객체 응답을 각각 만들어 보는 예제이다.


ResponseEntity는 응답을 직접 구성할 때 사용한다

ResponseEntity는 서버가 클라이언트에게 보낼 응답을 직접 구성할 때 사용한다.
일반적으로 @RestController에서 문자열이나 객체를 반환하면 Spring이 자동으로 응답을 만든다.
하지만 상태 코드나 헤더를 직접 정해야 할 때는 ResponseEntity를 사용하는 것이 좋다.


예를 들어 단순히 "성공"이라는 문자열을 반환하면 보통 200 OK 상태로 응답된다.
하지만 어떤 경우에는 성공이어도 201 Created를 보내야 하고, 어떤 경우에는 본문 없이 204 No Content를 보내야 한다.
또 어떤 경우에는 응답 헤더에 토큰이나 인증 정보를 넣어야 할 수도 있다.


이런 상황에서 ResponseEntity를 사용하면 응답을 더 명확하게 만들 수 있다.


ResponseEntity는 응답 본문만 보내는 것이 아니라, 상태 코드와 헤더까지 함께 정해야 할 때 사용한다.


test1은 문자열 본문과 200 OK를 반환한다

// ResponseEntityController.java
@GetMapping("/test1") // `GET /test1` 요청 처리
public ResponseEntity<String> work1(){
    return new ResponseEntity<>("*success*", HttpStatus.OK); // 문자열 본문과 `200` 상태 반환
}

test1은 가장 기본적인 ResponseEntity 사용 예제이다.
응답 본문에는 "*success*" 문자열이 들어간다.
응답 상태는 HttpStatus.OK로 지정한다.


HttpStatus.OK는 200 OK 상태를 의미한다.
200 OK는 요청이 정상적으로 처리되었을 때 가장 기본적으로 볼 수 있는 성공 상태 코드이다.


Talend API Tester에서는 요청 방식은 GET으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/test1

예상 응답은 아래와 같다.

// 응답 상태
// 200 OK
// 응답 본문
// *success*

Talend API Tester 결과를 보면 요청 방식은 GET이고, 요청 URL은 /test1이다.
응답 상태는 200이고, 응답 본문에는 *success* 문자열이 출력된다.
이 결과를 통해 ResponseEntity가 문자열 본문과 성공 상태 코드를 함께 반환한 것을 확인할 수 있다.


new ResponseEntity<>("*success*", HttpStatus.OK)는 문자열 응답 본문과 200 OK 상태를 함께 반환한다.


test2는 Message 객체와 200 OK를 반환한다

// ResponseEntityController.java
@GetMapping(value="/test2") // `GET /test2` 요청 처리
public ResponseEntity<Message> work2(){
    Message message = Message.builder() // `Builder` 방식으로 `Message` 객체 생성 시작
            .msg1("둘리") // 첫 번째 메시지 값 설정
            .msg2("또치") // 두 번째 메시지 값 설정
            .msg3("도우너") // 세 번째 메시지 값 설정
            .build(); // `Message` 객체 생성
    return new ResponseEntity<>(message, HttpStatus.OK); // `Message` 객체와 `200` 상태 반환
}

test2는 문자열이 아니라 객체를 응답 본문으로 반환한다.
여기서는 Message 객체를 만들고, 그 객체를 ResponseEntity에 담아 반환한다.


Message.builder()는 Message 객체를 생성하는 Builder 방식이다.
Builder는 값을 하나씩 이름으로 지정하면서 객체를 만드는 방식이다.
생성자에 값만 순서대로 넣는 방식보다 어떤 값이 어디에 들어가는지 읽기 쉽다.


msg1("둘리"), msg2("또치"), msg3("도우너")로 값을 설정한다.
그리고 build()를 호출하면 실제 Message 객체가 만들어진다.


응답 본문에는 Message 객체가 들어가지만, 클라이언트가 보는 응답은 JSON 형태이다.
Message 클래스의 실제 필드 이름은 message1, message2, message3이다.
그래서 응답 JSON의 이름도 message1, message2, message3으로 나온다.


Talend API Tester에서는 요청 방식은 GET으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/test2

예상 응답은 아래와 같다.

// 응답 상태
// 200 OK
// 응답 본문
// {
//   "message1": "둘리",
//   "message2": "또치",
//   "message3": "도우너"
// }

Talend API Tester 결과를 보면 요청 방식은 GET이고, 요청 URL은 /test2이다.
응답 상태는 200이고, 응답 본문에는 message1, message2, message3을 가진 JSON 객체가 반환된다.
이 값들은 Message.builder()에서 설정한 둘리, 또치, 도우너가 실제 필드에 저장된 결과이다.


콘솔에는 아래처럼 출력된다.

// 콘솔 출력
// @Builder 가 설정된 생성자 호출

콘솔의 @Builder 가 설정된 생성자 호출은 Message.builder().build() 과정에서 @Builder가 붙은 생성자가 실행되었다는 뜻이다.
즉, Builder 방식으로 객체가 만들어진 것을 콘솔에서도 확인할 수 있다.


객체를 ResponseEntity 본문에 담아 반환하면 JSON 형태의 응답 본문으로 변환될 수 있다.


Message 클래스는 Builder로 객체를 생성한다

// Message.java
@Getter // 필드 값을 읽는 `getter` 메서드 자동 생성
@NoArgsConstructor // 매개변수 없는 기본 생성자 자동 생성
public class Message {
    private String message1; // 첫 번째 메시지 저장
    private String message2; // 두 번째 메시지 저장
    private String message3; // 세 번째 메시지 저장

    @Builder // 이 생성자를 기준으로 `Builder` 생성
    public Message(String msg1, String msg2, String msg3) {
        this.message1 = msg1; // `msg1` 값을 `message1`에 저장
        this.message2 = msg2; // `msg2` 값을 `message2`에 저장
        this.message3 = msg3; // `msg3` 값을 `message3`에 저장
        System.out.println("@Builder 가 설정된 생성자 호출"); // `Builder` 생성자 호출 확인
    }
}

Message는 응답 본문으로 보낼 데이터를 담는 객체이다.
필드는 message1, message2, message3 세 개이다.


@Getter는 필드 값을 읽는 메서드를 자동으로 만든다.
객체가 JSON으로 변환될 때도 값을 읽을 수 있어야 하므로 getter가 필요하다.


@NoArgsConstructor는 매개변수 없는 기본 생성자를 자동으로 만든다.
기본 생성자는 객체를 만들 때 필요한 기본 형태이다.


@Builder는 객체를 만들 때 Message.builder() 같은 코드를 사용할 수 있게 해 준다.
여기서 주의할 점은 Builder에서 사용하는 이름과 실제 필드 이름이 다를 수 있다는 점이다.
생성자의 매개변수 이름은 msg1, msg2, msg3이다.
그래서 객체를 만들 때는 .msg1("둘리")처럼 호출한다.
하지만 실제 필드 이름은 message1, message2, message3이다.
그래서 응답 JSON에는 message1, message2, message3으로 출력된다.


Builder에서 값을 넣는 이름과 응답 JSON에 보이는 필드 이름은 다를 수 있으므로, 실제 필드 이름을 기준으로 응답 구조를 확인해야 한다.


test3은 본문 없이 500 오류 상태만 반환한다

// ResponseEntityController.java
@GetMapping(value="/test3") // `GET /test3` 요청 처리
public ResponseEntity work3(){
    return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR); // 본문 없이 `500` 상태 반환
}

test3은 응답 본문 없이 상태 코드만 반환한다.
상태 코드는 HttpStatus.INTERNAL_SERVER_ERROR이다.


HttpStatus.INTERNAL_SERVER_ERROR는 500 Internal Server Error 상태를 의미한다.
500은 서버 내부 오류를 나타내는 상태 코드이다.


이번 예제에서는 실제 예외가 발생한 것이 아니라, 코드에서 일부러 500 상태를 만들어 반환한다.
즉, 오류 상태 코드를 직접 반환하는 흐름을 확인하는 예제이다.


Talend API Tester에서는 요청 방식은 GET으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/test3

예상 응답은 아래와 같다.

// 응답 상태
// 500 Internal Server Error
// 응답 본문
// 비어 있음

Talend API Tester 결과를 보면 요청 방식은 GET이고, 요청 URL은 /test3이다.
응답 상태는 500이고, 응답 본문 영역에는 No Content가 표시된다.
즉, 서버가 본문 없이 오류 상태 코드만 반환한 결과이다.


new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)는 본문 없이 500 Internal Server Error 상태만 반환한다.


test4는 오류 메시지와 500 상태를 함께 반환한다

// ResponseEntityController.java
@GetMapping(value="/test4") // `GET /test4` 요청 처리
public ResponseEntity work4(){
    return new ResponseEntity("오류났슈!!", HttpStatus.INTERNAL_SERVER_ERROR); // 오류 메시지와 `500` 상태 반환
}

test4도 500 Internal Server Error 상태를 반환한다.
하지만 test3과 다르게 응답 본문에 "오류났슈!!"라는 문자열을 함께 넣는다.


즉, test3은 오류 상태만 반환하고, test4는 오류 상태와 오류 메시지를 함께 반환한다.
클라이언트 입장에서는 상태 코드로 오류 여부를 확인하고, 응답 본문으로 오류 메시지를 확인할 수 있다.


Talend API Tester에서는 요청 방식은 GET으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/test4

예상 응답은 아래와 같다.

// 응답 상태
// 500 Internal Server Error
// 응답 본문
// 오류났슈!!

Talend API Tester 결과를 보면 요청 방식은 GET이고, 요청 URL은 /test4이다.
응답 상태는 500이고, 응답 본문에는 오류났슈!!라는 문자열이 출력된다.
이 결과는 같은 500 오류 상태라도 응답 본문에 메시지를 함께 담을 수 있다는 것을 보여 준다.


ResponseEntity는 같은 오류 상태라도 본문을 넣을지 비울지 직접 정할 수 있다.


test5는 본문 없이 200 OK 상태만 반환한다

// ResponseEntityController.java
@GetMapping("/test5") // `GET /test5` 요청 처리
public ResponseEntity work5(){
    return new ResponseEntity(HttpStatus.OK); // 본문 없이 `200` 상태 반환
}

test5는 응답 본문 없이 200 OK 상태만 반환한다.
요청은 정상 처리되었지만, 클라이언트에게 따로 보낼 데이터가 없는 상황을 표현할 수 있다.


test3과 구조는 비슷하다.
다만 test3은 500 오류 상태이고, test5는 200 성공 상태이다.


Talend API Tester에서는 요청 방식은 GET으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/test5

예상 응답은 아래와 같다.

// 응답 상태
// 200 OK
// 응답 본문
// 비어 있음

Talend API Tester 결과를 보면 요청 방식은 GET이고, 요청 URL은 /test5이다.
응답 상태는 200이고, 응답 본문 영역에는 No Content가 표시된다.
응답 헤더에서도 Content-Length가 0 byte로 확인된다.
즉, 요청은 성공했지만 응답 본문은 없는 결과이다.


new ResponseEntity(HttpStatus.OK)는 본문 없이 200 OK 상태만 반환한다.


test6은 응답 헤더만 추가해서 반환한다

// ResponseEntityController.java
@GetMapping("/test6") // `GET /test6` 요청 처리
public ResponseEntity work6(){
    HttpHeaders headers = new HttpHeaders(); // 응답 헤더 객체 생성
    headers.add("AUTHCODE","xxxxxxx"); // `AUTHCODE` 헤더 추가
    headers.add("TOKEN", "yyyyyyy"); // `TOKEN` 헤더 추가
    return ResponseEntity.ok() // `200 OK` 응답 생성
            .headers(headers) // 응답 헤더 설정
            .build(); // 본문 없이 응답 생성
}

test6은 응답 본문보다 응답 헤더를 확인하는 예제이다.
HttpHeaders는 응답 헤더 정보를 담는 객체이다.


headers.add("AUTHCODE","xxxxxxx")는 응답 헤더에 AUTHCODE라는 이름으로 xxxxxxx 값을 추가한다.
headers.add("TOKEN", "yyyyyyy")는 응답 헤더에 TOKEN이라는 이름으로 yyyyyyy 값을 추가한다.


그 다음 ResponseEntity.ok()로 200 OK 응답을 만들고, .headers(headers)로 위에서 만든 헤더를 넣는다.
마지막 build()는 응답 본문 없이 응답을 완성한다.


Talend API Tester에서는 요청 방식은 GET으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/test6

예상 응답은 아래와 같다.

// 응답 상태
// 200 OK
// 응답 헤더
// AUTHCODE: xxxxxxx
// TOKEN: yyyyyyy
// 응답 본문
// 비어 있음

Talend API Tester 결과를 보면 요청 방식은 GET이고, 요청 URL은 /test6이다.
응답 상태는 200이고, 응답 헤더에는 AUTHCODE: xxxxxxx, TOKEN: yyyyyyy가 들어 있다.
응답 본문 영역에는 No Content가 표시되므로, 이 요청은 본문 없이 헤더 정보를 확인하는 예제라고 볼 수 있다.


ResponseEntity.ok().headers(headers).build()는 200 OK 상태와 헤더를 반환하고, 응답 본문은 비워 둔다.


test7은 MemberDTO 객체와 200 OK를 반환한다

// ResponseEntityController.java
@GetMapping("/test7") // `GET /test7` 요청 처리
public ResponseEntity<MemberDTO> work7(){
    MemberDTO dto = new MemberDTO(); // `MemberDTO` 객체 생성
    dto.setName("유니코"); // 이름 설정
    dto.setEmail("unico@naver.com"); // 이메일 설정
    dto.setPhone("010-3333-4444"); // 전화번호 설정
    return ResponseEntity
            .status(HttpStatus.OK) // `200 OK` 상태 설정
            .body(dto); // 응답 본문에 `DTO` 설정
}

test7은 MemberDTO 객체를 응답 본문으로 반환한다.
먼저 MemberDTO 객체를 만들고, 이름, 이메일, 전화번호 값을 저장한다.


그 다음 ResponseEntity.status(HttpStatus.OK).body(dto)를 사용한다.
이 코드는 응답 상태를 200 OK로 정하고, 응답 본문에는 dto 객체를 넣겠다는 뜻이다.


@RestController에서 객체가 응답 본문으로 반환되면 JSON 형태로 변환될 수 있다.
그래서 클라이언트는 name, email, phone 값을 가진 JSON 응답을 받는다.


Talend API Tester에서는 요청 방식은 GET으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/test7

예상 응답은 아래와 같다.

// 응답 상태
// 200 OK
// 응답 본문
// {
//   "name": "유니코",
//   "email": "unico@naver.com",
//   "phone": "010-3333-4444"
// }

Talend API Tester 결과를 보면 요청 방식은 GET이고, 요청 URL은 /test7이다.
응답 상태는 200이고, 응답 본문에는 MemberDTO 객체가 JSON 형태로 반환된다.
응답 화면에서는 필드 순서가 email, name, phone처럼 보일 수 있지만, 중요한 것은 세 값이 모두 정상적으로 반환되는지 확인하는 것이다.


ResponseEntity.status(HttpStatus.OK).body(dto)는 객체 응답 본문과 200 OK 상태를 함께 반환한다.


test8은 204 No Content를 반환한다

// ResponseEntityController.java
@GetMapping("/test8") // `GET /test8` 요청 처리
public ResponseEntity<MemberDTO> work8(){
    return ResponseEntity
            .noContent() // `204 No Content` 응답 생성
            .build(); // 본문 없이 응답 반환
}

test8은 응답 본문 없이 204 No Content 상태를 반환한다.
ResponseEntity.noContent()는 204 No Content 응답을 만들 때 사용하는 간단한 방식이다.


204 No Content는 요청은 정상 처리되었지만 응답 본문은 없다는 뜻이다.
앞에서 DELETE 예제에서도 본문 없이 성공 상태를 반환할 때 이 상태를 사용했다.


여기서는 메서드의 반환 타입이 ResponseEntity<MemberDTO>이지만, 실제 응답 본문은 없다.
즉, MemberDTO 형태의 본문을 보낼 수 있는 구조이지만, noContent()를 사용했기 때문에 본문을 비운다.


Talend API Tester에서는 요청 방식은 GET으로 선택하고, 요청 주소는 아래처럼 입력한다.

// 요청 주소
// http://localhost:9000/test8

예상 응답은 아래와 같다.

// 응답 상태
// 204 No Content
// 응답 본문
// 비어 있음

Talend API Tester 결과를 보면 요청 방식은 GET이고, 요청 URL은 /test8이다.
응답 상태는 204이고, 응답 본문 영역에는 No Content가 표시된다.
즉, 요청은 정상 처리되었지만 클라이언트에게 돌려줄 본문 데이터는 없는 응답이다.


ResponseEntity.noContent().build()는 본문 없이 204 No Content 상태를 반환한다.


ResponseEntity 응답 형태 비교하기

이번 예제에서 가장 중요한 부분은 같은 ResponseEntity라도 응답 형태를 다르게 만들 수 있다는 점이다.
응답은 크게 본문, 상태 코드, 헤더로 나누어 볼 수 있다.


test1은 문자열 본문과 200 OK를 반환한다.
test2는 객체 본문과 200 OK를 반환한다.
test3은 본문 없이 500 Internal Server Error를 반환한다.
test4는 오류 메시지와 500 Internal Server Error를 반환한다.
test5는 본문 없이 200 OK를 반환한다.
test6은 본문 없이 200 OK와 응답 헤더를 반환한다.
test7은 MemberDTO 객체와 200 OK를 반환한다.
test8은 본문 없이 204 No Content를 반환한다.


정리하면 아래처럼 볼 수 있다.

  • test1은 문자열 본문과 성공 상태를 반환한다.
  • test2는 객체 본문과 성공 상태를 반환한다.
  • test3은 오류 상태만 반환한다.
  • test4는 오류 상태와 오류 메시지를 함께 반환한다.
  • test5는 성공 상태만 반환한다.
  • test6은 성공 상태와 헤더를 반환한다.
  • test7은 DTO 객체와 성공 상태를 반환한다.
  • test8은 204 No Content 상태를 반환한다.

ResponseEntity를 사용하면 같은 요청이라도 응답 본문, 상태 코드, 헤더 구성을 자유롭게 조합할 수 있다.


실행 흐름 정리

이 예제의 실행 흐름은 아래 순서로 진행된다.

  • 서버를 실행한다.
  • Talend API Tester에서 요청 방식을 GET으로 선택한다.
  • /test1 요청으로 문자열 본문과 200 OK를 확인한다.
  • /test2 요청으로 Message 객체가 JSON으로 반환되는지 확인한다.
  • /test3 요청으로 본문 없는 500 Internal Server Error를 확인한다.
  • /test4 요청으로 오류 메시지와 500 Internal Server Error를 함께 확인한다.
  • /test5 요청으로 본문 없는 200 OK를 확인한다.
  • /test6 요청으로 응답 헤더에 AUTHCODE, TOKEN이 들어가는지 확인한다.
  • /test7 요청으로 MemberDTO 객체가 JSON으로 반환되는지 확인한다.
  • /test8 요청으로 본문 없는 204 No Content를 확인한다.

흐름을 한 줄로 정리하면 다음과 같다.


GET 요청 → Controller 메서드 실행 → ResponseEntity 생성 → 본문, 상태 코드, 헤더를 포함한 응답 반환


ResponseEntity는 요청 처리 결과를 클라이언트에게 어떤 형태로 돌려줄지 직접 정하는 도구이다.


핵심 정리

이 예제의 핵심은 ResponseEntity로 응답을 직접 구성하는 방식이다.
ResponseEntityController는 여러 /test 요청을 통해 응답 본문, 상태 코드, 헤더를 각각 다르게 반환한다.


이 흐름은 아래처럼 정리할 수 있다.

  • ResponseEntity는 응답 본문, 상태 코드, 헤더를 함께 다룰 수 있다.
  • new ResponseEntity<>(body, status)는 본문과 상태 코드를 함께 반환한다.
  • new ResponseEntity(status)는 본문 없이 상태 코드만 반환한다.
  • ResponseEntity.ok()는 200 OK 응답을 만들 때 사용한다.
  • headers(headers)는 응답 헤더를 추가할 때 사용한다.
  • body(dto)는 응답 본문을 설정할 때 사용한다.
  • ResponseEntity.noContent().build()는 204 No Content 응답을 만든다.
  • 객체를 응답 본문으로 반환하면 JSON 형태로 변환될 수 있다.
  • Message.builder()는 Message 객체를 이름 기반으로 생성한다.

따라서 이 예제가 알려 주는 핵심은 이것이다.
ResponseEntity를 사용하면 서버 응답의 본문, 상태 코드, 헤더를 직접 조합해서 반환할 수 있다.

0개의 댓글