기본 기능

'스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 수업을 듣고 정리한 내용입니다.

 

📚 1. 프로젝트 생성

스프링 부트 스타터 사이트로 이동해서 스프링 프로젝트 생성
https://start.spring.io

프로젝트 선택

  • Project: Gradle Project
  • Language: Java
  • Spring Boot: 2.4.x

Project Metadata

  • Group: hello
  • Artifact: springmvc
  • Name: springmvc
  • Package name: hello.springmvc
  • Packaging: Jar (주의!)
  • Java: 11

의존성 관리

  • Spring Web
  • Thymeleaf
  • Lombok

 

주의!
PackingWar가 아니라 Jar 를 선택해주어야 한다.
JSP를 사용하지 않기 때문에 Jar를 사용하는 것이 좋다.
앞으로 스프링 부트를 사용하면 이 방식을 주로 사용하게 된다.
Jar를 사용하면 항상 내장 서버(톰캣등)을 사용하고, webapp 경로도 사용하지 않는다.
내장 서버 사용에 최적화 되어 있는 기능이다.
최근에는 주로 이 방식을 사용한다.
War를 사용하면 내장 서버도 사용가능 하지만, 주로 외부 서버에 배포하는 목적으로 사용한다.

 

(1) Welcome 페이지 만들기

스프링 부트에 Jar를 사용하면 /resources/static/index.html 위치에 index.html 파일을 두면 Welcome 페이지로 처리해준다.
(스프링 부트가 지원하는 정적 컨텐츠 위치에 /index.html)이 있으면 된다.)

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body> <ul>
    <li>로그 출력 <ul>
        <li><a href="/log-test">로그 테스트</a></li> </ul>
    </li>
    <!-- --> <li>요청 매핑
    <ul>
        <li><a href="/hello-basic">hello-basic</a></li>
        <li><a href="/mapping-get-v1">HTTP 메서드 매핑</a></li>
        <li><a href="/mapping-get-v2">HTTP 메서드 매핑 축약</a></li>
        <li><a href="/mapping/userA">경로 변수</a></li>
        <li><a href="/mapping/users/userA/orders/100">경로 변수 다중</a></li> <li><a href="/mapping-param?mode=debug">특정 파라미터 조건 매핑</a></li> <li><a href="/mapping-header">특정 헤더 조건 매핑(POST MAN 필요)</a></
        li>
        <li><a href="/mapping-consume">미디어 타입 조건 매핑 Content-Type(POST MAN 필요)</a></li>
        <li><a href="/mapping-produce">미디어 타입 조건 매핑 Accept(POST MAN 필요)</a></li>
    </ul> </li>
    <li>요청 매핑 - API 예시 <ul>
        <li>POST MAN 필요</li> </ul>
    </li>
    <li>HTTP 요청 기본
        <ul>
            <li><a href="/headers">기본, 헤더 조회</a></li>
        </ul> </li>
    <li>HTTP 요청 파라미터 <ul>
        v1</a></li>
        <li><a href="/request-param-v1?username=hello&age=20">요청 파라미터 <li><a href="/request-param-v2?username=hello&age=20">요청 파라미터

        v2</a></li>
        v3</a></li>
        v4</a></li>
        <li><a href="/request-param-v3?username=hello&age=20">요청 파라미터
        <li><a href="/request-param-v4?username=hello&age=20">요청 파라미터
        <li><a href="/request-param-required?username=hello&age=20">요청 파라미터 필수</a></li>
        <li><a href="/request-param-default?username=hello&age=20">요청 파라미터 기본 값</a></li>
        <li><a href="/request-param-map?username=hello&age=20">요청 파라미터 <li><a href="/model-attribute-v1?username=hello&age=20">요청 파라미터
        MAP</a></li>
        @ModelAttribute v1</a></li>
        <li><a href="/model-attribute-v2?username=hello&age=20">요청 파라미터 @ModelAttribute v2</a></li>
    </ul> </li>
    <li>HTTP 요청 메시지 <ul>
        <li>POST MAN</li>
    </ul>
    </li>
    <li>HTTP 응답 - 정적 리소스, 뷰 템플릿
        <ul>
            <li><a href="/basic/hello-form.html">정적 리소스</a></li> <li><a href="/response-view-v1">뷰 템플릿 v1</a></li> <li><a href="/response-view-v2">뷰 템플릿 v2</a></li>
        </ul> </li>
    <li>HTTP 응답 - HTTP API, 메시지 바디에 직접 입력 <ul>
        <li><a href="/response-body-string-v1">HTTP API String v1</a></li>
        <li><a href="/response-body-string-v2">HTTP API String v2</a></li>
        <li><a href="/response-body-string-v3">HTTP API String v3</a></li>
        <li><a href="/response-body-json-v1">HTTP API Json v1</a></li>
        <li><a href="/response-body-json-v2">HTTP API Json v2</a></li>
    </ul> </li>
