[Spring] 3일차

김민지·2026년 4월 13일

kosa-Spring

목록 보기
3/13

Spring Boot HTTP 응답 처리 정리

1. 직렬화 & 역직렬화

방향설명
직렬화Java 객체 → JSON / XML (응답 시)
역직렬화JSON / XML → Java 객체 (요청 수신 시)

Spring Boot에서는 Jackson 라이브러리가 자동으로 직렬화/역직렬화를 처리한다.

반환 타입별 JSON 직렬화 형태

Java 타입JSON 직렬화 결과
단일 객체 (DTO/Entity){ "field": "value", ... }
List<T> (객체 배열)[{ ... }, { ... }]
Map<K, V>{ "key": "value", ... } (객체 형태)


2. HTTP 상태 코드

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

3. ResponseEntity vs HttpEntity

구분HttpEntity<T>ResponseEntity<T>
상속 관계부모 클래스HttpEntity를 상속
HTTP 상태 코드 설정❌ 불가✅ 가능
헤더 설정✅ 가능✅ 가능
바디 설정✅ 가능✅ 가능
주 용도요청/응답 공통컨트롤러 응답 반환

실제로 컨트롤러에서 응답을 반환할 때는 ResponseEntity를 사용한다.

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) 방식이 가독성이 더 좋아 선호된다.


4. 리턴 타입이 void인 경우 — 파일 다운로드

리턴 타입을 void로 선언하면 Spring이 응답을 자동으로 구성하지 않으므로, HttpServletResponse 객체를 직접 사용해 응답을 만들어야 한다.

HttpServletRequest / 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();
}

5. 한글 파일명 다운로드 처리

문제 원인

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)을 활용할 수 있다.


6. 정리 — 응답 방식 선택 가이드

상황권장 방법
JSON 데이터 반환 (상태코드 기본 200)@ResponseBody + 객체 반환 or @RestController
JSON 반환 + 상태코드/헤더 커스텀ResponseEntity<T> 반환
파일 다운로드void 반환 + HttpServletResponse 직접 사용

Spring Boot 직렬화 & DTO 정리

7. 직렬화/역직렬화를 하는 이유

핵심 한 줄

Java 객체는 Java 안에서만 존재할 수 있다. 네트워크로 보내려면 "누구나 읽을 수 있는 형태"로 변환해야 한다.

Java 객체는 왜 그대로 못 보낼까?

Java 객체는 JVM 메모리(힙) 위에 살아있다. Board 객체를 예로 들면 bno, btitle 같은 필드값뿐 아니라 객체의 메모리 주소(0x7f3a...)도 함께 존재하는데, 이 주소는 내 JVM 안에서만 유효한 값이다.

이걸 그대로 네트워크로 전송하면?

  • 받는 쪽은 Java가 아닐 수도 있다 (Python, JavaScript, iOS 등)
  • 받는 쪽은 0x7f3a... 주소가 아무 의미 없다
  • JVM 메모리 구조 자체를 상대방이 모르니까 해석 불가

그래서 JSON으로 변환한다

JSON은 그냥 텍스트다. 어떤 언어든 텍스트는 읽을 수 있다. Java 객체를 JSON으로 변환(직렬화)하면 {"bno": 1, "btitle": "제목"} 같은 텍스트가 되고, 이걸 Python이든 JavaScript든 iOS든 누구나 파싱해서 쓸 수 있다. 반대로 받은 JSON을 Java 객체로 되돌리는 것이 역직렬화다.

비유

한국어로만 쓴 계약서를 프랑스 사람한테 보내면 못 읽는다. 영어(공통 언어)로 번역해서 보내야 양쪽이 이해할 수 있는 것처럼, Java 객체를 JSON이라는 공통 형식으로 변환해서 주고받는 것이 직렬화/역직렬화다.


8. JSON이 숫자·문자·논리형만 지원하는 이유

JSON은 텍스트 기반 포맷이다. 메모장으로 열면 읽힌다.

메모장으로 읽으려면 모든 값이 문자로 표현 가능해야 한다.

타입문자로 표현 가능?JSON 지원
숫자 123
문자열 "hello"
논리형 true
이진 데이터 0xFF 0xD8
Java 객체 참조 (메모리 주소)

바이트 배열(byte[])이 JSON이 안 되는 이유

컴퓨터에서 문자도 사실 바이트다. 차이는 "이 바이트가 어떤 문자인지" 약속(인코딩)이 있냐 없냐다. 예를 들어 0x41은 ASCII 약속에 따라 'A'를 의미하지만, 0xFF는 유효한 UTF-8 문자가 아니라 어떤 문자로도 표현할 수 없다. 이미지나 파일 같은 이진 데이터에는 이런 약속 밖의 바이트가 섞여 있어서, 텍스트로 변환하면 깨지거나 의미가 손실된다.


