개발스토리

단 건 조회 시 데이터가 없다면..?부터 시작된 이야기 본문

삽질 기록

단 건 조회 시 데이터가 없다면..?부터 시작된 이야기

무루뭉 2021. 12. 10. 09:02

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을 다루는 것이 아직 너무 미숙하다..! 

이번 이슈를 통해 배운게 많은듯..!!!

Comments