이번 포스팅에서는 실무에 많이 사용되는
@RequestBody로 선언된 DTO의 LocalDateTime 활용시, 참고 할만한 정보를 소개합니다.
목차
먼저 제 피씨 환경은 아래와 같습니다.
- Springboot 3.1.4
- Java 17
- Kotlin 1.8.22
- Intellij
LocalDateTime 타입의 날짜 포맷 허용 범위는?
아래와 같은 데이터 클래스가 있습니다.
data class MemberRequestDto(
val id: Long?,
val name: String,
val email: String,
val age: Int,
val createdTime: LocalDateTime,
)
LocalDateTime의 Document를 봅시다.
A date-time without a time-zone in the ISO-8601 calendar system, such as 2007-12-03T10:15:30.
...
Timezone 없이 ISO-8601 캘린더 시스템. 주저리주저리...
ISO-8601라면 국제 표준의 날짜와 시간대이며
그렇다면 createdTime 값은 아래와 같은 패턴(Pattern)의 날짜 데이터를 셋팅 할 수있습니다.
- yyyy-MM-dd'T'HH:mm
- yyyy-MM-dd'T'HH:mm:ss
- yyyy-MM-dd'T'HH:mm:ss'Z'
- yyyy-MM-dd'T'HH:mm:ss.SSS
- yyyy-MM-dd'T'HH:mm:ss.SSSSSS
- yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS
예시를 들어보겠습니다.
- 2024-05-31T10:12:12
- 2024-05-31T10:12:12:55
- 2024-05-31T10:12:12:55Z
- 2024-05-31T10:12:12:55.221
- 2024-05-31T10:12:12:55.221122
- 2024-05-31T10:12:12:55.221122331
아래 데이터는 설정이 불가합니다.
- 2024-05-31 10:12:12 // 중간에 T가 빠짐
- 2024-05-31 T10:12:12:55+09:00 // 타임존이 포함
- 2024-05-31T10:12:12:55.2211223312 // 길이 초과
내부적으로 Jackson 라이브러리를 사용한다
Springboot로 애플리케이션을 개발할 때 기본적으로 사용하는
Json 파싱 라이브러리는 jackson입니다.
위 jackson 라이브러리 중 LocalDateTime 파싱에 사용되는 것은
jackson-datatype-jsr310 입니다.
jackson-datatype-jsr310은 Java 8 에 등장한 LocalDateTime를 쉽게 다룰수 있게 합니다.
스프링 버전이 높다면 크게 상관 없을 것이나
낮다면, 위 라이브러리를 별도로 추가해줘야합니다.
Error Handling은 어떻게?
@ControllerAdvice
class CustomExceptionHandler : ResponseEntityExceptionHandler() {
override fun handleHttpMessageNotReadable(
ex: HttpMessageNotReadableException,
headers: HttpHeaders,
status: HttpStatusCode,
request: WebRequest
): ResponseEntity<Any>? {
logger.error(ex.message, ex)
return ResponseEntity<Any>(ex.message, HttpStatus.BAD_REQUEST)
}
// ..(생략)
}
위와 같이 에러 핸들링을 위한 코드를 작성해봅니다.
이 후 잘못된 데이터를 넘기게 되면 400에러와 함께 아래 에러메시지가 응답될 것입니다.
JSON parse error: Cannot deserialize value of type `java.time.LocalDateTime`
from String "2022-10-12T12:12:2": Failed to deserialize java.time.LocalDateTime:
(java.time.format.DateTimeParseException) Text '2022-10-12T12:12:2' could not be parsed,
unparsed text found at index 16
@JsonFormat으로 날짜 패턴 정의하기
data class MemberRequestDto(
val id: Long?,
val name: String,
val email: String,
val age: Int,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'")
val createdTime: LocalDateTime,
)
@JsonFormat을 사용하면 날짜 패턴을 지정할수 있습니다.
위처럼 하게되면 'Z'가 없을 때 에러를 반환하게 됩니다.
만약 실무에서 엄격한 인터페이스로 날짜 패턴을 정의해야한다면
위 같이 @JsonFormat를 사용해볼 수 있을 겁니다.
ConstraintValidator 정의하여 유효성 검사
아래와 같이 Annotation + Validator 구현을 해볼 수 있습니다.
data class MemberRequestDto(
val id: Long?,
@field:NotBlank(message = "name cannot be blank")
val name: String,
@field:Email(message = "email is invalid")
val email: String,
// ..(생략)
)
위처럼 유효성 검사를 하기 위해서는
jakarta.validation.constraints 라이브러리가 필요합니다.
아래와 같이 build.gradle.kts에 의존성을 추가해줍니다.
dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation") // jakarta.validation 포함됨
// ..(생략)
}
그리고 커스텀 validator를 구현해봅시다.
import jakarta.validation.ConstraintValidator
import jakarta.validation.ConstraintValidatorContext
import java.time.DateTimeException
import java.time.LocalDateTime
class ReservationTimeValidator : ConstraintValidator<ReservationTimeChecker, LocalDateTime> {
override fun isValid(value: LocalDateTime?, context: ConstraintValidatorContext?): Boolean {
// Todo: check validation
if (value == null) return false
return try {
return value.isAfter(LocalDateTime.now())
} catch (e: DateTimeException) {
false
}
}
}
위 기능은 입력된 날짜 값이
현재 시간(LocalDateTime.now) 이 후의 날짜/시간 인지를 검사하는 기능의 Validator입니다.
원하는 요구에 따라 입맛에 맛게 작성하심 될겁니다.
어노테이션도 만들어 줍니다.
import jakarta.validation.Constraint
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [ReservationTimeValidator::class])
annotation class ReservationTimeChecker(
val message: String = "default message",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Any>> = []
)
사용은 아래와 같이 하면 됩니다.
data class MemberRequestDto(
// ..(중략)
@field:ReservationTimeChecker("reservation time cannot be past")
val reservationTime: LocalDateTime,
)
ConstraintValidator 정의 시, 에러 핸들링
만약 커스텀 Validator를 구현하였다면
아래와 같이 에러 핸들러에 메서드를 추가해줘야 합니다.
@ControllerAdvice
class CustomExceptionHandler : ResponseEntityExceptionHandler() {
override fun handleMethodArgumentNotValid(
ex: MethodArgumentNotValidException,
headers: HttpHeaders,
status: HttpStatusCode,
request: WebRequest
): ResponseEntity<Any>? {
logger.error(ex.message, ex)
val message = ex.fieldErrors.firstNotNullOf { it.defaultMessage }
return ResponseEntity<Any>(message, HttpStatus.BAD_REQUEST)
}
// .. (생략)
}
유효하지 않은 값으로 테스트해봅시다.
에러 핸들러에서 정상적으로 캐치하고
원하는 에러메시지를 출력할 수 있습니다.
'개발 이야기 > Springboot' 카테고리의 다른 글
Springboot 에서 Gzip으로 압축(with Kotlin) (0) | 2024.07.06 |
---|---|
AWS S3 타계정의 버킷 업로드 시 BucketOwnerFullControl 설정 (0) | 2024.06.06 |
JdbcTemplate로 batchUpdate 사용해보기 (0) | 2024.05.28 |
Springbatch에서 메타테이블 없이 실행(Springbatch 5) (0) | 2024.03.19 |
Multi-module에서 Domain 분리하기 (0) | 2024.02.21 |