스프링 애플리케이션을 개발하다 보면, 클라이언트로부터 날짜/시간을 파라미터나 JSON으로 입력받거나, 다시 응답으로 내려주는 경우가 자주 있습니다.
이때 Spring MVC와 Jackson(스프링에서 기본적으로 사용하는 JSON 직렬화 라이브러리)에서 제공하는 어노테이션인
- @DateTimeFormat
- @JsonFormat
을 활용하면, 날짜/시간을 특정 포맷으로 편리하게 다룰 수 있습니다.
그렇지만 둘의 역할이 미묘하게 달라 의도치 않은 혼란이 생길 수도 있습니다.
1. 예시 코드
1.1. 컨트롤러
@RestController
@RequestMapping("/date")
public class DateFormatController {
@GetMapping("/body")
public DateDto getDateBody(@RequestBody DateDto dateDto) {
System.out.println(dateDto);
return dateDto;
}
@GetMapping("/query")
public DateDto getDateQuery(@ModelAttribute DateDto dateDto) {
System.out.println(dateDto);
return dateDto;
}
}
1.2. DTO
@Data
@NoArgsConstructor
public class DateDto {
private String data;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime localDateTime;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate localDate;
}
여기서 @DateTimeFormat와 @JsonFormat을 같이 사용하면 에러가 발생해 둘중 1개만 사용!!
2. @DateTimeFormat은 언제 쓰는 걸까?
Spring MVC(특히 Controller 레벨)에서 쿼리 파라미터, PathVariable, Form 데이터 등으로 들어오는 문자열을
Java의 날짜 타입(LocalDate, LocalDateTime 등)으로 변환할 때 쓰입니다.
예시)
GET /date/query?data=test&localDateTime=2025-01-01 10:05:30&localDate=2025-01-01
와 같은 쿼리 파라미터를 보내면
- localDateTime -> LocalDateTime("2025-01-01T10:10:10")
- localDate -> LocalDate("2025-01-01")
로 매핑됩니다.
중요: @DateTimeFormat은 JSON 직렬화/역직렬화 관점에서는 관여하지 않습니다.
즉, JSON Body로 요청을 받을 때(예: @RequestBody),
@DateTimeFormat만으로는 포맷 지정이 안 될 수 있으며, Jackson의 내부 기본 전략에 따라 처리됩니다.
3. @JsonFormat은 언제 쓰는 걸까?
Jackson(스프링에서 JSON 직렬화에 사용)에 의해 JSON -> 객체 또는 객체 -> JSON 변환 시,
날짜/시간 포맷을 지정하고 싶을 때 사용합니다.
예시)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime localDateTime;
라고 하면,
- 클라이언트에서 {"localDateTime": "2025-01-01 10:05:30"}라는 JSON을 전송할 때 Jackson이 해당 문자열을 LocalDateTime으로 파싱
- 서버에서 응답을 보낼 때도 "yyyy-MM-dd HH:mm:ss" 포맷의 문자열로 직렬화
하지만 스프링 MVC의 쿼리 파라미터 바인딩과는 무관하며, 오직 JSON 변환에만 관여합니다.
4. 그럼 동시에 쓰면 어떻게 될까?
예를 들어,
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime localDateTime;
와 같이 동일 필드에 @DateTimeFormat과 @JsonFormat을 함께 지정하는 경우,
- 내부 매핑 로직에서 충돌이 발생하거나
- Jackson이 자기 방식대로 포맷을 처리하는 등
에러를 일으킬 수 있습니다.
특히 Spring Boot 버전, Jackson 버전 등에 따라 처리가 다르게 동작하기도 하고,
@RequestBody로 JSON을 받을 때와 @ModelAttribute로 파라미터를 받을 때 적용 경로가 달라지므로,
예기치 않은 문제가 생길 가능성이 높습니다.
따라서 동일 필드에는 보통 한 가지 어노테이션만 적용하는 게 안전하며,
JSON 전용이면 @JsonFormat, 쿼리 파라미터 전용이면 @DateTimeFormat 을 사용합니다.
5. 마무리: ZonedDateTime, 타임존까지 고려해야 한다면?
만약 날짜/시간을 다룰 때 특정 타임존(예: Asia/Seoul)이 필요한 경우,
- @JsonFormat(timezone = "Asia/Seoul")
- ZonedDateTime / OffsetDateTime 사용
등을 통해 더 세밀하게 핸들링할 수 있습니다.
다만 본 포스팅에서는 자세히 다루지 않았고, 프로젝트 요구사항에 따라 UTC 고정, 또는 사용자별 타임존 지원 등을 설계해야 합니다.
6. 정리
- @DateTimeFormat:
- Spring MVC(GET 파라미터, PathVariable, Form 데이터)에서 문자열 -> 날짜 타입 변환
- JSON 직렬화/역직렬화에는 영향을 주지 않음
- @JsonFormat:
- Jackson에서 JSON (역)직렬화 시 날짜 포맷 지정
- 쿼리 파라미터 처리에는 관여하지 않음
동일 필드에 두 어노테이션을 동시에 달면 충돌이나 에러가 발생할 수 있으니 주의!
최종적으로, 어떤 방식으로 날짜가 오고 가는지(쿼리 vs JSON) 구분하여 어노테이션을 적용하면,
안정적으로 날짜/시간 변환을 처리할 수 있습니다.