Blog

ObjectMapper가 LocalTime을 JSON으로 변환하지 못할 때

요청을 보내기 전에 변환이 실패했습니다

동물병원 운영 시간을 등록하는 MockMvc 테스트를 작성하고 있었습니다. 요일별로 시작 시간과 종료 시간을 넣은 요청 DTO를 만들고 JSON으로 변환해 보내는 코드입니다.

Java
void createOperatingTime() throws Exception {  
//given  
  
AnimalHospital savedAnimalHospital = getSavedAnimalHospital();  
  
List<HospitalOperationTimeCreateRequest> requests = new ArrayList<>();  
  
for (DayOfWeekStatus value : DayOfWeekStatus.values()) {  
	HospitalOperationTimeCreateRequest hospitalOperationTimeCreateRequest = HospitalOperationTimeCreateRequest.builder()  
		.dayOfWeekStatus(value.name())  
		.startTime(LocalTime.of(11, 0))  
		.endTime(LocalTime.of(21, 0))  
		.isDayOff(false)  
		.build();  
	requests.add(hospitalOperationTimeCreateRequest);  
}  
  
//when  
ResultActions resultActions = mockMvc.perform(post("/...", savedAnimalHospital.getId())  
	.contentType(MediaType.APPLICATION_JSON)  
	.characterEncoding(StandardCharsets.UTF_8)  
	.content(toJson(requests))  
);  
  
//then  
resultActions.andExpect(status().isNoContent())  
	.andDo(print());  
}

HTTP 응답을 확인하는 단계까지 가지도 못하고 toJson(requests)에서 문제가 생겼습니다. DTO에는 LocalTime 필드가 있었습니다.

Java
public record HospitalOperationTimeCreateRequest(  
  
@JsonProperty("day_of_week_status")  
String dayOfWeekStatus,  
  
@JsonProperty("start_time")  
LocalTime startTime,  
  
@JsonProperty("end_time")  
LocalTime endTime,  
  
@JsonProperty("is_day_off")  
Boolean isDayOff  
) {  
	@Builder  
	public HospitalOperationTimeCreateRequest {  
	}  
}

당시 테스트 유틸리티에서는 ObjectMapper를 직접 생성해 사용했습니다.

Java
public class TestUtils {  
  
private static final ObjectMapper mapper = new ObjectMapper();  
  
public static String toJson(Object obj) {  
	try {  
		return mapper.writeValueAsString(obj);  
	} catch (JsonProcessingException e) {  
		throw new IllegalStateException("[JsonUtils] -> toJson error\n");  
		}  
	}  
}

문자열 같은 값과 달리 LocalTime을 변환하지 못했습니다. 당시 참고한 글을 보고 날짜·시간 타입을 처리하는 모듈을 확인했습니다.

당시에는 LocalTime이 “표준 Java 라이브러리의 일부가 아니다”라고 생각했습니다. 그런데 java.time은 Java 8에 도입된 표준 API입니다. 이 사례에서 Java 표준 여부는 상관이 없었습니다. 직접 만든 Jackson 2 ObjectMapper에 해당 타입 지원이 등록되어 있지 않은 게 원인이었습니다.

JavaTimeModule을 등록했습니다

registerModule()은 Spring 빈을 등록하는 코드가 아니라 mapper에 Jackson 모듈을 등록하는 코드입니다. 아래 예시는 호출마다 등록하는 당시 코드입니다. 일반적으로 mapper 설정은 사용 전에 끝냅니다. 관련 설명은 Jackson Java 8 모듈 문서와 Java LocalTime API에 있습니다.

Java
public class TestUtils {  
  
private static final ObjectMapper mapper = new ObjectMapper();  
  
public static String toJson(Object obj) {  
	try {  
		// 추가
		mapper.registerModule(new JavaTimeModule());  
		return mapper.writeValueAsString(obj);  
	} catch (JsonProcessingException e) {  
		throw new IllegalStateException("[JsonUtils] -> toJson error\n");  
		}  
	}  
}

직접 만든 mapper에 JavaTimeModule을 등록한 뒤 테스트를 진행할 수 있었습니다.