현재 프로젝트에서 사용자가 자신이 수신한 공지사항, 이벤트, QNA, 파티 매칭 관련한 알림을 필터링 해서 읽을 수 있는 기능을 구현하고 있다.
이 때, URI를 “/notify/{notifyType}” 으로 지정하고 String 타입이 아닌 정의한 Enum 타입으로 받는다면 Stirng 타입에서 Enum타입으로 변경하는 추가 로직을 구현없이 편하게 비즈니스 로직에 접근할 수 있다고 판단했다.
물론 URI의 의미 전달까지 제대로 전달할 수 있다고 생각했다.
그래서, URI 엔드포인트에 들어가는 PathVariable 값을 String 값으로 받는 것이 아닌 바로 정의한 Enum 타입으로 받아보는 방법을 정리하고자 한다.
Converter?
Spring에서 제공하는 Converter를 먼저 간단하게 확인해보자.
Converter는 S 타입의 소스 오브젝트를 T 타입의 타겟으로 변환해주는 역할을 한다.
Converter 인터페이스의 convert 메서드를 오버라이딩 해 Custom Converter를 작성할 수 있다.
실제 구현
먼저 위의 서론에서 설명한 프로젝트 내 정의한 Enum 타입을 먼저 확인해보자.
public enum NotifyType {
PARTY, // 파티 매칭
QNA, // QNA
NOTICE, // 공지사항
EVENT // 이벤트
}
프론트엔드에서 호출하는 URI는 소문자의 String 타입으로 호출되기 때문에 PARTY, QNA, NOTICE, EVENT는 각각 /party, /qna, /notice, /event 엔드포인트로 호출될 것이다.
위에서 작성한 Enum 타입에 대한 Converter를 작성해보자.
URI의 엔드포인트는 String 타입으로 요청되기 때문에 Source 오브젝트를 String 타입 파라미터로 받는다.
우리의 목적은 NotifyType으로 바꾸는 것이기 때문에 타겟 타입을 NotifyType으로 지정하고 convert 메서드를 오버라이딩한다.
public class NotifyTypeConverter implements Converter<String, NotifyType> {
@Override
public NotifyType convert(String source) {
try {
System.out.println(source);
return NotifyType.valueOf(source.toUpperCase(Locale.ROOT)); // 소문자로 들어온 source를 대문자로 바꾸고 NotifyType으로 변환
} catch (IllegalArgumentException e) {
return null;
}
}
}
내부 로직을 살펴보면 간단하다.
위에서 설명한대로 Source 오브젝트가 소문자 형태로 들어올 테니 들어온 Source를 대문자로 변경하고 변경한 소스를 Enum의 valueOf를 이용해 실제 매칭되는 Enum 타입을 찾는다.
매핑되는 NotifyType이 없다면 잘못된 요청이기 때문에 예외 처리를 해주고, URI에 대한 매핑단계에서 예외가 발생한 것이기 때문에 해당 예외는 ExceptionHandler를 이용해 Bad Request로 처리한다.
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<CommonResponse> handleIllegalArgumentException(IllegalArgumentException e) {
log.warn("IllegalArgument Exception");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(CommonResponse.response(false, "Bad Request"));
}
Converter를 모두 구현했다. 이제 생성한 Converter를 Spring에 애플리케이션 시작 시에 등록해야한다.
Converter의 등록은 WebMvcConfigurer 인터페이스의 addFormatters 메서드를 오버라이딩해서 등록한다.
addFormatters의 디스크립션을 확인하면, Converter나 Formatter를 추가할 수 있다고 설명되어있다.
Spring이 애플리케이션 시작 시에 해당 설정 정보를 읽게 하기 위해서 @Configuration을 사용하면 Converter 등록까지 마무리 된다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new NotifyTypeConverter());
}
}
마지막으로 매핑될 Controller 로직을 확인하자.
프론트엔드에서 호출한 URI는 String 타입으로 전달되지만 Spring Boot 서버로 들어온 이후 @PathVariable로 매핑되는 값은 정의한 Enum 타입이다.
정상적으로 호출이 된다면, 내부로직을 모두 처리하고 200 OK와 함께 응답 데이터를 보내줄 것이다.
@GetMapping(value = "/{notifyType}")
public ResponseEntity<?> searchNotifiesWithFilter(@PathVariable(name = "notifyType") NotifyType notifyType, HttpServletRequest servletRequest,
@RequestParam(name = "notifyId") Long notifyId, @RequestParam(name = "size") int size) {
log.info("[Notify Controller Search Notifies With Filter] : 회원의 필터링 된 알림 리스트 읽어오기");
// 회원 아이디 파싱
String memberId = servletRequest.getHeader("X-Authorization-Id");
// 인증 회원 체크
if (memberId.length() < 1)
throw new NotAuthorizationException("잘못된 접근입니다.", NbbangException.BAD_REQUEST);
// 필터링 된 알림 리스트 조회하기 (변환된 NotifyType 사용)
List<NotifyDTO> findNotifies = notifyService.searchNotifyList(notifyType, memberId, notifyId, size);
String message = "회원의 알림 리스트 조회에 성공했습니다.";
// 무한 스크롤 마지막 알림까지 불러온 경우 알림 메세지 다르게 응답
if(findNotifies.size() < size)
message = "모든 알림 리스트 조회에 성공했습니다.";
return ResponseEntity.ok(CommonSuccessResponse.response(true, findNotifies, message));
}
테스트
Converter 등록 및 Configuration 등록까지 마무리 했으니 정상적으로 동작되는지 확인하자.
Stirng으로 요청된 URI가 NotifyType으로 정상적으로 매핑이 되었다면, Controller내 내부로직을 모두 처리할 것이다.
만약 매핑자체에서 문제가 생긴다면 ExceptionHandler에 미리 정의해놓은 HttpStatus.BAD_REQUEST로 응답할 것으로 예상된다.
아래 테스트 코드를 확인하자.
@Test
@DisplayName("알림 컨트롤러 : 필터링된 회원 알림 조회 성공")
void 필터링_회원_알림_조회_성공() throws Exception {
// given
String uri = "/notify/qna"; // 프론트 요청은 String 값인 qna가 들어온다. Converter 적용이 된다면 qna -> NotifyType.QNA로 바뀌어 사용된다.
given(notifyService.searchNotifyList(any(), anyString(), anyLong(), anyInt())).willReturn(testNotifies());
// when
MockHttpServletResponse response = mvc.perform(get(uri)
.header("X-Authorization-Id", "receiver")
.param("notifyId", "1000")
.param("size", "3"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(true))
.andExpect(jsonPath("$.data.[0].notifyId").value(1))
.andExpect(jsonPath("$.data.[1].notifyId").value(2))
.andExpect(jsonPath("$.data.[0].notifyDetail").value("detail"))
.andExpect(jsonPath("$.data.[1].notifyDetail").value("detail2"))
.andExpect(jsonPath("$.data.[0].notifyType").value("QNA"))
.andExpect(jsonPath("$.data.[1].notifyType").value("QNA"))
.andExpect(jsonPath("$.message").exists())
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
}
String 값으로 요청이 되었지만 NotifyType으로 제대로 변환되어 내부 로직에서 사용 되었고, 그 결과 200 OK와 함께 데이터를 반환한 것을 확인할 수 있다.
📄 References
'Backend > Spring Boot' 카테고리의 다른 글
도메인 개발 팁 간단 정리 (0) | 2022.07.05 |
---|---|
RestTemplate (0) | 2022.07.01 |
Spring Cloud Config의 설정 파일 비대칭키로 암/복호화 (0) | 2022.06.03 |
RestControllerAdvice, ExceptionHandler를 이용한 전역 예외 처리 (0) | 2022.05.27 |
Spring Cloud Gateway를 이용한 서비스 라우팅 및 JWT 토큰 검증 (2) | 2022.05.24 |