[ 김영한 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 #6 ] 스프링 MVC - 기본 기능 (1)

김수호·2023년 9월 3일
0
post-thumbnail

이번 섹션부터는 스프링 MVC가 제공하는 기본 기능들을 자세히 알아보자.
(이번 섹션은 내용이 방대하여, 1) ~ 4) , 5) ~ 10) , 11) ~ 15) 를 각각 나눠서 정리하고자 한다.)

👉 목차는 다음과 같다.

1) 프로젝트 생성
2) 로깅 간단히 알아보기
3) 요청 매핑
4) 요청 매핑 - API 예시

5) HTTP 요청 - 기본, 헤더 조회
6) HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form
7) HTTP 요청 파라미터 - @RequestParam
8) HTTP 요청 파라미터 - @ModelAttribute
9) HTTP 요청 메시지 - 단순 텍스트
10) HTTP 요청 메시지 - JSON
11) 응답 - 정적 리소스, 뷰 템플릿
12) HTTP 응답 - HTTP API, 메시지 바디에 직접 입력
13) HTTP 메시지 컨버터
14) 요청 매핑 헨들러 어뎁터 구조
15) 정리

1) 먼저 새로 프로젝트를 만들어 볼 것이다. 2) 그리고 지금까지는 System.out.println(...)으로 출력했었지만, 사실 실무에서 개발할 때는 로거를 통해서 로깅으로 남겨야 한다. 그래서 그와 관련해서 간단히 알아볼 것이다.
3) ~ 4) 이후 본격적으로 스프링의 요청 매핑에 대해서 알아볼 것이다. 5) 그리고 이전에 서블릿에서 했던 HTTP 요청 기본 정보, 헤더 정보 조회를 스프링 MVC로는 어떻게 더 편리하게 사용할 수 있는지 알아보자.
6) ~ 8) 그리고 서블릿에서 했던 request.getParameter(...)로 조회했던 것을 스프링으로 하면 굉장히 편리하게 조회할 수 있는데 그 방법에 대해서 알아볼 것이다.
9) ~ 10) 그 다음에는, HTTP 요청 메시지 바디는 어떻게 꺼내서 사용하는지 알아볼 것이다.
11) ~ 12) 그리고 HTTP 응답에 대한 것(정적 리소스, 뷰 템플릿, HTTP API)을 알아볼 것이다.
그리고 추가로 13) HTTP 메시지 컨버터, 14) 요청 매핑 헨들러 어뎁터 구조에 대해서 알아볼 것이다.


1) 프로젝트 생성

  • 스프링 부트 스타터 사이트로 이동해서 스프링 프로젝트를 생성하고(start.spring.io), 아래와 같이 정보를 입력하고 프로젝트를 생성하자.
    • Project: Gradle - Groovy
    • Language: Java
    • Spring Boot: 2.7.15 (강의에서는 2.4.3 버전으로 하였지만, 필자가 강의를 듣는 시점에는 해당 버전이 선택지에 없어 2.7.15 버전으로 하였다.)
    • Project Metadata
      • Group: hello
      • Artifact: springmvc
      • Packaging: Jar
      • Java: 11
    • Dependendies
      • Spring Web
      • Thymeleaf
      • Lombok
  • 프로젝트 생성 후 실행해보자.
    • http://localhost:8080 호출해서 Whitelabel Error Page가 나오면 정상 동작.
  • 그리고 이번 섹션에서 학습한 내용을 편리하게 참고하기 위해 Welcome 페이지를 만들자. (스프링 부트에 Jar 를 사용하면 /resources/static/ 위치에 index.html 파일을 두면 Welcome 페이지로 처리해준다. (스프링 부트가 지원하는 정적 컨텐츠 위치에 /index.html 이 있으면 된다.))
    • 참고
      • 스프링 부트 Welcome 페이지 지원 ( https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#boot-features-spring-mvc-welcome-page )

✔️ 참고

  • Packaging는 War가 아니라 Jar를 선택하자. (War는 몇 가지 기능이 더 들어간다. War는 1) 보통 톰캣을 별도로 설치하고 거기에 빌드된 파일을 넣을때 사용한다. 2) 그리고 JSP를 쓰는 경우 War를 선택해야 한다.) JSP를 사용하지 않고, 내장 톰캣 기능에 최적화해서 사용하는 경우 Jar를 사용하는 것이 좋다. 앞으로 스프링 부트를 사용하면 이 방식을 주로 사용하게 된다.
  • Jar를 사용하면 항상 내장 서버(톰캣 등)를 사용하고, webapp 경로도 사용하지 않는다. 그냥 내장 서버 사용에 최적화 되어 있는 기능이다. 최근에는 주로 이 방식을 사용한다.
  • War를 사용하면 내장 서버도 사용가능 하지만, 주로 외부 서버에 배포하는 목적으로 사용한다.

