# 응답이 변칙적이에요? 응답하라, 변칙 Response Body. Spring Boot API

> 🇰🇷 Kor: 지금 보는 중!  
> 🗺 Eng: Not yet!  
> 🇯🇵 Jap: まだです。

---

# API 데이터 입출력

여기 API 요청과 응답을 표현한 작은 그림이 있습니다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700274378415/de9a710d-12fb-41f1-8459-36653b299c7b.png align="center")

웹 표준이 XML이고 뭐고, 우리는 어차피 <mark>JSON 양식</mark>으로 데이터를 주고 받는 편입니다. JSON은 자바스크립트 객체의 모습을 띤 문자열 데이터로, 읽기 쉬우면서도 여러 환경에서 쉽게 호환되는 양식이기 때문에 여러 곳에서 선호하거든요.

스프링 부트에서는 이 JSON 문자열을 요청의 데이터로 받으면, 이것을 <mark>자바 객체로 변환</mark>함으로써 쉽게 사용할 수 있습니다.

## JSON 메시지를 해석하거나 생성한다는 것

이런 변환에 우리는 '**직렬화**', '**역직렬화**'라는 용어를 사용합니다. 데이터를 전송하기 좋은 형태로 나열하는 것을 '직렬화(Serialization)', 반대로 그것을 해석하여 프로그램 내에서 사용하는 모습으로 변환하는 것을 '역직렬화(Deserialization)'라고 해요.

이 경우엔 JSON 문자열로 만드는 게 직렬화, 그것을 해석해서 자바 객체로 만드는 게 역직렬화겠죠.

만약 이런 자바 클래스가 있고, 이것으로 어떤 객체를 만들었다고 합시다.

**자바 클래스 예시**

```java
// 설명에 불필요한 세세한 구현은 모두 생략했습니다.
public class ExampleData {
    String title;
    String content;
    Long views;
}
```

자바에선 클래스 대신 `Map<...>`도 쓸 수 있습니다. 그래도 클래스를 쓰는 게 더 명시적이고 좋아요.

이 클래스의 객체를 하나 JSON 문자열로 직렬화해 보면 다음 예시처럼 나올 수 있습니다.

**JSON 예시**

```json
{
  "title": "난 손이 두 갠데...",
  "content": "이건 제2 손(json)이야.",
  "views": 999
}
```

## Jackson, Gson

Jackson과 Gson은 모두 JSON 문자열을 자바 객체로 매핑해 주는 역할을 합니다. 변환해 준다고 보시면 돼요.

스프링 환경에서는 이 <mark>JSON 컨버터</mark>로 `jackson`을 많이 사용하는 편입니다. 우리가 코드에서 직접 사용한다기보다도, 우리에게 안 보는 어느 영역에서 `jackson`이 열일을 하고 있죠.

비슷한 일을 하는 기술로 구글에서 만든 `gson`도 있습니다. 둘 모두 퍼포먼스에서도 환경과 조건에 따라 측정 결과가 다르고, 편의 기능에서도 무엇이 낫다 할 순 없고 체감에 차이가 있을 수 있습니다. 아래 설명에서는 둘 다 용법 정도는 취급하려고 합니다. Gson에 대해서도 생각보다 많이들 궁금해하시더라구요.

---

# 네? API 하나로 그때그때 다른 응답 데이터 목록이요?

"네, 되죠?"

## 📡 응답하라, 변칙 Response Body

복잡한 프로세스를 띠는 API를 설계하다 보면, 여러 케이스에 따라 서로 다른 데이터를 응답할 때가 있습니다!

🗣 API 설계한 사람 "어, 요청을 보낼 건데 이때는 (프론트에) 인증 코드랑 이런 정보를 그냥 주시고요, 이러이럴 땐 회원가입 코드를 (프론트가) 받고 다음 저 요청을 보낼 때 서버에 이 코드를 담아서 서로 거시기하기로 했거든요."

그런데 백엔드 서버가 자바처럼 강타입 언어로 되어 있다면, 응답 데이터 명세를 써야 할 수 있죠. 게다가 프론트엔드 개발자는 요즘 타입스크립트를 쓰면서 `null`에 대한 기피를 보이는 분들도 계셔요. 그냥 `json`에서 완전히 제거하길 바라는 거죠. 그럼 이제 "자바 스프링에서 그게 되냐."를 고민할 시점입니다.

---

# 변칙적으로 응답하는 방안들

이 글에서는 우리가 잘 아는 일반적인 컨트롤러에서 할 수 있는 방안을 다룹니다!

그 외 AOP(Exception Handler 포함)를 활용하거나 커스텀한 여러 방식을 쓰거나, 아니면 WebFlux 등 애초에 다른 환경일 때에 대한 이야기는 생략하고, 우리가 스프링 부트라고 할 때 흔히 떠올릴 수 있는 WebMVC 환경의 컨트롤러에 대해서만 쓸게요.

## ResponseEntity