</ul>

</body>
</html>

 

💡 참고

  • 스프링 부트 Welcome 페이지 지원
  • https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot- features.html#boot-features-spring-mvc-welcome-page

 

📚 2. 로깅 간단히 알아보기

  • 지금까지 공부하며 콘솔창에 실행결과나 기대값을 System.out.println()을 통해 출력하였다.
  • 실제 운영을 할 때는 시스템 콘솔이 아닌 별도의 로깅 라이브러리를 사용해 출력을 한다.
  • 많은 로깅 라이브러리가 있지만, 그 중 SLF4J, Logback 을 알아보자!

 

📖 A. 로깅 라이브러리

스프링 부트 라이브러리를 사용하면 스프링 부트 로깅 라이브러리 (spring-boot-starter-logging)가 함께 포함된다.

스프링 부트 로깅 라이브러리는 기본으로 다음 로깅 라이브러리를 사용한다.

  • SLF4J = http://www.slf4j.org
  • Logback : http://logback.qos.ch

 

  • 로그 라이브러리는 Logback, Log4J, Log4J2 등등 수많은 라이브러기가 있는데, 그것을 통합해서 인터페이스로 제공하는 것이 SLF4J 라이브러리이다.
  • SLF4J는 인터페이스이고, 그 구현체는 Logback과 같은 로그 라이브러리를 선택하면 된다.
  • 실무에서는 스프링 부트가 기본으로 제공하는 Logback을 대부분 사용한다.

 

📖 B. 사용법

로깅을 사용하기위해 먼저 로깅 객체를 생성해야 한다.

(1) 클래스 참조변수 선언


// getClass()메서드를 통해 사용되는 클래스 타입 변환하여 삽입
private final Logger log = LoggerFactory.getLogger(getClass());

// 직접적으로 해당 클래스타입을 입력해주어도 된다.
private static final logger log = LoggerFactory.getLogger(Xxx.class);

 

(2) 롬북(Lombok) @Slf4j

@Slf4j
public class LogTestController {
	...
}

@Slf4j 를 넣어두면
private final Logger log = LoggerFactory.getLogger(getClass()); 와 같이 log 생성자를 호출할 필요가 없다.
(자동 등록)

 

(3) 로그 호출

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

 

📖 C. 소스코드

main.java.springmvc.basic.LogTestController

package hello.springmvc.basic;