2) 로깅 간단히 알아보기

앞으로 로그를 사용할 것이기 때문에, 이번에는 로그에 대해서 간단히 알아보자.

운영 시스템에서는 System.out.println() 같은 시스템 콘솔을 사용해서 필요한 정보를 출력하지 않고, 별도의 로깅 라이브러리를 사용해서 로그를 출력한다. 참고로 로그 관련 라이브러리도 많고, 깊게 들어가면 끝이 없기 때문에 여기서는 최소한의 사용 방법만 알아본다.

로깅 라이브러리

  • 스프링 부트 라이브러리를 사용하면 스프링 부트 로깅 라이브러리( spring-boot-starter-logging )가 함께 포함된다. 스프링 부트 로깅 라이브러리는 기본으로 다음 로깅 라이브러리를 사용한다.
    • SLF4J
    • Logback
  • (참고) 로그 라이브러리는 Logback, Log4J, Log4J2 등등 수 많은 라이브러리가 있는데, 그것을 통합해서 인터페이스로 제공하는 것이 바로 SLF4J 라이브러리다. 쉽게 이야기해서 SLF4J는 인터페이스이고, 그 구현체로 Logback 같은 로그 라이브러리를 선택하면 된다. 실무에서는 스프링 부트가 기본으로 제공하는 Logback을 대부분 사용한다. (로그를 출력할 때 SLF4J라는 인터페이스를 사용하고, 그 구현체로 Logback을 사용한다.)

로그 선언

  • private Logger log = LoggerFactory.getLogger(getClass());
  • private static final Logger log = LoggerFactory.getLogger(Xxx.class)
  • @Slf4j : 롬복 사용 가능 (롬복이 제공하는 애노테이션이다. 이렇게 적용하면 롬복이 위 코드를 자동으로 반영한다.)

로그 호출

  • log.info("hello")
  • System.out.println("hello")
  • 시스템 콘솔로 직접 출력하는 것 보다 로그를 사용하면 다음과 같은 장점이 있다. 실무에서는 항상 로그를 사용해야 한다.

 

👉 그 전에 먼저 로그 선언과 호출을 코드로 확인해보자.

  • LogTestController: src > main > java > hello > springmvc > basic 패키지를 생성하고, 내부에 LogTestController 클래스를 생성하자.
  • 실행해보자.
    • 정상적으로 로그가 출력됨을 확인할 수 있다.
    • 아래와 같은 경우도 테스트시 참고해보자.
      • 로그가 출력되는 포멧 확인
        • 시간, 로그 레벨, 프로세스 ID, 쓰레드 명, 클래스명, 로그 메시지
      • 로그 레벨 설정을 변경해서 출력 결과를 보자.
        • LEVEL: TRACE > DEBUG > INFO > WARN > ERROR
        • (일반적으로) 개발 서버는 debug 출력
        • (일반적으로) 운영 서버는 info 출력
      • @Slf4j 로 변경

로그 레벨 설정

  • application.properties

올바른 로그 사용법

  • log.debug("data="+data)
    • 로그 출력 레벨을 info로 설정해도 위 코드에 있는 "data="+data가 실제 실행이 되어버린다. 결과적으로 문자 더하기 연산이 발생한다. (자바 언어는 debug 메서드를 호출하기 전에, 더하기 연산이 있다면 더하는 연산을 먼저 수행해버린다. (연산이 일어나면서 메모리도 사용하고, cpu도 사용한다.) 따라서 해당 로그는 남겨지지도 않는데 불필요한 연산이 일어나면서 쓸모없이 리소스를 사용하게 된다.)
  • log.debug("data={}", data)
    • 로그 출력 레벨을 info로 설정하면 아무일도 발생하지 않는다. 따라서 앞과 같은 의미없는 연산이 발생하지 않는다. (debug 메서드를 호출하면서 파라미터만 넘기기 때문에 아무 연산이 일어나지 않는다.)

