노트 수정창에서 사용할 PATCH API, 즉 수정 API를 구현하던중 찝찝~한 부분을 발견하게 되었다.
PATCH 는 기본적으로 '수정할 부분만' 전송하도록 동작한다.
따라서 컨트롤러는 '값이 바인딩 된' 부분에 대해서만 수정작업을 진행하면 된다.
그런데 만약 사용자가 해당 필드를 '빈 값'으로 초기화 하려고 한다면?
즉, 위 화면에서 '내용'에 해당하는 부분을 Null로 초기화하고 싶다면?
프론트엔드에서 발생하는 요청은 다르지만, 백엔드 컨트롤러에서 받을 요청은 똑같아진다(구분할 수 없다.)
문제상황
자세히 설명하자면 다음과같다.
우선 프론트엔드에선 다음과 같이 요청이 발생한다.
그러나 백엔드에선 두 요청이 똑같이 보인다.
요청객체에 바인딩 되면서 할당되지 않은 값들은 Null로 초기화 되기 때문이다.
따라서 title만 수정했던 첫번째 요청의 content도 똑같이 null이 된다.
RFC 7386에 해당하는 JSON Merge Patch 스펙에 따르면, HTTP PATCH 요청시 부분적 Update의 대상이 되는 필드들을 가변적으로 전달할 수 있어야 한다. 그러나 Spring Boot 기반의 기본 프로젝트 구성으로는 특정 필드를 null로 교체하라는 것인지, 교체 대상이 아니라는 것인지 식별할 수 있는 방법이 없다는 것이다.
따라서 이 문제를 해결하기위해, "바인딩 자체가 되었는지" 자체를 구별 할 수 있는 방법이 필요했다.
결과적으로 <JsonNullable>을 활용해서 해결하는 방식을 통해 이 문제를 해결할 수 있었다.
해결방법
라이브러리를 통해 JsonNullable 형태로 요청객체를 재정의할 예정이다.
(1) 라이브러리 의존성 추가
implementation("org.openapitools:jackson-databind-nullable:0.2.4")
(2) Configuration 작성
@Configuration
public class JsonConfig {
@Bean("objectMapper")
public ObjectMapper objectMapper() {
return new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.ALWAYS)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS, SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.registerModules(new JsonNullableModule(), new JavaTimeModule());
}
}
(3) 요청객체를 JsonNullable을 활용한 형태로 재정의
@Getter
@Setter
@NoArgsConstructor
@ToString
public class NewNoteRequest {
private JsonNullable<ReadDateDto> readDate = JsonNullable.undefined();
private JsonNullable<LocalDateTime> selectedDate = JsonNullable.undefined();
@Size(max = 30)
private JsonNullable<String> title = JsonNullable.undefined();
@Size(max = 255)
private JsonNullable<String> content = JsonNullable.undefined();
private JsonNullable<Integer> page = JsonNullable.undefined();
private JsonNullable<PagesDto> pages = JsonNullable.undefined();
}
(참고로 @Valid를 위해 작성한 유효성 검사는 Nullable 안에 있는 객체에 대해 정상적으로 작동한다고 한다.)
위 상태에서, 이전에 보냈던 요청을 한번 더 보내보려고 한다. 명확한 차이를 보기 위해 컨트롤러에 다음 콘솔을 추가했다.
if (request.getTitle().isPresent()) System.out.println("title을 다음과같이 업데이트 합니다 : " + request.getTitle().get());
else System.out.println("title 을 업데이트 하지 않습니다.");
즉. Title객체(JsonNullable 타입)이 isPresent(존재)하는지 검사한 뒤, 존재하면 내부 정보를 꺼내 업데이트를 수행하고,
존재하지 않으면 수정하지 않는 필드이므로 업데이트를 수정하지 않는 형식이다.
PostMan을 통해 요청을 보내고, 응답을 테스트한 결과는 다음과 같다.
#1. title만 null로 수정하고, content는 유지
요청객체에 title이 포함되었으니, 무사히 Nullable 형태에 값이 바인딩된다.
즉, request.getTitle().isPresent()의 결과가 true가 되어 업데이트 작업을 이어나갈 수 있게된다.
#2. content만 null로 수정하고, title은 유지
요청객체엔 content만 포함하고, title은 포함되지 않았다.
따라서 request.getTitle().isPresent()의 결과가 false가 되고, 업데이트 작업을 실행하지 않게 된다.
이제 가변적인 업데이트 필드를 적절히 구분해 처리할 수 있게 되었다😎
아참, 조사결과 일부 사람들은 Optional을 사용하기도 하는것같은데, 이는 좋은 방식은 아닌것같다. 애초에 Optioanl의 설계의도가 메소드의 반환값에 사용하기 위해 만들어진것이기도 하고, Optional 자체가 Null이 되도록 설계하는것도 원래 목적과 다르기 때문이다. 라이브러리 리드미에도 자세히 적혀있다!
오늘의 문제를 해결하며, RestFul한 API에 대해 좀 더 이해하게 된것같다. 왜냐하면.. 지난 프로젝트들을 돌이켜보니, PATCH가 아니라 PUT에 가까운 형태로 수정 API를 개발 해왔던걸 깨달았다. 이번 기회로 두 차이를 구분하고, 백엔드에서 요청객체를 더 명확히 구분할 수 있는 PATCH 로직을 작성할 수 있었다.
자세한 코드는 여기에서 볼 수 있다. 바인딩할 요청 객체는 본문에 작성했으므로, 그를 바탕으로 엔티티를 수정하는 코드를 연결해두었다. 필요한분은 참고하시길~
'프로젝트 > 빼곡' 카테고리의 다른 글
여러기기에서 로그인, 다중 로그인 Spring에서 구현하기 (4) | 2024.12.20 |
---|---|
다른 도메인 환경에서 쿠키 셋팅하기(Feat.samesite Cookie) (4) | 2024.12.12 |
Redis로 최근검색어 구현하기 (1) | 2024.11.25 |
서버이전을 고려한 Jenkins기반 아키텍처 개선(Feat. GithubActions) (3) | 2024.11.22 |
라즈베리파이를 이용한 홈서버 구축 (1) | 2024.11.21 |