API 응답의 여러 요소를 직접 설정하고 싶을 때 선택할 수 있죠. 제네릭이 받는 타입은 바디 타입입니다!

```java
ResponseEntity<바디_타입>
```

* `ResponseEntity<Object>`: 모든 자바 객체를 받을 수 있습니다. 대신, `ResponseEntity<특정_타입>`으로 이미 `ResponseEntity` 객체가 있다면, 이것을 `ResponseEntity<Object>` 대신에 쓸 수는 없습니다. (약간 덜 유연합니다.)
    
* `ResponseEntity<?>`: 얘도 모든 자바 객체를 받을 수 있어요. 게다가 얘는 와일드카드(`<?>`)여서, `ResponseEntity<특정_타입>`으로 되어 있는 객체도 받을 수 있어요.
    

```java
@GetMapping("/example")
public ResponseEntity<?> iAmControllerMethod() { // [1] 반환 타입을 ResponseEntity<?>로 변경하고
    // ...
    return ResponseEntity
            .status(httpStatus) // 상태코드와
            .body(response); // [2] 반환할 객체를 담아 주면 됩니다.

    // ResponseEntity의 다른 정적(static) 메서드를 이용하는 경우가 많습니다.
}
```

`ResponseEntity`를 반환 타입으로 사용하면, 반환할 바디 데이터뿐 아니라 상태 코드도 변경할 수 있어서 유연한 응답에 좋습니다. 그래서 이것을 습관처럼 사용하는 분들도 있죠. (근데 사실 일반적인 경우는 대체할 편한 수단들이 있어요.)

이렇게 하면 여러 DTO 타입을 `response`로 사용할 수 있기 때문에 많은 응답 유형을 처리할 수 있습니다! 예를 들면 간단한 분기를 만들어서 이렇게 할 수 있습니다.

```java
@GetMapping("/example")
public ResponseEntity<?> iAmControllerMethod() {
    ResponseEntity<?> responseEntity;

    // ...

    if (isSomething) { // 어떤 경우에는
        ResponseDtoA dtoA = ...; // ResponseDtoA 타입 데이터를 응답할 데이터로 씁니다.
        // ...
        // (⬇️ ResponseEntity.ok(dtoA)로도 쓸 수 있어요.)
        responseEntity = ResponseEntity
                .status(HttpStatus.OK)
                .body(dtoA);
    } else { // 어떤 경우에는
        ResponseDtoB dtoB = ...; // ResponseDtoB 타입 데이터를 응답할 데이터로 씁니다.
        // ...
        responseEntity = ResponseEntity
                .status(HttpStatus.OK)
                .body(dtoB);
    }

    return responseEntity;
}
```

어떤 경우에는 `ResponseDtoA` 타입을 반환에 쓰고, 어떤 경우에는 `ResponseDtoB` 타입을 반환에 쓰고 있어요.

이러면 작업 방식은 "반환할 데이터 타입을 반환 유형에 따라 모두 작성해 두었을 때" 용이합니다. 마치 "우리는 모두 TDD를 할 수 있고, 당신은 안정적일 거예요!"라고 해서 TDD를 했더니 막상 우리 회사는 그런 체계적인 기획과 사업 설계 속에 움직이는 게 아니었어서 고생하게 되는 것처럼, 아주 계획적이라면 문제 없는데, 그런 게 아니라면 생산성에서는 검토할 기획과 설계가 비교적 빼곡할 수 있습니다!

물론 작업 방식에 따라서는 이 방식도 괜찮아 보입니다.

## 하나의 DTO가 데이터를 선택적으로 직렬화

복잡한 응답 유형으로 세세히 나뉘고, 데이터 응답 유형이 다양하고, 어떤 것은 공통으로 필수인 줄 알았더니 어떤 응답 때는 포함할 필요가 없고, 아 이거 정말 API 응답 데이터 명세를 설계하는 것도 심플하게만 되는 건 아닐 때도 있습니다.

게다가 개발자의 의도와 달리 기획이라는 건 <mark>언제든 변경될 수 있어야</mark> 하는 점도 있죠. 이런 경우는 위에서 다룬 `ResponseEntity<?>`로 여러 DTO 데이터 타입을 다루는 방식은 관리가 번거롭습니다. 그래서 개인적으로 위 방식은 (일반적인 경우) 오버스펙이라고 생각하고 있어요.

그래서 이번에는 <mark>하나의 DTO에 데이터를 모두 나열</mark>하고, 이중에 <mark>실제 로직에서 필요한 것</mark>만 선택적으로 채우고 나머진 삭제해 봅시다. 다행히 우리가 메시지 컨버터로 사용하는 `jackson`과 `gson` 모두 `null` 데이터를 JSON 문자열에서 제외하는 직렬화 보조 기능들을 제공합니다.

```java
@Getter
@AllArgsConstructor
// ...
public class ResponseDto {
    @JsonInclude(Include.NON_EMPTY) // null 또는 빈 문자열은 제외
    private final String title;

    @JsonInclude(Include.NON_EMPTY) // null 또는 빈 문자열은 제외
    private final String content;

    @JsonInclude(Include.NON_NULL) // null은 제외
    private final Long views;
}
```

