본문으로 건너뛰기

5.3 JSON 데이터 처리와 DTO — @RequestBody, Jackson 직렬화

REST API의 세계에서 클라이언트(브라우저, 프론트엔드 앱)와 서버가 대화를 나눌 때 사용하는 공용어는 바로 JSON(JavaScript Object Notation) 입니다. 스프링 부트에서 이 JSON 데이터를 어떻게 주고받는지, 그리고 왜 DTO라는 특별한 바구니 객체를 사용하는지 알아봅니다.

1. JSON 변환의 마법 (Jackson)​

과거 스프링에서는 자바 객체를 JSON으로 변환하거나 그 반대로 파싱하기 위해 복잡한 설정과 코드가 필요했습니다. 하지만 스프링 부트의 spring-boot-starter-web 안에는 Jackson 이라는 강력한 직렬화/역직렬화 라이브러리가 기본 내장되어 있어 이 과정을 100% 자동화 합니다.

public class User {
private String name;
private int age;
// getter, setter 생략
}

@RestController // 이 애너테이션이 붙은 클래스의 반환값은 자동으로 JSON 형식으로 응답 객체에 담깁니다.
public class UserController {

@GetMapping("/api/me")
public User getMe() {
User user = new User("홍길동", 20);
return user; // 자바 객체를 리턴했지만, 브라우저에는 자동으로 {"name":"홍길동", "age":20} 형태의 JSON으로 날아갑니다.
}
}

2. @RequestBody와 역직렬화​

데이터를 보낼 때는 자동으로 JSON이 되는데, 반대로 클라이언트가 등록(POST)을 위해 전송한 거대한 JSON 덩어리는 어떻게 자바에서 받아서 사용할까요?

이때 @RequestBody 애너테이션을 활용합니다. HTTP 요청(Request)의 본문(Body)에 담겨 구형으로 전송된 JSON 형태의 문자열을 찾아, 자바 객체 구조로 정확하게 매핑(역직렬화) 해 줍니다.

@PostMapping("/api/users")
public String createUser(@RequestBody User targetUser) {
// 프론트엔드에서 보낸 JSON 문자가 깔끔한 자바 User 객체(targetUser)로 변신해서 주입됩니다.
// JSON 키 값 이름과 User 클래스의 멤버 변수 이름이 동일무이하게 일치해야 합니다.
System.out.println("가입자 이름: " + targetUser.getName());
return "가입 성공";
}

3. 왜 DTO(Data Transfer Object)를 분리해서 사용하는가?​

위의 예시처럼 데이터베이스 테이블과 직결되는 아주 중요한 엔티티(Entity) 클래스를 컨트롤러 응답 직접용이나 단순 파라미터 수신용으로 섞어 써서는 절대 안 된다는 강력한 실무 원칙 이 존재합니다.

  1. 비밀번호 노출 등 보안 위험: User 엔티티에 password 필드가 있을 때, 그대로 전체 객체를 반환해버리면 고객의 비밀번호가 화면 밖으로 새어 나갑니다.
  2. 화면 종속성 문제: 회원 가입 시 수신해야 할 데이터 포맷(이름, 이메일, 패스워드 2번 확인)과 회원의 내 정보를 화면에 뿌려줄 때 필요한 출력 포맷(ID, 이름, 남은 포인트)은 모양이 전혀 다릅니다. 이 모든 변화무쌍한 파생 속성을 핵심 엔티티 클래스 1개만으로 제어하려 들면 코드가 매우 지저분해지고 예측이 어려워집니다.

DTO(데이터 전송 바구니)의 필수적인 도입​

따라서, 응답 전용 바구니 혹은 수신 전용 바구니 로만 쓰일 아주 가벼운 껍데기 객체를 따로 만들어 사용합니다. 이를 DTO라고 부릅니다.

// 수신 전용 바구니 (비밀번호 확인용 등 DB에는 안 들어가는 1회용 데이터도 담음)
public class UserCreateRequestDto {
private String name;
private String email;
private String password;
private String passwordConfirm;
// ...
}

// 응답 전용 바구니 (안전을 위해 패스워드는 제외하고 프론트에 넘겨줄 정보만 안전히 매핑)
public class UserResponseDto {
private Long id;
private String name;
private int totalPoints;
// ...
}
// DTO가 적용된 개선된 컨트롤러 코드
@PostMapping("/api/users")
public UserResponseDto createUser(@RequestBody UserCreateRequestDto request) {
// 1. 요청 데이터를 (request DTO 바구니에 담아) 안전히 받는다.
// 2. 내부적으로 DB 저장을 위한 거대한 진짜 User DB 엔티티로 변환하여 로직을 처리한다. (Service의 영역)

// 3. 다시 응답용 모양(UserResponseDto)의 껍데기에 예쁘게 포장해서 컨트롤러를 통해 내보낸다.
return new UserResponseDto(1L, request.getName(), 1000);
}

실무에서는 거의 100% 요청/응답 컨트롤러 매개변수에 이 DTO 를 별도로 생성하여 활용함으로써, 데이터베이스의 구조와 화면의 입출력 구조 결합도를 완벽하게 분리해냅니다.


4. HTTP Content-Type과 데이터 수신 방식​

클라이언트가 데이터를 보낼 때 헤더(Header)에 담는 Content-Type에 따라 스프링이 데이터를 파싱하는 방식이 달라집니다.

Content-Type데이터 형태스프링 수신 애너테이션주요 사용처
application/json{"name": "hong"}@RequestBody대부분의 REST API API 통신
application/x-www-form-urlencodedname=hong&age=20@ModelAttribute 또는 @RequestParam일반적인 HTML Form 전송
multipart/form-data바이너리 + 텍스트 데이터@RequestPart 또는 MultipartFile파일 업로드 포함 전송

예시: Form 방식 vs JSON 방식​

// 1. application/x-www-form-urlencoded (Form 전송)
@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password) { ... }

// 2. application/json (JSON 전송)
@PostMapping("/api/users")
public UserResponseDto createUser(@RequestBody UserCreateRequestDto request) { ... }