MySQL 의 Timestamp 타입은 Not Null 로 지정됐을 때 CURRENTTIME을 DEFAULT로 가지게 되고, 그 중에서도 첫번째 Timestamp 칼럼은 ON UPDATE CURRENT_TIMESTAM 속성도 가지게 됩니다. 이 두 가지는 둘 다 수정하지 못하고 강제로 고정됩니다.
해결하기 위해선 NOT NULL 을 해제하고 어플리케이션 차원에서 제어하거나, explicit_defaults_for_timestamp 시스템 변수를 활성화합니다.
나는 이렇게 생각하곤 했습니다.
RDBMS는 SQL 문법도 거의 동일하고, 이마저도 ORM을 통해서 표준화해서 사용하는 것이 보편적인 흐름이 되었으므로, 각 DB 간의 차이는 적거나 크게 신경 쓸 부분이 아닌 것 아닌가?
그러나 때때로 데이터베이스는 각자 독특한 사양을 지니기도 합니다. 가장 자주 사용되는 DB 중 하나인 MySQL인데도 모르던 사양이 있어 이 부분을 소개합니다.
여느 때처럼 데이터베이스를 구성하고 개발을 진행하는데 운영측에서 긴급히 연락이 왔습니다. 어떤 TIMESTAMP 필드가 주기적으로 현재 시간으로 변경된다는 것입니다. 모든 행이 변하는 것은 아닌데 수많은 행에 진행이 되었습니다.
당연하지만 프로그램 코드에는 "주기적으로 데이터베이스의 모든 행을 현재 시간으로 설정하는 코드" 같은 내용은 담겨있지 않습니다. 따라서 저는 이 문제가 "데이터베이스 사양"에서 발생하는 문제라고 어느 정도 확신을 가졌습니다.
이 때 왜 지금까지 겪지 못했나 싶은 경험을 하게 되는데, 그것은 바로 MySQL의 Timestamp 사양입니다. MySQL의 Timestap 타입은 아주 특별한 특징이 있는데 다음과 같습니다.
이 사실을 모르고 있던 저는 "테이블에 넣은 적도 없는 디폴트 값이 들어가있고 변경하려고 시도하면 롤백되는" 상황을 맞이하게 됩니다.
근데, 그럼 행들은 왜 update 가 된걸까요? 업데이트를 실행한 적은 없는데?
이 문제의 해결을 하기 위해 다음과 같은 방식으로 문제를 추적했습니다.
ON UPDATE CURRENT_TIMESTAMP 덕분에, 해당 업데이트가 실행된 시각을 알 수 있었습니다. 배포 공지 (슬랙 메시지)와 깃 커밋 시각을 토대로, 해당 업데이트는 "배포 시점"에 다량으로 발생했음을 확인했습니다.
배포 과정에서 데이터베이스에 영향을 줄만한 일은 mvn test (단위 테스트)와 어플리케이션 재실행입니다. 그러나 어플리케이션 재실행은 배포 이외에도 자주 일어나기 때문에 (로컬 실행 등) mvn test 에 의해서 해당 행들이 업데이트가 되었다고 봄이 타당합니다.
JUnit을 이용한 단위 테스트들을 확인해본 결과 Transaction 및 롤백 처리가 되어있습니다. Transaction을 사용할 수 없는 통합 테스트들도 존재했으나, 이 테스트들이 INSERT, UPDATE 하는 행들은 기존의 데이터에 영향을 미치지 않도록 설계 되어있었습니다. (테스트에 필요한 데이터를 생성하고 테스트 이후에 삭제)
문제가 발생한 테이블에 접근하는 서비스 메소드들을 찾아보며 트랜잭션에서 벗어날만한 부분이 있는지 우선적으로 조사했습니다. 조사 결과 서비스 중에 '낙관적 락'을 사용하는 메소드를 발견합니다.
낙관적 락은 엔티티에 VERSION을 부여하여, 동시에 한 행에 대한 여러 개의 접근으로 인한 충돌을 방지합니다. 달리말하면 엔티티를 불러오는 것만으로도 버전 상승으로 인한 UPDATE 쿼리가 실행될 수 있습니다.
즉, 단위 테스트로 인해 SELECT 쿼리가 실행되었고, SELECT 쿼리는 낙관적 락을 트리거해 UPDATE 쿼리를 실행했고, 이 업데이트 쿼리가 데이터베이스의 ON UPDATE 를 트리거했으며, 이 ON_UPDATE는 트랜잭션 외(!) 에서 실행이 되어 데이터에 영향을 미쳤다는 이야기가 됩니다.
jUnit 트랜잭션은 분리가 되어있고 롤백 처리도 되어있을텐데 어째서 ON UPDATE 가 트리거되었는지 아직 정확히 알지 못합니다. 이 부분은 근 시일 내에 테스트를 진행해서 검증해볼까 합니다.
단순한 방법으로 해결하기로 합니다.
해당 칼럼에서 Not Null을 해제합니다. Null 여부가 중요한 칼럼일 경우, 어플리케이션 차원에서 이를 체크하도록 합니다.
explicit_defaults_for_timestamp 시스템 변수를 수정해서 해결하는 방법도 있습니다. 이게 더 적절한 방법이고 접근일 수 있으나 다음의 이유로 택하지 않기로 합니다.
어플리케이션은 ORM을 사용하고 있고, 가능하면 코드 이외의 부분에서 데이터베이스 정의를 늘리고 싶지 않습니다. 현재 프로젝트의 메인 프로그래머긴 하지만, 근 시일내에 타 프로그래머에게 인수인계 예정이며 사내에는 별도의 서버 엔지니어도 있습니다. '코드에 작성되지 않은 알아야 하는 부분'을 늘리는 것은 바람직하지 않다고 판단됩니다.
https://dev.mysql.com/doc/refman/8.0/en/timestamp-initialization.html