스프링 MVC- 기본기능

byeol·2022년 12월 27일
0

스프링 웹 MVC 1

목록 보기
6/6

크게 정리를 해본다.

일단

  • 로깅에 대해서 배운다.
    로깅은 "기록하는 행위"이다.
    버그나 성능을 알아볼 수 있는 정보를 제공한다.
    크게 방법은 2가지 System.out.println()과 로깅 라이브러리
  • 요청매핑
    @RestController, @RequestMapping의 메서드 속성, 이를 축약한 @GetMapping 등, 경로변수(단일/다중), 특정파라미터 조건, 특정헤더조건, 요청이 들어올 때 미디어 타입 조건 매핑(HTTP Content-Type, Accept)
  • 요청매핑의 API 예시를 살펴본다.
  • HTTP 요청의 헤더 정보를 한번에 보거나 지정헤서 보는 방법에 대해서 배운다.
  • HTTP 요청이 파라미터로 데이터를 줄 때 서버는 쿼리파라미터, HTML Form이 있으며 스프링이 제공하는 기능으로 @RequestParam과 객체 파라미터를 받는 @ModelAttribute가 있다. 그리고 요청이 메세지로 오는 경우 단순 텍스트인지 Json인지에 따라 서버가 어떻게 요청을 받아 처리해야하는지 살펴본다.
  • 서버가 HTTP 응답을 클라이언트에 보낼 때 어떻게 보내야하는지 살펴본다. 크게 정적 리소스와 동적 리소스인 뷰 템플릿, 메세지 바디에 보내는 3가지 방식이 있다.
  • 이 중에서 메세지 바디에 보낼 때 사용되는 HTTP 메세지 컨버터에 대해서 배운다.
  • 마지막으로 요청 매핑 핸들러 구조를 통해서 언제 HTTP 메세지 컨버터가 이용되는지 자세히 살펴본다.

자 이제 자세히 살펴본다.

로깅

log : 기록하다
logging 이란?

  • 정보를 제공하는 일련의 기록인 로그를 생성하도록 시스템을 작성하는 활동
  • 프린트 줄 넣기는 간단한 로그를 생성하기만 한다.
  • 버그에 대한 유용한 정보를 제공
  • 성능에 관한 통계와 정보를 제공

✔️ 방법은 크게 2가지
1) System.out.println()
이전까지 내가 사용했던 방법
하지만 실무에서는 사용하지 않는다
왜?!

  • 속도가 너무 느리다
  • 최소한의 정보가 없다->날짜/시간/타입 등과 같은 정보가 기본적으로 없다.
  • 콘솔창에 출력만 해서 나중에 확인할 수 없다.
  • 데이터를 쌓고 남기기 힘들다.-> log 파일 만들 수 없음

2) 로깅(Logging) 라이브러리
로깅 라이브러리를 이용해서 남긴다.
최소한의 정보(날짜/시간/타입 등)를 제공
데이터를 서버에 저장하고 파일화 가능

  • java.util.logging -JDK 1.4부터 포함된 표중 로깅 API
  • Log4j - 가장 오래된 로깅 프레임 워크
  • Logback - Log4j이후 출시된 로깅 프레임 워크
  • Log4j2 - 가장 최근에 등장
  • SLF4J (Simle Logging Facade For Java) -logger 추상체
    Log4j2, Logback 등과 같은 프레임워크의 인터페이스

✔️ 로그 레벨

https://blog.lulab.net/programmer/what-should-i-log-with-an-intention-method-and-level/

로그 레벨은 6가지
TREACE>DEBUG>INFO>WARN>ERROR>FATAL
어떤 로그를 언제 작성할 것인가?

