Spring/Spring Boot

[Spring Boot] REST API 사용, DTO

jinsang-2 2025. 4. 29. 23:23

SPRING REST

GET

GET으로 원하는 데이터 요청하는 방법 2가지

  • @RequestParam
  • @PathVariable
@GetMapping(path="/echo/{message}")
public String echo(
        @PathVariable(name = "message") String msg,
        @PathVariable int age, // integer, int 중 integer은 null 으로 초기화, int는 0으로 초기화 지금은 int가 맞음
        @PathVariable boolean isMan
){
    System.out.println("echo message:"+msg);
    return msg;
}

// <http://localhost:8080/api/book?category=IT&issuedYear=2023&issued-month=01&issued_day=31>
@GetMapping(path="/book")
public queryParam(
        @RequestParam String category,

POST

  • 데이터 생성 요청
  • HTTP BODY로 데이터를 요청 받아서 처리 @RequestBody
  • POST니까 @PostMapping("/post") 어노테이션으로 처리

Controller

public class UserApiController {
    @PostMapping("/user")
    public void user(
            @RequestBody UserRequest userRequest
    ){
        System.out.println(userRequest);
    }
}

DTO (UserRequest)

//Lombok
@Data  // 클래스가 가지고 있는 메서드 자동 생성
@AllArgsConstructor  // 전체 파라미터 자동 생성(기본 생성자가 없어짐)
@NoArgsConstructor // 기본생성자를 달아줌
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class UserRequest {
    private String name;
    private String phoneNumber;
    private String email;
    private Boolean isKorean ;
}

primitive(기본형) 타입, reference(참조형) 타입이 있는데 DTO 사용할 때는 null 값이 들어올 수도 있기 때문에 reference 타입, 객체 타입을 사용해야 한다. (int, boolean 같은 기본형 타입은 안됨)

JSON

  • KEY : VALUE 형식
  • String, Number, Boolean, Object {}, Array [] 모두 가능

isKorean"으로 JSON 요청을 보내도 setter에서 Korean만 남는 문제

🤔질문 : "Boolean 타입 필드를 선언하면, getter를 먼저 만들고, getter를 참고해서 setter를 만들기 때문에 setter 이름에서 is를 떼는거냐? 그리고 이건 Boolean 타입일 때만 이런가?"

✅답 : 아님. getter를 참고해서 setter를 만드는 순서가 아니고, Java Bean 명세(스펙)에 따라 각각 독립적으로 규칙을 적용하는 거. 즉, Boolean 타입이면 "getter"는 isXxx(), "setter"는 setXxx() 이 패턴을 따르는 거. 순서로 따지는 게 아니라 타입이 Boolean이면 이렇게 규칙적으로 만드는 거

핵심 원인

Java Bean 규약(JavaBean Specification) 때문.

  • Java에서는 "Boolean 타입의 필드"에 대해 getter, setter 메서드 이름을 만들 때 특별 규칙이 있음.
  • Boolean 타입 필드는 보통 is로 시작하는 getter를 자동으로 기대 (isKorean()).
  • 그런데 setter를 만들 때는 set + 필드 이름의 첫 글자를 대문자로 만들어야 함.
  • 그런데 이때 is를 빼버리고 뒤에 있는 이름만 붙여서 처리하는 문제가 발생.

즉,

private Boolean isKorean;

이면 Java Bean 규약에 따라:

getter isKorean()
setter setKorean(Boolean korean)

setIsKorean(Boolean isKorean)이 아니라 setKorean(Boolean korean)이 만들어진다!!


다시 요약하면

  • Boolean isKorean 필드를 선언하면,
  • setter는 setKorean(Boolean korean) 형태로 만들어진다.
  • is가 제거되고 나머지(Korean)만 setter 이름으로 사용된다.

그래서 "isKorean"이라는 JSON 필드가 들어오면, 매핑할 때 Korean 필드(setter 기준)를 찾으려다가 꼬임.


정리

상황 설명

필드명 isKorean boolean 타입 필드
getter 이름 isKorean()
setter 이름 setKorean(Boolean korean)
문제 JSON에서 "isKorean"을 그대로 쓰면 매칭이 꼬인다 (setter를 못 찾을 수 있다).

해결 방법은?

1. 필드명을 korean으로 아예 바꾸기

private Boolean korean;

이렇게 하면 getter는 isKorean(), setter는 setKorean(Boolean korean) 정상 생성된다. 깔끔해.

2. @JsonProperty로 명시적으로 매핑하기

@JsonProperty("is_korean")
private Boolean isKorean;

이렇게 하면 JSON 쪽 이름과 Java 쪽 이름을 매칭 강제할 수 있어.


예시 코드를 보여줄게

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRequest {
    private String name;
    private String phoneNumber;
    private String email;

    @JsonProperty("is_korean")
    private Boolean isKorean;
}

요렇게 하면, 요청 JSON이

json
코드 복사
{
  "name": "spring boot",
  "phone_number": "111",
  "email": "JAVA",
  "is_korean": true}

일 때, isKorean 필드에 잘 매핑돼.


결론

  • Java Bean 규약 때문에 Boolean 필드는 is를 빼고 setter 이름이 만들어진다.
  • 이로 인해 JSON 필드명과 매칭이 어긋날 수 있다.
  • 해결하려면 @JsonProperty를 붙이거나 필드명을 명확히 조정해야 한다.

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 필요한 이유

  • POST 요청 Body는 JSON으로 들어오고,
  • UserRequest 클래스는 Java 객체로 매핑(deserialization)되어야 함.
  • JSON 키는 snake_case(is_korean)인데, Java 필드는 camelCase(isKorean)로 되어 있음.

=> 문제: Jackson(자바 JSON 라이브러리)이 기본적으로 camelCase 기준으로 JSON을 해석한다.

따라서 is_korean을 제대로 읽지 못하고 isKorean에 값을 매핑하지 못하는 문제가 생긴다.


그럼 왜 @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)가 필요한가?

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)를 붙이면,