로그 사용시 장점

  • 쓰레드 정보, 클래스 이름같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정할 수 있다.
  • 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절할 수 있다. (애플리케이션 코드를 수정하지 않고, 설정만으로 로그 레벨을 환경별로 다르게 지정할 수 있다.)
  • 시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등, 로그를 별도의 위치에 남길 수 있도록 설정할 수도 있다. (콘솔에 남기면서 파일로 남길수도 있다. 더 나아가 네트워크로 로그를 전송할 수도 있다.) 특히 파일로 남길 때는 일별, 특정 용량에 따라 로그를 분할하는 것도 가능하다.
  • 성능도 일반 System.out보다 좋다. (내부 버퍼링, 멀티 쓰레드 등등) 그래서 실무에서는 꼭 로그를 사용해야 한다.

✔️ 참고


3) 요청 매핑

요청 매핑이란, 요청이 왔을때 어떤 컨트롤러가 호출되어야하는지 매핑하는 것을 의미한다.
단순히 URL을 가지고 매핑하는 것 뿐만 아니라, 여러가지 요소들을 가지고 조합해서 매핑할 수 있다.

👉 먼저 간단하게 컨트롤러를 하나 만들어보자.

  • MappingController: src > main > java > hello > springmvc > basic 패키지 내 requestmapping 패키지를 생성하자. 그리고 내부에 MappingController 클래스를 생성하자.
    • 실행하면 정상적으로 로그와 함께, 브라우저에 "ok" 메시지가 노출된다.
    • 매핑 정보
      • @RestController
        • @Controller는 반환 값이 String이면 뷰 이름으로 인식된다. 그래서 뷰를 찾고 뷰가 랜더링 된다.
        • @RestController는 반환 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력한다. 따라서 실행 결과로 "ok" 메시지를 받을 수 있다. (이는 @ResponseBody와 관련이 있는데 뒤에서 더 자세히 설명한다.)
      • @RequestMapping("/hello-basic")
        • /hello-basic URL 호출이 오면 이 메서드가 실행되도록 매핑한다.
        • 대부분의 속성을 배열[] 로 제공하므로 다중 설정이 가능하다. {"/hello-basic", "/hello-go"}

 

HTTP 메서드 매핑

  • @RequestMappingmethod 속성으로 HTTP 메서드를 지정하지 않으면 HTTP 메서드와 무관하게 호출된다. (모두 허용 GET, HEAD, POST, PUT, PATCH, DELETE)
    • 위 예제에서도 HTTP 메서드를 지정하지 않았기에 GET으로 요청해도, POST로 요청해도, DELETE로 요청해도 다 정상적으로 처리된다. Postman으로 테스트 해보자.
    • method를 아래와 같이 지정할 수 있다. 수정 후 다시 확인해보자.
      • POST로 요청하면 스프링 MVC는 HTTP 405 상태코드(Method Not Allowed)를 반환된다.
      • (참고) 스프링은 @RestController 오류 모양을 JSON 스타일로 보내준다.
    • HTTP 메서드 매핑을 다음과 같이 축약할 수도 있다.
      • @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping
      • HTTP 메서드를 축약한 애노테이션을 사용하는 것이 더 직관적이다. 코드를 보면 내부에서 @RequestMapping 과 method 를 지정해서 사용하는 것을 확인할 수 있다.

 

PathVariable(경로 변수) 사용
: 요청 URL 자체에 값이 들어가있는 것. (경로 변수)

  • 참고)
    • 정상적으로 실행됨을 확인할 수 있다.
    • 최근 HTTP API는 다음과 같이 리소스 경로에 식별자를 넣는 스타일을 선호한다.
      • ex) /mapping/userA, /users/1
    • @RequestMapping 은 URL 경로를 템플릿화 할 수 있는데, @PathVariable 을 사용하면 매칭되는 부분을 편리하게 조회할 수 있다.
    • @PathVariable 의 이름과 파라미터 이름이 같으면 생략할 수 있다.
      • @PathVariable("userId") String data -> @PathVariable String userId

PathVariable 사용 - 다중 매핑

  • 참고)
    • 정상적으로 실행됨을 확인할 수 있다.

 

특정 파라미터 조건 매핑
: 쿼리 파라미터를 조건에 매핑할 수 있다. (URL 정보 뿐만 아니라, 파라미터 정보까지 매핑되어야 함.)

  • 참고)
    • 정상적으로 실행됨을 확인할 수 있다. (잘 사용하지는 않는다.)

 