최소한 명확한 목적은 가지는 레벨을 Error와 Info

  • ERROR 의도하지 않은 오류를 명시적으로 표현
    try {
       val user = userRepository.findById(userId)
       user.modifyMobile(mobile)
       userRepository.save(user)
    } catch (e: ConnectionException) {
       log.error("Fail to find a userDB is disconnected (userId: $userId)")
       throw InvalidUserServiceException(ErrorCode.DB_CONNECTION_ERROR, e)
    }
  • INFO 시스템 동작을 표현, 명확한 의도가 있는 로그들
    return try {
       val user = userService.findById(userId)
       log.info("User is ${user.status} status (userId: $userId)")
       return user.status
    } catch (e: NonexistentUserException) {
       log.info("User is not exist (userId: $userId)")
       return UserStatus.NOT_REGISTERED
    }
    Exception이 발생하는 경우 무의식적으로 ERROR 레벨을 사용하는 경우가 있으나 시나리오 상 의도된 Exception이라면 INFO레벨로 작성

✔️ logging.level.root로 일정 레벨 이상의 로그만 출력하기
1. application.properties에 들어간다.
2. " logging.level.root = ERROR " 를 추가한다.
결과
저 노란색 레벨의 로그만 콘솔에 찍힌다.
1. application.properties에 들어간다.
2. " logging.level.root = WARN " 를 추가한다.

따라서 TRACE의 경우 모든 정보가 찍힌다.

✔️ 로그 선언

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

✔️ 로그 호출

log.info("username={}",username);
System.out.println("username="+username);

올바른 로그 호출법
우리가 log.debug("username="+username);로 호출하지 않는 이유가 있나요?
그 이유는 저 방식이 메로리를 잡아 먹을 수도 있기 때문이다.
만약의 개발자가 출력 레벨을 INFO로 설정했다면 저 내용을 출력되지 않는다. 그렇지만 Java는 연산을 미리 해버리기 때문에 username="Kim"이라는 내용을 메모리에 저장하는 것이다. 즉 출력되지도 않을 데이터를 Java가 연산을 진행해 메모리에 저장하는 것이다. 따라서 저렇게 호출하지 않도록 한다!

요청 매핑

@RestController
public class MappingController {

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


  @RequestMapping("/hello-basic")
  public String helloBasic(){
      log.info("helloBasic");
      return "ok";
   }
 }
  • @RestController
    @Controller의 경우에는 반환값이 String인 경우 뷰 이름으로 인식
    그러나 @RestController의 경우느네 반환값이 String인 경우 HTTP 메세지 바디에 바로 입력
  • @RequestMapping("/hello-basic")
    이 것의 의미는 저 괄호안에 있는 url을 client가 호출할 경우 이 메서드가 실행된다 라는 것을 의미한다.
    (하나) 혹은{하나,둘 등}의 url이 들어갈 수 있다.
    또한 "/hello-basic"과 "/hello-basic/"를 같은 요청으로 매핑
  • @RequestMapping("/hello-basic")는 GET,HEAD,POST,PUT,PATCH,DELETE
  • @RequestMapping(value="/hello-basic",method=RequestMethod.GET) = @GetMapping("/hello-basic")

@GetMapping("/mapping/{userId}")
  public String mappingPath(@PathVariable("userId") String data){
      log.info("mappingPath userId={}",data);
      return "ok";
  }
  • @PathVarialbe(경로변수) : 경로에서 변수 가져오기
    @PathVariavle의 이름과 파라미터 이름이 같으면 생략할 수 있다.
    @PathVariable("userId") String userId-> @PathVariable String userId
    • 다중 사용 가능하며 설정은 아래와 같다.
      @GetMapping("/mapping/users/{userId}/order/{orderId}")
      public String mappingManyPath(@PathVariable String userId, @PathVariable Long orderId){
          log.info("mappingPath userId={}, orderId={}",userId, orderId);
          return "ok";
      }

