개발/Spring MVC

스프링 MVC- 기본 기능 (로깅, HTTP 요청)

Debin 2022. 1. 9.
반응형
본 게시글은 인프런 김영한 선생님 강의 스프링 MVC 1편을 완강하고 배운 것을 남기고자 적은 포스팅입니다.
(2022.08.07 수정) - 복습을 하면서, 기억할 부분 설명 추가.
 

Logging (로깅)

 

운영 시스템에서는 System.out.println 같은 시스템 콘솔을 사용해서 필요한 정보를 출력하지 않고,

별도의 로깅 라이브러리를 사용해서 로그를 출력한다. 로그 관련 라이브러리에 대해 정말 간단하게 알아보겠다.

로깅 라이브러리

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

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

 

  • SLF4J
  • Logback

로그 라이브러리는 Logback, Log4J, Log4J2 등등 수많은 라이브러리가 있는데,

그것을 통합해서 인터페이스로 제공하는 것이 바로 SLF4J 라이브러리다.

쉽게 이야기해서 SLF4J는 인터페이스이고, 그 구현체로 Logback 같은 로그 라이브러리를 선택하면 된다.

실무에서는 스프링 부트가 기본으로 제공하는 Logback을 대부분 사용한다고 한다.

로그 선언

  • private Logger log = LoggerFactory.getLogger(getClass());
  • private static final Logger log = LoggerFactory.getLogger(Xxx.class)
  • @Slf4j : 롬복 사용 가능

다음은 로그를 테스트한 클래스다.

//@Slf4j를 사용하면 private final Logger log= LoggerFactory.getLogger(getClass()); 이 코드를 안적어도됨
@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);
        //로그의 레벨을 정할 수 있다.

        return "ok";
    }
}

 

 

  • 로그가 출력되는 포맷 확인
    • 시간, 로그 레벨, 프로세스 ID, 스레드 명, 클래스명, 로그 메시지
  • 로그 레벨 설정을 변경해서 출력 결과를 보자.
    • LEVEL: TRACE > DEBUG > INFO > WARN > ERROR
    • 개발 서버는 debug 출력
    • 운영 서버는 info 출력

로그 레벨 설정은 application.properties에서 가능하다. 아래 코드를 추가해보자.

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

#hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=debug

올바른 로그 사용법 

  • log.debug("data="+data)
    • 로그 출력 레벨을 info로 설정해도 해당 코드에 있는 "data="+data가 실제 실행이 되어 버린다.
      결과적으로 문자 더하기 연산이 발생한다.
  • log.debug("data={}", data)
    • 로그 출력 레벨을 info로 설정하면 아무 일도 발생하지 않는다. 따라서 앞과 같은 의미 없는 연산이 발생하지 않는다.

로그 사용 장점

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

spring mvc 기본 기능

@RestController

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

@RequestMapping("/hello-basic");

  • /hello-basic URL 호출이 오면 이 메서드가 실행되도록 매핑한다.
  • 대부분의 속성을 배열[]로 제공하므로 다중 설정이 가능하다. {"/hello-basic", "/hello-go"}
  • 위와 같은 경우는 HTTP 메서드를 지정하지 않아서 HTTP 어떤 메서드에서도 URL에 대해 요청이 들어오면 매핑된다. 따라서 HTTP 메서드를 지정해야 한다.

PathVariable(경로 변수) 사용

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

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

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

 

다음은 특정 파라미터 조건이 들어와야 매핑되는 경우다. 잘 사용되지 않는다고 한다.

    @GetMapping(value = "/mapping-param", params = "mode=debug") //쿼리 파라미터가 조건을 만족하면 매핑된다.
    public String mappingParam() {
        log.info("mappingParam");
        return "ok";
    }

이제 특정 헤더 조건으로 매핑하고, 미디어 타입 조건 매핑에 대해 한 번에 코드로 알아보자.

미디어 타입 조건 매핑은 Accept 헤더 기반 미디어 타입으로 매핑하고 Content-Type 헤더를 기반으로 매핑하는 법에 대해 알아보겠다.

