코딩/sparta TIL

TIL 22 : Spring Annotation, Request Mapping

americanoallday 2025. 3. 19. 19:08

Spring Annotation 1강

@Slf4j

📚 Slf4j는 인터페이스이고 그 구현체로 Logback같은 라이브러리를 선택(default)한다. 실제 개발에서는 Spring Boot가 기본으로 제공하는 Logback을 대부분 사용한다.

  • IntelliJ로 clone한 프로젝트 열기 및 실행
  • Logging (로그 기능)
    • Thread 정보, 클래스 이름과 같은 부가 정보를 함께 확인할 수 있다.
    • 실제 운영 환경에서는 System.out.println();을 사용하여 Console에 정보를 출력하지 않고, 별도의 로깅 라이브러리를 사용하여 로그를 출력한다.
    • Log Level 설정을 통하여 Error 메세지만 출력하도록 하도록 하기도 하고 로그 메세지를 일자별로 모아서 저장하여 외부 저장소에 보관하기도 한다.
      • Log Level
        • TRACE > DEBUG > INFO > WARN > ERROR
    • 사용시 주의점
      package com.example.springbasicannotation.controller;
      
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.web.bind.annotation.*;
      
      @Slf4j // 로그를 작성하겠다.
      @RestController // 컨트롤러를 만들어준다.
      public class Slf4jController {
      
          @RequestMapping("/logging") //url 경로가 매개변수로 들어감.
          public String logging() {
      
              String sparta = "Sparta";
              // TRACE -> DEBUG -> INFO -> WARN -> ERROR
              log.trace("문자 trace={}", sparta);
              log.debug("문자 debug={}", sparta);
      
      	// resources > application.properties에서 레벨 설정. 설정하지않으면 기본 info 설정
              // info 설정레벨 하위 로그만 출력 됨
              // default
              log.info("문자 info={}", sparta);// 문자 연산을 진행하지 않는다.
              log.warn("문자 warn={}", sparta);
              log.error("문자 error={}", sparta);
      
              log.info("문자 info " + sparta); // 문자 연산을 먼저 해버린다.
              return "success";
          }
      
      }
      # com.example.springbasicannotation 하위 경로들의 로그 레벨을 설정한다.
      logging.level.com.example.springbasicannotation=TRACE
  • Postman 호출
    • Default Level(INFO) API 호출
    • 출력결과
      • resources > application.properties에서 레벨 설정. 설정하지않으면 기본 info 설정
      • info레벨 하위 로그만 출력 됨
    • level=TRACE

@Controller VS @RestController

📚 Annotation 기반의 Spring에서 Controller(Handler)를 만들 때 사용하는 어노테이션

    1. @Controller
      package com.example.springbasicannotation.controller;
      
      import org.springframework.stereotype.Controller;
      import org.springframework.web.bind.annotation.RequestMapping;
      
      @Controller
      public class ViewController {
      
          @RequestMapping("/view")
          public String example() {
              // logic
              return "sparta"; // ViewName이 return
          }
      
      }
      • View가 있는 경우에 사용한다.
      • 즉, Template Engine인 Thymeleaf, JSP 등을 사용하는 경우
      • Thymeleaf 예시
        • SpringBoot build.gradle 의존성 추가
        • main/resources/templates 가 기본 경로로 설정된다.
        • resources/templates/sparta.html
          <!DOCTYPE html>
          <html xmlns:th="http://www.thymeleaf.org">
          <head>
            <meta charset="UTF-8">
            <title>Hello</title>
          </head>
          <body>
          <h2>Thymeleaf Template Sample</h2>
          </body>
          </html>
      • 동작 순서
        • return 값이 String이면 ThymeleafViewResolver 에 의해 View Name으로 인식된다.
      • 호출결과
        • http://localhost:8080/view
    2. @RestController
      package com.example.springbasicannotation.controller;
      
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      @RestController
      public class ResponseController {
      
          @RequestMapping("/string")
          public String example() {
              // logic
              return "sparta"; // ViewName이 return 되는게 아니라, String Data가 반환된다.
          }
          
      }
      
      • 응답할 Data가 있는 경우에 사용한다.
      • 현재는 대부분 @RestController를 사용하여 API가 만들어진다. (Restful API)
      • return 값으로 View를 찾는것이 아니라 HTTP Message Body에 Data를 입력한다.
