서비스에 만료일(expire_at) 같은 컬럼을 설계하다 보면, 언젠가 이런 에러를 한 번쯤 보게 된다.
SQL Error [1292] [22001]: Data truncation: Incorrect datetime value: '9999-12-31 11:59:59' for column 'expire_at' at row 1
값 자체는 멀쩡해 보이는데, MySQL이 Incorrect datetime value 라면서 거부하는 상황이다.
- 왜 이런 에러가 나는지
- TIMESTAMP와 DATETIME의 차이가 뭔지
- 왜 TIMESTAMP는 2038년까지만 되는지
- 스프링 / JPA에서
LocalDateTime,Timestamp랑 어떻게 매핑되는지 DEFAULT CURRENT_TIMESTAMP를 DATETIME에 써도 되는지- 에러 메시지에 나오는
datetime텍스트의 진짜 의미
까지 한 번에 정리해본다.
1. 에러 로그부터 뜯어보기
문제 상황은 보통 이런 식이다.
- 컬럼 이름:
expire_at - 자바 / 스프링 코드에서
LocalDateTime또는Timestamp사용 - DB에는 대충 이런 값 넣으려고 한다:
'9999-12-31 11:59:59'
그리고 에러가 터진다.
Incorrect datetime value: '9999-12-31 11:59:59' for column 'expire_at'
여기서 중요한 포인트는:
이 에러는 값 형식이 이상해서가 아니라, 컬럼 타입 + 값 범위가 안 맞아서 나는 경우가 많다는 점이다.
그리고, 에러에 보이는 datetime 이 “MySQL DATETIME 타입이다”라는 뜻은 전혀 아니다
그냥 “날짜·시간(datetime) 값 파싱하다가 터졌다”는 고정 문구일 뿐이다.
실제 타입은 반드시 이렇게 따로 확인해야 한다.
SHOW CREATE TABLE your_table\G
-- 또는
DESC your_table;
여기서 expire_at 이 timestamp 로 찍혀 있으면, 9999-12-31 11:59:59 같은 값은 전부 범위 밖이라서 에러가 난다.
2. TIMESTAMP vs DATETIME: 진짜 차이
MySQL에서 헷갈리기 좋은 두 타입이 있다.
TIMESTAMP
- 내부적으로 Unix time(epoch time)에 가깝게 동작한다.
- “1970-01-01 00:00:00 UTC 기준, 몇 초 지났는지”를 정수로 표현한다고 보면 된다.
- 이 값이 전통적으로 4바이트(signed 32bit) 정수로 취급된다.
- 그래서 표현 가능한 범위가 제한적이다.
대략:
- 최소:
1970-01-01 00:00:01 - 최대:
2038-01-19 03:14:07근처까지
이 이후의 값은 오버플로우 때문에 표현할 수 없다.
DATETIME
- “연, 월, 일, 시, 분, 초”를 필드 단위로 저장하는 타입이다.
- Unix time 같은 1970 기준 초 카운터가 아니다.
- 그래서 범위가 훨씬 넓다.
범위는 대략:
1000-01-01 00:00:00- ~
9999-12-31 23:59:59
즉, '9999-12-31 11:59:59' 는 DATETIME 기준으로는 완전 정상적인 값이다.
문제가 되는 건 그 컬럼이 TIMESTAMP일 때뿐이다.
3. 왜 TIMESTAMP는 2038년까지만 되나? (Y2038 문제)
“2038년 문제(Y2038)” 라는 말을 들어본 적이 있을 수 있다.
핵심만 말하면:
1970년 이후 초 카운터를 4바이트(signed 32bit) 정수로 저장하는 오래된 유닉스 설계 때문에 한계가 2038년에 온다는 이야기다.
32비트 signed 정수 최대값은 2,147,483,647 이다.
이 숫자를 “1970년 이후 지난 초”라고 해석하면:
1970-01-01 00:00:00 UTC + 2,147,483,647초
≈ 2038-01-19 03:14:07 UTC
그래서:
- 이 시점까지만 표현 가능하다.
- 그 이후는 overflow가 나서 음수처럼 보이거나 엉뚱한 값이 된다.
MySQL의 TIMESTAMP도 이 유닉스 타임 전통을 따라와서
범위가 이 구간으로 묶이는 것이다.
반대로, DATETIME은 “내부 설계 자체”가 다르기 때문에
이 제약을 안 받는다.
4. 스프링 / JPA에서의 TIMESTAMP 표기와 오해
스프링 / JPA 코드에서 흔히 이런 걸 본다.
@Temporal(TemporalType.TIMESTAMP)
private Date createdAt;
혹은 요즘엔 이런 식이다.
private LocalDateTime expireAt;
여기서 TemporalType.TIMESTAMP 나 LocalDateTime 이라고 해서
DB 타입이 반드시 MySQL TIMESTAMP가 되어야 한다는 뜻은 아니다
JPA 입장에서 TIMESTAMP 는 그냥
“날짜 + 시간까지 다 쓰는 필드다”
라는 의미에 가깝다.
그래서 다음 조합들은 모두 가능하다.
- 엔티티:
LocalDateTime/ DB:DATETIME - 엔티티:
LocalDateTime/ DB:TIMESTAMP - 엔티티:
Timestamp/ DB:DATETIME - 엔티티:
Timestamp/ DB:TIMESTAMP
실제 물리 타입은:
- JPA가 스키마를 생성하는 경우 → Dialect에 따라 결정된다.
- 이미 존재하는 테이블에 붙는 경우 → DB 스키마 그대로 쓴다.
만약 Hibernate가 마음대로 TIMESTAMP로 컬럼을 만들지 못하게 하고 싶다면,
엔티티에 직접 선언해줄 수 있다.
@Column(name = "expire_at", columnDefinition = "DATETIME")
private LocalDateTime expireAt;
이렇게 쓰면 DB에서도 DATETIME으로 유지된다.
5. DATETIME에도 DEFAULT CURRENT_TIMESTAMP 써도 되나?
많이들 이렇게 쓰다가 헷갈린다.
expire_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
이걸 DATETIME으로 바꾸고 싶어서 다음처럼 고민한다.
expire_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; -- 이거 되나?
결론부터 말하면:
MySQL 5.6.5 이후라면, DATETIME에도
DEFAULT CURRENT_TIMESTAMP가 정상적으로 동작한다.
즉:
- 컬럼 타입이 TIMESTAMP가 아니어도 된다.
- DATETIME +
DEFAULT CURRENT_TIMESTAMP조합은 지원된다.
단, DB 기본값이 언제 적용되는지는 이해하고 있어야 한다.
DB DEFAULT가 동작하는 조건
“INSERT 문에서 그 컬럼을 아예 안 넣었을 때”만 기본값이 적용된다.
NULL 이라고 명시해서 넣으면 기본값이 아니라 진짜 NULL로 저장하려고 한다.
그래서 JPA에서 “DB 기본값을 믿고 쓰고 싶다”면 이런 패턴을 많이 쓴다.
@Column(name = "created_at", insertable = false, updatable = false)
private LocalDateTime createdAt;
이렇게 하면:
- INSERT 시
created_at컬럼이 SQL에 포함되지 않는다. - DB가
DEFAULT CURRENT_TIMESTAMP로 값을 채운다. - 이후 SELECT 하면 그 값이 엔티티에 매핑된다.
6. 정리
Incorrect datetime value에러는 값 형식이 틀리거나, 타입이 허용하는 범위를 벗어났을 때 발생한다.- MySQL TIMESTAMP 의 범위는 1970~2038년이다.
'9999-12-31 11:59:59'같은 값은 이 범위를 완전히 벗어난다. - MySQL DATETIME 은 1000~9999년까지 표현 가능하다.
- 스프링 / JPA에서
LocalDateTime,@Temporal(TemporalType.TIMESTAMP)라고 해서 DB에서 꼭 TIMESTAMP 타입을 써야 한다는 뜻은 아니다. - DATETIME 컬럼에도
DEFAULT CURRENT_TIMESTAMP를 사용할 수 있다. - 에러 메시지에 나오는
datetime이라는 단어는 타입 이름이 아니라, 그냥 “날짜·시간 값”이라는 범용 표현일 뿐이다. - 실무에서
expire_at같은 만료일 컬럼은:- DB 타입: DATETIME
- 엔티티:
LocalDateTime - 무제한 표현은
NULL또는 sentinel 값(예:9999-12-31 11:59:59)으로 처리하는 패턴이 가장 깔끔하다.
이제 “왜 9999년 넣었는데 TIMESTAMP에서 터지냐”,
“에러 메시지에 datetime이라고 써 있길래 DATETIME인 줄 알았다” 같은 헷갈림은 안 할 수 있을 것이다.
'WIL > 스터디' 카테고리의 다른 글
| CPU와 GPU의 차이와 최근 동향 (2) | 2024.10.23 |
|---|---|
| Session, JWT, OAuth 개념 (1) | 2024.09.22 |
| PintOS 메모리 할당 순서 (0) | 2024.09.10 |
| PintOS의 메모리에 관한 공부 (0) | 2024.09.10 |
| OS 지식 (0) | 2024.08.27 |