2025. 1. 24. 22:37ㆍ개발 회고/TIL
😊오늘 배운 내용
오늘은 스프링의 어노테이션과 클라이언트가 서버에게 데이터를 보내는 3가지 방식에 대해 배웠다.
[@Slf4j]
로그를 출력하는데 사용하는 어노테이션이다.
스프링의 여러개 어노테이션들은 인터페이스로 이루어져있다.
slf4j 어노테이션은 위와 같이 생겼다.
slf4j 어노테이션 위에도 여러개의 어노테이션이 붙어있다.
Target은 정말 말 그대로 이 어노테이션이 어떠한 것을 타겟으로 하는지에 대해 enum타입으로 범위를 지정한다. Retention은 보유하다 라는 뜻인데 해당 어노테이션이 어느 시점까지 유지되는지 나타내는 어노테이션이다.
Retention의 3가지 종류
@Retention은 다음 세 가지 값을 가질 수 있어:
- SOURCE
- 어노테이션이 컴파일 시점에만 유지되고, 컴파일된 .class 파일에는 포함되지 않음.
- 예: @Override, @SuppressWarnings
- 주로 코드 가독성이나 IDE 도움을 위해 사용.
- CLASS
- 어노테이션이 컴파일 후 .class 파일에 포함되지만, JVM이 실행 시점에 읽을 수는 없음.
- 기본값(디폴트)으로, 특정한 설정이 없으면 이 값으로 설정됨.
- 예: Lombok의 어노테이션
- 런타임에 필요 없고, 컴파일 과정에서만 의미 있는 경우 사용.
- RUNTIME
- 어노테이션이 런타임까지 유지되고, JVM에서도 참조 가능.
- 리플렉션을 통해 어노테이션 정보를 읽어야 할 때 사용.
- 예: 스프링의 @Controller, @Service 등 런타임에 처리되는 어노테이션.
@Target의 주요 역할
어노테이션을 클래스, 메서드, 필드, 매개변수 등 특정 위치에만 사용할 수 있도록 제한함.
이렇게 하면 어노테이션이 의도치 않은 곳에 잘못 사용되는 것을 방지할 수 있어.
@Target의 주요 ElementType 종류
@Target은 **ElementType**의 배열을 인수로 받아, 적용 대상을 지정해.
다음은 자주 사용되는 ElementType 값들이야:
- TYPE
- 클래스, 인터페이스, 열거형, 애노테이션에 적용 가능.
- 예: @Component, @Service
- FIELD
- 클래스의 멤버 변수(필드)에 적용.
- 예: @Autowired
- METHOD
- 메서드에 적용.
- 예: @GetMapping, @PostMapping
- PARAMETER
- 메서드 매개변수에 적용.
- 예: @RequestParam
- CONSTRUCTOR
- 생성자에 적용.
- 예: 특정 생성자 로직에 어노테이션을 붙이고 싶을 때.
- LOCAL_VARIABLE
- 로컬 변수에 적용. (잘 사용되지 않음.)
- ANNOTATION_TYPE
- 다른 어노테이션에 적용 가능하도록 설정.
- 메타 어노테이션을 정의할 때 사용.
- PACKAGE
- 패키지 선언에 적용.
- 예: 패키지 정보에 특정 메타데이터를 붙이고 싶을 때.
- 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 하기 위해 사용한다.
- Spring Boot 3.0 버전 이하
- URL path /example, /example**/** 모두 허용(Mapping)한다.
- Spring Boot 3.0 버전 이상(현재 버전)
- URL path /example 만 허용(Mapping)한다.
- 속성값들을 설정할 때 배열 형태로 다중 설정이 가능하다
ex) @RequestMapping**({**”/example”, “/example2”, “/example3”**})**
- HTTP Method POST, GET, PUT, PATCH, DELETE, HEAD 모두 허용한다
- 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을 명시해주기 귀찮으니 이렇게 사용한다.
- Target(ElementType.METHOD) Method Level에 해당 어노테이션을 적용한다 라는 의미
- 내부적으로 @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로 전달된 값을 파라미터로 받아오는 역할을 수행한다.
- 경로 변수를 중괄호에 둘러싸인 값으로 사용할 수 있다.
ex) user/{id}
- 기본적으로 @PathVariable로 설정된 경로 변수는 반드시 값을 가져야 하며 값이 없으면 응답 상태코드 404 Not Found Error가 발생한다.
- 최근 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가 날것이다.
속성 작성 규칙
- params = "gender"
- params의 key값은 커스텀이 가능하다
- value는 없어도 된다.
- params = "!gender"
- gender가 없어야 한다.
- params = "gender=man"
- gender=man 이어야 한다.
- params = "gender!=man"
- params의 value값이 man가 아니여야 한다.
- 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 형태로 사용한다.
속성 작성 방법
- consumes=”application/json”
- application/json 미디어 타입 허용
- consumes=”!application/json”
- application/json 제외 미디어 타입 허용
- consumes=”application/*”
- application/ 으로 시작하는 모든 미디어 타입 허용
- 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=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. 이 코드가 어떻게 동작하나요?
- 클라이언트(사용자)가 /form-data 경로로 POST 요청을 보낸다.
- 데이터 예시:
key1=Hello&key2=World
- 데이터 예시:
- 서버(WAS)가 데이터를 받는다.
- 데이터는 HttpServletRequest 객체에 담겨 있음.
- request.getParameter("key1") → "Hello"
- request.getParameter("key2") → "World"
- 서버가 데이터를 처리한다.
- log.info로 값을 출력.
- 사용자에게 success 메시지를 반환.
5. 왜 이런 방식을 사용하는 걸까?
- 폼 데이터 처리에 최적화: 이 방식은 HTML Form과 잘 맞아서, 간단한 데이터를 처리하기 좋아.
- 간결한 형식: 데이터가 작고 간단하면 효율적이야.
6. 비유로 이해하기
- POST 요청은 편지봉투에 데이터를 담아서 보내는 것과 같아.
- 봉투 안에는 key1=value1&key2=value2라는 내용물이 들어 있어.
- 서버는 이 봉투를 열어서 key1과 key2에 적힌 값을 읽어오는 거야.
- key1 → "Hello"
- key2 → "World"
[오늘 회고]
적어도 과제를 하려면 CRUD 강의까지는 듣고 과제를 시작해야할 것 같다. 그래도 그 전에 설계는 할 수 있을 것 같으니 내일 API 명세서와 ERD를 먼저 작성해보아야겠다. 그리고 이번 주 주말이 끝나기 전까지 강의 완강하기!
5주차까지 다 듣고는 필수기능 구현시작해야겠다.
'개발 회고 > TIL' 카테고리의 다른 글
[2/5] TIL - 쿠키, 세션, JWT (0) | 2025.02.05 |
---|---|
[2/4] TIL - Spring Bean, 의존성 주입, Validation (2) | 2025.02.04 |
[1/22] TIL - 포트, 쿠키, 세션 (0) | 2025.01.22 |
[1/17] TIL - 키오스크 프로젝트에 Enum, Stream, IntStream 적용해보기 (1) | 2025.01.17 |
[1/16] TIL - 키오스크 프로그램 구현 + 장바구니 기능, static은 왜 쓰면 안될까, 다른 달팽이들은 신경쓰지 말자 (0) | 2025.01.16 |