[1/24] TIL - Spring Annotation, Client에서 Server로 데이터를 보내는 방식

2025. 1. 24. 22:37개발 회고/TIL

😊오늘 배운 내용

오늘은 스프링의 어노테이션과 클라이언트가 서버에게 데이터를 보내는 3가지 방식에 대해 배웠다.

 

[@Slf4j]

로그를 출력하는데 사용하는 어노테이션이다.

스프링의 여러개 어노테이션들은 인터페이스로 이루어져있다. 

slf4j 어노테이션은 위와 같이 생겼다. 

slf4j 어노테이션 위에도 여러개의 어노테이션이 붙어있다. 

Target은 정말 말 그대로 이 어노테이션이 어떠한 것을 타겟으로 하는지에 대해 enum타입으로 범위를 지정한다. Retention은 보유하다 라는 뜻인데 해당 어노테이션이 어느 시점까지 유지되는지 나타내는 어노테이션이다. 

Retention의 3가지 종류

@Retention은 다음 세 가지 값을 가질 수 있어:

  1. SOURCE
    • 어노테이션이 컴파일 시점에만 유지되고, 컴파일된 .class 파일에는 포함되지 않음.
    • 예: @Override, @SuppressWarnings
    • 주로 코드 가독성이나 IDE 도움을 위해 사용.
  2. CLASS
    • 어노테이션이 컴파일 후 .class 파일에 포함되지만, JVM이 실행 시점에 읽을 수는 없음.
    • 기본값(디폴트)으로, 특정한 설정이 없으면 이 값으로 설정됨.
    • 예: Lombok의 어노테이션
    • 런타임에 필요 없고, 컴파일 과정에서만 의미 있는 경우 사용.
  3. RUNTIME
    • 어노테이션이 런타임까지 유지되고, JVM에서도 참조 가능.
    • 리플렉션을 통해 어노테이션 정보를 읽어야 할 때 사용.
    • 예: 스프링의 @Controller, @Service 등 런타임에 처리되는 어노테이션.

 

@Target의 주요 역할

어노테이션을 클래스, 메서드, 필드, 매개변수 등 특정 위치에만 사용할 수 있도록 제한함.
이렇게 하면 어노테이션이 의도치 않은 곳에 잘못 사용되는 것을 방지할 수 있어.


@Target의 주요 ElementType 종류

@Target은 **ElementType**의 배열을 인수로 받아, 적용 대상을 지정해.
다음은 자주 사용되는 ElementType 값들이야:

  1. TYPE
    • 클래스, 인터페이스, 열거형, 애노테이션에 적용 가능.
    • 예: @Component, @Service
  2. FIELD
    • 클래스의 멤버 변수(필드)에 적용.
    • 예: @Autowired
  3. METHOD
    • 메서드에 적용.
    • 예: @GetMapping, @PostMapping
  4. PARAMETER
    • 메서드 매개변수에 적용.
    • 예: @RequestParam
  5. CONSTRUCTOR
    • 생성자에 적용.
    • 예: 특정 생성자 로직에 어노테이션을 붙이고 싶을 때.
  6. LOCAL_VARIABLE
    • 로컬 변수에 적용. (잘 사용되지 않음.)
  7. ANNOTATION_TYPE
    • 다른 어노테이션에 적용 가능하도록 설정.
    • 메타 어노테이션을 정의할 때 사용.
  8. PACKAGE
    • 패키지 선언에 적용.
    • 예: 패키지 정보에 특정 메타데이터를 붙이고 싶을 때.
  9. TYPE_USE
    • 타입 선언에 적용. (Java 8 이상)
    • 예: 제네릭 타입, 배열, 타입 캐스팅 등 다양한 곳에 어노테이션을 붙일 수 있음.

slf4j는 Log Level 설정을 통하여 Error 메세지만 출력하도록 하도록 하기도 하고 로그 메세지를 일자별로 모아서 저장하여 외부 저장소에 보관하기도 한다.

  • Log Level
    • TRACE > DEBUG > INFO > WARN > ERROR
// default
log.info("문자 info={}", sparta);// 문자 연산을 진행하지 않는다.
log.warn("문자 warn={}", sparta);
log.error("문자 error={}", sparta);

log.info("문자 info " + sparta); // 문자 연산을 먼저 해버린다.

로그를 출력할 때 + 연산자를 이용하면 문자연산을 먼저 해버리기 때문에 항상 중괄호를 이용하여 치환해서 출력하는 습관을 길러야 한다. 

 

또한 application.properties 파일에서 로그레벨을 설정할 수 있다.