@GetMapping(value="/mapping-param",params = "mode=debug")
  public String mappingParam() {
      log.info("mappingParam");
      return "ok";
  }
  • url 경로에 특정 파라미터가 들어가야 메소드가 호출될 수 있도록 설정할 수 있다. 파라미터 변수 혹은 파라미터 변수와 값까지도 모두 설정할 수 있다.
    위와 같이 매핑정보를 설정할 경우 저 메소드는 "mapping-param?mode=debug"이어야 호출이 된다.
    • params="mode" : 파라미터에 무조건 mode의 값이 들어가야 한다.
    • params="!mode" : 들어가면 안된다
    • params="mode != debug" : 파라미터 mode가 들어가되 값이 debug이어서는 안된다.
    • params = {"mode=debug","data=sky"} : 파라미터 mode가 들어가고 값이 debug, 파라미터 data가 들어가고 값이 sky이어야 한다.

  @GetMapping(value="/mapping-header",headers= "mode=debug")
  public String mappingHeader() {
      log.info("mappingHeader");
      return "ok";
  }
  • HTTP 요청 헤더에 이 조건이 있으면 메소드가 호출될 수 있도록 설정할 수 있다.

 @PostMapping(value = "/mapping-consume", consumes ="application/json")
  public String mappingConsumes() {
      log.info("mappingConsumes");
      return "ok";

  }
  • HTTP 요청이 Post인데 Content-Type의 헤더가 application/json이면 서버가 Post 요청으로 온 데이터를 소비한다.
    • consumes ="application/json"
    • consumes ={"application/*","text/plain"}
    • consumes =MediaType.TEXT_PLAIN_VALUE

@PostMapping(value = "/mapping-produce", produces="text/html")
  public String mappingProduces() {
      log.info("mappingProduces");
      return "ok";
    }
  • Client가 text/html을 받아들일 수 있으니 다음 응답은 text/html로 만들억 달라!
    HTTP Header의 Accept 부분이다.
    • produces = "text/plain"
    • produces = {"text/plain","application.*"}
    • produces = MediaType.TEXT_PLAIN_VALUE
    • produces = "text.plain;charset=UTF-8"

HTTP요청 - 기본, 헤더조회

@Slf4j
@RestController
public class RequestHeaderController {

    @RequestMapping("/headers")
    public String headers(HttpServletRequest request,
                          HttpServletResponse response,
                          HttpMethod httpMethod,
                          Locale locale,
                          @RequestHeader MultiValueMap<String, String> multiValueMap,
                          @RequestHeader("host") String host,
                          @CookieValue(value="myCookie",required = false) String cookie){
  • @RequestHeader MultiValueMap<String, String> multiValueMap
    모든 HTTP 헤더의 정보를 MultiValueMap형식으로 저장
    • MultiValueMap은 Map과 유사하나 하나의 키에 여러개의 값을 받을 수 있다. HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용
      local:8080/main?keyA=value1&keyA=value2
  • @RequestHeader("host") 특정 헤더의 정보를 콕 집어서 저장
    속성으로 필수값 여부의 required와 기본값 속성으로 defaultValue를 설정할 수 있다.
  • @CookieValue(value="myCookie",required = false) 특정 쿠키를 조회할 수 있다.

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

HTTP 요청 메세지에서 데이터를 조회할 때 우리는 3가지 방식을 앞서 배웠다.
one) Get을 통해서 url에 있는 쿼리 파라미터에서 데이터를 가지고 왔다.
two) Post를 통해서 url이 아닌 HTTP 메세지 바디에서 쿼리 파라미터를 가지고 왔다.
three) Http message body에 데이터를 직접 담아서 요청하며 HTTP API에서 주로 사용하였고 데이터 형식은 주로 JSON을 사용했다. HTTP 요청 형식은 POST,PUT,PATCH이다.

먼저 저 위에 있는 one과 two 방법에 대해서 알아보자
불편했던 방법에서 좀 더 편리한 방법으로 단계를 밟아보며 정리하였다.

GET : url파라미터
POST : client가 html이 띄워진 브라우저에 데이터 입력

request.getParameter()

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

🔴 @RequestParam - 기본

 @ResponseBody
    @RequestMapping("/request-param-v2")
    public String requestParamV2(@RequestParam("username") String memberName,
                               @RequestParam("age") int memberAge)  {

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

        return "ok";
    }

if(HTTP 파라미터 이름 == 변수 이름)

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

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

        return "ok";
    }

@RequestParam ("username") 저 괄호부분 생략 가능


추가로 변수의 형이 기본형이라면 int, String, Integer 등

 @ResponseBody
    @RequestMapping("/request-param-v2")
    public String requestParamV2(String username,int age)  {

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

        return "ok";
    }