특정 헤더 조건 매핑
: 파라미터 매핑과 비슷하지만, HTTP 헤더를 사용한다.

  • 참고)
    • 지정된 키와 값이 헤더 정보에 있으면 정상적으로 실행됨을 확인할 수 있다. (없다면 404 Not Found가 반환된다.)

 

미디어 타입 조건 매핑 - HTTP 요청 Content-Type, consume
: HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑한다.
( 만약 Content-Type에 따른 조건(application/json인지 text/html인지 등)을 넣고싶은 경우, 이 경우는 위 특정 헤더 조건 매핑의 예시처럼 headers를 사용하면 안되고, consume을 사용해야 한다. 왜냐하면 스프링 MVC에서 내부적으로 이것을 가지고 처리하는 것들이 있기 때문이다. )

  • 확인해보자.
    • consume: 컨트롤러가 소비하는 타입이 무엇이어야 하는지 지정 (HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑한다.)
    • 유효하지 않은 경우를 확인해보자.
      • Content-Type이 application/json이 아닌 경우, HTTP 415 상태코드(Unsupported Media Type)을 반환된다.

(참고) consumes 예시

  • consumes = "text/plain"
  • consumes = {"text/plain", "application/*"}
  • consumes = MediaType.TEXT_PLAIN_VALUE

 

미디어 타입 조건 매핑 - HTTP 요청 Accept, produce

  • 참고)
    • produces: 컨트롤러가 생산해내는 타입이 무엇어야 하는지 지정 (HTTP 요청의 Accept 헤더를 기반으로 미디어 타입으로 매핑한다.)
    • 유효하지 않은 경우를 확인해보자.
      • HTTP 406 상태코드(Not Acceptable)을 반환한다. (클라이언트 입장에서 나는 application/json만 받아들일 수 있다고 요청했는데, 서버가 생산하는 것은 text/html 이므로 406 상태코드를 반환한다.)

(참고) produces 예시

  • produces = "text/plain"
  • produces = {"text/plain", "application/*"}
  • produces = MediaType.TEXT_PLAIN_VALUE
  • produces = "text/plain;charset=UTF-8"

 

✔️ 지금까지 요청 매핑에 대해서 알아보았다.

  • 기본적인 URL 매핑
  • HTTP method 매핑
  • @PathVariable 경로 변수 사용
  • 특정 파라미터 조건 매핑
  • 특정 헤더 정보 매핑
  • 미디어 타입 조건 매핑 - HTTP 요청 Content-Type, consumes
  • 미디어 타입 조건 매핑 - HTTP 요청 Accept, produces

다음에는 지금까지 했던 요청 매핑을 API로 만드는 예시를 확인해보자.


4) 요청 매핑 - API 예시

회원을 관리하는 HTTP API를 만든다고 생각하고 매핑을 어떻게 하는지 알아보자.
(실제 데이터가 넘어가는 부분은 생략하고, URL 매핑하는 것만 예제를 통해 확인해보자.)

회원 관리 API (회원 관리 API 스펙을 다음과 같이 정했다고 가정하자.)

  • 회원 목록 조회: GET ( /users )
  • 회원 등록: POST ( /users )
  • 회원 조회: GET ( /users/{userId} )
  • 회원 수정: PATCH ( /users/{userId} )
  • 회원 삭제: DELETE ( /users/{userId} )
  • (참고)
    • 회원 (목록 조회 / 등록)
    • 회원 (조회 / 수정 / 삭제)
      • URL은 똑같이 제공하고, HTTP Method로 행위를 구분하였다.

👉 코드로 확인해보자.

  • MappingClassController: src > main > java > hello > springmvc > basic > requestmapping 패키지 내부에 MappingClassController 클래스를 생성하자. (생성 후 postman으로 실행해보자.)
    • 정상적으로 수행됨을 확인할 수 있다.
    • (참고) /mapping는 강의의 다른 예제들과 구분하기 위해 사용했다.
    • (참고) @RequestMapping("/mapping/users")
      • 클래스 레벨에 매핑 정보를 두면 메서드 레벨에서 해당 정보를 조합해서 사용한다.

👉 매핑 방법을 이해했으니, 이제부터 HTTP 요청이 보내는 데이터들을 스프링 MVC로 어떻게 조회하는지 알아보자.


강의를 듣고 정리한 글입니다. 코드와 그림 등의 출처는 김영한 강사님께 있습니다.

profile
현실에서 한 발자국

0개의 댓글