운영 시스템에서는 System.out.println()과 같은 시스템 콘솔 출력 대신 별도의 로깅 라이브러리를 사용한다. 스프링 부트는 기본적으로 인터페이스로 SLF4J, 구현체로는 Logback을 사용한다.
로깅 사용 방식은 여러 가지가 있지만, 일반적으로 @Slf4j 애노테이션을 클래스에 선언하여 log라는 인스턴스 변수를 사용하는 방식을 주로 채택한다. 이를 통해 코드가 간결해지고, 다양한 로깅 레벨(예: DEBUG, INFO, WARN, ERROR)을 체계적으로 관리할 수 있다.
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);

로그 레벨은 TRACE부터 ERROR까지 다양한 수준으로 제공되며, 각각 다음과 같은 의미를 가진다:
TRACE
가장 상세한 로그 레벨로, 디버깅 용도로 사용된다. 코드의 특정 부분이 어떻게 동작하는지 상세히 추적하고 싶을 때 유용하며, 보통 개발 환경에서만 출력되도록 설정된다.
DEBUG
디버깅에 필요한 정보를 제공하는 레벨이다. 예상치 못한 이벤트나 상태를 기록하고 문제 해결에 필요한 추가 정보를 출력한다.
INFO
애플리케이션의 주요 이벤트나 상태 변경을 기록하는 레벨이다. 애플리케이션의 실행 상태를 추적하거나 주요 이벤트를 기록할 때 사용한다.
WARN
잠재적인 문제를 나타내는 경고 메시지를 기록하는 레벨이다. 아직 문제가 발생하지 않았지만, 발생할 가능성이 있는 상황을 예방적으로 기록한다.
ERROR
오류가 발생한 상황을 기록하는 레벨이다. 예기치 못한 예외나 치명적인 오류 상황을 기록할 때 사용한다.
부가 정보 출력
로그에는 쓰레드 정보, 클래스 이름 등 부가 정보를 함께 출력할 수 있다.
출력 형식 조절
로그의 출력 모양을 설정 파일(예: Logback 설정)을 통해 원하는 대로 조절할 수 있다.
로그 레벨 필터링
개발 서버에서는 모든 로그 레벨(TRACE ~ ERROR)을 출력하고, 운영 서버에서는 특정 레벨만 출력하거나 로그를 기록하지 않도록 설정할 수 있다.
로그 저장 위치
로그는 파일, 네트워크, 데이터베이스 등 별도의 위치에 저장할 수 있다. 또한, 로그를 일별 또는 특정 용량 단위로 분할 저장할 수 있다.
성능
로그 라이브러리를 활용하면 System.out.println()보다 성능이 훨씬 뛰어나다. 이는 로그 처리가 비동기적으로 이루어질 수 있고, 필요하지 않은 레벨의 로그를 무시하기 때문이다.
로그 레벨과 설정을 통해 개발 환경과 운영 환경에서 적합한 로그를 효율적으로 관리할 수 있다. 상세한 로그 추적(TRACE, DEBUG)부터 운영에 필요한 주요 이벤트 및 오류 기록(INFO, WARN, ERROR)까지, 로그의 역할과 범위를 적절히 설정하여 시스템 성능과 안정성을 유지하는 것이 중요하다.
@RestController
public class MappingController {
private Logger log = LoggerFactory.getLogger(getClass());
// (1)
@RequestMapping(value = "/hello-basic")
public String helloBasic() {
log.info("helloBasic");
return "ok";
}
// (2)
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
log.info("mappingGetV1");
return "ok";
}
// (3)
@GetMapping("/mapping-get-v2")
public String mappingGetV2() {
log.info("mappingGetV2");
return "ok";
}
// (4)
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
log.info("mappingPath userId={}", data);
return "ok";
}
/**
* PathVariable 다중 사용
*/
// (5)
@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"} */
// (6)
@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" (! = ) */
// (7)
@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
*/
// (8)
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
// (9)
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
}
@RestController = @Controller + @ResponseBody@Controller는 반환 값이 String일 경우, 뷰 이름으로 인식하여 뷰 리졸버(View Resolver)가 동작하고 해당 이름에 맞는 템플릿 파일을 찾는다.@ResponseBody를 사용하면 반환 값이 뷰 이름이 아닌 HTTP 메시지 바디에 직접 쓰여진다. 즉, @RestController를 사용하면 별도로 @ResponseBody를 명시하지 않아도, 반환 값이 HTTP 메시지 바디에 직접 작성되는 RESTful 응답을 손쉽게 구현할 수 있다.
실제로 @RequestMapping은 우리가 스프링MVC를 이용하여 백엔드를 구성할 때 가장 많이 사용하는 애노테이션 기반의 핸들러 매핑이다. 가장 높은 우선순위의 매핑 객체인 RequestMappingHandlerMapping은 @RequestMapping, @GetMapping, @PostMapping 등 애노테이션 기반 매핑을 처리하며, 스프링의 기본 동작에서 가장 먼저 검색된다.