import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class LogTestController {

//    private final Logger log = LoggerFactory.getLogger(getClass());

    @RequestMapping("/log-test")
    public String logTest() {
        String name = "Spring";
        log.trace("trace log={}", name);
        log.debug("debug log={}", name);
        log.info(" info log={}", name);
        log.warn(" warn log={}", name);
        log.error("error log={}", name);
        // 로그를 사용하지 않아도 a+b 계산 로직이 먼저 실행됨, 이런 방식으로 사용하면 X log.debug("String concat log=" + name);

        return "ok";
        // RestController
        // 문자 반환시 String이 반환된다.
    }
}
  • @RestController
    ➡️ @Controller는 반환 값이 String이면 뷰 이름으로 인식하기에 뷰를 찾고 뷰가 렌더링된다.
    ➡️ @RestController는 반환 값으로 뷰를 찾는게 아니라 HTTP 메세지 바디에 바로 입력한다.
    (클래스 레벨이 아닌 메서드 레벨에서 @ResponseBody를 사용하면 @Controller를 사용하더라도 바로 HTTP 메세지 바디에 입력해서 반환을 해준다.)

  • 로그 출력 포맷

    ➡️ 시간, 로그 레벨, 프로세스 ID(PID), 쓰레드 명, 클래스 명, 로그 메세지

 

📖 D. 로그 레벨

5개의 로그를 출력시도 하였지만, 실행결과에서는 3개가 나왔다.

이유

  • 로그에는 레벨이 있다. 로그레벨을 설정하면 그 로그 보다 우선순위가 높은 것만 출력된다.
  • 스프링 부트에서 기본으로 설정되어 있는 로그레벨은 info이다.
  • 그렇기에 info보다 우선순위가 낮은 debug, trace는 출력되지 않는다.
  • info를 줄시 info, warn, error가 출력된다.

 

(1) 로그 레벨 설정

원하는대로 로그 레벨을 설정할 수 있다.
application.properties에서 레벨을 설정할 수 있다.

# 전체 로그 레벨 설정(기본 info)
logging.level.root=info

# hello.springmvc 패키지와 그 하위 로그 레벨 설정
#logging.level.hello.springmvc=[변경을 원하는 로그 레벨]
logging.level.hello.springmvc=debug

 

(2) 로그 레벨 우선순위

  • LEVEL : TRACE > DEBUG > INFO > WARN > ERROR
  • 개발 서버는 debug 출력
  • 운영 서버는 info 출력

 

(3) 올바른 로그 사용법

log.debug("data"+data) : 올바르지 않은 로그 사용

  • 로그 출력 레벨을 info로 설정해도 해당 코드에 있는 "data="+data가 실제 실행이 되어 버린다.
  • 문자 더하기 연산이 발생한다.
  • 리소스 낭비이다.

 

log.debug("data={}", data) : 올바른 로그 사용

  • 이와 같이 작성시 로그 출력 레벨을 info로 설정하면 아무 일도 발생하지 않는다. (DEBUG > INFO)
  • 앞과 같은 의미없는 연산이 발생하지 않는다.

 

📖 E. 로그 사용시 장점

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

 

💡 참고
스프링 부트가 제공하는 로그 기능은 다음을 참고하자 : https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot- features.html#boot-features-logging

 

📚 3. 요청 매핑

요청 매핑 : 요청이 왔을 때 어떤 컨트롤러에서 매핑을 할지 조사해서 매핑을 진행하는 것

 

MappingController

(1) 기본 매핑(RequestMapping)

package hello.springmvc.basic.requestmapping;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

@RestController
public class MappingController {

    private Logger log = LoggerFactory.getLogger(getClass());

    /**
     * 기본 요청
     * 둘다 허용 /hello-basic, /hello-basic/
     * HTTP 메서드 모두 허용 GET, HEAD, POST, PUT, PATCH, DELETE */
    //    @RequestMapping({"/hello-basic", "/hello-go"})
    @RequestMapping("/hello-basic")
    public String helloBasic() {
        log.info("helloBasic");
        return "ok";
    }

...
}
	
  • @RestController
    ➡️ @Controller는 반환 값이 String이면 뷰 이름으로 인식된다. 그래서 뷰를 찾고 뷰가 렌더링 된다.
    ➡️ @RestController는 반환 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력한다. (따라서 실행 결과로 ok 메세지를 받을 수 있다. @ResponseBody)와 관련이 있다.

     

  • @RequestMapping("/hello-basic")
    ➡️ /hello-basic URL 호출이 오면 이 메서드가 실행되도록 매핑한다.
    ➡️ 대부분의 속성을 배열[]로 제공하므로 다중 설정이 가능하다. (ex. {"/hello-basic", "/hello-go"} )

 