이러면 우리가 만들 수 있는 JSON 종류는 다음과 같아요.

```json
// 아무 데이터도 없는 경우
{}

// 하나의 데이터만 있는 경우 1
{"title": "..."}

// 하나의 데이터만 있는 경우 2
{"content": "..."}

// 하나의 데이터만 있는 경우 3
{"views": 999}

// 두 데이터만 있는 경우 1
{"title": "...", "content": "..."}

// 두 데이터만 있는 경우 2
{"title": "...", "views": 999}

// 두 데이터만 있는 경우 3
{"content": "...", "views": 999}

// 세 데이터가 모두 있는 경우
{"title": "...", "content": "...", "views": 999}
```

그리고 이런 세부적인 결정은 데이터를 만드는 시점에 할 수 있습니다. 제외할 데이터는 모두 `null`을 넣으면 돼요. `gson`이라면 `@JsonInclude(...)` 애노테이션이 없어도 됩니다(`null`만 제외).

이러면 미리 계획한 케이스마다 그에 맞는 세부 DTO 대로 따로따로 클래스를 작성할 필요 없이, 로직 내에서 실제 데이터 목록을 결정할 수 있기 때문에 아주 유연한 대처가 가능합니다.

## 어떨 때 무엇을 쓸까?

* API에 대한 설계가 분명하고, 변경을 빈번히 할 필요가 없을 때 (말이 된다면.)
    
    * `ResponseEntity<Object>`
        
    * `ResponseEntity<?>` (권장)
        
    * `ResponseEntity<? extends SomeInterface>` (필수 `getter` 등 설계를 모두 구현할 때)
        
* API에 대해 변경 여지가 있거나 프로세스에 유연하게 대처하고 싶을 때
    
    * `@JsonInclude(...)` (jackson)
        
    * `gson` (`null`만 넣으면 알아서 JSON 직렬화 때 포함 안 함.)
        

사실 API는 유연하게 관리 가능하면서도 오버스펙이 없어야, **<mark>기획 변경에</mark>** <mark>(그저 기술적 이유로) </mark> **<mark>주저하는 일이 덜하다</mark>는 생각입니다**. 그래서 `ResponseEntity<...>`보다는 `jackson`이나 `gson`으로 불필요 데이터를 소거하는 쪽으로 택하는 편입니다. 물론 이러거나 저러거나 빈번한 변경은 싫긴 하구요.

`ResponseEntity<?>`로 세부적인 여러 DTO를 따로따로 관리하는 방식은 어떤 데이터가 어떤 경우에 어디에 포함되어야 하나 모두 결정한 다음부터 작업이 가능하기 때문에, API 하나 개발에도 너무 많은 관리 대상을 생성하고 혼란스러울 수 있어요.

그래서 제 관점에서는 하나의 DTO에서 유동적으로 데이터를 채우는 방식이 낫다고 보고 있습니다.

### 참고로 Gson은 기본 메시지 컨버터로 쓸 때...

스프링 부트에서는 기본적으로 Jackson을 메시지 컨버터로 사용하는데, 이러면 `@JsonInclude(...)` 또는 `@JsonIgnore(...)`를 써야 하고, 만약 이를 Gson으로 변경한다면 그냥 데이터에 `null`만 넣으면 알아서 JSON에서 제외합니다. 이거 참 진입장벽 낮고 편해 보이죠. 어차피 `null`은 일반적으로 프론트엔드에서 원하는 데이터가 아니니까 그냥 제외하는 게 낫잖아요.

그런데 한 가지 조심할 것은, Gson은 Jackson에 비해 초기 설정을 추가해야 할 수 있다는 겁니다. 지정한 타입에 대한 직렬화 객체와 역직렬화 객체를 모두 작성해야 할 수 있습니다. 이런 점은 선택하시면 됩니다. 그리고 Jackson에서 `@JsonProperty("new_alias")` 애노테이션은 Gson에서 `@SerializedName("new_alias")`인 등 차이점도 숙지해야 합니다.

---

이번 글은 급히 작성해서 두서가 없습니다. 나중에 한 차례 정리하는 기회를 둘게요! 하고 싶은 말은 결국 `@JsonInclude(...)`와 Gson이었는데! 배경지식이 다른 분들께도 설명하려다 보니 부연설명도 충분히 붙이며 쓰려고 했습니다! 필요하다면 `ResponseEntity<...>` 사용은 아예 제거하는 것도 생각해 보겠습니다!

---

자료는 모두 DALL·E, excalidraw, Power Point 등을 통해 스스로 만들었습니다. (그래서 영어가 어색할 수 있습니다.) 어색한 영어 문장은 남대문 열렸다고 알려 주듯 살짝 말씀해 주시면 얼른 고쳐 보겠습니다. 그러면 다들 모쪼록 착한 개발 하세요.
