관리 메뉴

나구리의 개발공부기록

스프링 MVC - 기본기능, 프로젝트 생성, 로깅(간단히알아보기), 요청매핑, 요청매핑 - API 예시 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

스프링 MVC - 기본기능, 프로젝트 생성, 로깅(간단히알아보기), 요청매핑, 요청매핑 - API 예시

소소한나구리 2024. 3. 1. 21:10

  출처 : 인프런 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 (유료) / 김영한님  
  유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용  

https://inf.run/Gmptq


1. 프로젝트 생성

1) 프로젝트 생성

(1) 프로젝트

  • Gradle
  • Java
  • SNAPSHOT, M1 등이 달려있지 않은 버전 중 가장 최신 버전의 스프링 부트

(2) Project Metadata

  • Group : hello
  • Artifact, Name : springmvc
  • Packaging: Jar
  • Java : 설치된 버전

(3) Dependencies

  • Spring Web
  • Thymeleaf
  • Lombok

** 주의 - Packaging: Jar

  • 스프링 부트를 사용하면 주로 이방식을 사용하게 되는데 Jar는 내장서버를 사용(톰캣 등)하고 webapp 경로도 사용하지 않아 내장 서버 사용에 최적화 되어있음
  • War는 내장 서버도 사용가능하지만 주로 외부 서버에 배포(WAS 서버를 별도로 설치하여 빌드된 파일을 넣을 때)하거나 JSP를 사용할 때 사용함

(4) 웰컴페이지 생성

  • 강의를 수월하게 진행하기 위한 메인 페이지 작성
  • 스프링 부트에 Jar를 사용하면 /resources/static 위치에 index.html을 파일을 두면 Welcome페이지로 처리를 해줌
  • 즉, 스프링 부트가 지원하는 정적 컨텐츠 위치에 /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(POSTMAN 필요)</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>

2. 로깅 간단히 알아보기

1) 로그 사용법

  • 실무에서는 System.out.println() 같은 시스템 콘솔을 정보를 출력하지 않고 별도의 로깅 라이브러리를 사용해서 로그를 출력
  • 로그 관련 라이브러리가 많고 깊게 들어가면끝이 없기 때문에 최소한의 사용 방법만 알아볼 예정

(1) 로깅 라이브러리

  • 스프링 부트 라이브러리를 사용하면 스프링 부트 로깅 라이브러리가 함께 포함되고 기본으로 아래의 로깅 라이브러리를 사용함
  • SLF4J, Logback
  • 로그 라이브러리는 Logback, Log4J, Log4J2 등등 수많은 라이브러리가 있는데 그것을 통합해서 인터페이스로 제공하는 것이 SLF4J 임
  • 즉, SLF4J는 인터페이스이며 그 구현체로 Logback 같은 로그 라이브러리를 선택하면 되며 실무에서는 기본으로 제공하는 Logback을 대부분 사용함

(2) LogTestController - 로그를 사용하기 위한 컨트롤러 생성

  • basic패키지를 생성하여 작성
  • 로그 선언: 아래의 방식으로 선언하여 로그를 사용할 수 있으나 롬복을 사용하면 로그 선언없이 편리하게 로그를 사용가능 함
    private Logger log = LoggerFactory.getLogger(getClass());
    private static final Logger log = LoggerFactory.getLogger(Xxx.class);
    @Slf4j: 롬복 애노테이션으로 위의 선언 코드 없이 간편하게 로그를 사용할 수 있음
  • 로그 호출: log.info("xxx")
package hello.springmvc.basic;

// @Slf4j // 롬복 사용
@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 계산 로직이 먼저 실행되기 때문에 이런 방식으로 사용하면 안됨
        log.trace("trace my log=" + name);

        return "ok";
    }
}

 

(3) @RestController

  • @Controller는 반환값이 String 이면 뷰 이름으로 인식되어 뷰를 찾고 뷰가 렌더링 됨
  • @RestController는 반환 값으로 뷰를 찾지 않고 HTTP 메시지 바디에 바로 입력하며 실행 결과로 "ok" 메시지를 바로 반환받을 수 있음
  • @ResponseBody와 관련이 있는데 뒤에서 더 자세히 설명함

(4) 로그가 출력되는 포멧

  • 시간: 2024-03-01T10:37:04.218+09:00
  • 로그레벨: INFO
  • 프로세스ID: 58649
  • 쓰레드 명: main
  • 클래스명: hello.spring.mvc.SpringmvcApplication
  • 로그 메세지: Started SpringmvcApplication in 0.635 seconds (process running for 0.87)
