본문 바로가기
Engineering WIKI/Spring Boot

[인프런 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 6장. 스프링 MVC - 기본 기능

by wonos 2023. 9. 27.
  • 해당 내용은, '인프런 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 (유료 - 69,300) 김영한님 강의'를 개인적으로 개념정리를 위해 정리한 내용입니다.

로깅 간단히 알아보기

  • 로깅 라이브러리
    • 스프링 부트 라이브러리를 사용하면 스프링 부트 로깅 라이브러리( spring-boot-starter-logging )가 함께 포함된다.
    • 스프링 부트 로깅 라이브러리는 기본으로 다음 로깅 라이브러리를 사용한다.
    • 로그 라이브러리는 Logback, Log4J, Log4J2 등등 수 많은 라이브러리가 있는데, 그것을 통합해서 인터페이스로 제공하는 것이 바로 SLF4J 라이브러리다.
    • 쉽게 이야기해서 SLF4J는 인터페이스이고, 그 구현체로 Logback 같은 로그 라이브러리를 선택하면 된다.
    • 실무에서는 스프링 부트가 기본으로 제공하는 Logback을 대부분 사용한다.
  • 로그 선언
    • private Logger log = LoggerFactory.getLogger(getClass());
    • private static final Logger log = LoggerFactory.getLogger(Xxx.class)
    • @Slf4j : 롬복 사용 가능
  • 로그 호출
    • log.info("hello")
    • 시스템 콘솔로 직접 출력하는 것 보다 로그를 사용하면 다음과 같은 장점이 있다. 실무에서는 항상 로그를 사용해야 한다
  • LEVEL: TRACE > DEBUG > INFO > WARN > ERROR
    • 개발 서버는 debug 출력
    • 운영 서버는 info 출력
    • @Slf4j 로 변경
  • 매핑 정보
    • @RestController
    • @Controller 는 반환 값이 String 이면 뷰 이름으로 인식된다. 그래서 뷰를 찾고 뷰가 랜더링 된다.
    • @RestController 는 반환 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력한다.
    • 따라서 실행 결과로 ok 메세지를 받을 수 있다. @ResponseBody 와 관련이 있는데, 뒤에서 더 자세히 설명한다.
  • 로그 레벨 설정
    • application.properties
#전체 로그 레벨 설정(기본 info)
logging.level.root=info
#hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=debug
  • 로그 사용시 장점
    • 쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정할 수 있다.
    • 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절할 수 있다.
    • 시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등, 로그를 별도의 위치에 남길 수 있다.
    • 특히 파일로 남길 때는 일별, 특정 용량에 따라 로그를 분할하는 것도 가능하다.
    • 성능도 일반 System.out보다 좋다. (내부 버퍼링, 멀티 쓰레드 등등) 그래서 실무에서는 꼭 로그를 사용해야 한다.

HTTP 요청 - 기본, 헤더 조회

  • MultiValueMap
    • MAP과 유사한데, 하나의 키에 여러 값을 받을 수 있다.
    • HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용한다.
      • keyA=value1&keyA=value2
    MultiValueMap<String, String> map = new LinkedMultiValueMap();
    map.add("keyA", "value1");
    map.add("keyA", "value2");
    //[value1,value2]
    List<String> values = map.get("keyA");
    

HTTP 요청 파라미터 - @RequestParam

스프링이 제공하는 @RequestParam 을 사용하면 요청 파라미터를 매우 편리하게 사용할 수 있다.

/**
 * @RequestParam 사용
 * - 파라미터 이름으로 바인딩
 * @ResponseBody 추가
 * - View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력
 */

@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(
 @RequestParam(required = true, defaultValue = "guest") String memberName,
 @RequestParam("age") int memberAge) 
{
	 log.info("username={}, age={}", memberName, memberAge);
	 return "ok";
}
  • @RequestParam : 파라미터 이름으로 바인딩
  • @ResponseBody : View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력
  • @RequestParam의 name(value) 속성이 파라미터 이름으로 사용
  • @RequestParam("username") String memberName
    • request.getParameter("username")
  • 파라미터를 Map으로 조회하기 - requestParamMap
/**
 * @RequestParam Map, MultiValueMap
 * Map(key=value)
 * MultiValueMap(key=[value1, value2, ...]) ex) (key=userIds, value=[id1, id2])
 */

@ResponseBody
@RequestMapping("/request-param-map")
public String requestParamMap(@RequestParam Map<String, Object> paramMap) {
	 log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));
	 return "ok";
}
  • 파라미터를 Map, MultiValueMap으로 조회할 수 있다
  • @RequestParam Map ,
    • Map(key=value)
  • @RequestParam MultiValueMap
    • MultiValueMap(key=[value1, value2, ...] ex) (key=userIds, value=[id1, id2])
  • 파라미터의 값이 1개가 확실하다면 Map 을 사용해도 되지만, 그렇지 않다면 MultiValueMap 을 사용하자.

