개발일기

Spring Boot MVC 기본기능 모음집 회고- 1편 본문

Spring/(김영한님)스프링 MVC 1편 - 백엔드 웹 개발 핵심기술

Spring Boot MVC 기본기능 모음집 회고- 1편

한둥둥 2024. 3. 27. 15:40

스프링 부트 로깅 라이브러리 

스프링 부트 로깅 라이브러리를 사용하기 위해서build.gradle 에다가 dependencies에 spring-boot-starter-logging을 포함해야 사용할 수 있다. 

 

새롭게 알게 된 지식은 Logback, Log4J, Log4J2 등등 수 많은 라이브러리들이 있는데, 그것을 통합해준 것이 sl4j라이브러리이다. 

인터페이스는 sl4j이고, 구현체는 Logback과 같은 로그 라이브러리를 선택하면 된다. 

 

private final Logger log = Loggerfactory.getLogger(getClass());

 

@Slf4j : 롬복 사용 가능 

 

추가적으로 좀 더 알게 된 사실은 log.info()로 호출하는 것과 System.out.println()사용하면 차이점이 있다는 것을 알게되었다. 

 

System.out.println()을 사용하면 표준출력으로 입력이 남아 파일로 남지 않고 log는 로깅 설정을 통해서 파일로 남길 수 있다. 

(로깅공부도 좀 더 해야될 것 같다.)

 

String name = "Spring";

log.trace("trace log = {}", name);
log.debufg("debug log = {}", name);
log.info("info log = {}", name);
log.warn("warn log= {}", name);
log.error("error log= {}", name);

// 로그를 사용하지 않아도 a + b로직 계산이 가장 먼저, 이루어지기 떄문에 
log.info("이런식으로 x = " + name);

 

 

@RestContoller

restContoller는 @Controller 와 @ResponseBody 애노테이션이 합쳐져 구현되어있는 것을 ResController에 들어가서 확인할 수 있다. 

  • Contoller는 반환 값이 String이면 뷰이름으로 인식된다.  
  • RestController는 View가 반환 되는게 아니라 HTTP 메시지 바디에 데이터를 넣어준다. 

 

요청 매핑 

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", "/hello-go"}, method = RequestMethod.GET)
    public String helloBasic(){
        log.info("helloBasic");
        return "ok";
    }

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

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

    /**
     * PathVariable 사용
     * 변수명이 같으면 생략 가능
     * @PathVariable("userId") String userId -> @PathVariable userId
     */
     @GetMapping("/mapping/users/{userId}/orders/{orderId}")
    public String mappingPath(@PathVariable("userId") String userId,@PathVariable("orderId") 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=good")
    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 헤더 기반 추가 매핑 MediaType
     * 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";
    }
}

 

 

{"/hello-basic", "/hello-go"} 처럼 다중 URL 설정이 가능하다. 

 

Http 메서드

@RequestMapping에 method속성으로 HTTP 메서드를 지정하지 않으면 HTTP 메서드와 무관하게 호출됩니다. 

 

이때, @RequestMapping은 url뒤에  콤마 찍고 method = RquestMethod.GET 또는 method = RequestMethod.POST이런식으로 구성 되어 있다. 

이걸 어노테이션을 통해서 메서드 매핑을 엄청 축약할 수 있는데 , @GetMapping, @PostMapping이런식으로 사용할 수 있음. 

현업에서는 대부분 이것만 씀.. 물론 @RequestMapping으로 되어있는 경우도 종종 보았음. 

 

 

@PathVariable(경로변수) 사용

URL 경로에 /mapping/hanseu9839 이런식으로 되어있으면 @PathVariable("userId") String data라고 매개변수를 받으면 String data = hanseu9839라는 값을 매핑하여 받을 수 있다. 

 

최근 방식을 보면 많은 사람들이 PathVariable을 사용하여 리소스 데이터를 찾는 방식을 선호하는 것 같다. 근데 만약에 회원정보나 이런걸 PathVariable로 한다면 /mapping/eunwha이런식으로 할 때, 다른 사용자들의 민감한 정보들이 다 보여진다면 이건 머리가 지끈거려 버리는 상황이 온다. 

 

@GetMapping("/mapping/users/{userId}/orders/{orderId}")

{userId} , {orderId} 를 통해서 위에처럼 두개 또는 여러개의 PathVariable을 받아 사용 할 수 있다. 

 

특정 파라미터 조건 매핑 

 

위에서 보다시피 특정 파라미터가 들어와야 조건적으로 매핑되도록 설정 할 수 있다. 

@GetMapping(value= "/mapping-param" , params = "mode=debug")이다. 

해당 부분은 parameter로 mode=debug가 있어야 Mapping이된다. 

URL 예시는 http://localhost:8080/mapping-param?mode=debug

 

이런식으로 특정 파라미터가 있거나 없거나의 조건을 넣어줄 수 있는데 사실 나는 실무에서 사용되는건 한번도 본적없는거 같다. 필자의 개발 경력이 짧은것도 한 몫 할 것이다.

 

특정 헤더 조건 매핑 

@GetMapping(value = "/mapping-header" , headers = "mode=debug")

public String mappingHeader() {



}

해당 코드에 자세한 부분은 위에 있다. 파라미터 매핑가 비슷한 형태이며 HTTP 헤더를 사용하여 진행한다. 

 

 

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

@PostMapping(value= "/mapping-consume" , consumes = "application/json")
public String mappingConsumes() {

}

 

HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑한다. 

