1. 시작하며: GET 요청의 한계와 JSON 바디의 필요성
지금까지 우리는 GET 방식을 통해 URL로 들어오는 요청을 프론트 컨트롤러로 동적 라우팅하는 데 성공했습니다. 하지만 실제 웹 서비스에서는 회원가입이나 로그인처럼 보안이 중요하고 데이터 양이 많은 경우 무조건 POST 방식을 사용해야 합니다.
POST 방식은 데이터를 URL이 아닌 HTTP 메시지의 Body(본문) 안에 안전하게 숨겨서 보냅니다. 최근에는 이 본문 데이터로 JSON(JavaScript Object Notation) 포맷을 가장 많이 사용합니다. 이번 시간에는 클라이언트가 보낸 JSON 문자열을 자바 객체(DTO)로 변환해 주는 마법의 도구, Jackson 라이브러리를 도입해 보겠습니다.
2. Jackson 라이브러리와 역직렬화(Deserialization)의 이해
코드를 짜기 전에, Jackson이 도대체 무슨 일을 하는지 핵심 개념 하나만 짚고 넘어가겠습니다. 바로 '역직렬화(Deserialization)'입니다.
- 직렬화(Serialization): 자바 객체(DTO)를 네트워크로 전송하기 위해 JSON 문자열로 바꾸는 과정
- 역직렬화(Deserialization): 네트워크를 통해 들어온 JSON 문자열을 다시 자바 객체(DTO)로 조립하는 과정
클라이언트가 POST 요청 바디에 {"email": "test@gmail.com"} 이라는 텍스트를 담아 보내면, Jackson 라이브러리는 이를 읽고 UserDTO라는 자바 객체로 역직렬화해 줍니다.
Jackson 라이브러리가 역직렬화를 할 때 사용하는 핵심 기술이 바로 우리가 6편에서 배웠던 리플렉션입니다. 구체적인 클래스 타입을 알지 못하더라도 리플렉션을 통해 동적으로 접근하여 기본 생성자로 빈 깡통 객체를 먼저 생성한 후, Setter 등을 통해 값을 주입하게 됩니다. (이 사실을 꼭 기억해 주세요! 뒤에서 엄청난 에러와 마주하게 됩니다.)
2-1. 라이브러리 세팅 및 타겟 컨트롤러 준비
이론을 알아봤으니 본격적으로 build.gradle에 Jackson의 핵심 모듈을 추가해 줍니다.
// JSON 파싱을 위한 Jackson 코어 라이브러리 (자바 프로젝트용)
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2'
그리고 회원가입 로직을 처리할 UserController에 UserDTO를 파라미터로 받는 join 메서드를 추가합니다.
package org.example.controller;
import org.example.dto.UserDTO;
public class UserController {
// POST 요청을 받아 처리할 메서드
public String join(UserDTO dto) {
System.out.println("--- 회원가입 로직 실행 ---");
System.out.println("이메일: " + dto.getEmail());
System.out.println("이름: " + dto.getName());
// 처리 결과를 JSON 형태의 문자열로 직접 조립하여 반환
return "{ \"message\": \"" + dto.getName() + "님 환영합니다!\" }";
}
}
3. DispatcherServlet 고도화: POST 분기 처리
이제 프론트 컨트롤러가 POST 요청을 구별하여 Jackson에게 역직렬화를 지시하도록 로직을 수정합니다. 이 부분이 바로 스프링의 @PostMapping과 @RequestBody가 탄생한 배경이기도 합니다.
// DispatcherServlet.java 내부
String httpMethod = request.getMethod(); // "GET" or "POST"
String body = request.getBody(); // JSON 텍스트 (POST일 때만 존재)
String resultBody = "";
if (httpMethod.equals("POST")) {
// 👉 💡 핵심: Jackson 라이브러리의 ObjectMapper 출동!
ObjectMapper objectMapper = new ObjectMapper();
// 👉 💡 핵심: JSON 문자열(body)을 UserDTO 자바 객체로 역직렬화 (데이터 바인딩)
UserDTO userDTO = objectMapper.readValue(body, UserDTO.class);
// 리플렉션으로 메서드를 실행할 때, 변환된 DTO를 파라미터로 넘겨줌!
Method method = clazz.getMethod(methodName, UserDTO.class);
resultBody = (String) method.invoke(controllerInstance, userDTO);
} else if (httpMethod.equals("GET")) {
// 파라미터가 없는 기존 GET 방식 로직
Method method = clazz.getMethod(methodName);
resultBody = (String) method.invoke(controllerInstance);
}
response.setBody(resultBody);
4. 🚨 트러블슈팅: Jackson과 기본 생성자 에러의 비밀
자, 이제 포스트맨(Postman)이나 개발자 도구를 이용해 POST 요청을 날려보겠습니다. 성공할 줄 알았는데 콘솔에 어마어마한 에러가 쏟아졌습니다.
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.example.dto.UserDTO` (no Creators, like default constructor, exist)에러 메시지를 직역하면 "UserDTO 객체를 만들 수 없어! 기본 생성자(default constructor)가 없잖아!"라는 뜻입니다.
🤔 왜 이런 에러가 났을까?
앞서 2번 목차에서 Jackson은 리플렉션을 통해 객체를 생성한다고 설명했습니다. 리플렉션은 newInstance()를 호출하여 일단 파라미터가 없는 깡통 객체를 만들어야 하는데, 제 UserDTO에는 파라미터가 있는 생성자만 존재하여 자바가 기본 생성자를 자동으로 만들어주지 않았습니다. 그 결과 Jackson이 깡통 객체를 만들지 못해 폭발해 버린 것입니다.
🛠️ 해결 방법
원인을 알았으니 해결은 아주 간단합니다. UserDTO에 빈 껍데기 역할을 할 기본 생성자를 명시적으로 추가해 줍니다. (스프링에서 DTO나 Entity를 만들 때 @NoArgsConstructor가 필수인 이유가 바로 이 잭슨의 역직렬화 원리 때문입니다!)
public class UserDTO {
String email;
String name;
String password;
// 👉 💡 핵심: Jackson의 리플렉션을 위한 기본 생성자 필수 추가!
public UserDTO() {}
// ... 파라미터 생성자, Getter, Setter 생략 ...
}
5. 최종 테스트: 완벽하게 동작하는 나의 미니 스프링
버그를 수정하고 다시 서버를 띄운 뒤, 클라이언트(브라우저 콘솔)에서 fetch API를 통해 JSON 데이터를 담아 POST 요청을 날려보았습니다.
[서버 인텔리제이 콘솔 출력]
/0:0:0:0:0:0:0:1와 연결 성공!
Start Line: POST /user/join HTTP/1.1
--- 회원가입 로직 실행 ---
이메일: test@gmail.com
이름: 스프링장인
비밀번호: 1234
[클라이언트 브라우저 응답 확인]
{ "message": "스프링장인님 환영합니다!" }
서버 내부에서 클라이언트가 보낸 JSON이 DTO로 완벽히 변환되어 비즈니스 로직을 타고, 컨트롤러가 뱉어낸 응답이 다시 클라이언트에게 예쁘게 도착했습니다!
6. 대장정을 마무리하며
이로써 '순수 Java로 WAS 프레임워크 만들기'라는 긴 여정이 막을 내렸습니다. 톰캣이 열어주는 소켓 연결부터, HTTP 메시지 파싱, 프론트 컨트롤러의 동적 라우팅, 그리고 Jackson을 이용한 데이터 바인딩까지 모두 직접 구현해 보았습니다.
이 프로젝트를 통해 @RestController, @PostMapping, @RequestBody 같은 스프링의 마법 같은 어노테이션들이 내부적으로 어떻게 동작하는지 그 원리를 바닥부터 뜯어볼 수 있었습니다. 이제 스프링 프레임워크를 사용할 때, 단순히 사용법을 암기하는 것이 아니라 그 이면에 숨겨진 '이유'를 알게 된 뜻깊은 시간이었습니다.
'정리 > WAS' 카테고리의 다른 글
| 순수 Java로 WAS 구현 (8) - 프론트 컨트롤러(DispatcherServlet) 직접 구현과 HTTP 응답 조립 (0) | 2026.05.16 |
|---|---|
| 순수 Java로 WAS 구현 (7) - 톰캣의 정체와 프론트 컨트롤러(Front Controller)의 탄생 (0) | 2026.05.15 |
| 순수 Java로 WAS 구현 (6) - 프레임워크의 마법, 리플렉션(Reflection) 완벽 이해 (0) | 2026.05.15 |
| 순수 Java로 WAS 구현 (5) - HTTP 요청 파서(Parser) 구현과 3가지 트러블슈팅 (0) | 2026.05.15 |
| 순수 Java로 WAS 구현 (4) - 파싱(Parsing)을 위한 HTTP 메시지 구조 완벽 분석 (1) | 2026.05.14 |