💡 View가 아닌 HTTP Message Body에 Data가 들어가는 이유는 아래에서 배울 @Responsebody와 관련이 있다.
    • 동작 순서
    • 호출결과

Spring Annotation 2강

Annotation 자세히 보기

💡 Spring에서 사용하는 Annotation들이 각각 어떤 역할을 수행하는지 알아보는 방법에 대해 학습한다면 모르는 Annotation이 나올 경우 어떻게 사용해야 하는지 스스로 찾아낼 수 있습니다.

      1. @Component
        • Spring Bean에 등록하는 역할을 수행한다.
          • Spring Bean은 애플리케이션의 구성 요소를 정의하는 객체이다.
          • WAS가 Servlet 코드를 읽어 컨테이너에 등록했던 것을 떠올려 봅시다.
          • Servlet Container(네트워크 기초 자료)
            📚 Servlet을 지원하는 WAS 내부에는 서블릿 컨테이너가 있다. 서블릿 컨테이너는 서블릿을 초기화, 생성, 관리, 호출, 종료하는 역할을 수행한다.
            • Servlet의 생명주기
              • Servlet은 서블릿 컨테이너가 생성 및 관리한다.
              • 즉, WAS(서블릿 컨테이너 포함)가 종료될 때 Servlet도 함께 종료된다.
            • Servlet 객체 생성시점
              • 개발자가 직접 인스턴스화 하여 사용하는것이 아닌, 코드만 작성하면 서블릿 컨테이너가 생성한다.
              • 서블릿 예시 코드
                @WebServlet(name="ExampleServlet", urlPatterns = "/example")
                public class ExampleServlet extends HttpServlet { // HttpServlet을 상속받아 구현한다.
                	
                	@Override
                	protected void service(
                		HttpServletRequest request,  // HTTP 요청 정보를 쉽게 사용할 수 있게 만드는 Servlet
                		HttpServletResponse response // HTTP 응답 정보를 쉽게 제공할 수 있게 만드는 Servlet
                	) {
                		// application logic
                	}
                
                }
                @WebServlet(name="Example2Servlet", urlPatterns = "/example2")
                // 위와 같은 코드
                
                @WebServlet(name="Example3Servlet", urlPatterns = "/example3")
                // 위와 같은 코드
                
                @WebServlet(name="Example4Servlet", urlPatterns = "/example4")
                // 위와 같은 코드
            • Servlet Container가 하는 일
              1. 서블릿을 초기화, 생성, 관리, 호출, 종료하는 역할을 수행
                1. Servlet 객체를 싱글톤으로 관리한다.
              2. 동시 요청에 대한 처리를 위해 Multi Thread를 지원한다.
                💬 Q. 싱글톤이 무엇인가요?
                A. 싱글톤은 객체를 하나만 생성하여 생성된 인스턴스를 공유하여 사용하는것을 의미합니다. 특정 클래스의 인스턴스가 여러개 생성되지 않도록 하여 자원의 낭비를 방지하고, 인스턴스를 공유함으로써 상태를 일관되게 유지하기 위함입니다. 하지만, 공유 변수 사용을 주의해야 합니다.
        •  @Indexed
          • 클래스가 컴포넌트 스캔의 대상으로 Spring Bean에 더 빠르게 등록되도록 도와준다.
        • Spring Bean, 컴포넌트 스캔에 대해서는 숙련주차 강의에서 자세히 다룰 예정
  1. @Target
    • @Target 이 선언된 하위 어노테이션이 어떤 범위에 적용되는지 설정한다.
    • ElementType Enum 속성
      • 각각의 Enum마다 적용되는 범위가 상단에 주석으로 설명되어 있다.
  2. @Retention
    • @Retention 하위의 어노테이션이 얼마나 오래 유지되는지를 결정한다.
    • RetentionPolicy Enum 속성
      1. SOURCE
        • 소스 코드(.java)에서만 유지된다.
        • 컴파일러에 의해 클래스 파일로 저장되지 않는다.
      2. CLASS
        • 컴파일된 클래스 파일(.class)에 저장되지만, JVM이 실행 시 읽지 않는다. (주석과 같음)
        • Default 값이다.
      3. RUNTIME
        • 클래스 파일(.class)에 저장되고, JVM에 의해 런타임 시점에 읽을 수 있다.
        • 즉, 실제 런타임 시점의 코드에 반영되어 영향을 준다.
  3. @Documented
    • Javadoc 등의 문서화 도구에 의해 문서화되어야 함을 나타낸다.