/**
     * 특정 헤더로 추가 매핑
     * 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 = "application/json") //json 타입의 컨텐트 타입이면 매핑됨
    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 = "text/html")//accept 즉 클라이언트가  받아들일 수 있는 것이 html이면 매핑됨.
    public String mappingProduces() {
        log.info("mappingProduces");
        return "ok";
    }
}

이렇게 요청 매핑에 대해 알아보았다.

 

HTTP 요청 기본, 헤더 조회

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

이번 에는 HTTP 헤더 정보를 조회하는 방법 코드로 알아보자.

@Slf4j //로그 찍기 위함
@RestController
public class RequestHeaderController {

    @RequestMapping("/headers")
    public String headers(
            HttpServletRequest req,
            HttpServletResponse res,
            HttpMethod httpMethod, //HTTP 메서드 조회
            Locale locale, //언어 관련 헤더, Locale 정보 조회
            @RequestHeader MultiValueMap<String,String> headerMap, //헤더를 다 받는다. multiValuemMap는 한 키에 여러 값을 가질 수 있다.
     		//HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용
     		@RequestHeader("host") String host, //http 특정 헤더 조회
            @CookieValue(value = "MyCookie",required = false) String cookie //특정 쿠키를 조회
            ){
        log.info("request={}", req);
        log.info("response={}", res);
        log.info("httpMethod={}", httpMethod);
        log.info("locale={}", locale);
        log.info("headerMap={}", headerMap);
        log.info("header host={}", host);
        log.info("myCookie={}", cookie);
        return "ok";
    }
}

다음은 HTTP 요청 쿼리 파라미터와 HTML FORM에 대해 알아보겠다.

HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방식은 크게 3가지가 있다고 저번에 정리했다.

 

  • GET- 쿼리 파라미터
  • POST - HTML FORM
  • HTTP message Body에 데이터를 직접 담아서 요청 ex HTTP API(JSON)

이전에 학습을 통해서는 HttpServletRequest의 request.getParameter()를 사용하면

GET 쿼리 파라미터와 POST HTML FORM 방식을 둘 다 조회할 수 있다고 배웠다. 이것을 간단히 요청 파라미터 조회라고 한다.

이제 스프링으로 요청 파라미터를 조회하는 방법을 알아보자. 

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

 

먼저 HTML FORM을 통해 메시지 바디에 쿼리 파라미터 형식으로 username, age를 서버로 전달한다고 생각하자.

/**
     * @RequestParam 사용
     * - 파라미터 이름으로 바인딩
     * @ResponseBody 추가
     * - View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력
     * HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="X") name 생략가능
     *   @RequestParam("username") String memberName -> @RequestParam String username
     */
    @ResponseBody
    @RequestMapping("/request-param-v2")
    public String requestParamV2(
            @RequestParam("username") 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")

 

위 코드도 좋지만 더욱 발전한 버전을 몇 가지 코드로 살펴보겠다.

/**
     * @RequestParam 사용
     * String, int 등의 단순 타입이면 @RequestParam 도 생략 가능
     */
    @ResponseBody
    @RequestMapping("request-param-v4")
    public String requestParamV4(String username, int age){//요청 파라미터 이름과 같다면 성공적
        log.info("username={}, age={}",username,age);
        return "ok";
        //String , int , Integer 등의 단순 타입이면 @RequestParam 도 생략 가능
    }
    
    
    @ResponseBody
    @RequestMapping("request-param-required")
    public String requestParamRequired(
        @RequestParam(required = false) String username, //기본 값이 required = true다. 즉 파라미터가 필수다.
        @RequestParam(required = true) Integer age //int는 null을 받을 수 없으므로 주의. Wrapper 클래스를 사용
    ){
        log.info("username={}, age={}", username, age);
        return "ok";
    }
    
    **
     * @RequestParam
     * - defaultValue 사용
     *
     * 참고: defaultValue는 빈 문자의 경우에도 적용
     * /request-param?username=
     */
    @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";
    }

