포스트

2023-11-08-TIL

Today I Learned

Cron, Crontab

cron은 Unix 계열에서 정해진 시각에 명령을 실행하는 데몬이고, crontab은 그 일정표 파일이다. crontab(5) 매뉴얼에 따르면 한 줄은 분, 시, 일, 월, 요일 다섯 칸과 실행할 명령으로 이루어진다. 요일은 0과 7이 모두 일요일이다.

헷갈리기 쉬운 규칙이 하나 있다. 일과 요일을 둘 다 *가 아닌 값으로 제한하면, 둘 중 하나만 맞아도 실행된다. 매뉴얼의 예인 30 4 1,15 * 5는 매월 1일과 15일 4시 30분에 더해 매주 금요일에도 실행된다. “1일이면서 금요일”이 아니다.

Spring의 @Scheduled(cron = ...)은 형식이 조금 다르다. Spring 레퍼런스는 초, 분, 시, 일, 월, 요일 여섯 칸을 쓴다. crontab 식을 그대로 옮기면 한 칸씩 밀리므로 맨 앞에 초를 붙여야 한다.

1
2
3
4
5
# crontab: 매일 새벽 3시
0 3 * * *

# Spring @Scheduled: 매일 새벽 3시
0 0 3 * * *

Spring 5.3부터는 CronExpression 클래스가 기존 CronSequenceGenerator를 대체했다. Spring 블로그에 따르면 @daily, @hourly 같은 매크로, 마지막 날을 뜻하는 L, 가장 가까운 평일을 뜻하는 W, n번째 요일을 뜻하는 #을 지원한다. 예를 들어 0 0 0 L * *는 매월 마지막 날 자정이다.

  • https://wonos.tistory.com/244
  • https://spidyweb.tistory.com/138
  • https://ko.wikipedia.org/wiki/Cron
  • https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/support/CronExpression.html
  • https://www.baeldung.com/cron-expressions
  • https://spring.io/blog/2020/11/10/new-in-spring-5-3-improved-cron-expressions
  • https://krksap.tistory.com/1269

CannotAcquireLockException

Spring의 데이터 접근 예외 중 하나로, Javadoc은 “select for update” 같은 갱신 중에 잠금을 얻지 못했을 때 던지는 예외라고 설명한다. 계층은 DataAccessException 아래 TransientDataAccessException, ConcurrencyFailureException, PessimisticLockingFailureException으로 이어진다. 이름에 Transient가 들어간 데서 알 수 있듯 다시 시도하면 성공할 수 있는 종류의 실패로 분류된다. Javadoc은 더 넓은 범위를 포괄하는 PessimisticLockingFailureException을 처리하는 것도 고려하라고 권한다.

링크한 글은 MySQL 데드락(오류 1213)이 이 예외로 올라온 사례를 분석한다. 같은 알림 여러 건을 동시에 갱신하는 요청들이 서로 다른 순서로 행을 잠그면서 데드락이 났다는 내용이다. MySQL 문서에 따르면 InnoDB는 데드락을 감지하면 트랜잭션 하나를 희생자로 골라 롤백한다. 데드락을 줄이는 방법으로는 트랜잭션을 작게 유지하고, 여러 행을 갱신할 때 트랜잭션마다 같은 순서로 접근하는 것을 권한다.

  • https://bgrooot.github.io/cannot-acquire-lock-exception-research/

Isolation Level

트랜잭션 격리 수준은 동시에 실행되는 트랜잭션이 서로의 변경을 어디까지 볼 수 있는지 정한다. SQL 표준의 네 단계는 READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE이다. MySQL 문서를 기준으로 InnoDB의 동작을 정리하면 다음과 같다.

  • READ UNCOMMITTED: 커밋되지 않은 변경까지 읽을 수 있다(dirty read).
  • READ COMMITTED: 일반 읽기마다 새 스냅숏을 만든다. 같은 트랜잭션 안에서도 다른 트랜잭션이 커밋한 값이 보일 수 있다. 갭 락을 쓰지 않아 데드락 가능성이 줄어든다.
  • REPEATABLE READ: InnoDB의 기본값이다. 트랜잭션 안의 일반 읽기는 첫 읽기 때 만든 스냅숏을 계속 본다. 잠금 읽기에서는 갭 락과 넥스트 키 락으로 다른 세션의 삽입을 막는다.
  • SERIALIZABLE: REPEATABLE READ와 비슷하지만, autocommit이 꺼져 있으면 일반 SELECT도 SELECT ... FOR SHARE처럼 잠금 읽기로 바뀐다.

앞의 CannotAcquireLockException과 연결해 보면, 격리 수준이 높을수록 잠금을 더 많이 잡으니 잠금 대기와 데드락도 늘어날 수 있다. Spring에서는 @Transactional(isolation = Isolation.READ_COMMITTED)처럼 트랜잭션 단위로 격리 수준을 지정할 수 있다.

이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다