다시보는 @Controller VS @RestController

💡 개발에서 우선순위는 항상 자세히 선언된것이 우선순위가 높다.

  1. @Controller 다시보기
    • @Target(ElementType.Type)
      • Class, Interface, Annotation, Enum, Record Declaration(Java16) 에 적용할 수 있다.
    • @Retention(RetentionPolicy.RUNTIME)
      • 클래스 파일(.class)에 저장되고, JVM에 의해 런타임 시점에 읽을 수 있다.
    • @Document
      • Javadoc 등의 문서화 도구에 의해 문서화되어야 함을 나타낸다.
    • @Component
      • Spring Bean에 등록한다.
      • 싱글톤으로 관리된다.
  2. @RestController 다시보기
    • @Controller에 @ResponseBody가 결합된 어노테이션
    • @RestController는 @Controller와 달리 각 메서드마다 @ResponseBody를 추가하지 않아도 된다

❓ @Controller에 동일한 Target이 선언되어 있는데 왜 중복으로 선언이 되어있나요?

  • @ResponseBody 자세히 보기
    • @Target{ElementType.TYPE, ElementType.METHOD} 이지만, @RestController@Target(ElementType.TYPE) 이기 때문에 TYPE 범위에만 @ResponseBody가 적용된다.
    • @ResponseBody의 역할은 추후 강의에서 다룰 예정

Request Mapping 1강

@RequestMapping

📚 특정 URL로 Request를 보내면 들어온 요청을 Controller 내부의 특정 Method와 Mapping 하기 위해 사용한다.

