Skip to content

Commit bdcd1bc

Browse files
authored
[#532] Todo 카테고리가 무지성 fetch되는 현상을 해결한다 (#562)
* refactor: preference -> CategoryPreference로 수정해서 가독성 개선 * refactor: 변수 이름 수정 * feat: store에 json화 할 수 있는 Codable 채택 타입 관련 메서드 추가 * refactor: DTO에 codable 채택 * fix: TodoCategoryPreference 캐시 사용 * refactor: 생성자 위치 이동 * fix: TodoCategoryPreference 캐시 처리 * refactor: swift 6 경고 해결 * refactor: preconcurrency 제거 * fix: Auth 세션 캐싱 후 상태 전파 * fix: 위젯 세션 동기화 상태 연결 * refactor: 위젯 Todo snapshot 의존성 정리 * refactor: PushNotificationRepositoryImpl에서 카테고리 캐싱 관련 구현 * fix: RootViewModel 구독 중복 방지 * fix: Auth 세션 중복 방출 방지 * feat: 메모리 캐싱 프로토콜, 실 구현체 구현 * refactor: 메모리 캐싱으로 대체 * refactor: 메모리 캐시 저장 방식 개선 * fix: Auth 세션 관찰 흐름 단일화
1 parent b2ffa27 commit bdcd1bc

29 files changed

Lines changed: 634 additions & 160 deletions

Application/DevLogData/Sources/DTO/TodoCategoryPreferenceResponse.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77

88
import Foundation
99

10-
public struct TodoCategoryPreferenceResponse: Equatable {
11-
public enum Category: Equatable {
10+
public struct TodoCategoryPreferenceResponse: Equatable, Codable {
11+
public enum Category: Equatable, Codable {
1212
case system(String)
1313
case user(UserCategory)
1414
}
1515

16-
public struct UserCategory: Equatable {
16+
public struct UserCategory: Equatable, Codable {
1717
public let id: String
1818
public let name: String
1919
public let colorHex: String

Application/DevLogData/Sources/DataAssembler.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,26 +40,29 @@ public final class DataAssembler: Assembler {
4040
TodoRepositoryImpl(
4141
todoService: container.resolve(TodoService.self),
4242
todoCategoryService: container.resolve(TodoCategoryService.self),
43+
store: container.resolve(MemoryCacheStore.self),
4344
widgetSyncEventBus: container.resolve(WidgetSyncEventBus.self),
4445
todoMutationEventBus: container.resolve(TodoMutationEventBus.self)
4546
)
4647
}
4748

4849
container.register(WidgetTodoSnapshotRepository.self) {
49-
WidgetTodoSnapshotRepositoryImpl(
50-
repository: container.resolve(TodoRepository.self)
51-
)
50+
WidgetTodoSnapshotRepositoryImpl(todoService: container.resolve(TodoService.self))
5251
}
5352

5453
container.register(TodoCategoryRepository.self) {
5554
TodoCategoryRepositoryImpl(
56-
todoCategoryService: container.resolve(TodoCategoryService.self)
55+
todoCategoryService: container.resolve(TodoCategoryService.self),
56+
store: container.resolve(MemoryCacheStore.self)
5757
)
5858
}
5959

6060
container.register(AuthSessionRepository.self) {
6161
AuthSessionRepositoryImpl(
62-
authService: container.resolve(AuthService.self)
62+
authService: container.resolve(AuthService.self),
63+
todoCategoryService: container.resolve(TodoCategoryService.self),
64+
store: container.resolve(MemoryCacheStore.self),
65+
provider: container.resolve(AuthSessionStateProvider.self)
6366
)
6467
}
6568

@@ -100,7 +103,8 @@ public final class DataAssembler: Assembler {
100103
container.register(PushNotificationRepository.self) {
101104
PushNotificationRepositoryImpl(
102105
pushNotificationService: container.resolve(PushNotificationService.self),
103-
todoCategoryService: container.resolve(TodoCategoryService.self)
106+
todoCategoryService: container.resolve(TodoCategoryService.self),
107+
store: container.resolve(MemoryCacheStore.self)
104108
)
105109
}
106110

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// AuthSessionStateProvider.swift
3+
// DevLogData
4+
//
5+
// Created by opfic on 6/9/26.
6+
//
7+
8+
import Combine
9+
10+
public protocol AuthSessionStateProvider {
11+
func publish(_ isSignedIn: Bool)
12+
func observeSignedIn() -> AnyPublisher<Bool, Never>
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// MemoryCacheStore.swift
3+
// DevLogData
4+
//
5+
// Created by opfic on 6/9/26.
6+
//
7+
8+
import Foundation
9+
10+
public protocol MemoryCacheStore {
11+
func value<T: Codable>(forKey key: String) -> T?
12+
func setValue<T: Codable>(_ value: T?, forKey key: String)
13+
}

Application/DevLogData/Sources/Protocol/TodoCategoryService.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
import Foundation
99

1010
public protocol TodoCategoryService {
11-
func fetchPreferences() async throws -> [TodoCategoryPreferenceResponse]
12-
func updatePreferences(_ preferences: [TodoCategoryPreferenceResponse]) async throws
11+
func fetchCategoryPreferences() async throws -> [TodoCategoryPreferenceResponse]
12+
func updateCategoryPreferences(_ preferences: [TodoCategoryPreferenceResponse]) async throws
1313
}

Application/DevLogData/Sources/Protocol/UserDefaultsStore.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import Foundation
99

1010
public protocol UserDefaultsStore {
11+
func value<T: Codable>(forKey key: String) -> T?
12+
func setValue<T: Codable>(_ value: T?, forKey key: String)
13+
func removeValues(withPrefix prefix: String)
1114
func string(forKey key: String) -> String?
1215
func setString(_ value: String?, forKey key: String)
1316
func stringArray(forKey key: String) -> [String]

Application/DevLogData/Sources/Repository/AuthSessionRepositoryImpl.swift

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,67 @@ import Combine
99
import DevLogDomain
1010

1111
final class AuthSessionRepositoryImpl: AuthSessionRepository {
12+
private enum Key {
13+
static let preferences = "TodoCategory.preferences"
14+
}
15+
1216
private let authService: AuthService
17+
private let todoCategoryService: TodoCategoryService
18+
private let store: MemoryCacheStore
19+
private let provider: AuthSessionStateProvider
20+
private var cancellables = Set<AnyCancellable>()
1321

14-
init(authService: AuthService) {
22+
init(
23+
authService: AuthService,
24+
todoCategoryService: TodoCategoryService,
25+
store: MemoryCacheStore,
26+
provider: AuthSessionStateProvider
27+
) {
1528
self.authService = authService
29+
self.todoCategoryService = todoCategoryService
30+
self.store = store
31+
self.provider = provider
32+
33+
setupObservation()
1634
}
1735

1836
func observeSignedIn() -> AnyPublisher<Bool, Never> {
37+
provider.observeSignedIn()
38+
}
39+
}
40+
41+
private extension AuthSessionRepositoryImpl {
42+
func setupObservation() {
1943
authService.observeSignedIn()
44+
.removeDuplicates()
45+
.sink { [weak self] isSignedIn in
46+
Task { [weak self] in
47+
guard let self else { return }
48+
49+
if isSignedIn {
50+
await self.cachePreferencesIfNeeded()
51+
} else {
52+
self.clearPreferencesCache()
53+
}
54+
self.provider.publish(isSignedIn)
55+
}
56+
}
57+
.store(in: &cancellables)
58+
}
59+
60+
func cachePreferencesIfNeeded() async {
61+
if store.value(forKey: Key.preferences) as [TodoCategoryPreferenceResponse]? != nil {
62+
return
63+
}
64+
65+
guard let preferences = try? await todoCategoryService.fetchCategoryPreferences() else {
66+
return
67+
}
68+
69+
store.setValue(preferences, forKey: Key.preferences)
70+
}
71+
72+
func clearPreferencesCache() {
73+
store.setValue(Optional<[TodoCategoryPreferenceResponse]>.none, forKey: Key.preferences)
2074
}
2175
}

Application/DevLogData/Sources/Repository/PushNotificationRepositoryImpl.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,22 @@ import DevLogCore
1111
import DevLogDomain
1212

1313
final class PushNotificationRepositoryImpl: PushNotificationRepository {
14+
private enum Key {
15+
static let preferences = "TodoCategory.preferences"
16+
}
17+
1418
private let pushNotificationService: PushNotificationService
1519
private let todoCategoryService: TodoCategoryService
20+
private let store: MemoryCacheStore
1621

1722
init(
1823
pushNotificationService: PushNotificationService,
19-
todoCategoryService: TodoCategoryService
24+
todoCategoryService: TodoCategoryService,
25+
store: MemoryCacheStore
2026
) {
2127
self.pushNotificationService = pushNotificationService
2228
self.todoCategoryService = todoCategoryService
29+
self.store = store
2330
}
2431

2532
/// 푸시 알림 On/Off 설정
@@ -59,7 +66,7 @@ final class PushNotificationRepositoryImpl: PushNotificationRepository {
5966
do {
6067
let cursorDTO = cursor.map { PushNotificationCursorDTO.fromDomain($0) }
6168
async let responseTask = pushNotificationService.requestNotifications(query, cursor: cursorDTO)
62-
async let preferencesTask = todoCategoryService.fetchPreferences()
69+
async let preferencesTask = todoCategoryPreferenceResponses()
6370

6471
let (response, preferenceResponses) = try await (responseTask, preferencesTask)
6572
return try resolvePage(from: response, with: preferenceResponses.toDomain())
@@ -91,7 +98,8 @@ final class PushNotificationRepositoryImpl: PushNotificationRepository {
9198

9299
Task {
93100
do {
94-
let preferences = try await self.todoCategoryService.fetchPreferences().toDomain()
101+
let preferences = try await self.todoCategoryPreferenceResponses()
102+
.toDomain()
95103
let page = try self.resolvePage(from: response, with: preferences)
96104
subject.send(page)
97105
} catch {
@@ -147,6 +155,16 @@ final class PushNotificationRepositoryImpl: PushNotificationRepository {
147155
}
148156

149157
private extension PushNotificationRepositoryImpl {
158+
func todoCategoryPreferenceResponses() async throws -> [TodoCategoryPreferenceResponse] {
159+
if let preferences: [TodoCategoryPreferenceResponse] = store.value(forKey: Key.preferences) {
160+
return preferences
161+
}
162+
163+
let preferences = try await todoCategoryService.fetchCategoryPreferences()
164+
store.setValue(preferences, forKey: Key.preferences)
165+
return preferences
166+
}
167+
150168
func resolvePage(
151169
from response: PushNotificationPageResponse,
152170
with preferences: [TodoCategoryPreference]

Application/DevLogData/Sources/Repository/TodoCategoryRepositoryImpl.swift

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,40 @@
88
import DevLogDomain
99

1010
final class TodoCategoryRepositoryImpl: TodoCategoryRepository {
11+
private enum Key {
12+
static let preferences = "TodoCategory.preferences"
13+
}
14+
1115
private let todoCategoryService: TodoCategoryService
16+
private let store: MemoryCacheStore
1217

13-
init(todoCategoryService: TodoCategoryService) {
18+
init(
19+
todoCategoryService: TodoCategoryService,
20+
store: MemoryCacheStore
21+
) {
1422
self.todoCategoryService = todoCategoryService
23+
self.store = store
1524
}
1625

17-
func fetchPreferences() async throws -> [TodoCategoryPreference] {
26+
func fetchCategoryPreferences() async throws -> [TodoCategoryPreference] {
1827
do {
19-
return try await todoCategoryService.fetchPreferences().toDomain()
28+
if let preferences: [TodoCategoryPreferenceResponse] = store.value(forKey: Key.preferences) {
29+
return preferences.toDomain()
30+
}
31+
32+
let responses = try await todoCategoryService.fetchCategoryPreferences()
33+
store.setValue(responses, forKey: Key.preferences)
34+
return responses.toDomain()
2035
} catch {
2136
throw error.toDomain()
2237
}
2338
}
2439

25-
func updatePreferences(_ preferences: [TodoCategoryPreference]) async throws {
40+
func updateCategoryPreferences(_ preferences: [TodoCategoryPreference]) async throws {
2641
do {
27-
try await todoCategoryService.updatePreferences(
28-
preferences.map(TodoCategoryPreferenceResponse.fromDomain)
29-
)
42+
let responses = preferences.map(TodoCategoryPreferenceResponse.fromDomain)
43+
try await todoCategoryService.updateCategoryPreferences(responses)
44+
store.setValue(responses, forKey: Key.preferences)
3045
} catch {
3146
throw error.toDomain()
3247
}

Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,26 @@ import DevLogCore
1010
import DevLogDomain
1111

1212
final class TodoRepositoryImpl: TodoRepository {
13+
private enum Key {
14+
static let preferences = "TodoCategory.preferences"
15+
}
16+
1317
private let todoService: TodoService
1418
private let todoCategoryService: TodoCategoryService
19+
private let store: MemoryCacheStore
1520
private let widgetSyncEventBus: WidgetSyncEventBus
1621
private let todoMutationEventBus: TodoMutationEventBus
1722

1823
init(
1924
todoService: TodoService,
2025
todoCategoryService: TodoCategoryService,
26+
store: MemoryCacheStore,
2127
widgetSyncEventBus: WidgetSyncEventBus,
2228
todoMutationEventBus: TodoMutationEventBus
2329
) {
2430
self.todoService = todoService
2531
self.todoCategoryService = todoCategoryService
32+
self.store = store
2633
self.widgetSyncEventBus = widgetSyncEventBus
2734
self.todoMutationEventBus = todoMutationEventBus
2835
}
@@ -31,11 +38,16 @@ final class TodoRepositoryImpl: TodoRepository {
3138
let responseCursor = cursor.map { TodoCursorDTO.fromDomain($0) }
3239

3340
do {
34-
async let response = todoService.fetchTodos(query, cursor: responseCursor)
35-
async let preferences = todoCategoryService.fetchPreferences()
36-
37-
let (todoResponse, todoPreferenceResponses) = try await (response, preferences)
38-
let userTodoCategories: [UserTodoCategory] = todoPreferenceResponses.toDomain().compactMap { preference in
41+
async let todos = todoService.fetchTodos(query, cursor: responseCursor)
42+
async let preferences = todoCategoryPreferenceResponses()
43+
44+
let (todoResponse, todoPreferenceResponses) = try await (
45+
todos,
46+
preferences
47+
)
48+
let userTodoCategories: [UserTodoCategory] = todoPreferenceResponses
49+
.toDomain()
50+
.compactMap { preference in
3951
guard case .user(let category) = preference.category else {
4052
return nil
4153
}
@@ -59,10 +71,15 @@ final class TodoRepositoryImpl: TodoRepository {
5971
func fetchTodo(_ todoId: String) async throws -> Todo {
6072
do {
6173
async let response = todoService.fetchTodo(todoId: todoId)
62-
async let preferences = todoCategoryService.fetchPreferences()
63-
64-
let (todoResponse, todoPreferenceResponses) = try await (response, preferences)
65-
let userTodoCategories: [UserTodoCategory] = todoPreferenceResponses.toDomain().compactMap { preference in
74+
async let preferences = todoCategoryPreferenceResponses()
75+
76+
let (todoResponse, todoPreferenceResponses) = try await (
77+
response,
78+
preferences
79+
)
80+
let userTodoCategories: [UserTodoCategory] = todoPreferenceResponses
81+
.toDomain()
82+
.compactMap { preference in
6683
guard case .user(let category) = preference.category else {
6784
return nil
6885
}
@@ -79,10 +96,15 @@ final class TodoRepositoryImpl: TodoRepository {
7996
func fetchReferences(_ numbers: [Int]) async throws -> [Int: TodoReference] {
8097
do {
8198
async let responseTask = todoService.fetchReferences(numbers)
82-
async let preferencesTask = todoCategoryService.fetchPreferences()
83-
84-
let (responses, preferenceResponses) = try await (responseTask, preferencesTask)
85-
let userTodoCategories: [UserTodoCategory] = preferenceResponses.toDomain().compactMap { preference in
99+
async let preferencesTask = todoCategoryPreferenceResponses()
100+
101+
let (responses, preferenceResponses) = try await (
102+
responseTask,
103+
preferencesTask
104+
)
105+
let userTodoCategories: [UserTodoCategory] = preferenceResponses
106+
.toDomain()
107+
.compactMap { preference in
86108
guard case .user(let category) = preference.category else {
87109
return nil
88110
}
@@ -149,6 +171,16 @@ final class TodoRepositoryImpl: TodoRepository {
149171
}
150172

151173
private extension TodoRepositoryImpl {
174+
func todoCategoryPreferenceResponses() async throws -> [TodoCategoryPreferenceResponse] {
175+
if let preferences: [TodoCategoryPreferenceResponse] = store.value(forKey: Key.preferences) {
176+
return preferences
177+
}
178+
179+
let preferences = try await todoCategoryService.fetchCategoryPreferences()
180+
store.setValue(preferences, forKey: Key.preferences)
181+
return preferences
182+
}
183+
152184
func resolve(
153185
_ response: TodoResponse,
154186
userTodoCategories: [UserTodoCategory]

0 commit comments

Comments
 (0)