Skip to content

Commit d5c3dcc

Browse files
committed
feat: 프로필 이미지 메모리 캐시 저장소 추가
1 parent 5f78a4a commit d5c3dcc

4 files changed

Lines changed: 147 additions & 0 deletions

File tree

Application/DevLogData/Sources/DataAssembler.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ public final class DataAssembler: Assembler {
9494
UserDataRepositoryImpl(userService: container.resolve(UserService.self))
9595
}
9696

97+
container.register(ProfileImageDataRepository.self) {
98+
ProfileImageDataRepositoryImpl(
99+
service: container.resolve(ProfileImageDataService.self),
100+
store: container.resolve(MemoryCacheStore.self)
101+
)
102+
}
103+
97104
container.register(AnalyticsRepository.self) {
98105
AnalyticsRepositoryImpl(
99106
analyticsService: container.resolve(AnalyticsService.self)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// ProfileImageDataService.swift
3+
// DevLogData
4+
//
5+
// Created by opfic on 6/11/26.
6+
//
7+
8+
import Foundation
9+
10+
public protocol ProfileImageDataService {
11+
func fetchImageData(from url: URL) async throws -> Data
12+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// ProfileImageDataRepositoryImpl.swift
3+
// DevLogData
4+
//
5+
// Created by opfic on 6/11/26.
6+
//
7+
8+
import Foundation
9+
import DevLogDomain
10+
11+
public final class ProfileImageDataRepositoryImpl: ProfileImageDataRepository {
12+
private let service: ProfileImageDataService
13+
private let store: MemoryCacheStore
14+
15+
public init(
16+
service: ProfileImageDataService,
17+
store: MemoryCacheStore
18+
) {
19+
self.service = service
20+
self.store = store
21+
}
22+
23+
public func fetchImageData(from url: URL) async throws -> Data {
24+
do {
25+
let data = try await service.fetchImageData(from: url)
26+
store.setValue(data, forKey: Self.cacheKey(for: url))
27+
return data
28+
} catch {
29+
if let data: Data = store.value(forKey: Self.cacheKey(for: url)) {
30+
return data
31+
}
32+
throw error
33+
}
34+
}
35+
36+
private static func cacheKey(for url: URL) -> String {
37+
"profileImageData:\(url.absoluteString)"
38+
}
39+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//
2+
// ProfileImageDataRepositoryImplTests.swift
3+
// DevLogDataTests
4+
//
5+
// Created by opfic on 6/11/26.
6+
//
7+
8+
import Foundation
9+
import Testing
10+
@testable import DevLogData
11+
12+
struct ProfileImageDataRepositoryImplTests {
13+
@Test("캐시가 있어도 원격 이미지 데이터를 다시 요청하고 성공 데이터를 저장한다")
14+
func 캐시가_있어도_원격_이미지_데이터를_다시_요청하고_성공_데이터를_저장한다() async throws {
15+
let cachedData = Data([1, 2, 3])
16+
let remoteData = Data([4, 5, 6])
17+
let service = ProfileImageDataServiceSpy(data: cachedData)
18+
let store = ProfileImageMemoryCacheStoreSpy()
19+
let repository = ProfileImageDataRepositoryImpl(service: service, store: store)
20+
let url = URL(string: "https://example.com/avatar.png")!
21+
22+
_ = try await repository.fetchImageData(from: url)
23+
service.data = remoteData
24+
let data = try await repository.fetchImageData(from: url)
25+
26+
#expect(data == remoteData)
27+
#expect(service.calledURLs == [url, url])
28+
#expect(store.storedData == remoteData)
29+
}
30+
31+
@Test("원격 이미지 요청 실패 시 메모리 캐시 데이터를 반환한다")
32+
func 원격_이미지_요청_실패_시_메모리_캐시_데이터를_반환한다() async throws {
33+
let cachedData = Data([1, 2, 3])
34+
let service = ProfileImageDataServiceSpy(data: cachedData)
35+
let store = ProfileImageMemoryCacheStoreSpy()
36+
let repository = ProfileImageDataRepositoryImpl(service: service, store: store)
37+
let url = URL(string: "https://example.com/avatar.png")!
38+
39+
_ = try await repository.fetchImageData(from: url)
40+
service.error = ProfileImageDataRepositoryImplTestsError.serviceFailed
41+
let data = try await repository.fetchImageData(from: url)
42+
43+
#expect(data == cachedData)
44+
#expect(service.calledURLs == [url, url])
45+
}
46+
}
47+
48+
private final class ProfileImageDataServiceSpy: ProfileImageDataService {
49+
var data: Data
50+
var error: Error?
51+
private(set) var calledURLs: [URL] = []
52+
53+
init(data: Data) {
54+
self.data = data
55+
}
56+
57+
func fetchImageData(from url: URL) async throws -> Data {
58+
calledURLs.append(url)
59+
60+
if let error {
61+
throw error
62+
}
63+
64+
return data
65+
}
66+
}
67+
68+
private final class ProfileImageMemoryCacheStoreSpy: MemoryCacheStore {
69+
private var values = [String: Any]()
70+
private(set) var storedData: Data?
71+
72+
func value<T: Codable>(forKey key: String) -> T? {
73+
values[key] as? T
74+
}
75+
76+
func setValue<T: Codable>(_ value: T?, forKey key: String) {
77+
guard let value else {
78+
values.removeValue(forKey: key)
79+
return
80+
}
81+
82+
values[key] = value
83+
storedData = value as? Data
84+
}
85+
}
86+
87+
private enum ProfileImageDataRepositoryImplTestsError: Error {
88+
case serviceFailed
89+
}

0 commit comments

Comments
 (0)