Jackson이 "snake_case JSON ↔ camelCase Java" 변환 규칙을 자동으로 적용하도록 설정하는 거야.

  • 즉, is_korean JSON 필드isKorean Java 필드로 자동 매칭해준다.
  • 변환 규칙이 없으면 Jackson은 "is_korean이라는 이름" 을 그대로 찾으려 하고, 못 찾으니까 매핑 실패하는 것.

요약

JSON Java 매핑 여부 (기본) 매핑 여부 (@JsonNaming 적용 시)

is_korean isKorean ❌ (매칭 실패) ✅ (매칭 성공)
  • 기본: JSON의 snake_case 키와 Java의 camelCase 필드가 다르기 때문에 매칭 실패
  • @JsonNaming 적용 시: snake_case ↔ camelCase 변환이 적용되어 매칭 성공

추가 참고

  • Spring Boot는 내부적으로 Jackson을 사용해서 JSON을 파싱한다.
  • Jackson의 기본 작동은 "필드 이름을 정확히 맞춰야 한다"는 것이고, naming strategy를 주지 않으면 변환을 안 해줘.
  • 글로벌 설정으로 application.yml에도 snake_case 전략을 적용할 수 있지만, 클래스별로 다르게 하고 싶으면 @JsonNaming을 쓰는 게 맞다.

PUT

  • 데이터가 없으면 추가, 있다면 수정
  • @PutMapping 사용
  • put도 post와 마찬가지로 http body로 정보를 받는다.
@Slf4j // log 사용하기 위한 어노테이션
@RestController
@RequestMapping("/api")
public class PutApiController {

    @PutMapping("/put")
    public void put(
        @RequestBody
        UserRequest userRequest
    ){
        System.out.println();
        log.info("Request :  {}", userRequest);
    }
}

System.out.println(); 과 log 의 차이

  • sout 은 바로바로 출력하기에 느리고, 출력이 끝날 때까지 프로그램 흐름이 잠깐 멈춤(블로킹)
  • log는 버퍼를 가지고 있어서 버퍼에 저장한 후, 버퍼가 쌓이면 출력 가능해서, 고급 설정하면 비동기 방식(논블락킹)으로 실행될 수 있음, (log도 기본은 동기 처리임), 확장성, 관리성이 좋음

Delete

  • GET 요청 방식과 같다고 생각하자.
//http://localhost:8080/api/user/abcd/delete
    @DeleteMapping(path = {
            "/user/{userName}/delete",
            "/user/{userName}/del"
    }) //path 쓰면 두개 이상 가능
    public void delete(@PathVariable String userName){

        log.info("user-name:{}",userName);
        }
    }