글을 작성하게 된 계기
MySQL YEARWEEK( ) 함수를 사용하다 ISO 주와 일반 주의 차이점에 대해 궁금해서 글을 작성하게 되었습니다.
1. ISO 주와 일반 주의 차이점
일반적인 달력에서 사용하는 주차 계산 방식은 단순합니다. 1월 1일이 속한 주를 해당 해의 첫 번째 주, 즉 1주차로 정합니다. 그래서 1월 1일이 포함된 주 전체가 새해의 1주차로 취급 됩니다. 이 방식은 직관적이고 사람들이 달력을 볼 때 자연스럽게 받아들이기 때문에 일상생활에서는 보통 이렇게 주차를 셉니다.
1
2
3
4
# 2009년 12 ~ 2010년 01
Mo Tu We Th Fr Sa Su
28 29 30 31 1 2 3 <- 2010년 1주차 (1월 1일이 포함된 주)
4 5 6 7 8 9 10
하지만 이 방식은 2009년 12월 입장에서 보면 12월 마지막 주이며, 2010년 1월 입장에서 보면 1월 첫 주로 보입니다. 즉, 12월 28일부터 1월 3일까지의 주는 2009년과 2010년 모두에 걸쳐 있는 주 가 됩니다. 이 때문에 어떤 해의 마지막 주가 다음 해의 첫 주로 넘어가는 경우가 생기죠.
1
2
3
4
# 2009년 12 ~ 2010년 01
Mo Tu We Th Fr Sa Su
28 29 30 31 1 2 3 <- 2010년 1주차 (1월 1일이 포함된 주)
4 5 6 7 8 9 10
1-1. KS X ISO8601 표준
이에 대해 대한민국 국가기술표준원은 KS X ISO8601 표준 을 정의했습니다. 이 표준은 국제적으로 쓰이는 ISO 8601 주차 규칙과 동일합니다. 예를 들어, 5월 28일(월)부터 6월 3일(일)까지 주간이 있다고 가정하면, 이 주는 목요일(5월 31일)을 포함하므로 5월 마지막 주로 보는 게 맞습니다.
1
2
3
Mo Tu We Th Fr Sa Su
28 29 30 31 1 2 3 <- 2018년 22주차 (목요일=5월31일 포함)
4 5 6 7 8 9 10 <- 2018년 23주차
반대로 5월 29일(화)부터 6월 4일(월)까지라면, 목요일(6월 1일)이 포함되어 있기 때문에 6월 첫째 주라고 할 수 있습니다.
1
2
3
Mo Tu We Th Fr Sa Su
29 30 31 1 2 3 4 <- 2017년 22주차 (목요일=6월1일 포함)
5 6 7 8 9 10 11
1-2. ISO Week Numbering
ISO 8601 표준에서 정의한 주차(ISO Week Numbering)도 같은데요, ISO 주는 반드시 월요일에 시작해서 일요일에 끝나는 주 로 구성됩니다. 이를 통해 국가 간의 달력 차이를 없애고, 무역이나 국제 표준 문서에서 주차를 일관성 있게 사용할 수 있죠. 여기서는 새해의 첫 번째 주는 그 해의 첫 번째 목요일을 포함하는 주 로 정해집니다. 예를 들어, 1월 1일이 금요일이라면 그 주에는 아직 새해의 첫 목요일이 포함되지 않으므로, 1월 1일은 전년도 마지막 주차로 계산됩니다.
1
2
3
4
# 2009년 12 ~ 2010년 01
Mo Tu We Th Fr Sa Su
28 29 30 31 1 2 3 <- ISO 주차: 2009-W53 (아직 2010년 첫 목요일 없음)
4 5 6 7 8 9 10 <- ISO 주차: 2010-W01 (첫 목요일 1/7 포함)
ISO 8601에서 12월 28일은 항상 그 해의 마지막 ISO 주에 포함됩니다.
반대로 1월 1일이 월요일이나 화요일이라면 이미 목요일이 같은 주에 포함되기 때문에 그 주가 곧 새해의 Week1이 됩니다. 이 차이 때문에 ISO 주 방식에서는 어떤 해의 마지막 며칠이 다음 해 첫 주차로 넘어가기도 하고, 반대로 새해의 첫 며칠이 전년도 마지막 주차로 계산되기도 합니다.
1
2
3
4
# 2024년 12 ~ 2025년 01
Mo Tu We Th Fr Sa Su
30 31 1 2 3 4 5 <- ISO 주차: 2025-W01 (1/2 목요일 포함)
6 7 8 9 10 11 12
2. 53주인 1년
ISO 주차는 항상 1년을 52주 또는 53주로 나누는데, 대부분은 52주로 끝납니다. 하지만 평년은 365일, 윤년은 366일이므로, ISO 규칙을 적용하다 보면 어떤 해는 추가로 하루(또는 이틀)가 남습니다. 이때 ISO 규칙은 단순히 일수를 세는 게 아니라 첫 목요일이 포함된 주 라는 원칙을 지키다 보니, 어떤 해는 주가 하나 더 필요해져서 53번째 주가 생깁니다. 즉, 그 해의 1월 1일이나 12월 31일이 목요일(혹은 윤년에 금요일)이라면 그 해는 53주차까지 존재 하게 됩니다.
- 그 해의 1월 1일이 목요일
- 그 해의 1월 1일이 금요일이면서, 윤년인 경우
- 그 해의 12월 31일이 목요일
- 그 해의 12월 31일이 금요일이면서, 윤년인 경우
이를 조금 더 자세히 살펴보면 1년은 365일로, 52주(364일)로 나누고 나면 항상 하루가 남습니다. 윤년에는 366일이라 이틀이 남게 되죠. 이 남는 하루나 이틀이 어디에 걸리느냐에 따라 ISO 규칙상 53번째 주가 생기기도 하고, 그냥 지나가기도 합니다. ISO 주차는 해당 연도의 첫 목요일이 들어 있는 주가 Week1 이라는 규칙을 따르기 때문에, 남는 날 중에 목요일이 끼면 Week53이 만들어집니다.
1
2
3
4
# 2014년 12월 ~ 2015년 1월
Mo Tu We Th Fr Sa Su
29 30 31 1 2 3 4 <- ISO 주차: 2015-W01 (첫 목요일 1/1 포함)
5 6 7 8 9 10 11 <- 2015-W02
1
2
3
4
# 2015년 12월 ~ 2016년 1월
Mo Tu We Th Fr Sa Su
28 29 30 31 1 2 3 <- ISO 주차: 2015-W53 (12/31 목요일 포함)
4 5 6 7 8 9 10 <- 2016-W01
2015년은 평년이면서 1월 1일이 목요일이었는데, 이 날이 Week1에 포함되었습니다. 그러다 보니 연말에 12월 28일 월요일부터 12월 31일 목요일까지가 또 하나의 주를 채워 Week53이 생겼습니다. 또 2020년은 윤년이었고 마지막 날인 12월 31일이 목요일이어서 마찬가지로 Week53이 만들어졌습니다. 만약 12월 31일이 수요일이었다면, 남는 날이 있더라도 목요일이 포함되지 않으므로 Week53은 존재하지 않았을 것입니다.
1
2
3
4
# 2019년 12월 ~ 2020년 1월
Mo Tu We Th Fr Sa Su
30 31 1 2 3 4 5 <- ISO 주차: 2020-W01 (첫 목요일 1/2 포함)
6 7 8 9 10 11 12 <- 2020-W02
1
2
3
4
# 2020년 12월 ~ 2021년 1월
Mo Tu We Th Fr Sa Su
28 29 30 31 1 2 3 <- ISO 주차: 2020-W53 (12/31 목요일 포함)
4 5 6 7 8 9 10 <- 2021-W01
이 53주차는 단순한 달력 용어의 문제가 아니라, 실제 서비스 로직에도 영향을 줄 수 있기 때문에 날짜 관련 로직이 있다면 이를 반드시 고려해야 합니다.
리포트/집계 로직: 주별 매출, 주간 활성 사용자 수(Weekly Active Users, WAU) 등을 뽑을 때 보통 52주 = 1년이라고 가정하는 경우가 많습니다. 그런데 어떤 해에는 53주가 생겨버리면, 연간 통계 합산에서 1주가 더 생겨 데이터 불일치 가 발생할 수 있습니다. DAU, WAU가 대표적이겠죠?회계/재무: ISO 주차를 기준으로 하는 회사(특히 유럽, 북미 일부 기업)는 회계연도를 주차 단위로 끊습니다. 이런 경우 어떤 해는 52주(364일), 어떤 해는 53주(371일)가 되는데, 53주차 회계연도 가 따로 존재합니다. 그 해는 인건비, 급여 지급 횟수, 임대료 등에서 한 번 더 지급/청구되는 이슈가 생길 수 있습니다. 회사 입장에서는 손해, 직원 입장에서는 추가 수입이 생길 수 있죠.달력 UI/UX: 캘린더 앱, ERP 시스템, 근태 시스템 같은 곳에서 주차를 ISO 기준으로 잡으면 2020년 같은 해에는 Week53이 표시되어야 합니다. 제대로 고려하지 않으면 없는 주차가 생기거나, 1월 첫 주 표시가 꼬여버릴 수 있습니다. 근태를 체크할 때, 52주로만 계산하면 문제가 발생하겠죠.
이런 문제를 해결하기 위해서는 연간 주차 수를 52로 고정하지 말고 실제로 존재하는 주차 수를 계산해서 반영해야 합니다. 참고로 Java/Kotlin에서는 자동으로 ISO 기준 주차와 연도를 계산할 수 있습니다.
1
2
3
4
val weekFields = WeekFields.ISO
val localDate = LocalDate.of(2020, 12, 31)
val week = localDate.get(weekFields.weekOfWeekBasedYear())
val year = localDate.get(weekFields.weekBasedYear())
MySQL도 YEARWEEK( ) 함수를 사용하면 ISO 주차를 쉽게 계산할 수 있습니다. YEARWEEK( ) 함수는 두 번째 인자로 1을 주면 ISO 주차를 반환합니다. 0은 일반 주차 계산 방식을, 1은 ISO 주차 계산 방식을 의미합니다.
1
2
3
4
SELECT
'2020-01-01' AS dt,
YEARWEEK('2020-01-01', 0) AS normal_week, -- 일반 주차(201952)
YEARWEEK('2020-01-01', 1) AS iso_week; -- ISO 주차(202001)
하지만 MySQL의 YEARWEEK(date, 0)은 우리가 흔히 1월 1일이 속한 주가 1주차라고 생각하는 방식과 다릅니다. MySQL의 mode 0은 일요일을 한 주의 시작으로 보고, 그 해의 첫 일요일이 포함된 주부터 1주차로 계산하는 독자적인 규칙을 사용합니다. 이 때문에 연초 날짜가 전년도 마지막 주로 계산될 수 있습니다. 아래 몇 가지 예제로 한 번 테스트 해보세요.
1
2
3
4
5
6
7
8
9
10
11
12
SELECT '2017-01-01' AS dt, DAYNAME('2017-01-01') AS dow,
YEARWEEK('2017-01-01',0) AS mode0
UNION ALL
SELECT '2018-01-01', DAYNAME('2018-01-01'), YEARWEEK('2018-01-01',0)
UNION ALL
SELECT '2019-01-01', DAYNAME('2019-01-01'), YEARWEEK('2019-01-01',0)
UNION ALL
SELECT '2020-01-01', DAYNAME('2020-01-01'), YEARWEEK('2020-01-01',0)
UNION ALL
SELECT '2021-01-01', DAYNAME('2021-01-01'), YEARWEEK('2021-01-01',0)
UNION ALL
SELECT '2022-01-01', DAYNAME('2022-01-01'), YEARWEEK('2022-01-01',0);
1
2
3
4
5
6
7
8
9
10
+------------+-----------+--------+
| dt | dow | mode0 |
+------------+-----------+--------+
| 2017-01-01 | Sunday | 201701 |
| 2018-01-01 | Monday | 201753 |
| 2019-01-01 | Tuesday | 201852 |
| 2020-01-01 | Wednesday | 201952 |
| 2021-01-01 | Friday | 202052 |
| 2022-01-01 | Saturday | 202152 |
+------------+-----------+--------+
3. 정리
ISO Week Numbering과 일반 주차 계산 방식의 차이는 단순히 날짜를 세는 방식의 차이일 뿐만 아니라, 실제 서비스나 비즈니스 로직에도 큰 영향을 미칠 수 있습니다. 특히 53주차가 생기는 경우, 데이터 집계, 회계 처리, UI/UX 등에서 예상치 못한 문제를 일으킬 수 있으므로, 이를 고려한 설계가 필요합니다.
MySQL YEARWEEK( ) 함수를 공부하다가 학습 정리까지 해버렸네요. 언젠간 사용할 날이 오길. 🙏