💡 Client로부터 요청이 왔을 때 어떤 Controller가 호출될지 Mapping하는것은 단순히 URL로 Mapping 하는것이 아니라 여러가지 요소(URL, Method 등)를 조합하여 Mapping한다.

  • @RequestMapping
    1. Spring Boot 3.0 버전 이하
      • URL path /example, /example**/** 모두 허용(Mapping)한다.
    2. Spring Boot 3.0 버전 이상(현재 버전)
      • URL path /example 만 허용(Mapping)한다.
    3. 속성값들을 설정할 때 배열 형태로 다중 설정이 가능하다
      ex) @RequestMapping**({**”/example”, “/example2”, “/example3”**})**
    4. HTTP Method POST, GET, PUT, PATCH, DELETE, HEAD 모두 허용한다
    5. method 속성으로 HTTP 메서드를 지정하면 지정된것만 허용한다.
      package com.example.springbasicannotation.controller;
      
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RequestMethod;
      import org.springframework.web.bind.annotation.RestController;
      
      // 응답 데이터를 반환한다.
      @RestController
      public class RequestMappingController {
      
          // HTTP Method 는 GET만 허용한다.
          @RequestMapping(value = "/v1", method = RequestMethod.GET)
          public String exampleV1() {
              // logic
              return "this is sparta!";
          }
      
      }
      
    • 실행결과
    • 만약, 속성으로 설정된 HTTP Method로 요청이 오지 않는다면?
      • localhost:8080/v1 + POST, PUT, PATCH, DELETE 의 경우
      • HTTP 응답 상태코드 405(Client측 에러), Method Not Allowed Exception 반환
    • @GetMapping
      1. Target(ElementType.METHOD) Method Level에 해당 어노테이션을 적용한다 라는 의미
      2. 내부적으로 @RequestMapping(method = RequestMethod.GET) 을 사용하고 있다.
    • 코드예시
      // Post, GET, Put, Patch, Delete 모두 가능
      @GetMapping(value = "/v2")
      public String exampleV2() {
      	// logic
      	return "this is sparta!";
      }
      • Spring이 제공하는 Annotation들의 내부에 다 선언되어 있다.
        • 대부분의 필요한 기능들이 이미 만들어져 있다. 사용 하면된다.
      • @RequestMapping 보다는 직관적이고 축약된 @GetMapping, @PostMapping 형식을 일반적으로 사용한다.
      • 실행 결과
    • @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping
      • 모두 위의 @GetMapping과 같은 구조를 가지고 있다.



❓ 그럼 실제로는 위 어노테이션들만 사용하고 @RequestMapping은 사용하지 않나요?

  • @RequestMapping 사용 방법
    • @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping의 Target은 Method Level 이다.
    • 반면에 @RequestMapping의 Target은 class, method 레벨에 적용이 가능하다.
    • Restful API의 계층 구조
      ex) users/{userId}, category/{categoryId}/product/{productId}
    • prefix로 선언할 URL을 class 레벨에 적용하는 것에 주로 사용된다.
    • 코드 예시
      @RequestMapping("/prefix")
      @RestController
      public class RequestMappingController {
      	// Post, GET, Put, Patch, Delete 모두 가능
      	@GetMapping(value = "/v3")
      	public String exampleV3() {
      		// logic
      		return "this is sparta!";
      	}
      
      }
    • 실행 결과

@PathVariable

📚 HTTP 특성 중 하나인 비연결성을 극복하여 데이터를 전달하기 위한 방법 중 하나이다. URL로 전달된 값을 파라미터로 받아오는 역할을 수행한다.

  • @PathVariable
    1. 경로 변수를 중괄호에 둘러싸인 값으로 사용할 수 있다.
    ex) user/{id}
    1. 기본적으로 @PathVariable로 설정된 경로 변수는 반드시 값을 가져야 하며 값이 없으면 응답 상태코드 404 Not Found Error가 발생한다.
    2. 최근 Restful API를 설계하는 것이 API의 기준이 되며 해당 어노테이션의 사용 빈도가 높아졌다.

💡 Restful API를 설계하게 되면 URL path 만으로 어떤 Resource을 사용하는지,
HTTP Method 만으로 어떤 기능이 동작되는지 쉽게 알아볼 수 있다.

  • Restful API
    REST API URI Naming Conventions and Best Practices
    • Create - POST
    • Read - GET
    • Update - PUT, PATCH
    • Delete - DELETE
  • Restful API 설계 예시
    • postId글의 comment 댓글 작성
      • POST + posts/{postId}/comments
    • postId글의 comment 댓글 전체 조회
      • GET + posts/{postId}/comments
    • postId글의 commentId 댓글 단 건 조회
      • GET + posts/{postId}/comments/{commentId}
    • postId글의 commentId 댓글 수정
      • PUT + posts/{postId}/comments/{commentId}
    • postId글의 commentId 댓글 삭제
      • DELETE + posts/{postId}/comments/{commentId}
  • @PathVariable 규칙
    1. 파라미터 변수명과 PathVariable 변수명이 같으면 속성 값 생략 가능
      @RequestMapping("/posts")
      @RestController
      public class PathVariableController {
      	
      	// postId로 된 post 단건 조회
      	@GetMapping("/{postId}")
      	public String pathVariableV1(@PathVariable("postId") Long data) {
      		// logic
      		String result = "PathvariableV1 결과입니다 : " + data;
      		return result;
      	}
      }
      • 실행결과
      • 생략
        @RequestMapping("/posts")
        @RestController
        public class PathVariableController {
        	
        	// 변수명과 같다면 속성값 생략가능
        	@GetMapping("/{postId}")
        	public String pathVariableV2(@PathVariable Long postId) {
        		// logic
        		String result = "PathvariableV2 결과입니다 : " + postId;
        		return result;
        	}
        	
        }
      • 실행결과
    2. @PathVariable 다중 사용 가능
      @RestController
      public class PathVariableController {
      	
      	@GetMapping("/{postId}/comments/{commentId}")
      	public String pathVariableV3(
      		@PathVariable Long postId,
      		@PathVariable Long commentId
          ) {
      		// logic
      		String result = "PathvariableV3 결과입니다 postId : " + postId + "commentsId : " + commentId;
      		return result;
      	}
      	
      }
      • 실행결과
        @RequestMapping("/posts/{postId}")
        @RestController
        public class PathVariableController {
        	
        	@GetMapping("/comments/{commentId}")
        	public String pathVariableV4(
        		@PathVariable Long postId,
        		@PathVariable Long commentId
                ) {
        		// logic
        		String result = "PathvariableV4 결과입니다 postId : " + postId + "commentsId : " + commentId;
        		return result;
        	}
        	
        }
      • 실행결과

Request Mapping 2강

특정 파라미터 매핑

📚 속성 설정을 통하여 특정 헤더, 특정 파라미터와 Mapping 할 수 있다.

  1. Parameter 추가 매핑
    • 특정 파라미터와 매핑하는 방법
      package com.example.springbasicannotation.controller;
      
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      @RestController
      public class ParameterController {
      
          // parms 속성값 추가
          @GetMapping(value = "/users", params = "gender=man")
          public String params() {
              // logic
              String result = "params API가 호출 되었습니다.";
              return result;
          }
      
      }
      
    • 실제 URL GET http://localhost:8080/users**?gender=man** 파라미터가 있어야 호출된다.
    • 실행결과
    • 파라미터가 없다면?
      • 400 Bad Request 클라이언트 측 에러
    • 속성 작성 규칙
      1. params = "gender"
        • params의 key값은 커스텀이 가능하다
        • value는 없어도 된다.
      2. params = "!gender"
        • gender가 없어야 한다.
      3. params = "gender=man"
        • gender=man 이어야 한다.
      4. params = "gender!=man"
        • params의 value값이 man가 아니여야 한다.
      5. params = {"gender=man", "gender=woman"}
        • 배열로 속성 값을 여러 개 설정이 가능하다.
  2. 특정 Header 매핑
    • 특정 Header와 매핑하는 방법
      @RestController
      public class ParameterController {
      	
      	// headers 속성값 추가
        @PostMapping(value = "/users", headers = "Content-Type=application/json")
        public String headers() {
            // logic
            String result = "headers API가 호출 되었습니다.";
            return result;
        }
      	
      }
    • Postman → Body → raw → JSON
    • Postman → Headers → hidden

      • HTTP Header를 사용하기 때문에 Postman으로 테스트 해야 한다.
        ex) key=Content-Type / value=application/json
      • 실행결과
      • 속성 작성 규칙은 위 params 속성 값의 규칙과 같다.
  3. MediaType 매핑, consume(수용)
    • HTTP Header Content-Type(요청)과 매핑 된다.
      @RestController
      public class ParameterController {
      	
      	// consumes 속성값 추가
        @PostMapping(value = "/users", consumes = "application/json") // MediaType.APPLICATION_JSON_VALUE
        public String consumes() {
            // logic
            String result = "consumes API가 호출 되었습니다.";
            return result;
        }
      	
      }
  • consumes 속성 value값으로는 이미 Spring에서 제공되는 Enum인 MediaType.APPLICATION_JSON_VALUE 형태로 사용한다.
  • Postman → Body → raw → JSON
  • Postman → Headers → Content-Type → Value
  • 파라미터가 없거나 다르다면?
    • HTTP 상태코드 405 Unsupported Media Type Exception 발생
  • 속성 작성 방법
    1. consumes=”application/json”
      • application/json 미디어 타입 허용
    2. consumes=”!application/json”
      • application/json 제외 미디어 타입 허용
    3. consumes=”application/*”
      • application/ 으로 시작하는 모든 미디어 타입 허용
    4. consumes=”*\/*”
      • 모두 허용
  1. MediaType 매핑 produces(제공)
    • 요청 헤더의 Accept 값에 따라서 produces 하는 값이 변한다.
      @RestController
      public class ParameterController {
      	
      	// produces 속성값 추가
        @GetMapping(value = "/users", produces = "text/plain")
        public String produces() {
            // logic
            String result = "text/plain 데이터 응답";
            return result;
        }
      	
      }
    • HTTP 요청 Accept Header에 Media Type이 있어야한다.
      • */* : 전체 Media Type 허용
    • 실행결과
    • consumes 속성 사용법과 같다.

    • 위에 나온 모든 MediaType은 Spring이 제공하는 Enum을 사용하면 된다.
      ex) produces = “application.json"produces = MediaType.APPLICATION_JSON_VALUE*

 