만약 맞지 않으면 HTTP 415 상태코드(Unsupported Media Type)을 반환한다. 

 

예시 )

consumes = "text/plain"
comsumes = {"text/plain", "application/json"}

 

 

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

@PostMapping(value = "/mapping-produce" , produces = "text/html")
public String mappingProduces() {

}

 

HTTP 요청의 Accept 헤더를 기반으로 미디어 타입으로 매핑한다. 

만약 맞지 않으면 HTTP 406 상태 코드(Not Acceptable)을 반환한다. 

 

 

Http 요청 - 기본, 헤더 조회

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

이번 시간에는 HTTP 헤더 정보를 조회하는 방법을 알아보자.

 

 

RequestHeaderController

package hello.springmvc.basic.request;


import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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 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";
    }
}

 

- HttpMethod: HTTP 메서드를 조회한다. org.springframework.http.HttpMethod

- Locale: Locale 정보를 조회한다. 

- @RequestHeader MultiValueMap<String, String> headerMap 

      - 모든 HTTP 헤더를 MultiValueMap형식으로 조회한다. 

 

MultiValueMap은 중복이 허용된다. "A"라는 키가 있으면 150, 200, 340 등 여러개의 중복된 키값을 가지고 있다. 

해당 부분은 스프링을 정리하는 부분이기 때문에 여기까지만 정리하겠다..!

 

@RequestHeader("host") => 특정 헤더를 조회한다. 

속성에는 필수 값 여부 required , 기본 값 속성 : defaultValue 가 있다. 

 

@CookieValue(value = "myCookie", required = false) String cookie 

- 특정 쿠키를 조회한다. 

value에 해당하는 부분의 myCookie에 해당 하는 부분

 

 

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

 

클라이언트에서 서버로 요청 데이터 전달 시 3가지 방법

1. GET 쿼리 파라미터 

특징)

- /url?username=hello&age=20

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

 

2. POST - HTML Form 

특징)

- content-type : application/x-www-form-urlencoded

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

 

3. HTTP message body에 데이터 담아서 요청 

JSON , XML, TEXT  주로 JSON데이터 사용

 

 

Form은 request.parameter를 post, get이든 상관없이 조회할 수 있음 이게 중요함..! 

 

 

자세한 코드는 생략하겠다..!

 

 

페이지 생성 

 

/resources/static 아래에 두면 스프링이 prefix, sufix를 입력하지 않아도 자동으로 인식한다. 

추가적으로 jar를 이용하면 webapp경로를 사용할 수 없다.

 

 

HTTP 요청 파라미터  - @RequestParam

 

package hello.springmvc.basic.request;


import hello.springmvc.basic.HelloData;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

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

    @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 requestParam3(
            @RequestParam String username,
            @RequestParam int age) {
        log.info("username = {} , age = {}", username, age);
        return "ok";
    }


    @ResponseBody
    @RequestMapping("/request-param-v4")
    public String requestParam4(
            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
    ) {
        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";
    }

    @ResponseBody
    @RequestMapping("/model-attribute-v1")
    public String modelAttributeV1(@ModelAttribute HelloData helloData){
        log.info("username = {} , age = {} ", helloData.getUsername(), helloData.getAge());
        log.info("helloData = {} ", helloData);
        return "ok";
    }

    @ResponseBody
    @RequestMapping("/model-attribute-v2")
    public String modelAttributeV2(HelloData helloData) {
        log.info("username = {}, age= {}", helloData.getUsername(), helloData.getAge());
        return "ok";
    }
}

 

Servlet을 이용하는 방식은 위에 보이듯이 requestParamV1 버전이다.

request.getParameter를 통해서 각각의 parameter값을 가져와 준 모습이다. 

 

requestParamV2버전은 @RequestParam을 사용해서 서블릿처럼 파라미터 이름으로 뽑아오는 코드부분이 굉장히 깔끔하게 사라져 극락인 것을 볼 수 있다. 

 

requestParamV3는 @RequestParam의 이름이 매개변수로 받은 이름과 비슷하게 되어 name마저도 안 적어주는 대박박 현상이 보인다. 

 

requestParam은 required를 통해서 필수 parameter 입력 , 필수가 아닌 파라미터 입력 그리고 아무런 값도 Mapping이 되지 않았을 경우 defaultValue를 설정하여 사용 할 수 있다. 

 

@ResponseBody를 통해서 뷰가 아닌 데이터를 내보내는 모습도 추가적으로 참고하면 좋다. 

 

@RequestParam Map<String, Obejct> paramMap 이런식으로 Map 형식에 parameter받아서 사용할 수 있다. 해당 방식은 실무에서 사용하는 것을 한번 본적이 있던거 같다. MultiValueMap도 가능함. 

 

@ModelAttribute를 사용하여 요청 파라미터를 받아서 필요한 객체를 만들고 객체에 값을 넣어주는 Annotation이다. 

 

 

이 어노테이션을 사용하지 않는다면 

  @RequestParam String username;
  @RequestParam int age;
  HelloData data = new HelloData();
  data.setUsername(username);
  data.setAge(age);

이런식으로 작성해야 할 것이다. 

 

하지만 ModelAttribute 어노테이션이 HelloData 객체를 생성한 후, 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾고 해당 프로퍼티의 Setter를 찾아서 입력을 바인딩해준다. 

 

@ModelAttribute도 생략할 수 있다. 

스프링은 생략 시 다음과 같은 규칙을 적용한다. 

String, int, Integer => @RequestParam

@ModelAttribute (argument resolver에서 지정해둔 타입 외)