[Spring MVC] [1] 6. 스프링 MVC - 기본 기능_1

윤경·2021년 9월 9일
0

Spring MVC

목록 보기
8/26
post-thumbnail

[1] 프로젝트 생성

📌 왜 꼭 Jar를 쓰는지 ?

JSP를 사용하지 않기 때문에 Jar를 사용하는 것이 좋다.
Jar를 사용하면 항상 내장 서버 톰캣등을 사용하고 webapp 경로도 사용하지 않는다. 내장 서버 사용에 최적화 된 기능이기 때문이다.

War를 사용해도 내장서버 사용이 가능하지만 이는 주로 외부 서버에 배포하는 목적으로 사용한다.

생성한 프로젝트의 압축을 풀어서 열고 아래의 설정을 해준다.

그리고 프로젝트를 생성했으면 한 번 메인 메소드가 있는 파일을 돌려보자.

✔️ 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>
            <li><a href="/request-param-v1?username=hello&age=20">요청 파라미터 v1</a></li>
            <li><a href="/request-param-v2?username=hello&age=20">요청 파라미터 v2</a></li>
            <li><a href="/request-param-v3?username=hello&age=20">요청 파라미터 v3</a></li>
            <li><a href="/request-param-v4?username=hello&age=20">요청 파라미터 v4</a></li>
            <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">요청 파라미터 MAP</a></li>
            <li><a href="/model-attribute-v1?username=hello&age=20">요청 파라미터 @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>

이는 앞으로 배울 내용에 대해 좀 더 편리하게 참고하기 위함

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


[2] 로깅 간단히 알아보기

별도의 로깅 라이브러리를 사용해 로그를 출력해보자.

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

로그 라이브러리는 여러 가지가 있는데 이것들을 통합해 인터페이스로 제공하는 것이 SLF4J 라이브러리이다. 그리고 그 구현체Logback 같은 로그 라이브러리를 선택하면 된다. (대부분 Logback 사용)

로그 선언
private Logger log = LoggerFactory.getLogger(getClass());
private static final Logger log = LoggerFactory.getLogger(Xxx.class)

@Slf4j : 롬복 사용 가능

로그 호출
log.info("hello")

✔️ LogTestController

package hello.springmvc.basic;

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

@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를 사용하는 이유
: 그냥 @Controller를 사용하지 않는 이유는 이것은 뷰를 찾고 뷰가 랜더링되는 반면,
@RestControllerHTTP 메시지 바디에 바로 입력되어 실행 결과로 메시지를 바로 받을 수 있다.

로그 레벨 설정
LEVEL: TRACE > DEBUG > INFO > WARN > ERROR

(개발 서버는 debug 출력, 운영 서버는 info 출력)

📌 올바른 로그 사용법

log.debug("data="+data) ❌
: 로그 출력 레벨을 info로 설정해도 해당 코드에 있는 "data="+data가 실제로 실행 되어버린다.
즉, 쓰지도 않는데 쓸모없는 연산이 발생해버린다.

log.debug("data={}", data) ⭕️
: 로그 출력 레벨을 info로 설정하면 아무일도 발생하지 않아 의미 없는 연산이 발생하지 않는다.


[3] 요청 매핑

: "요청이 왔을 때 어떤 컨트롤러가 매핑이 되어야 하지?"

✔️ MappingController

package hello.springmvc.basic.requestmapping;

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

@RestController
public class MappingController {

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

    @RequestMapping(value = "/hello-basic", method = RequestMethod.GET)
    private String helloBasic() {
        log.info("helloBasic");
        return "ok";
    }

    @RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
    public String mappingGetV1() {
        log.info("mappingGetV1");
        return "ok";
    }

    /**
     * 편리한 축약 애노테이션 (코드보기)
     * @GetMapping
     * @PostMapping
     * @PutMapping
     * @DeleteMapping
     * @PatchMapping
     */
    @GetMapping(value = "/mapping-get-v2")
    public String mappingGetV2() {
        log.info("mapping-get-v2");
        return "ok";
    }

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

    /**
     * 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";
    }

    /**
     * 파라미터로 추가 매핑
     * 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";
    }

    /**
     * 특정 헤더로 추가 매핑
     * 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";
    }

    /**
     * Content-Type 헤더 기반 추가 매핑 Media Type
     * consumes="application/json"
     * consumes="!application/json"
     * consumes="application/*"
     * consumes="*\/*"
     * MediaType.APPLICATION_JSON_VALUE
     */
    @PostMapping(value = "/mapping-consume", consumes = MediaType.APPLICATION_JSON_VALUE)
    public String mappingConsumes() {
        log.info("mappingConsumes");
        return "ok";
    }

