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

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