Search

[강의 요약] 김영한 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

Last update: @2/14/2023
주의
본 포스팅은 인프런 강의를 통해 학습한 내용을 임의로 요약한 것으로 일부 내용의 오류 및 누락, 링크 숨김 등이 존재합니다.

웹 어플리케이션 이해

웹 시스템 구성 - WEB Server, WAS, DB로 분리
웹서버
정적 리소스 처리
단순해서 잘 죽지 않음
WAS, DB 장애 시 오류 화면 제공 가능
WAS는 동적인 처리
DB는 데이터 저장 및 관리
만약 화면 제공이 아닌 API 서비스만 제공하면 굳이 웹서버가 없이 WAS만 구축해도 됨

HTTP, HTTP API, CSR, SSR

HTTP
정적인 HTML 페이지 전달
SSR(Server Side Rendering)
서버에서 동적으로 HTML 파일을 생성(rendering)해서 전달
JSP, Thymeleaf, Freemarker, Velocity 등
HTTP API
HTML이 아니라 데이터를 전달(주로 JSON)
CSR(Client Side Rendering)
클라이언트에서 HTTP API로 받은 데이터를 통해 동적으로 HTML 파일을 생성
React, Vue.js, Angular 등
서버 to 서버 통신에 사용

서블릿

서블릿의 기능 - HTTP 스펙을 편리하게 사용하도록 도와줌
서버 TCP/IP 대기, 소켓 연결
HTTP 요청 메시지 파싱
시작 라인 파싱
헤더 파싱
바디 파싱
저장 프로세스 실행 → request, response 객체 생성
[사용자의 비즈니스 로직]
HTTP 응답 메시지 작성
시작 라인 생성
헤더 생성
메시지 바디 생성
TCP/IP에 응답 전달, 소켓 종료
서블릿 컨테이너
톰캣과 같이 서블릿을 지원하는 WAS를 뜻함
서블릿 객체의 라이프사이클(생성, 초기화, 호출, 종료)을 관리 - 싱글톤으로 관리
동시 요청을 위한 멀티쓰레드 처리 지원(개발자는 신경 안 써도 됨)
요청 하나에 쓰레드 하나 할당
쓰레드를 미리 생성해놓고 쓰레드 풀에서 가져다 씀
이 쓰레드 풀의 최대치(max thread)가 WAS 튜닝의 포인트
너무 낮으면 리소스는 여유, 클라이언트는 응답 지연
너무 많으면 리소스 부족
아파치 ab, 제이미터, nGrinder 등 성능 테스트 툴로 성능 실험 필요
하나의 매핑에 하나의 서블릿호출 - service() 메서드 → 기본값은 doGet(), doPost() 메서드 호출, 재정의 가능

서블릿 기본 기능 - Request

기본 조회
start line 정보 조회
method, protocol, shceme, requestRL/URI, query string, secure)
헤더 정보 조회
host, accept-language, cookie, content-type 등
기타 정보 조회
remote host/address/remote port, local name/address/port
요청 데이터 사용
파라미터 형식 - Get(query string), Post(content-type: application/x-www-form-urlencoded)
getParameter(), getParameters(), getParameterNames()
메시지 바디 형식 - Post, Put, Patch
content-type: text/plain
request.getInputStream()
content-type: application/json
request.getInputStream()
위에서 받은 inputStream을 ObjectMapper(jackson)로 파싱
ObjectMapper objectMapper = new ObjectMapper(); String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
Java
복사

서블릿 기본 기능 - Response

Http 응답코드 지정
response.setStatus(HttpServletResponse.SC_OK);
Java
복사
응답 헤더 설정
response.setHeader("Content-Type", "text/plain;charset=utf-8");
Java
복사
편의 기능
컨텐츠 타입
//response.setHeader("Content-Type", "text/plain;charset=utf-8"); // response.setContentLength(2); response.setContentType("text/plain"); response.setCharacterEncoding("utf-8")
Java
복사
쿠키
// response.setHeader("Set-Cookie", "myCookie=good; Max-Age=600"); Cookie cookie = new Cookie("myCookie", "good"); cookie.setMaxAge(600); //600초 response.addCookie(cookie);
Java
복사
리다이렉트
//response.setStatus(HttpServletResponse.SC_FOUND); //302 //response.setHeader("Location", "/basic/hello-form.html" response.sendRedirect("/basic/hello-form.html");
Java
복사
응답 바디 생성
단순 텍스트
PrintWriter writer = response.getWriter(); writer.println("<html>");
Java
복사
JSON
ObjectMapper objectMapper = new ObjectMapper(); HelloData helloData = new HelloData("kim", 20); String result = objectMapper.writeValueAsString(helloData); response.getWriter().write(result);
Java
복사