@RequestMapping은 DispatcherServlet의 RequestMappingHandlerMapping을 통해 작동하며, 적절한 어댑터를 찾아 우리가 작성한 비즈니스 로직 코드를 실행시킨다. 아래는 @RequestMapping의 다양한 사용 방법이다.
기본적인 RequestMapping 사용
URI만 매핑하여, 모든 HTTP 메서드(GET, POST 등)에 대해 메서드가 실행된다.
@RequestMapping("/example")
HTTP 메서드 제한
method 속성을 추가해 GET 요청만 처리하도록 제한한다.
@RequestMapping(value = "/example", method = RequestMethod.GET)
축약형 애노테이션 사용
@RequestMapping의 GET 요청 전용 축약형인 @GetMapping 사용.
@GetMapping("/example")
동적 URI 매핑
URI의 패스 변수를 사용하여 동적인 URI를 처리한다.
@GetMapping("/example/{id}")
다중 패스 변수 사용
여러 패스 변수를 선언해 동적으로 처리 가능하다.
@GetMapping("/example/{category}/{id}")
필수 요청 파라미터 등록
특정 요청 파라미터가 포함된 요청만 처리하도록 제한한다.
@GetMapping(value = "/example", params = "key=value")
필수 요청 헤더 등록
특정 헤더가 포함된 요청만 처리하도록 제한한다.
@GetMapping(value = "/example", headers = "X-Custom-Header=custom-value")
요청 Content-Type 제한
요청의 Content-Type을 제한하여 특정 타입만 처리 가능하도록 설정한다.
@PostMapping(value = "/example", consumes = "application/json")
응답 Content-Type 고정
응답 Content-Type을 설정하고, 요청의 Accept 헤더와 매칭해야 처리된다.
@GetMapping(value = "/example", produces = "application/json")
@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";
}
}
response, request
기존에 사용하던 요청, 응답 객체로 서블릿에서 제공되는 기본 기능이다.
HttpMethod
Spring MVC에서 지원하며, 요청 HTTP의 Method 정보만 따로 주입받아 사용할 수 있다.
Locale
요청의 언어 정보를 제공한다.
@RequestHeader MultivalueMap<String, String>
요청 헤더의 모든 정보를 MultivalueMap 형태로 주입받는다.
MultivalueMap은 key: List<> 구조로, 하나의 키에 여러 값을 가질 수 있다.@RequestHeader("host")
특정한 헤더 정보를 따로 주입받는다.
@CookieValue(value, required)
요청 쿠키 중 value에 해당하는 쿠키 값을 가져올 수 있다.
required 속성을 통해 쿠키의 필수 여부를 설정할 수 있다.기본 서블릿으로 컨트롤러를 만들 때는 요청 객체인 request를 변수로 받아 사용해야 했다. 그러나 Spring MVC는 필요한 정보만 주입받을 수 있는 기능을 지원한다. 이를 통해 컨트롤러는 필요한 데이터만 접근할 수 있어 불필요한 비용이 줄어들고, 코드의 가독성과 유지보수성이 크게 향상된다.
위에서는 HTTP 요청에 대해 매핑하는 Spring MVC의 기능(@RequestMapping)과 헤더 조회에 관해 다루었다.
요청 데이터 조회는 크게 다음 3가지로 구분된다:
GET 쿼리 파라미터와 POST 폼 데이터
/example?key=value 또는 POST 요청의 application/x-www-form-urlencoded 데이터.HTTP 요청 Body 데이터
application/json 형태의 데이터로 전달된 요청.파일 업로드 데이터
multipart/form-data를 통한 파일 전송.Spring MVC는 각각의 데이터 조회 방식에 맞는 주입 및 처리를 간편하게 지원한다.
URL의 쿼리 파라미터에 데이터를 포함해서 전달하는 방식과 HTML 폼에 데이터를 담아 전달하는 방식은 둘 다 메시지 바디를 사용하지 않고 쿼리 파라미터에 데이터를 담아 전송한다는 공통점이 있다.
이 두 방식을 묶어 말하는 이유는 요청 데이터가 메시지 바디가 아닌 URL 또는 폼 필드에 포함되어 전송되며, 주로 다음과 같은 형태를 가진다는 점이다:
GET 요청 (쿼리 파라미터)
/example?key=value&name=JohnPOST 요청 (폼 데이터)
application/x-www-form-urlencoded 형식으로 전송. key=value&name=John (요청 본문에 포함되지만, 메시지 바디를 사용하는 JSON과는 다름). 이 방식들은 간단한 데이터 전달 및 조회 요청에 적합하며, 구조화된 대량 데이터 전송보다는 간단한 키-값 데이터 전송에 주로 사용된다.
@Slf4j
@Controller
public class RequestParamController {
// (1)
@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");
}
// (2)
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(
@RequestParam("username") String memberName,
@RequestParam("age") int memberAge
) {
log.info("username = {}, age={}", memberName, memberAge);
return "ok";
}
// (3)
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
@RequestParam String username,
@RequestParam int age
) {
log.info("username = {}, age={}", username, age);
return "ok";
}
// (4)
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4 (String username, int age) {
log.info("username = {}, age={}", username, age);
return "ok";
}
//(5)
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
@RequestParam(required = true) String username,
@RequestParam(required = false) Integer age // null이 들어와야 하므로 int는 불가능하다 -> 좋은 엣지 팁인데
// "" , null 구분
) {
log.info("username = {}, age={}", username, age);
return "ok";
}
//(6)
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefualt(
@RequestParam(required = true, defaultValue = "guest") String username,
@RequestParam(required = false, defaultValue = "-1") int age
// default -> 리콰이어드 필요 없어짐
// "" -> 빈문자도 디폴트로 바꿔줌
) {
log.info("username = {}, age={}", username, age);
return "ok";
}
//(7)
@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";
}
//(8)
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("username = {}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
//(9)
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData) {
log.info("username = {}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
기존 서블릿 요청, 응답 객체를 이용한 컨트롤러
request.getParameter를 활용하여 요청 데이터를 수동으로 추출. (단순 서블릿 방식)@RequestParam을 사용한 요청 데이터 추출
@RequestParam을 통해 요청 전체 객체를 주입받는 것이 아니라, 쿼리 파라미터의 내용만 전달받는다.@RequestParam의 인자 생략
@RequestParam에 명시적으로 이름을 지정하지 않고, 변수 이름을 쿼리 파라미터 이름과 동일하게 구성하면 자동으로 매핑된다.@RequestParam 자체 생략 가능
@RequestParam을 생략해도 쿼리 파라미터를 매핑한다. @RequestParam을 명시하면 해당 변수가 쿼리 파라미터임을 명확히 보여줄 수 있어 가독성과 유지보수에 유리하다.필수 쿼리 파라미터 지정 (required 속성)
required 속성을 설정하여 필수 쿼리 파라미터를 지정할 수 있다. 기본값은 true. key=)을 전달하면 여전히 required=true를 충족하기 때문에, null과 빈 문자열을 구분하여 활용해야 한다.디폴트 값 설정 (defaultValue 속성)
defaultValue를 통해 특정 값을 자동으로 적용할 수 있다. required는 사실상 필요하지 않다. 또한, 빈 문자열로 쿼리 파라미터가 전달되어도 디폴트 값이 적용된다.모든 쿼리 파라미터를 맵으로 가져오기
Map 자료구조로 한꺼번에 받을 수 있다.쿼리 파라미터와 객체 바인딩
@Data(롬복) 애노테이션이 적용되거나, 적절한 getter/setter 메서드가 정의되어 있어야 한다.@ModelAttribute 생략 가능
int, String 등)이 아닌 경우, 별도로 명시하지 않아도 스프링은 자동으로 @ModelAttribute를 적용한다. argument resolver에 등록되지 않은 클래스만 해당된다.@Slf4j
@Controller
public class RequesBodyStringController {
private ObjectMapper objectMapper = new ObjectMapper();
// (1)
@PostMapping("/request-body-string-v1")
public void requestBodyString(HttpServletRequest request, HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
response.getWriter().write("ok");
}
// (2)
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {
log.info("messageBody={}", httpEntity.getBody());
return new HttpEntity<>("ok");
}
// (3)
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) throws IOException {
log.info("messageBody={}", messageBody);
return "ok";
}
}
// (4)
@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 helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
response.getWriter().write("ok");
}
// (5)
@ResponseBody
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
log.info("messageBody={}", messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "OK";
}
//(6)
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "OK";
}
//(7)
@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> data) {
HelloData helloData = data.getBody();
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "OK";
}
//(8)
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return data;
}
서블릿만을 이용한 메시지 바디 처리
inputStream을 사용하여 요청 메시지 바디를 읽는다. response 객체를 활용하여 응답 메시지 바디를 수동으로 구성한다.HttpEntity를 이용한 메시지 처리
HttpEntity 객체를 메서드 인자로 사용하여 요청의 엔티티(헤더 및 바디 포함)를 자동으로 주입받는다. @RequestBody를 이용한 메시지 바디 처리
ObjectMapper를 사용하지 않고 스프링이 주입해준 객체를 자동으로 디코딩하여 바인딩할 수 있다.JSON 데이터 송신 컨트롤러 (서블릿 방식)
ObjectMapper를 사용해 JSON 데이터를 객체와 바인딩하거나 다시 JSON으로 변환한다.@RequestBody와 스프링의 메시지 컨버터
@RequestBody를 사용하면 별도의 ObjectMapper 없이도 메시지 바디를 읽고 자동으로 객체에 바인딩할 수 있다.바인딩 객체 사용
HttpEntity를 이용한 객체 바인딩
HttpEntity로 받아 바인딩된 데이터를 활용할 수 있다.@ResponseBody를 통한 응답 데이터 처리
@ResponseBody를 부여하여 응답 메시지 바디를 객체와 바인딩할 수 있다. 스프링 MVC는 다음과 같은 경우 메시지 컨버터(Message Converter)를 적용한다:
@RequestBody HttpEntity (또는 RequestEntity)@ResponseBody HttpEntity (또는 ResponseEntity)HttpMessageConverter 인터페이스스프링의 메시지 컨버터는 HttpMessageConverter 인터페이스를 기반으로 구현되며, 각 메시지 컨버터는 아래의 주요 메서드를 구현한다:
canRead(Class<?> clazz, MediaType mediaType)
canWrite(Class<?> clazz, MediaType mediaType)
read(Class<?> clazz, HttpInputMessage inputMessage)
write(Object t, MediaType contentType, HttpOutputMessage outputMessage)
HttpMessageConverter의 다양한 구현체가 있으며, 각각 특정 미디어 타입과 데이터 형식을 처리한다:
MappingJackson2HttpMessageConverter: JSON 데이터를 처리. (기본적으로 Jackson 라이브러리 사용)StringHttpMessageConverter: 텍스트 데이터를 처리.ByteArrayHttpMessageConverter: 바이너리 데이터를 처리.FormHttpMessageConverter: 폼 데이터를 처리.스프링 MVC는 요청과 응답의 내용과 미디어 타입에 따라 적합한 메시지 컨버터를 자동으로 선택해 사용한다.
우리는 이전에 @ModelAttribute를 공부하며 Argument Resolver의 존재를 확인했다. 컨트롤러 메서드의 인자로 객체를 주입받도록 설정하면, 해당 객체가 Argument Resolver에 등록되지 않은 경우 @ModelAttribute를 통해 커스텀 객체와 바인딩이 이루어진다는 것을 알게 되었다.
컨트롤러에서 다양한 파라미터를 사용할 수 있는 이유는 Argument Resolver 덕분이다.
핸들러 어댑터는 실제로 컨트롤러를 호출하기 전에 메서드의 파라미터를 살펴보고, 적절한 Argument Resolver를 호출하여 컨트롤러로 전달될 객체를 생성하거나 주입한다.
예:
HttpServletRequest, HttpSession)@ModelAttribute)@RequestParam)@RequestBody)컨트롤러의 반환값을 처리하는 것은 Return Value Handler가 담당한다.
이것은 Argument Resolver와 유사한 방식으로 동작하며, 컨트롤러의 반환값을 적절한 형식으로 변환하여 클라이언트로 전달한다.
MappingJackson2HttpMessageConverter 활용) @ResponseBody가 사용된 경우 HTTP 메시지 컨버터로 처리 Argument Resolver
Return Value Handler
핸들러 어댑터는 이 두 구성 요소를 통해 요청 데이터를 컨트롤러로 전달하고, 응답 데이터를 클라이언트로 반환하는 전반적인 작업을 처리한다.
출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard