feat: LocalDateTime / OffsetDateTime을 ISO 8601 형식으로 직렬화하는 ObjectMapper 모듈 추가

This commit is contained in:
이상진 2025-08-18 15:40:49 +09:00
parent 2e2b71743f
commit 477415b3ba
2 changed files with 82 additions and 23 deletions

View File

@ -14,16 +14,21 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import roomescape.common.exception.CommonErrorCode import roomescape.common.exception.CommonErrorCode
import roomescape.common.exception.RoomescapeException import roomescape.common.exception.RoomescapeException
import java.time.LocalDate import java.time.*
import java.time.LocalTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@Configuration @Configuration
class JacksonConfig { class JacksonConfig {
companion object {
private val ISO_OFFSET_DATE_TIME_FORMATTER: DateTimeFormatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX")
}
@Bean @Bean
fun objectMapper(): ObjectMapper = ObjectMapper() fun objectMapper(): ObjectMapper = ObjectMapper()
.registerModule(javaTimeModule()) .registerModule(javaTimeModule())
.registerModule(dateTimeModule())
.registerModule(kotlinModule()) .registerModule(kotlinModule())
.registerModule(longIdModule()) .registerModule(longIdModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
@ -52,28 +57,59 @@ class JacksonConfig {
simpleModule.addDeserializer(Long::class.java, StringToLongDeserializer()) simpleModule.addDeserializer(Long::class.java, StringToLongDeserializer())
return simpleModule return simpleModule
} }
}
class LongToStringSerializer : JsonSerializer<Long>() { private fun dateTimeModule(): SimpleModule {
override fun serialize(value: Long?, gen: JsonGenerator, serializers: SerializerProvider) { val simpleModule = SimpleModule()
if (value == null) { simpleModule.addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer())
gen.writeNull() simpleModule.addSerializer(OffsetDateTime::class.java, OffsetDateTimeSerializer())
} else { return simpleModule
gen.writeString(value.toString()) }
}
} class LongToStringSerializer : JsonSerializer<Long>() {
} override fun serialize(value: Long?, gen: JsonGenerator, serializers: SerializerProvider) {
if (value == null) {
class StringToLongDeserializer : JsonDeserializer<Long>() { gen.writeNull()
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Long? { } else {
val text = p.text gen.writeString(value.toString())
if (text.isNullOrBlank()) { }
return null }
} }
return try {
text.toLong() class StringToLongDeserializer : JsonDeserializer<Long>() {
} catch (_: NumberFormatException) { override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Long? {
throw RoomescapeException(CommonErrorCode.INVALID_INPUT_VALUE) val text = p.text
if (text.isNullOrBlank()) {
return null
}
return try {
text.toLong()
} catch (_: NumberFormatException) {
throw RoomescapeException(CommonErrorCode.INVALID_INPUT_VALUE)
}
}
}
class LocalDateTimeSerializer : JsonSerializer<LocalDateTime>() {
override fun serialize(
value: LocalDateTime,
gen: JsonGenerator,
serializers: SerializerProvider
) {
value.atZone(ZoneId.systemDefault())
.toOffsetDateTime()
.also {
gen.writeString(it.format(ISO_OFFSET_DATE_TIME_FORMATTER))
}
}
}
class OffsetDateTimeSerializer : JsonSerializer<OffsetDateTime>() {
override fun serialize(
value: OffsetDateTime,
gen: JsonGenerator,
serializers: SerializerProvider
) {
gen.writeString(value.format(ISO_OFFSET_DATE_TIME_FORMATTER))
} }
} }
} }

View File

@ -7,7 +7,10 @@ import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldContain
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
class JacksonConfigTest( class JacksonConfigTest(
private val objectMapper: ObjectMapper = JacksonConfig().objectMapper() private val objectMapper: ObjectMapper = JacksonConfig().objectMapper()
@ -66,4 +69,24 @@ class JacksonConfigTest(
deserialized shouldBe number deserialized shouldBe number
} }
} }
context("OffsetDateTime은 ISO 8601 형식으로 직렬화된다.") {
val date = LocalDate.of(2025, 7, 14)
val time = LocalTime.of(12, 30, 0)
val dateTime = OffsetDateTime.of(date, time, ZoneOffset.ofHours(9))
val serialized: String = objectMapper.writeValueAsString(dateTime)
test("OffsetDateTime 직렬화") {
serialized shouldBe "\"2025-07-14T12:30:00+09:00\""
}
}
context("LocalDateTime은 ISO 8601 형식으로 직렬화된다.") {
val dateTime = LocalDateTime.of(2025, 7, 14, 12, 30, 0)
val serialized: String = objectMapper.writeValueAsString(dateTime)
test("LocalDateTime 직렬화") {
serialized shouldBe "\"2025-07-14T12:30:00+09:00\""
}
}
}) })