JSP(Java Server Page)와 MVC

write() 노다가의 어려움을 극복하기 위한 템플릿 엔진 JSP의 등장
JSP에 HTML과 자바 코드가 섞이면서 개발 및 유지보수가 힘들어짐
그래서 View 로직을 담당하는 JSP와 비즈니스 로직을 담당하는 서블릿을 나눔
이후에 데이터를 실어나르는 Model까지 추가한 Model-View-Controller 패턴 등장(model 1)
이후 컨트롤러에서 웹 계층을 다루는 컨트롤러와 비즈니스 로직을 다루는 서비스/리포지토리 계층으로 나눈 패턴이 등장(model 2)
MVC 패턴의 한계
View로 이동하는 forward 코드의 중복
ViewPath 중복 - 절대경로와 확장자가 중복되고, 변경이 힘듦
사용하지 않는 파라미터가 생김(request, response 등)
테스트 작성이 힘듦
공통 로직의 처리가 어려움

스프링 MVC의 진화 과정

컨트롤러간의 공통 로직을 처리를 하는 수문장(front controller)을 두는 것이 프론트 컨트롤러 패턴임
스프링 웹 MVC의 DispatcherServlet이 이 FrontController 역할
1.
매핑 정보를 조회해서 해당 컨트롤러를 호출 → 컨트롤러에서 View 호출
2.
컨트롤러가 반환하는 View 객체를 통해 뷰 렌더링
3.
View Resolver 및 Model 도입으로 컨트롤러는 논리 경로만 반환하고 resolver에서 물리 경로로 변경
이 시점에서 컨트롤러는 서블릿을 전혀 사용하지 않아 단순해지고 테스트가 쉬워짐
4.
컨트롤러가 ModelView가 아닌 뷰 논리 경로를 단순 문자열로 반환
5.
파라미터와 반환값이 제각각인 여러 컨트롤러를 지원하기 위한 어댑터 목록 추가
이 시점에 어댑터 덕분에 컨트롤러가 아니라 어떤 클래스든 URL에 매핑해서 사용할 수 있게 됨. 따라서 이름도 더 넓은 핸들러(handler)로 변경됨
각 어댑터는 핸들러가 요청하는 파라미터를 건내주고 핸들러가 반환하는 값을 통해 요청을 처리함. 이후 모든 어댑터는 공통적으로 ModelView를 반환하도록 통일함
위의 과정을 거쳐 탄생한 것이 스프링 MVC 구조
스프링 MVC의 큰 강점은 DispatcherServlet의 코드 변경 없이 기능의 변경 및 확장이 가능한 것

스프링 MVC의 주요 인터페이스 및 기본 구현체

핸들러 매핑
org.springframework.web.servlet.HandlerMapping 인터페이스
자동 등록 및 우선순위
0 = RequestMappingHandlerMapping -> @RequestMapping 애노테이션으로 핸들러를 찾음 1 = BeanNameUrlHandlerMapping -> 스프링 빈의 이름으로 핸들러를 찾음 ...
Java
복사
핸들러 어댑터
org.springframework.web.servlet.HandlerAdapter 인터페이스
0 = RequestMappingHandlerAdapter -> @RequestMapping 애노테이션이 붙은 핸들러 처리 1 = HttpRequestHandlerAdapter -> HttpRequestHandler 처리 2 = SimpleControllerHandlerAdapter -> Controller 인터페이스를 구현한 컨트롤러 처리(과거) ...
Java
복사
뷰 리졸버
org.springframework.web.servlet.ViewResolver 인터페이스
1 = BeanNameViewResolver -> 빈 이름으로 뷰를 찾아서 반환 (: 엑셀 파일 생성 기능에 사용) 2 = InternalResourceViewResolver -> JSP를 처리할 수 있는 뷰를 반환
Java
복사
org.springframework.web.servlet.View 인터페이스