@RequestParam도 생략 가능
하지만 이 방법은 추천하지 않는다고 한다. 명확하게 무엇을 요청 파라미터로 가지고 있는지 알 수 없기 때문이다.


🔴 @RequestParam -필수로 쿼리 파라미터를 요청

꼭 필수적으로 들어와야 하는 쿼리 파라미터 정보가 있도록
있어야 메소드가 호출되도록 설정할 수 있다.
바로 @RequestParam에 required 속성을 넣는 것이다.

@ResponseBody
    @RequestMapping("/request-param-required")
    public String requestParamRequired(@RequestParam(required = true) String username,
                                       @RequestParam(required = false) int age)  {

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

        return "ok";
    }

우리가 local:8080/request-param-required?username="kim"를 호출하면 ERROR가 발생한다!

왜?

그것은 age의 형이 int이기 때문에 int의 기본값을 0이고 required가 false인 경우라도
ocal:8080/request-param-required?username="kim"&age=10
이라고 호출해야 하는 모순이 발생한다.

따라서
아래와 같이 age의 형을 Integer로 변경하여 null값이 기본 값으로 올수 있도록 한다.

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

🔴 @RequestParam - 파라미터에 값이 없는 경우 기본값 설정

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

localhost:8080/request-param-default?username=
이렇게 빈 문자열을 넣어도 guest로 설정된다.
required는 의미가 없다. 이미 기본값을 통해서 값이 정해져 있기 때문이다.


🔴 @RequestParam - 그냥 모든 요청 파라미터를 받고 싶을 때

@ResponseBody
    @RequestMapping("/request-param-map")
    public String requestParamDefault(@RequestParam Map<String, Object> paramMap)  {

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

        return "ok";
    }

Map을 통해서 받지만 하나의 파라미터에 여러개의 값이 존재하는 경우 MultiValueMap을 이용해서 받는다.

@ModelAttribute - HTTP 값을 받아서 어떤 객체에 값을 넣어줘야할 때

  • HelloData 객체 생성

    package hello.springmvc.basic;
    
    import lombok.Data;
    
    @Data
    public class HelloData {
       private String username;
       private  int age;
     }
    • 롬복 @Data로 인해서 @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor를 자동으로 적용
    • 객체에 getUsername()과 setUsername() 메서드가 있으면 이 객체는 username이라는 프로퍼티를 가지고 있다.
  • Controller
...
@Slf4j
@Controller
public class RequestParamController {
 @ResponseBody
    @RequestMapping("/model-attribute-v1")
    public String modelAttributeV1(@ModelAttribute HelloData helloData){
        log.info("username={} , age={}",helloData.getUsername(),helloData.getAge());
        return "ok";
    }
    
 }
  1. 스프링 MVC는 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다.
  2. 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력한다.
  3. 예) 요청 파라미터의 이름이 username이면 setUsername()메서드를 찾아서 호출하여 값 입력

🔴 @ModelAttribute생략

 @ResponseBody
    @RequestMapping("/model-attribute-v2")
    public String modelAttributeV2( HelloData helloData){
        log.info("username={} , age={}",helloData.getUsername(),helloData.getAge());
        return "ok";
    }
  • @RequestParam은 기본형
  • @ModelAttribute는 객체 및 기본형 외의 것

HTTP 요청 메세지

단순 텍스트

@RequestParam과 @ModelAttribute를 사용하지 못한다. 쿼리 파라미터로 데이터가 넘어오는 것이 아니라 HTTP message body를 통해서 데이터가 넘어오기 때문이다.

🔴 request.getInputStream()을 이용

@Slf4j
@Controller
public class RequestBodyStringController {

