StackDay. Date는 날짜가 아니다

StackDay는 일정 주기의 습관 수행 여부를 기록하는 앱이고, MVP에서는 그 주기를 매일로 고정했다. 지금까지 구현할 때, “어느 날에 수행했는가"와 “정확히 언제 기록했는가"를 모두 Foundation의 Date로 표현했다. 두 정보는 비슷해 보이지만 다르다. 이 차이를 타입으로 구분하지 않으면 날짜 검증, 중복 기록 검사, 통계 계산마다 같은 해석을 반복하게 된다. 이번 글에서는 그 문제가 실제 코드에서 어떻게 드러났고, LocalDay·Clock·TimeZone을 어떤 경계로 나눴는지 정리한다. Date타입이 저장하는 것은 순간 Date는 연, 월, 일이나 자정 같은 달력 개념을 저장하지 않는다. ...

August 18, 2026

Swift Concurrency. Explore Structured Concurrency in Swift (2) - WWDC21

앞선 포스팅에서는 구조화된 동시성이 task 계층 구조를 활용해 제어 흐름을 일반 동기 코드처럼 만들고, 에러 전파와 cancellation을 단순하게 만드는 방식을 정리했다. 이 포스팅에서는 계층 구조가 없는 task, 즉 unstructured task(구조화되지 않은 동시성)에 대해 정리한다. Not all tasks fit a structured pattern 동기 코드에서 처음으로 비동기 연산을 시작하는 경우처럼 parent task가 존재하지 않거나, task의 생명주기가 하나의 스코프에 들어맞지 않는 경우가 있을 수 있다. 이런 경우는 특히 UIKit에서 delegate를 구현할 때 자주 발생한다. ...

August 17, 2026

StackDay. 비즈니스 규칙과 Entity Identity 분리하기

현재 StackDay MVP에서는 하나의 Habit에 대해 하루 한 번만 Completion을 기록할 수 있다. 그래서 처음에는 (habitID, completedOn) 조합을 Completion의 identity처럼 사용해도 될 것 같았고, 실제로 그런 코드도 있었다. 하지만 Completion 삭제를 구현하면서 이 둘을 분리해야 한다는 점을 알게 되었다. struct Completion: Equatable, Identifiable { let id: UUID let habitID: UUID let completedOn: Date let recordedAt: Date } 같은 날의 기록을 찾는 조건 같은 Habit의 같은 날 Completion을 찾는 데에는 habitID와 completedOn이 필요하다. 현재 정책에서는 이 조건으로 중복 기록도 막을 수 있다. 하지만 이것은 현재 정책에서 필요한 조회 조건이다. 이후 시간 단위로 습관을 기록하거나, 하루에 여러 번 수행하는 Habit이 생기면 같은 조합으로 여러 Completion이 존재할 수 있다. ...

August 16, 2026

Swift Concurrency. Explore Structured Concurrency in Swift (1) - WWDC21

앞서 정리한 세션에서는 completion handler 기반의 비동기 코드를 async/await로 작성하면서, 비동기 코드에서도 return, throw, try-catch와 같은 일반적인 제어 흐름을 사용할 수 있게 되었다. 이번 포스팅에서는 WWDC21의 Explore Structured Concurrency in Swift 세션을 정리한다. async/await가 비동기 코드의 제어 흐름을 구조화했다면, structured concurrency는 여기서 더 나아가 동시에 실행되는 여러 task의 관계와 수명을 구조화한다. Explore Structed Concurrency in Swift Swift Concurrency는 구조화된 프로그래밍(structured programming)의 아이디어에 기반한 structured concurrency라는 개념을 활용한다. 옛날에는 프로그램에서 명령어를 나열하고, 컨트롤 플로우가 여기저기로 자유롭게 점프해서 코드를 읽기가 매우 어려웠다. (C의 goto를 생각해보자) ...

August 15, 2026

StackDay. UseCase와 Entity는 각각 어디까지 책임져야 할까

CreateHabitUseCase를 시작으로 여러 유즈케이스를 구현하면서, 도메인 규칙을 어디에 두어야 할지 계속 고민하게 됐다. Habit을 생성할 때 이름의 유효성을 누가 검증해야 하는지, Completion을 남길 때 해당 날짜에 기록할 수 있는지는 누가 판단해야 하는지처럼 비슷한 문제가 반복해서 나타났다. 처음에는 유즈케이스가 사용자의 작업을 처리하니 이런 검증도 함께 담당하면 된다고 생각할 수 있었다. 하지만 구현을 진행할수록 엔티티가 스스로 보장해야 하는 규칙과 유즈케이스 작업의 흐름을 위해 판단해야 하는 규칙을 구분할 필요가 있었다. 이 포스트에서는 Habit의 생성과 Completion 기록을 중심으로 어떤 고민을 했는지와 어떻게 해결했는지를 정리한다. ...

August 14, 2026