스프링 MVC 사용하기

컨트롤러 클래스 등록
@Controller@Component 포함
반환값이 String이면 뷰 이름으로 인식
@RestController
반환값이 String이면 Http 메시지 바디에 바로 입력
URL 매핑
매핑 - 컨트롤러 클래스 레벨에@RequestMapping("공통 URL 경로")
스프링 부트 3.0(스프링 6.0) 이전에는 클래스 레벨에 @RequestMapping만 붙여도 컨트롤러로 등록이 됐지만 이상 버전에서는 꼭 @Controller를 붙여줘야 컨트롤러로 등록됨
매핑 - 메서드 레벨에 다음 어노테이션 부착
@RequestMapping("URL경로") → Get + Post 요청 모두 처리
@GetMapping("URL경로")
=@RequestMapping(value = "/hello, method = RequestMethod.GET)
@PostMapping("URL경로")
= @RequestMapping(value = "/hello", method = RequestMethod.POST)
그 외 @PutMapping, @DeleteMapping, @PatchMapping 등이 있음
최종 매핑은 클래스 레벨의 매핑 + 메서드 레벨 매핑으로 조합한 URL에 매핑됨
메서드 레벨에 URL 경로를 생략하면 클래스 레벨의 URL을 받는 메서드가 됨
다중 매핑 가능함{"URL1", "URL2"}
url 마지막 슬래시(/) 관련
스프링 부트 3.0 이전 - 떼고 인식
/hello , /hello/ 요청 → /hello로 매핑
스프링 부트 3.0 이후 - 붙인 것과 뗀 것 구분하여 인식
/hello 요청 → /hello로 매핑
/hello/ 요청 → /hello/로 매핑
각종 요청 매핑(path variable, parameter, consumes, produces 등)

HTTP 요청 데이터 받기 - 헤더

헤더 조회 - 파라미터를 통해 받을 수 있음(공식 매뉴얼 파라미터 목록)
HttpServletRequest request HttpServletResponse response HttpMethod httpMethod // enum 타입 Locale locale @RequestHeader MultiValueMap<String, String> headerMap // 한 헤더에 할당된 여러 값들을 List로 반환 @RequestHeader("host") String host // 특정 HTTP 헤더를 조회 @CookieValue(value = "myCookie", required = false) String cookie // default value도 설정 가능
Java
복사

HTTP 요청 데이터 받기 - 파라미터

메서드 내부에서 request.getParamter(key) 사용
파라미터를 변수로 받기 - @RequestParam(key) 사용
매개변수명 같으면 key 생략 가능
String, int 등의 단순 타입이면 @RequestParam도 생략 가능(단, required=false 적용)
옵션
required - 값 필수 여부(없으면 오류 발생)
true가 기본값
빈 문자열("")은 값이 있는 것이니 주의
default - 값이 없을 경우 초기값
Map<String, Object>에 담에서 받을 수 있음
만약 파라미터 하나당 2개 이상의 값이 예상되면 MultiValueMap 사용(key 하나에 value 여러개)
파라미터를 객체로 받기 - @ModelAttribute 사용
파라미터 이름이 객체 필드와 일치할 경우 파라미터들을 객체에 조립한 후 model에도 자동으로 담아줌
생략 가능. 생략된 파라미터는 String, int, Integer 등 단순 타입일 경우 @RequestParam이 적용되고 나머지 객체는 @ModelAttribute가 적용됨
argument resolver로 지정해둔 타입은 제외

HTTP 요청 데이터 받기 - HTTP 메시지 바디 - 단순 텍스트

request.getInputStream() 사용
ServletInputStream inputStream = request.getInputStream();
Java
복사
파라미터에서 InputStream으로 받기
...(InputStream inputStream, Writer responseWriter)...
Java
복사
받은 inputStream은 아래처럼 처리
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
Java
복사
파라미터에서 HttpEntity 객체로 받기
...(HttpEntity<String> httpEntity)... String messageBody = httpEntity.getBody();
Java
복사
 파라미터에서 @RequestBody로 받기
...(@RequestBody String messageBody)...
Java
복사

HTTP 요청 데이터 받기 - HTTP 메시지 바디 - JSON

위의 텍스트 얻는 방법으로 문자열을 얻은 후 ObjectMapper(Jackson 라이브러리) 사용
...(@RequestBody String messageBody)... ObjectMapper objectMapper = new ObjectMapper(); HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
Java
복사
 파라미터에서 @RequestBody로 받기 - JSON을 객체로 조립해서 넘겨줌
...(@RequestBody HelloData helloData)...
Java
복사
@RequestBody 생략 불가. 생략 시 @ModelAttribute 적용됨
 파라미터에서 HttpEntity<객체>로 받기
...(HttpEntity<HelloData> data)...
Java
복사

HTTP 응답 - 정적 리소스, 뷰 템플릿

문자열 반환
정적 리소스 먼저 찾고, 없으면 템플릿 찾음
뷰 템플릿 경로
src/main/resources/templates
정적 리소스 경로
src/main/resources/static
ModelAndView 반환
@RequestMapping("/response-view-v1") public ModelAndView responseViewV1() { ModelAndView mav = new ModelAndView("response/hello") .addObject("data", "hello!"); return mav; }
Java
복사
void 반환 - URL을 논리 뷰 이름으로 사용
불명확하기 때문에 비추천

HTTP 응답 - HTTP API, 메시지 바디에 직접 입력

텍스트 응답
반환값 void
response.getWriter.wriet(”ok”)
반환값 ResponseEntity<String>
return new ResponseEntity<>(”ok”, HttpStatus.OK)
반환값 String + 메서드에 @ResponseBody
JSON 응답
반환값 객체 + 메서드에 @ResponseBody + @ResponseStatus(HttpStatus.OK)
반환값 ResponseEntity<객체>
return new ResponseEntity<>(객체, HttpStatus.OK)
@Controller 대신 @RestController를 사용하면 해당 컨트롤러에 모두 ResponseBody가 적용되는 효과가 있음 - REST(HTTP) API 전용
내부에 @ResponseBody가 적용되어 있음
메시지 컨버터
다음의 경우에 적용
HTTP 요청 시 - @RequestBody, @HttpEntity(RequestEntity)
HTTP 응답 시 - @ResponseBody, @HttpEntity(ResponseEntity)
적용 시, 다음 컨텐츠에 따라 적용
기본 문자열 처리 시 - StringHttpMessageConverter 동작
기본 객체 처리 시 - MappingJackson2HttpMessageConverter 동작
스프링 부트 기본 메시지 컨버터
org.springframework.http.converter.HttpMessageConverter 인터페이스
0 = ByteArrayHttpMessageConverter 1 = StringHttpMessageConverter 2 = MappingJackson2HttpMessageConverter ...
Java
복사
요청과 응답 시 데이터 형식(Content-Type)과 변환하는 타입(파라미터 대상 클래스)을 확인해서 둘 다 지원 가능하면 적용
메시지 컨버터의 위치
ArgumentResolver 및 ReturnValueHandler에서 사용

PRG (Post/Redirect/Get)

Post 요청을 통해 등록처리 후 뷰 템플릿이 아니라 다른 화면으로 리다이렉트하는 패턴
RedirectAttributes 이용
URL 인코딩 및 pathVariable, 쿼리 파라미터까지 처리해줌
@PostMapping("/add") public String addItemV6(Item item, RedirectAttributes redirectAttributes) { Item savedItem = itemRepository.save(item); redirectAttributes.addAttribute("itemId", savedItem.getId()); redirectAttributes.addAttribute("status", true); return "redirect:/basic/items/{itemId}"; //리다이렉트 결과 URL: http://localhost:8080/basic/items/3?status=true }
Java
복사
attribute로 추가된 항목 중에서 pathVariable인 것을 URL에 넣고 나머지는 쿼리 파라미터로 붙여줌