
이 예제는
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흐름을 확인한다.
이 예제는
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가 함께 있을 때 요청 흐름의 실행 순서를 비교한다.
이 예제는
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실행 전후를 나누어 처리한다.
이 흐름을 이해하면 요청이 들어와서 응답으로 돌아가기까지 공통 처리 코드가 어느 위치에서 실행되는지 구분할 수 있다.
이 예제는
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,phone이라는 값이 전달된다.// 요청 주소 // http://localhost:9000/restapi/req1?name=둘리&email=dooly@test.com&phone=010-1111-2222이 요청이 들어오면
name에는둘리,dooly@test.com,phone에는010-1111-2222가 들어간다.
메서드 매개변수 이름과 요청 파라미터 이름이 같기 때문에 자동으로 연결된다.
Talend API Tester에서는 주소에 요청 파라미터를 직접 붙여서 보내도 되고, 파라미터 입력 영역이 있다면name,phone을 각각 입력해서 보내도 된다.
결국 서버로 전달되는 요청 주소는 같은 의미가 된다.
예상 응답은 아래와 같다.// 응답 본문 // 둘리 dooly@test.com 010-1111-2222
Talend API Tester결과를 보면QUERY PARAMETERS영역에name,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,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,phone필드가 있다.
요청 파라미터 이름도name,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,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,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형태로 출력된다.
화면에서는name,phone순서로 보이지만, 중요한 것은 순서가 아니다.
name에둘리,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에는 요청으로 받은 값을 넣고,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에는 요청 파라미터로 보낸둘리가 들어간다.
phone은 요청으로 보낸 값이 아니라 코드에서 직접 넣은 고정값이다.
따라서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,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데이터를 받는 흐름을 확인한다.
이 예제는
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,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,phone을 가진JSON데이터가 들어간다.
응답 본문에는name - 둘리,email - dooly@test.com,phone - 010-1111-2222가 줄 단위로 출력된다.
Map응답은 출력 순서가 실행 환경에 따라 달라질 수 있다.
따라서 순서보다name,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필드에 들어간다.
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,phone을 가진JSON데이터가 들어간다.
응답 본문도JSON형태로 출력된다.
응답의JSON필드 순서는 화면에서 다르게 보일 수 있다.
중요한 것은name,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,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영역에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영역에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요청으로 데이터 수정 흐름을 확인한다.
이 예제는
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,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,phone을 가진JSON데이터가 들어간다.
응답 본문에는name - 둘리,email - dooly@test.com,phone - 010-1111-2222가 줄 단위로 출력된다.
Map응답은 출력 순서가 실행 환경에 따라 달라질 수 있다.
따라서 순서보다name,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필드에 들어간다.
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,phone을 가진JSON데이터가 들어간다.
응답 상태는200이고, 응답 본문은JSON형태로 출력된다.
응답의JSON필드 순서는 화면에서 다르게 보일 수 있다.
중요한 것은name,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,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,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요청으로 데이터 삭제 흐름을 확인한다.
이 예제는
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은 요청 파라미터로
요청 파라미터는 주소 뒤에?를 붙여 전달하는 값이다.
예를 들어 아래 주소를 보면// 요청 주소 // 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흐름을 확인한다.
이 예제는 앞에서 확인한
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에서는 게시글 같은 자원을 주소로 표현하고, 요청 방식으로 조회, 등록, 수정, 삭제 작업을 구분한다.
이 예제는
@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는 반환값을 응답 본문으로 보내는 역할이다.
이 예제는
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,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형태로 반환된다.
응답 화면에서는 필드 순서가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를 사용하면 서버 응답의 본문, 상태 코드, 헤더를 직접 조합해서 반환할 수 있다.