HTTP 요청 파라미터 - @ModelAttribute

  • 실제 개발을 하면 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야 한다. 보통 다음과 같이 코드를 작성할 것이다.
@RequestParam String username;
@RequestParam int age;

HelloData data = new HelloData();

data.setUsername(username);
data.setAge(age);
  • 스프링은 이 과정을 완전히 자동화해주는 @ModelAttribute 기능을 제공한다.

 

  • 먼저 요청 파라미터를 바인딩 받을 객체를 만들자
  • HelloData
package hello.springmvc.basic;

import lombok.Data;

@Data
public class HelloData {
 private String username;
 private int age;
}
  • 롬복 @Data
    • @Getter , @Setter , @ToString , @EqualsAndHashCode , @RequiredArgsConstructor 를 자동으로 적용해준다.
  • @ModelAttribute 적용 - modelAttributeV1
/**
 * @ModelAttribute 사용
 * 참고: model.addAttribute(helloData) 코드도 함께 자동 적용됨, 뒤에 model을 설명할 때 자세히 설명
 */
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
	log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
	return "ok";
}
  • 마치 마법처럼 HelloData 객체가 생성되고, 요청 파라미터의 값도 모두 들어가 있다
  • 스프링MVC는 @ModelAttribute 가 있으면 다음을 실행한다.
    • HelloData 객체를 생성한다.
    • 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다.
    • 예) 파라미터 이름이 username 이면 setUsername() 메서드를 찾아서 호출하면서 값을 입력한다.

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

    • 스프링(서버)에서 응답 데이터를 만드는 방법은 크게 3가지이다.
      • 정적 리소스
        • 예) 웹 브라우저에 정적인 HTML, css, js를 제공할 때는, 정적 리소스를 사용한다.
      • 뷰 템플릿 사용
        • 예) 웹 브라우저에 동적인 HTML을 제공할 때는 뷰 템플릿을 사용한다.
      • HTTP 메시지 사용
        • HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.
      • 정적 리소스
        • 스프링 부트는 클래스패스의 다음 디렉토리에 있는 정적 리소스를 제공한다.
          • /static , /public , /resources , /META-INF/resources
        • src/main/resources 는 리소스를 보관하는 곳이고, 또 클래스패스의 시작 경로이다. 따라서 다음 디렉토리에 리소스를 넣어두면 스프링 부트가 정적 리소스로 서비스를 제공한다.
        • 정적 리소스 경로
          • src/main/resources/static
          • 다음 경로에 파일이 들어있으면
            • src/main/resources/static/basic/hello-form.html
          • 웹 브라우저에서 다음과 같이 실행하면 된다.
          • http://localhost:8080/basic/hello-form.html
          • 정적 리소스는 해당 파일을 변경 없이 그대로 서비스하는 것이다
      • 뷰 템플릿
        • 뷰 템플릿을 거쳐서 HTML이 생성되고, 뷰가 응답을 만들어서 전달한다. 일반적으로 HTML을 동적으로 생성하는 용도로 사용하지만, 다른 것들도 가능하다. 뷰 템플릿이 만들 수 있는 것이라면 뭐든지 가능하다.
      • 스프링 부트는 기본 뷰 템플릿 경로를 제공한다
        • 뷰 템플릿 경로
          • src/main/resources/templates
        • 뷰 템플릿 생성
          • src/main/resources/templates/response/hello.html

HTTP 메시지 컨버터

  • HTTP 메시지 컨버터 위치
  • HTTP 메시지 컨버터는 어디쯤 있을까? HTTP 메시지 컨버터를 사용하는 @RequestBody 도 컨트롤러가 필요로 하는 파라미터의 값에 사용된다.
  • @ResponseBody 의 경우도 컨트롤러의 반환 값을 이용한다.
  • 요청의 경우 @RequestBody 를 처리하는 ArgumentResolver 가 있고, HttpEntity 를 처리하는 ArgumentResolver 가 있다. 이 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것이다. 
  • 응답의 경우 @ResponseBody 와 HttpEntity 를 처리하는 ReturnValueHandler 가 있다. 그리고 여기에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다.
  • 스프링 MVC는 @RequestBody @ResponseBody 가 있으면 RequestResponseBodyMethodProcessor (ArgumentResolver) HttpEntity 가 있으면 HttpEntityMethodProcessor (ArgumentResolver)를 사용한다.