    @PostMapping("/request-body-string-v1")
    public void requestBodyString(HttpServletRequest request, HttpServletResponse response)
    throws IOException {

        ServletInputStream inputStream = request.getInputStream();
        //InputStream과 StreamUtils
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

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

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

🔴 InputStream(Reader)과 OutputStream(Writer)을 이용하여
HTTP 요청 메세지 바디를 직접 조회 및 결과 출력

@PostMapping("/request-body-string-v2")
    public void requestBodyStringV2(InputStream inputStream, Writer responseWriter)
            throws IOException {

        //InputStream과 StreamUtils
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

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

        responseWriter.write("ok");
    }

🔴 HttpEntity : HTTP header과 body 정보를 편리하게 조회하며 응답에도 사용 가능하다.

  @PostMapping("/request-body-string-v3")
    public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) {
        String messageBody = httpEntity.getBody();
        log.info("messageBody={}",messageBody);

        return new HttpEntity<>("ok");//임에도 view로 조회되지 않는 HttpEntity
    }

🔴 @RequestBody : 가장 간단하고 실무에서 많이 사용하는 방법

메세지 바디정보를 편리하게 조회

 @ResponseBody//응답을 view가 아닌 HTTP 메시지 바디에 직접 담아 전달
   @PostMapping("/request-body-string-v4")
   public String requestBodyStringV4(@RequestBody String messageBody) {
       log.info("messageBody={}",messageBody);

       return "ok";
   }

헤더정보가 필요하다면 HttpEntity를 사용하거나 @RequestHeader를 사용

  • @ResponseBody는 return값의 String을 Http 응답 메세지 바디에 직접 담아 전달

JSON

JSON으로 받는 경우
ObjectMapper를 이용해야 한다.

https://recordsoflife.tistory.com/599
자세한 설명은 위 블로그를 참고하자.

간단하게 설명하면 JSON으로 들어온 데이터를 객체화 시키는 역직렬화와
객체화된 데이터를 JSON형식으로 바꾸는 직렬화를 손쉽게 만들어주는 것이 ObjectMapper이다.

예를 들어서
Car가 String color와 String type으로 이루어져 있다고 가정하면

  • 직렬화
ObjectMapper objectMapper = new ObjectMapper();
Car car = new Car("yellow","renault");
objectMapper.writeValue(new File("target/car.json"),car);
  • 역직렬화
String json = "{ \"color\" : \"Black\", \"type\" : \"FIAT\" }";
Car car = objectMapper.readValue(json,Car.class);

🔴 HttpServletRequest의 getInputStream()을 이용

@Slf4j
@Controller
public class RequestBodyJsonController {

    private ObjectMapper objectMapper = new ObjectMapper();

    @PostMapping("/request-body-json-v1")
    public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response)
        throws IOException {

        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

        log.info("messageBody={}",messageBody);
        HelloData data = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={},age={}",data.getUsername(), data.getAge());

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

🔴 @RequestBody를 이용해서 바로 조회하기

 @ResponseBody
    @PostMapping("/request-body-json-v2")
    public String requestBodyJsonV2(@RequestBody String messageBody)
            throws IOException {


        log.info("messageBody={}",messageBody);
        HelloData data = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={},age={}",data.getUsername(), data.getAge());

        return "ok";
    }

🔴 ObjectMapper 없이 바로 객체로 변환하는 방법 - @RequestBody

  @ResponseBody
    @PostMapping("/request-body-json-v3")
    public String requestBodyJsonV3(@RequestBody HelloData data)
            throws IOException {

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

        return "ok";
    }

여기서 중요한 것은 앞서 배웠던 쿼리 파라미터를 바로 객체로 받아버리는 @ModelAttribute와 메소드의 매개변수 부분이 매우 흡사하다. 따라서
@RequestBody 애노테이션을 통해서 객체를 받을 때는 @RequestBody를 생략할 수 없다!
생략하면 @ModelAttribute로 인식해버린다.


🔴 ObjectMapper 없이 바로 객체로 변환하는 방법 - HttpEntity

 @ResponseBody
    @PostMapping("/request-body-json-v4")
    public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity)
            throws IOException {

        HelloData data = httpEntity.getBody();
        log.info("username={},age={}",data.getUsername(), data.getAge());

        return "ok";
    }

HTTP 응답 - 정적 리소스 , 뷰 템플릿

profile
꾸준하게 Ready, Set, Go!

0개의 댓글