RE-Heat 개발자 일지

스프링 MVC 1편 - [6] 기본 기능(상편) 본문

백엔드/스프링

스프링 MVC 1편 - [6] 기본 기능(상편)

RE-Heat 2023. 6. 30. 23:54

출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -

www.inflearn.com

인프런 김영한님의 스프링 MVC 1편 강의를 듣고 정리한 내용입니다.

 

[1] 프로젝트 생성

Jar 선택 이유

 - JSP가 아닌 Thymeleaf로 작성 예정이라 War가 아닌 Jar 선택.

 - Jar를 사용하면 항상 내장 서버(톰캣)를 사용하고 webapp 경로를 사용하지 않음(내장 서버 최적화)

 - War도 내장서버 활용이 가능하나, 주로 외부 서버를 이용해 외부 서버에 배포하는 목적으로 사용

 

Welcome 페이지

/resources/static/ 위치에 index.html 파일을 두면 Welcome 페이지로 처리해 준다.

 

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

System.out.println() 대신 별도의 로킹 라이브러리를 사용해 로그를 출력. 콘솔뿐만 아니라 파일 생성도 가능

 

SLF4J(Simple Logging Facade for Java)

: java.util.logging, logback 및 log4j와 같은 다양한 로깅 프레임 워크에 대한 추상화(인터페이스) 역할을 하는 라이브러리

 

LogTestController

@Slf4j
@RestController
public class LogTestController {
    //private final Logger log = LoggerFactory.getLogger(getClass());

    @RequestMapping("/log-test")
    public String logTest(){
        String name = "Spring";

        System.out.println("name = " + name);

        log.trace(" trace my log="+name);
        
        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";
    }
}

로그 선언방법

private Logger log = LoggerFactory.getLogger(getClass()); : slf4j에 있는 것을 import해서 사용하기

② private static final Logger log = LoggerFactory.getLogger(Xxx.class) : slf4j에 있는 것을 import해서 사용하기

③ @Slf4j : 롬복이 제공하는 애노테이션으로 위 코드 대체 가능

 

@RestController

  • @Controller는 뷰 이름을 반환한다. 하지만 REST API 발달로 텍스트, JSON값 등을 리턴해줘야 하는 일이 생기면서 @RestController가 생김. 
  • @RestController는 반환값으로 뷰를 찾지 않고, HTTP 메시지 바디에 바로 입력함.

 

로그 출력

로그 출력 포맷 : 시간, 로그 레벨, 프로세스 ID, 쓰레드 명, 클래스명, 로그 메시지

 

로그 레벨 및 설정 방법

 - 로그 레벨 : TRACE > DEBUG > INFO > WARN >ERROR

 - 설정방법

 

logging.level.hello.springmvc = info로 적으면 info 값, debug로 적으면 debug 하위값이 콘솔에 출력

    개발 서버는 DEBUG, 운영서버는 INFO 출력이 대세 

기본값은 INFO [logging.level.root=info (default)]

    만일 info가 아닌 debug로 하면 스프링 라이브러리 관련 값까지 콘솔창에 다 찍히므로 유의

 

올바른 로그 사용법

log.trace("trace={}", data) ㅇ

log.trace("trace="+ data) X

이유 : 로그 레벨이 info면 위 로그는 debug이므로 사용 안됨. 그러나 JAVA 특성상 + 연산이 먼저 실행되므로 두 번째 코드는 출력에 관계없이 연산이 시작됨. 리소스 낭비를 막기 위해 첫 번째 방식이 올바름.

 

로깅의 장점

  • 쓰레드 정보, 클래스 이름 같은 부가 정보를 확인할 수 있고, 출력 모양을 조정할 수 있다.
  • 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영서버에서는 출력하지 않는 등 로그를 상황에 맞게 사용할 수 있다.
  • 콘솔 외에 파일이나 네트워크 등 별도의 위치에 로그를 남길 수 있다.
  • 성능도 System.out보다 수십 배 낫다. 그래서 실무에선 꼭 로그를 사용함.

 

[3] 요청 매핑

 

MappingController

@RestController
public class MappingController {
    private Logger log = LoggerFactory.getLogger(getClass());

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

    /**
     * method 특정 HTTP 메서드 요청만 허용
     * GET, HEAD, POST, PUT, PATCH, DELETE
     */

    @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("mappingGetV2");
        return "ok";
    }

    /**
     * PathVariable 사용
     * 변수명이 같으면 생략 가능
     *
     * @PathVariable("userId") String userId -> @PathVariable userId
     */

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