9. REST API 실습 코드 분석

전체 흐름

Postman이 JSON을 전송하면 @RequestBody가 이를 UserMessageRequest 객체로 역직렬화한다. 컨트롤러에서 비즈니스 로직을 처리한 뒤 AiMessageResponse 객체를 만들어 반환하면, @RestController가 자동으로 JSON으로 직렬화해 응답한다.

Postman 요청

{
    "mid": "spring",
    "question": "오늘 삼성전자의 주가를 알려줘"
}

DTO

UserMessageRequest — 요청을 담는 그릇

@Data
public class UserMessageRequest {
    private String mid;       // 사용자 ID
    private String question;  // 질문 내용
}

Postman JSON이 역직렬화되어 이 객체에 담긴다. JSON의 "mid" 키는 mid 필드로, "question" 키는 question 필드로 자동 매핑된다.

AiMessageResponse — 응답을 담는 그릇

@Data
public class AiMessageResponse {
    private String question;   // 받은 질문 그대로 반환
    private String response;   // AI 응답 내용
}

이 객체가 직렬화되어 JSON으로 응답된다.

@Data가 자동 생성하는 것들

생성되는 것역할
getterJackson이 직렬화할 때 값 읽기
setterJackson이 역직렬화할 때 값 넣기
기본 생성자Jackson이 객체 먼저 만들 때 사용
toString()log.info()에서 객체 내용 출력
equals(), hashCode()객체 비교

@Data가 없으면 Jackson이 역직렬화를 못 해서 400 에러 발생

Controller

@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 직렬화
@RequestBodyHTTP Body의 JSON → 객체로 역직렬화
log.info(userMessageRequest.toString())@DatatoString()으로 로그 출력
new AiMessageResponse()기본 생성자로 빈 응답 객체 생성
setQuestion(), setResponse()@Data의 setter로 값 세팅
return aiResponse@RestController가 자동으로 JSON 변환 후 응답

최종 응답 JSON

{
    "question": "오늘 삼성전자의 주가를 알려줘",
    "response": "삼성전자의 오늘 주가는 230000"
}

10. DTO 설계 원칙

Request / Response 파일 분리

Board 클래스 하나로 요청/응답을 모두 처리하면 관리가 어렵다.

저장(save)은 Request DTO, 응답은 Response DTO로 파일을 분리해서 관리하는 것이 실무 관례다.

dto/
├── BoardCreateRequest.java   ← 게시글 등록 요청 (저장할 데이터)
├── BoardUpdateRequest.java   ← 게시글 수정 요청
├── BoardResponse.java        ← 게시글 조회 응답 (클라이언트에게 보여줄 데이터)
└── BoardListResponse.java    ← 목록 조회 응답
구분클래스명 패턴용도
요청XxxRequest@RequestBody, @ModelAttribute로 데이터 수신
응답XxxResponse컨트롤러에서 반환, JSON 직렬화

왜 나눠야 할까?

  • 요청 필드와 응답 필드가 다른 경우가 많다
  • 각 DTO의 책임이 명확해져 유지보수가 쉬워진다
  • @JsonProperty 같은 설정이 한 클래스에 뒤섞이는 걸 방지할 수 있다

특정 필드를 응답에서 제외하기 — @JsonProperty

브라우저(클라이언트)에게 응답할 때 특정 필드를 숨기고 싶은 경우, @JsonPropertyaccess 속성을 사용한다.

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로 분리하면 이런 설정 없이도 자연스럽게 해결되는 경우가 많다.

11. 실제 AI 연동으로 확장한다면

지금은 응답이 하드코딩이지만, 실제로는 AI API 호출이 들어간다.

// 지금 (하드코딩)
aiResponse.setResponse("삼성전자의 오늘 주가는 230000");

// 실제라면
String aiAnswer = aiService.ask(userMessageRequest.getQuestion()); // AI API 호출
aiResponse.setResponse(aiAnswer);

지금 코드는 요청 받고 → 처리하고 → 응답 반환하는 REST API의 기본 뼈대다.

유효성 검사

Form Validation

  • 사용자로부터 입력받은 데이터를 검증하여 유효한 값이 들어왔는지 확인하는 과정
  • @ModelAttribute 또는 @RequestBody 폼 바인딩 객체의 유효성 검사 지원
  • 요청 매핑 메소드의 매개변수 적용: @Valid, @Validated

우리 과정에서는 Valid만 사용할 것.

profile
안녕하세요

0개의 댓글