궁금한 점

jar vs war

  • JAR, WAR 모두 Java의 -jar 옵션 (java -jar)을 이용해 생성된 압축(아카이브 파일)로 애플리케이션을 쉽게 배포하고 동작시킬수 있도록 관련 파일을 패키징 한 것이다.
  • JAR (JAVA Archive)
    • JAVA 어플리케이션이 동작할 수 있도록 자바 프로젝트를 압축한 파일
    • Class (JAVA리소스, 속성 파일), 라이브러리 파일을 포함함
    • JRE(JAVA Runtime Environment)만 있어도 실행 가능함 (java -jar 프로젝트네임.jar)
  • WAR (Web Application Archive)
    • Servlet / Jsp 컨테이너에 배치할 수 있는 웹 애플리케이션(Web Application) 압축파일 포맷
    • 웹 관련 자원을 포함함 (JSP, Servlet, JAR, Class, XML, HTML, Javascript)
    • 사전 정의된 구조를 사용함 (WEB-INF, META-INF)
    • 별도의 웹서버(WEB) or 웹 컨테이너(WAS) 필요
    • 즉, JAR파일의 일종으로 웹 애플리케이션 전체를 패키징 하기 위한 JAR 파일이다.

JSON은 객체인가요 문자인가요?

  • JSON은 규격화 한 문자열 입니다.
  • 규격화 했다는 것은 일정한 규칙에 맞게 작성했다는 의미입니다. 이 문자열을 해석해서 객체로 변환, 사용하기 쉽게 만드는 것을 '파싱'한다고 표현합니다.

스프링 동작과정

  • 스프링 부트 구동 시점에 스프링 컨테이너와 서블릿 컨테이너는 각각이 필요한 기본 빈들을 생성하고, 필요한 초기화 작업과 DI 작업을 수행
  • 클라이언트로부터 URL 호출 시, 서블릿 컨테이너가 request를 파싱하고, HttpServletRequest와 Response 객체를 생성 후, 파싱된 메세지를 HttpServletRequest에 전달함. 이후, 쓰레드가 DispatcherServlet 객체의 service()를 호출(service()는 실제로 부모 클래스에서 오버라이딩 되어 있고 그 내부에서 중요한 메서드인 doDispatch()가 호출됨)
  • 이후 DispatcherServlet은 미리 초기화 해둔 handlerMappings의 handlerMapping 구현 클래스를 순서대로 하나씩 꺼내와서 검사하는데, 1순위인 RequestMappingHandlerMapping 클래스의 조회 전략은 @Controller 클래스 중, 메서드 레벨에 @RequestMapping이 붙어 있고, request의 URL 정보를 토대로 일치하는지 검사. 있다면 해당 핸들러를 반환(컨트롤러) 하지만, 없다면 서블릿 예외를 던지고 애초에 @Controller 자체가 없다면 2순위로 검사 위임하지 않음
  • 해당 핸들러를 getHandlerAdapter()를 통해서 해당 핸들러를 지원하는 어댑터가 있다면 해당 어댑터를 반환하여 HandlerAdapter ha에 할당하고, 지원하는 어댑터가 없는 경우 서블릿 예외를 던짐
  • handlerAdapter의 handler()를 수행하는데 실제 컨트롤러 메서드를 호출하기 전에, 해당 메서드가 필요로 하는 매개변수 정보를 (@RequestBody인지, HttpEntity인지) ArgumentResolver에 제공하고, 각각에 특화된 Http메세지 컨버터를 사용해서 필요한 객체를 생성
  • 컨트롤러가 결과값을 반환
  • 메서드 레벨에 @ResponseBody가 붙어있다면 RetrunValueHandler가 HttpMessengerConverter를 이용해서 반환값을 응답 메세지 바디부에 실어서 ViewResolver등을 거치지 않고 즉시 요청 송신자에 반환
  • 위의 경우가 아니면, ReturnValueHandler는 반환 값의 타입에 따라 적절한 HttpMessengerConverter를 통해 응답 메세지를 생성 후, ViewResolver가 동작하는데 이는 View 객체를 생성하고 DispatcherServlet가 이를 이용하여 render()하고 최종적으로 클라이언트에게 반환.