2024-03-01T10:37:04.218+09:00  INFO 58649 --- [           main] hello.springmvc.SpringmvcApplication     : Started SpringmvcApplication in 0.635 seconds (process running for 0.87)

 

(5) 로그 레벨 설정

  • 로그 LEVEL: TRACE -> DEBUG -> INFO -> WARN -> ERROR
  • 개발서버: debug 출력
  • 운영서버: info 출력
  • application.properties에서 로그 레벨을 설정할 수 있음
# 전체 로그 레벨 설정(기본값: info)
# debug, trace로 하면 엄청난 다량의 정보가 출력됨
logging.level.root = info

#hello.springmvc 패키지와 그 하위 로그 레벨 설정
#trace로 설정 -> 모두 다보임
#debug로 설정 -> trace를 제외하고 다 보임 - 개발 PC에서 설정
#info로 설정 -> 운영 서버(로그 레벨 설정의 기본값)
logging.level.hello.springmvc=debug

 

(6) 올바른 로그 사용법 - 매우 중요!

  • 로그는 불필요한 연산이 발생하지 않도록 무조건 ("data = {}" , data)처럼 코드를 작성해야함
  • +로 작성하면 로그레벨이 info로 설정되어 있어도 해당 코드에 있는 더하기 연산이 실행되어 버려 의미없는 cpu, 메모리 등 리소스가 소모됨
log.debug("debug =" + debug) // 로그 출력 레벨을 info로 설정해도 무조건 + 연산이 발생함 -> 리소스 소모
log.debug("debug ={}",debug) // 로그 출력 레벨을 info로 설정하면 아무일도 발생하지 않음

 

(7) 로그 사용시 장점

  • 쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고 출력 모양을 조정할 수 있음
  • 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하는 등의 상황에 맞게 로그 레벨 조절이 가능함
  • 콘솔에만 출력하는 것이 아니라 파일이나 네트워크 전송 등으로 로그를 별도의 위치에 남길 수 있고 심지어 파일로 남길 때에는 일별, 특정 용량에 따라 로그를 분할, 자동 압축 백업 등의 기능을 설정할 수 있음
  • 성능도 System.out보다 좋아서(내부 버퍼링, 멀티쓰레드 등등) 실무에서는 꼭 로그를 사용해야함

** 로그 더 공부하기


3. 요청 매핑

1) MappingController

  • basic패키지 하위에 requestmapping 패키지를 생성 후 작성

(1) 가장 기본적인 매핑

  • /hello-basic URL 호출이 오면 메서드가 실행 되도록 매핑
  • 대부분의 속성을 배열[]로 제공하므로 다중 설정도 가능함, {"/hello-basic", "/hello-go"}
/**
 * 기본 요청
 * HTTP 메서드 모두 허용 GET, HEAD, POST, PUT, PATCH, DELETE
 * */
 
@RequestMapping("/hello-basic")
// @RequestMapping({"/hello-basic","/hello-go"}) // 다중 설정도 가능
public String helloBasic() {
    log.info("helloBasic");
    return "ok";
}

 

** 참고 - 스프링 부트 3.0 이전과 이후의 매핑과 URL요청

  • 스프링 부트 3.0 이전에는 마지막의 /를 제거하여 아래의 URL의 요청이 같은 요청으로 매핑되었음
  • 매핑: /hello-basic -> URL 요청: /hello-basic , /hello-basic/
  • 그러나 스프링부트 3.0이후 부터는 서로 다른 URL 요청으로 인식하므로 매핑도 각각 다르게 적용해야함
  • 매핑: /hello-basic -> URL 요청 : /hello-basic
  • 매핑: /hello-basic/ -> URL 요청 : /hello-basic/

(2-1) HTTP 메서드 매핑

  • @RequestMapping에 method속성으로 HTTP 메서드를 지정하지 않으면 HTTP 메서드와 무관하게 모든 요청이 허용 됨
  • method 속성을 지정하면 해당 요청만 허용하고 다른 요청이 들어오면 HTTP 405(Method Not Allowed) 상태코드를 반환함
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
    log.info("mappingGetV1");
    return "ok";
}

 

(2-1) HTTP 메서드 매핑 축약

  • HTTP 메서드를 축약한 애노테이션을 사용하는 것이 더 직관적임
  • 각 애노테이션에 들어가서 코드를 보면 @RequestMapping과 method를 지정해서 사용하는 것을 확인할 수 있음
