| 방향 | 설명 |
|---|---|
| 직렬화 | Java 객체 → JSON / XML (응답 시) |
| 역직렬화 | JSON / XML → Java 객체 (요청 수신 시) |
Spring Boot에서는 Jackson 라이브러리가 자동으로 직렬화/역직렬화를 처리한다.
| Java 타입 | JSON 직렬화 결과 |
|---|---|
| 단일 객체 (DTO/Entity) | { "field": "value", ... } |
List<T> (객체 배열) | [{ ... }, { ... }] |
Map<K, V> | { "key": "value", ... } (객체 형태) |

| 코드 범위 | 의미 | 예시 |
|---|---|---|
2xx | 성공 | 200 OK, 201 Created |
4xx | 클라이언트 오류 | 400 Bad Request, 404 Not Found |
5xx | 서버 오류 | 500 Internal Server Error |

| 구분 | HttpEntity<T> | ResponseEntity<T> |
|---|---|---|
| 상속 관계 | 부모 클래스 | HttpEntity를 상속 |
| HTTP 상태 코드 설정 | ❌ 불가 | ✅ 가능 |
| 헤더 설정 | ✅ 가능 | ✅ 가능 |
| 바디 설정 | ✅ 가능 | ✅ 가능 |
| 주 용도 | 요청/응답 공통 | 컨트롤러 응답 반환 |
실제로 컨트롤러에서 응답을 반환할 때는
ResponseEntity를 사용한다.
@GetMapping("/response-entity")
public ResponseEntity<Board> responseEntity() {
Board board = new Board();
board.setBno(1);
board.setBtitle("제목입니다.");
board.setBcontent("내용입니다.");
board.setBwriter("사용자");
board.setBhitcount(0);
board.setBdate(new Date());
// 커스텀 헤더 추가
HttpHeaders headers = new HttpHeaders();
headers.add("my-head", "my-value");
// 방법 1: 생성자 방식
return new ResponseEntity<>(board, headers, HttpStatus.OK);
// 방법 2: 메서드 체이닝 방식 (Builder 패턴)
// return ResponseEntity
// .status(HttpStatus.OK)
// .headers(headers)
// .body(board);
}
두 방법은 동일한 결과를 반환한다. 메서드 체이닝(Method Chaining) 방식이 가독성이 더 좋아 선호된다.
리턴 타입을 void로 선언하면 Spring이 응답을 자동으로 구성하지 않으므로, HttpServletResponse 객체를 직접 사용해 응답을 만들어야 한다.
HTTP 클라이언트가 웹 서버에 접근하면 서버는 두 객체를 자동으로 생성한다.
| 객체 | 역할 |
|---|---|
HttpServletRequest | 요청에 대한 모든 정보 포함 (URL, 헤더, 파라미터 등) |
HttpServletResponse | 응답을 직접 구성하는 역할 (상태코드, 헤더, 바디 설정) |
@GetMapping("/download")
public void download(HttpServletRequest request, HttpServletResponse response) throws Exception {
log.info("실행");
// 1) Content-Type: 응답 본문의 데이터 형식 지정
response.setContentType("image/jpeg");
// 2) Content-Disposition: 바로 렌더링하지 말고 파일로 다운로드하도록 설정
response.addHeader("Content-Disposition", "attachment; filename=\"photo1.jpg\"");
// 3) 응답 본문에 파일 데이터 출력
InputStream is = new FileInputStream("경로/photo1.jpg");
OutputStream os = response.getOutputStream();
byte[] buffer = new byte[1024];
while (true) {
int num = is.read(buffer);
if (num == -1) break; // 더 이상 읽을 데이터 없음
os.write(buffer, 0, num); // 읽은 만큼만 출력
}
os.flush();
is.close();
os.close();
}
HTTP 헤더(시작행 포함)는 ISO-8859-1 인코딩만 허용한다. 한글은 이 인코딩으로 표현할 수 없기 때문에, 한글 파일명을 그대로 넣으면 깨지거나 다운로드가 실패한다.
한글 파일명을 UTF-8로 바이트 변환 후, ISO-8859-1로 재인코딩해서 헤더에 넣는다.
브라우저가 이를 다시 한글로 복원해서 사용한다.
String fileName = "포토1.jpg";
// UTF-8 바이트 배열 → ISO-8859-1 문자열로 변환
fileName = new String(fileName.getBytes("UTF-8"), "ISO-8859-1");
response.addHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
참고: 현대 브라우저에서는
RFC 5987방식인filename*=UTF-8''인코딩된파일명형식도 지원한다. 더 표준적인 방법을 사용하려면UriUtils.encode(fileName, StandardCharsets.UTF_8)을 활용할 수 있다.
| 상황 | 권장 방법 |
|---|---|
| JSON 데이터 반환 (상태코드 기본 200) | @ResponseBody + 객체 반환 or @RestController |
| JSON 반환 + 상태코드/헤더 커스텀 | ResponseEntity<T> 반환 |
| 파일 다운로드 | void 반환 + HttpServletResponse 직접 사용 |
Java 객체는 Java 안에서만 존재할 수 있다. 네트워크로 보내려면 "누구나 읽을 수 있는 형태"로 변환해야 한다.
Java 객체는 JVM 메모리(힙) 위에 살아있다. Board 객체를 예로 들면 bno, btitle 같은 필드값뿐 아니라 객체의 메모리 주소(0x7f3a...)도 함께 존재하는데, 이 주소는 내 JVM 안에서만 유효한 값이다.
이걸 그대로 네트워크로 전송하면?
0x7f3a... 주소가 아무 의미 없다JSON은 그냥 텍스트다. 어떤 언어든 텍스트는 읽을 수 있다. Java 객체를 JSON으로 변환(직렬화)하면 {"bno": 1, "btitle": "제목"} 같은 텍스트가 되고, 이걸 Python이든 JavaScript든 iOS든 누구나 파싱해서 쓸 수 있다. 반대로 받은 JSON을 Java 객체로 되돌리는 것이 역직렬화다.
한국어로만 쓴 계약서를 프랑스 사람한테 보내면 못 읽는다. 영어(공통 언어)로 번역해서 보내야 양쪽이 이해할 수 있는 것처럼, Java 객체를 JSON이라는 공통 형식으로 변환해서 주고받는 것이 직렬화/역직렬화다.
JSON은 텍스트 기반 포맷이다. 메모장으로 열면 읽힌다.
메모장으로 읽으려면 모든 값이 문자로 표현 가능해야 한다.
| 타입 | 문자로 표현 가능? | JSON 지원 |
|---|---|---|
숫자 123 | ✅ | ✅ |
문자열 "hello" | ✅ | ✅ |
논리형 true | ✅ | ✅ |
이진 데이터 0xFF 0xD8 | ❌ | ❌ |
| Java 객체 참조 (메모리 주소) | ❌ | ❌ |
byte[])이 JSON이 안 되는 이유컴퓨터에서 문자도 사실 바이트다. 차이는 "이 바이트가 어떤 문자인지" 약속(인코딩)이 있냐 없냐다. 예를 들어 0x41은 ASCII 약속에 따라 'A'를 의미하지만, 0xFF는 유효한 UTF-8 문자가 아니라 어떤 문자로도 표현할 수 없다. 이미지나 파일 같은 이진 데이터에는 이런 약속 밖의 바이트가 섞여 있어서, 텍스트로 변환하면 깨지거나 의미가 손실된다.
Postman이 JSON을 전송하면 @RequestBody가 이를 UserMessageRequest 객체로 역직렬화한다. 컨트롤러에서 비즈니스 로직을 처리한 뒤 AiMessageResponse 객체를 만들어 반환하면, @RestController가 자동으로 JSON으로 직렬화해 응답한다.
{
"mid": "spring",
"question": "오늘 삼성전자의 주가를 알려줘"
}
@Data
public class UserMessageRequest {
private String mid; // 사용자 ID
private String question; // 질문 내용
}
Postman JSON이 역직렬화되어 이 객체에 담긴다. JSON의 "mid" 키는 mid 필드로, "question" 키는 question 필드로 자동 매핑된다.
@Data
public class AiMessageResponse {
private String question; // 받은 질문 그대로 반환
private String response; // AI 응답 내용
}
이 객체가 직렬화되어 JSON으로 응답된다.
@Data가 자동 생성하는 것들| 생성되는 것 | 역할 |
|---|---|
getter | Jackson이 직렬화할 때 값 읽기 |
setter | Jackson이 역직렬화할 때 값 넣기 |
| 기본 생성자 | Jackson이 객체 먼저 만들 때 사용 |
toString() | log.info()에서 객체 내용 출력 |
equals(), hashCode() | 객체 비교 |
@Data가 없으면 Jackson이 역직렬화를 못 해서 400 에러 발생
@PostMapping("/chat")
public AiMessageResponse ai(@RequestBody UserMessageRequest userMessageRequest) {
log.info(userMessageRequest.toString());
AiMessageResponse aiResponse = new AiMessageResponse();
aiResponse.setQuestion(userMessageRequest.getQuestion());
aiResponse.setResponse("삼성전자의 오늘 주가는 230000");
return aiResponse;
}
| 코드 | 역할 |
|---|---|
@PostMapping("/chat") | POST /chat 요청을 이 메서드가 처리 |
AiMessageResponse (반환 타입) | Jackson이 자동으로 JSON 직렬화 |
@RequestBody | HTTP Body의 JSON → 객체로 역직렬화 |
log.info(userMessageRequest.toString()) | @Data의 toString()으로 로그 출력 |
new AiMessageResponse() | 기본 생성자로 빈 응답 객체 생성 |
setQuestion(), setResponse() | @Data의 setter로 값 세팅 |
return aiResponse | @RestController가 자동으로 JSON 변환 후 응답 |
{
"question": "오늘 삼성전자의 주가를 알려줘",
"response": "삼성전자의 오늘 주가는 230000"
}
Board 클래스 하나로 요청/응답을 모두 처리하면 관리가 어렵다.
저장(save)은 Request DTO, 응답은 Response DTO로 파일을 분리해서 관리하는 것이 실무 관례다.
dto/
├── BoardCreateRequest.java ← 게시글 등록 요청 (저장할 데이터)
├── BoardUpdateRequest.java ← 게시글 수정 요청
├── BoardResponse.java ← 게시글 조회 응답 (클라이언트에게 보여줄 데이터)
└── BoardListResponse.java ← 목록 조회 응답
| 구분 | 클래스명 패턴 | 용도 |
|---|---|---|
| 요청 | XxxRequest | @RequestBody, @ModelAttribute로 데이터 수신 |
| 응답 | XxxResponse | 컨트롤러에서 반환, JSON 직렬화 |
왜 나눠야 할까?
@JsonProperty 같은 설정이 한 클래스에 뒤섞이는 걸 방지할 수 있다@JsonProperty브라우저(클라이언트)에게 응답할 때 특정 필드를 숨기고 싶은 경우, @JsonProperty의 access 속성을 사용한다.
| access 옵션 | 설명 |
|---|---|
Access.WRITE_ONLY | 역직렬화(요청 수신)만 허용 → 응답 JSON에 포함되지 않음 |
Access.READ_ONLY | 직렬화(응답)만 허용 → 요청에서 값을 받지 않음 |
Access.READ_WRITE | 기본값. 요청/응답 모두 허용 |
@Data
public class BoardRequest {
private String btitle;
private String bcontent;
// 요청으로는 받지만, 응답 JSON에는 포함하지 않음
@JsonProperty(access = Access.WRITE_ONLY)
private String bpassword;
}
비밀번호처럼 받기는 해야 하지만 절대 응답에 노출되면 안 되는 필드에 사용한다.
DTO를 Request/Response로 분리하면 이런 설정 없이도 자연스럽게 해결되는 경우가 많다.
지금은 응답이 하드코딩이지만, 실제로는 AI API 호출이 들어간다.
// 지금 (하드코딩)
aiResponse.setResponse("삼성전자의 오늘 주가는 230000");
// 실제라면
String aiAnswer = aiService.ask(userMessageRequest.getQuestion()); // AI API 호출
aiResponse.setResponse(aiAnswer);
지금 코드는 요청 받고 → 처리하고 → 응답 반환하는 REST API의 기본 뼈대다.
유효성 검사
Form Validation
우리 과정에서는 Valid만 사용할 것.