StackDay. Repository의 save를 insert와 update로 나누기

유즈케이스를 구현하면서 Habit과 Completion을 저장할 Repository 계약을 좀 정리했다. 처음에는 create와 update 둘 다 save 하나로 저장하면 간단해 보였지만, 두 기능이 기대하는 실패 조건이 서로 다르다는 점에서 계약을 나눌 필요가 있었다. save는 어떤 동작인가 save를 upsert로 구현하면 호출하는 쪽은 편하다. ID가 없으면 삽입하고, 이미 있으면 갱신하면 된다. protocol HabitRepository { func save(_ habit: Habit) async throws } 문제는 이 동작이 실패해야 하는 상황까지 조용히 성공으로 바꾼다는 점이다. (물론 UUID라 현실적으로 겹칠 일은 없지만) Habit을 생성하는 작업은 같은 ID가 이미 있으면 실패해야 하고, 보관처럼 기존 Habit을 바꾸는 작업은 대상이 없으면 실패해야 한다. ...

August 14, 2026

StackDay. 책임 분리와 타입 분리

이전 작업에서 HabitStreak을 구현하면서 계산 결과와 계산 로직을 분리했다. HabitStreak은 현재 스트릭과 최장 스트릭이라는 값만 표현하고, 계산은 HabitStreakCalculator가 담당한다. struct HabitStreak: Equatable { let current: Int let longest: Int } struct HabitStreakCalculator { func calculate( habit: Habit, completions: [Completion], referenceDate: Date, calendar: Calendar = .current ) -> HabitStreak { // ... } } 같은 원리로 HabitStatistics도 파생 값과 계산 책임을 분리해서 구현했다. HabitStatistics HabitStatistics는 Habit의 수행 기록을 요약한 파생 값이다. MVP에서는 다음 값을 제공한다. struct HabitStatistics: Equatable { let totalCompletedDays: Int let eligibleTrackingDays: Int let completionRate: Double let streak: HabitStreak } totalCompletedDays는 추적 기간 안에서 완료한 날짜의 수이고, eligibleTrackingDays는 시작일부터 기준일까지 추적 대상이 된 날짜의 수다. 아카이브된 Habit은 archivedOn을 마지막 추적일로 사용한다. ...

August 13, 2026

Data Structure. Segment Tree

Segment Tree 배열의 특정 구간에 대한 연산을 빠르게 처리하기 위한 이진 트리 형태의 자료구조. 구간 합, 최솟값, 최댓값처럼 배열의 일정 범위를 대상으로 반복해서 연산해야 하는 경우에 사용할 수 있다. 일반적인 배열에서 특정 구간의 합을 구하려면 해당 구간의 원소를 직접 순회해야 하므로 $O(n)$의 시간이 필요하다. 세그먼트 트리는 배열의 여러 구간에 대한 연산 결과를 트리 형태로 저장하여 구간 조회를 $O(\log n)$에 처리할 수 있도록 한다. 특히 배열의 값이 변경되는 상황에서도 구간에 대한 연산을 반복해서 수행해야 할 때 사용할 수 있다. ...

August 13, 2026

StackDay. 엔티티와 계산 책임 분리하기

앞서 도메인, 데이터 모델링한 내용을 실제 코드로 옮기기 시작했다. 모델 자체의 구조는 이미 정해져 있었지만 코드로 구현하니 Habit Entry같은 파생 값들은 어디서 생성해야 하는지 책임이 정해지지 않았다. Completion 먼저 습관을 수행했다는 사실을 기록하는 Completion을 구현했다. struct Completion: Equatable { let id: UUID let habitID: UUID let completedOn: Date let recordedAt: Date } Completion은 저장되는 원본 데이터이므로 완료 사실을 표현하는 데 필요한 값만 가진다. 미래의 날짜인지, Habit의 시작일 이전인지, 같은 날짜의 Completion이 이미 존재하는지와 같은 검증은 Completion 하나만으로 판단할 수 없기 때문에 넣지 않았다. ...

August 12, 2026

StackDay. 데이터 모델링, Entry가 아닌 Completion을 관리해야 하는 이유

StackDay의 MVP 범위와 정책을 가지고 앱의 도메인과 데이터를 모델링한다. 도메인 모델링 -> 데이터 모델링 -> 유즈케이스 검증 순서로 진행하고, 각각의 단계가 이전 단계를 검증하는 방향이다. 앱의 핵심 동작 이전 포스트에서 정한 앱의 컨셉과 정책에서부터, 이 앱이 어떤 동작을 해야 할지 먼저 정리했다. 사용자가 습관을 만들면 그날부터 습관의 추적이 시작된다. 사용자는 매일 습관을 수행했는지 기록하고, 앱은 누적된 기록을 바탕으로 현재 스트릭과 통계를 보여준다. 더 이상 이어가지 않을 습관은 추적을 종료할 수 있다. ...

August 11, 2026