📝 로깅 간단히 알아보기
- 앞으로 로그를 사용할 것이기 때문에, 로그에 대해서 간단히 알아보자.
- 운영 시스템에서는
System.out.println()
같은 시스템 콘솔을 사용해서 필요한 정보를 출력하지 않고, 별도의 로깅 라이브러리를 사용해서 로그를 출력한다. - 참고로 로그 관련 라이브러리도 많고, 깊게 들어가면 끝이 없기 때문에, 여기서는 최소한의 사용 방법만 알아본다.
📜 로깅 라이브러리
- 스프링 부트 라이브러리를 사용하면 스프링 부트 로깅 라이브러리(
spring-boot-starter-logging
)가 함께 포함된다. - 스프링 부트 로깅 라이브러리는 기본으로 다음 로깅 라이브러리를 사용한다.
- SLF4J - http://www.slf4j.org
- Logback - http://logback.qos.ch
- 로그 라이브러리는 Logback, Log4J, Log4J2 등등 수 많은 라이브러리가 있는데, 그것을 통합해서 인터페이스로 제공하는 것이 바로 SLF4J 라이브러리다.
- 쉽게 이야기해서 SLF4J는 인터페이스이고, 그 구현체로 Logback 같은 로그 라이브러리를 선택하면 된다.
- 실무에서는 스프링 부트가 기본으로 제공하는 Logback을 대부분 사용한다.
📜 로그 선언 방법
private Logger log = LoggerFactory.getLogger(getClass());
private static final Logger log = LoggerFactory.getLogger(Xxx.class);
@Slf4j
: 롬복 사용 가능
📜 로그 호출 방법
log.trace("trace log");
log.debug("debug log");
log.info("info log");
log.warn("warn log");
log.error("error log");
📜 테스트
//@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);
return "ok";
}
}
@RestController
@Controller
는 반환 값이 String이면 뷰 이름으로 인식된다. 그래서 뷰를 찾고 뷰가 랜더링 된다.@RestController
는 반환 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력한다.- 따라서 실행 결과로 "ok" 메세지를 받을 수 있다. 이는
@ResponseBody
와 관련이 있는데, 뒤에서 더 자세히 설명한다
- 로그가 출력되는 포멧을 확인해보면 다음과 같다.
- [시간] [로그 레벨] [프로세스 ID] [쓰레드 명] [클래스 명] [로그 메시지]
2023-06-22 12:32:53.026 INFO 12536 --- [nio-8080-exec-1] hello.springmvc.basic.LogTestController : info log=Spring
📜 로그 레벨 설정
- 로그 레벨 설정을 변경해서 출력 결과를 확인해보자.
# 전체 로그 레벨 설정(기본 info)
logging.level.root=info
# hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=debug
- 로그 레벨에는 다음 5가지가 존재한다.
TRACE
>DEBUG
>INFO
>WARN
>ERROR
- 개발 서버는 주로 DEBUG를 출력하고
- 운영 서버는 주로 INFO를 출력한다.
2023-06-22 12:32:53.026 DEBUG 12536 --- [nio-8080-exec-1] hello.springmvc.basic.LogTestController : debug log=Spring
2023-06-22 12:32:53.026 INFO 12536 --- [nio-8080-exec-1] hello.springmvc.basic.LogTestController : info log=Spring
2023-06-22 12:32:53.027 WARN 12536 --- [nio-8080-exec-1] hello.springmvc.basic.LogTestController : warn log=Spring
2023-06-22 12:32:53.028 ERROR 12536 --- [nio-8080-exec-1] hello.springmvc.basic.LogTestController : error log=Spring
📜 올바른 로그 사용법(❗)
log.trace("trace log=" + name);
log.info(" info log={}", name);
- 로그 출력 레벨을 INFO로 설정한 상태에서 위처럼 로그를 사용하면, 당연히 TRACE 로그는 출력되지 않겠지만 해당 코드에 있는
"trace log=" + name
연산, 즉 문자 더하기 연산은 발생하게 된다. - 이는 다시말해 불필요한 연산이 발생하여 리소스가 낭비된다는 것을 의미한다.
- 따라서 다음과 같이 사용하는 것이 바람직하다.
log.trace("trace log={}", name);
log.info(" info log={}", name);
- 이렇게 하면 메서드에 파리미터만 넘김으로써 아무런 연산이 일어나지 않는다. 메서드 내부에서 TRACE임을 파악하고 그 시점에 바로 로직을 중단해버리기 때문이다.
📜 로그 사용시 장점
- 쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정할 수 있다.
- 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영 서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절할 수 있다.
- 시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등, 로그를 별도의 위치에 남길 수 있다. 특히 파일로 남길 때는 일별, 특정 용량에 따라 로그를 분할하는 것도 가능하다.
- 성능도 일반
System.out
보다 좋다. (내부 버퍼링, 멀티 쓰레드 등등) 그래서 실무에서는 꼭 로그를 사용해야 한다.
📝 요청 매핑
- 요청 매핑이란 사용자 요청이 들어왔을 때 어떠한 컨트롤러가 호출되어야 하는지를 매핑하는 것을 말한다.
- 간단한 컨트롤러를 작성해서 확인해보자.
@RestController
public class MappingController {
private Logger log = LoggerFactory.getLogger(getClass());
/**
* 기본 요청
* 둘다 허용 /hello-basic, /hello-basic/
* HTTP 메서드 모두 허용 GET, HEAD, POST, PUT, PATCH, DELETE
*/
@RequestMapping("/hello-basic")
public String helloBasic() {
log.info("helloBasic");
return "ok";
}
}
@RequestMapping("/hello-basic")
/hello-basic
URL 호출이 오면 이 메서드가 실행되도록 매핑한다.- 대부분의 속성을 배열로 제공하므로 다중 설정이 가능하다.
{"/hello-basic", "/hello-go"}
스프링 부트 3.0 이전에는 다음 두 가지 URL에 대해 서로 같은 요청으로 매핑하였다
/hello-basic, /hello-bacis/ → /hello/basic
스프링 부트 3.0 이후부터는 위의 두 가지 URL 모두 서로 다른 요청으로 매핑한다. 즉, 기존에는 마지막에 있는 slash(/)를 제거했지만, 스프링 부트 3.0 이후부터는 마지막의 slash(/)를 유지한다. 따라서 다음과 같이 다르게 매핑해서 사용해야 한다.
/hello-basic → /hello-basic
/hello-basic/ → /hello-basic/
📜 HTTP 메서드
@RequestMapping
에method
속성으로 HTTP 메서드를 지정하여 원하는 요청을 받을 수 있다.- 만약
method
속성을 따로 지정해주지 않는다면 HTTP 메서드와 무관하게 호출된다.
/**
* method 특정 HTTP 메서드 요청만 허용
* GET, HEAD, POST, PUT, PATCH, DELETE
*/
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
log.info("mappingGetV1");
return "ok";
}
- HTTP 메서드를 축약한 애노테이션을 사용하는 것이 더 직관적이다. 아래 코드를 보면 내부에서
@RequestMapping
과method
를 지정해서 사용하는 것을 확인할 수 있다.
/**
* 편리한 축약 애노테이션 (코드보기)
* @GetMapping
* @PostMapping
* @PutMapping
* @DeleteMapping
* @PatchMapping
*/
@GetMapping(value = "/mapping-get-v2")
public String mappingGetV2() {
log.info("mapping-get-v2");
return "ok";
}
📜 PathVariable(❗)
- 최근 HTTP API는 다음과 같이 리소스 경로에 식별자를 넣는 스타일을 선호한다.
/mapping/userA
/users/1
@RequestMapping
은 URL 경로를 템플릿화 할 수 있는데,@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";
}
@PathVariable
의 이름과 파라미터 이름이 같으면 생략할 수 있다.
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable String userId) {
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";
}
- 위처럼 구현할 경우 파라미터로
"mode=debug"
가 포함되어 있어야 해당 메서드가 호출된다.
📜 특정 헤더 조건 매핑
- 특정 헤더가 있거나 없는 조건을 추가할 수 있다.
- 파라미터 매핑과 비슷하지만, HTTP 헤더를 사용한다.
/**
* 특정 헤더로 추가 매핑
* 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";
}
📜 미디어 타입 조건 매핑 - HTTP 요청 Content-Type, consume
- HTTP 요청의
Content-Type
헤더를 기반으로 미디어 타입으로 매핑한다. - 만약 맞지 않으면 HTTP 415 상태코드(Unsupported Media Type)를 반환한다.
/**
* 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";
}
📜 미디어 타입 조건 매핑 - 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";
}
📝 요청 매핑 - API 예시
- 회원 관리를 HTTP API로 만든다 생각하고 매핑을 어떻게 하는지 알아보자. (실제 데이터가 넘어가는 부분은 생략하고 URL 매핑만)
📜 회원 관리 API
- 회원 목록 조회: GET
/users
- 회원 등록: POST
/users
- 회원 조회: GET
/users/{userId}
- 회원 수정: PATCH
/users/{userId}
- 회원 삭제: DELETE
/users/{userId}
@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {
/**
* GET /mapping/users
*/
@GetMapping
public String users() {
return "get users";
}
/**
* POST /mapping/users
*/
@PostMapping
public String addUser() {
return "post user";
}
/**
* GET /mapping/users/{userId}
*/
@GetMapping("/{userId}")
public String findUser(@PathVariable String userId) {
return "get userId=" + userId;
}
/**
* PATCH /mapping/users/{userId}
*/
@PatchMapping("/{userId}")
public String updateUser(@PathVariable String userId) {
return "update userId=" + userId;
}
/**
* DELETE /mapping/users/{userId}
*/
@DeleteMapping("/{userId}")
public String deleteUser(@PathVariable String userId) {
return "delete userId=" + userId;
}
}
@RequestMapping("/mapping/users")
- 이전 시간에 설명했지만, 클래스 레벨에 매핑 정보를 두면 메서드 레벨에서 해당 정보를 조합해서 사용한다.
- 따라서 요청은 다음과 같이 진행할 수 있다.
- 회원 목록 조회: GET
/mapping/users
- 회원 등록: POST
/mapping/users
- 회원 조회: GET
/mapping/users/id1
- 회원 수정: PATCH
/mapping/users/id1
- 회원 삭제: DELETE
/mapping/users/id1
- 회원 목록 조회: GET
📌 Reference
'🍃 Spring, Spring Boot > 스프링 MVC' 카테고리의 다른 글
[Spring MVC] 7. 스프링 MVC - 기본 기능(3) (0) | 2023.06.22 |
---|---|
[Spring MVC] 6. 스프링 MVC - 기본 기능(2) (0) | 2023.06.22 |
[Spring MVC] 4. 스프링 MVC 구조 이해하기 (0) | 2023.06.19 |
[Spring MVC] 3. MVC 프레임워크 만들기 (0) | 2023.06.18 |
[Spring MVC] 2. MVC 패턴 (0) | 2023.06.16 |