둘다 허용
다음 두가지 요청은 다른 URL이지만, 스프링은 다음 URL 요청들을 같은 요청으로 매핑한다.

  • 매핑 : /hello-basic
  • URL 요청 : /hello-basic, /hello-basic/

HTTP 메서드

  • @RequestMapping 에 method 속성으로 HTTP 메서드를 지정하지 않으면 HTTP 메서드와 무관하게 호출된다.
  • 모두 허용 : GET, HEAD, POST, PUT, PATCH, DELETE

 

(2) HTTP 메서드 매핑

    /**
     * method 특정 HTTP 메서드 요청만 허용
     * GET, HEAD, POST, PUT, PATCH, DELETE
     */
    @RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
    public String mappingGetV1() {
        log.info("mappingGetV1");
        return "ok";
    }
  • 여기에 POST 요청을 하면 스프링 MVC는 HTTP 405 상태코드(Method Not Allowed)를 반환한다.

 

(3) HTTP 메서드 매핑 축약

    /**
     * 편리한 축약 애노테이션 (코드보기) 
	 * @GetMapping
     * @PostMapping
     * @PutMapping
     * @DeleteMapping
     * @PatchMapping
     */
    @GetMapping(value = "/mapping-get-v2")
    public String mappingGetV2() {
        log.info("mapping-get-v2");
        return "ok";
    }
  • 매번 method 속성을 설정해서 HTTP 메서드를 지정해주는게 번거롭고 가독성도 떨어지기에 축약한 애노테이션을 사용하는 것이 더 직관적이다.
  • GetMapping, PostMapping, PatchMapping, DeleteMapping
    ➡️ 코드를 보면 애노테이션 내부에서 @RequestMappingmethod를 지정해서 사용하는 것을 확인할 수 있다.

 

(4) PathVariable(경로 변수) 사용

    /**
     * PathValuer
     * PathVariable 사용
     * 변수명이 같으면 생략 가능
     *
     * @PathVariable("userId") String userId -> @PathVariable userId
     * /mapping/userA
     */
    @GetMapping
    public String mappingPath(@PathVariable("userId") String data) {
        log.info("mappingPath userId={}", data);
        return "ok";
    }

실행

  • http://localhost:8080/mapping/userA

 

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

 

(5) PathVariable 사용 - 다중

/**
     * PathVariable 사용 다중
     */
    @GetMapping("/mapping/users/{userId}/orders/{orderId}")
    public String mappingPath(@PathVariable String userId, @PathVariable Long
            orderId) {
        log.info("mappingPath userId={}, orderId={}", userId, orderId);
        return "ok";
    }

실행

  • http://localhost:8080/mapping/users/userA/orders/100
  • 하나 이상의 PathVariable도 사용이 가능하다.

 

(6) 특정 파라미터 조건 매핑

    /**
     * 파라미터로 추가 매핑
     * params="mode",
     * params="!mode"
     * params="mode=debug"
     * params="mode!=debug" (! = )
     * params = {"mode=debug","data=good"}
     */
    @GetMapping(value = "/mapping-param", params = "mode=debug")
    public String mappingParam() {
        log.info("mappingParam");
        return "ok";
    }

실행

  • http://localhost:8080/mapping-param?mode=debug
  • 특정 파라미터를 조건식으로 매핑해서 매핑여부를 결정할 수 있다.
  • 현재는 잘 사용하지 않는다.

 

(7) 특정 헤더 조건 매핑

    /**
     * 특정 헤더로 추가 매핑
     * headers="mode",
     * headers="!mode"
     * headers="mode=debug"
     * headers="mode!=debug" (! = )
     */
    @GetMapping(value = "/mapping-header", headers = "mode=debug")
    public String mappingHeader() {
        log.info("mappingHeader");
        return "ok";
    }

  • 특정 파라미터 매핑과 동일하게 헤더 역시 조건 매핑이 가능하다.
  • HTTP 헤더를 사용한다.

 

