본문으로 건너뛰기

고급 포매팅 가이드라인

이 문서는 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
}

기억해 주세요. 이런 고급 패턴은 신중하게 사용해야 해요. 똑똑해 보이는 구현보다 코드의 명확성과 유지 보수성을 우선해 주세요. 확신이 서지 않으면 요구 사항을 충족하는 가장 단순한 접근을 선택해 주세요.