고급 포매팅 가이드라인
이 문서는 SideStore 프로젝트에서 다루게 될 고급 포매팅 상황, 아키텍처 패턴, 복잡한 Swift/Objective-C 기능을 정리해요.
고급 Swift 패턴
프로토콜 지향 프로그래밍
프로토콜 정의
- 프로토콜은 집중도와 응집도를 유지해 주세요.
- 제네릭 프로토콜에는 연관 타입을 활용해 주세요.
- 필요하다면 익스텐션에서 기본 구현을 제공해 주세요.
// ✅ 좋습니다
protocol AppInstalling {
associatedtype AppType: App
func install(_ app: AppType) async throws
func uninstall(_ app: AppType) async throws
}
extension AppInstalling {
func validateApp(_ app: AppType) -> Bool {
return !app.identifier.isEmpty && app.version.isValid
}
}
// ❌ 나빠요
protocol AppManager {
func install(_ app: Any) -> Bool
func uninstall(_ app: Any) -> Bool
func update(_ app: Any) -> Bool
func backup(_ app: Any) -> Bool
func restore(_ app: Any) -> Bool
// Too many responsibilities
}
프로토콜 합성
- 요구 사항이 복잡하면 프로토콜 합성을 활용해 주세요.
- 개별 프로토콜은 작고 집중되게 유지해 주세요.
// ✅ 좋습니다다
protocol Downloadable {
var downloadURL: URL { get }
func download() async throws -> Data
}
protocol Installable {
var installationRequirements: InstallationRequirements { get }
func install() async throws
}
typealias DeployableApp = App & Downloadable & Installable
class AppDeployer {
func deploy<T: DeployableApp>(_ app: T) async throws {
let data = try await app.download()
try await app.install()
}
}
제네릭과 타입 제약
제네릭 타입 정의
- 의미 있는 제약 이름을 사용해 주세요.
- 클래스 제약보다 프로토콜 제약을 선호해 주세요.
- 복잡한 제약은
where절로 표현해 주세요.
// ✅ 좋습니다
struct Repository<Entity: Identifiable & Codable> {
private var entities: [Entity.ID: Entity] = [:]
func save<T>(_ entity: T) where T == Entity {
entities[entity.id] = entity
}
func find<ID>(byId id: ID) -> Entity? where ID == Entity.ID {
return entities[id]
}
}
// ❌ 나빠요
struct Repository<T> {
private var items: [String: T] = [:]
// No type safety
}
고급 제네릭 제약
- 조건부 채택을 적절히 사용해 주세요.
- 필요하다면 팬텀 타입으로 타입 안전성을 높여 주세요.
// ✅ 좋습니다
extension Array: AppCollection where Element: App {
var installedApps: [Element] {
return filter { $0.isInstalled }
}
func sortedByInstallDate() -> [Element] {
return sorted { $0.installDate < $1.installDate }
}
}
// Phantom types for type safety
struct AppState<Status> {
let app: App
}
enum Downloaded {}
enum Installed {}
typealias DownloadedApp = AppState<Downloaded>
typealias InstalledApp = AppState<Installed>
Async/Await 패턴
비동기 함수 설계
- async/await를 일관되게 사용해 주세요.
- 동시 작업의 구조를 명확히 해 주세요.
- 취소 흐름을 적절히 처리해 주세요.
// ✅ 좋습니다
actor AppInstallationManager {
private var activeInstallations: [String: Task<Void, Error>] = [:]
func installApp(_ app: App) async throws {
// Prevent duplicate installations
if activeInstallations[app.identifier] != nil {
throw InstallationError.alreadyInstalling
}
let task = Task {
try await performInstallation(app)
}
activeInstallations[app.identifier] = task
defer {
activeInstallations.removeValue(forKey: app.identifier)
}
try await task.value
}
private func performInstallation(_ app: App) async throws {
// Check for cancellation at key points
try Task.checkCancellation()
let data = try await downloadApp(app)
try Task.checkCancellation()
try await installData(data, for: app)
}
}
// ❌ 나빠요
func installApp(_ app: App, completion: @escaping (Error?) -> Void) {
DispatchQueue.global().async {
// Mixing old completion handler style with new async code
let result = await self.downloadApp(app)
DispatchQueue.main.async {
completion(nil)
}
}
}
구조화된 동시성
- 관련 작업에는 태스크 그룹을 사용해 주세요.
- 비구조화된 태스크보다 구조화된 동시성을 선호해 주세요.
// ✅ 좋습니다
func installMultipleApps(_ apps: [App]) async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
for app in apps {
group.addTask {
try await self.installApp(app)
}
}
// Wait for all installations to complete
try await group.waitForAll()
}
}
// For independent results
func downloadMultipleApps(_ apps: [App]) async throws -> [App: Data] {
try await withThrowingTaskGroup(of: (App, Data).self) { group in
for app in apps {
group.addTask {
let data = try await self.downloadApp(app)
return (app, data)
}
}
var results: [App: Data] = [:]
for try await (app, data) in group {
results[app] = data
}
return results
}
}
리절트 빌더와 DSL
커스텀 리절트 빌더
- 단일 목적에 집중한 리절트 빌더를 만들어 주세요.
- 도메인 특화 작업에 명확한 문법을 제공해 주세요.
// ✅ 좋아요
@resultBuilder
struct AppConfigurationBuilder {
static func buildBlock(_ components: AppConfigurationComponent...) -> AppConfiguration {
return AppConfiguration(components: components)
}
static func buildOptional(_ component: AppConfigurationComponent?) -> AppConfigurationComponent? {
return component
}
static func buildEither(first component: AppConfigurationComponent) -> AppConfigurationComponent {
return component
}
static func buildEither(second component: AppConfigurationComponent) -> AppConfigurationComponent {
return component
}
}
// Usage
func configureApp(@AppConfigurationBuilder builder: () -> AppConfiguration) -> App {
let config = builder()
return App(configuration: config)
}
let app = configureApp {
AppName("SideStore")
AppVersion("1.0.0")
if debugMode {
DebugSettings()
}
Permissions {
NetworkAccess()
FileSystemAccess()
}
}
고급 Objective-C 패턴
카테고리 구성
- 관련 기능을 묶을 때 카테고리를 활용해 주세요.
- 카테고리 이름은 구체적으로 지어 주세요.
// ✅ 좋아요
@interface NSString (SSValidation)
- (BOOL)ss_isValidAppIdentifier;
- (BOOL)ss_isValidVersion;
@end
@interface UIViewController (SSAppInstallation)
- (void)ss_presentAppInstallationViewController:(SSApp *)app;
- (void)ss_showInstallationProgress:(SSInstallationProgress *)progress;
@end
// ❌ 나빠요
@interface NSString (Helpers)
- (BOOL)isValid; // Too generic
- (NSString *)cleanup; // Unclear purpose
@end
고급 메모리 관리
- 델리게이트 관계에는 적절한 패턴을 사용해 주세요.
- 복잡한 오브젝트 그래프를 올바르게 처리해 주세요.
// ✅ 좋아요
@interface SSAppInstaller : NSObject
@property (nonatomic, weak) id<SSAppInstallerDelegate> delegate;
@property (nonatomic, strong) NSOperationQueue *installationQueue;
@end
@implementation SSAppInstaller
- (instancetype)init {
self = [super init];
if (self) {
_installationQueue = [[NSOperationQueue alloc] init];
_installationQueue.maxConcurrentOperationCount = 3;
_installationQueue.name = @"com.sidestore.installation";
}
return self;
}
- (void)installApp:(SSApp *)app completion:(void (^)(NSError *))completion {
__weak typeof(self) weakSelf = self;
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
NSError *error = nil;
[strongSelf performInstallationForApp:app error:&error];
dispatch_async(dispatch_get_main_queue(), ^{
completion(error);
});
}];
[self.installationQueue addOperation:operation];
}
@end
블록 사용 패턴
- 비동기 작업에는 알맞은 블록 패턴을 사용해 주세요.
- 블록 안에서도 메모리 관리를 정확히 해 주세요.
// ✅ 좋아요
typedef void (^SSInstallationProgressBlock)(float progress);
typedef void (^SSInstallationCompletionBlock)(SSApp *app, NSError *error);
@interface SSAppDownloader : NSObject
- (NSURLSessionTask *)downloadApp:(SSApp *)app
progress:(SSInstallationProgressBlock)progressBlock
completion:(SSInstallationCompletionBlock)completion;
@end
@implementation SSAppDownloader
- (NSURLSessionTask *)downloadApp:(SSApp *)app
progress:(SSInstallationProgressBlock)progressBlock
completion:(SSInstallationCompletionBlock)completion {
NSURLRequest *request = [NSURLRequest requestWithURL:app.downloadURL];
__weak typeof(self) weakSelf = self;
NSURLSessionDownloadTask *task = [[NSURLSession sharedSession]
downloadTaskWithRequest:request
completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
if (error) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(nil, error);
});
return;
}
// Process downloaded file
[strongSelf processDownloadedFile:location
forApp:app
completion:completion];
}];
[task resume];
return task;
}
@end
아키텍처 패턴
MVVM 구현
- 모델, 뷰, 뷰모델의 역할을 명확히 분리해 주세요.
- 올바른 데이터 바인딩 패턴을 사용해 주세요.
// ✅ 나빠요
// Model
struct App {
let identifier: String
let name: String
let version: String
let isInstalled: Bool
}
// ViewModel
@MainActor
class AppListViewModel: ObservableObject {
@Published var apps: [App] = []
@Published var isLoading = false
@Published var errorMessage: String?
private let appService: AppService
init(appService: AppService) {
self.appService = appService
}
func loadApps() async {
isLoading = true
errorMessage = nil
do {
apps = try await appService.fetchAvailableApps()
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
func installApp(_ app: App) async {
do {
try await appService.installApp(app)
await loadApps() // Refresh the list
} catch {
errorMessage = "Failed to install \(app.name): \(error.localizedDescription)"
}
}
}
// View
struct AppListView: View {
@StateObject private var viewModel: AppListViewModel
init(appService: AppService) {
_viewModel = StateObject(wrappedValue: AppListViewModel(appService: appService))
}
var body: some View {
NavigationView {
List(viewModel.apps, id: \.identifier) { app in
AppRowView(app: app) {
Task {
await viewModel.installApp(app)
}
}
}
.navigationTitle("Available Apps")
.overlay {
if viewModel.isLoading {
ProgressView()
}
}
.alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
Button("OK") {
viewModel.errorMessage = nil
}
} message: {
Text(viewModel.errorMessage ?? "")
}
}
.task {
await viewModel.loadApps()
}
}
}
의존성 주입
- 테스트 가능성과 유연성을 위해 의존성 주입을 사용해 주세요.
- 규모가 크면 DI 컨테이너를 도입해 주세요.
// ✅ 좋습니다
protocol AppService {
func fetchAvailableApps() async throws -> [App]
func installApp(_ app: App) async throws
}
protocol NetworkService {
func downloadData(from url: URL) async throws -> Data
}
class DefaultAppService: AppService {
private let networkService: NetworkService
private let deviceService: DeviceService
init(networkService: NetworkService, deviceService: DeviceService) {
self.networkService = networkService
self.deviceService = deviceService
}
func fetchAvailableApps() async throws -> [App] {
let data = try await networkService.downloadData(from: appsURL)
return try JSONDecoder().decode([App].self, from: data)
}
func installApp(_ app: App) async throws {
guard deviceService.hasSpace(for: app) else {
throw InstallationError.insufficientStorage
}
let appData = try await networkService.downloadData(from: app.downloadURL)
try await deviceService.installApp(data: appData)
}
}
// DI Container
class ServiceContainer {
static let shared = ServiceContainer()
private init() {}
lazy var networkService: NetworkService = DefaultNetworkService()
lazy var deviceService: DeviceService = DefaultDeviceService()
lazy var appService: AppService = DefaultAppService(
networkService: networkService,
deviceService: deviceService
)
}
오류 처리 아키텍처
- 포괄적인 오류 처리 전략을 세워 주세요.
- 타입이 있는 오류를 사용해서 처리를 명확히 해 주세요.
// ✅ 좋아요
enum SideStoreError: Error {
case network(NetworkError)
case installation(InstallationError)
case device(DeviceError)
case validation(ValidationError)
}
enum NetworkError: Error {
case noConnection
case timeout
case serverError(Int)
case invalidResponse
}
enum InstallationError: Error {
case insufficientStorage
case incompatibleDevice
case corruptedFile
case alreadyInstalled
}
extension SideStoreError: LocalizedError {
var errorDescription: String? {
switch self {
case .network(let networkError):
return "Network error: \(networkError.localizedDescription)"
case .installation(let installError):
return "Installation error: \(installError.localizedDescription)"
case .device(let deviceError):
return "Device error: \(deviceError.localizedDescription)"
case .validation(let validationError):
return "Validation error: \(validationError.localizedDescription)"
}
}
}
// Error handling in services
class AppService {
func installApp(_ app: App) async throws {
do {
try validateApp(app)
} catch {
throw SideStoreError.validation(error as! ValidationError)
}
do {
try await performInstallation(app)
} catch let error as NetworkError {
throw SideStoreError.network(error)
} catch let error as InstallationError {
throw SideStoreError.installation(error)
}
}
}
성능 고려 사항
메모리 최적화
- 비용이 큰 리소스에는 지연 로딩을 사용해 주세요.
- 적절한 캐싱 전략을 구현해 주세요.
// ✅ 좋아요
class AppImageCache {
private let cache = NSCache<NSString, UIImage>()
private let downloadQueue = DispatchQueue(label: "image-download", qos: .utility)
init() {
cache.countLimit = 50
cache.totalCostLimit = 50 * 1024 * 1024 // 50MB
}
func image(for app: App) async -> UIImage? {
let key = app.identifier as NSString
// Check cache first
if let cachedImage = cache.object(forKey: key) {
return cachedImage
}
// Download if not cached
return await withCheckedContinuation { continuation in
downloadQueue.async { [weak self] in
guard let self = self else {
continuation.resume(returning: nil)
return
}
do {
let data = try Data(contentsOf: app.iconURL)
let image = UIImage(data: data)
if let image = image {
self.cache.setObject(image, forKey: key)
}
continuation.resume(returning: image)
} catch {
continuation.resume(returning: nil)
}
}
}
}
}
스레딩 모범 사례
- 알맞은 큐 우선순위를 사용해 주세요.
- 컨텍스트 스위칭을 최소화해 주세요.
// ✅ 좋아요
actor BackgroundProcessor {
private let processingQueue = DispatchQueue(
label: "background-processing",
qos: .utility,
attributes: .concurrent
)
func processLargeDataSet(_ data: [LargeDataItem]) async -> [ProcessedItem] {
return await withTaskGroup(of: ProcessedItem?.self, returning: [ProcessedItem].self) { group in
let chunkSize = max(data.count / ProcessInfo.processInfo.activeProcessorCount, 1)
for chunk in data.chunked(into: chunkSize) {
group.addTask {
return await self.processChunk(chunk)
}
}
var results: [ProcessedItem] = []
for await result in group {
if let processed = result {
results.append(processed)
}
}
return results
}
}
private func processChunk(_ chunk: [LargeDataItem]) async -> ProcessedItem? {
// CPU-intensive processing
return await withCheckedContinuation { continuation in
processingQueue.async {
let result = chunk.map { self.expensiveOperation($0) }
continuation.resume(returning: ProcessedItem(results: result))
}
}
}
}
테스트 패턴
프로토콜 기반 테스트
- 테스트에서도 의존성 주입을 위해 프로토콜을 사용해 주세요.
- 목적에 맞는 테스트 더블을 만들어 주세요.
// ✅ 좋아요
class MockAppService: AppService {
var shouldFailInstallation = false
var installedApps: [App] = []
func fetchAvailableApps() async throws -> [App] {
return [
App(identifier: "test.app1", name: "Test App 1", version: "1.0.0"),
App(identifier: "test.app2", name: "Test App 2", version: "2.0.0")
]
}
func installApp(_ app: App) async throws {
if shouldFailInstallation {
throw InstallationError.insufficientStorage
}
installedApps.append(app)
}
}
class AppListViewModelTests: XCTestCase {
private var mockAppService: MockAppService!
private var viewModel: AppListViewModel!
override func setUp() {
super.setUp()
mockAppService = MockAppService()
viewModel = AppListViewModel(appService: mockAppService)
}
@MainActor
func testLoadAppsSuccess() async {
await viewModel.loadApps()
XCTAssertEqual(viewModel.apps.count, 2)
XCTAssertFalse(viewModel.isLoading)
XCTAssertNil(viewModel.errorMessage)
}
@MainActor
func testInstallAppFailure() async {
mockAppService.shouldFailInstallation = true
let testApp = App(identifier: "test", name: "Test", version: "1.0")
await viewModel.installApp(testApp)
XCTAssertNotNil(viewModel.errorMessage)
XCTAssertTrue(viewModel.errorMessage!.contains("insufficient storage"))
}
}
문서화 기준
복잡한 API 문서
- 복잡한 동작과 엣지 케이스를 문서화해 주세요.
- 비자명한 API에는 사용 예시를 제공해 주세요.
/**
* A thread-safe manager for handling app installations with automatic retry logic.
*
* This class manages the installation process for iOS applications, handling
* network downloads, signature verification, and device communication.
* It provides automatic retry functionality for transient failures and
* comprehensive error reporting.
*
* ## Usage Example
* ```swift
* let installer = AppInstallationManager()
*
* do {
* let result = try await installer.installApp(
* from: app.sourceURL,
* identifier: app.bundleID,
* maxRetries: 3
* )
* print("Installation completed: \(result.installedPath)")
* } catch InstallationError.insufficientStorage {
* // Handle storage error
* } catch {
* // Handle other errors
* }
* ```
*
* ## Thread Safety
* This class is thread-safe and can be called from any queue. All completion
* handlers are called on the main queue unless otherwise specified.
*
* ## Error Handling
* Installation failures are categorized into recoverable and non-recoverable
* errors. Recoverable errors (network timeouts, temporary device issues) will
* be automatically retried up to the specified limit. Non-recoverable errors
* (invalid signatures, incompatible devices) will fail immediately.
*/
@MainActor
class AppInstallationManager {
// Implementation
}
기억해 주세요. 이런 고급 패턴은 신중하게 사용해야 해요. 똑똑해 보이는 구현보다 코드의 명확성과 유지 보수성을 우선해 주세요. 확신이 서지 않으면 요구 사항을 충족하는 가장 단순한 접근을 선택해 주세요.