    /**
     * Accept 헤더 기반 Media Type
     * produces = "text/html"
     * produces = "!text/html"
     * produces = "text/*"
     * produces = "*\/*"
     */
    @PostMapping(value = "/mapping-produce", produces = MediaType.TEXT_HTML_VALUE)
    public String mappingProduces() {
        log.info("mappingProduces");
        return "ok";
    }
}

@RequestMapping("/hello-basic")

: URL 호출이 오면 이 메소드를 실행하도록 매핑.
대부분의 속성을 배열[]로 제공하므로 다중 설정가능.

HTTP 메서드

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

HTTP 메서드 매핑

: HTTP 메서드를 축약한 애노테이션을 사용하는 것이 더 직관적이다. 코드를 보면 내부에서 @RequestMapping 과 method 를 지정해서 사용하는 것을 확인할 수 있다.

⭐️ PathVariable(경로 변수) 사용

: 최근 HTTP API는 다음과 같이 리소스 경로에 식별자를 넣는 스타일을 선호한다.

@RequestMapping 은 URL 경로를 템플릿화 할 수 있는데, @PathVariable 을 사용하면 매칭 되는 부분을 편리하게 조회할 수 있다.
@PathVariable 의 이름과 파라미터 이름이 같으면 생략할 수 있다. (단, 이 자체를 생략하는 건 불가능)


[4] 요청 매핑 - API 예시

📌 회원 관리 API

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

➡️ URL은 똑같지만 HTTP 메소드로 동작을 구분할 수 있음.

✔️ MappingClassController

package hello.springmvc.basic.requestmapping;

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

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

    @GetMapping
    public String user() {
        return "Get users";
    }

    @PostMapping
    public String addUser() {
        return "post user";
    }

    @GetMapping("/{userId}")
    public String findUser(@PathVariable String userId) {
        return "get userId = " + userId;
    }

    @PatchMapping("/{userId}")
    public String updateUser(@PathVariable String userId) {
        return "patch userId = " + userId;
    }

    @DeleteMapping("/{userId}")
    public String deleteUser(@PathVariable String userId) {
        return "delete userId = " + userId;
    }
}

(/mapping: 강의의 다른 예제들과의 구분을 두기 위함)


[5] HTTP 요청 - 기본, 헤더 조회

애노테이션 기반의 스프링 컨트롤러는 다양한 파라미터를 지원

✔️ RequestHeaderController

package hello.springmvc.basic.request;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

@Slf4j
@RestController
public class RequestHeaderController {

    @RequestMapping("/headers")
    public String headers(HttpServletRequest request,
                          HttpServletResponse response,
                          HttpMethod httpMethod,
                          Locale locale,
                          @RequestHeader MultiValueMap<String, String> headerMap,
                          @RequestHeader("host") String host,
                          @CookieValue(value = "myCookie", required = false) String cookie) {

        log.info("request={}", request);
        log.info("response={}", response);
        log.info("httpMethod={}", httpMethod);
        log.info("locale={}", locale);
        log.info("headerMap={}", headerMap);
        log.info("header host={}", host);
        log.info("myCookie={}", cookie);

        return "ok";
    }
}

HttpServletRequest, HttpServletResponse

HttpMethod: HTTP 메서드를 조회

Locale: Locale 정보를 조회

@RequestHeader MultiValueMap<String, String> headerMa: 모든 HTTP 헤더를 MultiValueMap 형식으로 조회

@RequestHeader("host") String host: 특정 HTTP 헤더를 조회

  • 속성
    필수 값 여부: required, 기본 값 속성: defaultValue

@CookieValue(value = "myCookie", required = false) String: 특정 쿠키를 조회

  • 속성
    필수값 여부: required, 기본 값 속성: defaultValue

📌 MultiValueMap

: Map과 유사하지만 하나의 키에 여러 값을 받을 수 있다.
HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러개의 값의 받을 때 사용
(keyA=value1&keyA=value2)

📌 @Slf4j

: 코드를 자동으로 생성해 로그를 선언해줘, 개발자는 편리하게 log라고 사용하면 된다.


[6] HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form

클라이언트에서 서버로 요청 데이터를 전달할 때 주로 사용하는 세 가지 방법

- GET - 쿼리 파라미터
: 메시지 바디 없이 URL의 쿼리 파라미터에 데이터를 포함해 전달