/**
 * 편리한 축약 애노테이션 코드
 * @GetMapping
 * @PostMapping
 * @PutMapping
 * @DeleteMapping
 * @PatchMapping
 */
@GetMapping("/mapping-get-v2")
public String mappingGetV2() {
    log.info("mappingGetV2");
    return "ok";
}

 

(3) PathVariable(경로변수) 사용

  • 최근HTTP API는 리소스 경로에 식별자를 넣는 스타일을 선호하고 많이 쓰고 있음 (ex: /mapping/userA, /users/1)
  • @RequestMapping은 URL경로를 템플릿화 할 수 있는데 @PathVariable 애노테이션을 사용하면 매칭되는 부분을 편리하게 조회가 가능함
  • @PathVariable의 이름과 파라미터 이름이 같으면 생략할 수 있음
/**
 * PathVariable 사용
 * 변수명이 같으면 @PathVariable("UserId") String UserId -> @PathVariable String userId로 변경(생략)가능
 */
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
    log.info("mappingPath userId = {}", data);
    return "ok";
}

 

(3-1) PathVariable 사용 - 다중

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

 

(4) 특정 파라미터 조건 매핑

  • 특정 파라미터가 있거나 없는 조건을 추가할 수 있지만 잘 사용하지 않음
/**
 * 파라미터로 조건을 추가 매핑
 * params="mode",
 * params="!mode"
 * params="mode=debug"
 * params="mode!=debug"
 * params = {"mode=debug","data=good"}
 * http://localhost:8080/mapping-param?mode=debug 처럼 파라미터에 조건이 있어야 가능하도록 세팅
 */
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
    log.info("mappingParam");
    return "ok";
}

 

(5) 특정 헤더 조건 매핑

  • 파라미터 매핑과 비슷한 방식, HTTP 헤더에 조건을 추가함
  • Postman으로 테스트 해야함
/**
 * 특정 헤더로 추가 매핑
 * 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";
}

 

(6-1) 미디어 타입 조건 매핑 - HTTP 요청 Content-Type, consume(해당 타입을 소비함)

  • HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입을 매핑함
  • 맞지 않으면 HTTP 415(Unsupported Media Type)을 반환
  • Postman으로 테스트 해야함
/**
 * Content-Type 헤더 기반 추가 매핑 Media Type
 * consumes="application/json"
 * consumes="!application/json"
 * consumes="application/*"
 * consumes="*\/*"
 * MediaType.APPLICATION_JSON_VALUE
 */
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
    log.info("mappingConsumes");
    return "ok";
}

 

(6-2) 미디어 타입 조건 매핑 - HTTP 요청 Accept, produce(해당 타입의 요청만 받음)

  • HTTP 요청의 Accept 헤더를 기반으로 미디어 타입을 매핑
  • 맞지 않으면 HTTP 406(Not Acceptable)을 반환
/**
 * Accept 헤더 기반 Media Type
 * produces = "text/html"
 * produces = "!text/html"
 * produces = "text/*"
 * produces = "*\/*"
 */
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
    log.info("mappingProduces");
    return "ok";
}

 

** 참고

  • 미디어타입 조건 매핑은 타입을 직접 작성하는 것보다 MediaType.타입_VALUE 문법을 사용하는 것을 권장함
  • @PostMapping(value = "/mapping-consume", consumes = MediaType.APPLICATION_JSON_VALUE)
  • @PostMapping(value = "/mapping-consume", produces = MediaType.TEXT_HTML_VALUE)

4. 요청 매핑 - API 예시

1) HTTP API 매핑

  • 회원 관리 HTTP API를 만든다고 가정하고 매핑을 어떻게 하는지 실습
  • 실제 데이터가 넘어가는 부분은 생략하고 URL매핑만 실습

(1) 회원관리 API 예시

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

 

(2) MappingClassController

  • Postman으로 테스트 해보면 정상적으로 작동 되는 것을 확인할 수 있음
  • 클래스 레벨에 @RequestMapping으로 매핑 정보를 두어 코드 중목을 제거하고 메서드 레벨에서 해당 정보를 조합해서 사용
  • 이런식으로 리소스를 계층으로 식별하는 스타일을 많이 사용하며 실제 개발시에도 사람이 인지하기 좋고 코드가 깔끔해짐
@RestController
@RequestMapping("/mapping/users") // 매핑 중복 제거
public class MappingClassController {

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

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

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

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

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