개발스토리
단 건 조회 시 데이터가 없다면..?부터 시작된 이야기 본문
API를 만들어 내면서 단 건 조회 시 데이터가 없다면 response를 빈 객체로 내려주기로 했다.
아래와 같은 상황을 예시로 두겠다.
한 명의 유저를 조회하는 GET test.com/users/{userId}라는 API가 있다고 보자.
성공적으로 조회가 완료가 됐다면, 아래와 같이 response를 보내주었다.
{
"success": true,
"response": { "id": 1, "nickname": "king" },
"error": null
}
하지만, GET test.com/users/100 즉, 100번의 유저를 조회하려하는 데 100번의 유저가 없다면...??
{
"success": true,
"response": { "id": null, "nickname": null },
"error": null
}
원래는 위와 같이 응답이 내려갔다.
하지만 프론트 쪽에서 판단하기에는 매번 (프론트에서 선언한 변수).response.id가 null인 지를 체크하는 비효율적인 코드가 나왔다.
그래서 아래와 같이 빈 객체를 내려주기로 하였다.
{
"success": true,
"response": {},
"error": null
}
왜 이런 결정을 하였나??
> 모든 api에서 단 건 조회시 데이터가 없을 때 위와 같은 응답을 내려준다면 JS 메서드로 response에 담긴 객체가 비었는 지 아닌 지를 체크하는 공통 로직으로 다룰 수 있다.
대안으로는 어떤게 제안되었는가??
> 별도의 필드로 count를 내려주어 0이라면 데이터가 없다는 걸로 판별하자는 방법도 나왔었다.
그렇다면 아래와 같은 응답이 내려간다.
{
"success": true,
"response": { "id": null, "nickname": null },
"error": null,
"count" : 0
}
리스트 조회가 아닌 단건 조회에서 0아니면 1로만 이루어진 별도의 필드를 두는 것은 내키지 않았다.
그래서 결국에는 빈 객체로 내려주기로 하였다.
어떤 방식으로 내려주는 가?
여기서부터 삽질이 시작됨...
@Transactional(readOnly = true)
@Override
public UserDto findOneUser(Long userId) {
Optional<User> user = userRepository.findByUserId(userId);
return user.map(UserDto::new).orElseGet(UserDto::new);
}
원래 이런 코드에서 빈 객체가 내려가도록 바꿔야 했다.
하지만 이미 타입이 Optional이 걸려있고, UserDto가 걸려있어 빈 객체랑 타입 불일치가 있다.
여기서부터 무한 고민,,,, 어떻게 바꿀까,,, 어떻게 할까,,, ( 선배 개발자분에게 의견을 여쭤봄.... )
단 건 조회는 API마다 다 있고 데이터가 없는 상황일 때 동일한 처리를 해줘야 한다.
그래서 Exception Handler를 따로 생성해서 처리하기로 했다!
package com.meta.common.exception;
public class NoDataException extends RuntimeException {}
따로 메시지는 날리지 않을 거기 때문에 간단하게 Exception class를 만들었다.
그 다음 ExceptionHandler를 모아둔 파일에 다음과 같이 작성했다.
@RestControllerAdvice(annotations = {RestController.class, Controller.class})
public class ExceptionHandler {
@ExceptionHandler
public ResponseEntity<ApiResult<?>> noDataException(NoDataException e) {
Object result = new Object();
return new ResponseEntity<>(ApiUtils.success(result), HttpStatus.OK);
}
}
"No serializer found for class ~ and no properties discovered to create BeanSerializer"
직렬화를 해야 하는 데 지금은 가장 원초적인 Object 타입을 두었기 때문에 이 에러를 해결할 방법이 떠오르지 않았다.
그래서 EmptyDto를 따로두었다.
import java.io.Serializable;
public class EmptyDto implements Serializable {}
하지만 같은 에러가 나왔다..! 검색해보니 보통 private 필드를 두고 getter를 안두었거나 하는 등에 문제였다.
나는 어떤 필드도 없다..! 그래서 필드가 없으니..!? 직렬화 시켜줄 게 없다? 라는 생각을 했다.
또 검색,,,검색,,
ㄷㄷㄷㅈ!!
관련 레퍼런스들을 찾아보니 spring.jackson.serialization.FAIL_ON_EMPTY_BEANS 요 설정의 디폴트가 true인 것 같다.
기본값이 유형에 대한 접근자를 찾을 수 없을 때 직렬화를 할 수 없는 유형으로 나타내기 위해 예외가 발생되도록 한다.
값을 false로 바꿔주던가...!하면 된다.
또한, 다양한 어노테이션들로 해결이 가능했다.
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.Serializable;
@JsonSerialize
public class EmptyDto implements Serializable {}
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import java.io.Serializable;
@JsonIgnoreProperties(ignoreUnknown = true)
public class EmptyDto implements Serializable {}
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import java.io.Serializable;
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class EmptyDto implements Serializable {}
등등... 다양한 어노테이션들로 테스트가 다 통과된다...!!
최종 나의 서비스 파일이다.
@Transactional(readOnly = true)
@Override
public UserDto findOneUser(Long userId) {
Optional<User> user = userRepository.findByUserId(userId);
return user.map(UserDto::new).orElseThrow(NoDataException::new);
}
느낀점
> 진짜 다양한 해결 방법이 있다..
> Jackson 라이브러리를 정독해봐야겠다..!
> Optional을 다루는 것이 아직 너무 미숙하다..!
이번 이슈를 통해 배운게 많은듯..!!!