WIL/스터디

TIMESTAMP vs DATETIME, 그리고 2038년 문제까지 한 번에 정리하기

아크리미츠 2025. 11. 14. 19:10

서비스에 만료일(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_attimestamp 로 찍혀 있으면, 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.TIMESTAMPLocalDateTime 이라고 해서
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