Spring이 지원하는 Parameter

📚 어노테이션 기반 Spring의 Controller는 다양한 파라미터를 쉽게 사용할 수 있도록 지원한다.

    • HTTP 헤더 조회
      • Spring에서 요청 Header에 쉽게 접근할 수 있다.
        • HttpServletRequest와 같이 파라미터로 다룰 수 있다.
      • Controller 예시
        // 로깅
        @Slf4j
        @RestController
        public class RequestHeaderController {
        
            @GetMapping("/request/headers")
            public String headers(
                    HttpServletRequest request, // Servlet에서 사용한것과 같음
                    HttpServletResponse response, // Servlet에서 사용한것과 같음
                    @RequestHeader MultiValueMap<String, String> headerMap,
                    @RequestHeader("host") String host,
                    @CookieValue(value = "cookie", required = false) String cookie,
                    HttpMethod httpMethod,
                    Locale locale
            ) {
        		    // Servlet
                log.info("request={}", request);
                log.info("response={}", response);
                
                // @RequestHeader
                log.info("headerMap={}", headerMap);
                log.info("host={}", host);
                
                // @CookieValue
                log.info("cookie={}", cookie);
                
                // HttpMethod
                log.info("httpMethod={}", httpMethod);
                
                // Locale
                log.info("Locale={}", locale);
        
                return "success";
            }
        }
      • Postman API 호출
      • Log 출력 결과
        1. request
          • HttpServletRequest 객체 주소 값
        2. response
          • HttpServletRequest 객체 주소 값
        3. headerMap :
          hashMap={
          	user-agent=[PostmanRuntime/7.35.0], 
          	accept=[*/*], 
          	postman-token=[5f324c1c-7902-4750-9e01-2c4d093e8ad6],
          	host=[localhost:8080],
          	accept-encoding=[gzip, deflate, br],
          	connection=[keep-alive]
          }
        4. host
          • host 정보
        5. cookie
          • Header의 Cookie 값
          • @CookieValue(value = "cookie", required = false) String cookie
          • required 필수값인지 아닌지, true면 항상 쿠키가 있어야만 헤더 메서드가 실행및 맵핑됨.
        6. httpMethod
          • 호출에 사용한 HttpMethod
        7. Locale
          • 위치 정보를 나타내는 헤더
          • 우선순위가 존재한다.
    • MultiValueMap
💡 Map과 유사하게 Key, Value 형식으로 구현되어 있지만 하나의 Key가 여러 Value를 가질 수 있다 HTTP Header, Reqeust Parameter와 같이 하나의 Key에 여러 값을 받을 때 사용한다.

ex) key1=value1&key1=value2

  • 예시코드
    MultiValueMap<String, String> linkedMultiValuemap = new LinkedMultiValueMap();
    
    // key1에 value1 저장
    linkedMultiValuemap.add("key1", "value1");
    // key1에 value2 저장
    linkedMultiValuemap.add("key1", "value2");
    
    // key1에 저장된 모든 value get
    List<String> values = linkedMultiValuemap.get("key1");