Spring Data JDBC에서 LocalDate·LocalDateTime을 커스텀 컨버터로 처리하려 했는데, LocalDateTime은 되고 LocalDate만 Oracle 기본 DATE 형식으로 저장되며 어긋난 원인을 추적한 디버깅 회고다. 범인은 Spring JDBC 내부의 StatementCreatorUtils.setParameterValue()가 Java 타입별로 다른 경로를 타며 LocalDate를 먼저 가로채 커스텀 컨버터를 무시한 것이었다. LocalDate를 감싼 CustomLocalDate 래퍼 클래스로 우회해 해결했다.
핵심 포인트- 같은 방식으로 등록한 커스텀 컨버터인데 LocalDateTime은 동작하고 LocalDate만 Oracle DATE 형식으로 저장되는 불일치가 문제였다.
- StatementCreatorUtils.setParameterValue()가 instanceof로 타입을 먼저 판별해, LocalDate는 커스텀 컨버터를 거치지 않고 ps.setDate()로 바로 처리됐다.
- setDate·setTimestamp·setString은 JDBC 드라이버에 서로 다른 타입 힌트를 줘, setDate는 VARCHAR 컬럼에서 오류를, setTimestamp는 Oracle의 자동 문자열 변환으로 우연히 동작했다.
- LocalDateTime이 컨버터를 탄 것처럼 보인 건 실제로는 Oracle JDBC 드라이버의 TIMESTAMP에서 VARCHAR 자동 변환 덕분이지 컨버터가 동작한 게 아니었다.
- 최종 해결은 LocalDate를 감싼 CustomLocalDate 래퍼로, Spring JDBC가 특별 취급하지 않아 커스텀 컨버터가 정상 적용된다.
상세 정리- 문제 상황: Spring Data JDBC에 LocalDate·LocalDateTime용 커스텀 컨버터를 각각 등록했는데 LocalDate만 의도한 저장이 안 되고 Oracle 기본 DATE로 들어갔다.
- 1차 오해: LocalDateTime은 잘 되니 컨버터 설정 자체는 맞다고 보고 LocalDate 쪽 설정만 의심했으나 원인이 아니었다.
- 근본 원인: Spring JDBC의 StatementCreatorUtils.setParameterValue()가 값의 Java 타입에 따라 분기하는데, LocalDate가 instanceof 체크에서 먼저 걸려 컨버터를 건너뛰고 ps.setDate()를 호출했다.
- 타입 힌트 차이: setDate()는 DATE 타입 의미라 Oracle이 DATE로 변환하려다 VARCHAR 컬럼에서 오류가 났다.
- setTimestamp의 착시: LocalDateTime은 setTimestamp() 경로를 타고 Oracle이 TIMESTAMP를 문자열로 자동 변환해줘 결과적으로 저장돼, 컨버터가 동작한 것처럼 보였을 뿐이다.
- 시도1 @DateTimeFormat: 웹 요청 파라미터 바인딩용 어노테이션이라 JDBC 저장 경로에는 효과가 없었다.
- 시도2 JdbcTemplate 직접 사용: 가능은 하지만 Spring Data JDBC의 편의성을 포기해야 해 부적절했다.
- 시도3 String 필드 변환: Entity 필드를 String으로 두고 변환 메서드를 붙이면 엔티티의 의미가 모호해졌다.
- 최종 해결: data class CustomLocalDate(val value: LocalDate) 래퍼를 만들고 WritingConverter·ReadingConverter를 쌍으로 등록했다.
- 동작 원리: Spring JDBC는 CustomLocalDate를 내장 타입으로 특별 취급하지 않으므로 setParameterValue의 우선순위 분기를 우회해 커스텀 컨버터가 적용된다.
- 이점: Spring JDBC 우선순위 우회, 타입 안정성, Spring Data JDBC 호환 유지, 확장 가능성, 명시적 의도 전달을 얻는다.
- 실무 주의: 읽기·쓰기 컨버터를 반드시 쌍으로 등록하고, 테스트 코드로 실제 저장 형식을 확인하며, DB 벤더별 타입 변환 차이를 고려해야 한다.
왜 읽나Spring Data JDBC 커스텀 컨버터가 특정 타입에서만 무시돼 원인 모를 저장 오류를 겪는 백엔드 개발자에게 내부 동작과 우회법을 짚어주는 레퍼런스.