
이 파트는 어노테이션 이름만 외우는 구간이 아니다.
요청값을 어떻게 받고, 모델에 어떻게 담고, 뷰로 어떻게 넘기고, 데이터를 어떻게 응답하는지를 코드 흐름으로 이해하는 구간이다.
그래서 설명 방식도 개념 설명 → 바로 아래 기본 예제 → 마지막에 응용예제 묶음 분석 순서로 간다.
이렇게 해야 초보자가 개념을 먼저 잡고, 짧은 예제로 바로 이해한 뒤, 마지막에 실제 응용 흐름까지 연결해서 볼 수 있다.
어노테이션은 영어로
annotation이고,Java코드에 붙이는 부가 정보이다.
클래스, 메서드, 매개변수처럼 코드에서 특정 역할을 맡는 부분 위에 붙여서 이 코드가 어떤 의미를 가지는지 알려 주는 표지라고 이해하면 된다.
쉽게 말하면 코드 위에 붙이는 설명 표지판이다.
여기서 먼저 구분해야 할 점이 있다.
어노테이션이라는 문법 자체는Java문법이다.
하지만@Controller,@RequestParam,@ModelAttribute같은 어노테이션은Spring이 그 의미를 읽고 동작에 활용한다.
즉, 어노테이션은Java코드에 붙지만, 실제 처리 방식은Spring이 그 표시를 해석하면서 정해진다.
그리고 어노테이션 자체가 일을 직접 실행하는 것은 아니다.
@Controller가 붙었다고 해서@Controller가 직접 요청을 처리하는 것은 아니다.
스프링이 코드를 살펴보다가@Controller라는 표시를 발견하고, 이 클래스를 웹 요청을 처리하는 컨트롤러로 등록하고 관리하는 것이다.
즉, 어노테이션은 직접 일하는 코드가 아니라, 스프링이 어떻게 처리해야 하는지 알려 주는 표시이다.
예를 들어@Controller가 클래스 위에 붙어 있으면 스프링은 이 클래스를 컨트롤러로 인식한다.
컨트롤러는 브라우저 요청을 받아서 어떤 메서드를 실행할지 연결하고, 어떤 화면이나 데이터를 응답할지 정하는 역할을 한다.
즉,@Controller는 “이 클래스는 요청 처리 담당자이다”라고 스프링에게 알려 주는 표시이다.
또@RequestParam이 메서드의 매개변수 앞에 붙어 있으면 스프링은 요청으로 전달된 값을 그 매개변수에 넣어야 한다고 이해한다.
예를 들어 사용자가?name=lee처럼 값을 보내면, 스프링은name이라는 요청값을 찾아서 메서드 매개변수에 연결해 준다.
즉,@RequestParam은 “요청값을 이 변수에 넣어라”라고 알려 주는 표시이다.
이렇게 보면 어노테이션은 단순 장식이 아니다.
어노테이션이 있으면 스프링은 그 표시를 기준으로 객체를 등록하고, 요청 주소를 연결하고, 요청값을 매개변수에 넣고, 반환값을 화면이나 데이터로 처리한다.
즉, 스프링에서 어노테이션은 코드의 역할과 처리 방식을 알려 주는 핵심 표식이다.
다만 모든 어노테이션이 같은 일을 하는 것은 아니다.
어떤 어노테이션은 클래스를 스프링이 관리하게 만들고, 어떤 어노테이션은 요청 주소를 연결하고, 어떤 어노테이션은 요청값을 꺼내는 데 사용된다.
즉, 지금부터 보는@Controller,@RequestMapping,@RequestParam,@ModelAttribute는 모두 어노테이션이지만, 각각 맡은 역할은 다르다.
따라서 어노테이션은 이름만 외우면 안 된다.
어디에 붙는지, 스프링이 그 표시를 보고 무엇을 하는지, 요청 처리 흐름에서 어떤 역할을 맡는지를 함께 봐야 한다.
이 기준으로 보면 뒤에서 나오는 어노테이션들도 단순 암기가 아니라 요청 처리 흐름 안에서 자연스럽게 이해할 수 있다.
컨트롤러는 브라우저 요청을 실제 코드와 연결하는 역할을 한다.
사용자가 주소를 입력하거나 버튼을 눌러 요청을 보내면, 서버는 그 요청을 처리할 코드를 찾아야 한다.
Spring MVC에서는 이 요청 처리 담당 클래스를 컨트롤러라고 부른다.
여기서 먼저 구분해야 할 점이 있다.
Spring MVC도Java로 작성한다.
따라서 달라지는 것은 프로그래밍 언어가 아니다.
달라지는 것은 브라우저 요청을 어떤 구조로 받고, 어떤 방식으로 화면까지 연결하느냐이다.
Servlet/JSP방식에서는 요청을 처리하는Servlet클래스에서HttpServlet,doGet(),doPost(),RequestDispatcher,forward()를 직접 다루는 일이 많았다.
Spring MVC에서는 이 흐름을@Controller,@GetMapping,Model, 뷰 이름 반환으로 더 간단하게 표현한다.
즉,Spring MVC는Java를 버리는 것이 아니라, 기존Servlet/JSP요청 처리 방식을 더 편하게 감싼 구조라고 이해하면 된다.
그리고 이 섹션에서 가장 먼저 잡아야 할 핵심이 있다.
화면을 반환하는MVC컨트롤러라면 클래스 위에@Controller를 붙이는 것이 기본이다.
그래야 스프링이 이 클래스를 요청 처리 담당자로 인식한다.
다만 문자열이나JSON같은 데이터를 바로 응답하는 컨트롤러라면@RestController를 사용할 수 있다.
즉,@Controller는 주로 화면을 찾아 보여 주는 컨트롤러에서 사용하고,@RestController는 데이터를 바로 응답하는 컨트롤러에서 사용한다.
하지만 모든Java파일에@Controller를 붙이는 것은 아니다.
DTO,Service,Repository,Entity처럼 역할이 다른 클래스에는@Controller를 붙이지 않는다.
컨트롤러는 무엇을 하는가
컨트롤러는 모든 일을 혼자 처리하는 클래스가 아니다.
요청값을 받고, 필요한 로직을 호출하고, 결과를 화면이나 데이터 응답으로 넘기는 연결자 역할을 한다.
예를 들어 사용자가/hello라는 주소를 요청했다고 생각하면 된다.
Spring MVC에서는 요청이 먼저DispatcherServlet이라는 공통 입구로 들어온다.
DispatcherServlet은 요청 주소와 요청 방식을 보고 실행할 컨트롤러 메서드를 찾는다.
그다음 선택된 컨트롤러 메서드가 요청값을 받고, 필요한 데이터를 준비하고, 마지막에 어떤 화면이나 응답 데이터로 이어질지 결정한다.
즉, 컨트롤러는 요청 처리의 전체 흐름을 이어 주는 중간 담당자이다.
컨트롤러가 모든 요청의 첫 입구라는 뜻은 아니다.
전체 입구는DispatcherServlet이고, 컨트롤러는 그 다음 단계에서 실제 요청 처리를 맡는다.
이 개념은Servlet/JSP를 배웠다면 더 쉽게 연결할 수 있다.
Servlet에서도 브라우저 요청을 받는 클래스가 있었고, 그 안에서doGet()이나doPost()가 실행되었다.
Spring MVC의 컨트롤러도 큰 흐름에서는 요청을 받고 응답으로 연결하는 역할을 한다.
다만 코드 작성 방식이 달라진다.
Servlet에서는 요청 주소가@WebServlet으로Servlet클래스에 연결되고, 요청 방식에 따라doGet()또는doPost()가 실행된다.
Spring MVC에서는 요청 주소가@GetMapping,@PostMapping,@RequestMapping같은 매핑 어노테이션을 통해 컨트롤러 메서드와 연결된다.
정리하면 컨트롤러는 아래 흐름을 담당한다.
DispatcherServlet이 넘겨준 요청을 실제로 처리한다.- 요청값이 있으면 매개변수나 객체로 받는다.
- 필요한 로직을 호출한다.
- 화면에 보낼 데이터를 준비한다.
- 마지막에 뷰 이름이나 응답 데이터를 반환한다.
따라서 컨트롤러는 단순히 파일 하나를 만드는 것이 아니다.
브라우저 요청과 실제 처리 코드를 이어 주는 요청 처리 담당 클래스이다.
컨트롤러 파일이면 무조건 @Controller를 붙이면 되는가
Spring MVC에서 화면을 반환하는 요청 처리 클래스를 만들었다면, 보통 클래스 위에@Controller를 붙인다.
그래야 스프링이 이 클래스를 웹 요청을 처리하는 컨트롤러로 인식한다.
예를 들어 아래 코드는/hello요청을 처리하고, 마지막에hello.html화면으로 이동하는 컨트롤러 클래스이다.// exam01_controller.java @Controller // 이 클래스가 화면 반환용 컨트롤러임을 스프링에게 알림 public class HelloController { @GetMapping("/hello") // GET /hello 요청 처리 public String hello() { return "hello"; // hello.html 화면으로 이동 } }이 코드에서
HelloController는 화면을 반환하는 요청 처리 클래스이다.
그래서 클래스 위에@Controller를 붙인다.
만약 이 클래스에@Controller가 없으면 스프링이 이 클래스를 요청 처리 담당자로 인식하지 못할 수 있다.
그러면 메서드 위에@GetMapping("/hello")이 있어도, 스프링이 이 메서드를 요청과 연결할 대상으로 관리하지 못할 수 있다.
즉, 화면을 반환하는 컨트롤러 역할의 클래스라면 클래스 위에@Controller를 붙이는 것이 기본이다.
@Controller는 이 파일이 컨트롤러라는 것을 스프링에게 알려 주는 출발점이다.
하지만 모든Java파일에@Controller를 붙이면 되는 것은 아니다.
DTO는 요청값이나 데이터를 담는 객체이다.
Service는 핵심 로직을 처리하는 클래스이다.
Entity는 데이터베이스 테이블과 연결되는 객체이다.
Repository는 데이터베이스 접근을 담당하는 객체이다.
이런 클래스들은 브라우저 요청을 처리하는 컨트롤러 역할이 아니기 때문에@Controller를 붙이지 않는다.
역할별로 보면 아래처럼 구분할 수 있다.
- 화면을 반환하는 요청 처리 클래스에는
@Controller를 붙인다.- 문자열이나
JSON데이터를 바로 응답하는 요청 처리 클래스에는@RestController를 붙일 수 있다.- 핵심 로직을 처리하는 서비스 클래스에는
@Service를 붙인다.- 데이터를 담기 위한
DTO클래스에는 보통@Controller,@Service같은 스프링 계층 어노테이션을 붙이지 않는다.- 데이터베이스 테이블과 연결되는
Entity클래스에는@Entity를 붙일 수 있다.- 데이터베이스 접근 역할 클래스에는 상황에 따라
@Repository를 붙일 수 있다.따라서 “컨트롤러 파일이면 무조건
@Controller를 붙이면 되는가?”라는 질문에는 이렇게 답할 수 있다.
화면을 반환하는 요청 처리 클래스라면@Controller를 붙이는 것이 맞다.
하지만 단순히 파일 이름에Controller가 들어간다고 붙이는 것이 아니라, 실제 역할이 요청을 처리하고 화면으로 연결하는 클래스인지 확인해야 한다.
파일 이름이 Controller이면 자동으로 컨트롤러가 되는가
파일 이름에
Controller가 들어간다고 해서 자동으로 스프링 컨트롤러가 되는 것은 아니다.
예를 들어 클래스 이름을HelloController라고 지어도, 스프링이 이 클래스를 컨트롤러로 인식하려면@Controller같은 표시가 필요하다.
아래 코드는 이름은 컨트롤러처럼 보이지만,@Controller가 없다.// exam01_no_controller.java public class HelloController { @GetMapping("/hello") // GET /hello 요청 처리처럼 보임 public String hello() { return "hello"; // hello.html 화면으로 이동하려는 코드 } }이 코드는 파일 이름과 클래스 이름만 보면 컨트롤러처럼 보인다.
하지만 클래스 위에@Controller가 없다.
그래서 스프링이 이 클래스를 요청 처리 컨트롤러로 등록하지 못할 수 있다.
즉, 메서드 위에@GetMapping("/hello")이 있다고 해서 무조건 요청이 연결되는 것이 아니다.
스프링이 먼저 이 클래스를 관리 대상으로 인식해야 한다.
그 출발점이 클래스 위의@Controller이다.
반대로 아래 코드는 스프링이 요청 처리 컨트롤러로 인식할 수 있는 형태이다.// exam01_with_controller.java @Controller // 스프링이 이 클래스를 컨트롤러로 인식 public class HelloController { @GetMapping("/hello") // GET /hello 요청 처리 public String hello() { return "hello"; // hello.html 화면으로 이동 } }이 코드에서는
@Controller가 클래스 위에 붙어 있다.
그래서 스프링은 이 클래스를 컨트롤러로 관리하고, 그 안에 있는@GetMapping("/hello")도 요청 매핑 정보로 확인할 수 있다.
정리하면 아래와 같다.
- 클래스 이름이
Controller라고 해서 자동으로 컨트롤러가 되는 것은 아니다.- 스프링이 컨트롤러로 인식하려면 클래스 위에
@Controller가 필요하다.@GetMapping,@PostMapping은 컨트롤러로 등록된 클래스 안에서 요청 메서드를 찾는 기준이 된다.따라서 파일 이름보다 중요한 것은 클래스의 역할과 그 역할을 스프링에게 알려 주는 어노테이션이다.
@Controller가 붙으면 무엇이 달라지는가
@Controller는 이 클래스가 웹 요청을 처리하는 컨트롤러라는 뜻이다.
스프링은@Controller가 붙은 클래스를 그냥 지나치지 않는다.
스프링은@Controller가 붙은 클래스를 관리 대상으로 등록하고, 그 안에 있는@GetMapping,@PostMapping,@RequestMapping정보를 함께 읽어서 요청과 메서드를 연결할 준비를 한다.
여기서 스프링 컨테이너는 스프링이 객체를 만들고 보관하고 필요한 곳에 연결해 주는 저장소라고 이해하면 된다.
즉, 개발자가 컨트롤러 객체를 직접new로 만들지 않아도, 스프링이 미리 객체를 만들어 관리한다.
그래서@Controller는 단순한 이름표가 아니라 스프링이 이 클래스를 요청 처리 객체로 관리하게 만드는 표시이다.
아래 코드를 보면HelloController객체를 개발자가 직접 만들지 않는다.// exam01_controller_manage.java @Controller // 스프링이 이 클래스를 컨트롤러 객체로 관리 public class HelloController { @GetMapping("/hello") // GET /hello 요청과 hello() 연결 public String hello() { return "hello"; // hello.html 화면으로 이동 } }이 코드에는
new HelloController()가 없다.
그래도 스프링은@Controller가 붙은 클래스를 찾아 객체로 만들고 관리할 수 있다.
그리고 그 안에 있는@GetMapping,@PostMapping,@RequestMapping같은 어노테이션을 보고 어떤 주소 요청과 어떤 메서드를 연결할지 판단한다.
즉,@Controller가 클래스 단위의 출발점이고, 매핑 어노테이션은 메서드 단위의 연결 규칙이다.
이 흐름은 아래처럼 정리할 수 있다.
@Controller는 클래스를 스프링이 관리하는 컨트롤러로 등록하게 만든다.- 스프링은 컨트롤러 객체를 직접 생성하고 관리할 수 있다.
- 컨트롤러 안의
@GetMapping,@PostMapping,@RequestMapping을 읽고 요청 주소와 메서드를 연결한다.- 개발자는 컨트롤러 객체를 직접
new로 만들지 않아도 된다.따라서
@Controller는 요청 처리 클래스를 스프링이 찾고 관리하게 만드는 표시이다.
컨트롤러 파일을 만들었는데@Controller를 붙이지 않으면, 스프링 입장에서는 그 클래스가 요청 처리 담당자인지 알기 어렵다.
@Controller와 @RestController는 무엇이 다른가
@Controller와@RestController는 둘 다 요청을 처리하는 컨트롤러 클래스를 만들 때 사용한다.
하지만 반환값을 처리하는 방식이 다르다.
@Controller는 보통 화면을 반환하는 컨트롤러에서 사용한다.
예를 들어return "hello"라고 하면hello라는 문자열을 화면에 바로 출력하는 것이 아니라,hello.html같은 뷰를 찾는 흐름으로 이어진다.
반면@RestController는 데이터를 바로 응답하는 컨트롤러에서 사용한다.
예를 들어return "hello"라고 하면 뷰 이름으로 해석하지 않고, 응답 본문에hello라는 문자열을 그대로 보낸다.
아래 코드는 화면을 반환하는 컨트롤러이다.// exam01_controller_view_basic.java @Controller // 화면 반환용 컨트롤러 public class ViewController { @GetMapping("/view") // GET /view 요청 처리 public String view() { return "hello"; // hello.html 화면으로 이동 } }아래 코드는 데이터를 바로 응답하는 컨트롤러이다.
// exam01_restcontroller_basic.java @RestController // 데이터 응답용 컨트롤러 public class TextController { @GetMapping("/text") // GET /text 요청 처리 public String text() { return "hello"; // 응답 본문에 hello 문자열 출력 } }두 코드 모두 요청을 처리한다.
하지만 첫 번째 코드는 화면을 찾고, 두 번째 코드는 문자열 데이터를 바로 응답한다.
정리하면 아래와 같다.
@Controller는 보통 화면을 반환하는 컨트롤러에 사용한다.@RestController는 보통 문자열이나JSON같은 데이터를 바로 응답하는 컨트롤러에 사용한다.@Controller에서 문자열 반환은 보통 뷰 이름이다.@RestController에서 문자열 반환은 응답 본문 데이터이다.따라서 컨트롤러 클래스를 만들 때는 화면을 반환할지, 데이터를 바로 응답할지에 따라
@Controller와@RestController를 구분해야 한다.
이 섹션에서는 화면을 반환하는MVC컨트롤러를 기준으로@Controller를 설명한다.
@Controller와 @GetMapping은 역할이 다르다
@Controller와@GetMapping은 둘 다 요청 처리 흐름에서 자주 보인다.
하지만 역할은 다르다.
@Controller는 클래스 위에 붙어서 이 클래스가 요청 처리 담당 컨트롤러라는 것을 알려 준다.
반면@GetMapping은 메서드 위에 붙어서 어떤GET요청이 어떤 메서드로 들어올지 정한다.
아래 코드를 보면 역할 차이가 분명하다.// exam01_controller_mapping.java @Controller // 클래스 역할 지정 public class HelloController { @GetMapping("/hello") // 메서드와 GET /hello 요청 연결 public String hello() { return "hello"; // hello.html 화면으로 이동 } }
@Controller는HelloController클래스 전체에 붙어 있다.
그래서 스프링은 이 클래스를 요청 처리 담당 객체로 관리한다.
@GetMapping("/hello")은hello()메서드에 붙어 있다.
그래서GET /hello요청이 들어왔을 때 이 메서드가 실행된다.
따라서 둘의 역할은 아래처럼 나누어 보면 된다.
@Controller는 “이 클래스는 컨트롤러이다”라고 알려 준다.@GetMapping은 “이 메서드는 특정GET요청을 처리한다”라고 알려 준다.@Controller는 클래스 단위 표시이다.@GetMapping은 메서드 단위 요청 연결이다.즉,
@Controller와@GetMapping은 서로 대신할 수 없다.
@Controller는 클래스를 요청 처리 담당자로 등록하고,@GetMapping은 그 안의 메서드를 특정 요청 주소와 연결한다.
DispatcherServlet과 컨트롤러의 차이
가장 헷갈리기 쉬운 부분은
DispatcherServlet과 컨트롤러의 차이이다.
둘 다 요청 처리 흐름에 등장하지만 역할은 다르다.
DispatcherServlet은Spring MVC에서 웹 요청의 공통 입구 역할을 한다.
브라우저에서 들어온 요청은 먼저DispatcherServlet을 지난다.
그다음DispatcherServlet은 이 요청을 실제로 처리할 컨트롤러 메서드를 찾아서 넘긴다.
반면 컨트롤러는 그 요청을 실제로 처리하는 담당자이다.
요청값을 받고, 필요한 작업을 실행하고, 결과를 어떤 뷰나 응답 데이터로 보낼지 정한다.
즉,DispatcherServlet은 요청을 받아 알맞은 담당자를 찾는 입구이고, 컨트롤러는 그 요청을 처리하는 담당자이다.
이 흐름을/hello요청으로 보면 아래처럼 이해할 수 있다.
- 브라우저가
/hello요청을 보낸다.- 요청은 먼저
DispatcherServlet으로 들어간다.DispatcherServlet은GET /hello를 처리할 컨트롤러 메서드를 찾는다.@Controller가 붙은 클래스 안에서@GetMapping("/hello")이 붙은 메서드를 찾는다.- 찾은 컨트롤러 메서드를 실행한다.
- 컨트롤러 메서드가 반환한 결과를 기준으로 화면이나 응답 데이터가 만들어진다.
따라서 컨트롤러를 전체 입구라고 이해하면 안 된다.
전체 입구는DispatcherServlet이고, 컨트롤러는 그 다음 단계에서 요청을 실제로 처리하는 객체이다.
이 차이를Servlet/JSP와 비교하면 더 쉽게 보인다.
Servlet/JSP에서는 요청 주소가 특정Servlet에 직접 연결되는 느낌이 강하다.
Spring MVC에서는 요청이 먼저DispatcherServlet이라는 공통 입구로 들어오고, 그다음 알맞은 컨트롤러 메서드로 전달된다.
정리하면 아래와 같다.
DispatcherServlet은Spring MVC웹 요청의 공통 입구이다.- 컨트롤러는 입구를 지난 요청을 실제로 처리하는 담당자이다.
@Controller는 해당 클래스를 스프링이 컨트롤러로 인식하게 만드는 표시이다.@GetMapping,@PostMapping은 컨트롤러 안의 메서드를 요청 주소와 연결하는 표시이다.이 흐름을 알면
@Controller가 왜 필요한지도 더 분명해진다.
스프링이 컨트롤러를 알아야DispatcherServlet이 요청을 넘길 대상을 찾을 수 있기 때문이다.
컨트롤러 메서드는 무엇인가
컨트롤러 메서드는 컨트롤러 클래스 안에서 실제 요청을 처리하는 메서드이다.
클래스 전체가 요청 처리 담당자라면, 컨트롤러 메서드는 그 안에서 특정 요청 하나를 맡는 실제 실행 단위이다.
아래 코드를 보면HelloController는 컨트롤러 클래스이고,hello()는 컨트롤러 메서드이다.// exam01_controller_method.java @Controller // 요청 처리 담당 클래스 public class HelloController { @GetMapping("/hello") // GET /hello 요청과 연결 public String hello(Model model) { model.addAttribute("msg", "안녕하세요"); // 화면에 넘길 데이터 저장 return "hello"; // hello.html 화면으로 이동 } }이 코드에서
HelloController는 요청 처리 담당 클래스이다.
hello()는 실제로GET /hello요청이 들어왔을 때 실행되는 메서드이다.
즉, 컨트롤러 클래스와 컨트롤러 메서드는 같은 말이 아니다.
컨트롤러 클래스는 요청 처리 메서드들을 담고 있는 큰 단위이다.
컨트롤러 메서드는 그 안에서 특정 요청 하나를 실제로 처리하는 작은 단위이다.
정리하면 아래와 같다.
- 컨트롤러 클래스는 요청 처리 메서드를 담는 클래스이다.
- 컨트롤러 메서드는 특정 요청이 들어왔을 때 실제로 실행되는 메서드이다.
@Controller는 클래스 위에 붙는다.@GetMapping,@PostMapping,@RequestMapping은 주로 요청 처리 메서드 위에 붙는다.따라서 컨트롤러 클래스는 요청 처리 담당자이고, 컨트롤러 메서드는 그 담당자 안에서 특정 요청을 처리하는 실행 단위라고 이해하면 된다.
컨트롤러 메서드는 무엇을 받을 수 있는가
컨트롤러 안에서 실제 요청을 처리하는 메서드를 컨트롤러 메서드라고 부른다.
이 메서드는 단순히 문자열 하나만 받는 자리가 아니다.
요청 처리에 필요한 여러 값을 스프링이 매개변수로 넣어 줄 수 있다.
여기서 매개변수는 메서드가 실행될 때 바깥에서 값을 받아 오는 자리이다.
예를 들어 사용자가 보낸 이름, 나이, 요청 객체, 세션 객체 같은 것들이 컨트롤러 메서드의 매개변수로 들어올 수 있다.
아래 코드는 요청 파라미터와Model을 함께 받는 예제이다.// exam01_controller_parameter.java @Controller public class SearchController { @GetMapping("/search") // GET /search 요청 처리 public String search(@RequestParam("keyword") String keyword, Model model) { model.addAttribute("result", keyword); // 요청값을 화면에 넘길 데이터로 저장 return "searchResult"; // searchResult.html 화면으로 이동 } }이 코드에서
@RequestParam("keyword") String keyword는 요청으로 들어온keyword값을 받는 자리이다.
Model model은 화면에 넘길 데이터를 담는 자리이다.
예를 들어/search?keyword=java로 요청하면keyword매개변수에는java가 들어간다.
그리고model.addAttribute("result", keyword)를 통해 화면에서는${result}로 값을 사용할 수 있다.
컨트롤러 메서드에서 자주 받는 값은 역할별로 나누어 보면 더 쉽다.
HttpServletRequest는 요청 객체 자체를 받을 때 사용한다.Locale은 사용자 지역 정보를 받을 때 사용한다.Model은 화면에 넘길 데이터를 담을 때 매개변수로 받을 수 있다.DTO는 여러 요청값을 객체 하나로 묶어 받을 때 사용할 수 있다.HttpSession은 로그인 정보처럼 여러 요청에서 유지해야 하는 값을 다룰 때 사용할 수 있다.@RequestHeader는 요청 헤더 값을 받을 때 사용한다.@CookieValue는 요청에 담긴 쿠키 값을 받을 때 사용한다.BindingResult,Errors는 요청값을 객체에 넣는 과정에서 생긴 오류를 확인할 때 사용한다.반면
ModelAndView는 보통 컨트롤러 메서드의 매개변수로 받는 객체라기보다, 데이터와 뷰 이름을 한 객체에 담아 반환할 때 자주 사용한다.
예를 들어Model은public String hello(Model model)처럼 매개변수로 받는 경우가 많고,ModelAndView는public ModelAndView hello()처럼 반환 타입으로 쓰는 경우가 많다.
이렇게 보면 컨트롤러 메서드는 요청값만 받는 곳이 아니다.
요청 정보, 화면에 보낼 데이터, 세션 정보, 바인딩 오류 정보까지 함께 받을 수 있는 요청 처리 자리이다.
컨트롤러 메서드는 무엇을 반환하는가
컨트롤러 메서드는 요청을 처리한 뒤 결과를 반환한다.
이 반환값은 상황에 따라 의미가 다르다.
일반적인@Controller에서String을 반환하면 보통 그 문자열은 화면에 출력할 글자가 아니다.
대부분 보여 줄 뷰 이름으로 해석된다.
예를 들어return "hello"라고 작성하면 브라우저에hello라는 글자를 바로 출력하는 것이 아니라,hello.html같은 뷰를 찾는 흐름으로 이어진다.
아래 코드를 보면 이 차이가 분명하다.// exam01_return_view.java @Controller public class HelloController { @GetMapping("/hello") // GET /hello 요청 처리 public String hello(Model model) { model.addAttribute("msg", "안녕하세요"); // 화면에 보여 줄 데이터 저장 return "hello"; // hello라는 글자 출력이 아니라 hello.html 뷰 이름 } }이 코드에서
return "hello"는 화면에hello라는 문자를 출력하라는 뜻이 아니다.
스프링에게hello.html같은 뷰를 찾아서 보여 달라는 뜻이다.
ModelAndView를 반환하면 데이터와 뷰 이름을 한 객체에 담아서 반환한다.
이때는Model에 데이터를 담고String으로 뷰 이름을 따로 반환하는 대신,ModelAndView안에 데이터와 뷰 이름을 함께 담는다.
아래처럼 쓸 수 있다.// exam01_return_modelandview.java @Controller public class HelloMavController { @GetMapping("/hello-mav") // GET /hello-mav 요청 처리 public ModelAndView helloMav() { ModelAndView mav = new ModelAndView(); // 데이터와 뷰 이름을 담을 객체 생성 mav.addObject("msg", "안녕하세요"); // 화면에 보여 줄 데이터 저장 mav.setViewName("hello"); // hello.html 화면으로 이동 return mav; // 데이터와 뷰 이름을 함께 반환 } }즉,
ModelAndView는 컨트롤러 메서드가 받을 수 있는 값이라기보다, 컨트롤러 메서드가 반환할 수 있는 결과 형태로 이해하는 것이 더 자연스럽다.
또void를 반환하는 경우에는 요청 경로 이름을 기준으로 뷰 이름이 추론될 수 있다.
즉, 반환값은 단순한 결과값이 아니라 스프링이 다음 응답 흐름을 결정하는 기준이 된다.
따라서 컨트롤러 메서드를 볼 때는 세 가지를 함께 봐야 한다.
- 어떤 주소 요청과 연결되어 있는가.
- 어떤 값을 매개변수로 받는가.
- 마지막에 무엇을 반환해서 어디로 응답을 보내는가.
이 세 가지를 같이 보면 컨트롤러 메서드의 흐름이 훨씬 쉽게 보인다.
@Controller에서 문자열 반환과 @ResponseBody의 차이
@Controller에서String을 반환할 때 가장 많이 헷갈리는 부분이 있다.
일반@Controller에서return "hello"는 보통 뷰 이름이다.
하지만@ResponseBody가 붙으면 그 문자열은 응답 본문으로 바로 나간다.
먼저 일반@Controller예제를 보자.// exam01_controller_view.java @Controller public class ViewController { @GetMapping("/view") // GET /view 요청 처리 public String view() { return "hello"; // hello.html 화면으로 이동 } }이 코드는
hello.html화면을 찾는 흐름이다.
브라우저에hello라는 글자를 바로 출력하는 코드가 아니다.
반면 아래 코드는 다르다.// exam01_controller_responsebody.java @Controller public class TextController { @GetMapping("/text") // GET /text 요청 처리 @ResponseBody public String text() { return "hello"; // 응답 본문에 hello 문자열 출력 } }이 코드에서는
@ResponseBody가 붙어 있다.
그래서return "hello"는 뷰 이름이 아니라 응답 데이터이다.
브라우저에는hello라는 문자열이 그대로 출력된다.
이 차이는 아래처럼 정리할 수 있다.
- 일반
@Controller에서String반환은 보통 뷰 이름이다.@ResponseBody가 붙으면String반환은 응답 본문 데이터이다.- 화면을 보여 주려면 뷰 이름을 반환한다.
- 데이터를 바로 보내려면
@ResponseBody나@RestController를 사용한다.따라서 컨트롤러 메서드의 반환값은 어노테이션에 따라 화면 이름이 될 수도 있고, 응답 데이터가 될 수도 있다.
이 구분을 하지 않으면return "hello"가 왜 어떤 때는 화면 이동이고 어떤 때는 글자 출력인지 헷갈리게 된다.
Servlet/JSP 방식과 Spring MVC 방식 비교
아래 두 예제는 같은
/hello요청을Servlet/JSP방식과Spring MVC방식으로 비교하기 위한 코드이다.
같은 프로젝트에 두 코드를 동시에 넣어 실행하라는 뜻이 아니다.
요청 처리 흐름이 어떻게 달라지는지 비교해서 보기 위한 예제이다.
먼저Servlet/JSP방식에서는 요청을 처리하려면HttpServlet을 상속한Servlet클래스를 만들고,GET요청이면doGet()안에 코드를 작성한다.// exam01_servlet.java @WebServlet("/hello") public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setAttribute("msg", "안녕하세요"); // JSP로 넘길 데이터 저장 RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/hello.jsp"); // 이동할 JSP 지정 dispatcher.forward(request, response); // request, response를 가지고 JSP로 이동 } }// exam01_hello.jsp <%@ page contentType="text/html;charset=UTF-8" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>hello</title> </head> <body> <h1>${msg}</h1> </body> </html>// 출력결과 // 브라우저 주소: /hello // 화면 출력: 안녕하세요이 방식에서는
Servlet이 요청을 직접 받는다.
그리고request.setAttribute()로 데이터를 저장한 뒤,RequestDispatcher를 이용해서JSP로 이동한다.
즉,Servlet/JSP방식에서는 요청 처리, 데이터 저장, 화면 이동을 개발자가 직접 코드로 연결해야 한다.
이제 같은 흐름을Spring MVC방식으로 보면 코드가 달라진다.
HttpServlet을 직접 상속하지 않고,doGet()이라는 정해진 메서드 이름도 쓰지 않는다.
대신@Controller와@GetMapping으로 요청 처리 클래스를 표시하고,Model에 데이터를 담은 뒤 뷰 이름을 반환한다.// exam01_spring.java @Controller public class HelloController { @GetMapping("/hello") // /hello GET 요청을 이 메서드와 연결 public String hello(Model model) { model.addAttribute("msg", "안녕하세요"); // 뷰에 전달할 데이터 저장 return "hello"; // hello.html 뷰 이름 반환 } }// exam01_hello.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>hello</title> </head> <body> <h1 th:text="${msg}"></h1> </body> </html>// 출력결과 // 브라우저 주소: /hello // 화면 출력: 안녕하세요이 코드에서
@Controller는 이 클래스가 웹 요청을 처리하는 컨트롤러라는 뜻이다.
@GetMapping("/hello")는/hello로 들어오는GET요청을hello()메서드와 연결한다.
Model은 화면에 넘길 데이터를 담는 객체이다.
마지막의return "hello"는 화면에hello라는 글자를 출력하라는 뜻이 아니라,hello.html같은 뷰를 찾으라는 뜻이다.
즉,Spring MVC에서는Servlet에서 직접 작성하던RequestDispatcher,forward()같은 코드를 직접 쓰지 않아도 된다.
스프링이 컨트롤러의 반환값을 보고 어떤 뷰로 이동할지 처리해 준다.
두 방식의 차이 정리
Servlet/JSP방식과Spring MVC방식은 요청을 받고 화면으로 넘긴다는 큰 흐름은 같다.
하지만 코드를 작성하는 방식이 달라진다.
차이는 아래처럼 정리할 수 있다.
Servlet/JSP방식에서는 요청을 처리하는Servlet클래스가HttpServlet을 상속한다.Spring MVC에서는 화면을 반환하는 요청 처리 클래스에@Controller를 붙인다.- 데이터 자체를 바로 응답하는 요청 처리 클래스에는
@RestController를 사용할 수 있다.Servlet에서는doGet(),doPost()에 요청 처리 코드를 작성한다.Spring MVC에서는 원하는 메서드 이름을 정하고@GetMapping,@PostMapping으로 요청을 연결한다.Servlet에서는request.setAttribute()로 데이터를 담는다.Spring MVC에서는Model에 데이터를 담는다.ModelAndView를 사용하면 데이터와 뷰 이름을 한 객체에 함께 담아 반환할 수 있다.Servlet에서는RequestDispatcher와forward()로 화면 이동을 직접 처리한다.Spring MVC에서는 뷰 이름을 문자열로 반환하면 스프링이 화면을 찾아 준다.Servlet에서는 개발자가Servlet객체 흐름을 직접 다루는 느낌이 강하다.Spring MVC에서는 스프링이@Controller가 붙은 클래스를 관리하고 요청을 연결해 준다.따라서
Spring MVC는 기존 웹 처리 흐름을 없앤 것이 아니다.
기존Servlet/JSP에서 직접 하던 요청 연결, 데이터 전달, 화면 이동을 스프링이 더 간단한 구조로 바꿔 준 것이다.
이 섹션의 핵심은 이것이다.
화면을 반환하는 컨트롤러라면@Controller를 붙이는 것이 기본이고, 데이터를 바로 응답하는 컨트롤러라면@RestController를 사용할 수 있다.
컨트롤러 메서드는@GetMapping,@PostMapping,@RequestMapping으로 특정 요청과 연결되며, 요청값이나Model같은 값을 받을 수 있고, 반환값을 통해 뷰 이름이나 응답 데이터를 결정한다.
브라우저가 서버로 요청을 보내면 스프링은 그 요청을 어떤 메서드가 처리해야 하는지 찾아야 한다.
이때 요청 주소와 컨트롤러 메서드를 연결해 주는 어노테이션이@RequestMapping,@GetMapping,@PostMapping이다.
쉽게 말하면 이 어노테이션들은 요청 주소를 보고 실행할 메서드를 정해 주는 연결 규칙이다.
이 개념도Servlet/JSP를 배웠다면 기존 흐름과 비교해서 보면 더 쉽게 이해할 수 있다.
Servlet에서는 보통@WebServlet("/sample")처럼 요청 주소를Servlet클래스에 연결하고, 요청 방식에 따라doGet()이나doPost()가 실행되었다.
Spring MVC에서는@RequestMapping,@GetMapping,@PostMapping을 사용해서 요청 주소와 메서드를 더 직접적으로 연결한다.
여기서 가장 먼저 잡아야 할 핵심이 있다.
@GetMapping을 사용하기 위해@RequestMapping을 반드시 먼저 써야 하는 것은 아니다.
GET요청만 처리하고 싶으면 메서드 위에@GetMapping("/주소")만 써도 된다.
POST요청만 처리하고 싶으면 메서드 위에@PostMapping("/주소")만 써도 된다.
클래스 위의@RequestMapping은 주로 공통 주소를 묶기 위해 사용한다.
즉,@RequestMapping은 필수로 먼저 깔아야 하는 어노테이션이 아니라, 주소를 어떻게 나눠서 적을지 선택하는 방식 중 하나이다.
요청 주소 연결은 왜 필요한가
브라우저가
/sample이라는 주소로 요청을 보냈다고 해서 서버가 자동으로 어떤 메서드를 실행해야 하는지 아는 것은 아니다.
서버는 이 주소를 처리할 코드가 어디에 있는지 알아야 한다.
그래서 요청 주소와 실행할 코드를 연결하는 규칙이 필요하다.
Servlet/JSP방식에서는 이 연결을 주로@WebServlet으로 했다.
예를 들어@WebServlet("/sample")이라고 쓰면/sample요청이 해당Servlet클래스로 들어간다.
그다음 요청 방식이GET이면doGet(),POST이면doPost()가 실행된다.
Spring MVC에서는 이 연결을 컨트롤러 메서드 단위로 더 세밀하게 나눈다.
@GetMapping("/sample")은GET /sample요청을 특정 메서드에 연결한다.
@PostMapping("/sample")은POST /sample요청을 다른 메서드에 연결한다.
이 차이는 아래처럼 볼 수 있다.
Servlet에서는 주소가 먼저Servlet클래스에 연결된다.- 그 안에서 요청 방식에 따라
doGet()이나doPost()가 실행된다.Spring MVC에서는 주소와 요청 방식을 메서드에 직접 연결할 수 있다.따라서
Spring MVC의 매핑 어노테이션은 단순히 주소를 적는 문법이 아니다.
요청이 어떤 컨트롤러 메서드로 들어갈지 정하는 기준이다.
이제 여기서 중요한 점은 주소와 요청 방식을 따로 봐야 한다는 것이다.
/sample은 요청 주소이고,GET이나POST는 요청 방식이다.
스프링은 이 둘을 함께 보고 실행할 메서드를 결정한다.
@RequestMapping만 메서드 위에 쓰면 요청 방식 제한 없이 여러 방식이 들어올 수 있다
@RequestMapping은 특정 요청 주소가 어떤 클래스나 메서드로 들어올지 정하는 기본 매핑 어노테이션이다.
예를 들어/sample이라는 주소로 요청이 들어왔을 때 어떤 메서드가 처리할지 알려 주는 역할을 한다.
즉,@RequestMapping은 요청을 처리할 길을 정해 주는 길 안내판이라고 보면 된다.
먼저 메서드 위에@RequestMapping("/sample")만 쓰는 경우를 봐야 한다.
이 경우에는 주소만 연결되고, 요청 방식은 따로 제한되지 않는다.// exam02_requestmapping_only.java @Controller public class SampleController { @RequestMapping("/sample") // /sample 주소 처리, 요청 방식 제한 없음 public String sample() { return "result"; // result.html 화면으로 이동 } }// 출력결과 // GET /sample → sample() 실행 // POST /sample → sample() 실행이 코드에서
@RequestMapping("/sample")은/sample주소를sample()메서드와 연결한다.
하지만GET만 처리하겠다는 조건도 없고,POST만 처리하겠다는 조건도 없다.
그래서 대표적으로GET /sample요청도sample()메서드로 들어올 수 있다.
POST /sample요청도sample()메서드로 들어올 수 있다.
즉, 이 코드는GET과POST만 특별히 허용한다는 뜻이 아니라, 요청 방식을 따로 제한하지 않는다는 뜻이다.
즉, 메서드 위에@RequestMapping("/sample")만 쓰면 요청 방식 제한이 없어서 같은 주소의 여러 요청 방식이 같은 메서드로 들어올 수 있다.
이 방식은 주소 하나로 여러 요청 방식을 한꺼번에 받고 싶을 때는 가능하다.
하지만GET처리와POST처리를 명확하게 나누고 싶을 때는 헷갈릴 수 있다.
따라서 이 코드를 볼 때는 이렇게 이해해야 한다.
/sample주소는 연결되어 있다.- 하지만
GET전용은 아니다.POST전용도 아니다.- 요청 방식 제한이 없기 때문에 여러 방식이 같은 메서드로 들어올 수 있다.
이 부분이
@GetMapping,@PostMapping과 가장 크게 다른 지점이다.
GET만 처리하고 싶으면 @GetMapping만 써도 된다
GET요청만 처리하고 싶을 때는 메서드 위에@GetMapping("/sample")만 써도 된다.
이때@RequestMapping을 반드시 먼저 써야 하는 것은 아니다.
아래 코드는/sample주소로 들어오는GET요청만 처리한다.// exam02_getmapping_only.java @Controller public class SampleController { @GetMapping("/sample") // GET /sample 요청만 처리 public String sampleGet() { return "getResult"; // getResult.html 화면으로 이동 } }// 출력결과 // GET /sample → sampleGet() 실행 // POST /sample → sampleGet() 실행 안 됨이 코드에서
@GetMapping("/sample")은GET /sample요청을sampleGet()메서드와 연결한다.
따라서GET /sample요청이 들어오면sampleGet()이 실행된다.
하지만POST /sample요청은 이 메서드로 들어오지 않는다.
이유는 간단하다.
@GetMapping은 이름 그대로GET요청 전용 매핑이기 때문이다.
즉,GET만 처리하려고 일부러 클래스 위에@RequestMapping을 먼저 쓸 필요는 없다.
메서드 위에@GetMapping("/sample")만 써도GET /sample요청을 처리할 수 있다.
이 코드를 볼 때는 아래처럼 정리하면 된다.
@GetMapping("/sample")은 주소와 요청 방식을 한 번에 정한다.- 주소는
/sample이다.- 요청 방식은
GET이다.- 그래서
GET /sample만 이 메서드로 들어온다.따라서
@GetMapping은@RequestMapping뒤에 붙는 보조 어노테이션이 아니다.
GET요청을 처리할 수 있는 독립적인 매핑 어노테이션이다.
POST만 처리하고 싶으면 @PostMapping만 써도 된다
POST요청만 처리하고 싶을 때도 마찬가지이다.
메서드 위에@PostMapping("/sample")만 써도 된다.
이 경우에도@RequestMapping을 반드시 먼저 쓸 필요는 없다.
아래 코드는/sample주소로 들어오는POST요청만 처리한다.// exam02_postmapping_only.java @Controller public class SampleController { @PostMapping("/sample") // POST /sample 요청만 처리 public String samplePost() { return "postResult"; // postResult.html 화면으로 이동 } }// 출력결과 // GET /sample → samplePost() 실행 안 됨 // POST /sample → samplePost() 실행이 코드에서
@PostMapping("/sample")은POST /sample요청을samplePost()메서드와 연결한다.
따라서POST /sample요청이 들어오면samplePost()가 실행된다.
반대로GET /sample요청은 이 메서드로 들어오지 않는다.
@PostMapping은POST요청 전용 매핑이기 때문이다.
즉,POST만 처리하려고 일부러 클래스 위에@RequestMapping을 먼저 쓸 필요는 없다.
메서드 위에@PostMapping("/sample")만 써도POST /sample요청을 처리할 수 있다.
이 코드를 볼 때는 아래처럼 정리하면 된다.
@PostMapping("/sample")은 주소와 요청 방식을 한 번에 정한다.- 주소는
/sample이다.- 요청 방식은
POST이다.- 그래서
POST /sample만 이 메서드로 들어온다.따라서
@PostMapping도@RequestMapping뒤에 붙는 보조 어노테이션이 아니다.
POST요청을 처리할 수 있는 독립적인 매핑 어노테이션이다.
클래스 위의 @RequestMapping은 공통 주소를 묶을 때 사용한다
@RequestMapping은 메서드 위에도 붙을 수 있고, 클래스 위에도 붙을 수 있다.
그런데 클래스 위에 붙을 때는 보통 요청 방식을 제한하려는 목적보다, 여러 메서드가 함께 사용할 공통 주소를 정하려는 목적이 크다.
아래 코드는/sample이라는 공통 주소를 클래스 위에 적고, 메서드 위에서GET과POST를 나누는 예제이다.// exam02_common_requestmapping.java @Controller @RequestMapping("/sample") // 이 컨트롤러의 공통 주소 public class SampleController { @GetMapping // GET /sample 요청 처리 public String sampleGet() { return "getResult"; // getResult.html 화면으로 이동 } @PostMapping // POST /sample 요청 처리 public String samplePost() { return "postResult"; // postResult.html 화면으로 이동 } }이 코드에서 클래스 위의
@RequestMapping("/sample")은 공통 주소를 정한다.
그래서 이 컨트롤러 안의 메서드들은 기본적으로/sample주소를 기준으로 동작한다.
그다음 메서드 위의@GetMapping은GET /sample요청을 처리한다.
메서드 위의@PostMapping은POST /sample요청을 처리한다.
여기서 중요한 점은 클래스 위의@RequestMapping("/sample")이GET과POST를 처리하기 위해 반드시 필요한 것이 아니라는 점이다.
이 예제에서는 두 메서드가 같은/sample주소를 사용하기 때문에, 중복되는 주소를 클래스 위로 빼 둔 것이다.
같은 코드는 아래처럼 메서드마다 주소를 직접 적어도 된다.// exam02_same_result_without_class_requestmapping.java @Controller public class SampleController { @GetMapping("/sample") // GET /sample 요청 처리 public String sampleGet() { return "getResult"; // getResult.html 화면으로 이동 } @PostMapping("/sample") // POST /sample 요청 처리 public String samplePost() { return "postResult"; // postResult.html 화면으로 이동 } }이 코드도 위 코드와 같은 흐름으로 동작한다.
GET /sample요청은sampleGet()으로 들어가고,POST /sample요청은samplePost()로 들어간다.
따라서 두 코드는 주소를 적는 위치만 다르다.
첫 번째 코드는 공통 주소/sample을 클래스 위에 적었다.
두 번째 코드는/sample주소를 메서드마다 직접 적었다.
이 차이는 아래처럼 정리할 수 있다.
@GetMapping("/sample")은 메서드 하나가GET /sample을 직접 처리한다.@PostMapping("/sample")은 메서드 하나가POST /sample을 직접 처리한다.@RequestMapping("/sample")을 클래스 위에 쓰면/sample이라는 공통 주소를 묶을 수 있다.- 클래스 위의 공통 주소와 메서드 위의 매핑이 합쳐져 최종 요청 주소가 된다.
따라서 클래스 위의
@RequestMapping은 필수 선행 조건이 아니라, 공통 주소를 정리하기 위한 선택 방식이다.
@RequestMapping으로도 GET과 POST를 직접 제한할 수 있다
@RequestMapping은 주소만 연결할 수도 있지만, 요청 방식까지 직접 제한할 수도 있다.
이때는method옵션을 사용한다.
예를 들어GET /sample요청만 처리하고 싶으면 아래처럼 쓸 수 있다.// exam02_requestmapping_get.java @Controller public class SampleController { @RequestMapping(value = "/sample", method = RequestMethod.GET) // GET /sample 요청만 처리 public String sampleGet() { return "getResult"; // getResult.html 화면으로 이동 } }이 코드는
GET /sample요청만 처리한다.
하지만 표현이 조금 길다.
그래서 같은 의미를 더 짧게 쓰면 아래처럼 된다.// exam02_getmapping_compare.java @Controller public class SampleController { @GetMapping("/sample") // GET /sample 요청만 처리 public String sampleGet() { return "getResult"; // getResult.html 화면으로 이동 } }즉,
@GetMapping("/sample")은@RequestMapping(value = "/sample", method = RequestMethod.GET)을 짧게 쓴 형태이다.
POST요청도 마찬가지이다.// exam02_requestmapping_post.java @Controller public class SampleController { @RequestMapping(value = "/sample", method = RequestMethod.POST) // POST /sample 요청만 처리 public String samplePost() { return "postResult"; // postResult.html 화면으로 이동 } }이 코드는
POST /sample요청만 처리한다.
하지만 같은 의미를 더 짧게 쓰면 아래처럼 된다.// exam02_postmapping_compare.java @Controller public class SampleController { @PostMapping("/sample") // POST /sample 요청만 처리 public String samplePost() { return "postResult"; // postResult.html 화면으로 이동 } }즉,
@PostMapping("/sample")은@RequestMapping(value = "/sample", method = RequestMethod.POST)를 짧게 쓴 형태이다.
이제 이 관계를 정확히 정리하면 아래와 같다.
@RequestMapping("/sample")은/sample주소를 처리하지만 요청 방식 제한은 없다.@RequestMapping(value = "/sample", method = RequestMethod.GET)은GET /sample요청만 처리한다.@RequestMapping(value = "/sample", method = RequestMethod.POST)는POST /sample요청만 처리한다.@GetMapping("/sample")은GET /sample요청만 처리한다.@PostMapping("/sample")은POST /sample요청만 처리한다.따라서
@GetMapping과@PostMapping은 완전히 다른 기능이 아니라,@RequestMapping에서 요청 방식을 지정한 코드를 더 짧고 읽기 쉽게 만든 전용 어노테이션이다.
다만 초보자 입장에서는 이렇게 이해하면 된다.
GET만 처리하려면@GetMapping을 쓰고,POST만 처리하려면@PostMapping을 쓰면 된다.
@RequestMapping을 먼저 써야 하는지부터 고민할 필요는 없다.
Servlet/JSP 방식과 Spring MVC 방식 비교
아래 두 예제는 같은
/sample요청을Servlet/JSP방식과Spring MVC방식으로 비교하기 위한 코드이다.
같은 프로젝트에 두 코드를 동시에 넣어 실행하라는 뜻이 아니다.
요청 주소와 요청 방식이 어떻게 메서드로 연결되는지 비교해서 보기 위한 예제이다.
먼저Servlet/JSP방식에서는/sample주소가 하나의Servlet클래스에 연결된다.
그리고 요청 방식에 따라doGet()또는doPost()가 실행된다.// exam02_servlet.java @WebServlet("/sample") public class SampleServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setAttribute("msg", "GET 방식 요청에 대한 응답"); // GET 결과 데이터 저장 RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/result.jsp"); // 결과 JSP 지정 dispatcher.forward(request, response); // JSP로 이동 } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setAttribute("msg", "POST 방식 요청에 대한 응답"); // POST 결과 데이터 저장 RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/result.jsp"); // 결과 JSP 지정 dispatcher.forward(request, response); // JSP로 이동 } }// exam02_result.jsp <%@ page contentType="text/html;charset=UTF-8" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>result</title> </head> <body> <h1>${msg}</h1> </body> </html>// 출력결과 // 브라우저 요청: GET /sample // 실행 메서드: doGet() // 화면 출력: GET 방식 요청에 대한 응답 // 브라우저 요청: POST /sample // 실행 메서드: doPost() // 화면 출력: POST 방식 요청에 대한 응답이 방식에서는
/sample요청이SampleServlet으로 들어간다.
그리고 요청 방식이GET이면doGet()이 실행되고,POST이면doPost()가 실행된다.
즉,Servlet방식에서는 요청 주소는 클래스에 연결되고, 요청 방식은 정해진 메서드 이름으로 나뉜다.
이제 같은 흐름을Spring MVC방식으로 보면 요청 주소와 요청 방식을 컨트롤러 메서드에 직접 연결할 수 있다.// exam02_spring.java @Controller @RequestMapping("/sample") // /sample 공통 주소 지정 public class SampleController { @GetMapping // GET /sample 요청 처리 public String getForm() { return "getResult"; // GET 결과 화면으로 이동 } @PostMapping // POST /sample 요청 처리 public String submit() { return "postResult"; // POST 결과 화면으로 이동 } }// exam02_getResult.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>getResult</title> </head> <body> <h1>GET 방식 요청에 대한 응답</h1> </body> </html>// exam02_postResult.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>postResult</title> </head> <body> <h1>POST 방식 요청에 대한 응답</h1> </body> </html>// 출력결과 // 브라우저 요청: GET /sample // 실행 메서드: getForm() // 화면 출력: GET 방식 요청에 대한 응답 // 브라우저 요청: POST /sample // 실행 메서드: submit() // 화면 출력: POST 방식 요청에 대한 응답이 코드에서 클래스 위의
@RequestMapping("/sample")은 공통 주소를 정한다.
그래서 이 컨트롤러 안의 메서드들은 기본적으로/sample요청과 연결된다.
첫 번째 메서드에는@GetMapping이 붙어 있으므로GET /sample요청이 들어왔을 때 실행된다.
두 번째 메서드에는@PostMapping이 붙어 있으므로POST /sample요청이 들어왔을 때 실행된다.
여기서 클래스 위의@RequestMapping("/sample")은GET과POST를 처리하기 위해 반드시 필요한 어노테이션이 아니다.
이 예제에서는 두 메서드가 같은/sample주소를 사용하기 때문에 공통 주소를 클래스 위에 빼 둔 것이다.
같은 코드는 아래처럼 메서드마다 주소를 직접 적어도 된다.// exam02_spring_same_result.java @Controller public class SampleController { @GetMapping("/sample") // GET /sample 요청 처리 public String getForm() { return "getResult"; // GET 결과 화면으로 이동 } @PostMapping("/sample") // POST /sample 요청 처리 public String submit() { return "postResult"; // POST 결과 화면으로 이동 } }이 코드도 위 코드와 같은 흐름으로 동작한다.
GET /sample요청은getForm()으로 들어가고,POST /sample요청은submit()으로 들어간다.
즉,Spring MVC방식에서는doGet(),doPost()라는 고정된 메서드 이름을 쓰지 않아도 된다.
개발자가 메서드 이름을 정하고, 그 위에@GetMapping,@PostMapping을 붙여 요청 방식을 연결한다.
두 방식의 차이 정리
Servlet/JSP방식과Spring MVC방식은 같은 주소라도 요청 방식에 따라 다른 처리를 할 수 있다는 큰 흐름은 같다.
하지만 요청을 코드와 연결하는 방식이 달라진다.
차이는 아래처럼 정리할 수 있다.
Servlet에서는@WebServlet("/sample")로 요청 주소를Servlet클래스에 연결한다.Servlet에서는GET요청이면doGet(),POST요청이면doPost()가 실행된다.Spring MVC에서는@RequestMapping,@GetMapping,@PostMapping으로 요청 주소와 메서드를 연결한다.@RequestMapping("/sample")만 메서드 위에 쓰면 요청 방식 제한 없이/sample요청을 처리할 수 있다.@GetMapping("/sample")만 메서드 위에 쓰면GET /sample요청만 처리한다.@PostMapping("/sample")만 메서드 위에 쓰면POST /sample요청만 처리한다.- 클래스 위의
@RequestMapping("/sample")은 여러 메서드가 함께 사용할 공통 주소를 정할 때 자주 사용한다.Spring MVC에서는GET요청이면@GetMapping이 붙은 메서드,POST요청이면@PostMapping이 붙은 메서드가 실행된다.Servlet에서는RequestDispatcher와forward()로 결과 화면 이동을 직접 처리한다.Spring MVC에서는 뷰 이름을 반환하면 스프링이 결과 화면을 찾아 준다.따라서
Spring MVC의 매핑 어노테이션은 기존 요청 처리 흐름을 없앤 것이 아니다.
기존Servlet에서 주소와 요청 방식으로 처리 메서드를 나누던 흐름을 어노테이션으로 더 읽기 쉽게 표현한 것이다.
이 섹션의 핵심은 이것이다.
@RequestMapping만 메서드 위에 쓰면 요청 방식 제한이 없어서 같은 주소의 여러 요청 방식이 들어올 수 있고,@GetMapping과@PostMapping은 각각GET과POST요청만 처리한다.
클래스 위의@RequestMapping은GET이나POST를 처리하기 위해 반드시 쓰는 것이 아니라, 공통 주소를 묶기 위해 쓰는 경우가 많다.
@RequestParam은 요청으로 전달된 값 하나를 컨트롤러 메서드의 매개변수 하나에 연결해서 받는 어노테이션이다.
예를 들어 사용자가/search?query=java처럼 요청하면,query라는 요청값을 메서드 안의 변수로 받을 수 있다.
즉,@RequestParam은 요청값을 하나씩 매개변수로 받아서 쓰는 방식이라고 이해하면 된다.
이 개념도Servlet/JSP에서 배운 흐름과 비교하면 더 쉽게 이해할 수 있다.
Servlet에서는 요청값을 받을 때request.getParameter("query")처럼 요청 객체에서 직접 꺼냈다.
Spring MVC에서는@RequestParam("query") String query처럼 매개변수 앞에 어노테이션을 붙여 요청값을 바로 받을 수 있다.
즉,Spring MVC에서는Servlet에서 직접 꺼내던 요청 파라미터를 컨트롤러 메서드의 매개변수로 바로 연결할 수 있다.
값이 적을 때는 간단하고 직관적이지만, 값이 많아지면 매개변수가 길어질 수 있으므로 뒤에서 배우는 객체 바인딩 방식과 비교해서 봐야 한다.
요청 파라미터와 매개변수의 관계
요청 파라미터는 사용자가 서버에 보내는 이름과 값의 묶음이다.
예를 들어/search?query=java에서 요청 파라미터 이름은query이고, 값은java이다.
컨트롤러 메서드에서는 이 값을 매개변수로 받을 수 있다.
매개변수는 메서드가 실행될 때 바깥에서 들어오는 값을 담는 자리이다.
즉, 요청 파라미터가 브라우저에서 서버로 온 값이라면, 매개변수는 그 값을 자바 코드 안에서 쓰기 위해 받는 자리이다.
Servlet에서는 이 값을 직접 꺼내야 했다.
request.getParameter("query")라고 작성해야 요청 안에 들어 있는query값을 꺼낼 수 있었다.
반면Spring MVC에서는@RequestParam("query") String query라고 작성하면 스프링이 요청값을 찾아 매개변수에 넣어 준다.
이 흐름은 아래처럼 이해하면 된다.
- 브라우저가
/search?query=java요청을 보낸다.- 요청 안에는
query라는 이름과java라는 값이 들어 있다.Servlet에서는request.getParameter("query")로 직접 꺼낸다.Spring MVC에서는@RequestParam("query")가 붙은 매개변수로 바로 받는다.따라서
@RequestParam은Servlet의getParameter()흐름을 더 짧고 명확하게 바꾼 방식으로 볼 수 있다.
이름이 같을 때와 다를 때
요청 파라미터 이름과 메서드 매개변수 이름이 같으면 스프링이 값을 자동으로 연결할 수 있다.
예를 들어 요청이/search?query=java이고 매개변수 이름도query라면, 스프링은 같은 이름을 기준으로 값을 넣어 준다.
하지만 이름이 다르면 직접 연결 규칙을 적어야 한다.
예를 들어 요청 파라미터 이름은num인데 자바 변수 이름은number라면, 스프링이 둘을 같은 값이라고 바로 판단하기 어렵다.
이럴 때@RequestParam("num") int number처럼 작성해서 요청 이름num을 자바 변수number에 넣으라고 알려 준다.
이 차이는 아래처럼 정리할 수 있다.
- 요청 이름과 변수 이름이 같으면 자동 연결될 수 있다.
- 요청 이름과 변수 이름이 다르면
@RequestParam("요청이름")으로 직접 연결한다.- 이름이 다른데 연결 규칙을 적지 않으면 원하는 값이 들어오지 않을 수 있다.
따라서
@RequestParam을 볼 때는 요청에서 넘어오는 이름과 메서드에서 받는 변수 이름이 같은지를 먼저 확인해야 한다.
필수 여부와 기본값 처리
요청값은 항상 들어온다고 보장할 수 없다.
사용자가 값을 입력하지 않을 수도 있고, 선택 항목이라서 아예 보내지지 않을 수도 있다.
그래서@RequestParam에는 값이 없을 때 어떻게 처리할지 정하는 옵션이 있다.
required=false는 해당 요청값이 없어도 요청 처리를 계속하겠다는 뜻이다.
즉, 값이 꼭 필요하지 않은 선택 파라미터일 때 사용할 수 있다.
defaultValue는 요청값이 없거나 비어 있을 때 사용할 기본값을 정하는 옵션이다.
예를 들어@RequestParam(defaultValue="10") int number라고 하면,number값이 전달되지 않아도 기본값10을 넣어 처리할 수 있다.
둘의 차이는 아래처럼 보면 쉽다.
required=false는 값이 없어도 허용한다.defaultValue는 값이 없을 때 대신 넣을 값을 정한다.defaultValue를 사용하면 값이 없을 때도 계산이나 출력에 사용할 값이 생긴다.즉,
@RequestParam은 값을 받는 것뿐 아니라, 값이 없을 때의 처리 방식까지 정할 수 있다.
다만 숫자형 값은 값이 없을 때 더 조심해야 하므로 다음 내용을 같이 봐야 한다.
숫자형 파라미터는 왜 더 조심해야 하는가
Servlet에서request.getParameter()로 꺼낸 값은 항상 문자열로 들어온다.
그래서 숫자로 계산하려면Integer.parseInt()같은 코드로 직접 변환해야 했다.
Spring MVC에서@RequestParam int number처럼 작성하면 스프링이 요청값을 숫자로 바꿔서 넣어 줄 수 있다.
하지만 값이 없을 때는 문제가 생길 수 있다.
기본형 숫자인int는 값이 없다는 뜻의null을 담을 수 없기 때문이다.
예를 들어/querystring2처럼number값을 보내지 않았는데 컨트롤러가int number를 받으려고 하면 오류가 날 수 있다.
왜냐하면 스프링은 요청에서 값을 찾지 못했는데,int에는null을 넣을 수 없기 때문이다.
즉, 기본형 숫자는 “값이 없음”을 그대로 담을 수 없다.
이 문제를 피하려면 상황에 따라 아래 방법을 사용할 수 있다.
- 반드시 값이 있어야 한다면 요청에서 값을 빠뜨리지 않게 한다.
- 값이 없을 수도 있다면
Integer처럼null을 담을 수 있는 타입을 사용한다.- 값이 없을 때 기본값이 필요하다면
defaultValue를 사용한다.여기서
Integer는 기본형int를 객체처럼 다루는 타입이다.
int는 값이 반드시 있어야 하지만,Integer는 값이 없음을 의미하는null을 담을 수 있다.
따라서 숫자형 요청값은 값이 없을 가능성까지 생각해서 타입과 옵션을 정해야 한다.
@RequestParam을 언제 쓰면 좋은가
@RequestParam은 요청값을 하나씩 분명하게 받고 싶을 때 사용하기 좋다.
검색어, 페이지 번호, 정렬 기준처럼 값이 몇 개 되지 않을 때는@RequestParam으로 받으면 코드가 읽기 쉽다.
하지만 회원가입처럼 이름, 전화번호, 아이디, 비밀번호 등 값이 많아지면 매개변수가 너무 길어진다.
이럴 때는 값을 하나씩 받는 것보다DTO같은 객체 하나로 묶어 받는 방식이 더 자연스럽다.
즉,@RequestParam은 낱개 값 중심,@ModelAttribute는 객체 중심으로 비교해서 이해하면 된다.
Servlet/JSP 방식과 Spring MVC 방식 비교
아래 두 예제는 같은
/search?query=java요청을Servlet/JSP방식과Spring MVC방식으로 비교하기 위한 코드이다.
같은 프로젝트에 두 코드를 동시에 넣어 실행하라는 뜻이 아니다.
요청 파라미터를 어떻게 꺼내고 화면으로 넘기는지 비교해서 보기 위한 예제이다.
먼저Servlet/JSP방식에서는request.getParameter()로 요청값을 직접 꺼낸다.
그리고request.setAttribute()로JSP에 넘길 데이터를 저장한 뒤forward()로 화면을 연결한다.// exam03_servlet.java @WebServlet("/search") public class SearchServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String query = request.getParameter("query"); // query 요청값 직접 꺼내기 request.setAttribute("result", query); // JSP로 넘길 데이터 저장 RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/result.jsp"); // 결과 JSP 지정 dispatcher.forward(request, response); // JSP로 이동 } }// exam03_result.jsp <%@ page contentType="text/html;charset=UTF-8" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>result</title> </head> <body> <h1>${result}</h1> </body> </html>// 출력결과 // 브라우저 주소: /search?query=java // 화면 출력: java이 방식에서는 요청값을 직접 꺼내는 코드가 필요하다.
request.getParameter("query")는 요청 안에서query라는 이름의 값을 찾는다.
그리고 그 값을result라는 이름으로 저장한 뒤JSP에서${result}로 출력한다.
이제 같은 흐름을Spring MVC방식으로 보면 요청값을 매개변수로 바로 받을 수 있다.// exam03_spring.java @Controller public class ParamController { @GetMapping("/search") // /search 요청 처리 public String search(@RequestParam("query") String query, Model model) { model.addAttribute("result", query); // 요청값을 모델에 담음 return "result"; // 결과 화면으로 이동 } }// exam03_result.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>result</title> </head> <body> <h1 th:text="${result}"></h1> </body> </html>// 출력결과 // 브라우저 주소: /search?query=java // 화면 출력: java이 코드에서
/search?query=java로 요청하면query매개변수에는java가 들어간다.
@RequestParam("query")가 요청 파라미터 이름query와 메서드 매개변수query를 연결해 주기 때문이다.
그리고 컨트롤러는 그 값을Model에result라는 이름으로 담아 결과 화면으로 넘긴다.
result.html에서는${result}값을 출력하므로 화면에는java가 보인다.
즉, 이 흐름은 요청값 → 매개변수 → 모델 → 화면으로 이어진다.
여기서 Model은 왜 필요한가
@RequestParam으로 받은 단순 값은 컨트롤러 메서드 안에서 바로 사용할 수 있는 값이다.
예를 들어@RequestParam("query") String query라고 작성하면, 요청으로 들어온query값은query매개변수에 들어간다.
하지만 이 값이 자동으로 뷰에서 사용할 수 있는 데이터가 되는 것은 아니다.
컨트롤러에서 받은 값을 결과 화면에 보여 주려면, 그 값을 뷰에서 사용할 수 있는 저장 공간에 다시 담아야 한다.
Spring MVC에서는 이때Model을 사용한다.
Model은 컨트롤러가 뷰에 전달할 데이터를 담는 객체이다.
쉽게 말하면 컨트롤러와 화면 사이에서 데이터를 옮겨 주는 전달 상자이다.
Servlet/JSP에서는 화면에 넘길 데이터를request.setAttribute("result", query)처럼request객체에 담았다.
그리고forward()로JSP에 이동하면,JSP에서${result}로 그 값을 꺼내 쓸 수 있었다.
Spring MVC에서는 같은 역할을model.addAttribute("result", query)가 한다.
model.addAttribute()는 뷰에서 사용할 이름과 값을 함께 저장한다.
예를 들어model.addAttribute("result", query)라고 작성하면, 결과 화면에서는${result}라는 이름으로query값을 사용할 수 있다.
또 하나 중요한 점은Model에 담은 값이 무조건 오래 유지되는 값은 아니라는 점이다.
일반적인Model값은 현재 요청에서 뷰를 보여 줄 때 사용하는 값이다.
즉,Servlet/JSP의request.setAttribute()처럼 이번 요청의 결과 화면에 데이터를 넘기는 용도로 이해하면 된다.
로그인 정보처럼 여러 요청 동안 유지해야 하는 값은Model이 아니라Session같은 별도 저장 공간을 사용해야 한다.
즉,@RequestParam과Model은 역할이 다르다.
@RequestParam은 요청값을 컨트롤러 메서드로 가져오는 역할이다.
Model은 컨트롤러에서 만든 값이나 받은 값을 뷰로 넘기는 역할이다.
이 흐름은 아래처럼 정리할 수 있다.
@RequestParam은 요청값을 메서드 매개변수로 받는다.- 받은 값은 컨트롤러 메서드 안에서 사용할 수 있다.
- 그 값을 화면에 보여 주려면
Model에 다시 담아야 한다.Servlet/JSP의request.setAttribute()와Spring MVC의model.addAttribute()는 둘 다 뷰에 데이터를 넘기기 위한 역할을 한다.- 일반적인
Model값은 현재 요청의 결과 화면에서 사용하는 값이다.따라서
@RequestParam은 값을 받는 단계이고,Model은 받은 값을 화면으로 넘기는 단계라고 이해하면 된다.
두 방식의 차이 정리
Servlet/JSP방식과Spring MVC방식은 요청 파라미터를 받아서 화면에 출력한다는 큰 흐름은 같다.
하지만 요청값을 꺼내는 방식이 달라진다.
차이는 아래처럼 정리할 수 있다.
Servlet에서는request.getParameter("query")로 요청값을 직접 꺼낸다.Spring MVC에서는@RequestParam("query")로 요청값을 매개변수에 바로 연결한다.Servlet에서는 숫자값을 받으면Integer.parseInt()같은 변환을 직접 해야 한다.Spring MVC에서는 매개변수 타입을 보고 기본적인 타입 변환을 처리해 줄 수 있다.Servlet에서는request.setAttribute()로 화면에 넘길 데이터를 담는다.Spring MVC에서는Model에 화면에 넘길 데이터를 담는다.따라서
@RequestParam은 기존Servlet의getParameter()흐름을 없앤 것이 아니다.
요청값을 직접 꺼내던 코드를 어노테이션과 매개변수 연결 방식으로 더 간단하게 표현한 것이다.
이 섹션의 핵심은 이것이다.
@RequestParam은 요청값 하나를 매개변수 하나에 직접 연결하고, 이름 연결과 필수 여부, 기본값 처리까지 함께 정할 수 있는 어노테이션이다.
다만 받은 값을 화면에 보여 주려면Model에 담아 뷰로 넘겨야 한다.
@ModelAttribute는 여러 요청값을 객체 하나에 묶어서 받는 어노테이션이다.
앞에서 본@RequestParam은 요청값을 하나씩 매개변수로 받는 방식이었다.
반면@ModelAttribute는 이름, 나이, 주소처럼 서로 관련 있는 여러 값을DTO같은 객체 하나에 담아 받는다.
여기서DTO는 데이터를 담아 옮기기 위한 객체이다.
쉽게 말하면 여러 입력값을 따로따로 들고 다니지 않고, 하나의 그릇에 담아서 이동시키는 구조이다.
예를 들어 이름, 나이, 주소를 각각 따로 받는 대신UserDTO객체 하나에 묶어 받을 수 있다.
이 개념은Servlet/JSP에서 배운 흐름과 비교하면 더 쉽게 이해할 수 있다.
Servlet/JSP에서도 여러 요청값을 하나의 의미 있는 데이터로 다루고 싶을 때DTO를 만들 수 있었다.
다만 그때는 개발자가 요청 객체에서 값을 직접 꺼내고, 직접DTO객체를 만들고, 직접setter로 값을 넣어야 했다.
예를 들어request.getParameter("name")은 요청 객체 안에서name이라는 이름의 값을 개발자가 직접 찾는 코드이다.
즉, 요청값이 자동으로 객체에 들어오는 것이 아니라, 개발자가name,age,address를 하나씩 꺼내는 방식이다.
그다음 여러 값을 하나의 회원 정보로 묶고 싶다면new UserDTO()로 객체를 만들고setName(),setAge(),setAddress()를 직접 호출해야 했다.
반대로 화면에 값만 각각 보여 주면 된다면request.setAttribute("name", name),request.setAttribute("age", age)처럼 각각 담아 보낼 수도 있었다.
하지만 이 방식은 이름, 나이, 주소를 하나의 회원 정보 객체로 다루는 구조는 아니다.
Spring MVC에서는@ModelAttribute를 사용하면 이 객체 생성과 요청값 연결 과정을Spring이 처리해 준다.
즉,@ModelAttribute는 브라우저가 보낸 여러 요청값을DTO객체 하나에 바인딩해서 컨트롤러 메서드의 매개변수로 전달하는 방식이다.
DTO는 왜 필요한가
DTO는 여러 값을 하나로 묶어 담기 위해 사용한다.
입력값이 적을 때는@RequestParam으로 하나씩 받아도 크게 복잡하지 않다.
하지만 입력값이 많아지면 컨트롤러 메서드의 매개변수가 길어지고, 관련 있는 값들이 흩어져 보인다.
예를 들어 이름, 나이, 주소를 각각 받는다고 생각해 보자.// RequestParamManyExample.java @PostMapping("/user/add") // 회원 등록 요청 처리 public String addUser(@RequestParam("name") String name, @RequestParam("age") int age, @RequestParam("address") String address) { System.out.println(name); // 이름 요청값 확인 System.out.println(age); // 나이 요청값 확인 System.out.println(address); // 주소 요청값 확인 return "userComplete"; // 처리 완료 화면으로 이동 }// 출력결과 // lee // 20 // seoul이 방식은 값이 세 개 정도일 때는 이해하기 쉽다.
하지만 회원가입처럼 아이디, 비밀번호, 이름, 전화번호, 주소, 이메일처럼 값이 많아지면 매개변수가 계속 늘어난다.
매개변수가 많아지면 아래 문제가 생긴다.
- 컨트롤러 메서드가 너무 길어진다.
- 어떤 값들이 서로 관련 있는지 한눈에 보기 어렵다.
- 같은 입력값 묶음을 다른 메서드에서도 써야 할 때 반복이 많아진다.
- 값을 서비스로 넘기거나 화면으로 다시 넘길 때 하나씩 따로 넘겨야 해서 코드가 지저분해진다.
이럴 때 관련 있는 값을 하나의 객체로 묶으면 코드 흐름이 더 정리된다.
아래 코드는 바인딩용 완성 코드라기보다, 이름, 나이, 주소를 하나의 객체 구조로 묶는다는 점을 보기 위한 예시이다.// UserDTO.java public class UserDTO { private String name; // 이름 값 저장 private int age; // 나이 값 저장 private String address; // 주소 값 저장 }
UserDTO는 이름, 나이, 주소를 하나로 묶어 담는 객체이다.
컨트롤러는 이 객체 하나만 받으면 여러 요청값을 함께 다룰 수 있다.
다만 실제@ModelAttribute바인딩에서DTO를 사용할 때는 값이 들어가고, 다시 꺼내지는 구조까지 필요하다.
그래서 보통 기본 생성자,getter,setter를 함께 준비한다.
직접 작성하면 아래처럼 길어진다.// UserDTO.java public class UserDTO { private String name; // 이름 값 저장 private int age; // 나이 값 저장 private String address; // 주소 값 저장 public UserDTO() { // 기본 생성자 } public String getName() { return name; // 이름 값 반환 } public void setName(String name) { this.name = name; // 이름 값 저장 } public int getAge() { return age; // 나이 값 반환 } public void setAge(int age) { this.age = age; // 나이 값 저장 } public String getAddress() { return address; // 주소 값 반환 } public void setAddress(String address) { this.address = address; // 주소 값 저장 } }이 코드는 동작을 이해하기에는 좋지만, 매번 직접 작성하기에는 길다.
그래서Lombok을 사용하면 같은 구조를 더 짧게 작성할 수 있다.// UserDTO.java @Getter // getter 자동 생성 @Setter // setter 자동 생성 @NoArgsConstructor // 기본 생성자 자동 생성 @AllArgsConstructor // 모든 필드를 받는 생성자 자동 생성 public class UserDTO { private String name; // 이름 값 저장 private int age; // 나이 값 저장 private String address; // 주소 값 저장 }여기서 중요한 점은
Spring이getter,setter, 생성자를 만들어 주는 것이 아니라는 점이다.
Lombok이 컴파일 과정에서 해당 코드를 만들어 준다.
그리고Spring은 그렇게 만들어진 기본 생성자, 전체 필드 생성자,setter,getter를 사용할 수 있다.
정리하면 아래와 같다.
DTO는 여러 요청값을 하나의 객체로 묶기 위해 사용한다.@RequestParam이 많아질수록DTO를 쓰는 편이 코드가 정리된다.@Getter는 값을 읽는getter를 만들어 준다.@Setter는 값을 넣거나 수정하는setter를 만들어 준다.@NoArgsConstructor는 비어 있는 객체를 만들 수 있는 기본 생성자를 만들어 준다.@AllArgsConstructor는 모든 값을 한 번에 받아 객체를 만들 수 있는 생성자를 만들어 준다.따라서
DTO는 단순히 클래스를 하나 더 만드는 것이 아니다.
관련 있는 요청값을 하나의 의미 있는 데이터 묶음으로 다루기 위한 그릇이다.
HTML form에서 보낸 값이 DTO까지 들어가는 전체 흐름
@ModelAttribute를 정확히 이해하려면 실제로 작성하는 파일 기준으로 흐름을 봐야 한다.
여기서 직접 작성하는 핵심 파일은 보통 아래 세 가지이다.
userForm.html: 사용자가 값을 입력하고 서버로 전송하는 화면UserDTO.java: 요청값을 하나로 묶어 담는 객체UserController.java: 요청을 받아 처리하는 컨트롤러이 구간에서는 가장 기본적인
HTML form요청 흐름을 기준으로 본다.
실제Spring MVC에서는 이미Model에 있는 객체를 가져오거나,Session에 있는 객체를 사용하는 경우도 있다.
하지만 처음에는 이 예제처럼 사용자가 입력한form요청값이DTO객체에 들어가는 흐름부터 이해하면 된다.
먼저 입력 화면은 아래처럼 작성한다.// userForm.html <form action="/user/add" method="post"> <input type="text" name="name"> <!-- 서버로 name 값 전송 --> <input type="number" name="age"> <!-- 서버로 age 값 전송 --> <input type="text" name="address"> <!-- 서버로 address 값 전송 --> <button type="submit">등록</button> <!-- form 전송 --> </form>사용자가 화면에서 아래처럼 입력했다고 생각하면 된다.
// 입력값 예시 // name 입력칸: lee // age 입력칸: 20 // address 입력칸: seoul그러면 브라우저는 서버로 아래 값을 보낸다.
// 브라우저가 보내는 요청 // 요청 방식: POST // 요청 주소: /user/add // 요청값: name=lee&age=20&address=seoul여기서
input의name속성이 요청값 이름이 된다.
name="name"은 서버로name=lee값을 보낸다는 뜻이다.
name="age"는 서버로age=20값을 보낸다는 뜻이다.
name="address"는 서버로address=seoul값을 보낸다는 뜻이다.
그다음 요청값을 담을DTO를 만든다.// UserDTO.java @Getter // getter 자동 생성 @Setter // setter 자동 생성 @NoArgsConstructor // 기본 생성자 자동 생성 @AllArgsConstructor // 모든 필드를 받는 생성자 자동 생성 public class UserDTO { private String name; // name 요청값을 저장할 자리 private int age; // age 요청값을 저장할 자리 private String address; // address 요청값을 저장할 자리 }이
DTO에서name,age,address는 요청값이 저장될 자리이다.
즉,name=lee는name속성에 들어가고,age=20은age속성에 들어가고,address=seoul은address속성에 들어갈 수 있다.
그다음 컨트롤러는 아래처럼 작성한다.// UserController.java @Controller public class UserController { @PostMapping("/user/add") // POST /user/add 요청 처리 public String addUser(@ModelAttribute UserDTO user, Model model) { model.addAttribute("user", user); // 화면에서 사용할 객체 저장 return "userView"; // 결과 화면으로 이동 } }이제 실제 요청 흐름을 차례대로 보면 아래와 같다.
- 사용자가
userForm.html에서 값을 입력한다.- 등록 버튼을 누르면 브라우저가
POST /user/add요청을 보낸다.- 요청값으로
name=lee,age=20,address=seoul이 함께 전송된다.- 요청은 먼저
DispatcherServlet으로 들어간다.DispatcherServlet은POST /user/add를 처리할 컨트롤러 메서드를 찾는다.@PostMapping("/user/add")가 붙은addUser()메서드를 찾는다.addUser()메서드의 매개변수에@ModelAttribute UserDTO user가 있는 것을 확인한다.Spring은UserDTO객체를 준비한다.- 이때 대표적으로
setter바인딩 또는 생성자 바인딩 방식으로 객체에 값을 넣을 수 있다.- 값이 들어간
UserDTO객체가 컨트롤러 메서드의user매개변수로 들어온다.여기서 가장 중요한 부분은 “요청값 이름과
DTO속성 이름을 비교한 뒤 어떻게 저장되는가”이다.
이 저장 방식은 크게setter바인딩과 생성자 바인딩으로 나누어 볼 수 있다.
바로 다음 구간에서 이 차이를 정확히 보면 된다.
객체 바인딩은 setter 바인딩과 생성자 바인딩으로 나누어 볼 수 있다
객체 바인딩은 브라우저가 보낸 요청값을
Spring이DTO객체 안에 넣어 주는 과정이다.
여기서 값이 들어가는 방식은 크게 두 가지로 나누어 볼 수 있다.
setter 바인딩은 기본 생성자와 setter를 사용한다
setter바인딩은 기본 생성자로 빈 객체를 먼저 만들고, 그다음setter를 호출해서 값을 넣는 방식이다.
아래처럼DTO에@Getter,@Setter,@NoArgsConstructor가 있다고 생각하면 된다.// UserDTO.java @Getter // getter 자동 생성 @Setter // setter 자동 생성 @NoArgsConstructor // 기본 생성자 자동 생성 public class UserDTO { private String name; // name 요청값 저장 private int age; // age 요청값 저장 private String address; // address 요청값 저장 }이 코드는
Lombok때문에 실제로 아래 코드가 있는 것처럼 동작한다.// UserDTO.java public class UserDTO { private String name; // name 요청값 저장 private int age; // age 요청값 저장 private String address; // address 요청값 저장 public UserDTO() { // 기본 생성자 } public String getName() { return name; // name 값 읽기 } public int getAge() { return age; // age 값 읽기 } public String getAddress() { return address; // address 값 읽기 } public void setName(String name) { this.name = name; // name 값 저장 } public void setAge(int age) { this.age = age; // age 값 저장 } public void setAddress(String address) { this.address = address; // address 값 저장 } }여기서 중요한 점은
Spring이getter,setter, 생성자를 만들어 주는 것이 아니라는 점이다.
Lombok이 컴파일 과정에서 해당 코드를 만들어 준다.
그리고Spring은 그렇게 만들어진 기본 생성자와setter를 이용해 값을 넣을 수 있다.
이제 사용자가HTML입력 화면에서 아래 값을 보냈다고 생각하면 된다.// 요청값 // name=lee // age=20 // address=seoul
setter바인딩에서는 흐름이 이렇게 이어진다.
Spring이UserDTO()기본 생성자로 비어 있는 객체를 만든다.- 요청값
name=lee를 확인한다.UserDTO에setName()이 있으므로setName("lee")로 값을 넣는다.- 요청값
age=20을 확인한다.age속성 타입이int이므로 숫자로 변환할 수 있으면20으로 바꾼다.UserDTO에setAge()가 있으므로setAge(20)으로 값을 넣는다.- 요청값
address=seoul을 확인한다.UserDTO에setAddress()가 있으므로setAddress("seoul")로 값을 넣는다.이 흐름을 실제 값 기준으로 보면 아래와 같다.
// setter 바인딩 후 UserDTO 객체 상태 // user.name → "lee" // user.age → 20 // user.address → "seoul"즉,
setter바인딩에서는 기본 생성자가 빈 객체를 만들고,setter가 요청값을 객체 안에 저장한다.
이 방식에서는@NoArgsConstructor와@Setter가 중요하다.
@Getter는 저장된 값을 컨트롤러, 서비스, 화면에서 읽을 때 필요하다.
생성자 바인딩은 AllArgsConstructor 생성자를 사용할 수 있다
생성자 바인딩은 빈 객체를 먼저 만들고
setter로 값을 넣는 것이 아니라, 생성자를 호출할 때 요청값을 생성자 매개변수에 넣어서 객체를 만드는 방식이다.
아래처럼DTO에@Getter와@AllArgsConstructor만 있다고 생각하면 된다.// UserDTO.java @Getter // getter 자동 생성 @AllArgsConstructor // 모든 필드를 받는 생성자 자동 생성 public class UserDTO { private String name; // name 요청값 저장 private int age; // age 요청값 저장 private String address; // address 요청값 저장 }이 코드는
Lombok때문에 실제로 아래 코드가 있는 것처럼 동작한다.// UserDTO.java public class UserDTO { private String name; // name 요청값 저장 private int age; // age 요청값 저장 private String address; // address 요청값 저장 public UserDTO(String name, int age, String address) { this.name = name; // 생성자에서 name 값 저장 this.age = age; // 생성자에서 age 값 저장 this.address = address; // 생성자에서 address 값 저장 } public String getName() { return name; // name 값 읽기 } public int getAge() { return age; // age 값 읽기 } public String getAddress() { return address; // address 값 읽기 } }이 구조에는
setName(),setAge(),setAddress()가 없다.
따라서setter로 값을 넣는 방식이 아니다.
대신@AllArgsConstructor가 만들어 준 생성자를 통해 값을 넣을 수 있는 구조가 된다.
사용자가 아래 값을 보냈다고 생각하면 된다.// 요청값 // name=lee // age=20 // address=seoul생성자 바인딩에서는 흐름이 이렇게 이어진다.
Spring이 생성자 매개변수name,age,address를 확인한다.- 요청값
name=lee를 생성자 매개변수name에 연결한다.- 요청값
age=20을 생성자 매개변수age에 연결한다.age타입이int이므로 숫자로 변환할 수 있으면20으로 바꾼다.- 요청값
address=seoul을 생성자 매개변수address에 연결한다.- 이 예제 구조에서는
new UserDTO("lee", 20, "seoul")처럼 생성자를 호출해 객체를 만들 수 있다.이 흐름을 실제 값 기준으로 보면 아래와 같다.
// 생성자 바인딩 후 UserDTO 객체 상태 // user.name → "lee" // user.age → 20 // user.address → "seoul"즉,
setter가 없는데 값이 들어갔다면, 이 예제 구조에서는@AllArgsConstructor가 만들어 준 생성자를 통해 생성자 바인딩이 가능했기 때문이라고 이해하면 된다.
다만 모든 상황에서@AllArgsConstructor만 붙이면 무조건 생성자 바인딩이 된다고 단정하면 안 된다.
Spring이 생성자 매개변수 이름과 요청값 이름을 맞출 수 있어야 한다.
setter 바인딩과 생성자 바인딩의 차이
두 방식은 모두 요청값을
DTO객체에 넣는 방식이다.
다만 값을 넣는 시점과 필요한 코드가 다르다.
차이는 아래처럼 정리할 수 있다.
setter바인딩은 기본 생성자로 빈 객체를 만든 뒤setter로 값을 넣는다.- 생성자 바인딩은 생성자를 호출하는 순간 매개변수로 값을 넣어 객체를 만든다.
@Setter가 있으면setter바인딩이 가능하다.@AllArgsConstructor가 있으면 생성자 바인딩이 가능한 구조가 될 수 있다.@Getter는 저장된 값을 읽기 위해 함께 두는 경우가 많다.- 저장 방식 자체는
setter바인딩 또는 생성자 바인딩으로 이해해야 한다.따라서 초보자 단계에서는
@ModelAttribute용DTO를@Getter,@Setter,@NoArgsConstructor중심으로 이해하는 것이 가장 안전하다.
그 위에서setter없이도 값이 들어가는 경우는@AllArgsConstructor가 만들어 준 생성자를 통한 생성자 바인딩으로 따로 이해하면 된다.
input name과 DTO 속성 이름이 맞아야 바인딩된다
객체 바인딩에서 가장 중요한 기준은
input의name속성과DTO의 속성 이름이다.
Spring이 아무 값이나 아무 필드에 넣는 것이 아니다.
요청값 이름과 객체 속성 이름을 기준으로 어느 값이 어느 자리에 들어갈지 판단한다.
입력 화면에서 요청값 이름은 아래처럼 정해진다.// userForm.html <form action="/user/add" method="post"> <input type="text" name="name"> <!-- 요청값 이름: name --> <input type="number" name="age"> <!-- 요청값 이름: age --> <input type="text" name="address"> <!-- 요청값 이름: address --> <button type="submit">등록</button> </form>
DTO에는 같은 이름의 속성이 있어야 자연스럽게 연결된다.// UserDTO.java @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class UserDTO { private String name; // 요청값 name과 연결 private int age; // 요청값 age와 연결 private String address; // 요청값 address와 연결 }이 경우 값은 아래처럼 연결된다.
// 요청값 // name=lee // age=20 // address=seoul// 바인딩 후 UserDTO 객체 상태 // user.name → "lee" // user.age → 20 // user.address → "seoul"반대로
input name과DTO속성 이름이 다르면 값이 제대로 들어가지 않을 수 있다.// wrongUserForm.html <form action="/user/add" method="post"> <input type="text" name="userName"> <!-- DTO에는 userName 속성이 없음 --> <input type="number" name="userAge"> <!-- DTO에는 userAge 속성이 없음 --> <input type="text" name="userAddress"> <!-- DTO에는 userAddress 속성이 없음 --> <button type="submit">등록</button> </form>이 경우 요청값 이름은
userName,userAge,userAddress이다.
하지만UserDTO의 속성 이름은name,age,address이다.
이름이 다르므로Spring이 어떤 요청값을 어떤 속성에 넣어야 하는지 바로 연결하기 어렵다.
정리하면 아래와 같다.
input name="name"은UserDTO의name속성과 연결된다.input name="age"는UserDTO의age속성과 연결된다.input name="address"는UserDTO의address속성과 연결된다.input name과DTO속성 이름이 다르면 객체 바인딩이 제대로 되지 않을 수 있다.- 숫자 필드는 요청값이 숫자로 변환 가능한 형태여야 한다.
따라서 객체 바인딩은
input name과DTO속성 이름을 기준으로 요청값을DTO객체에 넣는 과정이다.
DTO 값을 사용하는 방법은 여러 가지가 있다
@ModelAttribute로 받은DTO객체는 컨트롤러 안에서도 사용할 수 있고, 화면으로 넘길 수도 있고, 서비스로 전달할 수도 있다.
즉,DTO는 단순히 화면 출력용 객체가 아니다.
요청값이 묶여 있는 객체이기 때문에 필요한 위치에서 꺼내 쓰거나 넘길 수 있다.
컨트롤러에서 getter로 값을 꺼내 사용할 수 있다
첫 번째 방법은 컨트롤러에서
getter로 값을 꺼내는 것이다.// UserControllerGetterExample.java @Controller public class UserController { @PostMapping("/user/add") // 회원 등록 요청 처리 public String addUser(@ModelAttribute UserDTO user, Model model) { String name = user.getName(); // DTO 안의 name 값 꺼내기 int age = user.getAge(); // DTO 안의 age 값 꺼내기 String address = user.getAddress(); // DTO 안의 address 값 꺼내기 System.out.println(name); // 이름 값 출력 System.out.println(age); // 나이 값 출력 System.out.println(address); // 주소 값 출력 model.addAttribute("user", user); // 화면으로 객체 전달 return "userView"; // 결과 화면으로 이동 } }// 출력결과 // lee // 20 // seoul이 방식은 컨트롤러 안에서 요청값을 확인하거나 간단히 사용할 때 쓴다.
user.getName()은name값을 꺼내고,user.getAge()는age값을 꺼낸다.
Model에 담아 화면으로 넘길 수 있다
두 번째 방법은
DTO객체를 그대로Model에 담아 화면으로 넘기는 것이다.// UserControllerModelExample.java @Controller public class UserController { @PostMapping("/user/add") // 회원 등록 요청 처리 public String addUser(@ModelAttribute UserDTO user, Model model) { model.addAttribute("user", user); // 화면에서 사용할 객체 저장 return "userView"; // 결과 화면으로 이동 } }화면에서는
Model에 담긴user객체를 꺼내 사용할 수 있다.// userView.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>userView</title> </head> <body> <p>이름: <span th:text="${user.name}"></span></p> <p>나이: <span th:text="${user.age}"></span></p> <p>주소: <span th:text="${user.address}"></span></p> </body> </html>
Thymeleaf에서${user.name}은user객체의name값을 읽는 표현이다.
초보자 기준에서는 내부적으로getName()을 통해 값을 읽는다고 이해하면 된다.
서비스로 전달할 수 있다
세 번째 방법은
DTO객체를 서비스로 넘기는 것이다.
컨트롤러는 요청을 받고 흐름을 연결하는 역할이기 때문에, 핵심 처리는 서비스에게 맡길 수 있다.// UserControllerServiceExample.java @Controller public class UserController { private final UserService userService; // 서비스 의존 public UserController(UserService userService) { this.userService = userService; // 생성자 주입 } @PostMapping("/user/add") // 회원 등록 요청 처리 public String addUser(@ModelAttribute UserDTO user, Model model) { userService.register(user); // DTO 객체를 서비스로 전달 model.addAttribute("user", user); // 화면에서 사용할 객체 저장 return "userView"; // 결과 화면으로 이동 } }// UserService.java @Service public class UserService { public void register(UserDTO user) { System.out.println(user.getName()); // 서비스에서 이름 값 사용 System.out.println(user.getAge()); // 서비스에서 나이 값 사용 System.out.println(user.getAddress()); // 서비스에서 주소 값 사용 } }이 방식은 회원 등록, 게시글 작성, 주문 처리처럼 실제 처리 로직이 필요한 경우에 자연스럽다.
컨트롤러는DTO를 받아 서비스로 넘기고, 서비스는 그 객체 안의 값을 사용해 필요한 처리를 한다.
정리하면 아래와 같다.
- 컨트롤러에서는
getter로DTO값을 꺼낼 수 있다.- 화면에 보여 줄 값은
Model에 담아 넘길 수 있다.- 실제 처리 로직이 필요하면
DTO객체를 서비스로 넘길 수 있다.따라서
@ModelAttribute로 받은DTO는 컨트롤러 안에서 읽고, 화면으로 넘기고, 서비스로 전달할 수 있는 요청 데이터 묶음이다.
setter는 언제 직접 사용하는가
@ModelAttribute바인딩에서는 개발자가 요청값을 하나씩 직접setter로 넣지 않아도 된다.
예를 들어name,age,address요청값은Spring이 바인딩 과정에서DTO객체 안에 넣어 준다.
하지만 이것이 개발자가setter를 절대 쓰지 않는다는 뜻은 아니다.
이미 들어온 값을 서버 기준에 맞게 정리하거나 바꿔야 할 때는 컨트롤러나 서비스에서setter를 직접 사용할 수 있다.
가장 쉬운 예시는 이름 앞뒤 공백 제거이다.
사용자가 이름을 입력할 때 실수로 앞뒤 공백을 넣을 수 있다.
예를 들어" lee "처럼 입력하면 그대로 저장하거나 출력하기 전에"lee"로 정리하고 싶을 수 있다.// UserControllerSetterExample.java @Controller public class UserController { @PostMapping("/user/add") // 회원 등록 요청 처리 public String addUser(@ModelAttribute UserDTO user, Model model) { String trimmedName = user.getName().trim(); // 이름 앞뒤 공백 제거 user.setName(trimmedName); // 정리된 이름으로 다시 저장 model.addAttribute("user", user); // 화면에서 사용할 객체 저장 return "userView"; // 결과 화면으로 이동 } }처음 요청값이 아래처럼 들어왔다고 생각하면 된다.
// 변경 전 // user.name → " lee "// 변경 후 // user.name → "lee"즉,
setter는 새로운 값을 억지로 추가할 때만 쓰는 것이 아니다.
이미 들어온 요청값을 정리하거나 수정해서 객체에 다시 저장할 때도 사용할 수 있다.
다만 복잡한 값 변경이나 핵심 처리 규칙은 컨트롤러보다 서비스에서 처리하는 것이 더 자연스럽다.
컨트롤러는 요청과 응답 흐름을 연결하는 역할이고, 서비스는 핵심 로직을 처리하는 역할이기 때문이다.
정리하면 아래와 같다.
Spring은 객체 바인딩 과정에서setter를 사용할 수 있다.- 개발자는 요청값을 직접 넣기 위해
setter를 반복해서 호출하지 않아도 된다.- 개발자가 직접
setter를 쓰는 경우는 이미 들어온 값을 정리하거나 수정할 때이다.- 예를 들어
" lee "를"lee"로 바꾼 뒤setName()으로 다시 저장할 수 있다.- 복잡한 값 변경은 컨트롤러보다 서비스에서 처리하는 편이 좋다.
따라서
Spring이 바인딩할 때 사용하는setter와 개발자가 값을 수정하기 위해 직접 사용하는setter는 구분해서 이해해야 한다.
ModelAttribute 이름 지정과 생략
@ModelAttribute는 이름을 지정해서 사용할 수도 있고, 생략할 수도 있다.
이 부분은 실제 코드에서 자주 보이기 때문에 정확히 알아야 한다.
먼저 아래처럼@ModelAttribute만 붙이면 요청값이UserDTO객체에 바인딩된다.// ModelAttributeDefaultNameExample.java @PostMapping("/user/add") public String addUser(@ModelAttribute UserDTO user, Model model) { model.addAttribute("user", user); // 화면에서 사용할 이름을 직접 지정 return "userView"; // 결과 화면으로 이동 }이 방식은 가장 명확하다.
요청값을UserDTO객체로 받고, 화면에서 사용할 이름은model.addAttribute("user", user)로 직접 정한다.
다음처럼@ModelAttribute("user")로 이름을 지정할 수도 있다.// ModelAttributeNamedExample.java @PostMapping("/user/add") public String addUser(@ModelAttribute("user") UserDTO user) { return "userView"; // user라는 이름으로 Model에 담길 수 있음 }이 코드에서
"user"는 모델에 담길 이름이다.
따라서 결과 화면에서는${user.name}처럼 사용할 수 있다.
즉,@ModelAttribute("user")는 요청값을 객체에 바인딩하면서, 그 객체를 모델에서 사용할 이름도 함께 지정하는 방식이다.
또 아래처럼@ModelAttribute를 생략할 수 있는 경우도 있다.// ModelAttributeOmittedExample.java @PostMapping("/user/add") public String addUser(UserDTO user, Model model) { model.addAttribute("user", user); // 화면에서 사용할 객체 저장 return "userView"; // 결과 화면으로 이동 }
UserDTO처럼 단순 값 타입이 아닌 객체가 컨트롤러 메서드 매개변수로 오면,Spring이 요청값을 객체에 바인딩할 수 있다.
그래서@ModelAttribute를 생략해도 동작할 수 있다.
하지만 처음 배우는 단계에서는 생략보다 명시가 더 좋다.
@ModelAttribute UserDTO user라고 적혀 있으면 이 객체가 요청값을 받아 만들어지는 객체라는 점이 코드에서 바로 보이기 때문이다.
세 방식을 비교하면 아래와 같다.
@ModelAttribute UserDTO user는 요청값을UserDTO객체로 받는다는 뜻이 분명하다.@ModelAttribute("user") UserDTO user는 요청값을 객체로 받으면서 모델 이름도user로 지정한다.UserDTO user처럼 생략해도 객체 바인딩이 일어날 수 있지만, 처음에는 의미가 덜 드러난다.model.addAttribute("user", user)를 직접 쓰면 화면에서 사용할 이름이 명확하다.따라서 처음에는 아래 방식이 가장 안전하다.
// ModelAttributeRecommendedExample.java @PostMapping("/user/add") public String addUser(@ModelAttribute UserDTO user, Model model) { model.addAttribute("user", user); // 화면에서 사용할 이름을 명확하게 지정 return "userView"; // 결과 화면으로 이동 }이렇게 쓰면 흐름이 단순하다.
요청값은UserDTO객체에 들어간다.
컨트롤러는 그 객체를user라는 이름으로Model에 담는다.
화면은user라는 이름으로 객체 안의 값을 꺼낸다.
정리하면@ModelAttribute는 생략할 수 있는 경우도 있지만, 처음에는 명시해서 요청값이 객체로 바인딩되는 흐름을 코드에 드러내는 것이 좋다.
그리고 화면에서 사용할 이름은Model에 직접 담거나@ModelAttribute("이름")으로 지정할 수 있다.
Servlet/JSP 방식과 Spring MVC 방식 비교
아래 예제는 같은 회원 입력값을
Servlet/JSP방식과Spring MVC방식으로 비교하기 위한 코드이다.
같은 프로젝트에 두 코드를 동시에 넣어 실행하라는 뜻이 아니다.
여러 요청값을 화면으로 넘기는 방식과, 여러 요청값을 객체로 묶어 넘기는 방식이 어떻게 달라지는지 비교해서 보기 위한 예제이다.
Servlet/JSP에서 값을 각각 화면에 넘기는 흐름
먼저
Servlet/JSP방식에서는 요청값을 하나씩 직접 꺼낼 수 있다.
이때 화면에 값만 각각 보여 주면 된다면request.setAttribute()로 각각 담아 보낼 수 있다.// UserServletSimpleAttribute.java @WebServlet("/user/add") public class UserServletSimpleAttribute extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String name = request.getParameter("name"); // name 요청값 직접 꺼내기 int age = Integer.parseInt(request.getParameter("age")); // age 요청값 숫자로 변환 String address = request.getParameter("address"); // address 요청값 직접 꺼내기 request.setAttribute("name", name); // 이름 값을 JSP로 전달 request.setAttribute("age", age); // 나이 값을 JSP로 전달 request.setAttribute("address", address); // 주소 값을 JSP로 전달 RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/userView.jsp"); // 결과 JSP 지정 dispatcher.forward(request, response); // JSP로 이동 } }이 방식은 값을 각각 화면에 보여 주기에는 가능하다.
하지만 이름, 나이, 주소를 하나의 회원 정보 객체로 다루는 구조는 아니다.
즉,name,age,address가 각각 따로 전달될 뿐이다.
Servlet/JSP에서 DTO로 묶어 화면에 넘기는 흐름
여러 값을 하나의 회원 정보처럼 묶어 다루고 싶다면
Servlet/JSP방식에서도DTO를 만들 수 있다.
다만 이 경우에는 개발자가 직접 요청값을 꺼내고, 직접DTO객체를 만들고, 직접setter로 값을 넣어야 한다.// UserServletDTO.java @WebServlet("/user/add") public class UserServletDTO extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String name = request.getParameter("name"); // name 요청값 직접 꺼내기 int age = Integer.parseInt(request.getParameter("age")); // age 요청값 숫자로 변환 String address = request.getParameter("address"); // address 요청값 직접 꺼내기 UserDTO user = new UserDTO(); // DTO 객체 직접 생성 user.setName(name); // 이름 값 직접 저장 user.setAge(age); // 나이 값 직접 저장 user.setAddress(address); // 주소 값 직접 저장 request.setAttribute("user", user); // JSP에서 사용할 객체 저장 RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/userView.jsp"); // 결과 JSP 지정 dispatcher.forward(request, response); // JSP로 이동 } }// userView.jsp <%@ page contentType="text/html;charset=UTF-8" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>userView</title> </head> <body> <p>이름: ${user.name}</p> <p>나이: ${user.age}</p> <p>주소: ${user.address}</p> </body> </html>// 출력결과 // 요청 방식: POST /user/add // 전달값: name=lee, age=20, address=seoul // 화면 출력: 이름: lee // 화면 출력: 나이: 20 // 화면 출력: 주소: seoul이 흐름에서는
name,age,address가 따로 흩어진 값으로 끝나지 않는다.
개발자가 직접UserDTO객체를 만들고, 그 안에 값을 넣은 뒤,user라는 이름으로 화면에 넘긴다.
그래서JSP에서는${user.name},${user.age},${user.address}처럼 객체 안의 값을 꺼내 출력할 수 있다.
Spring MVC에서 ModelAttribute로 DTO를 받는 흐름
이제 같은 흐름을
Spring MVC방식으로 보면 요청값을 객체에 묶어 받을 수 있다.// userForm.html <form action="/user/add" method="post"> <input type="text" name="name"> <!-- UserDTO의 name 속성과 연결 --> <input type="number" name="age"> <!-- UserDTO의 age 속성과 연결 --> <input type="text" name="address"> <!-- UserDTO의 address 속성과 연결 --> <button type="submit">등록</button> <!-- 입력값 전송 --> </form>// UserController.java @Controller public class UserController { @PostMapping("/user/add") // /user/add POST 요청 처리 public String addUser(@ModelAttribute UserDTO user, Model model) { model.addAttribute("user", user); // 결과 화면에서 사용할 객체 저장 return "userView"; // 결과 화면으로 이동 } }// userView.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>userView</title> </head> <body> <p>이름: <span th:text="${user.name}"></span></p> <p>나이: <span th:text="${user.age}"></span></p> <p>주소: <span th:text="${user.address}"></span></p> </body> </html>// 출력결과 // 요청 방식: POST /user/add // 전달값: name=lee, age=20, address=seoul // 화면 출력: 이름: lee // 화면 출력: 나이: 20 // 화면 출력: 주소: seoul
Spring MVC방식에서는 컨트롤러가request.getParameter()를 여러 번 호출하지 않아도 된다.
또 컨트롤러가new UserDTO()로 객체를 직접 만들고, 요청값을 하나씩setter로 넣지 않아도 된다.
Spring이 컨트롤러 메서드 실행 전에UserDTO객체를 준비하고, 요청값을 바인딩한 뒤, 그 객체를 매개변수로 전달하기 때문이다.
세 방식의 차이 정리
세 방식의 차이는 아래처럼 정리할 수 있다.
Servlet/JSP에서 값만 각각 화면에 보낼 때는request.setAttribute()로 각각 담을 수 있다.Servlet/JSP에서도 여러 값을 객체로 묶고 싶다면DTO를 만들 수 있다.- 다만
Servlet/JSP에서DTO를 사용할 때는 개발자가 직접new UserDTO()를 만들고setter로 값을 넣어야 한다.Spring MVC에서는@ModelAttribute로 여러 요청값을DTO객체에 묶어 받을 수 있다.Spring MVC에서는 컨트롤러 메서드 실행 전에Spring이DTO객체를 준비하고 요청값을 바인딩한다.- 일반적인
setter바인딩에서는 기본 생성자로 빈 객체를 만들고,setter로 값을 넣는 흐름을 사용한다.setter가 없어도 생성자 바인딩이 가능한 구조라면 생성자를 통해 값이 들어갈 수 있다.Spring MVC에서는 필드 타입에 맞춰 기본적인 타입 변환을 처리해 줄 수 있다.Servlet/JSP에서는request.setAttribute()로 화면에 넘길 값을 담는다.Spring MVC에서는Model에 화면에 넘길 값을 담는다.따라서
@ModelAttribute는 기존Servlet/JSP에서DTO를 쓰던 흐름과 완전히 다른 개념이 아니다.
요청값을 꺼내고, 객체를 만들고, 객체에 직접 값을 넣던 과정을Spring이 객체 바인딩으로 처리해 주는 방식이다.
이 섹션의 핵심은 이것이다.
@ModelAttribute는HTML form에서 전송된 여러 요청값을DTO객체 하나에 묶어 컨트롤러 메서드의 매개변수로 전달하는 방식이다.
값이 들어가는 방식은 대표적으로setter바인딩과 생성자 바인딩이 있으며, 초보자 단계에서는 기본 생성자로 빈 객체를 만들고setter로 값을 넣는 흐름을 먼저 이해하는 것이 가장 안전하다.
컨트롤러에서는getter로 값을 꺼내거나 서비스로 넘길 수 있고, 화면에 출력할 값은Model에 담아 뷰로 넘긴다고 이해하면 된다.
@PathVariable은 요청 주소의 경로 안에 들어 있는 값을 꺼내서 컨트롤러 메서드의 매개변수로 받는 어노테이션이다.
예를 들어/board/1처럼 요청이 들어오면, 마지막의1을 게시글 번호처럼 사용할 수 있다.
즉,@PathVariable은 주소의 일부를 값처럼 꺼내 쓰는 방식이다.
이 개념을 이해하려면 먼저@RequestParam과 비교해서 봐야 한다.
@RequestParam은/board/detail?id=1처럼?뒤에 붙은 값을 받는다.
반면@PathVariable은/board/1처럼 주소 경로 자체에 포함된 값을 받는다.
둘 다 컨트롤러 메서드의 매개변수로 값을 받는다는 점은 비슷하다.
하지만 값이 들어 있는 위치와 주소에서 표현하는 의미가 다르다.
@RequestParam은URL뒤의 쿼리 문자열에서 값을 꺼내고,@PathVariable은 주소 경로 안에서 값을 꺼낸다.
초보자 단계에서는 먼저 이렇게 구분하면 된다.
@RequestParam은 조건값을 받을 때 자연스럽고,@PathVariable은 특정 대상을 식별하는 값을 받을 때 자연스럽다.
검색어, 페이지 번호, 정렬 기준처럼 조건에 가까운 값은@RequestParam이 어울린다.
게시글 번호, 회원 번호, 상품 번호처럼 특정 대상을 가리키는 값은@PathVariable이 어울린다.
이 개념도Servlet/JSP에서 배운 흐름과 비교하면 더 쉽게 이해할 수 있다.
Servlet에서도/board/1처럼 주소 뒤에 값을 붙여 요청할 수 있다.
하지만 이 값을 쓰려면request.getPathInfo()로 경로 정보를 꺼내고, 필요한 부분을 직접 잘라서 사용해야 했다.
Spring MVC에서는/board/{id}처럼 주소 안에 값이 들어갈 자리를 표시하고,@PathVariable("id")로 그 값을 바로 받을 수 있다.
즉,Spring MVC에서는 주소 경로에 들어 있는 값을 직접 자르지 않고, 어노테이션으로 매개변수에 연결할 수 있다.
아래 코드들은 비교를 위한 예제이다.
같은 프로젝트에 전부 동시에 넣어 실행하라는 뜻이 아니다.
요청 주소 모양과 값이 들어오는 위치를 비교하기 위한 코드로 보면 된다.
@RequestParam과 @PathVariable의 차이
@RequestParam과@PathVariable은 모두 요청에서 값을 꺼내 컨트롤러 메서드로 전달한다.
그래서 처음 보면 둘이 비슷해 보인다.
하지만 실제 주소 모양을 보면 차이가 바로 보인다.
먼저@RequestParam은?뒤에 붙는 값을 받을 때 사용한다.
예를 들어/board/detail?id=1에서id=1은 쿼리 문자열로 전달된 요청 파라미터이다.
이런 값은@RequestParam("id")로 받을 수 있다.// BoardRequestParamExample.java @Controller public class BoardRequestParamController { @GetMapping("/board/detail") // /board/detail?id=1 요청 처리 public String detail(@RequestParam("id") int id, Model model) { model.addAttribute("id", id); // 화면에 넘길 게시글 번호 저장 return "boardView"; // boardView.html 화면으로 이동 } }// 출력결과 // 브라우저 주소: /board/detail?id=1 // id 매개변수 값: 1 // 화면 출력: 게시글 번호: 1이 방식에서는 주소가
/board/detail이고, 실제 값은?id=1부분에 들어 있다.
즉,id는 주소 경로가 아니라 요청 조건처럼 붙은 값이다.
반면@PathVariable은 주소 경로 자체에 포함된 값을 받을 때 사용한다.
예를 들어/board/1에서1은?뒤에 붙은 값이 아니라 주소 경로의 일부이다.
이런 값은@PathVariable("id")로 받을 수 있다.// BoardPathVariableExample.java @Controller public class BoardPathVariableController { @GetMapping("/board/{id}") // /board/1 같은 요청 처리 public String detail(@PathVariable("id") int id, Model model) { model.addAttribute("id", id); // 화면에 넘길 게시글 번호 저장 return "boardView"; // boardView.html 화면으로 이동 } }// 출력결과 // 브라우저 주소: /board/1 // id 매개변수 값: 1 // 화면 출력: 게시글 번호: 1두 예제에서 사용하는 결과 화면은 아래처럼 생각하면 된다.
// boardView.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>boardView</title> </head> <body> <h1>게시글 번호: <span th:text="${id}"></span></h1> </body> </html>
boardView.html은Model에 담긴id값을 화면에 출력한다.
따라서@RequestParam으로 받은 값이든,@PathVariable로 받은 값이든 컨트롤러에서model.addAttribute("id", id)로 담으면 화면에서는${id}로 출력할 수 있다.
두 코드를 비교하면 차이가 더 분명하다.
@RequestParam방식 주소는/board/detail?id=1이다.@PathVariable방식 주소는/board/1이다.@RequestParam은?id=1처럼 이름과 값을 함께 보낸다.@PathVariable은/1처럼 주소 경로 안에 값을 넣는다.@RequestParam은 검색 조건, 필터, 페이지 번호처럼 선택 조건에 자주 사용한다.@PathVariable은 게시글 번호, 회원 번호, 상품 번호처럼 특정 대상을 식별할 때 자주 사용한다.따라서 두 어노테이션은 단순히 문법만 다른 것이 아니다.
요청값을 주소에 어떤 의미로 표현할 것인지가 다르다.
같은 게시글 번호라도 주소 의미가 달라진다
같은
id=1이라는 값이라도 주소에 어떻게 표현하느냐에 따라 의미가 달라진다.
/board/detail?id=1은/board/detail이라는 기능에id=1이라는 조건을 붙인 느낌이다.
즉, “게시글 상세 보기 기능을 실행하는데, 그 조건으로id가1이다”라고 읽을 수 있다.
반면/board/1은 주소 자체가1번 게시글을 가리키는 느낌이다.
즉, “게시글 중에서1번 게시글이라는 자원에 접근한다”라고 읽을 수 있다.
예를 들어 게시글 검색은 아래처럼@RequestParam이 자연스럽다.// BoardSearchRequestParamExample.java @Controller public class BoardSearchController { @GetMapping("/boards") // 게시글 목록 검색 요청 처리 public String search(@RequestParam("keyword") String keyword, @RequestParam("page") int page, Model model) { model.addAttribute("keyword", keyword); // 검색어 저장 model.addAttribute("page", page); // 페이지 번호 저장 return "boardList"; // 목록 화면으로 이동 } }// 출력결과 // 브라우저 주소: /boards?keyword=java&page=1 // keyword 값: java // page 값: 1이 주소는
java라는 검색어와1페이지라는 조건으로 게시글 목록을 조회한다는 의미에 가깝다.
검색어와 페이지 번호는 특정 게시글 하나를 가리키는 값이 아니라, 목록을 조회할 때 붙는 조건이다.
그래서@RequestParam이 자연스럽다.
반면 게시글 한 개를 조회할 때는 아래처럼@PathVariable이 자연스럽다.// BoardDetailPathVariableExample.java @Controller public class BoardDetailController { @GetMapping("/boards/{boardId}") // 특정 게시글 조회 요청 처리 public String detail(@PathVariable("boardId") int boardId, Model model) { model.addAttribute("boardId", boardId); // 게시글 번호 저장 return "boardDetail"; // 상세 화면으로 이동 } }// 출력결과 // 브라우저 주소: /boards/10 // boardId 값: 10이 주소는 게시글 목록 중
10번 게시글 자체를 가리킨다.
따라서 특정 대상을 식별하는 값은@PathVariable로 받는 것이 더 자연스럽다.
정리하면 아래와 같다.
- 조건을 전달하는 값은
@RequestParam이 자연스럽다.- 특정 대상을 식별하는 값은
@PathVariable이 자연스럽다./boards?keyword=java&page=1은 게시글 목록에 검색 조건을 붙이는 요청이다./boards/10은10번 게시글이라는 특정 대상을 가리키는 요청이다.즉,
@RequestParam은 조건값,@PathVariable은 식별값이라고 먼저 구분하면 이해하기 쉽다.
경로 변수는 어떻게 표시하는가
@PathVariable을 사용하려면 먼저 요청 주소에 값이 들어갈 자리를 표시해야 한다.
이때 중괄호{}를 사용한다.
예를 들어/board/{id}라고 쓰면,/board/1,/board/2,/board/100처럼 들어오는 주소에서{id}자리에 해당하는 값을 꺼낼 수 있다.
여기서{id}는 실제 주소에 그대로 보이는 글자가 아니다.
값이 들어갈 자리라는 뜻이다.
즉,/board/{id}는 “/board/뒤에 오는 값을id라는 이름으로 받겠다”는 의미이다.
컨트롤러 메서드에서는@PathVariable("id") int id처럼 작성해서 그 값을 받을 수 있다.
그러면/board/1로 요청했을 때id매개변수에는1이 들어간다.
즉, 주소 경로에 있던 값이Java코드 안의 변수로 들어오는 것이다.// PathVariableBasicFlowExample.java @Controller public class PathVariableBasicController { @GetMapping("/board/{id}") // {id} 자리에 들어온 값을 받음 public String board(@PathVariable("id") int id, Model model) { model.addAttribute("id", id); // 화면에서 사용할 id 저장 return "boardView"; // 결과 화면으로 이동 } }// 출력결과 // 요청 주소: /board/1 // {id} 자리에 들어온 값: 1 // id 매개변수 값: 1이 흐름은 아래처럼 정리할 수 있다.
- 주소 패턴에
{id}처럼 값이 들어갈 자리를 만든다.- 실제 요청에서는
/board/1처럼 값이 들어온다.- 스프링은
{id}자리에 들어온1을 꺼낸다.- 꺼낸 값을
@PathVariable("id")이 붙은 매개변수에 넣어 준다.- 컨트롤러는 그 값을
Model에 담아 화면으로 넘길 수 있다.따라서
@PathVariable을 볼 때는 주소 패턴의{이름}과 메서드 매개변수의 연결 관계를 함께 봐야 한다.
이름이 같을 때와 다를 때
주소 패턴의 변수 이름과 메서드 매개변수 이름이 같으면 연결이 비교적 단순하다.
예를 들어 주소 패턴이/board/{id}이고 매개변수도int id라면, 스프링은id라는 이름을 기준으로 값을 연결할 수 있다.
하지만 이름이 다르면 연결 이름을 명확하게 적어 주는 것이 좋다.
예를 들어 주소 패턴은/board/{id}인데 매개변수 이름을number로 쓰고 싶다면@PathVariable("id") int number처럼 작성한다.
이렇게 하면 주소의{id}값을number변수에 넣겠다는 의미가 분명해진다.
아래 코드는 주소 패턴의 이름과 매개변수 이름이 같은 경우이다.// PathVariableSameNameExample.java @Controller public class PathVariableSameNameController { @GetMapping("/board/{id}") // 경로 변수 이름: id public String board(@PathVariable("id") int id, Model model) { model.addAttribute("id", id); // id 이름으로 화면에 전달 return "boardView"; // 결과 화면으로 이동 } }// 출력결과 // 요청 주소: /board/3 // id 매개변수 값: 3아래 코드는 주소 패턴의 이름과 매개변수 이름이 다른 경우이다.
// PathVariableDifferentNameExample.java @Controller public class PathVariableDifferentNameController { @GetMapping("/board/{id}") // 경로 변수 이름: id public String board(@PathVariable("id") int number, Model model) { model.addAttribute("id", number); // number 값을 id 이름으로 화면에 전달 return "boardView"; // 결과 화면으로 이동 } }// 출력결과 // 요청 주소: /board/3 // {id} 자리에 들어온 값: 3 // number 매개변수 값: 3이 코드에서 주소의 이름은
{id}이고,Java매개변수 이름은number이다.
하지만@PathVariable("id")라고 적었기 때문에 스프링은{id}자리에 들어온 값을number매개변수에 넣는다.
정리하면 아래와 같다.
{id}와 매개변수id처럼 이름이 같으면 같은 이름으로 연결된다.{id}와 매개변수number처럼 이름이 다르면@PathVariable("id")로 직접 연결한다.- 이름을 직접 적으면 어떤 경로값을 받는지 코드에서 더 분명하게 보인다.
따라서 학습 단계에서는
@PathVariable("id")처럼 이름을 명시해서 쓰는 편이 흐름을 이해하기 쉽다.
여러 개의 경로값도 받을 수 있다
@PathVariable은 경로값 하나만 받을 때만 쓰는 것이 아니다.
주소 안에 값이 여러 개 들어가면 여러 개의@PathVariable로 받을 수 있다.
예를 들어/members/5/boards/10이라는 주소가 있다고 생각해 보자.
이 주소는5번 회원이 작성한10번 게시글을 조회하는 느낌으로 읽을 수 있다.
이때5는 회원 번호이고,10은 게시글 번호이다.
코드는 아래처럼 작성할 수 있다.// MultiplePathVariableExample.java @Controller public class MultiplePathVariableController { @GetMapping("/members/{memberId}/boards/{boardId}") // 회원 번호와 게시글 번호를 경로에서 받음 public String detail(@PathVariable("memberId") int memberId, @PathVariable("boardId") int boardId, Model model) { model.addAttribute("memberId", memberId); // 회원 번호 저장 model.addAttribute("boardId", boardId); // 게시글 번호 저장 return "memberBoardView"; // 결과 화면으로 이동 } }// 출력결과 // 요청 주소: /members/5/boards/10 // memberId 값: 5 // boardId 값: 10이 코드에서
{memberId}자리에 들어온5는memberId매개변수로 들어간다.
{boardId}자리에 들어온10은boardId매개변수로 들어간다.
즉,@PathVariable은 주소 안에서 의미 있는 위치에 있는 값을 각각 꺼내서 사용할 수 있다.
다만 주소가 너무 길어지거나 값의 의미가 애매해지면 읽기 어려워질 수 있다.
그래서 경로값은 보통 특정 대상을 식별하는 데 필요한 값 위주로 사용하는 것이 좋다.
경로값도 타입 변환이 일어날 수 있다
주소 경로에 들어오는 값은 처음에는 문자열이다.
예를 들어/board/10에서10은 주소 안에 들어 있는 문자 형태의 값이다.
하지만 컨트롤러 매개변수를int id처럼 숫자 타입으로 작성하면, 스프링은 가능한 경우 문자열"10"을 숫자10으로 바꿔 넣어 줄 수 있다.
아래 예제를 보면 된다.// PathVariableTypeConvertExample.java @Controller public class PathVariableTypeConvertController { @GetMapping("/board/{id}") // /board/10 요청 처리 public String board(@PathVariable("id") int id, Model model) { model.addAttribute("id", id); // 숫자로 변환된 id 저장 return "boardView"; // 결과 화면으로 이동 } }// 출력결과 // 요청 주소: /board/10 // 경로값 원래 형태: "10" // int id 값: 10하지만
/board/abc처럼 숫자로 바꿀 수 없는 값이 들어오면 문제가 생길 수 있다.
왜냐하면abc는int로 변환할 수 없기 때문이다.
정리하면 아래와 같다.
- 경로값은 처음에는 문자열로 들어온다.
- 매개변수 타입이
int,long같은 숫자형이면 스프링이 변환을 시도할 수 있다.- 숫자로 바꿀 수 없는 값이 들어오면 타입 변환 오류가 발생할 수 있다.
따라서
@PathVariable로 숫자를 받을 때는 실제 주소에 숫자로 변환 가능한 값이 들어와야 한다.
경로값이 없으면 어떻게 되는가
@RequestParam은 값이 없어도required=false나defaultValue로 처리할 수 있는 경우가 많다.
예를 들어/boards?page=1에서page가 없으면 기본값을 줄 수 있다.
하지만@PathVariable은 주소 경로 자체에 값이 들어가야 매핑이 된다.
예를 들어 컨트롤러가/board/{id}를 처리한다고 했을 때,/board/1은 매핑될 수 있다.
하지만/board는{id}자리에 들어갈 값이 없기 때문에 같은 요청으로 보기 어렵다.// 경로값이 있는 요청 // /board/1 → /board/{id}와 매칭 가능// 경로값이 없는 요청 // /board → /board/{id}와 매칭되지 않음따라서
@PathVariable은 특정 값이 주소의 일부로 들어오는 경우에 사용해야 한다.
값이 선택 사항이고 없어도 되는 조건이라면@RequestParam이 더 자연스러운 경우가 많다.
정리하면 아래와 같다.
/board/{id}는/board/1처럼 값이 있는 주소와 맞는다./board처럼 값이 빠진 주소는/board/{id}와 다른 주소이다.- 선택 조건처럼 없어도 되는 값은
@RequestParam이 더 자연스러울 수 있다.- 특정 대상을 반드시 식별해야 하는 값은
@PathVariable이 자연스럽다.따라서 주소 설계에서 값이 필수인지 선택인지도 함께 생각해야 한다.
REST 스타일 주소에서 자주 쓰인다
@PathVariable은REST스타일 주소에서 자주 사용된다.
REST는 웹에서 자원을 주소로 표현하는 방식이라고 이해하면 된다.
여기서 자원은 게시글, 회원, 상품처럼 서버에서 다루는 대상이다.
예를 들어 게시글 목록은/boards로 표현할 수 있고, 특정 게시글 하나는/boards/1처럼 표현할 수 있다.
이때/boards/1의1은 단순한 옵션이 아니라 “1번 게시글”이라는 대상을 가리키는 값이다.
그래서 이런 값은@PathVariable로 받는 것이 자연스럽다.
반대로 검색어처럼 조건에 가까운 값은@RequestParam이 더 자연스럽다.
예를 들어/boards?keyword=java&page=1은 특정 게시글 하나가 아니라 게시글 목록 중에서 검색 조건을 붙인 요청이다.
즉,@PathVariable과@RequestParam은 주소 설계 의도에 따라 나누어 쓰면 된다.
정리하면 아래와 같다.
/boards/1은1번 게시글이라는 대상을 가리킨다./boards?keyword=java&page=1은 게시글 목록에 검색 조건을 붙인다.- 대상을 식별하는 값은
@PathVariable이 자연스럽다.- 조건을 전달하는 값은
@RequestParam이 자연스럽다.이 차이를 알면 주소를 볼 때 요청의 의미를 더 쉽게 읽을 수 있다.
Servlet/JSP 방식과 Spring MVC 방식 비교
아래 두 예제는 같은
/board/1요청을Servlet/JSP방식과Spring MVC방식으로 비교하기 위한 코드이다.
같은 프로젝트에 두 코드를 동시에 넣어 실행하라는 뜻이 아니다.
주소 경로에 들어 있는 값을 어떻게 꺼내고 화면으로 넘기는지 비교해서 보기 위한 예제이다.
먼저Servlet/JSP방식에서는/board/1처럼 주소 뒤에 붙은 값을 직접 꺼내서 가공해야 한다.
이때/board/*처럼 주소 패턴을 잡고,request.getPathInfo()로/1같은 경로 정보를 가져올 수 있다.// PathVariableServletExample.java @WebServlet("/board/*") public class BoardServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String pathInfo = request.getPathInfo(); // /1 같은 경로 정보 꺼내기 String idText = pathInfo.substring(1); // 앞의 / 제거 int id = Integer.parseInt(idText); // 문자열을 숫자로 변환 request.setAttribute("id", id); // JSP로 넘길 게시글 번호 저장 RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/boardView.jsp"); // 결과 JSP 지정 dispatcher.forward(request, response); // JSP로 이동 } }// boardView.jsp <%@ page contentType="text/html;charset=UTF-8" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>boardView</title> </head> <body> <h1>게시글 번호: ${id}</h1> </body> </html>// 출력결과 // 브라우저 주소: /board/1 // 화면 출력: 게시글 번호: 1 // 브라우저 주소: /board/10 // 화면 출력: 게시글 번호: 10이 방식에서는 주소 뒤에 붙은 값을 직접 다뤄야 한다.
request.getPathInfo()로/1같은 경로 정보를 가져오고,substring(1)로/를 제거한 뒤,Integer.parseInt()로 숫자로 바꾼다.
그리고 그 값을request.setAttribute()로JSP에 넘긴다.
즉,Servlet/JSP방식에서는 경로값 꺼내기 → 문자열 가공 → 숫자 변환 → 화면 전달을 개발자가 직접 코드로 작성한다.
이제 같은 흐름을Spring MVC방식으로 보면 주소 패턴에{id}를 표시하고,@PathVariable로 바로 받을 수 있다.// PathVariableSpringExample.java @Controller public class BoardController { @GetMapping("/board/{id}") // /board/값 형태의 요청 처리 public String board(@PathVariable("id") int id, Model model) { model.addAttribute("id", id); // 경로에서 꺼낸 값을 모델에 저장 return "boardView"; // 결과 화면으로 이동 } }// boardView.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>boardView</title> </head> <body> <h1>게시글 번호: <span th:text="${id}"></span></h1> </body> </html>// 출력결과 // 브라우저 주소: /board/1 // 화면 출력: 게시글 번호: 1 // 브라우저 주소: /board/10 // 화면 출력: 게시글 번호: 10이 코드에서
/board/{id}는/board/뒤에 오는 값을id라는 이름으로 받겠다는 뜻이다.
따라서/board/1로 요청하면id는1이 되고,/board/10으로 요청하면id는10이 된다.
컨트롤러는 이 값을Model에 담아서 화면으로 넘긴다.
boardView.html에서는${id}를 출력하므로 화면에는 요청 주소에서 꺼낸 게시글 번호가 보인다.
즉, 주소 경로에 들어 있던 값이 컨트롤러 매개변수로 들어오고, 다시 모델을 통해 뷰까지 전달되는 흐름이다.
두 방식의 차이 정리
Servlet/JSP방식과Spring MVC방식은 주소 경로 안의 값을 꺼내서 화면에 출력한다는 큰 흐름은 같다.
하지만 경로값을 꺼내는 방식이 달라진다.
차이는 아래처럼 정리할 수 있다.
Servlet에서는request.getPathInfo()로 경로 정보를 직접 꺼낸다.Servlet에서는substring()으로 필요한 문자열을 직접 잘라야 할 수 있다.Servlet에서는 숫자 변환을 직접 해야 한다.Spring MVC에서는/board/{id}처럼 경로 변수 자리를 표시한다.Spring MVC에서는{id}자리에 들어온 값이@PathVariable("id")매개변수로 바로 연결된다.Spring MVC에서는 매개변수 타입을 보고 기본적인 타입 변환을 처리해 줄 수 있다.Servlet에서는request.setAttribute()로 화면에 넘길 값을 담는다.Spring MVC에서는Model에 화면에 넘길 값을 담는다.따라서
@PathVariable은 기존Servlet의 경로값 처리 흐름을 없앤 것이 아니다.
주소 경로에서 값을 직접 꺼내고 자르던 과정을 스프링이 경로 변수와 매개변수 연결 방식으로 더 간단하게 처리해 주는 것이다.
이 섹션의 핵심은 이것이다.
@PathVariable은 주소 경로 안에 포함된 값을 꺼내 컨트롤러 메서드의 매개변수로 연결하는 어노테이션이다.
@RequestParam은?뒤의 조건값을 받을 때 자연스럽고,@PathVariable은 주소가 직접 가리키는 대상의 식별값을 받을 때 자연스럽다.
HttpServletRequest는 브라우저가 서버로 보낸 요청 정보를 담고 있는 객체이다.
요청 주소, 요청 방식, 요청 파라미터, 헤더, 쿠키 같은 정보가 이 객체 안에 들어 있다.
즉,HttpServletRequest는 요청 자체를 직접 다룰 때 사용하는 객체이다.
앞에서 본@RequestParam,@ModelAttribute,@PathVariable은Spring이 요청값을 매개변수나 객체에 자동으로 연결해 주는 방식이었다.
반면HttpServletRequest를 사용하면 개발자가 요청 객체에서 필요한 값을 직접 꺼낸다.
즉,HttpServletRequest는 자동 바인딩을 쓰지 않고 요청 객체를 직접 다루는 방식이다.
이 방식은Servlet/JSP에서 배운 흐름과 가장 가깝다.
Servlet에서는doGet()이나doPost()의 매개변수로HttpServletRequest request를 받아서request.getParameter()로 값을 직접 꺼냈다.
Spring MVC에서도 컨트롤러 메서드의 매개변수에HttpServletRequest를 적으면 같은 요청 객체를 받을 수 있다.
즉, 기존 방식이 사라진 것이 아니라 필요할 때Spring MVC컨트롤러 안에서도 요청 객체를 직접 사용할 수 있다.
다만 초보자 단계에서 먼저 기준을 잡아야 한다.
요청값만 단순히 받고 싶다면@RequestParam이나@ModelAttribute가 더 읽기 쉽다.
요청 방식, 요청 주소, 헤더처럼 요청 객체 자체의 정보가 필요하면HttpServletRequest가 더 자연스럽다.
따라서 요청값만 받을 때는 자동 바인딩을 먼저 생각하고, 요청 객체 자체가 필요할 때HttpServletRequest를 사용한다.
이 기준을 잡으면 언제 직접 꺼내야 하는지 헷갈리지 않는다.
HttpServletRequest는 무엇을 직접 꺼낼 수 있는가
HttpServletRequest에는 요청과 관련된 정보가 들어 있다.
가장 자주 보는 것은 요청 파라미터이다.
예를 들어/direct?name=lee&age=20처럼 요청이 들어오면request.getParameter("name"),request.getParameter("age")로 값을 꺼낼 수 있다.
또 요청 방식이나 요청 주소도 확인할 수 있다.
request.getMethod()를 사용하면GET,POST같은 요청 방식을 알 수 있다.
request.getRequestURI()를 사용하면 현재 요청된 주소를 알 수 있다.
헤더 정보도 꺼낼 수 있다.
예를 들어request.getHeader("Referer")를 사용하면 이전 페이지 주소를 확인할 수 있다.
다만Referer는 브라우저나 요청 상황에 따라 없을 수도 있다.
사용자가 주소창에 직접 입력해서 들어오거나, 브라우저 정책 때문에 전달되지 않을 수도 있기 때문이다.
그래서Referer는 항상 값이 온다고 생각하면 안 된다.
정리하면HttpServletRequest로는 아래 정보를 직접 다룰 수 있다.
getParameter()로 요청 파라미터를 꺼낼 수 있다.getMethod()로 요청 방식을 확인할 수 있다.getRequestURI()로 요청 주소를 확인할 수 있다.getHeader()로 요청 헤더 값을 확인할 수 있다.따라서
HttpServletRequest는 요청값뿐 아니라 요청 자체의 여러 정보를 직접 확인할 때 사용하는 객체이다.
@RequestParam과 HttpServletRequest의 차이
@RequestParam과HttpServletRequest는 둘 다 요청 파라미터를 받을 수 있다.
하지만 값을 받는 방식이 다르다.
@RequestParam은Spring이 요청값을 찾아서 매개변수에 넣어 주는 방식이다.
개발자는 매개변수만 선언하면 된다.// RequestParamCompareExample.java @Controller public class RequestParamCompareController { @GetMapping("/direct-param") // /direct-param?name=lee&age=20 요청 처리 public String directParam(@RequestParam("name") String name, @RequestParam("age") int age, Model model) { model.addAttribute("result", name + ":" + age); // 화면에 넘길 결과 저장 return "result"; // result.html 화면으로 이동 } }// 출력결과 // 브라우저 주소: /direct-param?name=lee&age=20 // name 값: lee // age 값: 20 // 화면 출력: lee:20이 방식에서는
Spring이name요청값을String name에 넣어 준다.
그리고age요청값은int age에 넣어 준다.
age는 원래 문자열로 전달되지만,Spring이 가능한 경우 숫자로 변환해 준다.
반면HttpServletRequest를 사용하면 개발자가 직접 요청 객체에서 값을 꺼내야 한다.// HttpServletRequestCompareExample.java @Controller public class HttpServletRequestCompareController { @GetMapping("/direct-request") // /direct-request?name=lee&age=20 요청 처리 public String directRequest(HttpServletRequest request, Model model) { String name = request.getParameter("name"); // name 요청값 직접 꺼내기 String ageText = request.getParameter("age"); // age 요청값을 문자열로 꺼내기 int age = Integer.parseInt(ageText); // 문자열 age를 숫자로 변환 model.addAttribute("result", name + ":" + age); // 화면에 넘길 결과 저장 return "result"; // result.html 화면으로 이동 } }// 출력결과 // 브라우저 주소: /direct-request?name=lee&age=20 // name 값: lee // ageText 값: "20" // age 값: 20 // 화면 출력: lee:20이 방식에서는
request.getParameter("name")으로name값을 직접 꺼낸다.
request.getParameter("age")로 꺼낸 값은 바로 숫자가 아니라 문자열이다.
그래서 숫자로 계산하거나 숫자값으로 다루려면Integer.parseInt()로 직접 변환해야 한다.
두 방식의 차이는 아래처럼 정리할 수 있다.
@RequestParam은Spring이 요청값을 매개변수에 넣어 준다.HttpServletRequest는 개발자가request.getParameter()로 직접 꺼낸다.@RequestParam int age는Spring이 가능한 경우 숫자로 변환해 준다.request.getParameter("age")로 꺼낸 값은 문자열이므로 숫자 변환을 직접 해야 한다.즉,
@RequestParam은 요청값을 편하게 받는 방식이고,HttpServletRequest는 요청 객체에서 직접 찾아 꺼내는 방식이다.
getParameter로 꺼낸 값은 항상 문자열이다
HttpServletRequest에서getParameter()로 꺼낸 값은 기본적으로 문자열이다.
요청 주소가/age-check?age=20이어도20은 처음부터 숫자20으로 들어오는 것이 아니다.
요청 파라미터는 문자열"20"으로 들어온다.
그래서 숫자로 사용하려면 직접 변환해야 한다.// HttpServletRequestStringValueExample.java @Controller public class HttpServletRequestStringValueController { @GetMapping("/age-check") // /age-check?age=20 요청 처리 public String ageCheck(HttpServletRequest request, Model model) { String ageText = request.getParameter("age"); // "20" 문자열로 꺼냄 int age = Integer.parseInt(ageText); // 문자열을 숫자로 변환 model.addAttribute("age", age); // 화면에 넘길 나이 저장 return "ageResult"; // ageResult.html 화면으로 이동 } }// 출력결과 // 브라우저 주소: /age-check?age=20 // ageText 값: "20" // age 값: 20여기서
ageText는 문자열이다.
따라서 숫자 계산이 필요하다면 반드시Integer.parseInt(ageText)처럼 숫자로 바꿔야 한다.
하지만 요청값이 숫자로 바꿀 수 없는 값이면 오류가 생길 수 있다.
예를 들어/age-check?age=abc처럼 요청하면abc는 숫자로 변환할 수 없다.
정리하면 아래와 같다.
request.getParameter()로 꺼낸 값은 문자열이다.- 숫자로 사용하려면
Integer.parseInt()같은 변환이 필요하다.- 숫자로 바꿀 수 없는 값이 들어오면 변환 오류가 발생할 수 있다.
- 숫자 요청값은 값이 있는지, 숫자로 바꿀 수 있는지 확인해야 안전하다.
따라서
HttpServletRequest로 직접 꺼낼 때는 자동 변환이 되지 않는다는 점을 꼭 기억해야 한다.
요청값이 없으면 null이 나올 수 있다
request.getParameter("name")은 요청 안에서name이라는 값을 찾는다.
그런데 요청에name이 없으면 값이 들어오지 않는다.
이때 결과는null이 될 수 있다.
예를 들어/direct-null처럼 요청했는데name파라미터가 없다고 생각하면 된다.// HttpServletRequestNullExample.java @Controller public class HttpServletRequestNullController { @GetMapping("/direct-null") // /direct-null 또는 /direct-null?name=lee 요청 처리 public String directNull(HttpServletRequest request, Model model) { String name = request.getParameter("name"); // 없으면 null 가능 if (name == null) { name = "이름 없음"; // 값이 없을 때 기본 문구 지정 } model.addAttribute("name", name); // 화면에 넘길 이름 저장 return "nameResult"; // nameResult.html 화면으로 이동 } }// 출력결과 // 브라우저 주소: /direct-null // request.getParameter("name") 결과: null // 최종 name 값: 이름 없음 // 화면 출력: 이름 없음 // 브라우저 주소: /direct-null?name=lee // request.getParameter("name") 결과: lee // 최종 name 값: lee // 화면 출력: lee이 코드에서 요청에
name이 없으면request.getParameter("name")은null이 될 수 있다.
그래서 값을 사용하기 전에null인지 확인하는 코드가 필요할 수 있다.
이 예제에서는 처음에 꺼낸 값이null이면if문 안에서name값을"이름 없음"으로 바꾼다.
그래서request.getParameter("name")의 결과는null이지만, 최종적으로 화면에 출력되는 값은"이름 없음"이다.
이 부분은@RequestParam의required=false나defaultValue와 비교해서 생각하면 좋다.
@RequestParam(defaultValue = "이름 없음")처럼 쓰면 기본값 처리를Spring에게 맡길 수 있다.
하지만HttpServletRequest로 직접 꺼내면 개발자가 직접 기본값 처리를 작성해야 한다.
정리하면 아래와 같다.
- 요청값이 없으면
request.getParameter()결과가null일 수 있다.- 직접 꺼낸 값은 사용하기 전에
null처리를 고민해야 한다.- 기본값이 필요하면 개발자가 직접 조건문으로 처리할 수 있다.
- 단순 기본값 처리는
@RequestParam(defaultValue = "...")가 더 간단할 수 있다.따라서
HttpServletRequest는 자유도가 높지만, 그만큼 직접 처리해야 할 코드도 많아진다.
요청 객체 자체의 정보를 확인할 수 있다
HttpServletRequest는 요청 파라미터만 꺼내는 객체가 아니다.
요청 자체의 정보도 확인할 수 있다.
예를 들어 현재 요청 방식이GET인지POST인지, 어떤 주소로 요청이 들어왔는지, 이전 페이지 정보가 있는지 확인할 수 있다.// HttpServletRequestInfoExample.java @Controller public class HttpServletRequestInfoController { @GetMapping("/request-info") // /request-info 요청 처리 public String requestInfo(HttpServletRequest request, Model model) { String method = request.getMethod(); // 요청 방식 확인 String uri = request.getRequestURI(); // 요청 주소 확인 String referer = request.getHeader("Referer"); // 이전 페이지 주소 확인 가능 model.addAttribute("method", method); // 요청 방식 저장 model.addAttribute("uri", uri); // 요청 주소 저장 model.addAttribute("referer", referer); // 이전 페이지 주소 저장 return "requestInfo"; // requestInfo.html 화면으로 이동 } }// 출력결과 // 브라우저 주소: /request-info // method 값: GET // uri 값: /request-info // referer 값: 요청 상황에 따라 없을 수 있음
request.getMethod()는 요청 방식을 알려 준다.
request.getRequestURI()는 현재 요청된 주소를 알려 준다.
request.getHeader("Referer")는 이전 페이지 주소를 알려 줄 수 있다.
다만Referer는 항상 존재하는 값이 아니다.
사용자가 주소창에 직접 입력해서 들어오거나, 브라우저 정책이나 보안 설정에 따라 전달되지 않을 수 있다.
그래서Referer는 값이 없을 수도 있다고 생각해야 한다.
이런 요청 자체의 정보는@RequestParam만으로는 확인하기 어렵다.
그래서 요청 파라미터가 아니라 요청 객체 전체의 정보가 필요할 때HttpServletRequest를 사용한다.
자동 바인딩과 직접 꺼내기의 차이
스프링의 자동 바인딩을 사용하면 요청값을 매개변수나 객체로 바로 받을 수 있다.
예를 들어@RequestParam("name") String name이라고 쓰면Spring이name요청값을 찾아서 매개변수에 넣어 준다.
@ModelAttribute UserDTO user처럼 쓰면 여러 요청값을UserDTO객체에 묶어서 받을 수 있다.
@PathVariable("id") int id처럼 쓰면 주소 경로에 있는 값을 매개변수로 받을 수 있다.
반대로HttpServletRequest를 사용하면 개발자가 직접 값을 꺼내야 한다.
예를 들어request.getParameter("name")처럼 작성해야 요청값을 얻을 수 있다.
즉,Spring이 대신 연결해 주는 것이 아니라 개발자가 요청 객체 안에서 직접 찾는 방식이다.
차이는 아래처럼 볼 수 있다.
- 자동 바인딩은
Spring이 요청값을 매개변수나 객체에 넣어 준다.- 직접 꺼내기는 개발자가
request.getParameter()로 값을 꺼낸다.- 직접 꺼낸 값은 문자열이므로 숫자가 필요하면 직접 변환해야 한다.
- 요청 객체 전체가 필요할 때는
HttpServletRequest를 쓰는 것이 자연스럽다.즉, 자동 바인딩은 값을 받는 코드를 줄여 주고,
HttpServletRequest는 요청 객체 자체를 직접 다룰 수 있게 해 준다.
언제 HttpServletRequest를 쓰면 좋은가
요청값만 단순히 받을 때는
@RequestParam이나@ModelAttribute가 더 읽기 쉽다.
이름, 나이, 주소 같은 입력값을 받을 때는Spring이 자동으로 연결해 주는 방식이 코드도 짧고 실수도 줄어든다.
하지만 요청 객체 자체가 필요할 때는HttpServletRequest를 사용할 수 있다.
예를 들어 요청 방식, 요청 주소, 헤더 정보, 이전 페이지 정보처럼 단순 파라미터가 아닌 요청 정보를 확인해야 할 때는HttpServletRequest가 자연스럽다.
아래처럼 구분하면 된다.
- 이름, 나이, 검색어처럼 요청값만 받으면
@RequestParam이나@ModelAttribute를 먼저 생각한다.- 요청 방식, 요청 주소, 헤더 정보처럼 요청 객체 정보가 필요하면
HttpServletRequest를 생각한다.Servlet/JSP에서 하던 방식처럼 직접 요청 객체를 다뤄야 할 때HttpServletRequest를 사용할 수 있다.따라서
HttpServletRequest는 항상 써야 하는 기본 방식이라기보다, 요청 객체 전체에서 직접 정보를 꺼내야 할 때 사용하는 방식으로 이해하면 된다.
Servlet/JSP 방식과 Spring MVC 방식 비교
아래 두 예제는 같은
/direct?name=lee&age=20요청을Servlet/JSP방식과Spring MVC방식으로 비교하기 위한 코드이다.
같은 프로젝트에 두 코드를 동시에 넣어 실행하라는 뜻이 아니다.
요청 객체에서 값을 직접 꺼내는 흐름이 두 방식에서 어떻게 이어지는지 비교해서 보기 위한 예제이다.
먼저Servlet/JSP방식에서는doGet()의 매개변수로 받은HttpServletRequest에서 값을 직접 꺼낸다.// HttpServletRequestServletExample.java @WebServlet("/direct") public class DirectServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String name = request.getParameter("name"); // name 요청값 직접 꺼내기 int age = Integer.parseInt(request.getParameter("age")); // age 요청값을 숫자로 변환 request.setAttribute("result", name + ":" + age); // JSP로 넘길 결과 저장 RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/result.jsp"); // 결과 JSP 지정 dispatcher.forward(request, response); // JSP로 이동 } }// result.jsp <%@ page contentType="text/html;charset=UTF-8" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>result</title> </head> <body> <h1>${result}</h1> </body> </html>// 출력결과 // 브라우저 주소: /direct?name=lee&age=20 // 화면 출력: lee:20이 방식에서는
request.getParameter("name")으로 이름을 꺼내고,request.getParameter("age")로 나이를 꺼낸다.
그리고 나이는 문자열로 들어오기 때문에Integer.parseInt()로 숫자로 바꾼다.
마지막으로request.setAttribute()에 결과를 담고JSP로 이동한다.
이제 같은 흐름을Spring MVC방식으로 보면 컨트롤러 메서드에서도HttpServletRequest를 받을 수 있다.// HttpServletRequestSpringExample.java @Controller public class RequestController { @GetMapping("/direct") // /direct 요청 처리 public String direct(HttpServletRequest request, Model model) { String name = request.getParameter("name"); // name 요청값 직접 꺼내기 int age = Integer.parseInt(request.getParameter("age")); // age 요청값을 숫자로 변환 model.addAttribute("result", name + ":" + age); // 모델에 결과 저장 return "result"; // 결과 화면으로 이동 } }// result.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>result</title> </head> <body> <h1 th:text="${result}"></h1> </body> </html>// 출력결과 // 브라우저 주소: /direct?name=lee&age=20 // 화면 출력: lee:20이 코드에서
HttpServletRequest request는 현재 요청 객체를 받는 매개변수이다.
컨트롤러 메서드 안에서는Servlet에서 하던 것처럼request.getParameter()로 값을 직접 꺼낼 수 있다.
다만 화면에 데이터를 넘기는 방식은 달라진다.
Servlet/JSP방식에서는request.setAttribute()를 사용했지만,Spring MVC방식에서는Model에 데이터를 담는다.
그리고return "result"로 뷰 이름을 반환하면Spring이result.html을 찾아 화면을 보여 준다.
두 방식의 차이 정리
Servlet/JSP방식과Spring MVC방식은HttpServletRequest로 요청 객체를 직접 다룰 수 있다는 점은 같다.
하지만 요청 처리 클래스와 화면 연결 방식이 달라진다.
차이는 아래처럼 정리할 수 있다.
Servlet에서는doGet()이나doPost()의 매개변수로HttpServletRequest를 받는다.Spring MVC에서는 컨트롤러 메서드의 매개변수로HttpServletRequest를 받을 수 있다.- 두 방식 모두
request.getParameter()로 요청값을 직접 꺼낼 수 있다.- 두 방식 모두 숫자값은 필요하면 직접 변환해야 한다.
Servlet에서는request.setAttribute()로 화면에 넘길 값을 담는다.Spring MVC에서는Model에 화면에 넘길 값을 담는다.Servlet에서는RequestDispatcher와forward()로 화면 이동을 직접 처리한다.Spring MVC에서는 뷰 이름을 반환하면Spring이 화면을 찾아 준다.따라서
HttpServletRequest는Spring MVC에서 사라진 개념이 아니다.
기존Servlet에서 쓰던 요청 객체를Spring MVC컨트롤러에서도 필요할 때 직접 받을 수 있다.
이 섹션의 핵심은 이것이다.
HttpServletRequest는 요청 객체 자체를 직접 다루는 방식이고, 자동 바인딩으로 처리하기 어려운 요청 정보가 필요할 때 사용할 수 있다.
Model은 컨트롤러가 뷰에 전달할 데이터를 담는 객체이다.
컨트롤러는 요청을 처리한 뒤 화면에 보여 줄 값을 준비해야 한다.
이때 그 값을 화면에 바로 출력하는 것이 아니라,Model이라는 저장 공간에 이름과 값으로 담아서 뷰로 넘긴다.
이 개념은Servlet/JSP에서 배운request.setAttribute()와 비교하면 쉽게 이해할 수 있다.
Servlet/JSP방식에서는request.setAttribute("msg", "안녕하세요")처럼request객체에 데이터를 담고,forward()로JSP에 넘겼다.
Spring MVC에서는model.addAttribute("msg", "안녕하세요")처럼Model에 데이터를 담고, 뷰 이름을 반환해서 화면으로 넘긴다.
즉,Model은 기존Servlet/JSP에서request.setAttribute()로 뷰에 데이터를 넘기던 흐름을Spring MVC방식으로 표현한 객체라고 이해하면 된다.
다만Model은 데이터를 담는 역할이고, 실제 화면 이동은 컨트롤러 메서드가 반환한 뷰 이름을 기준으로Spring이 처리한다.
Model은 무엇을 하는가
Model은 뷰에서 사용할 데이터를 담는 공간이다.
예를 들어 컨트롤러에서"안녕하세요"라는 값을 화면에 보여 주고 싶다면, 그 값을Model에 담아야 한다.
Model에 값을 담을 때는addAttribute()를 사용한다.
예를 들어model.addAttribute("msg", "안녕하세요")라고 작성하면, 뷰에서는msg라는 이름으로"안녕하세요"값을 꺼낼 수 있다.
이때 중요한 점은Model에 값을 담는다고 해서 그 값이 바로 화면에 출력되는 것은 아니라는 점이다.
컨트롤러는Model에 값을 담고, 마지막에 어떤 화면으로 갈지 뷰 이름을 반환한다.
그다음 뷰에서${msg}같은 표현으로Model에 담긴 값을 꺼내 출력한다.
아래 코드는Model에 값을 담아 화면으로 넘기는 가장 기본적인 예제이다.// ModelBasicExample.java @Controller public class ModelBasicController { @GetMapping("/model-basic") // /model-basic 요청 처리 public String modelBasic(Model model) { model.addAttribute("msg", "안녕하세요"); // msg라는 이름으로 값 저장 return "modelResult"; // modelResult.html 화면으로 이동 } }// modelResult.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>modelResult</title> </head> <body> <h1 th:text="${msg}"></h1> </body> </html>// 출력결과 // 브라우저 주소: /model-basic // Model에 저장된 이름: msg // Model에 저장된 값: 안녕하세요 // 화면 출력: 안녕하세요이 흐름을 실제 값 이동으로 보면 아래와 같다.
- 컨트롤러가
"안녕하세요"라는 값을 만든다.model.addAttribute("msg", "안녕하세요")로Model에 저장한다.return "modelResult"로 보여 줄 화면 이름을 반환한다.Spring은modelResult.html을 찾는다.modelResult.html은${msg}로Model에 담긴 값을 꺼낸다.- 화면에는
안녕하세요가 출력된다.따라서
Model은 단순한 변수 하나가 아니다.
컨트롤러와 뷰 사이에서 데이터를 전달하는 저장 공간이다.
Model에 담는 이름과 화면에서 꺼내는 이름은 맞아야 한다
Model에 데이터를 담을 때는 이름과 값을 함께 저장한다.
예를 들어model.addAttribute("msg", "안녕하세요")에서"msg"는 화면에서 사용할 이름이고,"안녕하세요"는 실제 값이다.
뷰에서는 이 이름을 기준으로 값을 꺼낸다.
Thymeleaf에서는${msg}처럼 작성해서Model에 저장된msg값을 읽을 수 있다.
아래 코드를 보면 이름이 어떻게 연결되는지 더 분명하다.// ModelNameMatchExample.java @Controller public class ModelNameMatchController { @GetMapping("/model-name") // /model-name 요청 처리 public String modelName(Model model) { model.addAttribute("userName", "둘리"); // userName이라는 이름으로 값 저장 model.addAttribute("userAge", 10); // userAge라는 이름으로 값 저장 return "userResult"; // userResult.html 화면으로 이동 } }// userResult.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>userResult</title> </head> <body> <p>이름: <span th:text="${userName}"></span></p> <p>나이: <span th:text="${userAge}"></span></p> </body> </html>// 출력결과 // 브라우저 주소: /model-name // userName 값: 둘리 // userAge 값: 10 // 화면 출력: 이름: 둘리 // 화면 출력: 나이: 10이 코드에서
userName이라는 이름으로 저장한 값은 화면에서도${userName}으로 꺼낸다.
userAge라는 이름으로 저장한 값은 화면에서도${userAge}로 꺼낸다.
만약 컨트롤러에서model.addAttribute("userName", "둘리")라고 저장했는데, 화면에서${name}으로 꺼내려고 하면 값이 맞지 않는다.
왜냐하면Model에 저장된 이름은userName이지name이 아니기 때문이다.
정리하면 아래와 같다.
model.addAttribute("userName", "둘리")에서userName은 화면에서 사용할 이름이다."둘리"는 실제로 화면에 출력할 값이다.- 화면에서는
${userName}으로 값을 꺼낸다.Model에 담은 이름과 뷰에서 꺼내는 이름이 맞아야 한다.따라서
Model은 이름과 값을 함께 저장하고, 뷰는 그 이름으로 값을 꺼낸다.
이 기준을 놓치면 컨트롤러에서는 값을 담았는데 화면에는 값이 안 나오는 상황이 생길 수 있다.
Servlet/JSP의 request.setAttribute와 Model의 관계
Servlet/JSP에서는 화면에 데이터를 넘기기 위해request.setAttribute()를 자주 사용했다.
예를 들어request.setAttribute("msg", "안녕하세요")라고 작성하면,JSP에서는${msg}로 값을 출력할 수 있었다.
Spring MVC에서는 이 흐름을Model로 처리한다.
예를 들어model.addAttribute("msg", "안녕하세요")라고 작성하면,Thymeleaf에서는${msg}로 값을 출력할 수 있다.
둘의 역할은 비슷하다.
둘 다 컨트롤러 쪽에서 만든 값을 화면 쪽으로 넘기기 위해 사용한다.
하지만 화면으로 이동하는 방식은 다르다.
Servlet/JSP에서는RequestDispatcher와forward()를 직접 사용한다.
Spring MVC에서는 컨트롤러 메서드가 뷰 이름을 반환하면,Spring이 그 이름에 맞는 화면을 찾아 준다.
아래는 같은 결과를Servlet/JSP방식으로 작성한 예제이다.// ModelServletCompareExample.java @WebServlet("/model") public class ModelServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setAttribute("msg", "Model 사용"); // JSP에 넘길 데이터 저장 RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/result.jsp"); // 결과 JSP 지정 dispatcher.forward(request, response); // JSP로 이동 } }// result.jsp <%@ page contentType="text/html;charset=UTF-8" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>result</title> </head> <body> <h1>${msg}</h1> </body> </html>// 출력결과 // 브라우저 주소: /model // request에 저장된 이름: msg // request에 저장된 값: Model 사용 // 화면 출력: Model 사용같은 흐름을
Spring MVC방식으로 작성하면 아래처럼 바뀐다.// ModelSpringCompareExample.java @Controller public class ModelController { @GetMapping("/model") // /model 요청 처리 public String model(Model model) { model.addAttribute("msg", "Model 사용"); // 화면에 보낼 데이터를 Model에 저장 return "result"; // result.html 화면으로 이동 } }// result.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>result</title> </head> <body> <h1 th:text="${msg}"></h1> </body> </html>// 출력결과 // 브라우저 주소: /model // Model에 저장된 이름: msg // Model에 저장된 값: Model 사용 // 화면 출력: Model 사용두 방식 모두 화면에
Model 사용을 출력한다.
하지만 처리 흐름은 다르다.
Servlet/JSP방식에서는request.setAttribute()로 데이터를 담고,RequestDispatcher와forward()로 직접JSP에 이동한다.
Spring MVC방식에서는model.addAttribute()로 데이터를 담고,return "result"로 뷰 이름을 반환한다.
그러면Spring이result.html을 찾아서 화면을 보여 준다.
차이는 아래처럼 볼 수 있다.
Servlet/JSP에서는request.setAttribute()로 데이터를 담는다.Spring MVC에서는model.addAttribute()로 데이터를 담는다.Servlet/JSP에서는forward()로 직접 화면 이동을 처리한다.Spring MVC에서는 뷰 이름을 반환하면Spring이 화면을 찾아 준다.즉,
Model은 기존request.setAttribute()와 비슷하게 뷰에 데이터를 넘기는 역할을 한다.
다만 화면 이동은Spring MVC의 뷰 처리 흐름과 함께 동작한다.
ModelAndView는 무엇이 다른가
ModelAndView는 이름 그대로 모델 데이터와 뷰 이름을 한 객체에 함께 담는 방식이다.
Model을 사용할 때는 데이터는Model에 담고, 뷰 이름은String으로 따로 반환한다.
반면ModelAndView는 데이터와 뷰 이름을 하나의 객체에 같이 넣어서 반환한다.
예를 들어Model방식에서는 아래처럼 흐름이 나뉜다.
컨트롤러 메서드는Model에 데이터를 담고, 마지막에"result"라는 뷰 이름을String으로 반환한다.
반대로ModelAndView방식에서는ModelAndView객체를 만들고, 그 안에 데이터와 뷰 이름을 모두 넣는다.
즉,Model방식은 데이터와 뷰 이름을 따로 다루고,ModelAndView방식은 데이터와 뷰 이름을 한 객체에 함께 담는다.
먼저Model방식 예제를 다시 보면 아래와 같다.// ModelReturnStringExample.java @Controller public class ModelReturnStringController { @GetMapping("/model-result") // /model-result 요청 처리 public String modelResult(Model model) { model.addAttribute("msg", "Model 방식"); // 데이터는 Model에 저장 return "result"; // 뷰 이름은 String으로 반환 } }// 출력결과 // 브라우저 주소: /model-result // Model에 저장된 값: Model 방식 // 반환한 뷰 이름: result // 화면 출력: Model 방식같은 결과를
ModelAndView방식으로 작성하면 아래처럼 된다.// ModelAndViewBasicExample.java @Controller public class ModelAndViewBasicController { @GetMapping("/mav-result") // /mav-result 요청 처리 public ModelAndView mavResult() { ModelAndView mav = new ModelAndView(); // 데이터와 뷰 이름을 담을 객체 생성 mav.addObject("msg", "ModelAndView 방식"); // 화면에 보낼 데이터 저장 mav.setViewName("result"); // 보여 줄 화면 이름 저장 return mav; // 데이터와 뷰 이름을 함께 반환 } }// 출력결과 // 브라우저 주소: /mav-result // ModelAndView에 저장된 값: ModelAndView 방식 // ModelAndView에 저장된 뷰 이름: result // 화면 출력: ModelAndView 방식이 코드에서는
ModelAndView객체 안에 데이터와 뷰 이름을 모두 담는다.
mav.addObject("msg", "ModelAndView 방식")은 화면에 넘길 데이터를 저장한다.
mav.setViewName("result")는 보여 줄 화면 이름을 저장한다.
마지막에return mav를 하면Spring은ModelAndView안에 들어 있는 뷰 이름을 보고result.html을 찾는다.
그리고result.html에서는${msg}값을 출력한다.
즉,ModelAndView는 데이터와 뷰 이름을 한 번에 묶어 반환하는 방식이다.
Model 방식과 ModelAndView 방식의 값 이동 비교
Model방식과ModelAndView방식은 결과적으로 화면에 데이터를 전달한다는 점은 같다.
하지만 컨트롤러에서 반환하는 형태가 다르다.
Model방식의 값 이동은 아래처럼 볼 수 있다.// Model 방식 값 이동 // 컨트롤러에서 만든 값: "Model 방식" // model.addAttribute("msg", "Model 방식") // return "result" // Spring이 result.html 찾기 // result.html에서 ${msg} 출력
Model방식은 데이터 저장과 뷰 이름 반환이 나뉘어 있다.
데이터는model.addAttribute()에 담고, 화면 이름은return "result"로 따로 반환한다.
ModelAndView방식의 값 이동은 아래처럼 볼 수 있다.// ModelAndView 방식 값 이동 // 컨트롤러에서 만든 값: "ModelAndView 방식" // mav.addObject("msg", "ModelAndView 방식") // mav.setViewName("result") // return mav // Spring이 mav 안의 result를 보고 result.html 찾기 // result.html에서 ${msg} 출력
ModelAndView방식은 데이터와 뷰 이름을mav객체 안에 함께 담는다.
그래서 컨트롤러 메서드의 반환값도String이 아니라ModelAndView가 된다.
차이는 아래처럼 정리할 수 있다.
Model은 데이터만 담고, 뷰 이름은String으로 따로 반환한다.ModelAndView는 데이터와 뷰 이름을 한 객체에 함께 담는다.- 둘 다 결국 뷰에 데이터를 전달하기 위한 도구이다.
Model방식은 코드가 짧고 자주 사용된다.ModelAndView방식은 데이터와 뷰 이름을 한 번에 묶어 반환하고 싶을 때 사용한다.따라서 둘 중 하나만 무조건 맞는 것은 아니다.
컨트롤러가 결과를 어떤 형태로 반환하느냐에 따라 선택해서 사용할 수 있다.
Spring MVC의 ModelAndView 방식
이번에는
ModelAndView방식만 전체 코드로 다시 정리한다.
ModelAndView는 데이터와 뷰 이름을 한 객체에 함께 담아서 반환한다.// ModelAndViewFullExample.java @Controller public class ModelAndViewController { @GetMapping("/mav") // /mav 요청 처리 public ModelAndView mav() { ModelAndView mav = new ModelAndView(); // 데이터와 뷰 이름을 담을 객체 생성 mav.addObject("msg", "ModelAndView 사용"); // 화면에 보낼 데이터 저장 mav.setViewName("result"); // 보여 줄 화면 이름 저장 return mav; // 데이터와 뷰 이름 함께 반환 } }// result.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>result</title> </head> <body> <h1 th:text="${msg}"></h1> </body> </html>// 출력결과 // 브라우저 주소: /mav // 화면 출력: ModelAndView 사용이 코드에서는
ModelAndView객체 안에 데이터와 뷰 이름을 모두 담는다.
mav.addObject("msg", "ModelAndView 사용")은 화면에 넘길 데이터를 저장한다.
mav.setViewName("result")는 보여 줄 화면 이름을 저장한다.
마지막에return mav를 하면Spring은ModelAndView안에 들어 있는 뷰 이름을 보고result.html을 찾는다.
그리고result.html에서${msg}값을 출력한다.
즉,ModelAndView는 데이터와 뷰 이름을 한 번에 묶어 반환하는 방식이다.
세 방식의 차이 정리
Servlet/JSP,Spring MVC의Model,Spring MVC의ModelAndView는 모두 화면에 데이터를 전달한다는 큰 흐름은 같다.
하지만 데이터를 담고 화면으로 이동하는 방식이 다르다.
차이는 아래처럼 정리할 수 있다.
Servlet/JSP에서는request.setAttribute()로 데이터를 담는다.Spring MVC의Model방식에서는model.addAttribute()로 데이터를 담는다.Spring MVC의ModelAndView방식에서는mav.addObject()로 데이터를 담는다.Servlet/JSP에서는RequestDispatcher와forward()로 화면 이동을 직접 처리한다.Model방식에서는 뷰 이름을String으로 반환한다.ModelAndView방식에서는 뷰 이름을ModelAndView객체 안에 담아 반환한다.이 차이를 실제 흐름으로 보면 아래와 같다.
// Servlet/JSP 방식 // request.setAttribute("msg", 값) // RequestDispatcher로 JSP 지정 // forward(request, response) // JSP에서 ${msg} 출력// Spring MVC Model 방식 // model.addAttribute("msg", 값) // return "result" // Spring이 result.html 찾기 // Thymeleaf에서 ${msg} 출력// Spring MVC ModelAndView 방식 // mav.addObject("msg", 값) // mav.setViewName("result") // return mav // Spring이 result.html 찾기 // Thymeleaf에서 ${msg} 출력따라서
Model과ModelAndView는 기존Servlet/JSP의 데이터 전달 흐름을 없앤 것이 아니다.
화면에 데이터를 넘기고 이동하던 흐름을Spring MVC방식으로 더 간단하게 표현한 것이다.
이 섹션의 핵심은 이것이다.
Model은 뷰에 전달할 데이터를 담고 뷰 이름은String으로 따로 반환하는 방식이고,ModelAndView는 데이터와 뷰 이름을 한 객체에 함께 담아 반환하는 방식이다.
@ModelAttribute는 매개변수에만 붙는 어노테이션이 아니다.
메서드 위에 붙이면 요청 처리 메서드가 실행되기 전에Model데이터를 미리 만들어 둘 수 있다.
즉, 컨트롤러가 본격적으로 요청을 처리하기 전에 뷰에서 사용할 값을 먼저 준비하는 방식이다.
앞에서 본 매개변수@ModelAttribute는 요청값을DTO같은 객체에 묶어 받는 역할이었다.
예를 들어 사용자가 보낸name,age,address같은 요청값을UserDTO객체 하나에 넣는 흐름이었다.
하지만 메서드 레벨@ModelAttribute는 역할이 다르다.
메서드 위에@ModelAttribute("data")처럼 붙이면, 이 메서드는 요청값을 받는 용도가 아니라Model에 미리 넣을 데이터를 만드는 용도가 된다.
따라서 같은@ModelAttribute라도 붙는 위치에 따라 역할이 완전히 달라진다.
매개변수에 붙으면 요청값을 객체로 받는 흐름이고, 메서드 위에 붙으면 요청 처리 전에Model데이터를 준비하는 흐름이다.
이 개념도Servlet/JSP에서 배운 흐름과 비교하면 더 쉽게 이해할 수 있다.
Servlet/JSP방식에서는JSP에서 사용할 데이터를 요청 처리 코드 안에서request.setAttribute()로 직접 넣어야 했다.
반면Spring MVC에서는 메서드 위에@ModelAttribute를 붙여 두면, 요청 처리 메서드가 실행되기 전에Spring이 그 메서드를 먼저 실행해서Model데이터를 준비해 준다.
즉, 메서드 레벨@ModelAttribute는 요청 처리 전에 뷰에서 사용할Model데이터를 미리 준비하는 기능이다.
이 기능을 알면 같은 컨트롤러 안에서 반복해서 필요한Model데이터를 매번 직접 넣는 코드를 줄일 수 있다.
매개변수 @ModelAttribute와 메서드 레벨 @ModelAttribute의 차이
@ModelAttribute는 어디에 붙는지에 따라 의미가 달라진다.
매개변수에 붙으면 요청값을 객체에 묶어 받는 역할을 한다.
메서드 위에 붙으면 요청 처리 전에Model데이터를 준비하는 역할을 한다.
먼저 매개변수에 붙는@ModelAttribute를 보면 아래와 같다.// ParameterModelAttributeExample.java @Controller public class UserController { @PostMapping("/user/add") // 회원 등록 요청 처리 public String addUser(@ModelAttribute UserDTO user, Model model) { model.addAttribute("user", user); // 요청값이 담긴 객체를 화면으로 전달 return "userView"; // 결과 화면으로 이동 } }이 코드에서
@ModelAttribute UserDTO user는 사용자가 보낸 요청값을UserDTO객체에 묶어 받는다.
즉, 핵심은 요청값을 객체로 받는 것이다.
반대로 메서드 위에 붙는@ModelAttribute는 의미가 다르다.// MethodModelAttributeExample.java @Controller public class PrepController { @ModelAttribute("data") public String createData() { return "미리 준비된 값"; // Model에 data라는 이름으로 들어갈 값 } @GetMapping("/prep") public String prep() { return "result"; // 결과 화면으로 이동 } }이 코드에서
createData()는/prep요청을 직접 처리하는 메서드가 아니다.
/prep요청과 직접 연결된 메서드는prep()이다.
하지만prep()가 실행되기 전에createData()가 먼저 실행된다.
그리고createData()가 반환한"미리 준비된 값"은data라는 이름으로Model에 들어간다.
차이는 아래처럼 정리할 수 있다.
- 매개변수
@ModelAttribute는 요청값을 객체에 묶어 받는다.- 메서드 레벨
@ModelAttribute는 요청 처리 전에Model데이터를 준비한다.- 매개변수에 붙으면 객체 바인딩 흐름이다.
- 메서드에 붙으면
Model준비 흐름이다.따라서 같은
@ModelAttribute라도 붙는 위치를 먼저 봐야 한다.
붙는 위치를 보지 않고 이름만 보면 두 기능을 헷갈리기 쉽다.
요청 처리 전에 실행된다는 뜻
메서드 레벨
@ModelAttribute의 핵심은 실행 순서이다.
이 메서드는 요청 처리 메서드보다 먼저 실행된다.
그래서 요청 처리 메서드 안에서 직접model.addAttribute()를 하지 않아도, 뷰에서는 미리 준비된Model값을 사용할 수 있다.
예를 들어/prep요청이 들어왔다고 생각하면 된다.
Spring은 바로prep()메서드만 실행하는 것이 아니다.
같은 컨트롤러 안에 메서드 레벨@ModelAttribute가 있으면, 그 메서드를 먼저 실행해서Model에 값을 넣는다.
그다음 실제 요청을 처리하는prep()메서드를 실행한다.
아래 코드로 실행 순서를 보면 더 분명하다.// ModelAttributeOrderExample.java @Controller public class ModelAttributeOrderController { @ModelAttribute("data") public String createData() { return "미리 준비된 값"; // 1. 먼저 실행되어 Model 데이터 준비 } @GetMapping("/prep") public String prep() { return "result"; // 2. 그 다음 요청 처리 메서드 실행 } }// result.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>result</title> </head> <body> <h1 th:text="${data}"></h1> </body> </html>// 출력결과 // 브라우저 주소: /prep // 먼저 실행되는 메서드: createData() // Model에 저장되는 이름: data // Model에 저장되는 값: 미리 준비된 값 // 실제 요청 처리 메서드: prep() // 화면 출력: 미리 준비된 값이 코드에서
prep()안에는Model매개변수가 없다.
또model.addAttribute("data", "...")코드도 없다.
그런데도result.html에서는${data}를 사용할 수 있다.
이유는@ModelAttribute("data")가 붙은createData()가 먼저 실행되어Model에data값을 넣어 두었기 때문이다.
흐름은 아래처럼 볼 수 있다.
- 브라우저가
/prep을 요청한다.Spring이 메서드 레벨@ModelAttribute메서드를 먼저 실행한다.- 반환값을
Model에 저장한다.- 그다음
@GetMapping("/prep")이 붙은 요청 처리 메서드가 실행된다.- 요청 처리 메서드는 뷰 이름을 반환한다.
- 뷰는 미리 준비된
Model값을 출력한다.즉, 메서드 레벨
@ModelAttribute는 요청 처리 전에 화면에서 사용할 재료를 먼저 만들어 두는 준비 단계라고 이해하면 된다.
Servlet/JSP에서는 어떻게 처리했는가
Servlet/JSP방식에서는JSP로 넘길 데이터를 요청 처리 코드 안에서request.setAttribute()로 직접 넣어야 했다.
예를 들어 화면에서 사용할 값을 보여 주려면, 요청을 처리하는Servlet안에서 데이터를 직접 준비하고request에 저장했다.
아래 예제는/prep요청이 들어오면data라는 값을JSP로 넘기는 흐름이다.// ModelAttributeServletExample.java @WebServlet("/prep") public class PrepServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setAttribute("data", "미리 준비된 값"); // JSP에서 사용할 데이터 직접 저장 RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/result.jsp"); // 결과 JSP 지정 dispatcher.forward(request, response); // JSP로 이동 } }// result.jsp <%@ page contentType="text/html;charset=UTF-8" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>result</title> </head> <body> <h1>${data}</h1> </body> </html>// 출력결과 // 브라우저 주소: /prep // 화면 출력: 미리 준비된 값이 방식에서는 요청 처리 메서드 안에서 데이터를 직접 준비한다.
request.setAttribute("data", "미리 준비된 값")으로 값을 저장하고,forward()로JSP에 넘긴다.
즉,Servlet/JSP방식에서는 요청 처리 코드 안에서 화면에 보낼 데이터를 직접 넣는 흐름이다.
반복해서 필요한 데이터가 많아지면request.setAttribute()코드가 여러 요청 처리 코드에 반복될 수 있다.
Spring MVC에서는 어떻게 바뀌는가
Spring MVC에서는 메서드 레벨@ModelAttribute를 사용해서Model데이터를 미리 준비할 수 있다.
요청 처리 메서드 안에서 직접 데이터를 넣지 않아도, 별도의Model준비 메서드가 먼저 실행된다.
아래 예제는/prep요청을 처리하기 전에data라는Model값을 미리 준비하는 코드이다.// ModelAttributeSpringExample.java @Controller public class ModelPrepController { @ModelAttribute("data") public String createData() { return "미리 준비된 값"; // 요청 처리 전에 Model에 들어갈 값 반환 } @GetMapping("/prep") public String prep() { return "result"; // 결과 화면 이름 반환 } }// result.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>result</title> </head> <body> <h1 th:text="${data}"></h1> </body> </html>// 출력결과 // 브라우저 주소: /prep // 화면 출력: 미리 준비된 값이 코드에서
createData()는 요청 처리 메서드가 아니다.
/prep요청과 직접 연결된 메서드는prep()이다.
하지만prep()가 실행되기 전에createData()가 먼저 실행되고, 그 반환값이data라는 이름으로Model에 들어간다.
그래서prep()안에는Model매개변수가 없고,model.addAttribute()코드도 없다.
그런데도result.html에서는${data}를 사용할 수 있다.
이유는@ModelAttribute("data")가 붙은createData()가 미리Model데이터를 준비했기 때문이다.
즉,Spring MVC방식에서는Model준비와 요청 처리 메서드를 분리할 수 있다.
요청 처리 메서드는 어떤 화면으로 갈지만 정하고,Model데이터 준비는 별도의@ModelAttribute메서드가 맡을 수 있다.
여러 요청 처리 메서드에서 공통으로 사용할 수 있다
메서드 레벨
@ModelAttribute는 같은 컨트롤러 안의 요청 처리 메서드가 실행되기 전에 먼저 실행될 수 있다.
그래서 같은 컨트롤러 안에서 여러 화면이 공통으로 필요로 하는 데이터를 준비할 때 사용할 수 있다.
예를 들어 여러 화면에서 공통으로categoryList가 필요하다고 생각하면 된다.
각 요청 처리 메서드마다model.addAttribute("categoryList", ...)를 반복해서 쓰면 코드가 길어진다.
이럴 때 메서드 레벨@ModelAttribute로 공통 데이터를 준비할 수 있다.// CommonModelAttributeExample.java @Controller public class BoardController { @ModelAttribute("categoryList") public List<String> categoryList() { List<String> categories = new ArrayList<>(); // 카테고리 목록 생성 categories.add("공지사항"); // 첫 번째 카테고리 추가 categories.add("자유게시판"); // 두 번째 카테고리 추가 categories.add("질문게시판"); // 세 번째 카테고리 추가 return categories; // categoryList라는 이름으로 Model에 저장 } @GetMapping("/board/write") public String writeForm() { return "boardWrite"; // 글쓰기 화면으로 이동 } @GetMapping("/board/edit") public String editForm() { return "boardEdit"; // 수정 화면으로 이동 } }// 출력결과 // 브라우저 주소: /board/write // 먼저 실행되는 메서드: categoryList() // Model에 저장되는 이름: categoryList // 이동 화면: boardWrite.html // 브라우저 주소: /board/edit // 먼저 실행되는 메서드: categoryList() // Model에 저장되는 이름: categoryList // 이동 화면: boardEdit.html이 예제에서
writeForm()과editForm()은 직접Model에categoryList를 넣지 않는다.
하지만 같은 컨트롤러 안에@ModelAttribute("categoryList")가 붙은 메서드가 있기 때문에 요청 처리 전에 카테고리 목록이 준비될 수 있다.
이런 구조는 여러 요청에서 공통으로 필요한 화면 데이터를 준비할 때 유용하다.
다만 모든 데이터를 무조건 메서드 레벨@ModelAttribute로 빼는 것은 아니다.
특정 요청에서만 필요한 데이터라면 해당 요청 처리 메서드 안에서model.addAttribute()로 넣는 편이 더 명확할 수 있다.
여러 타입의 데이터도 미리 준비할 수 있다
메서드 레벨
@ModelAttribute는 문자열만Model에 넣는 기능이 아니다.
배열, 객체, 날짜, 리스트 같은 값도 미리 준비할 수 있다.
즉, 뷰에서 사용할 다양한 타입의 데이터를 요청 처리 전에Model에 넣어 둘 수 있다.
아래 코드는 문자열, 배열, 객체, 날짜, 리스트를Model에 미리 넣는 예제이다.// MyVO.java public class MyVO { private int number; // 숫자 값 저장 private String color; // 색상 값 저장 public MyVO(int number, String color) { this.number = number; // 숫자 값 저장 this.color = color; // 색상 값 저장 } public int getNumber() { return number; // 숫자 값 반환 } public String getColor() { return color; // 색상 값 반환 } }// MultipleModelAttributeExample.java @Controller public class ModelPrepMultiController { @ModelAttribute("v1") public String createString() { return "테스트!!"; // 문자열 Model 준비 } @ModelAttribute("v2") public int[] createArray() { return new int[] {10, 20, 30, 40, 50}; // 배열 Model 준비 } @ModelAttribute("v3") public MyVO createVO() { return new MyVO(23, "yellow"); // 객체 Model 준비 } @ModelAttribute("v4") public Date createDate() { return new Date(); // 날짜 Model 준비 } @ModelAttribute("v5") public List<MyVO> createList() { List<MyVO> list = new ArrayList<MyVO>(); // 리스트 생성 list.add(new MyVO(7, "red")); // 첫 번째 객체 추가 list.add(new MyVO(11, "pink")); // 두 번째 객체 추가 return list; // 리스트 Model 준비 } @RequestMapping("/modeltest1") public String handle() { return "modelResult1"; // 미리 준비한 Model을 보여 줄 화면으로 이동 } }// modelResult1.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>modelResult1</title> </head> <body> <p th:text="${v1}"></p> <p th:text="${v2[0]}"></p> <p th:text="${v3.color}"></p> <p th:text="${v4}"></p> <p th:text="${v5[0].color}"></p> </body> </html>// 출력결과 // 브라우저 주소: /modeltest1 // 화면 출력: 테스트!! // 화면 출력: 10 // 화면 출력: yellow // 화면 출력: 실행 시점의 날짜와 시간 // 화면 출력: red이 코드에서
handle()은Model데이터를 직접 만들지 않는다.
그런데도 뷰에서는v1,v2,v3,v4,v5를 사용할 수 있다.
요청 처리 전에 각각의@ModelAttribute메서드가 먼저 실행되어Model에 값을 넣기 때문이다.
v1은 문자열이므로 그대로 출력된다.
v2는 배열이므로v2[0]처럼 첫 번째 값을 꺼낼 수 있다.
v3는MyVO객체이므로v3.color처럼 객체의 값을 꺼낼 수 있다.
v4는 실행 시점에 만들어진 날짜 객체이므로 실행할 때마다 화면에 보이는 값이 달라질 수 있다.
v5는MyVO객체가 들어 있는 리스트이므로v5[0].color처럼 첫 번째 객체의 색상 값을 꺼낼 수 있다.
정리하면 아래와 같다.
createString()은 문자열을v1로 준비한다.createArray()는 배열을v2로 준비한다.createVO()는 객체를v3로 준비한다.createDate()는 날짜를v4로 준비한다.createList()는 리스트를v5로 준비한다.handle()은Model을 직접 만들지 않고 뷰 이름만 반환한다.따라서 메서드 레벨
@ModelAttribute는 뷰에서 사용할 여러 종류의Model데이터를 요청 처리 전에 미리 준비하는 기능이다.
언제 메서드 레벨 ModelAttribute를 쓰면 좋은가
메서드 레벨
@ModelAttribute는 모든 상황에서 무조건 쓰는 기능이 아니다.
요청 처리 메서드 안에model.addAttribute()를 쓰는 것이 더 명확한 경우도 있다.
이 기능은 보통 같은 컨트롤러 안에서 여러 요청이 공통으로 사용하는 데이터가 있을 때 유용하다.
예를 들어 여러 화면에서 카테고리 목록, 선택 옵션 목록, 공통 안내 문구처럼 반복해서 필요한 데이터가 있다면 메서드 레벨@ModelAttribute로 미리 준비할 수 있다.
반대로 특정 요청에서만 필요한 데이터라면 해당 요청 처리 메서드 안에서model.addAttribute()로 직접 넣는 것이 더 읽기 쉽다.
모든 데이터를 메서드 레벨@ModelAttribute로 분리하면 오히려 어느 요청에서 어떤 데이터가 필요한지 찾기 어려워질 수 있다.
정리하면 아래와 같다.
- 여러 요청에서 공통으로 필요한 데이터라면 메서드 레벨
@ModelAttribute가 유용하다.- 한 요청에서만 필요한 데이터라면 요청 처리 메서드 안의
model.addAttribute()가 더 명확할 수 있다.- 메서드 레벨
@ModelAttribute는 요청 처리 전에 실행되므로 실행 순서를 이해해야 한다.- 같은 컨트롤러 안에서 반복되는
Model준비 코드를 줄일 수 있다.따라서 메서드 레벨
@ModelAttribute는 공통Model데이터를 미리 준비할 때 사용하는 기능으로 이해하면 된다.
두 방식의 차이 정리
Servlet/JSP방식과Spring MVC방식은 뷰에서 사용할 데이터를 준비한다는 큰 흐름은 같다.
하지만 데이터를 준비하는 위치와 방식이 달라진다.
차이는 아래처럼 정리할 수 있다.
Servlet/JSP에서는 요청 처리 메서드 안에서request.setAttribute()로 데이터를 직접 넣는다.Spring MVC에서는 메서드 레벨@ModelAttribute로 요청 처리 전에 데이터를 미리 넣을 수 있다.Servlet/JSP에서는 반복 데이터가 많아지면setAttribute()코드가 여러 요청 처리 코드에 반복될 수 있다.Spring MVC에서는 같은 컨트롤러 안에서Model준비 메서드를 분리해서 반복을 줄일 수 있다.Servlet/JSP에서는 데이터 준비와 요청 처리가 한 메서드 안에 섞이기 쉽다.Spring MVC에서는Model준비와 요청 처리를 나누어 볼 수 있다.따라서 메서드 레벨
@ModelAttribute는 기존 데이터 전달 흐름을 없앤 것이 아니다.
뷰에서 사용할 데이터를 요청 처리 전에 미리 준비하도록 분리해 주는Spring MVC방식이다.
이 섹션의 핵심은 이것이다.
메서드 레벨@ModelAttribute는 요청 처리 메서드가 실행되기 전에Model데이터를 미리 준비하고, 요청 처리 메서드는 그 데이터를 사용하는 뷰로 흐름을 연결한다.
@SessionAttributes는Model속성 중 일부를 세션에 유지하는 어노테이션이다.
보통Model에 담긴 값은 요청 하나가 처리되는 동안만 사용된다.
하지만@SessionAttributes("data1")처럼 지정하면data1이라는 이름의Model속성은 세션에도 저장되어 다음 요청에서도 이어서 사용할 수 있다.
여기서 세션은 사용자별로 서버에 유지되는 저장 공간이다.
한 번의 요청이 끝나도 바로 사라지지 않고, 같은 사용자의 다음 요청에서도 값을 꺼내 쓸 수 있다.
따라서 여러 단계의 입력 과정, 장바구니, 값 누적처럼 이전 요청의 데이터를 다음 요청에서도 계속 써야 할 때 세션이 필요하다.
이 개념은Servlet/JSP에서 배운HttpSession과 연결해서 보면 더 쉽다.
Servlet/JSP방식에서는request.getSession()으로 세션 객체를 직접 꺼낸 뒤,session.setAttribute()로 값을 저장했다.
반면Spring MVC의@SessionAttributes는 개발자가 세션에 직접 넣는 방식이 아니라,Model에 담긴 값 중 지정한 이름만 세션에 유지하도록Spring에게 알려 주는 방식이다.
즉,@SessionAttributes는 세션 저장소 전체를 직접 다루는 기능이 아니다.
Model속성 중 일부를 세션과 연결해서 여러 요청 동안 유지하는 기능이다.
일반 Model 값과 세션 값의 차이
Model은 원래 컨트롤러가 뷰에 데이터를 전달할 때 사용하는 객체이다.
예를 들어model.addAttribute("msg", "안녕하세요")라고 하면 현재 요청의 결과 화면에서${msg}로 값을 출력할 수 있다.
하지만 일반Model값은 기본적으로 요청 하나에서 사용되는 값이다.
첫 번째 요청에서Model에 값을 담았다고 해서, 두 번째 요청에서도 그 값이 자동으로 남아 있는 것은 아니다.
아래 예제는 일반Model에 값을 담는 흐름이다.// NormalModelExample.java @Controller public class NormalModelController { @GetMapping("/normal-model") // /normal-model 요청 처리 public String normalModel(Model model) { model.addAttribute("data1", "요청 하나에서만 사용하는 값"); // 현재 요청의 화면에 넘길 값 저장 return "normalResult"; // normalResult.html 화면으로 이동 } }// 출력결과 // 브라우저 주소: /normal-model // Model에 저장된 이름: data1 // Model에 저장된 값: 요청 하나에서만 사용하는 값 // 화면 출력: 요청 하나에서만 사용하는 값이 값은
/normal-model요청을 처리하고 결과 화면을 보여 줄 때 사용된다.
하지만 다음 요청에서도 자동으로 이어지는 값은 아니다.
반대로 세션에 저장된 값은 같은 사용자의 여러 요청 동안 유지될 수 있다.
예를 들어 첫 번째 요청에서java를 저장하고, 두 번째 요청에서spring을 이어 붙이면javaspring처럼 누적된 값을 만들 수 있다.
정리하면 아래와 같다.
- 일반
Model값은 현재 요청의 결과 화면에 데이터를 넘기는 용도이다.- 일반
Model값은 다음 요청에 자동으로 이어지지 않는다.- 세션 값은 같은 사용자의 여러 요청 동안 유지될 수 있다.
@SessionAttributes는 지정한Model속성을 세션에도 유지하게 만든다.따라서 한 번 보여 주고 끝나는 값은 일반
Model로 충분하고, 다음 요청에서도 이어져야 하는 값은 세션 유지가 필요하다.
모델 속성이 세션에 유지된다는 것은 무엇인가
@SessionAttributes("data1")라고 쓰면data1이라는 이름의Model속성이 세션 유지 대상이 된다.
즉,Model에data1이라는 이름으로 들어간 값이 세션에도 보관되어 다음 요청에서도 다시 사용할 수 있다.
여기서 중요한 점은 모든Model속성이 세션에 들어가는 것이 아니라는 점이다.
@SessionAttributes("data1")라고 지정했다면data1만 세션 유지 대상이다.
data2,msg,result같은 다른Model속성은 따로 지정하지 않으면 일반Model속성처럼 요청이 끝나면 사라지는 값으로 보면 된다.
아래처럼 이해하면 쉽다.// SessionAttributeNameExample.java @Controller @SessionAttributes("data1") // data1이라는 Model 속성만 세션 유지 대상 public class SessionAttributeNameController { @GetMapping("/session-name") public String sessionName(Model model) { model.addAttribute("data1", "세션에 유지될 수 있는 값"); // 세션 유지 대상 model.addAttribute("data2", "현재 요청에서만 사용할 값"); // 일반 Model 값 return "sessionNameResult"; // 결과 화면으로 이동 } }// 출력결과 // data1: @SessionAttributes("data1") 대상이므로 세션에 유지될 수 있음 // data2: 지정 대상이 아니므로 일반 Model 값으로 사용됨이 코드에서
data1과data2는 모두Model에 들어간다.
하지만 세션 유지 대상으로 지정된 이름은data1뿐이다.
그래서data1은 세션에도 유지될 수 있고,data2는 일반Model값처럼 현재 요청에서 사용하는 값으로 이해하면 된다.
정리하면 아래와 같다.
- 일반
Model속성은 요청 하나가 끝나면 사라진다.@SessionAttributes로 지정한Model속성은 세션에도 저장된다.- 세션에 저장된 값은 같은 사용자의 다음 요청에서도 이어질 수 있다.
- 모든
Model속성이 아니라, 지정한 이름의Model속성만 세션에 유지된다.따라서
@SessionAttributes를 볼 때는 어떤Model이름이 세션 유지 대상으로 지정되었는지를 먼저 확인해야 한다.
StringBuffer와 append는 왜 사용하는가
StringBuffer는 문자열을 계속 이어 붙일 수 있는 객체이다.
일반String은 값을 붙일 때 기존 문자열을 직접 바꾸는 구조라기보다, 새 문자열을 다시 만들어 변수에 넣는 흐름에 가깝다.
반면StringBuffer는 같은 객체 안에 문자열을 계속 누적할 수 있다.
이 예제에서는 요청이 여러 번 들어올 때마다 기존 값 뒤에 새 문자열을 붙여야 한다.
그래서StringBuffer를 사용한다.
그리고 기존 문자열 뒤에 값을 붙일 때는append()메서드를 사용한다.
아래처럼 생각하면 된다.// StringBufferAppendExample.java StringBuffer data1 = new StringBuffer(); // 문자열을 누적할 객체 생성 data1.append("java"); // 기존 값 뒤에 java 추가 data1.append("spring"); // 기존 값 뒤에 spring 추가// 출력결과 // data1 값: javaspring첫 번째로
append("java")를 실행하면data1안에는java가 들어간다.
그 다음append("spring")을 실행하면 기존 값 뒤에spring이 붙는다.
그래서 최종 값은javaspring이 된다.
이 흐름이 세션 예제와 연결된다.
첫 번째 요청이/session?str=java이면data1에java가 붙는다.
두 번째 요청이/session?str=spring이면 세션에 유지되던data1뒤에spring이 붙는다.
그래서 화면에는javaspring이 출력된다.
정리하면 아래와 같다.
String은 일반 문자열이다.StringBuffer는 문자열을 계속 수정하거나 이어 붙이기 위한 객체이다.append()는 기존 문자열 뒤에 새 값을 붙인다.- 이 예제에서는 세션에 남아 있는 값에
java,spring,boot같은 요청값을 계속 누적하려고StringBuffer를 사용한다.따라서 이 예제에서
StringBuffer를 쓰는 이유는 요청이 여러 번 들어올 때마다 문자열을 새로 덮어쓰는 것이 아니라, 기존 값 뒤에 계속 누적하기 위해서이다.
HttpSession 직접 사용과 @SessionAttributes의 차이
HttpSession을 직접 사용하면 개발자가 세션 저장소 자체를 직접 다룬다.
예를 들어session.setAttribute("data1", value)처럼 세션에 값을 직접 저장하고,session.getAttribute("data1")로 직접 꺼낸다.
반면@SessionAttributes는Model속성을 세션에 연결하는 방식이다.
개발자가 처음부터session.setAttribute()를 호출하는 것이 아니라,Model에 들어간 특정 이름의 값이 세션에도 유지되도록 지정한다.
즉, 출발점이 세션이 아니라Model이다.
아래는HttpSession을 직접 사용하는 방식이다.// HttpSessionDirectExample.java @Controller public class HttpSessionDirectController { @GetMapping("/session-direct") // /session-direct?str=java 요청 처리 public String sessionDirect(@RequestParam(value = "str", required = false) String str, HttpSession session, Model model) { StringBuffer data1 = (StringBuffer) session.getAttribute("data1"); // 세션에서 직접 값 꺼내기 if (data1 == null) { data1 = new StringBuffer(); // 처음 요청이면 새 객체 생성 session.setAttribute("data1", data1); // 세션에 직접 저장 } if (str != null) { data1.append(str); // 기존 값 뒤에 새 문자열 누적 } model.addAttribute("data1", data1); // 화면에 출력할 값 저장 return "sessionResult"; // 결과 화면으로 이동 } }이 방식은 세션 객체를 직접 다룬다.
session.getAttribute("data1")로 값을 직접 꺼내고, 값이 없으면session.setAttribute("data1", data1)로 직접 저장한다.
반면@SessionAttributes방식은 아래처럼Model속성 이름을 세션 유지 대상으로 지정한다.// SessionAttributesBasicExample.java @Controller @SessionAttributes("data1") public class SessionAttributesBasicController { @ModelAttribute("data1") public StringBuffer createData1() { return new StringBuffer(); // data1 Model 속성의 초기값 준비 } @GetMapping("/session-attr") // /session-attr?str=java 요청 처리 public String sessionAttr(@ModelAttribute("data1") StringBuffer data1, @RequestParam(value = "str", required = false) String str, Model model) { if (str != null) { data1.append(str); // 세션에 유지되는 Model 속성에 새 문자열 누적 } model.addAttribute("data1", data1); // 화면에 출력할 값 저장 return "sessionResult"; // 결과 화면으로 이동 } }이 방식에서는 개발자가
session.setAttribute()를 직접 호출하지 않는다.
대신@SessionAttributes("data1")로data1이라는Model속성을 세션에 유지하겠다고 지정한다.
차이는 아래처럼 보면 쉽다.
HttpSession직접 사용은 세션 저장소에 직접 값을 넣고 뺀다.@SessionAttributes는Model속성 중 지정한 이름을 세션에 유지한다.HttpSession은 세션 자체를 직접 다루는 방식이다.@SessionAttributes는Model과 세션을 연결하는 방식이다.따라서 둘은 모두 세션을 사용할 수 있지만 접근 방식이 다르다.
@SessionAttributes는Model에서 시작한 값을 세션에 유지하는 구조라고 이해해야 한다.
SessionStatus는 언제 사용하는가
@SessionAttributes로 유지한 값은 작업이 끝나면 정리해야 한다.
그대로 두면 같은 세션 안에서 계속 남아 있어서 다음 요청에도 영향을 줄 수 있다.
예를 들어 입력 단계가 끝났는데도 이전 입력값이 계속 남아 있으면 화면이나 처리 결과가 헷갈릴 수 있다.
이때 사용하는 것이SessionStatus이다.
SessionStatus의setComplete()를 호출하면@SessionAttributes로 유지하던Model속성의 세션 유지 흐름을 끝낼 수 있다.
주의할 점은setComplete()가 세션 전체를 비우는 기능은 아니라는 점이다.
세션에 저장된 모든 값을 지우는 것이 아니라, 해당 컨트롤러에서@SessionAttributes로 관리하던 속성의 유지 상태를 끝내는 쪽에 가깝다.
아래 코드는 세션 유지 흐름을 끝내는 예제이다.// SessionStatusClearExample.java @Controller @SessionAttributes("data1") public class SessionStatusClearController { @ModelAttribute("data1") public StringBuffer createData1() { return new StringBuffer(); // 세션에 유지될 Model 속성 초기값 } @GetMapping("/session/clear") public String clear(SessionStatus status) { status.setComplete(); // @SessionAttributes로 유지하던 값 정리 return "sessionClear"; // 정리 결과 화면으로 이동 } }// 출력결과 // 요청 주소: /session/clear // 실행 코드: status.setComplete() // 결과: @SessionAttributes로 유지하던 data1 유지 흐름 종료
status.setComplete()는@SessionAttributes로 관리하던data1의 유지 흐름을 끝낸다.
그래서 이후 다시data1이 필요하면 새로 준비되는 흐름으로 이어질 수 있다.
정리하면 아래와 같다.
@SessionAttributes는 지정한Model속성을 세션에 유지한다.- 작업이 끝나면
SessionStatus#setComplete()로 유지 흐름을 끝낼 수 있다.setComplete()는 세션 전체 삭제가 아니다.@SessionAttributes로 관리하던Model속성 정리에 가깝다.따라서
@SessionAttributes를 사용하면 값 유지뿐 아니라 언제 정리할지도 함께 생각해야 한다.
Servlet/JSP에서는 어떻게 처리했는가
Servlet/JSP방식에서는 세션 값을 직접 저장하고 직접 꺼냈다.
아래 예제는/session?str=java처럼 요청할 때마다 세션에 저장된 문자열 뒤에 새 값을 이어 붙이는 흐름이다.
같은 프로젝트에 아래 코드와 뒤의Spring MVC코드를 동시에 넣어 비교하라는 뜻이 아니다.
세션 값을 직접 다루는 방식과@SessionAttributes를 사용하는 방식을 비교하기 위한 예제이다.// SessionServletExample.java @WebServlet("/session") public class SessionServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(); // 세션 객체 직접 꺼내기 StringBuffer data1 = (StringBuffer) session.getAttribute("data1"); // 세션 값 직접 꺼내기 if (data1 == null) { data1 = new StringBuffer(); // 처음 요청이면 새 객체 생성 session.setAttribute("data1", data1); // 세션에 직접 저장 } String str = request.getParameter("str"); // 요청값 직접 꺼내기 if (str != null) { data1.append(str); // 기존 값 뒤에 새 값 누적 } request.setAttribute("data1", data1); // JSP에서 출력할 값 저장 RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/sessionResult.jsp"); // 결과 JSP 지정 dispatcher.forward(request, response); // JSP로 이동 } }// sessionResult.jsp <%@ page contentType="text/html;charset=UTF-8" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>sessionResult</title> </head> <body> <h1>${data1}</h1> </body> </html>// 출력결과 // 첫 번째 요청: /session?str=java // 세션에 저장된 data1 기존 값: 없음 // 새 StringBuffer 생성 후 java 누적 // 화면 출력: java // 두 번째 요청: /session?str=spring // 세션에 저장된 data1 기존 값: java // 기존 값 뒤에 spring 누적 // 화면 출력: javaspring이 방식에서는 개발자가 세션 객체를 직접 꺼낸다.
그리고session.getAttribute("data1")로 기존 값을 확인하고, 값이 없으면new StringBuffer()로 새 객체를 만든 뒤session.setAttribute()로 직접 저장한다.
그다음 요청 파라미터str을 꺼내서 기존 문자열 뒤에 붙인다.
첫 번째 요청에서는 기존 값이 없으므로java가 된다.
두 번째 요청에서는 기존 값java가 세션에 남아 있으므로spring이 뒤에 붙어javaspring이 된다.
즉,Servlet/JSP방식에서는 세션 객체 가져오기 → 세션 값 확인 → 값 생성 → 세션 저장 → 값 누적 → 화면 전달을 개발자가 직접 코드로 작성한다.
Spring MVC에서는 어떻게 바뀌는가
Spring MVC에서는@SessionAttributes를 사용해서Model속성 일부를 세션에 유지할 수 있다.
아래 예제는data1이라는Model속성을 세션에 유지하고, 요청이 반복될 때마다 문자열을 누적하는 흐름이다.// SessionAttributesSpringExample.java @Controller @SessionAttributes("data1") public class SessionController { @ModelAttribute("data1") public StringBuffer createData1() { return new StringBuffer(); // 세션에 유지될 Model 데이터 준비 } @GetMapping("/session") public String session(@ModelAttribute("data1") StringBuffer data1, @RequestParam(value = "str", required = false) String str, Model model) { if (str != null) { data1.append(str); // 세션에 유지되는 값에 새 문자열 누적 } model.addAttribute("data1", data1); // 화면에 출력할 값 저장 return "sessionResult"; // 결과 화면으로 이동 } @GetMapping("/session/clear") public String clear(SessionStatus status) { status.setComplete(); // @SessionAttributes로 유지하던 값 정리 return "sessionClear"; // 정리 결과 화면으로 이동 } }// sessionResult.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>sessionResult</title> </head> <body> <h1 th:text="${data1}"></h1> </body> </html>// sessionClear.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>sessionClear</title> </head> <body> <h1>세션 유지 흐름 정리 완료</h1> </body> </html>// 출력결과 // 첫 번째 요청: /session?str=java // data1 기존 값: 빈 StringBuffer // java 누적 // 화면 출력: java // 두 번째 요청: /session?str=spring // data1 기존 값: java // spring 누적 // 화면 출력: javaspring // 정리 요청: /session/clear // status.setComplete() 실행 // 화면 출력: 세션 유지 흐름 정리 완료 // 다시 요청: /session?str=boot // data1 새로 준비 // boot 누적 // 화면 출력: boot이 코드에서
@SessionAttributes("data1")는data1이라는Model속성을 세션에 유지하겠다는 뜻이다.
@ModelAttribute("data1")가 붙은createData1()은data1Model속성을 미리 준비한다.
그리고session()메서드에서@ModelAttribute("data1") StringBuffer data1로 그 값을 다시 받아 사용한다.
첫 번째 요청에서str=java가 들어오면data1에는java가 누적된다.
두 번째 요청에서str=spring이 들어오면 기존 값이 유지되어 있으므로javaspring이 된다.
StringBuffer의append()는 기존 문자열 뒤에 새 문자열을 붙이기 때문이다.
/session/clear를 요청하면SessionStatus#setComplete()가 실행된다.
이때@SessionAttributes로 유지하던data1의 세션 유지 흐름이 끝난다.
그래서 다시/session?str=boot를 요청하면 이전의javaspring이 이어지지 않고 새로 시작되어boot만 출력된다.
요청 범위와 세션 범위의 차이
이 예제에서 중요한 차이는 요청 범위와 세션 범위이다.
요청 범위는 요청 하나가 처리되는 동안만 값이 유지되는 범위이다.
세션 범위는 같은 사용자의 여러 요청 동안 값이 유지되는 범위이다.
일반Model속성은 요청 범위에 가깝다.
한 번 화면에 넘기고 나면 다음 요청에 자동으로 이어지지 않는다.
반면@SessionAttributes로 지정한Model속성은 세션 범위에 저장되어 다음 요청에서도 이어질 수 있다.
이 차이는 아래처럼 정리할 수 있다.
- 요청 범위 값은 요청 하나가 끝나면 사라진다.
- 세션 범위 값은 같은 사용자의 다음 요청에서도 유지될 수 있다.
- 일반
Model속성은 기본적으로 요청 범위로 이해하면 된다.@SessionAttributes로 지정한Model속성은 세션 범위에도 유지된다.따라서 값이 누적되거나 다음 단계로 이어져야 한다면 세션 유지가 필요하다.
반대로 한 화면에서만 쓰고 끝나는 값이라면 일반Model속성으로 충분하다.
언제 @SessionAttributes를 쓰면 좋은가
@SessionAttributes는 모든 세션 처리에 무조건 사용하는 기능이 아니다.
Model에 담긴 값 중 일부를 여러 요청 동안 유지해야 할 때 사용한다.
예를 들어 여러 단계로 나누어 입력을 받는 화면이 있다고 생각하면 된다.
첫 번째 화면에서 입력한 값을 두 번째 화면에서도 사용해야 하고, 마지막 완료 화면까지 이어가야 한다면 세션 유지가 필요할 수 있다.
이때@SessionAttributes는Model속성을 세션에 유지하는 방식으로 사용할 수 있다.
반대로 로그인 사용자 정보처럼 세션 전체에서 직접 관리하는 값은HttpSession을 직접 사용하는 방식이 더 명확할 수 있다.
즉,@SessionAttributes는HttpSession을 완전히 대신하는 기능이라기보다, 컨트롤러의Model속성을 세션과 연결하는 기능으로 이해하는 것이 좋다.
정리하면 아래와 같다.
Model에 담은 값을 여러 요청 동안 이어가고 싶을 때@SessionAttributes를 사용할 수 있다.- 여러 단계 입력 화면처럼 중간 데이터를 유지해야 할 때 유용할 수 있다.
- 세션 저장소 자체를 직접 다뤄야 한다면
HttpSession직접 사용이 더 명확할 수 있다.@SessionAttributes를 사용한 값은 작업이 끝났을 때SessionStatus#setComplete()로 정리해야 한다.따라서
@SessionAttributes는Model속성을 임시로 세션에 유지해야 하는 흐름에서 사용하고, 끝나면 반드시 정리까지 생각해야 한다.
두 방식의 차이 정리
Servlet/JSP방식과Spring MVC방식은 세션에 값을 유지한다는 큰 흐름은 같다.
하지만 값을 세션에 연결하는 방식이 다르다.
차이는 아래처럼 정리할 수 있다.
Servlet/JSP에서는request.getSession()으로 세션 객체를 직접 꺼낸다.Servlet/JSP에서는session.setAttribute()로 세션에 직접 값을 저장한다.Spring MVC에서는@SessionAttributes로 특정Model속성을 세션에 유지하도록 지정한다.Spring MVC에서는 메서드 레벨@ModelAttribute로 세션에 유지될Model값을 먼저 준비할 수 있다.Spring MVC에서는 매개변수@ModelAttribute로 세션에 유지되던Model값을 다시 받아 사용할 수 있다.SessionStatus#setComplete()는@SessionAttributes로 유지하던 흐름을 끝낼 때 사용한다.따라서
@SessionAttributes는 기존 세션 기능을 없앤 것이 아니다.
Model속성 중 일부를 세션과 연결해서 여러 요청 동안 유지하고, 필요할 때SessionStatus로 정리하는Spring MVC방식이다.
이 섹션의 핵심은 이것이다.
@SessionAttributes는 지정한Model속성을 세션에 유지하고,SessionStatus#setComplete()는 그 유지 흐름을 끝내는 역할을 한다.
@RequestBody와@ResponseBody는 요청과 응답의 본문을 다룰 때 사용하는 어노테이션이다.
여기서 본문은HTTP Body를 의미한다.
HTTP Body는 요청이나 응답에서 실제 데이터가 담기는 부분이다.
앞에서 배운@RequestParam,@ModelAttribute,@PathVariable은 주로URL, 폼 데이터, 주소 경로에 있는 값을 받는 방식이었다.
반면@RequestBody는JSON처럼 요청 본문에 담긴 데이터를 읽을 때 사용한다.
그리고@ResponseBody는 컨트롤러 메서드의 반환값을 뷰 이름으로 보지 않고, 응답 본문에 그대로 담아 보낼 때 사용한다.
이 구간에서 가장 먼저 구분해야 할 점이 있다.
일반@Controller는 보통 화면으로 이동하는 흐름에서 사용한다.
예를 들어return "result"라고 하면result.html같은 화면을 찾는 흐름이다.
하지만@ResponseBody나@RestController를 사용하면 반환값은 화면 이름이 아니라 응답 데이터가 된다.
즉, 같은return "result"라도 어떤 어노테이션이 붙었는지에 따라 화면 이름이 될 수도 있고, 응답 본문 데이터가 될 수도 있다.
이 개념은Servlet/JSP와 비교하면 더 쉽게 이해할 수 있다.
Servlet에서는 요청 본문을 읽으려면request.getReader()같은 방식으로 직접 읽어야 했다.
응답 본문을 보내려면response.getWriter()로 직접 출력해야 했다.
Spring MVC에서는@RequestBody,@ResponseBody,@RestController를 사용해서 요청 본문 읽기와 응답 본문 생성을 더 간단하게 처리할 수 있다.
즉, 요청 본문과 응답 본문을 다루는 방식은 화면 이동 중심이 아니라 데이터 교환 중심의 처리 방식이다.
일반 화면을 보여 주는 컨트롤러와 데이터를 바로 주고받는 컨트롤러를 구분해서 이해해야 한다.
요청 본문은 무엇을 읽는가
@RequestBody는 요청 본문에 담긴 데이터를 읽을 때 사용한다.
예를 들어 클라이언트가JSON데이터를 서버로 보냈다면, 그JSON은 보통HTTP Body안에 들어 있다.
이때@RequestBody를 사용하면Spring이 요청 본문을 읽어서Java객체로 바꿔 줄 수 있다.
여기서 중요한 점은 값이 들어 있는 위치이다.
@RequestParam은/search?keyword=java처럼URL뒤의 쿼리 문자열에서 값을 받는다.
@ModelAttribute는 주로 폼에서 전송된 값을 객체에 묶어 받는다.
@PathVariable은/boards/10처럼 주소 경로 안에 들어 있는 값을 받는다.
반면@RequestBody는HTTP Body안의 데이터를 읽는다.
차이는 아래처럼 정리할 수 있다.
@RequestParam은URL뒤의 쿼리 문자열 값을 받는다.@ModelAttribute는 폼에서 넘어온 값을 객체에 묶어 받는 흐름에서 자주 사용한다.@PathVariable은 주소 경로 안의 값을 받는다.@RequestBody는 요청 본문에 담긴 데이터를 읽는다.따라서
@RequestBody는 단순히 요청값을 받는 또 다른 문법이 아니다.
값이URL이나 폼 파라미터가 아니라 요청 본문에 들어 있을 때 사용하는 방식이다.
@RequestParam과 @RequestBody의 차이
@RequestParam과@RequestBody는 둘 다 클라이언트가 보낸 값을 컨트롤러에서 받는 방식이다.
하지만 값을 읽는 위치가 다르다.
먼저@RequestParam은URL뒤의 쿼리 문자열에서 값을 읽는다.
예를 들어/api/member?name=lee&age=20처럼 요청하면name,age값을 매개변수로 받을 수 있다.// RequestParamBodyCompareExample.java @Controller public class RequestParamBodyCompareController { @GetMapping("/api/member-param") // /api/member-param?name=lee&age=20 요청 처리 @ResponseBody public String memberParam(@RequestParam("name") String name, @RequestParam("age") int age) { return name + ":" + age; // 응답 본문으로 문자열 반환 } }// 출력결과 // 요청 방식: GET // 브라우저 주소: /api/member-param?name=lee&age=20 // name 값: lee // age 값: 20 // 응답 본문: lee:20이 방식에서는 값이
URL뒤에 보인다.
name=lee,age=20은 요청 본문이 아니라 쿼리 문자열이다.
반면@RequestBody는 요청 본문에 들어 있는 데이터를 읽는다.
예를 들어 클라이언트가 아래처럼JSON을 본문에 담아 보낸다고 생각하면 된다.// 요청 예시 // 요청 방식: POST // 요청 주소: /api/member-body // 요청 헤더: Content-Type: application/json // 요청 본문: {"name":"lee","age":20}이때 컨트롤러는 아래처럼
@RequestBody로 요청 본문을 받을 수 있다.// MemberDTO.java public class MemberDTO { private String name; // 이름 저장 private int age; // 나이 저장 public MemberDTO() { // JSON을 객체로 바꿀 때 필요한 기본 생성자 } public String getName() { return name; // 이름 반환 } public void setName(String name) { this.name = name; // 이름 저장 } public int getAge() { return age; // 나이 반환 } public void setAge(int age) { this.age = age; // 나이 저장 } }// RequestBodyCompareExample.java @Controller public class RequestBodyCompareController { @PostMapping("/api/member-body") // JSON 본문을 가진 POST 요청 처리 @ResponseBody public String memberBody(@RequestBody MemberDTO member) { return member.getName() + ":" + member.getAge(); // 응답 본문으로 문자열 반환 } }// 출력결과 // 요청 방식: POST // 요청 주소: /api/member-body // 요청 본문: {"name":"lee","age":20} // member.getName() 값: lee // member.getAge() 값: 20 // 응답 본문: lee:20이 코드에서
@RequestBody MemberDTO member는 요청 본문에 들어온JSON데이터를MemberDTO객체로 받는다는 뜻이다.
JSON의name값은member.name에 들어가고,age값은member.age에 들어간다.
정리하면 아래와 같다.
@RequestParam은URL뒤에 붙은 값을 읽는다.@RequestBody는 요청 본문에 들어 있는 값을 읽는다.@RequestParam은 검색 조건이나 간단한 요청값을 받을 때 자주 사용한다.@RequestBody는JSON데이터를 객체로 받을 때 자주 사용한다.따라서 값이 어디에 담겨 오는지 먼저 확인해야
@RequestParam을 쓸지,@RequestBody를 쓸지 판단할 수 있다.
@RequestBody로 JSON을 객체로 받는 흐름
@RequestBody를 사용하면 요청 본문에 들어온JSON데이터를 객체로 받을 수 있다.
이때JSON의 필드 이름과DTO의 필드 이름이 맞아야 값이 자연스럽게 들어간다.
예를 들어 요청 본문이 아래처럼 들어온다고 생각하면 된다.// 요청 본문 // {"name":"lee","age":20}이
JSON에는name과age라는 이름이 있다.
따라서MemberDTO에도name,age필드와 값을 넣을 수 있는setter가 있어야 한다.// RequestBodyMemberDTO.java public class RequestBodyMemberDTO { private String name; // JSON의 name 값이 들어갈 필드 private int age; // JSON의 age 값이 들어갈 필드 public RequestBodyMemberDTO() { // 요청 본문 JSON을 객체로 만들 때 사용할 기본 생성자 } public String getName() { return name; // 이름 반환 } public void setName(String name) { this.name = name; // JSON name 값을 필드에 저장 } public int getAge() { return age; // 나이 반환 } public void setAge(int age) { this.age = age; // JSON age 값을 필드에 저장 } }// RequestBodyMemberController.java @Controller public class RequestBodyMemberController { @PostMapping("/api/request-body-member") // JSON 본문 요청 처리 @ResponseBody public String requestBodyMember(@RequestBody RequestBodyMemberDTO member) { String name = member.getName(); // 객체에 들어온 name 값 꺼내기 int age = member.getAge(); // 객체에 들어온 age 값 꺼내기 return name + ":" + age; // 응답 본문으로 반환 } }// 출력결과 // 요청 방식: POST // 요청 주소: /api/request-body-member // 요청 헤더: Content-Type: application/json // 요청 본문: {"name":"lee","age":20} // 객체에 들어간 name 값: lee // 객체에 들어간 age 값: 20 // 응답 본문: lee:20이 흐름을 값 이동으로 보면 아래와 같다.
- 클라이언트가 요청 본문에
{"name":"lee","age":20}을 담아 보낸다.- 요청 헤더에는
Content-Type: application/json이 들어간다.Spring은 요청 본문이JSON데이터라고 이해한다.@RequestBody가 붙은RequestBodyMemberDTO member에 값을 넣는다.JSON의name값은member.name에 들어간다.JSON의age값은member.age에 들어간다.- 컨트롤러는
member.getName(),member.getAge()로 값을 꺼낸다.- 반환 문자열은
@ResponseBody때문에 응답 본문으로 나간다.즉,
@RequestBody는 요청 본문을 읽고, 그 내용을 객체로 바꿔 컨트롤러 메서드에 전달하는 역할을 한다.
@ResponseBody는 반환값을 응답 본문으로 보낸다
@ResponseBody는 컨트롤러 메서드의 반환값을 응답 본문에 바로 담아 보내는 어노테이션이다.
일반@Controller에서String을 반환하면 보통 뷰 이름으로 해석된다.
하지만 메서드에@ResponseBody가 붙으면 그 반환값은 뷰 이름이 아니라 응답 데이터가 된다.
먼저 일반@Controller에서 문자열을 반환하는 예제이다.// ControllerViewNameExample.java @Controller public class ControllerViewNameController { @GetMapping("/view-result") // 화면 요청 처리 public String viewResult() { return "result"; // result.html 화면 이름 } }// 출력결과 // 요청 주소: /view-result // 반환값: result // 의미: result.html 화면을 찾음이 코드에서
return "result"는 브라우저에result라는 글자를 출력하라는 뜻이 아니다.
result.html같은 뷰를 찾아 보여 주라는 뜻이다.
반면@ResponseBody가 붙으면 같은 문자열도 응답 본문으로 나간다.// ResponseBodyTextExample.java @Controller public class ResponseBodyTextController { @GetMapping("/text-result") // 데이터 응답 요청 처리 @ResponseBody public String textResult() { return "result"; // 응답 본문에 result 문자열 반환 } }// 출력결과 // 요청 주소: /text-result // 반환값: result // 의미: 응답 본문에 result 문자열 출력 // 브라우저 화면: result두 코드의 반환값은 모두
"result"이다.
하지만 첫 번째 코드는 뷰 이름으로 해석되고, 두 번째 코드는 응답 본문 데이터로 해석된다.
정리하면 아래와 같다.
- 일반
@Controller에서String반환은 보통 뷰 이름이다.@ResponseBody가 붙은 메서드의 반환값은 응답 본문으로 나간다.@ResponseBody는 화면을 찾는 흐름을 거치지 않는다.- 문자열, 객체,
JSON데이터를 바로 응답할 때 사용한다.따라서
@ResponseBody가 붙었는지에 따라 반환 문자열의 의미가 완전히 달라진다.
객체를 응답하면 JSON으로 나갈 수 있다
@ResponseBody는 문자열만 응답할 때 사용하는 것이 아니다.
객체를 반환하면Spring이 그 객체를JSON형태로 바꿔 응답할 수 있다.
예를 들어 컨트롤러가MemberDTO객체를 반환한다고 생각하면 된다.// ResponseBodyMemberDTO.java public class ResponseBodyMemberDTO { private String name; // 이름 저장 private int age; // 나이 저장 public ResponseBodyMemberDTO(String name, int age) { this.name = name; // 이름 저장 this.age = age; // 나이 저장 } public String getName() { return name; // 이름 반환 } public int getAge() { return age; // 나이 반환 } }// ResponseBodyJsonController.java @Controller public class ResponseBodyJsonController { @GetMapping("/api/member-json") // JSON 응답 요청 처리 @ResponseBody public ResponseBodyMemberDTO memberJson() { return new ResponseBodyMemberDTO("lee", 20); // 객체를 응답 본문으로 반환 } }// 출력결과 // 요청 주소: /api/member-json // 응답 본문: {"name":"lee","age":20}이 코드에서 반환값은
ResponseBodyMemberDTO객체이다.
@ResponseBody가 붙어 있으므로Spring은 이 객체를 뷰 이름으로 보지 않는다.
객체 안의 값을 응답 본문에 담아 보낸다.
실제 응답은 보통JSON형태로 만들어진다.
name필드는"lee"로,age필드는20으로 응답된다.
즉,@ResponseBody는 문자열뿐 아니라 객체 응답에도 사용되는 어노테이션이다.
데이터 응답을 중심으로 하는API에서는 이런 방식이 자주 사용된다.
@RestController는 @ResponseBody를 클래스 전체에 적용한 것처럼 이해할 수 있다
@RestController는 데이터 응답 중심 컨트롤러를 만들 때 사용하는 어노테이션이다.
일반@Controller에서는 메서드마다@ResponseBody를 붙여야 반환값이 응답 본문으로 나간다.
반면@RestController를 클래스 위에 붙이면, 그 클래스 안의 메서드 반환값은 기본적으로 응답 본문으로 나간다.
아래는 일반@Controller와@ResponseBody를 함께 사용하는 방식이다.// ControllerResponseBodyExample.java @Controller public class ControllerResponseBodyController { @GetMapping("/api/text1") // 데이터 응답 요청 처리 @ResponseBody public String text1() { return "hello"; // 응답 본문으로 반환 } }// 출력결과 // 요청 주소: /api/text1 // 응답 본문: hello같은 흐름을
@RestController로 작성하면 아래처럼 된다.// RestControllerTextExample.java @RestController public class RestControllerTextController { @GetMapping("/api/text2") // 데이터 응답 요청 처리 public String text2() { return "hello"; // 응답 본문으로 반환 } }// 출력결과 // 요청 주소: /api/text2 // 응답 본문: hello두 예제 모두 응답 본문에
hello를 보낸다.
하지만 첫 번째는 일반@Controller이기 때문에 메서드에@ResponseBody를 붙였다.
두 번째는 클래스 자체가@RestController이기 때문에 메서드마다@ResponseBody를 붙이지 않아도 데이터 응답 흐름으로 동작한다.
정리하면 아래와 같다.
@Controller는 보통 화면 반환 컨트롤러에 사용한다.@Controller에서 데이터를 바로 응답하려면 메서드에@ResponseBody를 붙인다.@RestController는 데이터 응답 중심 컨트롤러에 사용한다.@RestController에서는 반환값이 기본적으로 응답 본문으로 나간다.따라서 화면을 보여 주는 컨트롤러인지, 데이터를 바로 응답하는 컨트롤러인지에 따라
@Controller와@RestController를 구분해야 한다.
Servlet에서는 어떻게 처리했는가
Servlet방식에서는 요청 본문을 읽고 응답 본문을 보내는 작업을 직접 작성해야 했다.
요청 본문은request.getReader()로 읽고, 응답 본문은response.getWriter()로 출력하는 방식이다.
아래 예제는 요청 본문을 문자열로 읽고, 그대로 응답하는 흐름이다.// BodyServletExample.java @WebServlet("/api/body") public class BodyServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); // 요청 한글 인코딩 설정 response.setContentType("text/plain;charset=UTF-8"); // 응답 타입과 인코딩 설정 BufferedReader reader = request.getReader(); // 요청 본문을 읽기 위한 객체 String body = reader.readLine(); // 요청 본문 한 줄 읽기 PrintWriter writer = response.getWriter(); // 응답 본문 출력 객체 writer.write(body); // 읽은 본문을 응답으로 출력 } }// 요청 예시 // 요청 방식: POST /api/body // 요청 헤더: Content-Type: text/plain // 요청 본문: hello// 출력결과 // 응답 본문: hello이 방식에서는 개발자가 요청 본문을 직접 읽는다.
request.getReader()로 본문을 읽고,response.getWriter()로 응답 본문을 직접 출력한다.
만약 요청 본문이JSON이라면 문자열로 읽은 뒤, 그 문자열을 다시 객체로 바꾸는 처리가 필요할 수 있다.
즉,Servlet방식에서는 요청 본문 읽기와 응답 본문 출력 흐름을 개발자가 직접 코드로 작성한다.
Spring MVC에서는 어떻게 바뀌는가
Spring MVC에서는@RequestBody로 요청 본문을 객체로 받을 수 있고,@ResponseBody로 반환값을 응답 본문에 바로 보낼 수 있다.
아래 예제는 요청 본문에 들어온 회원 정보를 객체로 받고, 그 값을 문자열 응답으로 돌려주는 흐름이다.// SpringBodyMemberDTO.java public class SpringBodyMemberDTO { private String name; // name 값 저장 private int age; // age 값 저장 public SpringBodyMemberDTO() { // JSON 데이터를 객체로 만들 때 사용할 수 있는 기본 생성자 } public String getName() { return name; // 이름 값 반환 } public void setName(String name) { this.name = name; // 이름 값 저장 } public int getAge() { return age; // 나이 값 반환 } public void setAge(int age) { this.age = age; // 나이 값 저장 } }// SpringBodyController.java @Controller public class SpringBodyController { @PostMapping("/api/member") // /api/member POST 요청 처리 @ResponseBody public String member(@RequestBody SpringBodyMemberDTO member) { return member.getName() + ":" + member.getAge(); // 응답 본문으로 보낼 문자열 반환 } }// 요청 예시 // 요청 방식: POST /api/member // 요청 헤더: Content-Type: application/json // 요청 본문: {"name":"lee","age":20}// 출력결과 // 응답 본문: lee:20이 코드에서
@RequestBody SpringBodyMemberDTO member는 요청 본문에 들어온JSON데이터를SpringBodyMemberDTO객체로 받는다는 뜻이다.
JSON의name값은member.name에 들어가고,age값은member.age에 들어간다.
이때 요청 헤더에Content-Type: application/json이 있어야 요청 본문을JSON으로 해석하는 흐름이 자연스럽다.
그리고@ResponseBody가 붙어 있기 때문에return member.getName() + ":" + member.getAge()는 뷰 이름이 아니다.
이 반환값은 응답 본문에 그대로 들어간다.
따라서 클라이언트는lee:20이라는 응답 데이터를 받는다.
즉,Spring MVC방식에서는 요청 본문 읽기, 객체 변환, 응답 본문 출력 흐름을 어노테이션 중심으로 처리할 수 있다.
@RestController로 JSON 요청과 JSON 응답 처리하기
@RestController를 사용하면 데이터 응답 중심의 컨트롤러를 더 간단하게 작성할 수 있다.
아래 예제는JSON요청을 객체로 받고, 다시 객체를 반환해서JSON응답을 만드는 흐름이다.// RestMemberDTO.java public class RestMemberDTO { private String name; // 이름 저장 private int age; // 나이 저장 public RestMemberDTO() { // 요청 JSON을 객체로 만들 때 사용할 기본 생성자 } public RestMemberDTO(String name, int age) { this.name = name; // 이름 저장 this.age = age; // 나이 저장 } public String getName() { return name; // 이름 반환 } public void setName(String name) { this.name = name; // 이름 저장 } public int getAge() { return age; // 나이 반환 } public void setAge(int age) { this.age = age; // 나이 저장 } }// RestMemberController.java @RestController public class RestMemberController { @PostMapping("/api/rest-member") // JSON 요청을 받고 JSON 응답 반환 public RestMemberDTO restMember(@RequestBody RestMemberDTO member) { return new RestMemberDTO(member.getName(), member.getAge()); // 객체를 응답 본문으로 반환 } }// 요청 예시 // 요청 방식: POST /api/rest-member // 요청 헤더: Content-Type: application/json // 요청 본문: {"name":"lee","age":20}// 출력결과 // 응답 본문: {"name":"lee","age":20}이 코드에서는 클래스 위에
@RestController가 붙어 있다.
그래서 메서드 위에@ResponseBody를 따로 붙이지 않아도 반환값이 응답 본문으로 나간다.
@RequestBody RestMemberDTO member는 요청 본문JSON을 객체로 받는다.
그리고return new RestMemberDTO(member.getName(), member.getAge())는 객체를 응답 본문으로 반환한다.
이 객체는 응답에서JSON형태로 전달될 수 있다.
즉,@RestController는JSON요청과JSON응답처럼 데이터를 주고받는API컨트롤러에서 자연스럽게 사용된다.
일반 @Controller와 데이터 응답 컨트롤러의 차이
일반
@Controller와@RestController는 모두 컨트롤러이다.
하지만 반환값을 해석하는 방식이 다르다.
일반@Controller에서 문자열을 반환하면 보통 뷰 이름으로 해석된다.
반면@RestController에서 문자열을 반환하면 그 문자열이 응답 본문으로 바로 나간다.
또 일반@Controller에서도 메서드에@ResponseBody를 붙이면 해당 메서드는 데이터 응답 방식으로 동작한다.
차이는 아래처럼 정리할 수 있다.
- 일반
@Controller는 보통 뷰 이름을 반환해서 화면을 보여 준다.@ResponseBody가 붙은 메서드는 반환값을 응답 본문으로 보낸다.@RestController는 클래스 전체가 데이터 응답 중심으로 동작한다.- 화면을 보여 줄 때는 일반
@Controller가 자연스럽다.JSON이나 문자열 데이터를 바로 응답할 때는@ResponseBody나@RestController가 자연스럽다.따라서 컨트롤러를 볼 때는 반환값이 화면 이름인지, 응답 데이터인지를 반드시 구분해야 한다.
두 방식의 차이 정리
Servlet방식과Spring MVC방식은 요청 본문을 읽고 응답 본문을 보낼 수 있다는 큰 흐름은 같다.
하지만 본문 데이터를 처리하는 방식이 달라진다.
차이는 아래처럼 정리할 수 있다.
Servlet에서는request.getReader()로 요청 본문을 직접 읽는다.Spring MVC에서는@RequestBody로 요청 본문을 객체로 받을 수 있다.Servlet에서는response.getWriter()로 응답 본문을 직접 출력한다.Spring MVC에서는@ResponseBody로 반환값을 응답 본문에 바로 보낼 수 있다.- 일반
@Controller는 보통 뷰 이름을 반환한다.@RestController는 반환값을 응답 본문으로 바로 보내는 데이터 중심 컨트롤러이다.따라서
@RequestBody,@ResponseBody,@RestController는 기존 본문 처리 흐름을 없앤 것이 아니다.
요청 본문을 읽고 응답 본문을 보내던 흐름을Spring이 어노테이션 중심으로 더 간단하게 처리해 주는 방식이다.
이 섹션의 핵심은 이것이다.
@RequestBody는 요청 본문을 읽어 객체로 받고,@ResponseBody는 반환값을 응답 본문으로 보내며,@RestController는 데이터 응답 중심 컨트롤러를 만들 때 사용한다.
@Service는 이 클래스가 서비스 계층이라는 뜻을 나타내는 어노테이션이다.
서비스 계층은 실제 핵심 비즈니스 로직을 처리하는 영역이다.
여기서 비즈니스 로직은 프로그램이 실제로 해야 하는 중요한 처리 규칙을 뜻한다.
예를 들어 할인 금액 계산, 주문 가능 여부 판단, 회원 가입 처리, 게시글 등록 처리 같은 흐름이 비즈니스 로직에 해당한다.
컨트롤러는 요청을 받고 응답으로 연결하는 입구 역할을 한다.
반면 서비스는 요청을 받은 뒤 실제로 어떤 처리를 해야 하는지 판단하고 실행하는 중심 역할을 한다.
즉, 컨트롤러는 흐름 연결을 담당하고, 서비스는 핵심 처리를 담당한다.
따라서@Service는 컨트롤러와 핵심 로직의 역할을 분리하기 위해 사용하는 어노테이션이라고 이해하면 된다.
컨트롤러가 모든 일을 직접 처리하면 코드가 빠르게 복잡해진다.
요청 처리, 값 검증, 계산, 데이터 저장, 화면 이동이 한 곳에 섞이기 때문이다.
Spring MVC에서는 컨트롤러가 요청을 받은 뒤, 필요한 실제 처리는 서비스에게 맡기는 구조를 많이 사용한다.
이렇게 나누면 컨트롤러는 요청과 응답 흐름에 집중할 수 있고, 서비스는 실제 처리 규칙에 집중할 수 있다.
@Service는 무엇을 뜻하는가
@Service는 해당 클래스가 서비스 계층의 클래스라는 뜻이다.
동시에Spring은@Service가 붙은 클래스를 스프링 빈으로 등록해서 컨테이너에서 관리한다.
여기서 빈은Spring이 직접 만들고 관리하는 객체를 뜻한다.
즉,@Service는 단순히 사람이 보기 좋으라고 붙이는 이름표만은 아니다.
이 클래스가 서비스 역할을 한다는 의미도 있고,Spring이 이 객체를 관리하게 만드는 의미도 있다.
아래 코드는 가장 단순한 서비스 클래스이다.// HelloService.java @Service public class HelloService { public String getMessage() { return "안녕하세요"; // 실제 처리 결과 반환 } }이 코드에서
HelloService는 문자열을 만들어 반환하는 실제 처리 역할을 한다.
컨트롤러가 직접"안녕하세요"를 만들 수도 있지만, 이 예제에서는 그 일을 서비스가 맡는다.
정리하면 아래와 같다.
@Service는 서비스 계층 클래스를 표시한다.@Service가 붙은 클래스는Spring이 관리하는 빈이 된다.- 서비스는 실제 핵심 처리 로직을 담당한다.
- 컨트롤러는 서비스를 호출해서 처리 결과를 받아 사용한다.
따라서
@Service는 역할 표시와 스프링 빈 등록을 함께 담당하는 어노테이션이다.
컨트롤러와 서비스는 무엇이 다른가
컨트롤러와 서비스는 둘 다 자바 클래스일 수 있다.
하지만 맡는 역할이 다르다.
컨트롤러는 브라우저나 클라이언트의 요청을 받는다.
그리고 요청값을 받고, 필요한 서비스를 호출하고, 결과를Model에 담거나 응답 본문으로 보낸다.
즉, 컨트롤러는 웹 요청과 응답 흐름을 연결하는 역할을 한다.
서비스는 실제 처리 규칙을 담당한다.
예를 들어 주문 금액이 얼마인지 계산하거나, 회원 가입이 가능한지 판단하거나, 입력값을 저장하기 전에 필요한 처리를 수행한다.
즉, 서비스는 애플리케이션이 실제로 해야 하는 일을 처리한다.
차이는 아래처럼 볼 수 있다.
- 컨트롤러는 요청을 받는다.
- 컨트롤러는 어떤 서비스를 호출할지 결정한다.
- 컨트롤러는 처리 결과를 화면이나 응답으로 연결한다.
- 서비스는 실제 핵심 처리를 수행한다.
- 서비스는 계산, 검증, 저장 전 처리 같은 로직을 담당한다.
즉, 컨트롤러는 흐름 연결, 서비스는 핵심 처리라고 구분하면 이해하기 쉽다.
컨트롤러가 모든 일을 직접 처리하면 왜 문제가 되는가
처음에는 컨트롤러 안에 모든 코드를 넣는 것이 쉬워 보일 수 있다.
요청을 받은 메서드 안에서 계산하고, 결과를 만들고, 화면까지 넘기면 한눈에 보이는 것처럼 느껴진다.
하지만 기능이 조금만 커져도 문제가 생긴다.
컨트롤러 안에 요청 처리 코드와 실제 업무 처리 코드가 섞이면 코드가 길어진다.
또 같은 로직을 다른 컨트롤러에서도 써야 할 때 복사해서 붙여 넣게 될 수 있다.
아래 코드는 컨트롤러가 할인 계산까지 직접 처리하는 예시이다.// BadOrderController.java @Controller public class BadOrderController { @GetMapping("/bad-order") // /bad-order?price=10000 요청 처리 public String badOrder(@RequestParam("price") int price, Model model) { int discountPrice = price / 10; // 할인 금액 계산 int finalPrice = price - discountPrice; // 최종 결제 금액 계산 model.addAttribute("finalPrice", finalPrice); // 화면에 넘길 값 저장 return "orderResult"; // 결과 화면으로 이동 } }이 코드는 짧을 때는 크게 문제 없어 보인다.
하지만 할인 규칙이 바뀌거나, 다른 화면에서도 같은 할인 계산을 써야 한다면 컨트롤러 코드가 점점 복잡해진다.
이런 계산 로직은 컨트롤러보다 서비스에 두는 것이 더 자연스럽다.
컨트롤러는 요청값을 받고 서비스에게 계산을 맡긴다.
서비스는 할인 계산이라는 핵심 로직을 처리한다.
서비스로 분리하면 흐름이 어떻게 바뀌는가
서비스로 분리하면 컨트롤러와 실제 처리 로직이 나뉜다.
컨트롤러는 요청값을 받고 서비스를 호출한다.
서비스는 계산 결과를 만들어 반환한다.
컨트롤러는 반환받은 결과를 화면에 넘긴다.
먼저 할인 계산을 담당하는 서비스 클래스를 만든다.// OrderService.java @Service public class OrderService { public int calculateFinalPrice(int price) { int discountPrice = price / 10; // 할인 금액 계산 return price - discountPrice; // 최종 결제 금액 반환 } }// OrderController.java @Controller public class OrderController { private final OrderService orderService; // 서비스 의존 public OrderController(OrderService orderService) { this.orderService = orderService; // 생성자 주입 } @GetMapping("/order") // /order?price=10000 요청 처리 public String order(@RequestParam("price") int price, Model model) { int finalPrice = orderService.calculateFinalPrice(price); // 서비스에 계산 맡김 model.addAttribute("finalPrice", finalPrice); // 계산 결과를 화면에 전달 return "orderResult"; // 결과 화면으로 이동 } }// orderResult.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>orderResult</title> </head> <body> <h1>최종 결제 금액: <span th:text="${finalPrice}"></span></h1> </body> </html>// 출력결과 // 브라우저 주소: /order?price=10000 // 요청값 price: 10000 // 서비스에서 계산한 최종 금액: 9000 // 화면 출력: 최종 결제 금액: 9000이 구조에서 컨트롤러는 할인 계산 방법을 자세히 알 필요가 없다.
컨트롤러는orderService.calculateFinalPrice(price)를 호출하고 결과만 받는다.
할인 계산 규칙은OrderService안에 들어 있다.
따라서 할인 규칙이 바뀌면 컨트롤러를 고치는 것이 아니라OrderService를 고치면 된다.
이렇게 역할을 나누면 코드 수정 위치가 분명해진다.
생성자 주입으로 서비스를 사용하는 흐름
컨트롤러가 서비스를 사용하려면 서비스 객체가 필요하다.
Spring에서는@Service가 붙은 클래스를 빈으로 관리하고, 컨트롤러가 필요로 할 때 넣어 줄 수 있다.
이렇게 필요한 객체를 외부에서 넣어 주는 방식을 의존성 주입이라고 한다.
의존성은 한 클래스가 다른 클래스를 필요로 하는 관계를 뜻한다.
예를 들어OrderController는 계산을 하기 위해OrderService가 필요하다.
따라서OrderController는OrderService에 의존한다고 말할 수 있다.
생성자 주입은 생성자를 통해 필요한 객체를 전달받는 방식이다.
아래 코드에서OrderController(OrderService orderService)생성자가 바로 그 역할을 한다.// ConstructorInjectionServiceExample.java @Controller public class ConstructorInjectionServiceController { private final OrderService orderService; // 필요한 서비스 저장 public ConstructorInjectionServiceController(OrderService orderService) { this.orderService = orderService; // Spring이 넣어 준 서비스를 필드에 저장 } @GetMapping("/service-injection") public String serviceInjection(Model model) { int finalPrice = orderService.calculateFinalPrice(10000); // 서비스 메서드 호출 model.addAttribute("finalPrice", finalPrice); // 결과 저장 return "orderResult"; // 결과 화면으로 이동 } }이 코드에서 컨트롤러는
new OrderService()를 직접 하지 않는다.
Spring이@Service가 붙은OrderService객체를 관리하고 있다가, 컨트롤러를 만들 때 생성자로 넣어 준다.
정리하면 흐름은 아래와 같다.
@Service가 붙은OrderService를Spring이 빈으로 등록한다.OrderController는 생성자에서OrderService를 필요로 한다.Spring이 컨트롤러를 만들 때OrderService빈을 생성자로 넣어 준다.- 컨트롤러는 주입받은 서비스를 호출해서 실제 처리를 맡긴다.
따라서
@Service는 서비스 객체를Spring이 관리하게 만들고, 컨트롤러는 그 서비스를 주입받아 사용한다.
기본 예제로 보는 Controller와 Service 연결
아래 코드는 컨트롤러가 직접 모든 일을 처리하지 않고, 서비스 계층으로 넘기는 가장 단순한 구조이다.
서비스는 메시지를 만들고, 컨트롤러는 그 메시지를 화면으로 넘긴다.// HelloService.java @Service public class HelloService { public String getMessage() { return "안녕하세요"; // 실제 처리 결과 반환 } }// HelloController.java @Controller public class HelloController { private final HelloService helloService; // 서비스 의존 public HelloController(HelloService helloService) { this.helloService = helloService; // 생성자 주입 } @GetMapping("/service-test") // /service-test 요청 처리 public String hello(Model model) { String msg = helloService.getMessage(); // 서비스 호출 model.addAttribute("msg", msg); // 서비스 결과를 화면에 전달 return "hello"; // hello.html 화면으로 이동 } }// hello.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>hello</title> </head> <body> <h1 th:text="${msg}"></h1> </body> </html>// 출력결과 // 브라우저 주소: /service-test // 실행 흐름: Controller → Service → Controller → View // 서비스 반환값: 안녕하세요 // 화면 출력: 안녕하세요이 예제에서
HelloController는/service-test요청을 받는다.
하지만"안녕하세요"라는 값을 직접 만들지 않는다.
대신helloService.getMessage()를 호출해서 서비스에게 실제 값을 만들어 달라고 맡긴다.
HelloService는getMessage()메서드에서"안녕하세요"를 반환한다.
컨트롤러는 그 값을Model에 담고hello.html로 넘긴다.
즉, 요청 흐름은 컨트롤러에서 시작하지만 실제 처리 결과는 서비스에서 만들어진다.
Service가 Repository나 DAO를 호출하는 흐름
실제 프로젝트에서는 서비스가 단순히 문자열만 반환하지 않는다.
보통 서비스는 필요한 데이터를 가져오기 위해Repository나DAO를 호출한다.
여기서Repository나DAO는 데이터베이스에 접근하는 계층이라고 이해하면 된다.
전체 흐름은 보통 아래처럼 이어진다.// 계층 흐름 // Controller → Service → Repository 또는 DAO → DB컨트롤러는 요청을 받고 서비스에게 맡긴다.
서비스는 실제 처리 규칙을 수행하면서 필요한 데이터가 있으면Repository나DAO에게 요청한다.
Repository나DAO는 데이터베이스와 연결되어 데이터를 조회하거나 저장한다.
아래 코드는 실제DB대신 문자열을 반환해서 계층 흐름만 보여 주는 예제이다.// MessageRepository.java @Repository public class MessageRepository { public String findMessage() { return "서비스 계층 테스트"; // 실제 DB 조회 대신 문자열 반환 } }// MessageService.java @Service public class MessageService { private final MessageRepository messageRepository; // Repository 의존 public MessageService(MessageRepository messageRepository) { this.messageRepository = messageRepository; // 생성자 주입 } public String getMessage() { return messageRepository.findMessage(); // Repository에서 데이터 조회 } }// MessageController.java @Controller public class MessageController { private final MessageService messageService; // Service 의존 public MessageController(MessageService messageService) { this.messageService = messageService; // 생성자 주입 } @GetMapping("/message") // /message 요청 처리 public String message(Model model) { String msg = messageService.getMessage(); // Service 호출 model.addAttribute("msg", msg); // 조회 결과를 화면에 전달 return "messageView"; // 결과 화면으로 이동 } }// 출력결과 // 브라우저 주소: /message // 실행 흐름: Controller → Service → Repository // Repository 반환값: 서비스 계층 테스트 // 화면 출력: 서비스 계층 테스트이 예제에서는 컨트롤러가
Repository를 직접 호출하지 않는다.
컨트롤러는MessageService만 호출한다.
그리고MessageService가 필요한 데이터를 얻기 위해MessageRepository를 호출한다.
이렇게 나누면 각 계층의 역할이 분명해진다.
컨트롤러는 요청과 화면 연결을 맡는다.
서비스는 핵심 처리 흐름을 맡는다.
Repository는 데이터 접근을 맡는다.
Servlet/JSP 방식과 비교하면 어떻게 다른가
Servlet/JSP방식에서도 역할을 나눌 수 있었다.
컨트롤러 역할을 하는Servlet이 요청을 받고, 서비스 역할의 자바 클래스를 호출하는 구조를 만들 수 있었다.
즉, 서비스 계층이라는 개념 자체는Spring MVC에서 갑자기 생긴 것이 아니다.
다만Spring MVC에서는@Service를 사용해서 서비스 역할의 클래스를 더 명확하게 표시하고,Spring이 그 객체를 빈으로 관리하게 할 수 있다.
그래서 컨트롤러가 서비스 객체를 직접 만들지 않고 주입받아 사용할 수 있다.
차이는 아래처럼 정리할 수 있다.
Servlet/JSP에서도 서비스 역할의 자바 클래스를 만들 수 있다.- 하지만 직접 객체를 만들거나 연결하는 코드가 필요할 수 있다.
Spring MVC에서는@Service로 서비스 계층을 표시한다.Spring MVC에서는Spring이 서비스 객체를 빈으로 관리한다.- 컨트롤러는 필요한 서비스를 생성자 주입으로 받아 사용할 수 있다.
따라서
@Service는 단순히 클래스 이름을 보기 좋게 만드는 표시가 아니다.
서비스 역할의 객체를Spring이 관리하게 하고, 컨트롤러와 핵심 로직을 분리하는 구조와 연결된다.
언제 Service로 분리하면 좋은가
모든 코드를 무조건 서비스로 빼야 하는 것은 아니다.
아주 단순한 화면 이동만 처리하는 경우에는 컨트롤러만으로 충분할 수도 있다.
하지만 실제 처리 규칙이 들어가기 시작하면 서비스로 분리하는 것이 좋다.
예를 들어 아래와 같은 코드가 컨트롤러에 들어가기 시작하면 서비스 분리를 생각할 수 있다.
- 할인 금액을 계산한다.
- 회원 가입 가능 여부를 판단한다.
- 게시글 등록 전에 입력값을 검사한다.
- 주문 상태를 변경한다.
- 데이터 저장 전에 여러 조건을 확인한다.
- 여러
Repository나DAO를 호출해서 결과를 만든다.이런 로직은 단순 화면 이동이 아니라 애플리케이션의 실제 처리 규칙이다.
따라서 컨트롤러 안에 그대로 두면 컨트롤러가 너무 많은 일을 하게 된다.
서비스로 분리하면 컨트롤러는 요청 흐름에 집중하고, 서비스는 실제 처리 규칙에 집중할 수 있다.
또 같은 로직을 여러 컨트롤러에서 다시 사용할 수도 있다.
정리하면 아래와 같다.
- 단순 요청 연결은 컨트롤러에서 처리할 수 있다.
- 실제 업무 처리 규칙이 들어가면 서비스로 분리하는 것이 좋다.
- 여러 곳에서 재사용할 로직은 서비스에 두는 것이 좋다.
- 데이터 접근 계층과 연결되는 처리 흐름도 서비스가 담당하는 경우가 많다.
따라서
Service는 컨트롤러가 직접 처리하기에는 무겁거나 중요한 로직을 맡기는 계층이라고 이해하면 된다.
두 방식의 차이 정리
컨트롤러만 사용하는 방식과 서비스로 분리하는 방식은 모두 요청을 처리할 수 있다.
하지만 역할을 나누는 정도가 다르다.
차이는 아래처럼 정리할 수 있다.
- 컨트롤러만 사용하는 방식은 요청 처리와 핵심 로직이 한 클래스에 섞이기 쉽다.
- 서비스로 분리하면 컨트롤러는 요청과 응답 흐름에 집중한다.
- 서비스로 분리하면 핵심 비즈니스 로직은 서비스 클래스에 모인다.
@Service가 붙은 클래스는Spring이 서비스 빈으로 관리한다.- 컨트롤러는 서비스를 직접 만들지 않고 주입받아 사용할 수 있다.
- 서비스는 필요하면
Repository나DAO를 호출해서 데이터 접근 계층과 연결된다.따라서
@Service는 기존 처리 흐름을 없앤 것이 아니다.
요청 처리 흐름에서 핵심 로직을 컨트롤러 밖으로 분리하고, 그 서비스 객체를Spring이 관리하도록 만드는 방식이다.
이 섹션의 핵심은 이것이다.
@Service는 핵심 비즈니스 로직을 처리하는 서비스 계층을 나타내며, 컨트롤러는 서비스를 호출해 처리 결과를 받아 화면이나 응답으로 연결한다.
@Component,@Autowired,@Qualifier,@Primary,@PostConstruct는 스프링이 객체를 만들고, 필요한 객체를 넣어 주고, 초기 작업까지 실행하는 흐름과 관련된 어노테이션이다.
이 구간을 이해하려면 먼저 스프링이 객체를 직접 관리한다는 개념부터 잡아야 한다.
일반Java코드에서는 필요한 객체를 사용할 때new로 직접 만든다.
예를 들어CoffeeMaker가CoffeeMachine을 필요로 하면new EspressoMachine()처럼 직접 객체를 생성할 수 있다.
하지만Spring에서는 개발자가 모든 객체를 직접 만들기보다,Spring컨테이너가 객체를 만들고 관리하게 할 수 있다.
여기서 컨테이너는 객체를 담아 두고 관리하는 공간이라고 이해하면 된다.
Spring컨테이너가 관리하는 객체를 빈이라고 한다.
즉, 스프링 빈은Spring이 직접 만들고 관리하는 객체이다.
@Component는 클래스를 스프링 빈으로 등록하는 역할을 한다.
@Autowired는 필요한 빈을 스프링이 찾아서 넣어 주는 역할을 한다.
@Qualifier와@Primary는 같은 타입의 빈이 여러 개 있을 때 어떤 빈을 넣을지 정하는 역할을 한다.
@PostConstruct는 빈 생성과 의존성 주입이 끝난 뒤 실행할 초기화 메서드를 지정한다.
따라서 이 구간의 핵심 흐름은 아래와 같다.
@Component로 객체를 스프링 빈으로 등록한다.@Autowired로 필요한 빈을 주입받는다.- 같은 타입 빈이 여러 개면
@Qualifier나@Primary로 선택 기준을 정한다.- 의존성 주입이 끝난 뒤
@PostConstruct로 초기 작업을 실행한다.즉, 이 어노테이션들은 각각 따로 외우는 것이 아니라, 스프링이 객체를 관리하고 연결하는 흐름 안에서 함께 이해해야 한다.
아래 코드들은 각 어노테이션의 역할을 비교하기 위한 예제이다.
같은 프로젝트에 모든 예제 클래스를 한 번에 넣어 실행하라는 뜻이 아니다.
특히CoffeeMachine을 구현한 클래스가 여러 번 나오기 때문에, 실제 실습에서는 필요한 예제 묶음만 선택해서 실행해야 한다.
스프링 빈은 무엇인가
스프링 빈은
Spring이 만들고 관리하는 객체이다.
일반 객체는 개발자가new로 직접 만들고 직접 관리한다.
반면 스프링 빈은Spring컨테이너가 생성하고 보관하고 필요한 곳에 넣어 줄 수 있다.
예를 들어 아래 코드는 일반Java방식으로 객체를 직접 만드는 흐름이다.// DirectObjectCreateExample.java public class DirectObjectCreateExample { public static void main(String[] args) { CoffeeService service = new CoffeeService(); // 개발자가 직접 객체 생성 String result = service.orderCoffee(); // 직접 만든 객체 사용 System.out.println(result); // 결과 출력 } }// CoffeeService.java public class CoffeeService { public String orderCoffee() { return "커피 주문 완료"; // 주문 결과 반환 } }// 출력결과 // 커피 주문 완료이 방식에서는
CoffeeService객체를 직접 만든다.
그래서 객체가 필요한 곳마다new CoffeeService()를 작성해야 할 수 있다.
반면Spring방식에서는CoffeeService를 스프링 빈으로 등록하고, 필요한 클래스가 그 빈을 주입받아 사용할 수 있다.
이때 사용하는 대표적인 어노테이션이@Component이다.
@Component는 클래스를 스프링 빈으로 등록한다
@Component는 이 클래스를Spring이 관리하는 빈으로 등록하겠다는 뜻이다.
즉, 개발자가 직접new로 객체를 만드는 대신,Spring이 이 클래스의 객체를 만들어 컨테이너 안에 보관할 수 있게 한다.
아래 코드는CoffeeService를 스프링 빈으로 등록하는 예제이다.// CoffeeServiceComponentExample.java @Component public class CoffeeServiceComponentExample { public String orderCoffee() { return "커피 주문 완료"; // 서비스 결과 반환 } }이 코드에서
@Component가 붙어 있기 때문에Spring은 이 클래스를 스캔해서 빈으로 등록할 수 있다.
스캔은Spring이 정해진 범위 안에서@Component같은 어노테이션이 붙은 클래스를 찾아 빈으로 등록하는 과정이다.
즉,@Component가 붙은 클래스는 “이 객체는 내가 직접 만들 테니 건드리지 마”가 아니라,Spring이 만들고 관리할 수 있는 대상으로 등록한다는 의미이다.
정리하면 아래와 같다.
@Component는 클래스를 스프링 빈으로 등록한다.- 빈으로 등록된 객체는
Spring컨테이너가 관리한다.- 다른 클래스는 이 빈을 주입받아 사용할 수 있다.
- 개발자가 직접
new로 객체를 만드는 흐름을 줄일 수 있다.따라서
@Component는 객체를 스프링 컨테이너 안에 맡기는 시작점이라고 이해하면 된다.
@Autowired는 필요한 빈을 자동으로 넣어 준다
@Autowired는 필요한 의존성 객체를Spring이 자동으로 찾아 넣어 주게 하는 어노테이션이다.
여기서 의존성은 한 클래스가 다른 클래스를 필요로 하는 관계를 뜻한다.
예를 들어CoffeeMaker가 커피를 만들기 위해CoffeeService를 필요로 한다면,CoffeeMaker는CoffeeService에 의존한다.
일반Java방식에서는 필요한 객체를 직접 만든다.// DirectDependencyExample.java public class DirectDependencyExample { private CoffeeService coffeeService = new CoffeeService(); // 필요한 객체를 직접 생성 public String makeCoffee() { return coffeeService.orderCoffee(); // 직접 만든 객체 사용 } }이 방식은 단순해 보이지만, 클래스가 직접 객체 생성까지 담당한다.
객체 생성 방식이 바뀌면 이 클래스도 같이 수정될 수 있다.
Spring에서는 필요한 객체를 직접 만들지 않고 주입받을 수 있다.// CoffeeMakerAutowiredExample.java @Component public class CoffeeMakerAutowiredExample { @Autowired private CoffeeServiceComponentExample coffeeService; // Spring이 필요한 빈을 찾아 주입 public String makeCoffee() { return coffeeService.orderCoffee(); // 주입받은 빈 사용 } }이 코드에서
CoffeeMakerAutowiredExample은CoffeeServiceComponentExample객체를 직접 만들지 않는다.
대신@Autowired가 붙은 필드에Spring이 알맞은 빈을 찾아 넣어 준다.
즉,@Autowired의 핵심은 이것이다.
필요한 객체를 내가 직접new로 만들지 않고,Spring이 컨테이너 안에서 찾아 넣어 준다.
필드 주입과 생성자 주입
@Autowired는 필드, 생성자, 메서드에 붙일 수 있다.
학습 단계에서는 필드에 바로 붙이는 방식이 눈에 잘 들어온다.
하지만 실제 프로젝트에서는 생성자 주입을 자주 사용한다.
필드 주입은 아래처럼 필드 위에@Autowired를 붙이는 방식이다.// FieldInjectionExample.java @Component public class FieldInjectionExample { @Autowired private CoffeeServiceComponentExample coffeeService; // 필드에 바로 주입 public String makeCoffee() { return coffeeService.orderCoffee(); // 주입받은 객체 사용 } }필드 주입은 코드가 짧고 보기 쉽다.
하지만 이 클래스가 어떤 객체를 반드시 필요로 하는지 생성자만 보고는 알기 어렵다.
생성자 주입은 필요한 객체를 생성자로 받는 방식이다.// ConstructorInjectionExample.java @Component public class ConstructorInjectionExample { private final CoffeeServiceComponentExample coffeeService; // 필요한 객체를 필드로 보관 public ConstructorInjectionExample(CoffeeServiceComponentExample coffeeService) { this.coffeeService = coffeeService; // 생성자로 주입받은 객체 저장 } public String makeCoffee() { return coffeeService.orderCoffee(); // 주입받은 객체 사용 } }생성자 주입은 이 클래스가 만들어질 때 어떤 객체가 꼭 필요한지 분명하게 보여 준다.
ConstructorInjectionExample객체가 만들어지려면CoffeeServiceComponentExample이 필요하다는 사실이 생성자에 드러난다.
정리하면 아래와 같다.
- 필드 주입은 필드에 바로
@Autowired를 붙이는 방식이다.- 생성자 주입은 생성자를 통해 필요한 빈을 받는 방식이다.
- 필드 주입은 코드가 짧다.
- 생성자 주입은 의존 관계가 더 분명하게 보인다.
따라서 초보자 단계에서는 필드 주입으로 흐름을 이해하고, 실제 코드 구조를 잡을 때는 생성자 주입을 함께 익히는 것이 좋다.
같은 타입 빈이 여러 개 있으면 문제가 생길 수 있다
@Autowired는 기본적으로 필요한 타입의 빈을 찾아 넣어 준다.
그런데 같은 타입의 빈이 여러 개 있으면Spring은 어떤 빈을 넣어야 할지 헷갈릴 수 있다.
예를 들어CoffeeMachine이라는 인터페이스가 있고, 이를 구현한 클래스가 두 개 있다고 생각하면 된다.
하나는EspressoMachine이고, 다른 하나는DripCoffeeMachine이다.
둘 다CoffeeMachine타입으로 볼 수 있다.
아래 코드를 보면 구조가 더 분명하다.// CoffeeMachine.java public interface CoffeeMachine { String brew(); // 커피 추출 기능 }// EspressoMachine.java @Component public class EspressoMachine implements CoffeeMachine { @Override public String brew() { return "에스프레소 추출"; // 에스프레소 추출 결과 } }// DripCoffeeMachine.java @Component public class DripCoffeeMachine implements CoffeeMachine { @Override public String brew() { return "드립 커피 추출"; // 드립 커피 추출 결과 } }이 상태에서 아래처럼
CoffeeMachine타입을 주입받으려고 하면 문제가 생길 수 있다.// CoffeeMakerConflictExample.java @Component public class CoffeeMakerConflictExample { @Autowired private CoffeeMachine coffeeMachine; // CoffeeMachine 타입 빈이 여러 개라 선택이 애매함 }
Spring입장에서는CoffeeMachine타입으로 볼 수 있는 빈이 두 개이다.
EspressoMachine도 가능하고,DripCoffeeMachine도 가능하다.
이때 어떤 빈을 넣어야 할지 기준이 없으면 주입 대상이 애매해진다.
이런 상황을 해결하는 방법이@Primary와@Qualifier이다.
@Primary는 기본 선택 대상을 정하는 방식이고,@Qualifier는 이름으로 정확히 지정하는 방식이다.
@Primary는 기본 선택 대상을 정한다
@Primary는 같은 타입의 빈이 여러 개 있을 때 기본으로 선택할 빈을 지정한다.
즉, 특별히 다른 이름을 지정하지 않으면 이 빈을 우선으로 사용하라는 뜻이다.
아래 예제에서는DripCoffeeMachine에@Primary를 붙였다.// DripCoffeeMachinePrimary.java @Component @Primary public class DripCoffeeMachinePrimary implements CoffeeMachine { @Override public String brew() { return "드립 커피 추출"; // 기본 선택 대상 } }// EspressoMachineComponent.java @Component public class EspressoMachineComponent implements CoffeeMachine { @Override public String brew() { return "에스프레소 추출"; // 다른 CoffeeMachine 빈 } }// PrimaryCoffeeMaker.java @Component public class PrimaryCoffeeMaker { private final CoffeeMachine coffeeMachine; // CoffeeMachine 타입 의존 public PrimaryCoffeeMaker(CoffeeMachine coffeeMachine) { this.coffeeMachine = coffeeMachine; // @Primary가 붙은 빈이 우선 주입될 수 있음 } @PostConstruct public void init() { System.out.println(coffeeMachine.brew()); // 주입 후 결과 확인 } }// 출력결과 // 드립 커피 추출이 예제에서는
CoffeeMachine타입 빈이 여러 개 있어도DripCoffeeMachinePrimary에@Primary가 붙어 있다.
그래서 특별히 다른 빈을 지정하지 않으면DripCoffeeMachinePrimary가 기본 선택 대상이 된다.
정리하면 아래와 같다.
@Primary는 같은 타입 빈이 여러 개일 때 기본 선택 대상을 정한다.- 여러 후보 중 특별한 지정이 없으면
@Primary가 붙은 빈이 우선 선택될 수 있다.- 기본값처럼 자주 사용할 구현체에 붙이면 이해하기 쉽다.
즉,
@Primary는 여러 후보 중 기본으로 사용할 빈을 정하는 방식이다.
@Qualifier는 이름으로 정확히 지정한다
@Qualifier는 어떤 빈을 주입받을지 이름으로 정확히 지정하는 방식이다.
같은 타입 빈이 여러 개 있을 때 “기본값 말고 이 빈을 넣어 줘”라고 직접 선택하는 느낌이다.
먼저 빈 이름을 명확하게 지정할 수 있다.// EspressoMachineNamed.java @Component("espressoMachine") public class EspressoMachineNamed implements CoffeeMachine { @Override public String brew() { return "에스프레소 추출"; // 에스프레소 추출 결과 } }// DripCoffeeMachineNamed.java @Component("dripCoffeeMachine") @Primary public class DripCoffeeMachineNamed implements CoffeeMachine { @Override public String brew() { return "드립 커피 추출"; // 기본 선택 대상 } }이 상태에서
@Qualifier("espressoMachine")를 사용하면@Primary가 붙은dripCoffeeMachine이 아니라espressoMachine을 정확히 주입받을 수 있다.// QualifierCoffeeMaker.java @Component public class QualifierCoffeeMaker { private final CoffeeMachine coffeeMachine; // 선택된 커피 머신 저장 public QualifierCoffeeMaker(@Qualifier("espressoMachine") CoffeeMachine coffeeMachine) { this.coffeeMachine = coffeeMachine; // 이름으로 지정한 빈 주입 } @PostConstruct public void init() { System.out.println(coffeeMachine.brew()); // 주입 후 결과 확인 } }// 출력결과 // 에스프레소 추출이 예제에서
DripCoffeeMachineNamed에는@Primary가 붙어 있다.
하지만 생성자 매개변수에@Qualifier("espressoMachine")가 붙어 있으므로espressoMachine빈이 정확히 선택된다.
정리하면 아래와 같다.
@Qualifier는 빈 이름으로 주입 대상을 직접 지정한다.- 같은 타입 빈이 여러 개 있을 때 특정 빈을 정확히 고를 수 있다.
@Primary가 기본 선택이라면,@Qualifier는 직접 지정이다.따라서
@Qualifier는 여러 후보 중 특정 빈을 이름으로 정확히 찍어 선택하는 방식이다.
@Primary와 @Qualifier의 차이
@Primary와@Qualifier는 모두 같은 타입 빈이 여러 개 있을 때 사용한다.
하지만 선택 방식이 다르다.
@Primary는 기본 선택 대상을 정한다.
그래서 특별한 지정이 없으면@Primary가 붙은 빈이 우선 선택될 수 있다.
@Qualifier는 주입받는 위치에서 어떤 빈을 사용할지 이름으로 직접 지정한다.
그래서 특정 위치에서 반드시 특정 구현체를 써야 할 때 더 분명하다.
차이는 아래처럼 정리할 수 있다.
@Primary는 빈 쪽에 붙여 기본 선택 대상을 정한다.@Qualifier는 주입받는 쪽에서 이름으로 정확히 지정한다.@Primary는 기본값을 정하는 방식이다.@Qualifier는 특정 빈을 직접 선택하는 방식이다.- 둘이 함께 있을 때는
@Qualifier처럼 직접 지정한 정보가 더 명확하다.이 차이를 알면 같은 타입 빈이 여러 개 있을 때 어떤 방식으로 해결해야 하는지 판단할 수 있다.
기본적으로 자주 쓰는 구현체가 있다면@Primary가 자연스럽고, 특정 위치에서 반드시 특정 빈이 필요하면@Qualifier가 자연스럽다.
@PostConstruct는 주입이 끝난 뒤 실행된다
@PostConstruct는Spring이 객체를 만들고 필요한 의존성 주입까지 끝낸 뒤, 자동으로 실행할 메서드에 붙인다.
즉, 객체가 만들어진 직후 아무 때나 실행되는 것이 아니라, 필요한 빈이 모두 들어온 뒤 실행되는 초기화 단계이다.
생성자와@PostConstruct는 역할이 다르다.
생성자는 객체가 만들어지는 과정이다.
@PostConstruct는 객체가 만들어지고 의존성 주입까지 끝난 뒤 실행되는 초기 작업이다.
아래 예제를 보면 차이가 더 분명하다.// PostConstructCoffeeMaker.java @Component public class PostConstructCoffeeMaker { private final CoffeeMachine coffeeMachine; // 주입받을 의존성 public PostConstructCoffeeMaker(@Qualifier("espressoMachine") CoffeeMachine coffeeMachine) { this.coffeeMachine = coffeeMachine; // 생성자에서 의존성 주입 } @PostConstruct public void init() { System.out.println(coffeeMachine.brew()); // 주입이 끝난 뒤 실행 } }// 출력결과 // 에스프레소 추출이 코드에서
init()은 개발자가 직접 호출하지 않는다.
Spring이PostConstructCoffeeMaker객체를 만들고, 생성자를 통해coffeeMachine을 넣어 준 뒤,@PostConstruct가 붙은init()을 자동으로 실행한다.
정리하면 아래와 같다.
- 생성자는 객체를 만드는 단계이다.
- 의존성 주입은 필요한 빈을 객체에 넣는 단계이다.
@PostConstruct는 의존성 주입이 끝난 뒤 실행되는 초기화 단계이다.- 주입받은 객체를 사용해야 하는 초기 작업은
@PostConstruct에 두면 자연스럽다.따라서 의존성이 모두 준비된 뒤 실행해야 하는 코드는
@PostConstruct에 두는 것이 좋다.
기본 예제로 전체 흐름 보기
아래 코드는
@Component,@Autowired,@Qualifier,@Primary,@PostConstruct가 어떤 흐름으로 연결되는지 가장 단순하게 보여 주는 예제이다.
먼저 커피 머신 역할을 정의하는 인터페이스를 만든다.// CoffeeMachine.java public interface CoffeeMachine { String brew(); // 커피 추출 기능 }// EspressoMachine.java @Component("espressoMachine") public class EspressoMachine implements CoffeeMachine { @Override public String brew() { return "에스프레소 추출"; // 에스프레소 추출 결과 } }// DripCoffeeMachine.java @Component("dripCoffeeMachine") @Primary public class DripCoffeeMachine implements CoffeeMachine { @Override public String brew() { return "드립 커피 추출"; // 기본 우선 선택 대상 } }// CoffeeMaker.java @Component public class CoffeeMaker { private final CoffeeMachine coffeeMachine; // 주입받은 커피 머신 저장 public CoffeeMaker(@Qualifier("espressoMachine") CoffeeMachine coffeeMachine) { this.coffeeMachine = coffeeMachine; // 이름으로 정확히 지정한 빈 주입 } @PostConstruct public void init() { System.out.println(coffeeMachine.brew()); // 주입 후 자동 실행 } }// 출력결과 // 에스프레소 추출이 예제에서
EspressoMachine,DripCoffeeMachine,CoffeeMaker는 모두 스프링 빈이다.
@Component가 붙어 있기 때문에Spring이 객체를 만들고 관리할 수 있다.
EspressoMachine과DripCoffeeMachine은 둘 다CoffeeMachine타입이다.
그래서 그냥CoffeeMachine만 주입받으려고 하면 후보가 여러 개가 된다.
이때DripCoffeeMachine에는@Primary가 붙어 있으므로 기본 선택 대상이 될 수 있다.
하지만CoffeeMaker생성자에는@Qualifier("espressoMachine")가 붙어 있다.
그래서 기본 선택 대상인dripCoffeeMachine이 아니라, 이름이espressoMachine인 빈이 정확히 주입된다.
마지막으로@PostConstruct가 붙은init()메서드는 주입이 끝난 뒤 자동으로 실행된다.
그래서coffeeMachine.brew()를 호출했을 때에스프레소 추출이 출력된다.
전체 흐름은 아래처럼 정리할 수 있다.// 실행 흐름 // 1. Spring이 @Component가 붙은 클래스를 빈으로 등록한다. // 2. CoffeeMachine 타입 빈 후보가 espressoMachine, dripCoffeeMachine 두 개 생긴다. // 3. dripCoffeeMachine은 @Primary 때문에 기본 선택 대상이 된다. // 4. CoffeeMaker는 @Qualifier("espressoMachine")로 espressoMachine을 직접 지정한다. // 5. Spring이 CoffeeMaker 생성자에 espressoMachine 빈을 주입한다. // 6. 의존성 주입이 끝난 뒤 @PostConstruct가 붙은 init()을 실행한다. // 7. 에스프레소 추출이 출력된다.즉, 이 흐름은 빈 등록 → 의존성 주입 → 필요한 빈 선택 → 주입 후 초기화 순서로 이해하면 된다.
두 방식의 차이 정리
일반
Java방식과Spring방식은 모두 객체를 만들어 사용할 수 있다.
하지만 객체를 만들고 연결하는 책임이 다르다.
일반Java방식에서는 개발자가new로 객체를 직접 만든다.
객체가 다른 객체를 필요로 하면 그 객체도 직접 만들거나 직접 전달해야 한다.
Spring방식에서는@Component가 붙은 클래스를Spring이 빈으로 등록하고 관리한다.
그리고@Autowired나 생성자 주입을 통해 필요한 빈을 넣어 준다.
같은 타입 빈이 여러 개이면@Primary나@Qualifier로 선택 기준을 정한다.
차이는 아래처럼 정리할 수 있다.
- 일반
Java방식은 개발자가 직접 객체를 만든다.Spring방식은Spring컨테이너가 빈을 만들고 관리한다.@Component는 클래스를 빈으로 등록한다.@Autowired는 필요한 빈을 자동으로 주입받게 한다.@Primary는 기본 선택 대상을 정한다.@Qualifier는 특정 빈을 이름으로 정확히 지정한다.@PostConstruct는 의존성 주입 후 초기 작업을 실행한다.따라서 이 어노테이션들은 각각 따로 떨어진 개념이 아니다.
스프링 컨테이너가 객체를 만들고, 필요한 곳에 넣고, 같은 타입 빈 중 어떤 것을 쓸지 고르고, 마지막 초기 작업까지 실행하는 흐름으로 연결된다.
이 섹션의 핵심은 이것이다.
@Component는 빈 등록,@Autowired는 의존성 주입,@Primary는 기본 선택,@Qualifier는 이름 기반 선택,@PostConstruct는 주입 후 초기화를 담당한다.
@RequestHeader,@CookieValue,HttpSession,BindingResult,Errors는 일반 요청값과는 조금 다른 정보를 받을 때 사용한다.
앞에서 본@RequestParam,@ModelAttribute,@PathVariable,@RequestBody는 주로 사용자가 보낸 값을 받는 방식이었다.
반면 이 구간의 객체와 어노테이션은 요청에 함께 담겨 온 부가 정보, 세션 저장소, 바인딩 결과를 다룬다.
@RequestHeader는 요청 헤더 값을 받을 때 사용한다.
헤더는 브라우저나 클라이언트가 서버에 요청을 보낼 때 함께 보내는 부가 정보이다.
예를 들어 브라우저 종류를 담는User-Agent, 이전 페이지 주소를 나타낼 수 있는Referer, 인증 정보를 담을 수 있는Authorization같은 값이 헤더에 들어갈 수 있다.
@CookieValue는 요청에 담겨 온 쿠키 값을 받을 때 사용한다.
쿠키는 브라우저에 저장되어 있다가 요청할 때 서버로 함께 전달되는 작은 값이다.
예를 들어 테마 설정, 자동 로그인 관련 값, 사용자 선호 값처럼 브라우저에 남겨 둘 수 있는 값이 쿠키로 다뤄질 수 있다.
HttpSession은 사용자별로 서버에 유지되는 세션 저장소를 직접 다룰 때 사용한다.
@SessionAttributes가Model속성 일부를 세션에 연결하는 방식이었다면,HttpSession은 세션 객체 자체에 접근하는 방식이다.
즉,@SessionAttributes는 모델 중심 세션 유지이고,HttpSession은 세션 저장소 직접 접근이다.
BindingResult와Errors는 요청값을 객체에 바인딩한 뒤 생긴 오류를 확인할 때 사용한다.
예를 들어 숫자 필드에 문자가 들어오거나, 검증 조건에 맞지 않는 값이 들어오면 오류 정보를 확인할 수 있다.
특히BindingResult는 보통 바인딩 대상 객체 바로 뒤에 둬야 한다.
따라서 이 구간은 단순히 값을 받는 문법을 외우는 구간이 아니다.
요청 안의 부가 정보, 사용자별 저장 공간, 요청값 바인딩 결과를 컨트롤러 메서드에서 어떻게 받는지 이해하는 구간이다.
요청 헤더는 무엇인가
요청 헤더는 클라이언트가 서버에 요청을 보낼 때 함께 전달하는 부가 정보이다.
사용자가 화면에 직접 입력한 값이라기보다, 요청을 설명하는 정보에 가깝다.
예를 들어 브라우저가 서버에 요청을 보낼 때 자신이 어떤 브라우저인지, 어떤 형식의 응답을 받을 수 있는지, 이전에 어떤 페이지에서 왔는지 같은 정보를 헤더에 담아 보낼 수 있다.
그래서 헤더는 요청 본문이나 요청 파라미터와 다르다.
요청 파라미터는 사용자가 보낸 값에 가깝고, 헤더는 요청 자체에 대한 정보에 가깝다.
Spring MVC에서는 특정 헤더 하나를 컨트롤러 매개변수로 바로 받고 싶을 때@RequestHeader를 사용할 수 있다.
예를 들어User-Agent헤더를 받고 싶으면 아래처럼 작성한다.// RequestHeaderBasicExample.java @Controller public class RequestHeaderBasicController { @GetMapping("/header-info") // /header-info 요청 처리 public String headerInfo(@RequestHeader(value = "User-Agent", required = false) String userAgent, Model model) { model.addAttribute("userAgent", userAgent); // User-Agent 헤더 값을 화면에 전달 return "headerResult"; // 결과 화면으로 이동 } }// headerResult.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>headerResult</title> </head> <body> <p>브라우저 정보: <span th:text="${userAgent}"></span></p> </body> </html>// 출력결과 // 요청 주소: /header-info // 읽는 헤더 이름: User-Agent // userAgent 값: 브라우저가 보낸 User-Agent 값 // 화면 출력: 브라우저 정보: 브라우저가 보낸 User-Agent 값이 코드에서
@RequestHeader(value = "User-Agent", required = false)는 요청 헤더 중User-Agent값을 받겠다는 뜻이다.
required = false는 해당 헤더가 없어도 오류로 처리하지 않겠다는 뜻이다.
헤더는 클라이언트나 요청 상황에 따라 없을 수도 있다.
그래서 꼭 들어온다고 확신할 수 없는 헤더는required = false를 함께 쓰면 안전하다.
정리하면 아래와 같다.
@RequestHeader는 요청 헤더 값을 매개변수로 받을 때 사용한다.- 헤더는 요청 자체를 설명하는 부가 정보이다.
User-Agent는 브라우저나 클라이언트 정보를 담을 수 있다.- 헤더가 없을 수 있으면
required = false를 사용할 수 있다.따라서
@RequestHeader는 사용자가 입력한 값보다 요청에 함께 붙어 온 부가 정보를 읽는 방식으로 이해하면 된다.
쿠키는 무엇이고 @CookieValue는 언제 쓰는가
쿠키는 브라우저에 저장되어 있다가 요청할 때 서버로 함께 전달되는 작은 값이다.
서버가 브라우저에 쿠키를 저장하게 만들 수 있고, 이후 브라우저는 같은 사이트로 요청을 보낼 때 그 쿠키를 함께 보낼 수 있다.
쿠키는 사용자 설정이나 간단한 상태 값을 저장할 때 사용될 수 있다.
예를 들어 테마 설정, 자동 로그인 관련 값, 최근 선택값 같은 작은 데이터를 쿠키로 다룰 수 있다.
@CookieValue는 요청에 포함된 쿠키 값을 컨트롤러 매개변수로 바로 받을 때 사용한다.
예를 들어theme이라는 쿠키 값을 받고 싶다면 아래처럼 작성한다.// CookieValueBasicExample.java @Controller public class CookieValueBasicController { @GetMapping("/cookie-info") // /cookie-info 요청 처리 public String cookieInfo(@CookieValue(value = "theme", required = false) String theme, Model model) { model.addAttribute("theme", theme); // theme 쿠키 값을 화면에 전달 return "cookieResult"; // 결과 화면으로 이동 } }// cookieResult.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>cookieResult</title> </head> <body> <p>선택한 테마: <span th:text="${theme}"></span></p> </body> </html>// 출력결과 // 요청 주소: /cookie-info // 읽는 쿠키 이름: theme // theme 값: 브라우저가 보낸 theme 쿠키 값 // 화면 출력: 선택한 테마: theme 쿠키 값이 코드에서
@CookieValue(value = "theme", required = false)는 요청에 담겨 온 쿠키 중theme값을 받겠다는 뜻이다.
theme쿠키가 없을 수도 있으므로required = false를 사용했다.
쿠키도 항상 존재하는 값이 아니다.
처음 방문한 사용자라면 쿠키가 없을 수 있고, 사용자가 브라우저에서 쿠키를 지웠을 수도 있다.
그래서 쿠키 값을 받을 때도 없는 경우를 생각해야 한다.
정리하면 아래와 같다.
@CookieValue는 요청에 담겨 온 쿠키 값을 받을 때 사용한다.- 쿠키는 브라우저에 저장되었다가 요청 시 함께 전달되는 작은 값이다.
- 쿠키가 없을 수 있으면
required = false를 사용할 수 있다.- 쿠키라는 목적이 분명한 값을 받을 때는
@CookieValue가 코드 의도를 잘 보여 준다.따라서
@CookieValue는 요청 헤더 전체를 직접 뒤지는 방식이 아니라, 쿠키 값을 바로 받겠다는 의도를 드러내는 방식이다.
@RequestHeader와 @CookieValue의 차이
@RequestHeader와@CookieValue는 둘 다 요청에 함께 들어온 부가 정보를 받는다.
그래서 처음 보면 비슷해 보일 수 있다.
하지만 읽으려는 정보의 성격이 다르다.
@RequestHeader는 요청 헤더 값을 읽는다.
헤더는 요청 자체를 설명하는 정보이다.
브라우저 정보, 인증 정보, 요청 형식 같은 부가 정보가 여기에 들어갈 수 있다.
@CookieValue는 쿠키 값을 읽는다.
쿠키는 브라우저에 저장되어 있다가 요청할 때 함께 전달되는 값이다.
사용자 설정이나 작은 상태 값을 유지하는 데 사용될 수 있다.
아래 코드는 헤더와 쿠키를 함께 받는 예제이다.// HeaderCookieCompareExample.java @Controller public class HeaderCookieCompareController { @GetMapping("/request-info") public String requestInfo(@RequestHeader(value = "User-Agent", required = false) String userAgent, @CookieValue(value = "theme", required = false) String theme, Model model) { model.addAttribute("userAgent", userAgent); // 요청 헤더 값 저장 model.addAttribute("theme", theme); // 쿠키 값 저장 return "requestInfoResult"; // 결과 화면으로 이동 } }// requestInfoResult.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>requestInfoResult</title> </head> <body> <p>헤더 정보: <span th:text="${userAgent}"></span></p> <p>쿠키 정보: <span th:text="${theme}"></span></p> </body> </html>// 출력결과 // 요청 주소: /request-info // User-Agent 헤더 값: 브라우저가 보낸 User-Agent 값 // theme 쿠키 값: 브라우저가 보낸 theme 쿠키 값 // 화면 출력: 헤더 정보와 쿠키 정보이 예제에서
userAgent는 요청 헤더에서 가져온 값이다.
반면theme은 쿠키에서 가져온 값이다.
둘 다 요청 안에 함께 들어온 부가 정보이지만 목적이 다르다.
차이는 아래처럼 정리할 수 있다.
@RequestHeader는 요청 헤더 값을 읽는다.@CookieValue는 요청에 포함된 쿠키 값을 읽는다.- 헤더는 요청 자체를 설명하는 부가 정보에 가깝다.
- 쿠키는 브라우저에 저장되었다가 요청 때 함께 전달되는 값이다.
따라서 요청 자체의 부가 정보는
@RequestHeader, 브라우저에 저장된 쿠키 값은@CookieValue로 구분하면 된다.
HttpSession은 세션 저장소를 직접 다룬다
HttpSession은 사용자별로 서버에 유지되는 세션 저장소에 접근할 때 사용한다.
세션은 한 번의 요청이 끝나도 바로 사라지지 않고, 같은 사용자의 다음 요청에서도 값을 꺼내 쓸 수 있는 저장 공간이다.
앞에서 본@SessionAttributes는Model속성 일부를 세션에 유지하는 방식이었다.
반면HttpSession은 세션 객체 자체를 직접 다루는 방식이다.
즉,session.setAttribute()로 값을 직접 저장하고,session.getAttribute()로 값을 직접 꺼낸다.
아래 코드는HttpSession에 로그인 사용자 이름을 직접 저장하고 꺼내는 예제이다.// HttpSessionBasicExample.java @Controller public class HttpSessionBasicController { @GetMapping("/session-login") // /session-login?name=lee 요청 처리 public String sessionLogin(@RequestParam("name") String name, HttpSession session) { session.setAttribute("loginUser", name); // 세션에 로그인 사용자 이름 저장 return "redirect:/session-my"; // 내 정보 확인 요청으로 이동 } @GetMapping("/session-my") // 세션에 저장된 값 확인 public String sessionMy(HttpSession session, Model model) { Object loginUser = session.getAttribute("loginUser"); // 세션에서 로그인 사용자 값 꺼내기 model.addAttribute("loginUser", loginUser); // 화면에 전달 return "sessionMy"; // 결과 화면으로 이동 } }// sessionMy.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>sessionMy</title> </head> <body> <p>로그인 사용자: <span th:text="${loginUser}"></span></p> </body> </html>// 출력결과 // 첫 번째 요청: /session-login?name=lee // session.setAttribute("loginUser", "lee") 실행 // 다음 요청: /session-my // session.getAttribute("loginUser") 결과: lee // 화면 출력: 로그인 사용자: lee이 흐름에서는 첫 번째 요청에서 세션에
loginUser값을 저장한다.
그리고 두 번째 요청에서 같은 세션에 저장된loginUser값을 다시 꺼낸다.
즉,HttpSession은 여러 요청 동안 유지해야 하는 값을 직접 저장하고 꺼낼 때 사용할 수 있다.
로그인 사용자 정보처럼 서버 쪽에서 사용자별로 유지해야 하는 값은HttpSession으로 직접 다루는 흐름이 자연스러울 수 있다.
정리하면 아래와 같다.
HttpSession은 사용자별 세션 저장소에 직접 접근한다.session.setAttribute()로 세션에 값을 저장한다.session.getAttribute()로 세션에서 값을 꺼낸다.- 로그인 사용자 정보처럼 여러 요청 동안 유지해야 하는 값을 다룰 때 사용할 수 있다.
따라서
HttpSession은 세션 저장소 자체를 직접 다루는 방식이다.
@SessionAttributes와 HttpSession의 차이
@SessionAttributes와HttpSession은 둘 다 세션과 관련이 있다.
하지만 중심이 다르다.
@SessionAttributes는Model속성 중 일부를 세션에 유지하는 방식이다.
즉, 출발점이Model이다.
컨트롤러에서Model에 담긴 값 중 지정된 이름의 값이 세션에도 유지된다.
반면HttpSession은 세션 저장소 자체를 직접 다룬다.
session.setAttribute()와session.getAttribute()를 사용해서 값을 직접 넣고 꺼낸다.
즉, 출발점이 세션 객체이다.
아래처럼 구분하면 된다.
@SessionAttributes는Model속성 일부를 세션에 유지한다.HttpSession은 세션 저장소 자체를 직접 다룬다.@SessionAttributes는 폼 단계처럼 컨트롤러 안의 모델 값을 임시로 유지할 때 어울린다.HttpSession은 로그인 사용자 정보처럼 세션 전체에서 직접 관리할 값에 어울릴 수 있다.따라서
@SessionAttributes는 모델 중심이고,HttpSession은 세션 객체 직접 접근 중심이라고 구분하면 된다.
BindingResult는 바인딩 결과와 오류를 담는다
BindingResult는 요청값을 객체에 바인딩한 뒤 생긴 오류를 담는 객체이다.
여기서 바인딩은 요청값을 자바 객체에 연결해서 넣는 과정을 뜻한다.
예를 들어 요청 파라미터name=lee,age=20을MemberDTO객체의name,age필드에 넣는 것이 바인딩이다.
그런데 바인딩 과정에서 문제가 생길 수 있다.
예를 들어age는 숫자여야 하는데 사용자가abc를 보냈다면 숫자로 바꿀 수 없다.
이런 오류 정보를 확인할 수 있는 객체가BindingResult이다.
중요한 점은 위치이다.
BindingResult는 보통 바인딩 대상 객체 바로 뒤에 둬야 한다.
예를 들어@ModelAttribute MemberDTO member의 바인딩 결과를 확인하려면 바로 뒤에BindingResult bindingResult를 둔다.
아래 코드는 바인딩 오류를 확인하는 기본 예제이다.// MemberDTO.java public class MemberDTO { private String name; // 이름 저장 private int age; // 나이 저장 public String getName() { return name; // 이름 반환 } public void setName(String name) { this.name = name; // 이름 저장 } public int getAge() { return age; // 나이 반환 } public void setAge(int age) { this.age = age; // 나이 저장 } }// BindingResultBasicExample.java @Controller public class BindingResultBasicController { @PostMapping("/member/check") // 회원 입력값 확인 요청 처리 public String check(@ModelAttribute MemberDTO member, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { return "memberForm"; // 바인딩 오류가 있으면 입력 화면으로 이동 } model.addAttribute("member", member); // 정상 객체를 화면에 전달 return "memberResult"; // 결과 화면으로 이동 } }// 출력결과 // 요청 예시 1: name=lee&age=20 // 바인딩 결과: 오류 없음 // 이동 화면: memberResult.html // 요청 예시 2: name=lee&age=abc // 바인딩 결과: age를 int로 바꿀 수 없어 오류 발생 // 이동 화면: memberForm.html이 코드에서
MemberDTO의age는int이다.
따라서age=20처럼 숫자가 들어오면 정상적으로 바인딩될 수 있다.
하지만age=abc처럼 숫자로 바꿀 수 없는 값이 들어오면 바인딩 오류가 생길 수 있다.
이때bindingResult.hasErrors()로 오류가 있는지 확인할 수 있다.
오류가 있으면 다시 입력 화면으로 보내고, 오류가 없으면 결과 화면으로 이동한다.
정리하면 아래와 같다.
BindingResult는 바인딩 결과와 오류 정보를 담는다.- 바인딩은 요청값을 객체에 넣는 과정이다.
- 숫자 변환 실패 같은 오류를 확인할 수 있다.
BindingResult는 바인딩 대상 객체 바로 뒤에 둬야 한다.따라서
BindingResult는 요청값을 객체로 받은 뒤 그 과정에서 문제가 있었는지 확인하는 객체이다.
BindingResult의 위치가 중요한 이유
BindingResult는 아무 위치에나 두면 안 된다.
보통 오류를 확인할 바인딩 대상 객체 바로 뒤에 둬야 한다.
예를 들어 아래처럼 작성하면member객체의 바인딩 결과를 자연스럽게 이어서 받을 수 있다.// BindingResultPositionGoodExample.java @Controller public class BindingResultPositionGoodController { @PostMapping("/member/good") public String good(@ModelAttribute MemberDTO member, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { return "memberForm"; // member 바인딩 오류가 있으면 입력 화면 } model.addAttribute("member", member); // 정상 객체 저장 return "memberResult"; // 결과 화면 } }이 구조는
@ModelAttribute MemberDTO member바로 뒤에BindingResult bindingResult가 온다.
그래서bindingResult는 바로 앞의member객체에 대한 바인딩 결과를 담는다고 이해하면 된다.
반대로 아래처럼 중간에 다른 매개변수가 끼면 흐름이 어색해지고, 바인딩 결과를 자연스럽게 연결하기 어렵다.// BindingResultPositionBadExample.java @Controller public class BindingResultPositionBadController { @PostMapping("/member/bad") public String bad(@ModelAttribute MemberDTO member, Model model, BindingResult bindingResult) { return "memberResult"; // BindingResult 위치가 적절하지 않은 예 } }이 코드는
BindingResult가 바인딩 대상 객체 바로 뒤에 있지 않다.
학습 단계에서는 이런 형태를 피하고, 항상 바인딩 대상 객체 바로 뒤에 둔다고 기억하는 것이 좋다.
정리하면 아래와 같다.
BindingResult는 확인할 객체 바로 뒤에 둔다.@ModelAttribute MemberDTO member, BindingResult bindingResult순서가 자연스럽다.- 중간에
Model같은 다른 매개변수를 끼우지 않는다.- 위치가 맞아야 어떤 객체의 바인딩 결과인지 분명하다.
따라서
BindingResult는 위치까지 문법의 일부처럼 생각해야 한다.
Errors는 BindingResult와 무엇이 다른가
Errors는 바인딩이나 검증 과정에서 생긴 오류를 다루는 상위 타입으로 이해하면 된다.
BindingResult는Errors를 더 구체적으로 확장한 타입이다.
학습 단계에서는 둘이 비슷한 오류 확인 역할을 한다고 이해해도 흐름을 따라가는 데 큰 문제는 없다.
다만 실제 코드에서는BindingResult를 더 자주 보는 경우가 많다.
왜냐하면BindingResult는 바인딩 결과를 더 구체적으로 다루는 타입이고, 컨트롤러 메서드에서 바인딩 대상 객체 바로 뒤에 두는 패턴이 많이 사용되기 때문이다.
아래 코드는Errors로 오류를 확인하는 예제이다.// ErrorsBasicExample.java @Controller public class ErrorsBasicController { @PostMapping("/member/errors") public String errorsCheck(@ModelAttribute MemberDTO member, Errors errors, Model model) { if (errors.hasErrors()) { return "memberForm"; // 오류가 있으면 입력 화면 } model.addAttribute("member", member); // 정상 객체 저장 return "memberResult"; // 결과 화면 } }이 코드에서
errors.hasErrors()는 오류가 있는지 확인한다.
BindingResult의hasErrors()와 사용 흐름이 비슷하다.
정리하면 아래와 같다.
Errors는 오류 정보를 다루는 상위 타입에 가깝다.BindingResult는 바인딩 결과를 더 구체적으로 다루는 타입이다.- 둘 다
hasErrors()로 오류 여부를 확인할 수 있다.- 학습 단계에서는
BindingResult를 중심으로 익히는 것이 더 자연스럽다.따라서
Errors는 비슷한 오류 확인 역할을 하는 타입이고, 실제 바인딩 결과 확인은BindingResult중심으로 먼저 이해하면 된다.
Servlet/JSP에서는 어떻게 처리했는가
Servlet/JSP방식에서는 헤더, 쿠키, 세션, 오류 처리를 직접 코드로 다뤄야 했다.
헤더는request.getHeader()로 꺼내고, 쿠키는request.getCookies()로 배열을 받아 직접 찾아야 했다.
세션은request.getSession()으로 꺼낸 뒤setAttribute()나getAttribute()를 사용했다.
아래 예제는 헤더, 쿠키, 세션을 직접 다루는 흐름이다.// InfoServletExample.java @WebServlet("/info") public class InfoServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String userAgent = request.getHeader("User-Agent"); // 헤더 직접 읽기 String theme = null; // theme 쿠키 값을 담을 변수 Cookie[] cookies = request.getCookies(); // 쿠키 배열 직접 꺼내기 if (cookies != null) { for (Cookie cookie : cookies) { if ("theme".equals(cookie.getName())) { theme = cookie.getValue(); // theme 쿠키 값 저장 } } } HttpSession session = request.getSession(); // 세션 객체 직접 꺼내기 Object loginUser = session.getAttribute("loginUser"); // 세션 값 직접 읽기 request.setAttribute("userAgent", userAgent); // JSP에 넘길 헤더 값 저장 request.setAttribute("theme", theme); // JSP에 넘길 쿠키 값 저장 request.setAttribute("loginUser", loginUser); // JSP에 넘길 세션 값 저장 RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/infoResult.jsp"); // JSP 지정 dispatcher.forward(request, response); // JSP로 이동 } }이 방식에서는 필요한 정보를 전부 요청 객체에서 직접 꺼낸다.
특히 쿠키는 원하는 쿠키 이름을 찾기 위해 배열을 반복해서 확인해야 한다.
즉,Servlet/JSP방식에서는 헤더 읽기, 쿠키 찾기, 세션 접근, 화면 전달을 개발자가 직접 코드로 작성한다.
Spring MVC에서는 어떻게 바뀌는가
Spring MVC에서는 헤더와 쿠키를 매개변수로 바로 받을 수 있고, 세션 객체도 컨트롤러 메서드 매개변수로 받을 수 있다.
또 바인딩 결과도BindingResult로 이어서 확인할 수 있다.
아래 코드는@RequestHeader,@CookieValue,HttpSession을 함께 사용하는 예제이다.// InfoControllerExample.java @Controller public class InfoControllerExample { @GetMapping("/info") public String info(@RequestHeader(value = "User-Agent", required = false) String userAgent, @CookieValue(value = "theme", required = false) String theme, HttpSession session, Model model) { Object loginUser = session.getAttribute("loginUser"); // 세션값 확인 model.addAttribute("userAgent", userAgent); // 헤더 값 저장 model.addAttribute("theme", theme); // 쿠키 값 저장 model.addAttribute("loginUser", loginUser); // 세션 값 저장 return "infoResult"; // 결과 화면으로 이동 } }// infoResult.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>infoResult</title> </head> <body> <p>헤더: <span th:text="${userAgent}"></span></p> <p>쿠키: <span th:text="${theme}"></span></p> <p>세션: <span th:text="${loginUser}"></span></p> </body> </html>// 출력결과 // 요청 주소: /info // userAgent 값: User-Agent 헤더 값 // theme 값: theme 쿠키 값 또는 null // loginUser 값: 세션에 저장된 loginUser 값 또는 null // 화면 출력: 헤더, 쿠키, 세션 값이 코드에서
@RequestHeader는 헤더 값을 바로 받는다.
@CookieValue는 쿠키 값을 바로 받는다.
HttpSession은 세션 객체 자체를 받는다.
즉,Spring MVC에서는 요청 객체를 직접 뒤지는 코드를 줄이고, 필요한 값을 메서드 매개변수로 바로 받을 수 있다.
바인딩 결과 확인까지 함께 처리할 수 있다
요청값을 객체로 받을 때는 바인딩 결과를 함께 확인할 수 있다.
이때BindingResult를 바인딩 대상 객체 바로 뒤에 둔다.
아래 코드는 회원 입력값을 객체로 받고, 바인딩 오류가 있으면 다시 입력 화면으로 보내는 예제이다.// MemberFormControllerExample.java @Controller public class MemberFormControllerExample { @PostMapping("/member/check") public String check(@ModelAttribute MemberDTO member, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { return "memberForm"; // 바인딩 오류가 있으면 다시 입력 화면 } model.addAttribute("member", member); // 정상 데이터 저장 return "memberResult"; // 결과 화면으로 이동 } }// 출력결과 // 정상 요청: name=lee&age=20 // bindingResult.hasErrors(): false // 이동 화면: memberResult.html // 오류 요청: name=lee&age=abc // bindingResult.hasErrors(): true // 이동 화면: memberForm.html이 예제에서
BindingResult는MemberDTO member바로 뒤에 있다.
그래서member객체로 요청값을 묶는 과정에서 생긴 오류를bindingResult에서 확인할 수 있다.
이 위치가 바뀌면 어떤 객체의 바인딩 결과인지 흐름이 깨질 수 있다.
따라서BindingResult는 항상 확인할 객체 바로 뒤에 둔다고 기억하는 것이 좋다.
두 방식의 차이 정리
Servlet/JSP방식과Spring MVC방식은 모두 요청의 부가 정보와 세션, 바인딩 결과를 다룰 수 있다.
하지만 값을 꺼내는 방식과 오류를 확인하는 방식이 달라진다.
차이는 아래처럼 정리할 수 있다.
Servlet/JSP에서는request.getHeader()로 헤더를 직접 읽는다.Spring MVC에서는@RequestHeader로 특정 헤더 값을 바로 받을 수 있다.Servlet/JSP에서는request.getCookies()로 쿠키 배열을 직접 꺼내 원하는 쿠키를 찾아야 한다.Spring MVC에서는@CookieValue로 특정 쿠키 값을 바로 받을 수 있다.Servlet/JSP에서는request.getSession()으로 세션 객체를 직접 꺼낸다.Spring MVC에서는 컨트롤러 메서드 매개변수로HttpSession을 바로 받을 수 있다.Spring MVC에서는 객체 바인딩 결과를BindingResult나Errors로 확인할 수 있다.따라서 이 구간의 어노테이션과 객체들은 요청 처리 흐름을 없앤 것이 아니다.
요청에 포함된 부가 정보, 쿠키, 세션, 바인딩 결과를 컨트롤러 메서드에서 더 명확하게 받을 수 있도록 도와주는 방식이다.
이 섹션의 핵심은 이것이다.
@RequestHeader는 헤더 값,@CookieValue는 쿠키 값,HttpSession은 세션 저장소 직접 접근,BindingResult와Errors는 객체 바인딩 뒤의 오류 결과를 확인할 때 사용한다.