(8) 미디어 타입 조건 매핑 - HTTP 요청 Content-Type, consume

    /**
     * Content-Type 헤더 기반 추가 매핑 Media Type
     * consumes="application/json"
     * consumes="!application/json"
     * consumes="application/*"
     * consumes="*\/*"
     * MediaType.APPLICATION_JSON_VALUE
     */
    @PostMapping(value = "/mapping-consume", consumes = "application/json")
    public String mappingConsumes() {
        log.info("mappingConsumes");
        return "ok";
    }
  • HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑한다.
  • 일치하지 않을 경우 HTTP 415 상태코드(Unsupported Media Type)을 반환한다.
  • 조건을 배열로 설정할 수도 있고 상수로 제공하는 매직넘버를 사용해도 된다.
    ex)
consumes = "text/plain"
consumes = {"text/plain", "application/*"}
consumes = MediaType.TEXT_PLAIN_VALUE

 

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

    /*
     * Accept 헤더 기반 Media Type
     * produces = "text/html"
     * produces = "!text/html"
     * produces = "text/*"
     * produces = "*\/*"
     */
    @PostMapping(value = "/mapping-produce", produces = "text/html")
    public String mappingProduces() {
        log.info("mappingProduces");
        return "ok";
    }
  • HTTP 요청의 Accept 헤더를 기반으로 미디어 타입으로 매핑한다.
  • 만약 맞지 않으면 HTTP 406 상태코드(Not Acceptable)을 반환한다.
    ex)
produces = "text/plain"
produces = {"text/plain", "application/*"}
produces = MediaType.TEXT_PLAIN_VALUE
produces = "text/plain;charset=UTF-8"

 

📚 4. 요청 매핑 - API 예시

회원관리를 HTTP API로 만든다 생각하고 매핑을 어떻게 하는지 알아보자!
HTTP API 구조만 만들자!
(실제 데이터가 넘어가는 부분은 생략하고 URL 매핑만)

 

회원 관리 API

  • 회원 목록 조회 : GET /users
  • 회원 등록 : POST /users
  • 회원 조회 : GET /users/{userId}
  • 회원 수정 : PATCH /users/{userId}
  • 회원 삭제 : DELETE /users/{userId}

 

MappingClassController

package hello.springmvc.basic.requestmapping;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {

    /**
     * GET /mapping/users
     */
    @GetMapping
    public String users() {
        return "get users";
    }
    /**
     * POST /mapping/users
     */
    @PostMapping
    public String addUser() {
        return "post user";
    }
    /**
     * GET /mapping/users/{userId}
     */
    @GetMapping("/{userId}")
    public String findUser(@PathVariable String userId) {
        return "get userId=" + userId;
    }
    /**
     * PATCH /mapping/users/{userId}
     */
    @PatchMapping("/{userId}")
    public String updateUser(@PathVariable String userId) {
        return "update userId=" + userId;
    }
    /**
     * DELETE /mapping/users/{userId}
     */
    @DeleteMapping("/{userId}")
    public String deleteUser(@PathVariable String userId) {
        return "delete userId=" + userId;
    }
}

  • /mapping : 강의의 다른 예제들과 구분하기 위해 사용
  • @RequestMapping("/mapping/users") : 클래스 레벨에 매핑 정보를 두면 메서드 레벨에서 해당 정보를 조합해서 사용한다.

 

📖 A. Postman으로 테스트

  • 회원 목록 조회 : GET /users
  • 회원 등록 : POST /users
  • 회원 조회 : GET /users/userA
  • 회원 수정 : PATCH /users/userA
  • 회원 삭제 : DELETE /users/userA

 

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

 


참고

좋은 웹페이지 즐겨찾기