음식점 storeId를 파라미터로 받아 해당 음식점의 메뉴 리스트를 response로 GET하는 search api를 개발 중이다. 아래는 storeId 1의 메뉴 리스트를 반환한 데이터다. 그런데 name 필드 값에 null이 들어가 문제 원인을 찾아야 했다.
{
"result": {
"result_code": 200,
"result_message": "성공",
"result_description": "성공"
},
"body": [
{
"id": 1,
"store_id": 1,
"name": null,
"amount": 3000,
"status": "REGISTERED",
"thumbnail_url": "https://chingchanmall.com/web/product/big/1652667152924l0.jpg",
"like_count": 0,
"sequence": 0
},
{
"id": 2,
"store_id": 1,
"name": null,
"amount": 4000,
"status": "REGISTERED",
"thumbnail_url": "https://chingchanmall.com/web/product/big/1652667152924l0.jpg",
"like_count": 0,
"sequence": 0
}
]
}
}
이 문제는 아래와 같은 도메인형 패키지 구조 덕분에 직감적으로 빠르게 해결 할 수 있었다. 아래 어느 영역에서 문제가 생겨 name에 null값이 들어간 것 일까?

필드에 null이 들어간 실수라면 변환 과정에서 값을 옮겨와 빌드하는 3번 converter가 유력하다. 검증을 위해 search 로직의 흐름을 알아보자.
@RestController
@RequestMapping("/open-api/store-menu")
@RequiredArgsConstructor
public class StoreMenuApiController {
private final StoreMenuBusiness storeMenuBusiness;
@GetMapping("/search")
public Api<List<StoreMenuResponse>> search(
@RequestParam Long storeId
){
var response = storeMenuBusiness.search(storeId);
return Api.OK(response);
}
}
@RequiredArgsConstructor
@Business
public class StoreMenuBusiness {
private final StoreMenuService storeMenuService;
private final StoreMenuConverter storeMenuConverter;
public List<StoreMenuResponse> search(
Long storeId
){
var list = storeMenuService.getStoreMenuByStoreId(storeId);
return list.stream()
.map(it->{
return storeMenuConverter.toResponse(it);
})
.collect(Collectors.toList());
}
}
@Service
@RequiredArgsConstructor
public class StoreMenuService {
private final StoreMenuRepository storeMenuRepository;
public List<StoreMenuEntity> getStoreMenuByStoreId(Long storeId){
return storeMenuRepository.findAllByStoreIdAndStatusOrderBySequenceDesc(storeId,StoreMenuStatus.REGISTERED);
}
public StoreMenuEntity register(
StoreMenuEntity storeMenuEntity
){
return Optional.ofNullable(storeMenuEntity)
.map(it->{
it.setStatus(StoreMenuStatus.REGISTERED);
return storeMenuRepository.save(it);
})
.orElseThrow(()->new ApiException(ErrorCode.NULL_POINT));
}
}
business에서 converter로 list의 elements를 response 모델로 변환 후 반환.
controller에서 storeMenuResponse list 반환.
5번에 list가 내려오기까지 내부 객체에 접근하여 수정한 것은 4번 business에서 사용한 converter 밖에 없다. 아래는 converter 패키지의 StoreMenuConverter 클래스다.
@Converter
public class StoreMenuConverter {
public StoreMenuEntity toEntity(StoreMenuRegisterRequest request){
return Optional.ofNullable(request)
.map(it->{
return StoreMenuEntity.builder()
.storeId(it.getStoreId())
.name(it.getName())
.thumbnailUrl(it.getThumbnailUrl())
.price(it.getAmount())
.build();
})
.orElseThrow(()->new ApiException(ErrorCode.NULL_POINT));
}
public StoreMenuResponse toResponse(StoreMenuEntity entity){
return Optional.ofNullable(entity)
.map(it->{
return StoreMenuResponse.builder()
.id(it.getId())
.storeId(it.getStoreId())
.amount(it.getPrice())
.status(it.getStatus())
.thumbnailUrl(it.getThumbnailUrl())
.likeCount(it.getLike_count())
.sequence(it.getSequence())
.build();
})
.orElseThrow(()->new ApiException(ErrorCode.NULL_POINT));
}
}
역시나 toResponse에서 name을 매핑하지 않은 것을 확인 할 수 있었다. 즉 4번에서 list속 entity를 response 모델로 변환하는데 name 필드에 null이 들어간 채 build 된 것이다. 아래는 수정 후 결과다.
{
"result": {
"result_code": 200,
"result_message": "성공",
"result_description": "성공"
},
"body": [
{
"id": 1,
"store_id": 1,
"name": "ice americano",
"amount": 3000,
"status": "REGISTERED",
"thumbnail_url": "https://chingchanmall.com/web/product/big/1652667152924l0.jpg",
"like_count": 0,
"sequence": 0
},
{
"id": 2,
"store_id": 1,
"name": "ice caffe latte",
"amount": 4000,
"status": "REGISTERED",
"thumbnail_url": "https://chingchanmall.com/web/product/big/1652667152924l0.jpg",
"like_count": 0,
"sequence": 0
}
]
}
storeId 1에 대한 store menu list가 잘 반환된 것을 확인 할 수 있다.