마지막으로 파라미터를 Map 방식으로 조회하는 방법도 있다.

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

파라미터의 값이 1개가 확실하면 Map을 사용하지만, 그렇지 않다면 MultiValueMap을 사용하자.

 

이제 HTTP 요청 파라미터 - @ModelAttribute에 대해 알아보겠다.

보통 개발을 하면 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어준다.

스프링은 이 과정을 자동화해주는 @ModelAttribute 기능을 제공한다.

먼저 요청 파라미터를 바인딩받을 객체를 만들자.

import lombok.Data;

@Data
public class HelloData {
    private String username;
    private int age;
}

//롬복 @Data
//@Getter , @Setter , @ToString , @EqualsAndHashCode , @RequiredArgsConstructor 를 자동으로 적용해준다.

이제 @ModelAttribute를 적용해보겠다.

// model.addAttribute(helloData) 코드도 함께 자동 적용됨
@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";
    }
    //modelAttribute를 사용하면 객체도 생성하고, 요청 파라미터의 값도 모두 들어가있다.

스프링 MVC는 @ModelAttribute가 있으면 다음을 실행한다.

 

  • HelloData 객체를 생성한다.
  • 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력한다.
  • 파라미터의 이름이 username이면 setUsername() 메서드를 찾아서 호출하면서 값을 입력한다.
    @ResponseBody
    @RequestMapping("/model-attribute-v2")
    public String modelAttributeV2(HelloData helloData){
        log.info("username={}, age={}", helloData.getUsername(),
                helloData.getAge());
        return "ok";
    }

@ModelAttribute는 생략할 수 있다.

그러나 그러면 @RequestParam도 생략할 수 있으니 혼란이 발생할 수 있다.

따라서 스프링은 규칙을 제공하는데,

String, int, Integer 같은 단순 타입은 -> @RequestParam.

나머지는 @ModelAttribute로 인지한다. (argument resolver로 지정해둔 타입 외)

 

 

이번에는 HTTP 요청 메시지가 단순 텍스트일 때를 살펴보자.

우리는 서블릿에서 이 과정을 한 차례 학습했다. 단순 텍스트 메시지를 HTTP 메시지 바디에 담아서 전송하고 HttpServletRequest를 통해 InputStream을 사용해서 직접 읽었다.

그 전 버전에서 조금 더 나아가 InputStream과 Writer를 직접 인자로 받을 수 있다. 코드는 아래와 같다.

  @PostMapping("/request-body-string-v2")
    public void requestBodyStringV2(InputStream inputStream, Writer responseWriter)throws IOException {
        //서블릿 코드를 지우고 스트림을 바로 받고 writer를 받을 수 있다.
        String messageBody=StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        //항상 바이트를 문자로 바꾸면 인코딩을 지정

        log.info("messageBody={}", messageBody);
        responseWriter.write("ok");
}

스프링 MVC는 다음 파라미터를 지원한다.

  • InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
  • OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력

이 방식도 저번보다 발전했지만 여전히 인코딩을 하는 과정을 겪어야 한다. 당연히 스프링 MVC는 이 방식보다 더 발전한 기능도 제공한다. 먼저 HttpEntity를 사용하는 방식이다.

/**
     * HttpEntity: HTTP header, body 정보를 편라하게 조회
     * - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
     * - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
     *
     * 응답에서도 HttpEntity 사용 가능
     * - 메시지 바디 정보 직접 반환(view 조회X)
     * - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
     */

    @PostMapping("/request-body-string-v3")
    public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity){//http 바디의 것을 문자열로 바꿔서 객체에 넣는다
        String body=httpEntity.getBody();
        log.info("messageBody={}",body);

        return new HttpEntity<>("ok");
    }

중요한 점은 요청 파라미터를 조회하는 기능과 관계가 없다.

@RequestParam, @ModelAttribute X. 메시지 바디 정보를 직접 조회하는 것이다. 

 

