3.3 JSON 데이터 처리와 DTO (Data Transfer Object)
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) 클래스를 컨트롤러 응답 직접용이나 단순 파라미터 수신용으로 섞어 써서는 절대 안 된다는 강력한 실무 원칙 이 존재합니다.
- 비밀번호 노출 등 보안 위험: User 엔티티에
password필드가 있을 때, 그대로 전체 객체를 반환해버리면 고객의 비밀번호가 화면 밖으로 새어 나갑니다. - 화면 종속성 문제: 회원 가입 시 수신해야 할 데이터 포맷(이름, 이메일, 패스워드 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-urlencoded | name=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) { ... }