# com.example.springbasicannotation 하위 경로들의 로그 레벨을 설정한다.
logging.level.com.example.springbasicannotation=TRACE

 

[@Controller VS @RestController]

두 어노테이션의 가장 큰 차이점 -> 반환할 View가 있을 경우에는 @Controller 사용! View가 아닌 단순 데이터를 반환할 때는 @RestController를 사용!

 

@Controller
public class ViewController {

    @RequestMapping("/view")
    public String example() {
        // logic
        return "sparta"; // ViewName이 return
    }

}
  • View가 있는 경우에 사용한다.
  • 즉, Template Engine인 Thymeleaf, JSP 등을 사용하는 경우
@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를 입력한다.

 

 

[@Component]

  • Spring Bean에 등록하는 역할을 수행한다. (싱글톤으로 관리된다.)
    • Spring Bean은 애플리케이션의 구성 요소를 정의하는 객체이다.
    • WAS가 Servlet 코드를 읽어 컨테이너에 등록했던 것을 떠올려 봅시다.
    • @Indexed
      • 클래스가 컴포넌트 스캔의 대상으로 Spring Bean에 더 빠르게 등록되도록 도와준다.

 

[@RequestMapping]

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

  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”**})**

  1. HTTP Method POST, GET, PUT, PATCH, DELETE, HEAD 모두 허용한다
  2. method 속성으로 HTTP 메서드를 지정하면 지정된것만 허용한다.
// 응답 데이터를 반환한다.
@RestController
public class RequestMappingController {

    // HTTP Method 는 GET만 허용한다.
    @RequestMapping(value = "/v1", method = RequestMethod.GET)
    public String exampleV1() {
        // logic
        return "this is sparta!";
    }

}

 

[@GetMapping]

RequestMapping을 사용할 때 매번 method = RequestMethod.GET을 명시해주기 귀찮으니 이렇게 사용한다. 

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

 

GetMapping 이외에도 다음과 같이 http 메소드 별로 있다.

  • @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping

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

 

  • @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping의 Target은 Method Level 이다.
  • 반면에 @RequestMapping의 Target은 class, method 레벨에 적용이 가능하다.
@RequestMapping("/prefix")
@RestController
public class RequestMappingController {
	// Post, GET, Put, Patch, Delete 모두 가능
	@GetMapping(value = "/v3")
	public String exampleV3() {
		// logic
		return "this is sparta!";
	}

}

 

따라서 해당 컨트롤러 클래스의 메소드들에 공통되는 api는 RequestMapping을 통해서 api prefix를 해주는 것처럼 사용한다.

 

[@PathVariable]

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

 

  1. 경로 변수를 중괄호에 둘러싸인 값으로 사용할 수 있다.

ex) user/{id}

  1. 기본적으로 @PathVariable로 설정된 경로 변수는 반드시 값을 가져야 하며 값이 없으면 응답 상태코드 404 Not Found Error가 발생한다.
  2. 최근 Restful API를 설계하는 것이 API의 기준이 되며 해당 어노테이션의 사용 빈도가 높아졌다.

 

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}
@RequestMapping("/posts")
@RestController
public class PathVariableController {
	
	// postId로 된 post 단건 조회
	@GetMapping("/{postId}")
	public String pathVariableV1(@PathVariable("postId") Long data) {
		// logic
		String result = "PathvariableV1 결과입니다 : " + data;
		return result;
	}
}

 

Long data에 해당하는 변수명이 PathVariable 이름과 동일하다면 어노테이션의 속성값을 안써줘도 된다.

@RequestMapping("/posts")
@RestController
public class PathVariableController {
	
	// 변수명과 같다면 속성값 생략가능
	@GetMapping("/{postId}")
	public String pathVariableV2(@PathVariable Long postId) {
		// logic
		String result = "PathvariableV2 결과입니다 : " + postId;
		return result;
	}
	
}

 

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

 

 

[특정 파라미터 매핑]

쿼리파라미터를 이용해서 컨트롤러의 특정 메서드와 매핑할 수 있다.

@RestController
public class ParameterController {

    // parms 속성값 추가
    @GetMapping(value = "/users", params = "gender=man")
    public String params() {
        // logic
        String result = "params API가 호출 되었습니다.";
        return result;
    }

}

 

localhost:8080/users?gender=man 과 같이 요청해야 해당 메서드와 매핑된다. 

?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"}
    • 배열로 속성 값을 여러 개 설정이 가능하다.

[특정 Header 매핑]

@RestController
public class ParameterController {
	
	// headers 속성값 추가
  @PostMapping(value = "/users", headers = "Content-Type=application/json")
  public String headers() {
      // logic
      String result = "headers API가 호출 되었습니다.";
      return result;
  }
	
}