① 매핑은 배열형태로 URL 다중 설정이 가능하다.

    @RequestMapping({"/hello-basic, "hello-go"}) /hello-basic or /hello-go면 호출됨. 단, 배열은 {}로 묶어줘야 함.

② 매핑할 때 HTTP 메서드(GET, HEAD, POST, PUT, PATCH, DELETE)를 지정할 수 있음

  @RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET) 여기에 POST 요청을 하면

  스프링이 HTTP 405 상태코드(Method Not Allowed)를 반환함.

③ ②번을 축약해서 사용할 수 있음 (@GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping)

④ @PathVariable로 경로 변수 사용

@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data){

=> userId는 식별자 부분

⑤ @PathVariable과 파라미터의 이름이 같으면 생략 가능

public String mappingPath(@PathVariable String userId){

⑥ @PathVariable 다중사용도 가능

@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long
        orderId) {

⑥ 특정 파라미터 조건 매핑

@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {

http://localhost:8080/mapping-param?mode=debug

mode=debug가 넘어와야 가능. 실질적으로 잘 쓰이진 않음

 

⑦ 특정 헤더 조건 매핑

@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {

헤더도 조건 파라미터처럼 사용가능

 

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

@PostMapping(value = "/mapping-consume", consumes = MediaType.APPLICATION_JSON_VALUE)

HTTP 요청 메시지의 Content-Type헤더를 기반으로 타입 매핑.

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

PostMapping(value = "/mapping-produce", produces = MediaType.TEXT_HTML_VALUE)

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

 

"applicaton/json", "text/html"등 직접 쓰는 방식도 있으나, MediaType의 변수명으로 하면 오타가 날 확률도 없어 바람직하다.

 

[4] 요청 매핑 - API 예시

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

 MappingClassController

@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {
    @GetMapping
    public String user(){
        return "get users";
    }

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

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

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

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

 

①클래스 위에 @RequestMapping("/mapping/users")를 작성하면 메소드에 "/mapping/users" 생략 가능

    ex) @Getmapping("/{userId}") == @Getmapping("/mapping/users/{userId}) 앞 상위 경로가 생략된 것.

② 같은 URL + HTTP 메서드(GET, POST, PATCH, DELETE 등)로 구분하는 게 깔끔하다. 

 

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

HTTP 헤더 조회하는 방법

 

RequestHeaderController

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

 

 ① 스프링은 @Controller 파라미터에 다양한 방식을 지원함. 

    Locale: 지역, 선호언어

    MultiValueMap<> :  하나의 키에 여러 값을 받을 때 사용

        ex) userId=1&userId=2

    @RequestHeader("host") String host : 헤더 이름을 지정하면 해당 헤더값만 가져 옴

    @CookieValue(value="쿠키명", 속성) : 헤더 Cookie값 조회

 

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

HTTP 요청 메시지를 통해 클라이언트에서 서버로 전달하는 3가지 방법

GET- 쿼리 파라미터 : ex) /url?userId=1&age=30

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

HTTP message body에 데이터를 직접 담아 요청

 

스프링으로 요청파라미터 조회하는 방법

RequestParamController

@Slf4j
@Controller
public class RequestParamController {
    @RequestMapping("/request-param-v1")
    public void requestParmaV1(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 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) {
        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";
    }
}

 

① HttpServletRequest의 getParameter()로 데이터를 조회하는 방식

② @RequestParam

    1] @RequestParam을 사용해 요청 파라미터를 조회하는 방식

    2] 파라미터명과 변수명이 가능할 경우 파라미터명 생략 가능

    3] String, int, Integer 등 단순 타입이면 @RequestParam도 생략 가능.

        영한님 말씀 : 하지만, 코드를 읽을 때 명확하게 구분하기 위해 @ReuqestParam을 써주는 게 바람직

③ @RequestParam(required=true) 

    request에 해당 파라미터가 있어야 하는지 여부. default값은 true. 디폴트값이 true인데 파라미터가 없으면 400 예외 발생

    ☞ 주의사항 : 빈문자("")와 null값은 다르다. 빈문자를 넣으면 디폴트값이 true여도 빈문자로 통과

④ @RequestParam(defaultValue="값")

    빈문자나 null값이 올 경우 디폴트로 작성한 값을 넣어준다. 그래서 required=true or false가 사실상 필요 없음.

⑤ @RequestParam Map<String, Object> paramMap

    파라미터 값을 받은 후 Map으로 꺼내쓸 수 있음. 

    단, 파라미터 값이 하나라면 Map을 써도 되나 그렇지 않다면(?userId=1&userId=2) MultiValueMap을 써야 함.

 

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

@RequestParam을 사용하면 파라미터를 매우 편리하게 사용할 수 있다.

 

RequestParamController-V2

@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 : return값을 메시지 바디에 담아 보냄. 

 

RequestParamController-V3

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

HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name=”xxx”) 생략 가능

RequestParamController-V4

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

String, int, Integer 등 단순 타입이면 @RequestParam도 생략 가능.

영한님 견해 : @Requestparam이 있으면 명확하게 요청 파라미터에서 데이터를 읽는다는 것을 이해할 수 있으므로 V4보단 V3이 바람직하다.

 

RequestParamController: param-required

@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(required=true) 파라미터 필수 여부. 넘어오지 않으면 400 예외 발생.

참고 : int에 null문자 불가능해 Integer로 받음.

주의!!! 빈문자 “”와 null은 다르다. 

 

RequestParamController: param-default

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

 빈 문자나 null값이 올 경우 defaultValue값으로 자동 할당. 

 

RequestParamController: param-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";
}

 

Map으로도 하나하나 꺼내쓸 수 있다.

파라미터 값이 1개가 아니면 Map 대신 MultiValueMap을 써야 함.