[Dalendar DevLog 3] 핵심 기능 구현 1 (백엔드 & 로직)
Rata Die와 유클리드 아핀 함수(EAF)를 활용하여 정확하고 빠른 날짜 계산 로직을 구현하는 과정을 다룹니다.
![[Dalendar DevLog 3] 핵심 기능 구현 1 (백엔드 & 로직)](/images/blog/dalendar_dev_3_math_logic.png)
3편: 핵심 기능 구현 1 (백엔드 & 로직)
서문: 겉모습에서 두뇌로
이 포스트는 달력 앱 만들기 시리즈의 3편입니다. 이전 2편에서는 중첩된 RecyclerView를 사용하여 달력의 기본 UI 구조를 만드는 과정을 다루었습니다. 이번 편에서는 달력 앱의 시각적인 부분을 넘어, 날짜 계산을 처리하는 핵심적인 ‘두뇌’, 즉 비즈니스 로직과 알고리즘을 구현하는 방법을 탐구할 것입니다.
윤년이나 매달 길이가 다른 문제 때문에 달력 계산은 겉보기보다 복잡합니다. 정확한 날짜 계산과 빠른 성능을 동시에 만족시키기 위해서는 효율적인 알고리즘의 설계가 필수적입니다.
1. 달력 로직의 근본적인 난제와 해결 전략
달력 알고리즘 구현의 핵심적인 어려움은 달과 연도의 길이가 불규칙하여 발생하는 비선형성(non-linearities)에 있습니다. 매달 일수가 28, 29, 30, 31일로 다르고, 4년마다(예외 존재) 하루가 추가되는 윤년의 존재는 단순한 수식으로 날짜를 계산하기 어렵게 만듭니다.
이 문제를 해결하기 위한 효과적인 전략은 ‘계산용 달력(computational calendar)‘이라는 개념을 도입하는 것입니다. 이 접근법의 핵심 아이디어는 길이가 유일하게 변하는 2월을 연도의 마지막 달로 이동시키는 것입니다. 이렇게 순서를 조정하면, 연초부터 대부분의 기간 동안 월별 일수가 31, 30, 31, 30, 31… 과 같이 매우 규칙적인 패턴을 따르게 됩니다. 변수의 원인인 윤일을 계산 시퀀스의 맨 끝으로 격리함으로써, 복잡한 if/else 분기문 없이도 간결하고 일관된 수식으로 날짜 계산 로직을 구현할 수 있는 기반이 마련됩니다.
2. 핵심 수학 도구: 유클리드 아핀 함수 (EAF)
달력 계산 문제를 체계적으로 해결하기 위해 ‘유클리드 아핀 함수(Euclidean Affine Functions, EAF)‘라는 수학적 도구를 사용할 수 있습니다. EAF는 f(r) = (α·r + β)/δ 형태로 정의되며, 정수 나눗셈과 나머지 연산을 일반화한 함수입니다. 이 구조를 통해 복잡한 달력 계산을 정형화된 수식으로 표현할 수 있습니다.
실제 적용 사례로, 위에서 언급한 계산용 달력에서 특정 월까지의 누적 일수를 구하는 공식을 들 수 있습니다. mc(m0) = (153 · m0 − 457)/5 라는 EAF는 계산용 달력의 시작 월인 3월(m0=3)부터 14월(그레고리력의 2월에 해당)까지의 누적 일수를 정확하게 계산해 줍니다. 이처럼 EAF를 사용하면 복잡한 조건문 없이 하나의 수식으로 핵심 로직을 구현할 수 있습니다.
3. 핵심 로직 구현: Rata Die에서 날짜 구하기
‘Rata Die’는 특정 기준일(epoch)로부터 경과한 총 일수를 의미하는 정수 값입니다. 달력 로직의 핵심은 이 Rata Die 값으로부터 년, 월, 일을 역산하여 구체적인 날짜를 알아내는 기능입니다.
이 과정은 본질적으로 **혼합 기수 변환(mixed-radix number system conversion)**으로 이해할 수 있습니다. Rata Die라는 거대한 단일 기수(일) 값을, 기수가 계속 변하는 ‘년-월-일’ 체계로 변환하는 것입니다. 각 단계는 큰 주기의 일수(예: 400년 주기)로 나누어 상위 단위를 구하고, 나머지를 다음 단계로 넘겨 더 작은 주기(예: 4년 주기)로 계산을 반복하는 과정의 연속입니다.
1단계: 세기(Century) 및 세기 내 경과일 계산
먼저 전체 경과일(r0)로부터 400년 주기(Leap cycle)의 길이에 해당하는 146,097일을 기준으로 나눗셈을 수행합니다. 이를 통해 현재가 기준일로부터 몇 번째 세기인지(q1), 그리고 그 세기가 시작된 후 며칠이 경과했는지(r1)를 계산합니다.
n1 = 4 * r0 + 3
q1 = n1 / 146097 // 세기 (Century)
r1 = (n1 % 146097) / 4 // 세기 내 경과일 (Day of the century)
2단계: 세기 내 연도 및 연내 경과일 계산
1단계에서 구한 ‘세기 내 경과일’(r1)을 4년 주기의 길이에 해당하는 1,461일로 나누어, 해당 세기 내에서 몇 년이 지났는지(q2), 그리고 그 해가 시작된 후 며칠이 경과했는지(r2)를 계산합니다.
n2 = 4 * r1 + 3
q2 = n2 / 1461 // 세기 내 연도 (Year of the century)
r2 = (n2 % 1461) / 4 // 연내 경과일 (Day of the year)
3단계: 월 및 일 계산
마지막으로 2단계에서 구한 ‘연내 경과일’(r2)을 이용하여 최종적으로 몇 월인지(q3), 그리고 며칠인지(r3)를 계산합니다. 이 단계에서는 계산용 달력의 5개월 주기(153일) 패턴을 활용합니다.
n3 = 5 * r2 + 461
q3 = n3 / 153 // 월 (Month)
r3 = (n3 % 153) / 5 // 일 (Day)
4. 고성능의 비결: 값비싼 나눗셈 연산 최적화
CPU에서 정수 나눗셈 연산은 곱셈이나 비트 연산에 비해 수십 배 이상 느린, 매우 값비싼 연산입니다. 달력 앱의 로직은 수많은 날짜에 대해 반복적으로 실행될 수 있으므로, 성능을 극대화하기 위해 이 나눗셈 연산을 최적화하는 것이 중요합니다.
핵심 최적화 기법은 ‘강도 축소(strength reduction)‘입니다. 특정 수 D로 나누는 것은 그 수의 역수 1/D를 곱하는 것과 수학적으로 동일합니다. 정수 연산에서는 1/D를 M / 2^k (거대한 정수 M을 2의 거듭제곱으로 나눈 값) 형태로 근사할 수 있습니다. 이를 통해 n / D 연산을 (n * M) >> k 라는, 훨씬 빠른 곱셈과 비트 시프트(bitwise shift) 연산의 조합으로 변환할 수 있습니다.
사례 1: 연도 계산 최적화
2단계의 연도 및 연내 경과일 계산은 최적화의 가장 강력한 효과를 보여주는 예시입니다.
- 최적화 이전 (데이터 의존성 존재): 몫(q2)을 먼저 계산해야만 나머지(r2)를 계산할 수 있는 구조입니다. r2 계산은 q2의 결과가 나올 때까지 대기해야 합니다.
- 최적화 이후 (병렬 처리 가능): 강도 축소를 적용하면 q2와 r2를 공통 중간값 u2로부터 각각 독립적으로 계산할 수 있습니다. CPU는 두 명령어의 데이터 의존성이 없으므로 동시에 실행(instruction-level parallelism)하여 처리 속도를 극적으로 높입니다.
결론: 효율적인 로직 위에 세워진 달력
이번 포스트를 통해 달력 앱의 UI 너머에 존재하는 복잡하지만 체계적인 수학적 로직을 살펴보았습니다. 달력의 불규칙성을 ‘계산용 달력’으로 단순화하고, EAF와 같은 수학적 프레임워크를 사용해 핵심 로직을 수식으로 표현했으며, 강도 축소와 병렬 처리 기법을 통해 CPU 연산을 최적화하는 과정까지 다루었습니다.
![[Dalendar DevLog 1] 프로젝트의 시작과 기술 스택 선정](/images/blog/dalendar_dev_1_ideation.png)
![[Dalendar DevLog 2] 견고한 앱의 뼈대 - 아키텍처와 데이터 구조 설계](/images/blog/dalendar_dev_2_architecture.png)
![[Dalendar DevLog 5] 달력 앱의 보이지 않는 적, 성능 저하와 싸운 기록 (트러블슈팅)](/images/blog/dalendar_dev_5_performance.png)