이렇게 하면 요청의 body에 json 데이터를 답아온 경우만 매핑된다.

 

[MediaType 매핑, consume(수용)]

@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 형태로 사용한다.

 

속성 작성 방법

  1. consumes=”application/json”
    • application/json 미디어 타입 허용
  2. consumes=”!application/json”
    • application/json 제외 미디어 타입 허용
  3. consumes=”application/*”
    • application/ 으로 시작하는 모든 미디어 타입 허용
  4. consumes=”*\\/*”
    • 모두 허용

 

[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 허용

 

위에 나온 모든 MediaType은 Spring이 제공하는 Enum을 사용하면 된다.

ex) produces = “application.json"→ produces = MediaType.APPLICATION_JSON_VALUE

 

[HTTP 헤더 조회]

  • Spring에서 요청 Header에 쉽게 접근할 수 있다.
    • HttpServletRequest와 같이 파라미터로 다룰 수 있다.
// 로깅
@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";
    }
}

 

request

  • HttpServletRequest 객체 주소 값

response

  • HttpServletRequest 객체 주소 값

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]
}

host

  • host 정보

 cookie

  • Header의 Cookie 값

httpMethod

  • 호출에 사용한 HttpMethod

Locale

  • 위치 정보를 나타내는 헤더
  • 우선순위가 존재한다.

[MultiValueMap]

Map과 유사하게 Key, Value 형식으로 구현되어 있지만 하나의 Key가 여러 Value를 가질 수 있다 HTTP Header, Reqeust Parameter와 같이 하나의 Key에 여러 값을 받을 때 사용한다.

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");

 

[Client에서 Server로 Data를 전달하는 방법]

 

1. GET + Query Parameter(=Query String)

URL의 쿼리 파라미터를 사용하여 데이터 전달하는 방법이다. GET메서드는 바디에 데이터를 담지않는다. 그래서 url 쿼리파라미터에 담는다.

http://localhost:8080/request-params?key1=value1&key2=value2

 

HttpServletRequest 사용하는 경우

@Slf4j
@Controller
public class RequestParamController {

    @GetMapping("/request-params")
    public void params(HttpServletRequest request, HttpServletResponse response) throws IOException {
															 
        String key1Value = request.getParameter("key1");
        String key2Value = request.getParameter("key2");
		
        log.info("key1Value={}, key2Value={}", key1Value, key2Value);
        response.getWriter().write("success");
    }
}

 

response.getWriter().write()

  • HttpServletResponse를 사용해서 응답값을 직접 다룰 수 있다.
  • @Controller 지만 @ResponseBody를 함께 사용한 것과 같다.

2. POST + HTML Form(x-www-form-urlencoded)

html 폼을 통해 받은 데이터를 바디에 담아서 보내는 방식이다. 그 대신 바디에 쿼리파라미터 형식으로 작성한다.

POST /form-data
content-type: application/x-www-form-urlencoded

key1=value1&key2=value2

 

HttpServletRequest 사용하는 경우

@Slf4j
@Controller
public class RequestBodyController {

    @PostMapping("/form-data")
    public void requestBody(HttpServletRequest request, HttpServletResponse response) throws IOException {
											 
        String key1Value = request.getParameter("key1");
        String key2Value = request.getParameter("key2");
        
        log.info("key1Value={}, key2Value={}", key1Value, key2Value);
        response.getWriter().write("success");
    }
}

 

HttpServletRequest.getParameter(”key”);를 사용하면 Query Parameter, HTML Form Data 두가지 경우 모두 데이터 형식(key=value)이 같기 때문에 해당값에 접근할 수 있다.

 

3. HTTP Request Body

데이터(JSON, TEXT, XML 등)를 직접 HTTP Message Body에 담아서 전달한다.

주로 @RestController에서 사용하며, 대부분 JSON 형식으로 데이터를 전달한다.

  • POST, PUT, PATCH Method에서 사용한다.
  • GET, DELETE Method는 Body에 데이터를 담는것을 권장하지 않는다.

HttpServletRequest 사용하는 경우

@Getter
@Setter
public class Board {

	private String title;
	private String content;

}

 

package com.example.springbasicannotation.controller;

import com.example.springbasicannotation.entity.Board;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.PostMapping;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Slf4j
@Controller
public class RequestBodyController {

    // JSON을 객체로 변환해주는 Jackson 라이브러리
    private ObjectMapper objectMapper = new ObjectMapper();

    @PostMapping("/request-body")
    public void requestBody(
            HttpServletRequest request,
            HttpServletResponse response
    ) throws IOException {

        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

        log.info("messageBody={}", messageBody);

        Board board = objectMapper.readValue(messageBody, Board.clss);

        log.info("board.getTitle()={}, board.getContent()={}", board.getTitle(), board.getContent());

        response.getWriter().write("success");

    }
    
}

 

JSON을 Java 객체로 변환하려면 Jackson과 같은 라이브러리를 사용해야 한다. Spring Boot는 기본적으로 Jackson 라이브러리의 ObjectMapper를 제공하며, starter-web에 포함되어 있다.

 

[어떤 문제가 있었는지 + 어떻게 해결하였는지]

클라이언트에서 서버로 데이터를 보내는 방식에서 처음에 이해하기 전까지는 POST방식에서 바디에 데이터를 담아서 보내는 건데 왜 파라미터 형식으로 보내는 거지? 파라미터에 담아서 보내는 건가? 하고 헷갈렸었다.

 

그래서 지피티로 다음과 같이 다시한번 정리해봤다.

 

이 부분을 쉽게 풀어서 설명해줄게. 핵심은 HTML Form을 통해 데이터를 서버로 보내는 방식과, 이를 서버에서 처리하는 방법이야.


1. HTML Form + POST 방식이란?

HTML Form은 사용자로부터 데이터를 입력받고, 이를 서버로 전송하는 도구야.
POST 방식은 데이터를 숨겨서(HTTP Request Body에 넣어서) 서버에 보낸다고 생각하면 돼.

예시 상황:

  • 너는 친구에게 이메일을 보낼 거야.
  • 이메일 제목(key1)과 내용을(key2) 작성해서 "보내기" 버튼을 누르면, 서버로 데이터가 전송돼.

2. 데이터가 서버에 어떻게 전송되나요?

서버로 데이터를 보낼 때 Content-Type이 중요해.
application/x-www-form-urlencoded은 데이터를 쿼리 파라미터 형태로 만들어서 전송해.

전송된 데이터의 모습:

key1=value1&key2=value2

이 데이터는 이름=값의 형태로, 여러 쌍이 &로 이어진 거야.

  • 예:
    • key1=Hello
    • key2=World
    • 전체: key1=Hello&key2=World

3. 서버에서 이 데이터를 처리하는 방법은?

서버에서는 이 데이터를 받으면, 개발자가 파싱해서 각 값을 꺼내와야 해.
Java에서는 HttpServletRequest를 사용해서 getParameter() 메서드로 값을 읽어올 수 있어.

요청 처리 예제:

@PostMapping("/form-data")
public void requestBody(HttpServletRequest request, HttpServletResponse response) throws IOException {
    String key1Value = request.getParameter("key1"); // key1에 담긴 값 꺼내기
    String key2Value = request.getParameter("key2"); // key2에 담긴 값 꺼내기

    log.info("key1Value={}, key2Value={}", key1Value, key2Value); // 로그로 출력
    response.getWriter().write("success"); // 성공 메시지 반환
}

4. 이 코드가 어떻게 동작하나요?

  1. 클라이언트(사용자)가 /form-data 경로로 POST 요청을 보낸다.
    • 데이터 예시:
       
      key1=Hello&key2=World
  2. 서버(WAS)가 데이터를 받는다.
    • 데이터는 HttpServletRequest 객체에 담겨 있음.
    • request.getParameter("key1") → "Hello"
    • request.getParameter("key2") → "World"
  3. 서버가 데이터를 처리한다.
    • log.info로 값을 출력.
    • 사용자에게 success 메시지를 반환.

5. 왜 이런 방식을 사용하는 걸까?

  • 폼 데이터 처리에 최적화: 이 방식은 HTML Form과 잘 맞아서, 간단한 데이터를 처리하기 좋아.
  • 간결한 형식: 데이터가 작고 간단하면 효율적이야.

6. 비유로 이해하기

  • POST 요청은 편지봉투에 데이터를 담아서 보내는 것과 같아.
    • 봉투 안에는 key1=value1&key2=value2라는 내용물이 들어 있어.
  • 서버는 이 봉투를 열어서 key1과 key2에 적힌 값을 읽어오는 거야.
    • key1 → "Hello"
    • key2 → "World"

 

[오늘 회고]

적어도 과제를 하려면 CRUD 강의까지는 듣고 과제를 시작해야할 것 같다. 그래도 그 전에 설계는 할 수 있을 것 같으니 내일 API 명세서와 ERD를 먼저 작성해보아야겠다. 그리고 이번 주 주말이 끝나기 전까지 강의 완강하기!

5주차까지 다 듣고는 필수기능 구현시작해야겠다.