마지막으로 @RequestBody를 통해 메시지 바디 정보를 조회하는 경우다.

 /**
     * @RequestBody
     * - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
     * - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
     *
     * @ResponseBody
     * - 메시지 바디 정보 직접 반환(view 조회X)
     * - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
     */
    @ResponseBody
    @PostMapping("/request-body-string-v4")
    public String requestBodyStringV4(@RequestBody String messageBody) {
        log.info("messageBody={}", messageBody);
        return "ok";
    }

@RequestBody @RequestBody를 사용하면 HTTP 메시지 바디 정보를 편리하게 조회할 수 있다.

참고로 헤더 정보가 필요하다면 HttpEntity를 사용하거나 @RequestHeader를 사용하면 된다.

이렇게 메시지 바디를 직접 조회하는 기능은 요청 파라미터를 조회하는 @RequestParam , @ModelAttribute 와는 전혀 관계가 없다.

 

마지막으로 HTTP API에서 주로 사용하는 JSON 데이터 형식 조회를 알아보자.

기존 서블릿에서는 ObjectMapper을 통해서 문자로 된 JSON 데이터를 자바 객체로 변환했다.

이번에는 우선 서블릿이 아닌 방금 살펴본 RequestBody를 통해 JSON 데이터를 조회해보자.

 @ResponseBody
    @PostMapping("/request-body-json-v2")
    public String requestBodyJsonV2(@RequestBody String messageBody)throws IOException{
        HelloData data = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return "ok";
    }

이전에 배운 서블릿보다 코드가 훨씬 깔끔해지고 알아보기 쉬운 것을 확인할 수 있다.

그러나 여기서도 문자로 된 JSON 데이터를 messageBody를 objectMapper을 통해서 자바 객체로 변환한다.

한 번에 변환하는 방법은 없을까? 당연히 스프링 MVC에서 그런 기능을 제공한다.

/**
     * @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
     * HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (contenttype: application/json)
     */
    @ResponseBody
    @PostMapping("/request-body-json-v3")
    public String requestBodyJsonV3(@RequestBody HelloData helloData ){
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
        return "ok";
    }

@RequestBody 객체 파라미터 @RequestBody HelloData data @RequestBody에 직접 만든 객체를 지정할 수 있다.

HttpEntity , @RequestBody를 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해준다. HTTP 메시지 컨버터는 문자뿐만 아니라 JSON도 객체로 변환해준다.

 

@ModelAttribute에서 학습한 내용을 떠올려보자.

스프링은 @ModelAttribute , @RequestParam 해당 생략 시 다음과 같은 규칙을 적용한다.

String , int , Integer 같은 단순 타입 = @RequestParam

나머지 = @ModelAttribute (argument resolver로 지정해둔 타입 외)

따라서 이 경우 HelloData에 @RequestBody를 생략하면 @ModelAttribute 가 적용되어버린다.

HelloData data @ModelAttribute HelloData data 따라서 생략하면 HTTP 메시지 바디가 아니라 요청 파라미터를 처리하게 된다.

따라서 @RequestBody를 생략하면 안 된다.

 

물론 앞서 배운 것처럼 HttpEntity를 사용해도 된다.

    @ResponseBody
    @PostMapping("/request-body-json-v4")
    public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
        HelloData data = httpEntity.getBody();
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return "ok";
    }

마지막 경우는 @ResponseBody를 사용해 해당 객체를 Http 메시지 바디에 넣는 경우다.

v3과 큰 차이가 없다. 물론 이 경우도 HttpEntity를 사용할 수 있다.

 @ResponseBody
    @PostMapping("/request-body-json-v5")
    public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return data; //이 객체가 http converter에 의해 json으로 바뀌고 res body에 들어감
    }

 

  • @RequestBody 요청 - JSON 요청 -> HTTP 메시지 컨버터 -> 객체
  • @ResponseBody 응답 - 객체 -> HTTP 메시지 컨버터 -> JSON 응답

이상으로 포스팅을 마칩니다. 감사합니다.

반응형

댓글