- POST - HTML Form
: 메시지 바디에 쿼리 파라미터 형식으로 전달

- HTTP message body에 직접 데이터를 담아 요청
: HTTP API에 주로 사용 (데이터 형식은 대부분 JSON)

✔️ RequestParamController

package hello.springmvc.basic.request;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
@Controller
public class RequestParamController {

    @RequestMapping("/request-param-v1")
    public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        log.info("username={}, age={}", username, age);

        response.getWriter().write("ok");
    }
}

✔️ hello-form.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<form action="/request-param-v1" method="post">
  username: <input type="text" name="username" />
  age:      <input type="text" name="age" />
  <button type="submit">전송</button>
</form>
</body>
</html>


[7] HTTP 요청 파라미터 - @RequestParam

✔️ RequestParamController (코드 추가)

package hello.springmvc.basic.request;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

@Slf4j
@Controller
public class RequestParamController {

    @RequestMapping("/request-param-v1")
    public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        log.info("username={}, age={}", username, age);

        response.getWriter().write("ok");
    }

    // 위에서 그냥 controller 어노테이션을 썼기 때문에 뷰이름으로 찾게 됨.
    // 우리가 원하는건 http 응답 메시지로 바로 받길 원하므로 붙여주는 애노테이션
    @ResponseBody
    @RequestMapping("/request-param-v2")
    public String requestParamV2(
            @RequestParam("username") String memberName,
            @RequestParam("age") int memberAge
    ) {
        log.info("username={}, age={}", memberName, memberAge);

        return "ok";
    }

    @ResponseBody
    @RequestMapping("/request-param-v3")
    public String requestParamV3(
            @RequestParam String username,
            @RequestParam int age) {

        log.info("username={}, age={}", username, age);

        return "ok";
    }
    
    @ResponseBody
    @RequestMapping("/request-param-v4")
    public String requestParamV4(String username, int age) {
        log.info("username={}, age={}", username, age);
        return "ok";
    }

    @ResponseBody
    @RequestMapping("/request-param-required")
    public String requestParamRequired(
            @RequestParam(required = true) String username,
            @RequestParam(required = false) Integer age) { // int=null은 안되지만 Integer에는 null이 들어갈 수 있음

        log.info("username={}, age={}", username, age);
        return "ok";
    }

    @ResponseBody
    @RequestMapping("/request-param-default")
    public String requestParamDefault(
            @RequestParam(required = true, defaultValue = "guest") String username,
            @RequestParam(required = false, defaultValue = "-1") int age) {

        log.info("username={}, age={}", username, age);
        return "ok";
    }

    @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";
    }
}

✔️ requestParamV2
@RequestParam: 파라미터 이름으로 바인딩
@ResponseBody: View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력

@RequestParam의 name(value) 속성이 파라미터 이름으로 사용
@RequestParam("username") String memberName ➡️ request.getParameter("username")

✔️ requestParamV3
HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능

✔️ requestParamV4
String, int, Integer 등 단순 타입이면 @RequestParam 생략 가능

@RequestParam을 생략하면 스프링 MVC는 내부에서 required=false를 적용.

🙋🏻‍♀️ 그런데 강의 선생님은 너무 애노테이션을 이렇게 막 생략하는 건 좀 과하다고,,^^
@RequestParam이 있으면 명확히 요청 파라미터에서 데이터를 읽는다는걸 알 수 있으니까 !

✔️ 파라미터 필수 여부 - requestParamRequired
@RequestParam.required: 파라미터 필수 여부. 기본값이 true.

‼️ 파라미터 이름만 사용
파라미터 이름만 있고 값이 없는 경우 빈 문자로 통과됨
(null과 ""는 다름! ""는 통과 가능)

‼️ 기본형 (primitive)에 null 입력
int에 null을 입력할 수 없음 (500예외)
Integer에는 null이 들어갈 수 있으므로 Integer 사용

✔️ 기본 값 적용 - requestParamDefault
파라미터에 값이 없는 겨우 defaultValue를 사용하면 기본 값을 적용할 수 있음.
이미 기본 값이 있기 때문에 required는 필요 없음.

✔️ 파라미터를 Map으로 조회하기 - requestParamMap
파라미터를 Map, MultiValueMap(하나의 키에 여러가지 값)으로 조회 가능


너무 기계적으로 공부하고 있어서 헷갈리는 부분부터 다시 강의 듣고 이어서 _2부터 쓸게요,,😸

profile
개발 바보 이사 중

0개의 댓글