^(코딩캣)^ = @"코딩"하는 고양이;
[번역글] Thread-safe한 싱글톤(singleton) 패턴과 그 사용법(3) [完]
이 글은 Sachithra Siriwardhane 님의 글 Thread-safe singletons and their usage in Swift를 바탕으로 작성되었습니다. Thread-safe한 싱글톤(singleton)과 그 사용법 개발자라면 다들 ‘싱글톤 패턴(singleton pattern)’에 대해 알고 있을 것이고 사용해 보셨을 것이며, iOS 어플리케이션에서 구현이 가능하실 것입니다. 이 글은 입문자 또는 싱글톤 디자인 패턴(design pattern)을 적절하게 사용하기 위한 배경 지식 향상을 원하는 분들에게 도움이 되어 드리고자 작성되었습니다. 이 글에서는 다음과 같은 주제들을 다뤄볼 것입니다. 싱글톤 패턴의 사용: 해야 할 것과 하지 말아야 할 것 Swift/Objective-C에서 싱글..
Language/Objective-C & Swift
2021. 9. 24. 09:41

[번역글] Thread-safe한 싱글톤(singleton) 패턴과 그 사용법(3) [完]

Language/Objective-C & Swift
2021. 9. 24. 09:41

이 글은 Sachithra Siriwardhane 님의 글 Thread-safe singletons and their usage in Swift를 바탕으로 작성되었습니다.

Thread-safe한 싱글톤(singleton)과 그 사용법

개발자라면 다들 ‘싱글톤 패턴(singleton pattern)’에 대해 알고 있을 것이고 사용해 보셨을 것이며, iOS 어플리케이션에서 구현이 가능하실 것입니다. 이 글은 입문자 또는 싱글톤 디자인 패턴(design pattern)을 적절하게 사용하기 위한 배경 지식 향상을 원하는 분들에게 도움이 되어 드리고자 작성되었습니다. 이 글에서는 다음과 같은 주제들을 다뤄볼 것입니다.

 

동시성 문제 및 thread-safe하게 싱글톤 패턴을 작성하기

Swift에서 let 키워드로 선언되는 식별자는 상수입니다. 그러므로 읽기 전용이면서 thread-safe입니다. var로 선언되는 식별자는 수정이 가능하지만 그 데이터 타입이 특별히 thread-safe하면서 수정 가능하게 지정되지 않았다면 thread-safe하지 않습니다. Swift에서 ArrayDictionary 같은 데이터 타입들은 수정 가능하더라도 thread-safe하지 않습니다.

위에서 언급한 Dictionary는 수정 가능합니다. 또한 여러 쓰레드에서 동시에 읽기만 할 때는 아무 문제가 없습니다. 하지만 다른 쓰레드들이 읽기만 하고 있는데 한 쓰레드가 수정하려 한다면 그 구조가 안전하지 않게 됩니다. 위의 예제에서 보인 싱글톤은 그러한 문제가 발생하는 것을 막아주지 못합니다. 이에 대한 해답은 DispatchQueue에 있습니다. Apple에 따르면 DispatchQueue의 의미는 다음과 같습니다.

여러분의 앱의 메인 쓰레드 또는 백그라운드 쓰레드에서 작업의 동기 또는 동시적 실행을 관리하는 객체(An object that manages the execution of tasks serially or concurrently on your app's main thread or on a background thread).

DispatchQueue에 대한 자세한 정보는 다음 Apple 문서를 참조하시기 바랍니다: https://developer.apple.com/documentation/dispatch/dispatchqueue

 

Concurrent Dispatch Queue

Concurrent Dispatch Queue는 여러 개의 작업들을 한 번에 실행하는데, 호출되는 순서는 작업이 큐(queue)에 추가된 순서에 따릅니다. 하지만 각 작업은 병렬로 실행 중이기 때문에 종료되는 순서는 제각각이 될 수 있습니다. DispatchQueue는 작업들이 실행되고 있는 쓰레드(thread)들을 관리하는 역할을 합니다.

위 예제에서 동시 사용은 어떻게 표현할 수 있을까요? 예시를 위해 동시적 실행은 다음과 같이 단위 테스트로 작성될 수 있습니다.

// Swift
func testConcurrentUsage throws {
    let concurrentLabel = "concurrentQueue"
    let concurrentQueue = DispatchQueue(label: concurrentLabel, attributes: .concurrent)
    let iterations = 100
    
    for currentIteration in 1 ... iterations {
        concurrentQueue.async {
            let key = "\(currentIteration)"
            EventLogger.shared.writeToLog(key: key, content: "iteration = \(currentIteration)")
        }
    }
    
    while EventLogger.shared.readLog(for: "\(iterations)") != String("iteration = \(currentIteration)") {
        // a blocker
    }
    
    expect.fulfill()
    
    waitForExpectations(timeout: 5) { (error) in
        XCTAssertNil(error, "Concurrent expectation failed")
    }
}

위 코드는 이 글에서 앞서 작성한 싱글톤을 동시적으로 사용해보기 위해 작성된 단위 테스트를 보여주고 있습니다. 먼저 concurrentQueuewriteToLog 함수를 동시에 병렬로 여러 번 호출할 수 있도록 만들어졌습니다. 그리고 for 반복문에서 writeToLog에 대한 작업을 큐에 50회 추가하는데, 그 결과 EventLogger 내부의 Dictionary에는 항목들이 추가됩니다. 이와 동시에 readLog 메소드가 반복적으로 호출되면서 읽기와 쓰기 작업이 동시적으로 적절하게 실행되고 있는지 확인하게 됩니다. 하지만...

// Swift
final public class EventLogger {
    // ...
    public func readLog(for key: String) -> String? {
        return eventsFired[key] as? String // Thread 1: EXC_BAD_ACCESS
    }
    // ...
}

앱이 실행 중에 충돌합니다. 현재 작업이 읽기인지 쓰기인지와는 무관하게 writeToLogreadLog 중 하나에서 충돌이 계속됩니다. 근본적인 문제는 Dictionary라는 데이터 타입 자체가 thread-safe하지 않기 때문입니다. 그리고 현재와 같은 구현은 동시 접근에 대해 올바르게 방호해주지 못하고 있습니다.

 

싱글톤을 thread-safe하게 만들기

Serial Dispatch Queue는 오직 한 번에 한 작업만을 실행시킵니다. 대부분의 경우 시리얼 큐(Serial Queue)는 데이터 폭주가 발생하는 것을 막으려고 특정 값이나 자원을 동기적으로 접근하기 위해 사용됩니다.

병렬이면서 동시적인 읽기와 쓰기 작업은 데이터 충돌을 일그이고 결과적으로 앱이 충돌하게 만듭니다. 그래서 읽기와 쓰기 작업이 병렬로 발생하지 않도록 확인해 주는 것이 해결책이 될 수 있습니다. 다시 표현하면, 읽기와 쓰기가 동시에 발생하는 것을 막는 것이 이 문제 해결책으로 보입니다. 그런 의미에서 serial queue는 한 번에 한 작업만이 호출되는 것을 보장해 줍니다.

// Swift
final public class EventLogger {
    // ...
    private func readLog(for key: String) -> String? {
        var value: String? = nil
        serialQueue.sync {
            value = eventsFired[key] as? String
        }
        return value
    }
    
    private func writeLog(key: String, content: Any) {
        serialQueue.sync {
            eventFired[key] = content
        }
    }
}
// Objective-C
@implementation EventLogger {
    // ...
    dispatch_queue_t serialQueue;
}
// ...

- (instancetype)init {
    if (self = [super init]) {
        // ...
        self->serialQueue = dispatch_queue_create("kr.codingcat.EventLoggerExample", NULL);
    }
    return self;
}


- (NSString *)readLogForKey:(nonnull NSString *)key {
    __block NSString * value = nil;
    
    dispatch_sync(serialQueue, ^{
        value = [self->eventFired valueForKey:key];
    });
    
    return value;
}

- (void)writeLogForKey:(nonnull NSString *)key andContent:(nullable id)content {
    dispatch_sync(serialQueue, ^{
        [self->eventFired setValue:content forKey:key];
    });
}
// ...
@end

읽기와 작업 모두 시리얼 큐 안에 추가되어서 순서대로 실행되는 것이 보장됩니다. 하지만 병렬로 처리되던 작업들 일부가 이와 같이 특정 부분에서 직렬로 실행되는 구조라면 성능 문제가 발생할 수 있기에 이 방법을 사용할 때는 추가적인 최적화 작업이 필요합니다.

 

읽기-쓰기 잠금

위키백과 “Readers-Writer lock”에 따르면, Readers-Writer Lock이란 읽기 전용 작업에 대해서는 동시 접근을 허용하고, 쓰기 작업을 할 때는 배타적인 접근 권한을 부여하는 방식을 뜻합니다. 즉 어떤 데이터가 있을 때 그것을 읽는 것이라면 여러 쓰레드에서 동시에 읽어가도 상관 없지만, 그 데이터를 수정하거나 기록하는 작업이라면 배타적인 권한을 얻을 때까지 기다려야 한다는 것입니다. 쓰기 작업이 데이터를 기록하고자 할 때 그 외 쓰기가 필요한 다른 쓰레드 및 읽기 작업만 하려 했던 쓰레드들까지도 이 쓰기 작업이 완료될 때까지 일시 중지 됩니다. 여기서 중요한 개념 두 가지가 등장합니다.

데이터 경쟁(data race) 여러 쓰레드에서 동일한 메모리를 동기화 메커니즘도 갖춰지지 않은 채 접근하려 할 때 발생합니다. 그리고 접근하려는 쓰레드 중 최소한 하나는 쓰기 작업이 포함될 때 발생합니다. 예를 들어 메인 쓰레드에서 어떤 Dictionary로부터 값을 읽고 있을 때, 동시에 백그라운드 쓰레드에서는 이 Dictionary를 수정하려 드는 경우입니다.

장벽(barrier) 장벽은 동기적으로 쓰기 작동을 하기 위해 사용되는 concurrent queue입니다. barrier 플래그(flag)를 지시함으로써 특정 자원이나 값을 thread-safe한 방식으로 접근하는 것이 가능합니다. 그러므로 동시적으로 읽기 작업을 하면서 얻는 이점을 유지하면서도 동기적으로 쓰기 작업도 가능합니다.

 

데이터 경쟁을 예방하면서도 병렬 처리도 수행하기 위해 concurrent queue를 사용하면 이점을 얻는 경우들이 있습니다. 장벽은 여기서 필요하게 됩니다. concurrent queue를 사용하면 Dictionary에 대한 읽기 및 쓰기에 대한 작업이 병렬로 발생할 수 없습니다. 때문에 동시적인 읽기 작업은 성능 향상으로 이어질 수 있습니다.

읽기 작업의 호출을 직렬화하는 코드는 제거될 수 있지만 Dictionary의 내용이 이미 수정되고 있을 때 동시에 다른 쓰레드에서 읽기 및 쓰기될 수는 없고, Dictionary 내용의 수정 작업은 다른 쓰레드에서 읽기 및 쓰기를 시도하기 전에 완료되어야만 합니다. Dispatch 장벽 및 Grand Central Dispatch를 사용하면 이를 해결할 수 있습니다.

여기서 장벽 또는 ‘배리어(barrier)’는 동시적 큐와 비동시적 큐 사이를 전환하면서 ‘배리어 블록(barrier block)’에 있는 코드 실행이 끝날 때까지 비동시적(serial) 큐로 작동되다가 배리어 블록을 벗어나면 동시적 큐로 전환되는 특징이 있습니다. 위 싱글톤 클래스에서 비동시적 큐는 동시적 큐로 대체되고 writeToLog 메소드가 Dictionary를 수정할 때 배리어 블록이 설정됩니다. 여기서 writeToLog 메소드에 있을 배리어 블록은 이전에 호출되었던 모든 블록들이 다 종료될때까지 기다리게 되는데, 이렇게 하면 데이터 경쟁이나 데이터 충돌이 예방됩니다. readLog 메소드는 읽기만을 할 것이므로 동시적 큐에 의해 작동되며 병렬로 작동됩니다.

// Swift
import Foundation

final public class EventLogger {
    public static let shared = EventLogger()
    
    private var eventFired: [String:Any] = ["initialization":"Property initialized"]
    private let concurrentLabel = "concurrentQueue"
    private let concurrentQueue = DispatchQueue(label: concurrentLabel, attribute: .concurrent)
    
    private init() {
    }
    
    public func readLog(for key: String) -> String? {
        var value: String?
        concurrentQueue.sync {
            // 동시에 실행될 수 있는 부분
            value = eventFired[key] as? string
        }
        return value
    }
    
    public func writeToLog(key: String, content: Any) {
        concurrentQueue.async(flags: .barrier) {
            // 동기적으로 실행되어야 하는 부분
            self.eventFired[key] = content
        }
    }
}

이제 이 싱글톤 패턴은 thread-saf하면서도 성능도 최적화되었습니다.

카테고리 “Language/Objective-C & Swift”
more...
[번역글] Thread-safe한 싱글톤(singleton) 패턴과 그 사용법(2)
이 글은 Sachithra Siriwardhane 님의 글 Thread-safe singletons and their usage in Swift를 바탕으로 작성되었습니다. Thread-safe한 싱글톤(singleton)과 그 사용법 개발자라면 다들 ‘싱글톤 패턴(singleton pattern)’에 대해 알고 있을 것이고 사용해 보셨을 것이며, iOS 어플리케이션에서 구현이 가능하실 것입니다. 이 글은 입문자 또는 싱글톤 디자인 패턴(design pattern)을 적절하게 사용하기 위한 배경 지식 향상을 원하는 분들에게 도움이 되어 드리고자 작성되었습니다. 이 글에서는 다음과 같은 주제들을 다뤄볼 것입니다. 싱글톤 패턴의 사용: 해야 할 것과 하지 말아야 할 것 Swift/Objective-C에서 싱글..
Language/Objective-C & Swift
2021. 9. 24. 09:40

[번역글] Thread-safe한 싱글톤(singleton) 패턴과 그 사용법(2)

Language/Objective-C & Swift
2021. 9. 24. 09:40

이 글은 Sachithra Siriwardhane 님의 글 Thread-safe singletons and their usage in Swift를 바탕으로 작성되었습니다.

Thread-safe한 싱글톤(singleton)과 그 사용법

개발자라면 다들 ‘싱글톤 패턴(singleton pattern)’에 대해 알고 있을 것이고 사용해 보셨을 것이며, iOS 어플리케이션에서 구현이 가능하실 것입니다. 이 글은 입문자 또는 싱글톤 디자인 패턴(design pattern)을 적절하게 사용하기 위한 배경 지식 향상을 원하는 분들에게 도움이 되어 드리고자 작성되었습니다. 이 글에서는 다음과 같은 주제들을 다뤄볼 것입니다.

 

Swift/Objective-C에서 싱글톤 패턴의 작성

다음 소스는 데이터를 수집하여 관련된 호출자에게 이 데이터를 반환하기 위해 준비된 EventLogger 클래스를 싱글톤으로 작성한 기본적인 예제입니다.

// Swift
import Foundation

final public class EventLogger {
    public static let shared = EventLogger()
    
    private var eventFired: [String: Any] = ["initialization":"Property initialized"]
    
    private init() {
    }
    
    private func readLog(for key: String) -> String? {
        return eventFired[key] as? String
    }
    
    private func writeLog(key: String, content: Any) {
        eventFired[key] = content
    }
}

아래는 Swift 코드에 해당하는 Objective-C 코드이지만 완전히 일치하지는 않을 것입니다.

// Objective-C
@interface EventLogger : NSObject
@end

@implementation EventLogger {
    NSDictionary * eventFired;
}

+ (instancetype)sharedEventLogger {
    static dispatch_once_t once = 0;
    static id sharedInstance = nil;
    
    dispatch_once(&once, ^{
        sharedInstance = [[self alloc] init];
    });
    
    return sharedInstance;
}

- (instancetype)init {
    if (self = [super init]) {
        self->eventFired = [[NSDictionary alloc] initWithObjectsAndKeys:@"initialization",@"Property initialized", nil];
    }
    return self;
}

- (void)dealloc {
    // ...
}

- (NSString *)readLogForKey:(nonnull NSString *)key {
    return [self->eventFired valueForKey:key];
}

- (void)writeLogForKey:(nonnull NSString *)key andContent:(nullable id)content {
    [self->eventFired setValue:content forKey:key];
}
@end

여기서 EventLogger는 파생 클래스가 생겨나지 않도록 final 키워드로 선언되었습니다.

 

public static let shared = EventLogger()

shared라고 이름이 적힌 이 상수는 EventLogger 클래스형 인스턴스를 참조하는 싱글톤입니다. 여기서 static 키워드는 클래스 자체에 소속된 프로퍼티이지 그 클래스의 인스턴스에 소속된 프로퍼티가 아니라는 뜻입니다. 이는 해당 프로퍼티를 보다 전역적인 범위에서 접근 가능하도록 해 줍니다.

 

private init()

클래스의 생성자는 클래스 외부에서 새 인스턴스가 만들어지지 못하도록 private로 선언되었는데 이는, 이 클래스의 생성자가 이 클래스 내부적으로만 호출될 수 있도록 보장해 줍니다.

 

다음은 싱글톤이 사용되는 예입니다. readLog 메소드가 옵셔널 문자열을 반환함에 따라 값을 ‘언랩(unwrap)’하여 사용하기 위해 옵셔널 바인딩이 사용되었습니다.

// Swift
if let contents = EventLogger.shared.readLog(for: "initialization") {
    print(contents)
} else {
    print("File not found")
}
// Objective-C
NSString * contents = [[EventLogger sharedEventLogger] readLogForKey:@"initialization"];

if (contents != nil) {
    NSLog(contents);
} else {
    NSLog(@"File not found");
}

 

카테고리 “Language/Objective-C & Swift”
more...
[번역글] Thread-safe한 싱글톤(singleton) 패턴과 그 사용법(1)
이 글은 Sachithra Siriwardhane 님의 글 Thread-safe singletons and their usage in Swift를 바탕으로 작성되었습니다. Thread-safe한 싱글톤(singleton)과 그 사용법 개발자라면 다들 ‘싱글톤 패턴(singleton pattern)’에 대해 알고 있을 것이고 사용해 보셨을 것이며, iOS 어플리케이션에서 구현이 가능하실 것입니다. 이 글은 입문자 또는 싱글톤 디자인 패턴(design pattern)을 적절하게 사용하기 위한 배경 지식 향상을 원하는 분들에게 도움이 되어 드리고자 작성되었습니다. 이 글에서는 다음과 같은 주제들을 다뤄볼 것입니다. 싱글톤 패턴의 사용: 해야 할 것과 하지 말아야 할 것 Swift/Objective-C에서 싱글..
Language/Objective-C & Swift
2021. 9. 22. 20:49

[번역글] Thread-safe한 싱글톤(singleton) 패턴과 그 사용법(1)

Language/Objective-C & Swift
2021. 9. 22. 20:49

이 글은 Sachithra Siriwardhane 님의 글 Thread-safe singletons and their usage in Swift를 바탕으로 작성되었습니다.

Thread-safe한 싱글톤(singleton)과 그 사용법

개발자라면 다들 ‘싱글톤 패턴(singleton pattern)’에 대해 알고 있을 것이고 사용해 보셨을 것이며, iOS 어플리케이션에서 구현이 가능하실 것입니다. 이 글은 입문자 또는 싱글톤 디자인 패턴(design pattern)을 적절하게 사용하기 위한 배경 지식 향상을 원하는 분들에게 도움이 되어 드리고자 작성되었습니다. 이 글에서는 다음과 같은 주제들을 다뤄볼 것입니다.

 

싱글톤 패턴의 사용: 해야 할 것과 하지 말아야 할 것

싱글톤 패턴의 주된 목적은 주어진 타입의 인스턴스를 단 하나만 생성하여 여럿이 돌려 쓰는 경우를 확실하게 하기 위함입니다. 해당 어플리케이션 전반에서 접근되고 관리될 필요가 있는 싱글톤 자원이 있을 때 싱글톤 패턴은 유용하게 사용될 것입니다. 싱글톤 패턴은 UIApplication, FileManager, UserDefaults, URLSessionUIAccelerometer 등 Apple의 플랫폼에서 흔히 쓰입니다. 예를 들어 어떤 앱에 대한 UIApplication 클래스 인스턴스는 반드시 존재해야 하지만, 그것이 또한 여러 개가 존재해서는 안 됩니다. 다음은 Apple의 플랫폼에서 싱글톤으로 구현되는 몇 가지 예입니다.

UIApplication.shared, UserDefaults.standard, FileManager.default, URLSession.sharedOperationQueue.main 등...

 

싱글톤 패턴의 잘못된 사용

다른 디자인 패턴에 비해 싱글톤 패턴은 상대적으로 구현이 쉽기는 하나 실무에서 잘못 사용될 가능성도 있습니다. 뷰 컨트롤러(View Controller) 사이에서 데이터를 교환하거나, 전역 다목적 컨테이너로서 싱글톤 패턴을 사용하는 경우가 개발자들 사이에서 흔히 있는 일인데, 이는 싱글톤 디자인 패턴 남용의 대표적 사례이며 single responsibility principle(SOLID principle)에 반하는 코딩 스타일입니다(SOLID principle이란 어떤 프로그램에 있는 모든 클래스들은 하나의 기능만을 담당해야 한다는 원칙입니다).

게다가 싱글톤 패턴이 일단 한 번 구현되었다면, 싱글톤에 의돈하는 객체들의 추적을 어렵게 만들어서 결과적으로 상호 의존성(coupling)을 높이는 결과가 됩니다. 더 나아가서는 이 싱글톤을 수정하기 위해 상당한 비용이 소요될 것입니다.

동시성 문제를 피하고자 어떤 대상을 보호하기 위해 thread-safe한 싱글톤으로 작성할 경우, 다중 스레드로 병렬처리가 될 때 해당 객체를 접근하는 부분에서 병목현상이 발생할 수도 있음을 염두에 두시기 바랍니다. 그러므로 꼭 필요할 때 성능을 고려해가면서 싱글톤 패턴을 사용하는 것이 중요합니다.

 

카테고리 “Language/Objective-C & Swift”
more...
[번역글] Objectiv-C 블록(block)으로 작업하기(3) [完]
이 글은 애플 측 개발자 문서인 Working with Blocks를 바탕으로 작성한 것입니다. Swift의 ‘클로저(closure)’에 대응하는 Objective-C의 ‘블록(block)’에 대한 내용입니다. Objectiv-C 블록(block)으로 작업하기 Objective-C 클래스는 데이터 및 이에 관계된 행동으로 구성된 객체를 정의합니다. 때때로 Objective-C에서는 일련의 메소드들보다는 하나의 작업 또는 단위 행동을 표현하는 다른 방법을 찾는 것이 더 나은 경우도 있습니다. 블록(block)은 C, Objective-C 및 C++에서 마치 변수처럼 다른 곳으로 어떤 메소드나 함수 조각을 전달하기 위한 언어 수준에서 정의된 기능입니다. 블록은 Objective-C에서 객체인데 이는 NSAr..
Language/Objective-C & Swift
2021. 9. 22. 15:07

[번역글] Objectiv-C 블록(block)으로 작업하기(3) [完]

Language/Objective-C & Swift
2021. 9. 22. 15:07

이 글은 애플 측 개발자 문서인 Working with Blocks를 바탕으로 작성한 것입니다. Swift의 ‘클로저(closure)’에 대응하는 Objective-C의 ‘블록(block)’에 대한 내용입니다.

Objectiv-C 블록(block)으로 작업하기

Objective-C 클래스는 데이터 및 이에 관계된 행동으로 구성된 객체를 정의합니다. 때때로 Objective-C에서는 일련의 메소드들보다는 하나의 작업 또는 단위 행동을 표현하는 다른 방법을 찾는 것이 더 나은 경우도 있습니다. 블록(block)은 C, Objective-C 및 C++에서 마치 변수처럼 다른 곳으로 어떤 메소드나 함수 조각을 전달하기 위한 언어 수준에서 정의된 기능입니다. 블록은 Objective-C에서 객체인데 이는 NSArrayNSDictionary 같은 콜렉션으로 추가될 수 있음을 뜻합니다. 블록은 또한 자신을 감싸고 있는 스코프에서 값을 캡쳐할 수 있는 능력을 가지고 있는데 이는 타 프로그래밍 언어에서 ‘클로저(closure)’나 ‘람다(lambda)’와 비슷합니다. 이 장에서는 블록을 선언하고 참조하는 구문에 대해 설명하고 콜렉션 열거와 같은 통상적인 작업에서 블록을 사용하는 방법에 대해 보일 것입니다.

  1. 블록 기본 구문; 블록의 매개변수 및 반환 형식
  2. 블록의 스코프 캡쳐와 strong 순환 참조 해결
  3. 블록 응용 예

 

블록은 열거 작업을 간단하게 할 수 있다.

completion handler 뿐만 아니라 많은 Cocoa 및 Cocoa Touch API는 공통의 작업을 단순화시키는 데 블록 구문을 쓰기도 합니다. 예를 들어 콜렉션 열거와 같은 작업들이 있습니다. NSArray 클래스는 세 가지의 블록 기반 메소드를 가지고 있습니다. 예를 들어,

- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL * stop))block;

이 메소드는 한 개의 매개변수를 받는데 이는 배열 내 각 원소에 도달할 때마다 호출될 블록입니다.

// Objective-C
NSArray * array = /* ... */;
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL * stop) {
    NSLog(@"array[%lu] = \"%@\"", idx, obj);
}];

블록 자체는 세 개의 매개변수를 받습니다. 그 중 처음 두 개는 현재 참조된 객체 및 배열에서 그 원소의 인덱스입니다. 세 번째 매개변수는 boolean 변수에 대한 포인터로서, 열거 작업을 중지시키고 싶을 때 사용될 수 있습니다. 예를 들어 다음과 같습니다.

// Objective-C
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL * stop) {
    if (/* ... */) {
        *step = YES;
    }
}

물론 enumerateObjectsWithOptions:usingBlock: 메소드를 사용하여 열거 작업에 대한 옵션을 지정할 수도 있는데 예를 들어, NSEnumerationReverse를 사용하면 배열에서 각 원소들을 역순으로 참조합니다.

열거 작업을 하는 블록이 processor-intensive 하면서 동시 실행을 해도 안전한 경우라면 NSEnumerationConcurrent 옵션을 사용할 수도 있습니다.

// Objective-C
[array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id obj, NSUInteger idx, BOOL * stop) {
    // ...
}

이 플래그를 지정할 경우 블록의 실행이 여러 쓰레드에 걸쳐거 분신될 수 있습니다. 만일 블록 내의 코드가 processor-intensive하다면 이 속성은 잠재적인 성능 향상을 가져다 줄 것입니다. 다만 주의할 것은 이 옵션을 사용하면 각 원소들의 접근 순서가 일정하지 않게 됩니다.

NSDictionary 클래스도 블록 기반의 메소드를 다음과 같이 제공합니다.

// Objective-C
NSDictionary * dictionary = /* ... */;
[dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL * stop) {
    NSLog(@"dictionary[\"%@\"] = \"%@\"", key, obj);
}];

이렇게 열거 메소드를 사용하여 각 원소들을 순회한다면 통상적인 반복문을 사용하는 것보다 더 편리합니다.

 

블록은 동시 수행될 작업을 단순화시킬 수 있다.

블록은 “실행 가능한 코드와 스코프로부터 캡쳐된 옵셔널 상태들로 이루어진 구분되는 단위 작업(a distinct unit of work, combining executable code with optional state captured from the surrounding scope)”입니다. 이것은 OS X이나 iOS에서 지원하는 동시성 옵션 중 하나를 사용하게 되는 비동기 호출과도 잘 맞습니다. 쓰레드(thread)처럼 저 수준 메커니즘을 가지고 어떻게 작업해야 하나 찾아봐야 하는 대신, 여러분은 블록을 사용하여 여러분의 작업을 간단하게 정의하고 그 작업을 프로세서 자원이 가능하게 되는대로 시스템에게 수행하도록 맡기면 됩니다.

OS X이나 iOS는 태스크 스케줄링 메커니즘인 Operation Queue와 Grand Central Dispatch를 포함하여 다양한 동시성 기술을 제공하는데, 이들 메커니즘은 호출되어야 할 작업들이 순서를 기다리고 있는 큐(queue)의 형태로부터 구현된 것입니다. 호출시키고자 하는 블록을 큐에 추가하면 시스템은 프로세서 시간과 자원이 사용 가능해졌을 때 순서대로 큐에서 블록들을 꺼내게 됩니다.

시리얼 큐(serial queue)는 한 번에 하나의 오직 하나의 작업만을 실행합니다. 이전에 실행해 두었던 작업이 종료되기 전까지는 큐에 대기 중인 다음 작업이 호출되지 않습니다. 동시성 큐(concurrent queue)는 이전 작업이 종료되기를 기다리지 않고 가능한 많은 작업들을 한꺼번에 호출합니다.

 

오퍼레이션 큐를 가지고 블록 오퍼레이션 사용하기

오퍼레이션 큐(operation queue)는 작업 스케줄링을 하기 위한 Cocoa 및 Cocoa Touch의 접근법입니다. 여러분은 실행할 작업과 이에 수반되는 각종 데이터들을 NSOperation 클래스형 인스턴스로 캡슐화할 수 있습니다. 이렇게 해서 만들어진 오퍼레이션을 실행을 위해 NSOperationQueue로 전달할 수 있습니다. 물론 여러분은 복잡한 작업들을 구현하기 위해 NSOperation을 상속받아서 새로운 클래스를 만들 수도 있기는 하나 다음과 같이 미리 준비된 NSBlockOperation을 사용하면 블록을 생성해서 작업을 기술할 수 있습니다.

// Objective-C
NSBlockOperation * operation = [NSBlockOperation blockOperationWithBlock:^{
    // ...
}];

이렇게 만든 오퍼레이션은 여러분이 직접 호출할 수도 있지만 대개는 이미 존재하는 오퍼레이션 큐, 또는 여러분이 새로 만든 오퍼레이션 큐에 추가하여 실행되도록 대기시킵니다.

// Objective-C
// main queue에서 작업 스케줄링
[[NSOperationQueue mainQueue] addOperation:operation];

// background queue에서 작업 스케줄링
NSOperationQueue * queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];

여러분이 오퍼레이션 큐를 사용한다면 여러분은 큐에 있는 오퍼레이션 사이의 우선 순위라든가 의존성을 설정할 수도 있습니다. 예를 들어 몇 개의 오퍼레이션들로 이루어진 오퍼레이션 그룹이 완전이 종료될 때까지 어느 하나의 오퍼레이션 실행을 보류하도록 지시할 수도 있습니다. 또한 key-value observing을 통해 여러분의 오퍼레이션의 상태 변화를 관찰할 수도 있는데, 이는 예를 들어 작업이 완료될 때마다 진행율 표시기에 나타날 숫자를 변경하는데 유용할 것입니다.

 

카테고리 “Language/Objective-C & Swift”
more...
썸네일 이미지
[번역글] Objectiv-C 블록(block)으로 작업하기(2)
이 글은 애플 측 개발자 문서인 Working with Blocks를 바탕으로 작성한 것입니다. Swift의 ‘클로저(closure)’에 대응하는 Objective-C의 ‘블록(block)’에 대한 내용입니다. Objectiv-C 블록(block)으로 작업하기 Objective-C 클래스는 데이터 및 이에 관계된 행동으로 구성된 객체를 정의합니다. 때때로 Objective-C에서는 일련의 메소드들보다는 하나의 작업 또는 단위 행동을 표현하는 다른 방법을 찾는 것이 더 나은 경우도 있습니다. 블록(block)은 C, Objective-C 및 C++에서 마치 변수처럼 다른 곳으로 어떤 메소드나 함수 조각을 전달하기 위한 언어 수준에서 정의된 기능입니다. 블록은 Objective-C에서 객체인데 이는 NSAr..
Language/Objective-C & Swift
2021. 9. 22. 15:01

[번역글] Objectiv-C 블록(block)으로 작업하기(2)

Language/Objective-C & Swift
2021. 9. 22. 15:01

이 글은 애플 측 개발자 문서인 Working with Blocks를 바탕으로 작성한 것입니다. Swift의 ‘클로저(closure)’에 대응하는 Objective-C의 ‘블록(block)’에 대한 내용입니다.

Objectiv-C 블록(block)으로 작업하기

Objective-C 클래스는 데이터 및 이에 관계된 행동으로 구성된 객체를 정의합니다. 때때로 Objective-C에서는 일련의 메소드들보다는 하나의 작업 또는 단위 행동을 표현하는 다른 방법을 찾는 것이 더 나은 경우도 있습니다. 블록(block)은 C, Objective-C 및 C++에서 마치 변수처럼 다른 곳으로 어떤 메소드나 함수 조각을 전달하기 위한 언어 수준에서 정의된 기능입니다. 블록은 Objective-C에서 객체인데 이는 NSArrayNSDictionary 같은 콜렉션으로 추가될 수 있음을 뜻합니다. 블록은 또한 자신을 감싸고 있는 스코프에서 값을 캡쳐할 수 있는 능력을 가지고 있는데 이는 타 프로그래밍 언어에서 ‘클로저(closure)’나 ‘람다(lambda)’와 비슷합니다. 이 장에서는 블록을 선언하고 참조하는 구문에 대해 설명하고 콜렉션 열거와 같은 통상적인 작업에서 블록을 사용하는 방법에 대해 보일 것입니다.

  1. 블록 기본 구문; 블록의 매개변수 및 반환 형식
  2. 블록의 스코프 캡쳐와 strong 순환 참조 해결
  3. 블록 응용 예

 

블록은 자신을 감싸는 스코프로부터 값들을 캡쳐할 수 있다

통상적인 실행 구문 뿐만 아니라 블록도 자신을 감싸는 스코프에 있는 상태를 캡쳐할 수 있는 능력을 갖고 있습니다. 여러분이 어떤 메소드 안에서 블록을 선언했다면, 그 메소드의 스코프 안에서 접근할 수 있는 모든 변수들은 블록에서도 접근이 가능합니다.

// Objective-C
- (void)testMethod {
    int anInteger = 42;
    
    void (^testBlock)(void) = ^{
        NSLog(@"Integer is %i", anInteger);
    };
    
    testBlock();
}

이 예제에서 anInteger는 블록의 바깥에 선언되어 있지만 블록을 정의하고 있는 그 스코프 내에서 캡쳐가 되었기에 블록 내에서도 사용이 가능합니다. 여러분이 명시하지 않는 이상 블록이 정의될 당시의 값만이 캡쳐되는데, 다시 표현하면 여러분이 블록을 선언하고 그 블록을 호출하는 도중에 값이 변경되었다면 이는 반영되지 않습니다.

// Objective-C
- (void)testMethod {
    int anInteger = 42;
    
    void (^testBlock)(void) = ^{
        NSLog(@"Integer is %i", anInteger);
    };
    
    anInteger = 84;
    
    testBlock();
}

블록이 정의되고 변수들이 캡쳐된 이후 그 변수들의 값이 변경되었어도 이는 블록 내에 다시 반영되지 않습니다. 이것은 다음과 같이 출력 결과로 알 수 있습니다.

캡쳐된 변수의 값 변화가 없는 Block 예제 소스.
값 변화가 없는 Block 예제 소소의 실행 결과.

블록은 변수의 원 값을 바꿀 수 없다는 것을 의미하기도 합니다. 마치 const 속성으로 캡쳐되기 때문입니다.

 

저장소를 공유하기 위해 __block 변수를 사용하기

블록 안에서 캡쳐된 변수의 값을 변경할 필요가 있을 때 여러분은 변수 선언문에서 __block 저장소 지정자를 사용할 수 있습니다. 이것은 저장소의 변수가 원 변수 및 그 스코프 내에서 선언된 블록들 사이에서도 공유됨을 의미합니다. 앞서 적었던 예제를 다음과 같이 작성할 수 있습니다.

// Objective-C
- (void)testMethod {
    __block int anInteger = 42;
    
    void (^testBlock)(void) = ^{
        NSLog(@"Integer is: %i", anInteger);
    };
    
    anInteger = 84;
    
    testBlock();
}

anInteger 변수는 __block 지시자로 선언되었기 때문에 이 변수의 저장소는 블록이 정의되는 곳과 공유됩니다. 이것은 다음의 출력 결과로써 확인이 가능합니다.

캡쳐된 변수의 값 변화가 있는 Block 예제 소스.
값 변화가 있는 Block 예제 소소의 실행 결과.

또한 __block 지시자는 블록 내에서 다음과 같이 원 변수의 값을 수정할 수 있음을 뜻합니다.

// Objective-C
- (void)testMethod {
    __block int anInteger = 42;
    
    void (^testBlock)(void) = ^{
        NSLog(@"Integer is: %i", anInteger);
        anInteger = 100;
    };
    
    testBlock();
    NSLog(@"Value of original variable is now: %i", anInteger);
}

출력은 다음과 같을 것입니다.

블록 내에서 값 변화가 있는 예제 소스.
블록 내 값 변화가 있는 예제의 실행 결과.

 

블록은 함수나 메소드의 매개변수로서 전달될 수 있다

앞서 살펴본 매개변수들은 블록을 정의한 즉시 호출합니다. 실무에서는 블록을 다른 곳에서 호출시키기 위해 함수나 메소드의 매개변수로 넘기는 경우가 일반적입니다. 예를 들어 블록을 백그라운드에서 호출시키기 위해, 또는 반복적으로 호출되는 작업(예를 들어 콜렉션의 각 원소들을 열거하는 것)을 나타내는 블록을 정의하기 위해 Grand Central Dispatch를 사용하게 될 수도 있습니다. 이 장의 후반부에서는 동시성과 열거에 대해 좀 더 알아보도록 하겠습니다.

블록은 또한 어떤 작업이 완료되었을 때 호출될 코드 조각을 정의하는 ‘콜백(callback)’ 목적으로 사용되기도 합니다. 예를 들어 여러분의 앱은 복잡한 작업을 수행하는 객체를 생성할 때 또한 사용자의 행동에도 반응해야 할 수 있습니다. 웹 서비스에 정보를 요청하는 경우가 그런 예이지요. 특히 웹과 관련된 작업은 상대적으로 소요 시간이 길기 때문에 여러분은 작업이 진행되는 동안 진행율 표시기(progress indicator) 같은 것을 사용자에게 보일 필요가 있습니다. 그리고 작업이 완료되면 진행율 표시기를 숨겨야 합니다.

이와 같은 직업은 델리게이션(delegation)으로써 구현할 수 있습니다. 이를 위해서 여러분은 적절한 델리게이트 프로토콜을 만들어야 하고, 필요한 메소드들을 구현해야 하며, 오래 걸리는 작업에 대한 델리게이트로서 여러분의 객체를 설정해야 하고, 작업이 완료되어 여러분에 객체 안에 만들어 놓은 델리게이트 메소드가 호출될 때까지 기다려야 할 것입니다. 블록은 이와 같은 조치들을 간단하게 만듭니다. 여러분은 작업을 초기화할 때 콜백의 행동을 다음과 같이 정의할 수 있기 때문입니다.

// Objective-C
- (IBAction)fetchRemoteInformation:(id)sender {
    [self showProgressIndicator];
    
    XYZWebTask * task = /* ... */;
    
    [task beginTaskWithCallbackBlock:^{
        [self hideProgressIndicator];
    |;
}

위 예제는 진행율 표시기를 나타내는 메소드를 호출한 다음에 작업을 생성하고 시작합니다. 콜백 블록은 작업이 완료된 후 실행될 코드를 지정하고 있는데, 이 경우 진행율 표시기를 숨깁니다. 블록 내에서 hideProgressIndicator 메소드가 호출되기 위하여 self가 캡쳐되고 있음을 주목하기 바랍니다. 이 글의 후반부에서 다시 설명하겠지만 strong 참조 순환이 발생하기 쉽기 때문에 self를 캡쳐할 때는 특별히 주의해야 합니다.

소스 코드의 가독성을 위해 블록은 작업이 발생하기 전 또는 발생한 후에 실행될 것으로 예측되는 그 장소에 적혀있어야 합니다. 이렇게 하면 내부적으로 호출되고 수행되는 작업들을 찾아가기 위해 델리게이트 메소드들을 따라 추적할 필요가 없게 됩니다.

이 예제에서 beginTaskWithCallbackBlock 메소드의 선언은 다음과 같이 생겼을 것입니다.

- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock;

(void (^)(void)) 타입은 매개변수가 없고 반환 형식도 없는 블록을 뜻합니다. 이 메소드의 내부 구현에서 블록 호출은 다음과 같을 것입니다.

// Objective-C
- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock {
    // ...
    callbackBlock();
}

하나 이상의 매개변수를 받는 블록에 대한 타입도 다음과 같이 지정 가능합니다.

// Objective-C
- (void)doSomethingWithBlock:(void (^)(double, double))block {
    // ...
    block(21.0, 2.0);
}

 

블록은 항상 메소드의 마지막 매개변수가 되어야 한다

메소드에서 블록형 매개변수는 하나만 받는 것이 가장 좋은 실무입니다. 메소드가 블록형이 아닌 매개변수도 함께 받을 경우 블록 매개변수는 맨 마지막에 와야 합니다.

- (void)beginTaskWithName:(NSString *)name completion:(void (^)(void))callback;

이렇게 선언된 메소드는 호출 시 블록을 인라인으로 작성할 때 그 문장을 읽기 좋게 만듭니다.

// Objective-C
[self beginTaskWithName:@"MyTask" completion:^{
    NSLog(@"The task is complete.");
}];

 

블록 구문을 단순화하기 위하여 타입 재정의를 사용하기

동일한 시그니처를 갖는 블록을 여러 번 사용한다면 여러분은 그 시그니처에 대해 여러분만의 타입으로 재정의할 수 있습니다. 예를 들어 다음과 같이 매개변수도 없고 반환 형식도 없는 타입을 재정의합니다.

typedef void (^XYZSimpleBlock)(void);

그 다음 여러분은 블록 변수나 블록 매개변수를 사용할 때 이 타입을 적을 수 있습니다.

// Objective-C
XYZSimpleBlock anotherBlock = ^{
    // ...
};

- (void)beginFetchWithCallbackBlock:(XYZSimpleBlock)callbackBlock {
    // ...
}

특히 재정의된 블록 타입은 블록 자체를 반환하거나 블록이 매개변수로서 또 다른 어떤 블록을 받을 때 유용합니다.

// Objective-C
void (^(^complexBlock)(void (^)(void)))(void) = ^(void ^(aBlock)(void)) {
    // ...
    return ^{
        // ...
    };
};

complexBlock 변수는 불록을 매개변수로 받으면서 그 반환 형식 또한 블록인 어떤 블록을 참조하고 있습니다. 타입 재정의를 사용하면 이러한 소스 코드를 보다 가독성 있게 작성할 수 있습니다.

// Objective-C
XYZSimpleBlock (^betterBlock)(XYZSimpleBlock) = ^(XYZSimpleBlock){
    // ...
    return ^{
        // ...
    };
};

 

객체는 블록을 기억해 두기 위해 프로퍼티를 사용한다.

블록을 기억해 둘(keep track of) 목적으로 프로퍼티를 정의하는 구문은 블록 변수를 정의하는 구문과 유사합니다.

// Objective-C
@interface XYZObject : NSObject
@property (copy) void (^blockProperty)(void);
@end

참고 스코프를 벗어나도 그 블록이 캡쳐하고 있던 상태를 유지하기 위해서는 프로퍼티 속성으로 copy를 지정해야 합니다. 이것은 여러분이 ARC를 사용할 때 염려하던 것과는 경우가 다릅니다.

블록 프로퍼티도 다른 블록 변수들과 마찬가지로 대입되고 호출됩니다.

// Objective-C
[self setBlockProperty:^{
    // ...
}];
[self blockProperty]();

또한 다음과 같이 재정의된 타입을 사용하는 것도 됩니다.

// Objective-C
typedef void (^XYZSimpleBlock)(void);

@interface XYZObject : NSObject@
@property (copy) XYZSimpleBlock blockProperty;
@end

 

self를 캡쳐할 때 strong 순환 참조를 피하기

콜백 블록과 같이 여러분이 블록 안으로 self를 캡쳐할 때, 메모리 관리 구현 방법을 고려하는 것이 중요합니다. 블록은 캡쳐된 객체에 대해 strong 참조를 유지합니다. 이는 self도 마찬가지입니다. 예를 들어 self를 캡쳐하는 어떤 블록에 대한 프로퍼티가 copy 속성을 가지고 있다면 결국 strong 순환 참조가 발생하게 됩니다.

// Objective-C
@interface XYZBlockKeepter : NSObject
@property (copy) void (^block)(void);
@end
@implementation XYZBlockKeeper
@synthesize block;
- (void)configureBlock {
    [self setBlock:^{
        [self doSomething]; // self에 대한 strong 참조
        // 이후 strong 순환 참조 발생
    }];
}
@end

이런 간단한 소스 코드라면 컴파일러는 여러분에게 경고해 줄 것입니다. 그러나 복잡한 예제라면 객체들 사이에 여러 strong 참조가 얽히게 되어서 컴파일러의 진단이 어려울 수 있습니다. 이런 문제를 피하기 위해 self에 대해서는 다음과 같이 weak 참조를 하도록 지정합니다.

// Objective-C
- (void)configureBlock {
    XYZBlockKeeper * __weak weakSelf = self;
    [self setBlock:^{
        [weakSelf doSomething]; // weak 참조를 캡쳐해 왔음
        // 순환 참조를 피할 수 있음
    }];
}

self를 weak 포인터로 참조한 변수를 캡쳐함으로써 블록은 XYZBlockKeeper 객체를 거꾸로 strong 참조하지 않게 됩니다. 블록이 호출되기 전에 객체가 소멸되었다면 weakSelf 포인터는 단순히 nil을 가리키게 될 것입니다.

 

카테고리 “Language/Objective-C & Swift”
more...
[번역글] Objectiv-C 블록(block)으로 작업하기(1)
이 글은 애플 측 개발자 문서인 Working with Blocks를 바탕으로 작성한 것입니다. Swift의 ‘클로저(closure)’에 대응하는 Objective-C의 ‘블록(block)’에 대한 내용입니다. Objectiv-C 블록(block)으로 작업하기 Objective-C 클래스는 데이터 및 이에 관계된 행동으로 구성된 객체를 정의합니다. 때때로 Objective-C에서는 일련의 메소드들보다는 하나의 작업 또는 단위 행동을 표현하는 다른 방법을 찾는 것이 더 나은 경우도 있습니다. 블록(block)은 C, Objective-C 및 C++에서 마치 변수처럼 다른 곳으로 어떤 메소드나 함수 조각을 전달하기 위한 언어 수준에서 정의된 기능입니다. 블록은 Objective-C에서 객체인데 이는 NSAr..
Language/Objective-C & Swift
2021. 9. 21. 12:20

[번역글] Objectiv-C 블록(block)으로 작업하기(1)

Language/Objective-C & Swift
2021. 9. 21. 12:20

이 글은 애플 측 개발자 문서인 Working with Blocks를 바탕으로 작성한 것입니다. Swift의 ‘클로저(closure)’에 대응하는 Objective-C의 ‘블록(block)’에 대한 내용입니다.

Objectiv-C 블록(block)으로 작업하기

Objective-C 클래스는 데이터 및 이에 관계된 행동으로 구성된 객체를 정의합니다. 때때로 Objective-C에서는 일련의 메소드들보다는 하나의 작업 또는 단위 행동을 표현하는 다른 방법을 찾는 것이 더 나은 경우도 있습니다. 블록(block)은 C, Objective-C 및 C++에서 마치 변수처럼 다른 곳으로 어떤 메소드나 함수 조각을 전달하기 위한 언어 수준에서 정의된 기능입니다. 블록은 Objective-C에서 객체인데 이는 NSArrayNSDictionary 같은 콜렉션으로 추가될 수 있음을 뜻합니다. 블록은 또한 자신을 감싸고 있는 스코프에서 값을 캡쳐할 수 있는 능력을 가지고 있는데 이는 타 프로그래밍 언어에서 ‘클로저(closure)’나 ‘람다(lambda)’와 비슷합니다. 이 장에서는 블록을 선언하고 참조하는 구문에 대해 설명하고 콜렉션 열거와 같은 통상적인 작업에서 블록을 사용하는 방법에 대해 보일 것입니다.

  1. 블록 기본 구문; 블록의 매개변수 및 반환 형식
  2. 블록의 스코프 캡쳐와 strong 순환 참조 해결
  3. 블록 응용 예

 

블록 구문

블록을 정의하는 구문은 다음과 같이 ‘캐럿(caret)’ 기호(^)를 사용합니다.

// Objective-C
^{
    NSLog(@"This is a block");
}

함수나 메소드 정의와 마찬가지로 중괄호는 블록의 시작과 끝을 지시합니다. 이 예에서 블록은 어떤 값도 반환하지 않도 어떤 매개변수도 받지 않습니다.

여러분이 C 함수를 참조하고 어딘가로 이를 전달하기 위해 함수 포인털르 사용하듯이, 여러분은 Objective-C에서 다음과 같이 블록을 보관할 수 있는 변수를 선언할 수 있습니다.

// Objective-C
void (^simpleBlock)(void);

여러분이 C 함수 포인터를 다루는 것에 익숙하지 않다면, 이런 구문이 다소 이상해 보일 것입니다. 다음 예는 simpleBlock이라는 변수에 매개변수도 없고 반환 타입도 없는 블록을 참조시킬 것입니다.

// Objective-C
simpleBlock = ^{
    NSLog(@"This is a block");
}

이것은 통상의 변수 대입과 차이가 없습니다. 그래서 문장의 끝은 세미콜론으로 끝납니다. 여러분은 다음과 같이 변수의 선언과 대입을 한 문장으로 적을 수 있습니다.

// Objective-C
void (^simpleBlock)(void) = ^{
    NSLog(@"This is a block");
};

일단 변수를 선언하고 대입한 후에 다음과 같이 블록을 호출 및 실행할 수 있습니다.

// Objective-C
simpleBlock();

참고 블록이 대입되지 않은 변수 또는 nil이 대입된 변수를 호출하려 할 경우 앱은 충돌합니다.

 

매개변수를 받고 값을 반환하는 블록

블록도 메소드 또는 함수와 마찬가지로 매개변수를 받고 값을 반환할 수 있습니다. 다음 예는 두 개의 값을 받아서 그 값을 곱한 결과를 반환하는 블록을 참조하는 변수입니다.

// Objective-C
double (^multiplyTwoValues)(double, double);

여기에 참조될 수 있는 블록은 다음과 같이 생겼습니다.

// Objective-C
^(double firstValue, double secondValue) {
    return firstValue * secondValue;
}

보통의 함수와 마찬가지로 firstValuesecondValue는 블록이 호출될 때 주어진 값을 참조합니다. 이 예제에서 블록의 반환 형식은 블록 내의 return 문장이 반환하는 형식에서 유래된 것입니다. 물론 여러분의 선호에 따라 캐럿과 매개변수 목록 사이에 블록의 반환 형식을 직접 명시할 수도 있습니다.

// Objective-C
^ double (double firstValue, double secondValue) {
    return firstValue * secondValue;
}

블록을 일단 선언 및 정의하고 변수에 참조시키면 여러분은 그 이후부터는 함수처럼 호출할 수 있습니다.

// Objective-C
double (^multiplyTwoValues)(double, double) 
    = ^(double firstValye, double secondValue) {
    return firstValue * secondValue;
}

 

카테고리 “Language/Objective-C & Swift”
more...
[번역글] Swift 클로저 사용을 위한 궁극의 가이드(3) [完]
본 게시글은 The Ultimate Guide to Closures in Swift를 바탕으로 작성하였습니다. Swift 클로저 사용을 위한 궁극의 가이드 이 튜토리얼은 Swift의 클로저(closure)를 여러분에게 상세히 안내해 줄 것입니다. 클로저란 여러분이 마치 변수에 값을 대입하거나 매개변수로서 전달하듯 소스 코드 안에서 주고 받을 수 있는 코드 블럭입니다. 클로저를 완전히 이해하는 것은 iOS 개발을 배우는 과정에서 중대한 부분입니다. 여러분이 옵셔널(optional)에 대해 이해하는 데 어려움이 있었다면 클로저를 이해하는 것은 더욱 큰 일이 될 지도 모릅니다. 하지만 걱정마시기 바랍니다. 클로저는 보기보다 해롭지 않습니다. 알고보면 매우 유용합니다. 이 튜토리얼에서는 다음과 같은 것을 설명할..
Language/Objective-C & Swift
2021. 9. 20. 21:30

[번역글] Swift 클로저 사용을 위한 궁극의 가이드(3) [完]

Language/Objective-C & Swift
2021. 9. 20. 21:30

본 게시글은 The Ultimate Guide to Closures in Swift를 바탕으로 작성하였습니다.

Swift 클로저 사용을 위한 궁극의 가이드

이 튜토리얼은 Swift의 클로저(closure)를 여러분에게 상세히 안내해 줄 것입니다. 클로저란 여러분이 마치 변수에 값을 대입하거나 매개변수로서 전달하듯 소스 코드 안에서 주고 받을 수 있는 코드 블럭입니다. 클로저를 완전히 이해하는 것은 iOS 개발을 배우는 과정에서 중대한 부분입니다.

여러분이 옵셔널(optional)에 대해 이해하는 데 어려움이 있었다면 클로저를 이해하는 것은 더욱 큰 일이 될 지도 모릅니다. 하지만 걱정마시기 바랍니다. 클로저는 보기보다 해롭지 않습니다. 알고보면 매우 유용합니다.

이 튜토리얼에서는 다음과 같은 것을 설명할 것입니다.

  • 클로저란 무엇이고 어떻게 사용할 것인가
  • 클로저를 선언하고 호출하는 구문
  • 클로저의 타입을 구성하고 해체하는 방법
  • “클로징 오버(closing over)”란 무엇이고 캡처 리스트(capture list)는 어떻게 사용할 것인가?
  • completion handler로서 클로저를 어떻게 사용할 것인가

준비되었다면 지금부터 시작하겠습니다.

 

Swift의 클로저와 캡쳐

클로저를 단순히 변수에 대입이 가능한 함수처럼 보는 것은 클로저의 진정한 의미에 부합되지 않습니다. 이전까지 필자가 클로저를 이렇게 표현해 온 것은 가장 중요한 개념인 “캡쳐(capture)”를 빼고 설명해 온 것입니다.

“클로저(closure)”라는 이름은 “동봉하다(enclosing)”라는 말에서 따온 것인데, 좀 더 확장해 보면 이 단어는 함수형 프로그래밍의 개념인 “closing over”에서 온 단어입니다. Swift에서 클로저는 자신을 감싸고 있는 범위 내의 변수와 상수들을 캡쳐합니다(참고: ‘closing over’, ‘capturing’, ‘enclosing’은 여기에서 모두 같은 의미로 쓰였습니다).

Swift에서 모든 변수, 함수 및 클로저들은 스코프를 가졌습니다. 스코프(scope)는 여러분이 어디에서 특정 변수, 함수 또는 클로저에 접근할 수 있는지 그 범위를 결정해줍니다. 스코프 내에 변수, 함수 또는 클로저가 없는 경우 여러분은 대상에 접근할 수 없습니다. 스코프는 때떄로 맥락 또는 “컨텍스트(context)”라고도 부릅니다.

스코프를 여러분의 집 열쇠가 어디에 있을 지에 대한 컨텍스트와 비교해 보겠습니다. 여러분이 집 밖에 나가 있을 때 집 열쇠도 집 밖에 있을 것입니다. 여러분은 집 밖에 나와 있는데 열쇠는 집 안에 있다면 여러분과 열쇠는 같은 컨텍스트 내에 있는 것이 아닙니다. 따라서 여러분은 현관문을 열 수 없습니다. 여러분과 집 열쇠가 모두 집 밖에 나와 있어야 같은 컨텍스트에 있는 것이므로, 여러분은 현관문을 열 수 있습니다.

여러분의 코드는 글로벌 스코프(global scope)와 로컬 스코프(local scope)로 나눌 수 있습니다.

클래스에 정의된 프로퍼티는 글로벌 스코프의 일부입니다. 클래스 내 어느 곳에서든 여러분은 프로퍼티의 값을 가져오거나 설정할 수 있습니다.

함수나 메소드 안에서 정의된 변수는 로컬 스코프입니다. 그 함수 범위 안에서만 여러분이 값을 가져오거나 설정할 수 있습니다.

 

클로저가 자신을 감싸고 있는 스코프를 어떻게 캡쳐하는지 예제로 확인해 보겠습니다.

// Swift
let name = "Zaphod"
let greeting = {
    print("Don't panic, \(name)!")
}

greeting()

(1) name이라 이름 붙은 상수는 문자열인 "Zaphod"를 보관하고 있습니다.

(2) greeting이라 이름 붙은 상수에 클로저가 정의되고 보관됩니다. 이 클로저는 메시지를 출력합니다.

(3) 마지막 줄에서 클로저가 호출됩니다.

 

위 예에서 클로저는 지역 변수인 name을 캡처(close over)합니다. 다시 말하면 클로저는 자신이 정의된 스코프 내에서 접근 가능한 변수들을 캡슐화합니다. 그 결과 우리는 클로저 안에서 지역적으로 선언되지 않은 식별자인 name을 클로저 안에서 사용할 수 있습니다.

 

다음에 제시될 좀 더 복잡한 예를 보겠습니다.

// Swift
func addScore(_ points: Int) -> Int {
    let score = 42
    let calculate = {
        score + points
    }
    
    return calculate()
}

let value = addScore(11)
print(value)

(1) addScore(_:)가 정의됩니다. 이것은 매개변수 point로 넘어온 값에 이전 점수인 42를 더한 값을 반환합니다.

(2) 이 함수 안에는 클로저 calculate가 정의되어 있습니다. 이것은 단순히 scorepoint를 더하여 그 값을 반환합니다. 이 함수는 calculate()로써 클로저를 호출합니다.

(3) addScore(_:)가 호출되고 그 결과를 보관한 뒤 출력합니다.

 

클로저 calculate는 두 개의 값인 scorepoints를 캡쳐합니다. 이들 변수는 둘 다 클로저 안에서는 선언된 적이 없지만 클로저는 문제없이 이들의 값을 가져올 수 있습니다. 클로저가 자신의 스코프 내 변수들을 캡쳐했기 때문입니다. 클로저의 캡쳐에 대해 몇 가지 주목할만한 사항은 다음과 같습니다.

(1) 클로저는 자신의 내부에서 실제로 사용하는 변수 등을 캡쳐합니다. 클로저 내에서 접근되지 않는 변수 등은 캡쳐되지 않습니다.

(2) 클로저는 변수와 상수를 캡쳐할 수 있습니다. 물론 기술적으로 클로저도 변수나 상수의 일종이기 때문에 다른 클로저를 캡쳐할 수도 있습니다. 또한 프로퍼티의 경우 변수 또는 상수에 대입되어 있는 어떤 객체에 소속된 프로퍼티이기 때문에 여러분은 이 또한 클로저로 캡쳐할 수 있습니다. 물론 클로저 안에서 클로저 밖에 있는 함수(메소드)를 호출할 수도 있습니다. 함수(메소드)는 변수 또는 상수도 아니고 여기에 보관되는 개념도 아니기에 캡쳐되지는 않지만 호출이 됩니다.

(3) 캡쳐는 일방향성을 갖습니다. 클로저는 자신이 정의된 스코프 내의 것들을 캡쳐하는데, 클로저 바깥에서는 클로저 안에서 선언 및 정의된 사항들에 대해서 접근할 수 없습니다.

 

한편 클로저로 값을 캡쳐하는 것은 strong 참조의 순환이나 메모리 누수로 이어질 수 있습니다. 그래서 ‘캡쳐 리스트(capture list)’가 도입되게 되었습니다.

 

Strong 참조와 캡쳐 리스트

클로저는 값들을 캡쳐하는데 이 때 그 값에 대한 strong 참조를 자동으로 생성합니다. 예를 들어 Bob이 Alice에 대해 strong 참조를 가지고 있을 때 Bob이 메모리에서 제거되지 않는 한 Alice도 메모리에서 제거되지 않습니다.

여기까지는 좋습니다. 하지만 Alice가 Bob에 대해 strong 참조를 가지고 있다면 어떻게 될까요? Bob과 Alice는 서로 붙잡고 있기 때문에 둘 다 메모리에서 제거될 수 없습니다. 이것을 string 참조 순환이라 하며 메모리 누수의 원인이 됩니다. 만일 Bob과 Alice가 10MB씩 차지하고 있다면 다른 앱들은 그 만큼의 메모리를 사용 못하게 되고 이를 제거할 방법도 없게 됩니다.

캡처 리스트를 사용하면 strong 참조 순환을 깰 수 있습니다. 클래스의 프로퍼티에 weak 키워드를 붙인 것과 같이 클로저가 캡쳐할 값에 weakunowned 참조를 표시할 수 있습니다. 이를 명시하지 않는다면 기본 속성인 strong이 지정됩니다.

// Swift
class Database {
    var data = 0
}

let database = Database()
database.data = 11010101

let calculate = { [weak database] multiplier in
    database.data * multiplier
}

let result = calculate(2)
print(result)

(1) data라는 프로퍼티 하나를 갖는 Database 클래스를 선언 및 정의하고 이에 대한 새 인스턴스를 생성합니다. 그리고 data 프로퍼티에 어떤 정수 값을 설정합니다.

(2) calculate 클로저를 정의합니다. 클로저는 하나의 매개변수인 multiplier를 받습니다. 또한 database를 캡쳐합니다. 클로저 안에서 database.data 값은 multiplier로 곱해진 다음 반환됩니다.

(3) 마지막 줄에서 클로저는 2라는 매개변수를 가지고 호출된 다음 그 결과가 result에 보관됩니다.

 

여기서 중요한 부분은 다음과 같은 캡쳐 리스트입니다.

// Swift
... { [weak database] ... } ...

캡쳐 리스트는 컴마로 구분되는 변수 이름의 목록인데 대괄호로 둘러싸여서 weak 또는 unowned 키워드가 붙을 수 있습니다.

// Swift
[weak self]
[unowned navigationController]
[unowned self, weak database]

여러분은 strong 순환 참조를 깨기 위해 위와 같이 weak 또는 unowned 키워드로써 캡쳐 리스트를 사용할 수 있습니다.

(1) weak 키워드는 캡쳐된 값이 nil이 될 수 있음을 지시합니다.

(2) unowned 키워드는 캡쳐된 값이 절대 nil이 될 수 없음을 지시합니다.

 

weakunowned 키워드는 둘 다 strong 참조의 반대 개념입니다. 다만 weak는 캡쳐된 변수가 nil을 참조할 수 있다는 것이 unowned와의 차이입니다.

통상적으로 클로저와 캡쳐 값이 항상 서로 참조하고 있으며 동시에 소멸될 때 unowned 키워드를 사용합니다. 예를 들어 뷰 컨트롤러에서 [unowned self]가 있는데, 이 경우 클로저는 뷰 컨트롤러보다 오래 존속될 수 없습니다.

여러분은 또한 어떤 경우 캡쳐 값이 nil이 될 수 있을 때 weak를 사용할 수 있습니다. 주로 클로저가 자신을 생성해 준 컨텍스트보다 더 오래 존속될 수 있을 때 이것을 사용할 수 있는데, 예를 들어 오래 걸리는 작업이 완료되기 전에 뷰 컨트롤러가 소멸될 수 있을 경우가 해당됩니다. 결과적으로 캡쳐된 값은 옵셔널 타입입니다.

캡쳐, 캡쳐 리스트 및 메모리 관리의 개념은 어려울 수 있겠으나 그렇게 복잡한 것만은 아닙니다. 추상적인 개념을 설명하기가 참으로 어려운 일이지만, 실무 iOS 개발에서 일반적으로 캡쳐 리스트는 [weak self]이나 [unowned self]와 같이 작성합니다.

여기까지 살펴본 내용을 정리하면 다음과 같습니다.

(1) 클로저는 자신을 감싸는 스코프를 캡쳐할 수 있습니다. 그래서 해당 스코프 내의 변수와 상수들은 클로저 안에서 접근이 가능합니다.

(2) 변수와 상수는 기본적으로 strong 참조로 캡쳐되는데 이는 strong 참조 순환을 야기할 수 있습니다.

(3) 여러분은 캡쳐 리스트를 사용하여 strong 순환 참조 문제를 깰 수 있습니다. 예를 들어 weakunowned로 명시될 수 있습니다.

 

클로저에 대한 내용은 여기까지입니다.

카테고리 “Language/Objective-C & Swift”
more...
[번역글] Swift 클로저 사용을 위한 궁극의 가이드(2)
본 게시글은 The Ultimate Guide to Closures in Swift를 바탕으로 작성하였습니다. Swift 클로저 사용을 위한 궁극의 가이드 이 튜토리얼은 Swift의 클로저(closure)를 여러분에게 상세히 안내해 줄 것입니다. 클로저란 여러분이 마치 변수에 값을 대입하거나 매개변수로서 전달하듯 소스 코드 안에서 주고 받을 수 있는 코드 블럭입니다. 클로저를 완전히 이해하는 것은 iOS 개발을 배우는 과정에서 중대한 부분입니다. 여러분이 옵셔널(optional)에 대해 이해하는 데 어려움이 있었다면 클로저를 이해하는 것은 더욱 큰 일이 될 지도 모릅니다. 하지만 걱정마시기 바랍니다. 클로저는 보기보다 해롭지 않습니다. 알고보면 매우 유용합니다. 이 튜토리얼에서는 다음과 같은 것을 설명할..
Language/Objective-C & Swift
2021. 9. 20. 18:15

[번역글] Swift 클로저 사용을 위한 궁극의 가이드(2)

Language/Objective-C & Swift
2021. 9. 20. 18:15

본 게시글은 The Ultimate Guide to Closures in Swift를 바탕으로 작성하였습니다.

Swift 클로저 사용을 위한 궁극의 가이드

이 튜토리얼은 Swift의 클로저(closure)를 여러분에게 상세히 안내해 줄 것입니다. 클로저란 여러분이 마치 변수에 값을 대입하거나 매개변수로서 전달하듯 소스 코드 안에서 주고 받을 수 있는 코드 블럭입니다. 클로저를 완전히 이해하는 것은 iOS 개발을 배우는 과정에서 중대한 부분입니다.

여러분이 옵셔널(optional)에 대해 이해하는 데 어려움이 있었다면 클로저를 이해하는 것은 더욱 큰 일이 될 지도 모릅니다. 하지만 걱정마시기 바랍니다. 클로저는 보기보다 해롭지 않습니다. 알고보면 매우 유용합니다.

이 튜토리얼에서는 다음과 같은 것을 설명할 것입니다.

  • 클로저란 무엇이고 어떻게 사용할 것인가
  • 클로저를 선언하고 호출하는 구문
  • 클로저의 타입을 구성하고 해체하는 방법
  • “클로징 오버(closing over)”란 무엇이고 캡처 리스트(capture list)는 어떻게 사용할 것인가?
  • completion handler로서 클로저를 어떻게 사용할 것인가

준비되었다면 지금부터 시작하겠습니다.

 

클로저의 타입에 대하여

변수, 상수 및 프로퍼티 등과 마찬가지로 모든 클로저는 타입을 가지고 있습니다. 개념적으로 보았을 때 Int 타입으로 선언된 변수와 (Int) -> () 타입으로 선언된 변수 사이에는 차이가 없습니다. 전자는 정수 형식으로 후자는 클로저 형식일 뿐입니다.

다음과 같이 하나의 매개변수를 갖는 클로저를 선언해 봅니다.

// Swift
let birthday: (String) -> () = { (name: String) -> () in 
    // ...
}

다소 복잡해보이지만, 이것을 상세히 살펴보겠습니다.

(1) 중괄호 {} 사이에 적힌 내용은 ‘클로저 표현식(closure expression)’입니다. 이러한 클로저는 let 키워드로 선언된 상수 birthday에 보관됩니다.

(2) 첫 번째로 등장하는 (String) -> ()는 ‘클로저 형식(closure type)’입니다. 이 클로저는 하나의 매개변수를 받는데 그 형식은 String이고, 아무것도 반환하지 않는다는 의미에서 반환 형식은 ()로 되어 있습니다.

(3) 두 번째로 등장하는 (name:String) -> ()은 첫 번째와 형식은 똑같으나 매개변수의 이름에 name이 붙었다는 차이가 있습니다. 물론 이 구문은 중괄호 안에 적혀있기 때문에 클로저 표현식의 일부입니다.

(4) in 키워드는 클로저 매개변수와 클로저 본문을 구분하는 역할을 합니다.

 

클로저가 어떻게 작동하는지를 이해하기 위해 이 클로저 타입을 분해해 보겠습니다. 클로저 구문은 다음과 같습니다.

// Swift
let closureName:(/* parameter types */) -> /* return type */ = {
    (/* parameter name */:/* parameter type */) -> /* return type */ in
    ...
}

먼저 우리는 3가지의 중요한 부분들을 얻을 수 있습니다.

(1) let closureName으로 선언된 클로저의 이름, 다시 표현하면 클로저가 보관될 상수의 이름

(2) 클로저 타입 구문: ( /* parameter type */ ) -> /* return type */

(3) 클로저 표현식: { ( /* parameter name */:/* parameter type */ ) -> /* return type */ in ... }

클로저를 보관하는 상수는 간단합니다. 이제부터 이 상수(또는 변수)의 이름을 사용하여 클로저를 다른 곳으로 전달할 수도 있습니다.

클로저 타입을 지정하는 구문은 매개변수의 타입과 반환 형식으로 구성됩니다. 함수와 마찬가지로 클로저도 매개변수와 반환 형식을 가질 수 있다고 했는데, 이 타입을 선언하는 구문은 괄호로 싸인 타입들과 화살표 기호와 또 다른 타입으로 구성됩니다. 몇 가지 예를 들어 이를 이해해 보겠습니다.

(1) (Int, Int) -> Double 두 개의 정수를 매개변수로 받아서 Double형 값을 반환하는 클로저 타입입니다.

(2) () -> Int 아무 매개변수도 받지 않지만 정수를 반환하는 클로저 타입입니다.

(3) (String) -> String 하나의 문자열을 매개변수로 받아서 문자열을 반환하는 클로저 타입입니다.

 

계속해서 클로저 표현식으로 넘어가겠습니다. 여기 비슷한 구문이 또 나타납니다.

// Swift
{ ( /* parameter name */:/* parameter type */ ) -> /* return type */ in
    ...
}

자세히 실펴보면 중괄호 밖에서 선언했던 클로저 타입이 표현식 안에서 반복됨을 알 수 있습니다. 클로저 타입이 여기에서는 조금 다르게선언이 되긴 했는데 이 곳에서는 매개변수에 대한 이름이 지정되고 있습니다. 앞서 살펴 보았던 예제를 이 곳으로 끌어와 보겠습니다.

(1) (Int, Int) -> Double 타입의 클로저는 표현식 안에서, { (width: Int, height: Int) -> Double in ... }으로 시작합니다.

(2) () -> Int 타입의 클로저는 표현식 안에서, { () -> Int in ... }로 시작합니다.

(3) (String) -> String 타입의 클로저는 표현식 안에서, { (text: String) -> String in ... }으로 시작합니다.

 

여러분은 이들 매개변수에 대해 필요한 대로 이름을 지정할 수 있습니다. 이렇게 지정한 매개변수와 그 이름은 함수(메소드)의 매개변수와 마찬가지로 클로저 내부에서 상수성을 가지고 쓰일 것입니다.

in은 무엇일까요? 통상의 영어라면 “in”를 쉽게 받아들일 수 있을 지 모르나, 우리는 지금 클로저를 다루고 있습니다. 이것을 문장으로 표현하자면 이렇습니다.

// Swift
{ (X: Int, Y: Int, Z: Int) -> () in
    ...
}
우리는 X, YZ 매개변수를 클로저인 이 코드 블록 안에서(in) 받았다.
We've got parameters X, Y, Z in this block of code; which is the closure

 

매개변수도 없고 반환 형식도 없는 클로저는 다음과 같은 타입을 갖습니다.

() -> ()

위 표현식은 두 개의 비어있는 튜플(tuple)과 하나의 화살표를 가지고 있는데 첫 번째 튜플은 클로저의 입력이고 두 번째 튜플은 클로저의 출력입니다. 참고로 튜플은 (a, b, c)와 같이 원소들의 순서 있는 목록이며 각 원소들은 컴마로 구분되고 전체적으로 괄호로 싸입니다. 예를 들어 다음과 같습니다.

// Swift
let flight = (airport: "LAX", airplane: 747)

또한 여러분은 Void를 클로저의 반환 형식으로 지정할 수도 있습니다.

() -> Void

Swift에서 Void는 아무것도 없다는 의미를 갖습니다. 여러분의 함수가 Void 반환 형식으로 지정된다면, 그 함수는 아무것도 반환하지 않습니다. nil이라든가 빈 문자열조차도 반환하지 않습니다. 애플 개발자 문서에 따르면 Void는 빈 튜플 ()의 별칭이라 합니다.

다음의 예를 보겠습니다. 클로저는 두 개의 매개변수를 받고 반환 형식도 있습니다.

// Swift
let greeting:(String, String) -> String = { (time: String, name: String) -> String in
    return "Good \(time), \(name)!"
}

let text = greeting("morning", "Arthur")
print(text)

클로저 greeting은 두 개의 매개변수를 갖는데 둘 다 String입니다. 또한 그 클로저의 반환 형식도 String입니다. greeting 클로저의 타입은 명시적으로 선언되었고 클로저 표현식 안에서 매개변수들도 명시적으로 타입이 선언되어 있습니다.

클로저가 호출될 때 문자열 타입 매개변수 두 개가 전달되고, 그 반환 값이 text에 보관된 다음 출력되는 구조입니다.

또한 여러분은 다음과 같이 정확히 똑같이 작동하는 클로저 표현식으로 작성할 수도 있습니다.

// Swift
let greeting: (String, String) -> String = { "Good \($0), \($1)1" }
let text = greeting("morning", "Arthur")
print(text)

지금까지 살펴본 내용을 요약해 보면 다음과 같습니다.

(1) 모든 클로저는 타입을 갖는데 이 타입은 여러분이 클로저 표현식을 정의함으로써 얻어집니다.

(2) 가장 기본적인 클로저 타입은 () -> Void로서, 아무 입력도 아무 출력도 없는 클로저를 나타냅니다.

(3) 여러분은 변수나 상수를 선언할 때 다음과 같이 명시적으로 클로저 타입을 선언할 수도 있습니다.

// Swift
let greeting: (String) -> Void ...

(4) 클로저 표현식에서는 클로저 타입이 한 번 더 적힌 다음, 그 매개변수들에 대한 이름이 다음과 같이 부여될 수 있습니다.

// Swift
... { (name: String) -> Void in ... }

 

참고 클로저에 옵셔널을 사용할 수 있을까요? 물론입니다! 클로저에 대한 매개변수 또는 클로저에 대한 반환 형식 모두 옵셔널을 사용할 수 있습니다(예: (String?, Int) -> Int?). 또한 여러분은 클로저 자체를 옵셔널로 만들 수 있는데, 이는 클로저를 참조하게 될 변수, 상수 또는 프로퍼티가 nil이 될 수도 있다는 것입니다. 이를 표현하기 위해서는 클로저 타입을 전체적으로 괄호로 한 번 감싼 다음 물음표를 넣으면 됩니다(예: ((String) -> ())?).

 

클로저와 타입 추론

Swift는 “타입 추론(type inference)”이라는 매우 강력한 기능을 가지고 있습니다. 타입 추론을 사용하면 여러분은 변수의 타입을 명시적으로 지정하지 않아도 됩니다. Swift는 변수가 가진 값의 타입을 스스로 찾아낼 수 있기 때문입니다. 예를 들어 보겠습니다.

// Swift
let age = 104

여기서 age의 타입은 무엇일까요? Swift는 위 코드의 맥락에 따라 age의 타입을 추론할 것입니다. 여기서 104는 정수의 리터럴 값이기 때문에 age의 타입은 Int가 됩니다. 이처럼 Swift는 여러분이 직접 타입을 명시하지 않아도 타입을 스스로 찾아낼 수 있습니다.

참고 Swift는 강한 타입(strong-typed)의 프로그래밍 언어입니다. 즉 추론된 타입이라 할 지라도 모든 값들은 타입을 가지고 있다는 뜻입니다. Swift가 타입 추론을 할 수 있다는 사실과 “이 값에는 타입이 없다”라는 낭설은 구분할 필요가 있습니다. 여러분이 직접 명시하지 않아도 값은 항상 타입을 가집니다. 타입 추론은 그래서 혼동을 야기할 수도 있습니다. 때문에 여러분은 여러분의 변수 하나하나에 대한 타입을 항상 알고 있어야 합니다.

 

타입 추론과 클로저가 이제 손에 잡히는 듯 합니다. 그 결과 여러분은 클로저 표현식의 않은 부분을 생략해가며 활용할 수 있을 것입니다. Swift는 생략된 부분을 스스로 추론할 수 있기 때문입니다. 다음 예를 보겠습니다.

// Swift
let names = ["Zaphod". "Slartibartfast", "Ford"]
let sortedNamed = names.sorted(by: <)
print(sortedNames)

위 코드에서 여러분은 문자열 배열을 하나 생성한 뒤 이를 sorted(by:) 메소드(함수)를 사용하여 알파벳 순으로 정렬하게 될 것입니다. 그 결과는 sortedNames에 보관될 것이고 정렬된 이름들이 출력될 것입니다.

여기서 핵심이 되는 부분은 names.sorted(by: <)입니다. 매개변수 by는 클로저를 받는데 이 클로저는 정렬 기준이 되어줄 것입니다. 그런데 이 클로저에 연산자 <만이 덩그러니 적혀 있습니다.

어째서 이런 표현이 가능한지 순서대로 생략해 나갑니다. 아무것도 생략되지 않은 풀 버전의 클로저는 다음과 같을 것입니다.

// Swift
names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 < s2
})

그런데 이것은 다음과 같이 생략될 수 있습니다.

// Swift
names.sorted(by: { s1, s2 in
    return s1 < s2
})

이것을 다시 한 줄로 적으면...

// Swift
names.sorted(by: { s1, s2 in return s1 < s2 })

그리고 반환 구문까지 생략하면...

// Swift
names.sorted(by: { s1, s2 in s1 < s2 })

앞서 매개변수에 굳이 이름을 지정하지 않고 shorthand를 써도 된다고 했습니다. 한 번 더 축약하면

// Swift
names.sorted(by: { $0 < $1 })

또한 함수(메소드)의 매개변수의 맨 끝에 클로저가 오는 경우 매개변수의 이름이 생략될 수 있습니다. 이를 trailing closure syntax라고 하는데, 다음과 같이 호출 구문까지 간소화가 가능합니다.

// Swift
names.sorted { $0 < $1 }

이로써 < 연산자가 함수로서 축약될 수 있습니다.

// Swift
names.sorted(by: <)

이 각기 다른 구문들의 기능은 놀랍게도 서로 똑같습니다! 이는 모두 타입 추론과 클로저 표현식 덕입니다. 각 표현식들의 요점을 풀 버전의 클로저부터 하나씩 살펴보겠습니다.

(1) 처음 나온 구문은 가장 완전한 형태의 클로저 구문인데 두 String 매개변수에 s1, s2 이름도 붙이고 반환 형식도 Bool이라 명시합니다. 또한 in 키워드와 return 키워드도 빠짐없이 적혀있습니다.

(2) 두 번째 나온 구문은 매개변수 타입이 생략된 형태입니다. 왜냐하면 매개변수 타입은 맥락을 통해 추론이 가능하기 때문입니다. 우리는 문자열들의 배열에 대해 정렬을 하고자 하므로 여기에 사용될 클로저의 매개변수는 이미 String입니다. 또한 타입이 생략될 때 괄호 또한 생략이 가능하게 됩니다. 따라서 s1, s2 in과 같은 구문이 허용되는 이유입니다.

(3) 세 번째 나온 구문은 클로저가 여기서 한 줄의 코드만으로 구성되기 때문에 return 키워드가 생략되었습니다. 이러한 경우 Swift는 return을 적지 않아도 여러분이 어떤 값을 반환하고 싶다는 의도를 알아차립니다.

(4) 네 번째 나온 구문은 클로저의 첫 번째 및 두 번째 매개변수를 참조하기 위해 각각 $0$1 숏 핸드가 사용되었습니다. 매개변수가 클로저 타입 선언에 이미 있었기 때문에 여러분은 이름을 굳이 적어가며 쓸 필요가 없습니다. 또한 이들 매개변수는 String 형식임이 추론됩니다.

(5) 다섯 번째 나온 구문은 trailing closure syntax입니다. 클로저가 어떤 함수(메소드)의 마지막 매개변수이거나 또는 유일한 매개변수라면 여러분은 함수를 호출하기 위한 소괄호()를 적지 않고 곧바로 클로저를 적을 수 있습니다.

(6) 마지막에 나온 구문은 < 연산자를 클로저로서 사용한 것입니다. 연산자는 최우선 순위 함수(top-level function)이기도 합니다. 특히 비교 연산자는 그 형식이 (lhs:(), rhs:()) -> Bool인 클로저로서 볼 수도 있는데 이렇게 하면 sorted(by:) 함수(메소드)에서 요구하는 클로저의 형식과 완벽히 같습니다.

 

클로저의 힘, 클로저의 표현력, 그리고 Swift 프로그래밍 언어가 여러분에게 어떻게 우아한 코드를 작성할 수 있게 하고 있는지를 지금부터 여러분이 보기 시작하였다면 다행입니다. 여러분이 할 수 있는 한 자주 Swift 코딩을 해 보고 클로저를 가지고 작업해 보고 한다면 많은 도움이 될 것입니다. 연습이 완벽을 만들어 냅니다(Practive makes perfect)! 상황이 닥쳐서야 배우려고 미뤄두지 마시고, 여러분의 시간을 할애하여 새로운 프로그래밍 개념과 최고의 예제들을 발견하고 여러분의 것으로 만들어보시기 바랍니다.

개인적으로 필자는 sorted { $0 < $1 }과 같은 표현을 가장 좋아합니다. 하지만 명심하셔야 할 것은, 축약된 프로그래밍 접근법을 선택하는 것만이 가장 현명한 것만은 아니라는 것입니다. 때로는 재주를 부리는 것보다 서술적으로 작성하는 것이 더 도움이 될 때도 있습니다.

한편 클로저는 소스 코드 읽는 것을 환장하게 만들 때도 있습니다. 다음과 같은 예를 보시기 바랍니다.

// Swift
let squared = { $0 * $0 }(12)
print(squared)

클로저 { $0 * $0 }는 정의된 그 줄에서 즉시 (12)로써 실행이 됩니다. 그 결과 squared에는 클로저가 아니라 그 실행 값인 12 * 12 = 144가 보관됩니다. 정의되지마자 클로저를 실행하는 것은 lazy computered property에 기초를 두고 있습니다.

여기까지의 내용을 요약해보겠습니다.

(1) Swift는 타입 추론을 사용하는데, 변수나 상수의 타입이 명시적으로 제공되지 않았을 때 스스로 찾아낼 수 있습니다.

(2) 클로저와 타입 추론은 함께 어울리기 때문에 여러분은 소스 코드의 가독성을 높이기 위해 클로저 표현식에서 일부를 생략할 수 있습니다. 하지만 생략이 과도하면 안 하는 것만 못할 수 있습니다.

(3) 클로저 표현식은 때때로 여러분을 환장하게 만들 수도 있기 때문에 자주 연습해 보고 다루어 보는 것이 도움이 됩니다.

 

다음 장에서는 이어서 클로저의 변수 캡처에 대해 살펴보겠습니다.

카테고리 “Language/Objective-C & Swift”
more...
썸네일 이미지
[번역글] Swift 클로저 사용을 위한 궁극의 가이드(1)
본 게시글은 The Ultimate Guide to Closures in Swift를 바탕으로 작성하였습니다. Swift 클로저 사용을 위한 궁극의 가이드 이 튜토리얼은 Swift의 클로저(closure)를 여러분에게 상세히 안내해 줄 것입니다. 클로저란 여러분이 마치 변수에 값을 대입하거나 매개변수로서 전달하듯 소스 코드 안에서 주고 받을 수 있는 코드 블럭입니다. 클로저를 완전히 이해하는 것은 iOS 개발을 배우는 과정에서 중대한 부분입니다. 여러분이 옵셔널(optional)에 대해 이해하는 데 어려움이 있었다면 클로저를 이해하는 것은 더욱 큰 일이 될 지도 모릅니다. 하지만 걱정마시기 바랍니다. 클로저는 보기보다 해롭지 않습니다. 알고보면 매우 유용합니다. 이 튜토리얼에서는 다음과 같은 것을 설명할..
Language/Objective-C & Swift
2021. 9. 18. 10:36

[번역글] Swift 클로저 사용을 위한 궁극의 가이드(1)

Language/Objective-C & Swift
2021. 9. 18. 10:36

본 게시글은 The Ultimate Guide to Closures in Swift를 바탕으로 작성하였습니다.

Swift 클로저 사용을 위한 궁극의 가이드

이 튜토리얼은 Swift의 클로저(closure)를 여러분에게 상세히 안내해 줄 것입니다. 클로저란 여러분이 마치 변수에 값을 대입하거나 매개변수로서 전달하듯 소스 코드 안에서 주고 받을 수 있는 코드 블럭입니다. 클로저를 완전히 이해하는 것은 iOS 개발을 배우는 과정에서 중대한 부분입니다.

여러분이 옵셔널(optional)에 대해 이해하는 데 어려움이 있었다면 클로저를 이해하는 것은 더욱 큰 일이 될 지도 모릅니다. 하지만 걱정마시기 바랍니다. 클로저는 보기보다 해롭지 않습니다. 알고보면 매우 유용합니다.

이 튜토리얼에서는 다음과 같은 것을 설명할 것입니다.

  • 클로저란 무엇이고 어떻게 사용할 것인가
  • 클로저를 선언하고 호출하는 구문
  • 클로저의 타입을 구성하고 해체하는 방법
  • “클로징 오버(closing over)”란 무엇이고 캡처 리스트(capture list)는 어떻게 사용할 것인가?
  • completion handler로서 클로저를 어떻게 사용할 것인가

준비되었다면 지금부터 시작하겠습니다.

 

클로저란 무엇이고 어떻게 사용할 것인가

클로저에 대한 Apple의 공식 설명은 다음과 같습니다.

클로저(closure)란 여러분의 소스 코드에서 전달될 수도 있고 사용될 수도 있는 독립적인 기능성 블록입니다(Closures are self-contained blocks of functionality that can be passed around and used in your code).

다르게 설명하면 클로저는 여러분이 변수에 대입시킬 수 있는 코드 조각입니다. 그 다음 함수와 같이 소스 코드 내 다른 곳으로 전달할 수 있습니다. 클로저를 받은 함수는 그 클로저를 호출하여 내부에 담긴 코드를 실행시킬 수 있습니다. 이 떄는 클로저가 마치 일반적인 함수처럼 역할을 합니다.

여러분도 알다시피 Swift 코드에서 변수는 정보를 보관하기 위해 있습니다. 그리고 함수(메소드)들은 작업을 실행할 수 있습니다. 여기에 클로저가 들어가게 되면 함수 구문을 변수에 보관하여 전달할 수도 있고, 어딘가에서는 그 구문을 실행할 수도 있습니다.

클로저를 이해하기 위해 비유를 들어 보면 다음과 같습니다.

(1) Bob이 Alice에게 지시사항을 주면서 이렇게 말합니다. “손을 흔들어 보시오!” Alice는 이 지시사항을 듣고 손을 흔듭니다. 이 때 손을 흔드는 행위는 함수라고 본다면 Bob은 함수를 직접적으로 호출한 것입니다.

(2) Alice는 본인의 나이를 쪽지에 적어서 Bob에게 건네줍니다. 이 쪽지는 변수이고, Alice는 어떤 데이터를 보관하기 위해 쪽지를 활용한 것입니다.

(3) Bob은 “손을 흔들어 보시오!”를 쪽지에 적어서 Alice에게 건네줍니다. Alice는 이 쪽지를 읽고 손을 흔듭니다. 이 때 쪽지에 적힌 이 지시사항을 ‘클로저”라 합니다.

(4) 비유하자면 훨씬 나중에 우리는 이 쪽지를 우편을 통해 전달할 수도 있고 Alice가 본인의 작업을 다 마친 후에 이 쪽지를 읽어보게끔 할 수도 있습니다. 이런 특징이 클로저가 갖는 이점 중 하나입니다.

이해가 되었는지요? Swift에서 클로저를 작성해 보겠습니다.

// Swift
let birthday = {
    print("Happy Birthday!")
}

birthday()

 

위 코드에서 무엇이 일어나는지 보겠습니다.

첫 번째 줄에서 여러분은 클로저를 하나 정의하여 변수 birthday에 대입하였습니다. 클로저는 중괄호 { } 사이에 둘러싸인 모양을 하고 있습니다. 함수와 비슷하게 생겼음을 확인할 수 있겠지요? 클로저는 변수에 대해 대입 연산자=가 사용됨을 확인합니다.

마지막 줄에서 클로저는 호출됩니다. 상수의 이름인 birthday에 괄호 ()가 붙어서 호출 및 실행이 되는데, 이는 함수와도 비슷해 보입니다.

위 코드를 실행하면 클로저는 birthday()로서 실행되고 "Happy birthday!"라는 print() 함수에 의해 문자열이 출력됩니다.

이 때 작성한 클로저의 타입 및 이 클로저를 보관하고 있는 birthday의 타입은 () -> ()입니다. 클로저의 타입에 대해서는 나중에 좀 더 설명합니다.

클로저에 매개변수를 추가해 보겠습니다. ‘매개변수(parameter)’란, 함수와 클로저를 위한 입력 값입니다. 함수에서 매개변수를 지정할 때와 마찬가지로 클로저도 이를 가질 수 있습니다.

// Swift
let birthday: (String) -> () = { name in
    print("Happy birthday, \(name)!")
}

birthday("Arthur")

 

소스코드가 약간 더 복잡해졌습니다. 이를 하나씩 분해해가며 어떤 일들이 일어나는지 살펴보겠습니다.

(1) 이전과 같이 클로저를 선언하여 birthday 상수에 대입한 후 맨 마지막 줄에서 이 클로저를 호출합니다.

(2) 이 클로저는 String 타입의 매개변수 하나를 갖습니다. 그러므로 이 클로저의 타입은 (String) -> ()으로 정의됩니다.

(3) 여러분은 클로저 안에서 이 매개변수 하나를 name이라는 이름으로서 사용할 수 있습니다. 클로저가 호출될 때 매개변수에 대한 값도 지정되어야 합니다.

 

어떻게 이해가 되십니까? 필수적으로 가장 중요한 세 가지는 다음과 같습니다.

(1) 클로저 타입은 (String) -> ()이다.

(2) 클로저의 표현식은 { name in ... }이다.

(3) 클로저의 호출은 birthday()이다.

 

Swift의 일반적인 함수(메소드)와는 다르게, 클로저의 매개변수에는 이름이 지정되지 않습니다. 위 예제처럼 String과 같이 매개변수의 타입은 선언되더라도 그 이름은 지정되지 않고 있습니다.

클로저 표현식인 { name in ... } 부분에서 여러분은 클로저의 첫 번째 매개변수에 name이라는 지역변수를 할당했습니다. 이 지역변수는 클로저 내용 안에서 매개변수에게 이름을 부여해줍니다. 지역변수의 이름은 여러분이 임의로 부여하고 상관없습니다.

사실 여러분은 매개변수에 굳이 이름을 부여하지 않아도 다음과 같이 $0과 같은 ‘숏 핸드(shorthand)’를 사용할 수 있습니다.

// Swift 
let birthday: (String) -> () = {
    print("Happy birthday, \($0)!")
}

위 코드에서 birthday에 보관되는 클로저는 하나의 매개변수를 가지고 있습니다. 이 클로저 안에서 숏 핸드 $0는 첫 번째 매개변수를 참조하게 됩니다.

 

Swift에서 일반적으로 함수(메소드)의 매개변수에서는 이름이 명시되지만(이를 ‘argument label’이라 합니다), 여러분이 클로저를 호출할 때 어떤 매개변수에도 이름을 지정하지 않았습니다. 이것이 함수(메소드)와의 차이입니다. 이 예제의 실행 결과는 다음과 같습니다.

클로저 실행 결과.

 

어떻게 작동되는 것인지 보이십니까? 우리는 이 클로저를 마치 birthday(_:)라는 함수처럼 호출하였고 이 때 String형 매개변수인 "Arthur"를 전달하였습니다. 매개변수는 이름이 명시되지 않고 오로지 값만을 취합니다. 앞서 살펴보았듯이 이 매개변수는 클로저 안에서 지역변수에 할당됨으로써 그 클로저 안에서 이름을 갖게 될 것입니다.

여기까지 살펴본 내용을 요약하면 다음과 같습니다.

(1) 클로저는 소스 코드 안에서 전달이 가능한 한 블록의 코드 조각이다.

(2) 클로저는 하나 이상의 매개변수를 가질 수도 있고 매개변수가 없을 수도 있다.

(3) 모든 클로저는 타입을 가지고 있다.

 

다음 장에서는 클로저의 ‘타입(type)”에 대해 좀 더 자세히 살펴보겠습니다.

카테고리 “Language/Objective-C & Swift”
more...
[번역글] Swift의 자동 레퍼런스 카운트 (2) [完]
본 게시글은 Automatic Reference Counting (ARC) in Swift를 바탕으로 작성하였습니다. Swift의 자동 레퍼런스 카운트 자동 레퍼런스 카운트(ARC: Automatic Reference Counting)는 Swift에서 메모리를 관리하는 메커니즘입니다. ARC라는 개념을 가지고 작업하는 것은 최신의 iOS 개발에서 필수적입니다. 이번 게시글에서 우리는 iOS의 메모리 관리를 위해 ARC가 어떻게 사용되는지 살펴보겠습니다. ARC는 어떻게 작동되는가 iOS 앱에서 메모리 관리가 왜 중요한가 객체의 retain 횟수가 소멸에 어떻게 영향을 주는가 메모리 관리의 실전 예제 순환 retain과 같은 흔한 결함 strong 참조 및 weak 참조를 써서 작업하기 준비 되었다면 시작..
Language/Objective-C & Swift
2021. 9. 17. 19:11

[번역글] Swift의 자동 레퍼런스 카운트 (2) [完]

Language/Objective-C & Swift
2021. 9. 17. 19:11

본 게시글은 Automatic Reference Counting (ARC) in Swift를 바탕으로 작성하였습니다.

Swift의 자동 레퍼런스 카운트

자동 레퍼런스 카운트(ARC: Automatic Reference Counting)는 Swift에서 메모리를 관리하는 메커니즘입니다. ARC라는 개념을 가지고 작업하는 것은 최신의 iOS 개발에서 필수적입니다. 이번 게시글에서 우리는 iOS의 메모리 관리를 위해 ARC가 어떻게 사용되는지 살펴보겠습니다.

  • ARC는 어떻게 작동되는가
  • iOS 앱에서 메모리 관리가 왜 중요한가
  • 객체의 retain 횟수가 소멸에 어떻게 영향을 주는가
  • 메모리 관리의 실전 예제
  • 순환 retain과 같은 흔한 결함
  • strong 참조 및 weak 참조를 써서 작업하기

준비 되었다면 시작하겠습니다.

 

할당과 소멸이 어떻게 작동하는가

이전 예제에서 우리는 메모리에 있는 인스턴스에 대해 strong 참조가 그 인스턴스를 retain하여 그 retainCount 값을 0보다 큰 값으로 설정한 것을 확인하였습니다.

우리는 또한 생성자로서 Car()와 같은 코드가 어떻게 객체를 만들고 메모리에 이를 보관하는지에 대해 살펴보았습니다. 이것을 “할당(allocation)”이라고 하는데 Car 클래스형 객체를 담기 위해 RAM의 일부가 확보되기 때문입니다. 그렇다면 Car 객체가 “소멸(deallocation)”하는 것은 무엇일까요?

다른 데이터를 위해 메모리는 언제 비워지고 언제 해제되는 것일까요? 게다가 이전 예제에서 우리는 명시적으로 car1car2nil로 대입해 주었는데, 이것은 여러분이 일상적으로 코딩하는 방식은 아닐 것입니다. 여러분이 사용했던 모든 참조형 변수에 대해 nil을 대입하며 마치는 것은 분명히 아닐 것입니다.

명시적으로 nil이 대입되지 않을 경우 strong 참조형 변수에게는 어떤 일이 일어날까요?

이 질문에 대한 답을 찾기 위해서는 ARC의 핵심부를 건드리게 됩니다. 좋은 소식은 이것들이 자동으로 수행되기 때문에 여러분은 이에 대해 생각할 필요가 없다는 것이고, 나쁜 소식은 이것이 자동으로 수행되기 때문에 여러분이 이에 대해 이해하기 위해서는 다소 깊게 파고 들어가야 한다는 것입니다.

다음 예제를 살펴보겠습니다.

// Swift
func rentCar(type: String) {
    var rental = Car()
    rental.type = type
    
    // 차를 운전하고, 돌려줍니다. 그리고 아무것도 안 합니다.
    rental.drive()
}
// Objective-C
-(void) rentCarWithType:(NSString *)type {
    Car * rental = [[Car alloc] init];
    [rental setType: type];
    
    // 차를 운전하고, 돌려줍니다. 그리고 아무것도 안 합니다.
    [rental drive];
}

 

위 함수에서 우리는 특정 유형의 렌터카를 새로 만들었습니다. 그 다음 그 차를 가지고 운전을 했고 렌탈 회사에 반납한 후 집에 돌아갑니다. 그리고 질문합니다. “그렇다면 Car 클래스형 인스턴스는 언제 소멸되는 것일까?”

 

여러분이 retainCount를 기억하고 있다면 여러분은 rentCar()의 본문이 끝나는 곳에서 Car 인스턴스가 소멸됨을 보장받을 수 있습니다. 그 이유는 다음과 같습니다.

(1) Car 인스턴스의 retainCountrental 변수이 이를 참조하면서 0에서 1로 증가합니다.

(2) type이라는 문자열은 Swift에서 값 형식(value-type)입니다. 그래서 rental 변수의 retainCount에 영향을 주지 않습니다.

(3) 적어도 우리가 본 바로는 drive() 함수는 어떤 부수적인 효과도 갖지 않기에 retainCount 값에 영향을 주지 않습니다.

(4) rentCat 메소드(함수)의 본문 끝 부분에서 rental 변수는 더 이상 필요가 없게 됩니다. 다시 말하면 함수의 스코프(scope) 존재성이 여기에서 마치게 됩니다. 때문에 rental 변수는 암시적으로 nil로 설정되면서 Car 객체는 메모리에서 소멸됩니다.

 

코드 내 다른 부분 어디에서도 rental에 대해 strong 참조를 하는 부분은 없습니다. 그리고 이 변수에 참조되는 인스턴스는 rentCar 메소드(함수)를 벗어나지 않습니다, 달리 표현하면 메소드(함수) 바깥으로 전달이 된다거나 반환이 된다거나 하지 않는다는 것입니다. 그 결과 Car 클래스형 인스턴스는 메모리에서 해제됩니다.

 

그렇다면 rental이 클래스의 프로퍼티로 있을 때는 어떨까요? 모든 것이 바뀝니다. 이를 확인해 보겠습니다.

// Swift
class RentalCompany {
    var rental: Car?
    
    func rentCar(type: String) {
        rental = Car()
        rental.type = type
        
        // 차를 운전하고, 돌려줍니다. 그리고 아무것도 안 합니다.
        rental.drive()
    }
}

let company = RentalCompany()
company.rentCar(...)
// Objective-C
@interface RentalCompany
@property Car * rental;
@end
@implement RentalCompany
@synthesize rental;
-(void) rentCarWithType:(NSString *)type {
    [self setRental: [[Car alloc] init]];
    [[self rental] setType:type];
    
    // 차를 운전하고, 돌려줍니다. 그리고 아무것도 안 합니다.
    [rental drive];
}
@end

 

rentCar() 메소드의 본문이 끝나더라도 rental 프로퍼티는 여전히 새로 만들어진 Car 클래스형 인스턴스를 참조하고 있습니다.

RentalCompany 인스턴스가 rental 프로퍼티를 통해 해당 Car 인스턴스 참조를 유지하고 있기 때문에 retainCount 값이 0보다 큽니다. 다시 표현하면 RentalCompany 클래스에 의해 해당 인스턴스가 여전히 사용중이기에 strong 참조는 이 인스턴스를 메모리에 유지시키고 있습니다.

그렇다면 이렇게 짜여진 코드에서 Car 인스턴스는 메모리로부터 언제 제거될 것일까요? RentalCompany 클래스가 소멸될 때 함께 소멸됩니다. RentalCompany 인스턴스가 더 이상 필요가 없게 되어 retainCount0이 되었을 때, 여기에 딸린 Car 인스턴스도 더 이상 필요가 없게 되어서 두 인스턴스가 메모리에서 함꼐 소멸됩니다.

 

ARC를 사용할 때 흔히 발생하는 문제

여러분이 객체들의 참조를 주의깊게 추적해가는 동안 다른 누군가는 너무 어렵고 복잡해 할 것입니다. 천만다행인 것은 이것은 (거의) 자동으로 수행되는 메커니즘이라는 것입니다.

이전 장에서 우리는 짤막하게 strong 참조에 대해 살펴보았습니다. 이런 형태의 변수는 참조하고 있는 인스턴스의 retainCount를 증가시킵니다. strong 참조의 반대 개념에는 두 가지가 있는데 그 중 하나는 weak 참조입니다. weak 참조는 인스턴스의 retainCount를 증가시키지 않습니다.

이에 근거하여 여러분은 ARC를 사용할 때 두 가지의 흔한 문제를 실수로 일으킬 수 있습니다.

(1) strong 참조를 해야하는 곳에서 weak 참조를 사용하여 여러분이 나중에 해당 인스턴스를 사용하려 할 때 그 인스턴스가 이미 메모리에서 사라지고 없을 수 있습니다.

(2) weak 참조를 해야하는 곳에서 strong 참조를 사용하여 retain의 순환을 일으킬 수 있습니다.

 

특히 retain이 순환하는 것은 골치아픈 문제입니다. 이는 간단하게 이해하면 두 개의 객체가 서로를 strong으로 참조하고 있는데 이렇게 하면 두 객체는 항상 retainCount0보다 크게 되어 둘 다 메모리에서 제거될 수 없습니다. 결국 메모리 누수를 야기하는데, 이는 우리가 만든 앱이 운영체제에서 “모범 시민”으로서 행동하기 위해 피해야 할 첫 번째 불량입니다.

retain 순환 참조는 여러분이 어느 한 참조를 weak 참조로 지정하는 것으로써 해결될 수 있습니다. retain 순환은 옵셔널에도 영향을 주는데 예를 들어 클로저에서 [weak self]라든가 unowned 같은 것이 이에 해당합니다. 클로저에 대해 배울 때 함께 알아보겠습니다.

카테고리 “Language/Objective-C & Swift”
more...
썸네일 이미지
[번역글] Swift의 자동 레퍼런스 카운트 (1)
본 게시글은 Automatic Reference Counting (ARC) in Swift를 바탕으로 작성하였습니다. Swift의 자동 레퍼런스 카운트 자동 레퍼런스 카운트(ARC: Automatic Reference Counting)는 Swift에서 메모리를 관리하는 메커니즘입니다. ARC라는 개념을 가지고 작업하는 것은 최신의 iOS 개발에서 필수적입니다. 이번 게시글에서 우리는 iOS의 메모리 관리를 위해 ARC가 어떻게 사용되는지 살펴보겠습니다. ARC는 어떻게 작동되는가 iOS 앱에서 메모리 관리가 왜 중요한가 객체의 retain 횟수가 소멸에 어떻게 영향을 주는가 메모리 관리의 실전 예제 순환 retain과 같은 흔한 결함 strong 참조 및 weak 참조를 써서 작업하기 준비 되었다면 시작..
Language/Objective-C & Swift
2021. 9. 16. 23:36

[번역글] Swift의 자동 레퍼런스 카운트 (1)

Language/Objective-C & Swift
2021. 9. 16. 23:36

본 게시글은 Automatic Reference Counting (ARC) in Swift를 바탕으로 작성하였습니다.

Swift의 자동 레퍼런스 카운트

자동 레퍼런스 카운트(ARC: Automatic Reference Counting)는 Swift에서 메모리를 관리하는 메커니즘입니다. ARC라는 개념을 가지고 작업하는 것은 최신의 iOS 개발에서 필수적입니다. 이번 게시글에서 우리는 iOS의 메모리 관리를 위해 ARC가 어떻게 사용되는지 살펴보겠습니다.

  • ARC는 어떻게 작동되는가
  • iOS 앱에서 메모리 관리가 왜 중요한가
  • 객체의 retain 횟수가 소멸에 어떻게 영향을 주는가
  • 메모리 관리의 실전 예제
  • 순환 retain과 같은 흔한 결함
  • strong 참조 및 weak 참조를 써서 작업하기

준비 되었다면 시작하겠습니다.

 

메모리 관리란 무엇인가?

모든 iPhone은 한정된 양의 RAM을 가졌는데 오늘날에는 대략 4GB 정도입니다. RAM은 여러분이 iPhone을 사용하는 동안 정보를 일시적으로 보관할 수 있는 저장소 역할을 합니다. RAM에 보관된 데이터는 iPhone이 재부트될 때 유지되지 않습니다. 하지만 읽고 쓰는 속도는 정말로 빠릅니다.

  • RAM 빠르긴 하지만 작은 메모리이며 컴퓨터에서 데이터를 단기간 보관할 수 있는 기억 장치입니다.
  • flash 느리긴 하지만 큰 메모리이며 컴퓨터에서 데이터를 장기간 보관할 수 있는 기억 장치입니다.

iPhone에서는 활성화된 모든 앱들, 심지어 iOS라는 운영체제 자체도 가용 RAM을 공유합니다. 하나의 앱이 많은 메모리를 사용한다면 나머지 앱은 적은 메모리밖에 사용할 수 없습니다. iOS는 운영체제 차원에서 메모리를 효율적으로 사용할 수 있는 프로세스들을 가지고 있습니다만, 우리는 우리의 앱 내에서 메모리를 어떻게 관리할 지에 대해서만 살펴보겠습니다.

Swift로 새 객체를 생성할 때, 그 객체에 맞는 소정의 메모리가 할당됩니다. 객체가 더 이상 필요가 없을때 이 객체는 메모리 공간을 비워주며 소멸됩니다. 할당과 소멸은 전력 및 CPU 시간을 적지않게 소비합니다. 여러분은 이러한 개념을 저장 창고에 비유할 수 있습니다. 어떤 꾸러미를 창고에 보관하기 위해 일정한 시간과 노력이 필요하다는 의미입니다.

메모리 관리는 소멸 및 할당 해제될 수 있는 대상들을 찾아내는 과정입니다. 그래서 RAM 공간 내에서 여유분이 확보됩니다. 이것은 연속적으로 수행되는 작업인데, 새로운 데이터를 위해 RAM 일부가 사용되고 그 데이터가 더 이상 필요 없을 때는 RAM에서 제거됩니다. 물론 필요한 데이터가 RAM에서 잘못 소멸되면 문제입니다. 앱 개발자로서 여러분이 할 업무는 메모리가 (거의) 자동으로 관리될 수 있게끔 앱을 작성하는 것입니다.

PC나 매킨토시와는 다르게 iOS는 자주 참조되지 않거나 크기가 큰 데이터를 보조기억장치에 기록하는 스왑(swap) 파일을 사용하지 않습니다. iPhone에 탑재되는 플래시 저장장치는 스왑을 사용할 수 있을만큼 빠르지 않기 때문입니다. 대신에 iOS 앱은 운영체제가 할당해 준 양보다 더 많은 메모리를 사용하려할 때 iOS는 그 앱에게 활성 메모리의 점유를 줄이도록 요구하거나 아예 그 앱을 직권으로 종료시켜 버립니다.

 

왜 메모리를 관리해야 하는가?

RAM의 크기는 한정적이고 모든 앱이 동일한 풀(pool)을 공유하기 때문에 각각의 앱들은 그 운영체제 안에서 "모범 시민"으로서 행동하며 효율적으로 메모리를 관리해나가야 합니다. 이론적으로 메일(Mail) 앱이 단말기 RAM의 90%를 차지하고 있는 중이라면, 곧 메모리가 바닥날 것이기 때문에 이와 동시에 지도(Maps) 앱을 사용할 수는 없습니다.

여러분의 iPhone에서 RAM의 여분이 부족할 것이라 판단되면, iOS는 메모리를 너무 많이 차지하고 있는 앱들을 강제종료시킵니다. 이러한 작동은 사용자 경험(User Experience)을 떨어뜨릴 것이고 개발자로서도 그러한 사태를 원치 않을 것입니다.

특히 모바일 플랫폼이기 때문에 Apple은 배터리 사용에 가장 엄격하게 주안점을 두고 있습니다. 활성화된 데이터를 RAM에 적재해 두고 있는 것은 상당한 에너지를 소비하게 됩니다. 그렇다면 어떤 앱이 너무 많은 메모리를 사용하고 있고 어쩌면 필요 이상으로 RAM을 잡아먹고 있다면 배터리 또한 빨리 소모될 것임을 여러분도 상상하실 것입니다. 그러한 앱을 강제 종료시켰을 때 어쩌면 그 앱의 당시 상태가 보조기억장치인 플래시 저장소에 기록될 수 있고 이는 또 다른 CPU 및 전력 소비로 이어질 수 있습니다.

그렇다면 iOS에서 왜 메모리 관리에 대해 공부해야 할까요?

  1. 그냥 재미있기 때문에?
  2. retain의 순환을 피하기 위해?

간단히 말하면 iPhone에서 RAM은 한정된 자원이기 때문에 이 메모리를 효율적으로 잘 사용한다면 iPhone 사용자에게 최상의 사용자 경험을 제공할 수 있을 것입니다. 이것이 우리가 개발자로서 메모리를 관리해야 하는 이유입니다.

 

ARC와 가비지 콜렉션(Garbage Collection)

계속 설명하겠으나 iOS에서 여러분이 메모리 관리를 위해 ARC를 사용할 때, 안드로이드 기반 플랫폼에서는 메모리를 관리하기 위해 가비지 콜렉션(Garbage Collection)을 사용합니다.

간단히 설명하면 가비지 콜렉션은 사용되지 않는 데이터를 메모리에서 삭제하는 작동을 담당하는 독립된 OS 프로세스입니다. 이 작업은 메모리에서 제거되어야 할 대상을 찾아내야 하기 때문에(이를 “mark-and-sweep”라 합니다), 종종 기기의 성능을 잡아먹습니다.

하지만 ARC는 이와 같인 병목 현상이 없습니다. 왜냐하면 메모리 관리를 위한 코드가 이미 그 앱의 일부로 들어가기 때문입니다(이에 비해 가비지 콜렉션은 앱과는 별도의 프로세스로 돌아갑니다). 물론 ARC는 개발자에게 좀 더 많은 노력을 요구하는 특징이 있으며 잘못 관리될 경우 retain 순환 같은 문제가 발생할 수 있습니다.

 

ARC는 어떻게 작동되는가?

지금부터는 ARC가 어떻게 작동되는지 살펴보겠습니다. ARC를 이해하기 위한 필수 사항들을 훑어보시기 바랍니다.

  • ARC의 목적은 RAM 공간 확보를 위해 어떤 데이터가 RAM에서 제거되어야 하는 지를 알기 위함입니다.
  • ARC의 필수적인 개념은 retain의 횟수입니다. 이것은 어떤 객체가 다른 객체들에게 몇 번째로 참조(hold into)되고 있는지를 추적할 수 있는 숫자입니다.
  • ARC는 클래스(class)와 같은 레퍼런스 타입(reference type)에만 적용될 수 있습니다. 구조체(struct)와 같은 값 타입(value type)에는 적용되지 않습니다. 값 타입은 통째로 복제가 이루어질 뿐 참조로써 작동되지는 않습니다.

Automatic Reference Counting이라는 이름이 어떻게 유래되었는지 여러분은 이미 살펴보았습니다.

(1) ARC는 여러분의 소스 코드에서 객체들 사이에 이뤄지는 연결고리, 즉 참조(reference)와 관계가 있는 것입니다.

(2) ARC는 어떤 객체가 retain된 횟수를 추적하는 계수(counting)에 대한 것입니다.

(3) ARC는 여러분이 개발자로서 메모리 관리에 대해 최소한의 노력만 하면 되게끔 도울 수 있는 자동화(automating)에 대한 것입니다.

참고로 2011년 iOS5 및 Xcode 4.2가 공개되기 전까지 개발자들은 객체의 할당과 소멸을 일일이 직접 해줘야 했습니다. 여러분은 어쩌면 어떤 객체에 대한 참조 횟수 증감을 위해 alloc, retainrelease 등의 메소드를 사용해 보았을 지도 모릅니다. retain 메소드는 어떤 객체의 참조 횟수를 증가시키는 역할을 하고, release 메소드는 감소시키는 역할을 합니다. ARC와 마찬가지로 참조 횟수가 0이 되면 그 객체는 메모리에서 제거됩니다. 오늘날에는 ARC가 이 과정을 자동으로 수행합니다.

 

Retain Count를 추적하기

Swift에서 모든 객체(클래스의 인스턴스들)은 retainCount라는 프로퍼티를 가지고 있습니다. 이 값이 0보다 크면 그 객체는 메모리에 계속 남아있겠지만 계속 감소하다가 0에 도달하면 메모리에서 제거됩니다.

앞서 살펴보았듯이 클래스로 표현되는 레퍼런스 타입에 한에서 이 retainCount를 갖습니다. 구조체는 소스 코드의 한 부분에서 다른 부분으로 전달될 때 복사가 이뤄지기 때문에 참조 횟수를 증가시켜야할 이유가 없습니다. 여러분의 소스 코드에서 ARC는 오직 “공유”되는 인스턴스 또는 참조에 의해 전달되는 인스턴스에 대해서만 역할을 합니다.

계속 내용을 이어가기에 앞서 비유를 하나 들어보겠습니다. Berg, Pete 및 Sharon이라는 3명의 사람이 전화번호가 적혀있는 쪽지 하나를 서로 공유하고 있다고 상상해 봅니다. 우리는 전화번호가 적혀 있는 그 쪽지를 paper라고 이름짓겠습니다. 물론 그 paper라는 것은 Swift 클래스입니다.

이 때 여러분은 세 사람이 하나의 쪽지를 가지고 서로 손을 잡게 되는 상황을 상상할 수 있습니다. Berg, Pete 및 Sharon은 모두 각자의 용도가 있어서 그 쪽지를 필요로 합니다. 그래서 하나의 쪽지가 세 명에게 붙잡혀 있는 것이지요.

이 상황에서 paperretainCount3입니다. 왜일까요? 3명이 paper라는 그 객체를 붙잡고 있기 때문입니다. 여기서 결론이 나옵니다. paper 객체는 현재 사용중이기 때문에 메모리에서 제거될 수 없습니다.

만일 Sharon이 그 쪽지가 더 이상 필요 없기에 손을 놓아버렸다고 가정합니다. 이후 Pete도 쪽지에서 손을 놓습니다. 이 두 명은 쪽지에 적힌 전화번호를 가지고 용무를 다 끝냈기 때문이죠. 그러면 retainCount2가 되었다가 다시 1로 감소합니다. Berg만이 아직 그 쪽지를 붙잡고 있습니다. 때문에 이 쪽지는 아직 사라질 수 없습니다.

마침내 Berg도 그 쪽지에서 손을 놓아버립니다. 이는 retainCount0에 도달하게 만듭니다. 따라서 paper 객체가 메모리에서 제거됩니다. 그 객체를 필요로 하는 곳이 더 이상 없기 때문에 그 객체가 점유하고 있던 메모리도 해제되어서 다른 앱이 사용할 수 있게 됩니다.

실전 Swift 프로그래밍을 통해 이것이 어떻게 작동될 수 있는지 확인해 보겠습니다.

 

실전 ARC

우리는 이제 ARC가 실제로 코드에서 어떻게 나타나는지를 보려 합니다. 먼저 다음과 같이 Car라는 이름의 간단한 클래스를 하나 선언 및 정의해 보겠습니다.

// Swift
class Car {
    var name = ""
    init (name: String) {
        self.name = name
        print("Instance of Car '\(name)' is initialized.")
    }
    deinit {
        print("Instance of Car '\(name)' is deinitialized.")
    }
}
// Objective-C
@interface Car : NSObject
@property NSString * name;
@end

@implementation Car
@synthesize name;
- (instancetype)initWithName:(NSString *)name {
    self = [self init];
    if (self) {
        [self setName:name];
        NSLog(@"Instance of Car '%@' is initialized.", [self name]);
    }
    return self;
}
- (void)dealloc {
    NSLog(@"Instance of Car '%@' is deinitialized.", [self name]);
}
@end

 

Car 클래스는 String 타입의 프로퍼티인 name을 갖고 있습니다. 우리는 또한 initdeinit라는 생성자와 소멸자도 정의하였습니다. 각각 Car형 인스턴스가 생성되고 소멸될 때 호출될 것이고, print가 호출되기에 우리에게 내부적으로 어떤 호출이 발생하는지 알 수 있도록 도와줄 것입니다.

그리고 다음과 같이 두 개의 변수를 선언합니다.

// Swift
var car1: Car?
var car2: Car?
// Objective-C
Car * car1;
Car * car2;

 

두 변수의 타입은 Car?형, 즉 옵셔널(optional) 타입이고 둘 다 아직 초기화되지는 않았습니다.

Car 클래스의 새 인스턴스를 car1 변수에 대입합니다. 새 인스턴스를 만들 때 name 프로퍼티도 다음과 같이 설정합니다.

// Swift
car1 = Car(name: "Maserati")
// Objective-C
car1 = [[Car alloc] initWithName: @"Maserati"];

그리고 car1가 갖게 된 참조를 car2에도 다음과 같이 대입합니다.

// Swift
car2 = car1
// Objective-C
car2 = car1;

이렇게 하면 동일한 Car 클래스형 인스턴스에 대해 car1car2함께 참조하게 됩니다. Swift에서 이것이 어떻게 작동하는가 하면, 객체 자체는 복사되지 않았지만 그 객체에 대한 참조가 복사되었습니다. 그래서 car1car2동일한 객체를 현재 가리키고 있습니다.

흥미로운 부분은 여기에서부터 시작합니다. 다음과 같이 해 보겠습니다.

// Swift
car1 = nil
// Objective-C
car1 = nil;

car1 변수가 nil로 설정되어 이 변수는 더 이상 앞서 생성한 Car 클래스형 인스턴스를 가리키고 있지 않습니다. 그렇다면 car2는 어떨까요?

car1 변수가 nil 참조로 바뀌었다 하더라도 car2 변수는 여전히 Car 클래스형 인스턴스를 가리키고 있습니다. 그래서 이 인스턴스는 소멸되지 않고 여전히 메모리에 남아있습니다.

여기서 우리는 이것도 해 보겠습니다.

// Swift
car2 = nil
// Objective-C
car2 = nil;

이를 실행해 보았을 때 다음과 같이 출력될 것입니다.

 

추측하였겠지만 위와 같은 코드로 인해 인스턴스를 참조하고 있는 변수가 없어졌기 때문에 이 인스턴스의 retainCount0이 되면서 메모리에서 소멸됩니다.

이 예제 실습에서 우리가 알게 모르게 확인했던 핵심 개념으로 강한(strong) 참조가 있습니다. 이 예제에서 car1 변수와 car2 변수는 Car 클래스형 인스턴스에 대해 “strong” 참조를 하고 있습니다.

strong 참조 변수는 자신이 참조하게 될 객체에 대해 retainCount를 증가시킵니다. 그 결과 객체는 다른 변수에서 전부 nil 참조로 바뀌어도 한 변수가 참조를 유지하고 있는 한 메모리에 남아있을 것이고, 모든 변수에서 참조가 해제되었을 때가 되어서야 그 객체는 메모리에서 사라질 것입니다. 이것이 ARC가 작동하는 방식입니다.

다음의 예제를 자유롭게 변형해보면서 ARC의 작동을 확인해보시기 바랍니다.

// Swift
class Car {
    var name = ""
    
    init(name: String) {
        self.name = name
        print("Instance of Car '\(name)' is initialized.")
    }
    
    deinit {
        print("Instance of Car '\(name)' is deinitialized.")
    }
}

var var1: Car?
var var2: Car?

car1 = Car(name: "Maserati")
car2 = car1

car1 = nil
car2 = nil
// Objective-C
@interface Car : NSObject
@property NSString * name;
@end

@implementation Car
@synthesize name;
- (instancetype)initWithName:(NSString *)name {
    self = [self init];
    if (self) {
        [self setName:name];
        NSLog(@"Instance of Car '%@' is initialized.", [self name]);
    }
    return self;
}
- (void)dealloc {
    NSLog(@"Instance of Car '%@' is deinitialized.", [self name]);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        Car * car1;
        Car * car2;
        
        car1 = [[Car alloc] initWithName:@"Maserati"];
        car2 = car1;
        
        car2 = nil;
        car1 = nil;
    }
    return 0;
}

다음 장에서는 할당 해제에 대해 보다 상세히 살펴보도록 하겠습니다.

 

카테고리 “Language/Objective-C & Swift”
more...
썸네일 이미지
[번역글] View Controller 사이에 데이터를 교환하는 방법(6) - NotificationCenter [完]
본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다. 여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요? 뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다. 뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(Ap..
Language/Objective-C & Swift
2021. 9. 16. 09:49

[번역글] View Controller 사이에 데이터를 교환하는 방법(6) - NotificationCenter [完]

Language/Objective-C & Swift
2021. 9. 16. 09:49

본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다.

 

여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요?

뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다.

뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(App architecture)는 여러분이 뷰 컨트롤러 사이에 어떻게 작동이 이루어 질 것인지에 영향을 주고, 반대로 여러분이 뷰 컨트롤러 사이에 데이터 교환을 어떻게 할 것인지에 따라 앱의 구조가 달라집니다.

 

Swift에서 여러분은 뷰 컨트롤러 사이에 다음의 6가지 방법으로써 데이터를 주고 받을 수 있습니다.

  1. 인스턴스 프로퍼티(property)를 사용하는 방법(A → B 방향)
  2. 스토리보드(Storyboard)와 세그웨(segue)를 사용하는 방법
  3. 인스턴스 프로퍼티와 함수를 사용하는 방법(A ← B 방향)
  4. 델리게이션(delegation) 패턴을 사용하는 방법
  5. 클로저(closure) 또는 핸들러(completion handler)를 사용하는 방법
  6. NotificationCenter 또는 Observer 패턴을 사용하는 방법

 

이 게시글에서 여러분은 뷰 컨트롤러 사이에 데이터를 주고 받는 6가지의 각기 다른 방법에 대해 익히게 될 것입니다. 이 방법에는 프로퍼티(property)를 사용하는 방법, 세그웨(segue)를 사용하는 방법 및 NSNotificationCenter를 사용하는 방법도 들어 있습니다. 비교적 간단한 이들 방법을 통해 좀 더 복잡한 응용까지 나아갈 수 있습니다. 준비가 되었다면 이제 시작하겠습니다.

 

NotificationCenter 또는 Observer 패턴을 사용하기

NotificationCenter 클래스를 사용하여 뷰 컨트롤러 사이에 데이터를 전달할 수도 있습니다. Swift 3부터는 NS 접두어가 빠지면서 그냥 NotificationCenter으로 씁니다. 또한 여기서 말하는 알림(notification)은 푸시 알림(push notification)과는 다른 것임을 알아두시기 바랍니다.

NotificationCenter는 각종 알림들을 다루는데, 특히 접수되는 알림들을 자신의 상태를 청취하고 있는 각종 요소들에게 다시 배분해주는 역할도 합니다. iOS의 SDK에서 NotificationCenter를 구현하는 것은 관찰자(Observer-Observable) 디자인 패턴으로 접근합니다.

전혀 관계가 없는 두 클래스 사이에서도 Notification Center를 사용하면 데이터 교환이 가능하다.

NotificationCenter는 크게 세 가지의 핵심 단계들로 이뤄집니다.

  1. 알림(notification)이 있는지 관찰하기
  2. 알림(notification) 보내기
  3. 알림(notification)에 응답하기

지금부터 알림(notification)을 “관찰(observe)”해보겠습니다. 어떤 알림에 응답하기에 앞서, 여러분은 NotificationCenter에게 여러분이 자신을 관찰하고 싶어함을 알려주어야 합니다. 그러면 NotificationCenter는 자신을 거쳐가는 어떤 알림이든 여러분께 알려줄 것입니다.

모든 알림은 스스로를 식별할 수 있는 이름을 갖고 있습니다. MainViewController 클래스 상단에 다음과 같이 정적 프로퍼티를 하나 만듭니다.

// Swift
static let notificationName = Notification.Name("myNotificationName")
// Objective-C
@interface ...
+ (NSNotificationName)notificationName;
@end
@implement ...
+ (NSNotificationName)notificationName {
    static NSString * name = @"myNotificationName";
    return name;
}
@end

 

“클래스 프로퍼티(class property)”라고도 하는 정적 프로퍼티(static property)는 코드의 어디에서든 MainViewController.notificationName로써 호출될 수 있습니다. 즉 문자열 상수만 가지고 여러 notification 중 이에 해당하는 것만을 식별할 수 있는 것입니다. 이렇게 문자열 상수를 한 곳에 정의해 놓는다면 문자열을 잘못 입력하여 알림을 못 받는 불상사가 일어나지 않을 것 입니다.

이제 notification을 관찰해 보겠습니다.

// Swift
NotificationCenter.default.addObserver(self, selector:#selector(onNotification(notification:)), 
                                                 name:MainViewController.notificationName,
                                               object:nil)
// Objective-C
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(onNotification:)
                                             name:notificationName
                                           object:nil];

 

여러분은 위와 같은 코드를 대체로 viewDidLoad()viewWillAppear(_:) 같은 메소드에 작성할 것입니다. 그래서 뷰 컨트롤러가 화면에 나타날 때 여러분의 관찰 행동이 notification에 등록이 되겠지요. 위 예제에서 어떤 작업들이 수행되는지 살펴보겠습니다.

(1) 먼저 기본으로 내장된 NotificationCenter를 뜻하는 NotificationCenter.default를 사용하고 있습니다. 물론 특수한 용도를 위해 여러분이 직접 NotificationCenter를 생성할 수도 있습니다. 하지만 기본 제공된 것을 사용하는 것이 나을 때가 많습니다.

(2) 위의 NotificationCenter에 대해 addObserver(_:selector:name:object:) 메소드를 호출하고 있습니다.

  1. 첫 번째 매개변수는 관찰을 수행하는 객체(인스턴스)를 지정하는 것인데 거의 항상 self일 때가 많습니다.
  2. 두 번째 매개변수는 알림이 관측되었을 때 호출될 함수(메소드)를 가리키는 셀렉터(selector)입니다. 대개 여기서 지정되는 함수는 현 클래스 내에 있는 메소드일 경우가 많습니다.
  3. 세 번째 매개변수는 알림의 이름입니다. 여기서는 앞서 정적 프로퍼티로 선언 및 정의했던 notificationName을 적어주면 됩니다.
  4. 네 번째 매개변수는 여러분이 알림에 동반되는 객체를 받고자 할 때 사용되는 매개변수인데, 대개는 nil을 지정하지만 특정 객체에서 생성되는 알림만을 받고자 할 때 그 객체를 이 곳에 지정할 수도 있습니다.

이후에 다음과 같은 코드로써 알림 관측을 중지시킬 수도 있습니다.

// Swift
NotificationCenter.default.removeObserver(self, name:MainViewController.notificationName,
                                              object:nil)
// Objective-C
[[NotificationCenter defaultCenter] removeObserver:self
                                              name:notificationName
                                            object:nil];

 

등록시켰던 모든 알림 관측을 한 번에 중지시킬 수도 있습니다.

// Swift
NotificationCenter.default.removeObserver(self)
// Objective-C
[[NotificationCenter defaultCenter] removeObserver:self];

 

모든 알림은 명시적(explicit)이기 때문에 여러분은 알림이 발생할 때 하나의 객체당(대개는 self) 하나의 함수(메소드) 호출이 이뤄지는 한 가지 유형의 알림 단위로 관측하게 될 것임을 기억해 두기 바랍니다.

알림이 발생하였을 때 호출될 함수(메소드)는 onNotification(notification:)입니다. 이것도 클래스에 추가해 보겠습니다.

// Swift
@objc func onNotification(notification:Notification) {
    print(notification.userInfo)
}
// Objective-C
-(void) onNotification:(NSNotification *)notification {
    NSLog("%@", [notification userInfo])
}

 

Swift 4부터 @objc 키워드가 필요하게 되었습니다. 왜냐하면 NSNotificationCenter는 Objective-C 코드로 적힌 프레임워크의 일부이기 때문입니다. 이 함수(메소드)에서 여러분은 간단하게 notification.userInfo를 출력할 것입니다.

알림을 띄우는 것은 쉽습니다. 다음과 같이 하면 됩니다.

// Swift
NotificationCenter.default.post(name:MainViewController.notificationName,
                              object:nil,
                            userInfo:["data": 42, "isImportant": true])
// Objective-C
[[NSNotificationCenter defaultCenter] postNotificationName:notificationName 
                                                    object:nil 
                                                  userInfo:@{@"data": @42, @"isImportant": @true}];

몇 가지 중요한 사실을 다시 확인해 보겠습니다.

(1) 여러분은 앞서 사용한 Notification Center인 NotificationCenter.default 객체를 통해 post(name:object:userInfo:)를 호출하여 알림을 띄워야 합니다.

(2) 첫 번째 매개변수는 띄울 알림을 식별할 수 있는 이름입니다. 여러분이 앞서 정의한 정적 상수를 이 곳에서도 사용하면 됩니다.

(3) 두 번째 매개변수는 알림을 띄우는 객체입니다. 대개 nil을 전달하겠지만, 알림을 관측하기 위해 어떤 객체를 지정하였다면 알람을 띄울 때에도 같은 객체를 이 곳에 지정하여 그 객체만을 위해 배타적으로 알림을 띄우고 수신할 수 있습니다.

(4) 세 번째 매개변수는 userInfo라고 불리는 부가적인 정보입니다. 여러분은 딕셔너리(Dictionary / NSDictionary) 형식으로써 어떤 종류의 데이터든 전달할 수 있습니다. 이 예제에서도 정수와 불리언 값으로 이루어진 약간의 데이터를 추가로 주고 받아보았습니다.

 

Notification Center를 사용하여 뷰 컨트롤러 사이에 데이터를 주고 받는 방법은 이것으로 모두 살펴보았고, 예를 들어 다음과 같은 상황에서 사용할 수 있습니다.

(1) 데이터를 주고 받고자 하는 뷰 컨트롤러 또는 다른 클래스 사이에는 밀접하게 관련되어있지 않는 경우입니다. REST API가 새로운 데이터를 받을 때마다 응답해야 하는 테이블 뷰 컨트롤러(Table View Controller)에 대해 떠올려봅니다.

(2) 뷰 컨트롤러가 꼭 지금 당장 존재할 필요는 없는 경우입니다. 테이블 뷰 컨트롤러가 화면에 나타나기에 앞서 REST API가 데이터를 수신하는 경우도 생길 수 있습니다. 알림을 관찰하는 것은 선택적인 사항이기 때문에 앱의 요소 중 알림을 받아야 하는 일부 요소가 아직 만들어지지 않았거나 이미 사라졌더라도 알림을 띄우는 입장에서는 이에 개의치 않고 알림을 띄울 수 있습니다.

(3) 뷰 컨트롤러들은 어떤 하나의 알림에 대해 응답할 필요가 있을 것이고, 하나의 뷰 컨트롤러는 여러 개의 알림에 응답할 필요가 있을 수 있습니다. 다시 말하면 Notification Center는 다대다(many-to-many) 관계를 갖습니다.

여러분은 이 Notification Center를 마치 정보의 간선 고속도로처럼 생각할 수도 있습니다. iOS 내 각 부분에서 띄워진 알림은 일단 이 쪽으로 모인 다음에 다시 다양한 목적지로 향해 가기 때문입니다. 단지 뷰 컨트롤러 사이에 발생하는 “지역적인 트래픽(local traffic)”만을 원할 경우 Notification Center를 선택하는 것은 적합하지 않습니다. 앞서 살펴 본 델리게이트, 프로퍼티 또는 클로저 같은 비교적 단순한 방법들이 있기 때문입니다. 그러나 여러분이 어떤 데이터를 반복적으로, 규칙적으로 여러분의 앱의 한 부분에서 다른 부분으로 보내고자 한다면 Notification Center는 최고의 방법입니다.

카테고리 “Language/Objective-C & Swift”
more...
썸네일 이미지
[번역글] View Controller 사이에 데이터를 교환하는 방법(5) - closure
본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다. 여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요? 뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다. 뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(Ap..
Language/Objective-C & Swift
2021. 9. 15. 19:44

[번역글] View Controller 사이에 데이터를 교환하는 방법(5) - closure

Language/Objective-C & Swift
2021. 9. 15. 19:44

본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다.

 

여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요?

뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다.

뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(App architecture)는 여러분이 뷰 컨트롤러 사이에 어떻게 작동이 이루어 질 것인지에 영향을 주고, 반대로 여러분이 뷰 컨트롤러 사이에 데이터 교환을 어떻게 할 것인지에 따라 앱의 구조가 달라집니다.

 

Swift에서 여러분은 뷰 컨트롤러 사이에 다음의 6가지 방법으로써 데이터를 주고 받을 수 있습니다.

  1. 인스턴스 프로퍼티(property)를 사용하는 방법(A → B 방향)
  2. 스토리보드(Storyboard)와 세그웨(segue)를 사용하는 방법
  3. 인스턴스 프로퍼티와 함수를 사용하는 방법(A ← B 방향)
  4. 델리게이션(delegation) 패턴을 사용하는 방법
  5. 클로저(closure) 또는 핸들러(completion handler)를 사용하는 방법
  6. NotificationCenter 또는 Observer 패턴을 사용하는 방법

 

이 게시글에서 여러분은 뷰 컨트롤러 사이에 데이터를 주고 받는 6가지의 각기 다른 방법에 대해 익히게 될 것입니다. 이 방법에는 프로퍼티(property)를 사용하는 방법, 세그웨(segue)를 사용하는 방법 및 NSNotificationCenter를 사용하는 방법도 들어 있습니다. 비교적 간단한 이들 방법을 통해 좀 더 복잡한 응용까지 나아갈 수 있습니다. 준비가 되었다면 이제 시작하겠습니다.

 

클로저(closure) 또는 핸들러(completion handler)를 사용하는 방법

클로저(closure)를 사용하여 뷰 컨트롤러 사이에 데이터를 전달하는 것은 프로퍼티나 델리게이션을 사용하는 것과 크게 다르지 않습니다. 클로저를 사용하면 상대적으로 사용하기가 쉽고 또한 별도의 메소드(함수)나 프로토콜을 선언하지 않아도 지역적으로 데이터 교환 구문을 정의할 수 있다는 이점이 있습니다.

클로저를 사용하여 뷰 컨트롤러 사이에 데이터를 주고받을 수 있다.

나중에 등장하게 될 뷰 컨트롤러에 다음과 같이 프로퍼티를 하나 만드는 것으로 소스 코드를 시작합니다.

// Swift
var completionHandler: ((String) -> Int)?
// Objective-C
@interface ...
@property (atomic, weak) NSInteger (^completionHandler)(NSString *);
@end
@implementation ...
@synthesize completionHandler;
@end

 

여기서 completionHandler는 클로저 타입을 가지고 있습니다. 이 클로저는 옵셔널(optional)이기 때문에 끝에 ?가 붙습니다. 또한 클로저의 시그니처(signature)는 (String) -> Int입니다. 이것은 이 클로저가 String형 매개변수를 하나 받고 Int형 값을 반환하는 메소드(함수 또는 람다식)임을 뜻합니다.

나중에 등장할 뷰 컨트롤러에서 작성할 또 하나의 사항은 버튼이 탭(tap)되었을 때 그 클로저를 호출하는 것입니다.

// Swift
@IBAction func onButtonTap(sender: Any) {
    let result = completionHandler?("FooBar")
    print("completionHandler returns ... \(result)")
}
// Objective-C
-(IBAction) onButtonTapWithSender:(id)sender {
    NSInteger result;
    
    if ([self completionHandler] != nil) {
        result = [self completionHandler](@"FooBar");
        NSLog("completionHandler returns ... %d", result);
    }
}

 

제시된 예제는 다음과 같이 작동합니다.

한 개의 매개변수가 지정되면서 completionHandler 클로저가 호출되면, 그 클로저의 호출 결과가 result 변수에 보관됩니다.

결과는 print()의 호출에 의해 출력됩니다.

그 다음 MainViewController에서 여러분은 다음과 같이 클로저를 정의합니다.

// Swift
secondaryViewController.completionHandler = { text in
    print("text = \"\(text)\"")
    return text.characters.count
}
// Objective-C
[[self secondaryViewController] setCompletionHandler: ^(NSString * text) {
    NSLog("text = \"%@\", text);
    return (NSInteger)[text length];
}

 

이것이 클로저 그 자체입니다. 이것은 지역 범위에서 선언되었기 때문에 클로저가 선언된 스코프 내의 로컬 변수, 프로퍼티 및 메소드(함수)들을 모두 사용할 수 있습니다.

text 매개변수가 출력되어 나오는 클로저에서는 실행의 결과로서 문자열의 길이가 반환됩니다. 여기서 흥미로운 사실이 발견됩니다. 이런 식으로 작성되는 클로저는 뷰 컨트롤러 사이에서 데이터를 양방향으로 전달해준다는 것입니다. 여러분이 클로저를 정의하고 유입될 데이터에 대하여 작업한 다음 이 클로저를 호출한 코드에게 결과 데이터를 반환할 수 있습니다.

물론 여러분은 델리게이션을 사용하거나 프로퍼티 자체를 사용한 메소드(함수) 호출에서도 그 코드 블록을 호출해 준 코드에게 소정의 값을 반환할 수 있다는 것을 알고 있을 지 모릅니다. 전적으로 맞습니다. 다음과 같은 상황에서 클로저는 여러분의 손에 잡히게 될 것입니다.

(1) 여러분은 단지 짧고 간략한 내용의 함수를 작성하고자 굳이 프로토콜을 만들어가며 완전한 델리게이션 방식으로 접근하기를 원하지 않을 수 있습니다.

(2) 여러분은 여러 클래스에 걸쳐 하나의 클로저를 전달하고 싶어질 수도 있습니다. 클로저가 없었다면 여러분은 쏟아지는 함수 호출들을 만들었어야 하겠으나, 클로저와 함께 여러분은 한 블록의 코드만 전달하면 됩니다.

(3) 어떤 데이터가 지역적으로만 존재할 때 여러분은 클로저 안에서 그 데이터에 접근할 수 있는 수준에서 코드 한 블록을 지역적으로(locally) 정의할 필요가 있을 것입니다.

클로저를 사용하여 뷰 컨트롤러 사이에 데이터 전달이 이뤄지게 할 때의 손해는, 여러분의 코드가 다소 복잡해 보일 수 있다는 것입니다. 다른 방법을 사용할 때보다 클로저를 쓰는 것이 더 이치에 맞다고 여겨질 때, 뷰 컨트롤러 사이에 데이터를 전달할 때에 한하여 클로저를 사용하는 것이 현명한 선택입니다.

그렇다면 두 개의 뷰 컨트롤러 사이에 연결고리가 형성되지 않았거나 형성될 수 없음에도 데이터를 주고 받기 위해서는 어떻게 해야 할까요? 다음 장에서 살펴보겠습니다.

카테고리 “Language/Objective-C & Swift”
more...
썸네일 이미지
[번역글] View Controller 사이에 데이터를 교환하는 방법(4) - delegation
본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다. 여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요? 뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다. 뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(Ap..
Language/Objective-C & Swift
2021. 9. 14. 23:05

[번역글] View Controller 사이에 데이터를 교환하는 방법(4) - delegation

Language/Objective-C & Swift
2021. 9. 14. 23:05

본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다.

 

여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요?

뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다.

뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(App architecture)는 여러분이 뷰 컨트롤러 사이에 어떻게 작동이 이루어 질 것인지에 영향을 주고, 반대로 여러분이 뷰 컨트롤러 사이에 데이터 교환을 어떻게 할 것인지에 따라 앱의 구조가 달라집니다.

 

Swift에서 여러분은 뷰 컨트롤러 사이에 다음의 6가지 방법으로써 데이터를 주고 받을 수 있습니다.

  1. 인스턴스 프로퍼티(property)를 사용하는 방법(A → B 방향)
  2. 스토리보드(Storyboard)와 세그웨(segue)를 사용하는 방법
  3. 인스턴스 프로퍼티와 함수를 사용하는 방법(A ← B 방향)
  4. 델리게이션(delegation) 패턴을 사용하는 방법
  5. 클로저(closure) 또는 핸들러(completion handler)를 사용하는 방법
  6. NotificationCenter 또는 Observer 패턴을 사용하는 방법

 

이 게시글에서 여러분은 뷰 컨트롤러 사이에 데이터를 주고 받는 6가지의 각기 다른 방법에 대해 익히게 될 것입니다. 이 방법에는 프로퍼티(property)를 사용하는 방법, 세그웨(segue)를 사용하는 방법 및 NSNotificationCenter를 사용하는 방법도 들어 있습니다. 비교적 간단한 이들 방법을 통해 좀 더 복잡한 응용까지 나아갈 수 있습니다. 준비가 되었다면 이제 시작하겠습니다.

 

델리게이션(delegation)을 사용하여 데이터를 되돌려 보내기

델리게이션(delegation)은 iOS SDK에서 중요하면서 빈번하게 사용되는 소프트웨어 디자인 패턴입니다. 여러분이 iOS 앱을 코딩할 것이라면 이를 이해하는 것이 필수적입니다!

델리게이션을 사용하면 부모 클래스(base class)는 파생 클래스(secondary class)의 기능에 대해 손을 떼어도 됩니다. 코더는 파생 클래스를 구현하면서 프로토콜(protocol)을 사용하기에 부모 클래스로부터 전해지는 이벤트에 반응만 하면 됩니다. 즉 부모 클래스와 파생 클래스 사이에 결합성 및 의존성이 줄어든다는 것입니다.

간단한 예를 통해 이해해 보겠습니다.

(1) 여러분이 피자 레스토랑에서 일하며 피자를 굽고 있다고 상상해 봅니다.

(2) 고객은 피자를 가지고 여러 가지를 할 수 있습니다. 바로 먹을 수도 있고, 나중에 먹으려고 얼려 놓을 수도 있고, 친구와 나눠먹을 수도 있습니다.

(3) 피자를 굽는 입장에서는 피자를 만드는 작업에 대해서만 생각하면 되고, 피자를 어떻게 먹을 것인지에 대한 작업과는 분리(decoupled)되어 있습니다. 단지 여러분은 피자가 완성되었을 때 고객에게 이를 가져갈 수 있게 알려주기만 하면 됩니다. 다시 말하면 피자를 먹는 방법에 대해서는 위임(delegate)해 버리고 여러분은 피자 굽는 일 자체에만 집중하면 됩니다.

델리게이션을 사용하여 뷰 컨트롤러 사이에 데이터를 교환한다.

피자를 굽는 여러분과 피자를 받아 먹게 될 고객이 서로를 이해하기 위하여 여러분은 “프로토콜(protocol)”을 정의할 필요가 있습니다.

// Swift
protocol PizzaDelegate {
    func onPizzaReady(type: String)
}
// Objective-C
@protocol PizzaDelegate {
    -(void) onPizzaReadyWithType:(NSString *)type;
}>

 

“프로토콜(protocol)”은 어떤 클래스가 구현해야 하는 함수(메소드)들에 대한 규약입니다. 클래스가 프로토콜을 준수하도록 하고 싶다면 다음과 같은 구문을 클래스 선언할 때 적어주면 됩니다.

// Swift
class MainViewController: UIViewController, PizzaDelegate {
    // ...
}
// Objective-C
@interface MainViewController: UIViewController<PizzaDelegate>
    // ...
@end

 

위와 같은 구문은,

(1) 클래스의 이름이 MainViewController이고;

(2) UIViewController에서 확장된 서브클래스이면서;

(3) PizzaDelegate 프로토콜을 준수(구현)하는 클래스라는 의미를 가지고 있습니다.

여러분이 이 클래스에 대해 프로토콜을 구현한다고 적었다면 이를 실제로 구현해주어야 합니다. 다음과 같이 MainViewController에 메소드(함수)를 적어줍니다.

// Swift
func onPizzaReady(type: String) {
    print("Pizza ready. type = \"\(type)\"")
}
// Objective-C
-(void) onPizzaReadyWithType:(NSString *)type {
    NSLog(@"Pizza ready. type = \"%@\"", type)
}

 

여러분이 나중에 등장할 뷰 컨트롤러에 대한 클래스 인스턴스를 생성할 때, 앞서 살펴본 예제에서와 같이 델리게이트를 연결시켜줍니다.

// Swift
let secondaryViewController = SecondaryViewController()
secondaryViewController.delegate = self
// Objective-C
SecondaryViewController * secondaryViewController
    = [[SecondaryViewController alloc] init];
[secondaryViewController setDelegate: self];

 

그리고나서 델리게이션의 진면목이 나타나게 됩니다. 나중에 등장하는 뷰 컨트롤러를 작성할 때 여러분은 프로퍼티를 추가하고 몇 가지 코드를 적습니다. 먼저 프로퍼티는 이렇게 적습니다.

// Swift
weak var delegate: PizzaDelegate?
// Objective-C
@interface ...
    @property (atomic, weak) PizzaDelegate * delegate;
@end
@implement ...
    @synthesize delegate;
@end

 

그리고 다음과 같이 메소드(함수)를 작성합니다.

// Swift
@IBAction func onButtonTap() {
    delegate?.onPizzaReady(type: "Pizza di Mama")
}
// Objective-C
-(IBAction) onButtonTap {
    [[self delegate] onPizzaReadyWithType: @"Pizza di Mama"];
}

 

onButtonTap()이 피자가 완성되었을 때 호출된다고 가정하면 이 메소드(함수)는 델리게이트에서 선언된 onPizzaReady(type:)을 호출합니다.

피자를 굽는 입장에서는 delegate 프로퍼티로 지정된 객체가 있는지 아니면 nil인지 여부에는 관심이 없습니다. 그저 피자를 완성시켜서 고객을 향해 내어 놓기만 하면 됩니다. 이로써 여러분은 고객이 피자를 어떤 식으로 먹든 상관하지 않을 수 있게 됩니다.

 

번역자 추가 설명 만일 Swift로 weak var 델리게이트를 작성 시 “'weak' must not be applied to non-class-bound...”로 시작하는 오류가 발생한다면, 프로토콜을 선언할 때 무조건 참조 형식인 클래스에서만 사용되도록 제한함으로써 해소시킬 수 있다.

// Swift
protocol PizzaDelegate: class {
    // ...
}

그러면 class 키워드를 써서 제한하는 것은 deprecated되었다는 경고가 뜰 수 있다. 이 때는 다음과 같이 수정한다.

// Swift
protocol PizzaDelegate: AnyObject {
    // ...
}

 

델리게이션을 통해 뷰 컨트롤러 사이에 데이터를 주고 받는 핵심적인 과정을 짚어보면 다음과 같습니다.

(1) 델리게이트 프로토콜 선언이 필요합니다.

(2) 델리게이트 프로퍼티 선언이 필요합니다.

(3) 여러분이 데이터 처리에 관여하고 싶지 않은 클래스가 있다면, 데이터를 받을 그 클래스는 해당 프로토콜을 구현하고 있어야 합니다.

(4) 여러분이 데이터를 전달한 후의 과정에 관여하고 싶지 않다면 프로토콜에서 선언된 메소드(함수)를 호출함으로써 데이터를 전달하면 됩니다.

 

앞서 살펴보았던 프로퍼티만을 사용한 예제하고 여기서 다루어 본 델리게이션을 사용한 예자하고 비교해 보았을때 어떤 차이점이 있을까요? 다음과 같이 정리할 수 있습니다.

(1) 각각의 클래스 개발을 맡은 서로 다른 개발자들은 프로토콜 및 그 프로토콜이 갖는 메소드(함수)에 대해서만 합의를 보면 됩니다. 각 개발자들은 자신의 클래스가 프로토콜을 준수할 것인지 아닌지를 선택할 수 있고, 필요하다면 프로토콜 내 메소드들을 구현할 수 있습니다.

(2) MainViewControllerSecondaryViewController 사이에는 직접적인 참조가 없습니다. 이는 앞선 예제에 비해 뷰 컨트롤러 객체들끼리 다소 느슨하게 결합되었음을 의미합니다.

(3) 여기서 선언한 프로토콜은 MainViewController 뿐만 아니라 어떤 클래스에서든 구현될 수 있습니다.

 

굉장합니다. 이제 클로저(closure)를 사용하는 또 다른 예제에 대해 살펴보도록 하겠습니다.

참고 weak라고 표시된 델리게이트 프로퍼티는 무엇일까요? Automatic Reference Counting(ARC) in Swift를 참고해 보세요.

카테고리 “Language/Objective-C & Swift”
more...
썸네일 이미지
[번역글] View Controller 사이에 데이터를 교환하는 방법(3) - property와 method
본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다. 여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요? 뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다. 뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(Ap..
Language/Objective-C & Swift
2021. 9. 14. 21:12

[번역글] View Controller 사이에 데이터를 교환하는 방법(3) - property와 method

Language/Objective-C & Swift
2021. 9. 14. 21:12

본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다.

 

여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요?

뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다.

뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(App architecture)는 여러분이 뷰 컨트롤러 사이에 어떻게 작동이 이루어 질 것인지에 영향을 주고, 반대로 여러분이 뷰 컨트롤러 사이에 데이터 교환을 어떻게 할 것인지에 따라 앱의 구조가 달라집니다.

 

Swift에서 여러분은 뷰 컨트롤러 사이에 다음의 6가지 방법으로써 데이터를 주고 받을 수 있습니다.

  1. 인스턴스 프로퍼티(property)를 사용하는 방법(A → B 방향)
  2. 스토리보드(Storyboard)와 세그웨(segue)를 사용하는 방법
  3. 인스턴스 프로퍼티와 함수를 사용하는 방법(A ← B 방향)
  4. 델리게이션(delegation) 패턴을 사용하는 방법
  5. 클로저(closure) 또는 핸들러(completion handler)를 사용하는 방법
  6. NotificationCenter 또는 Observer 패턴을 사용하는 방법

 

이 게시글에서 여러분은 뷰 컨트롤러 사이에 데이터를 주고 받는 6가지의 각기 다른 방법에 대해 익히게 될 것입니다. 이 방법에는 프로퍼티(property)를 사용하는 방법, 세그웨(segue)를 사용하는 방법 및 NSNotificationCenter를 사용하는 방법도 들어 있습니다. 비교적 간단한 이들 방법을 통해 좀 더 복잡한 응용까지 나아갈 수 있습니다. 준비가 되었다면 이제 시작하겠습니다.

 

프로퍼티(property)와 메소드(method, function)를 사용하는 방법(A ← B 방향)

나중에 등장한 뷰 컨트롤러가 자신을 호출해 준 뷰 컨트롤러에게 어떤 값을 돌려 보내려면 어떻게 해야 할까요?

프로퍼티를 사용하여 나중에 등장할 뷰 컨트롤러로 데이터를 전달하는 방법은 이미 살펴보았지만 확실히 순방향(straightforward)으로의 전달입니다. 그렇다면 거꾸로 자신을 호출해 준 뷰 컨트롤러로 어떤 값을 전달해 주려면 어떻게 해야 할까요?

이러한 상황이 필요한 시나리오입니다.

1) 사용자가 뷰 컨트롤러 A를 떠나서 뷰 컨트롤러 B로 향합니다.

2) 나중에 나타난 뷰 컨트롤러에서 사용자는 데이터를 가지고 상호작용하고 있고 여러분은 그 데이터를 뷰 컨트롤러 A로 되돌려 보내고 싶습니다.

다시 말하자면 A에서 B로 데이터를 전달하는 것이 아니라 B에서 A로 데이터를 전달하는 것입니다.

...

데이터를 돌려보내는 가장 쉬운 방법은 뷰 컨트롤러 B에다가 뷰 컨트롤러 A에 대한 참조를 생성하는 것이고, 뷰 컨트롤러 B에서 그 A의 함수(메소드)를 호출하는 것입니다. 그렇다면 나중에 나타나는 뷰 컨트롤러 B의 소스 코드는 다음과 같을 것입니다.

// Swift
class SecondaryViewController: UIViewController {
    var mainViewController: MainViewController!
    
    @IBAction func onButtonTap() {
        mainViewController.onUserAction(data: "Hello, World!")
    }
}
// Objective-C
@interface SecondaryViewController: UIViewController {
    @property (atomic, retain) MainViewController * mainViewController;
}
@implement SecondaryViewController {
    @synthesize mainViewController;
    
    -(IBAction) onButtonTap {
        [[self mainViewController] onUserActionWithData: @"Hello, World!"];
    }
}

 

그리고 MainViewController는 다음과 같은 메소드(함수)가 추가될 것입니다.

// Swift
func onUserAction(data: String) {
    print("Data received: \"\(data)\"")
    // ...
}
// Objective-C
-(void) onUserActionWithData:(NSString *)data {
    NSLog(@"Data received: \"%@\"", data);
    // ...
}

 

그리고 이전 장에서 앞서 보였던 예제와 같이 나중에 등장할 뷰 컨트롤러가 생성된 후 내비게이션 스택에 푸쉬(push)될 것입니다. 이 때 MainViewControllerSecondaryViewController 사이의 연결은 다음과 같이 만들어집니다.

// Swift
class MainViewController: UIViewController {
    // ...
    let secondaryViewController = SecondaryViewController(nibName: "SecondaryViewController", bundle: nil)
    secondaryViewController.mainViewController = self
// Objective-C
@implement MainViewController {
    // ...
    SecondaryViewController * secondaryViewController
        = [[SecondaryViewController alloc] init];
    [secondaryViewController setMainViewController: self];
}

 

위의 예제에서 보듯 mainViewController라는 프로퍼티에 self가 대입됩니다. 이로써 나중에 등장할 뷰 컨트롤러는 자신을 호출해 준 뷰 컨트롤러의 존재를 알고 있습니다. 때문에 나중에 등장할 뷰 컨트롤러는 onUserAction(data:)와 같이 해당 뷰 컨트롤러의 메소드(함수)들을 호출할 수 있습니다.

이것은 전부입니다. 하지만... 데이터를 전달하기 위한 이와 같은 접근법은 그리 이상적인 방안이 아닙니다. 몇 가지 문제점을 꼽자면 다음과 같습니다.

(1) MainViewControllerSecondaryViewController는 너무 강하게 결합되어 있습니다. 여러분은 소프트웨어를 설계할 때 강한 결합성을 피하라는 말을 들어 보았을 것이고 그렇게 하기를 원할 것입니다. 왜냐하면 강한 결합성은 여러분의 코드에 대한 모듈화 정도를 떨어뜨리기 때문입니다. 이와 같이 작성된 두 개의 클래스는 너무 과도하게 얽혀있으면서 서로의 메소드(함수)에 의존하고 있는데 이는 종종 “스파게티 코드”로 이어지게 됩니다.

(2) 위의 예제 코드에서는 객체에 대한 retain이 순환합니다. 나중에 등장한 뷰 컨트롤러 객체는 먼저 등장했던 뷰 컨트롤러가 제거될 때까지는 메모리에서 해제되지 않습니다. 하지만 먼저 등장했던 뷰 컨트롤러도 나중에 등장한 뷰 컨트롤러가 제거되기 전까지는 메모리에서 해제될 수 없습니다. 이에 대한 해법은 weak 프로퍼티 키워드를 사용하는 것입니다.

(3) 만일 MainViewControllerSecondaryViewController의 개발자가 서로 다르다면 두 클래스에 대한 작업이 쉽게 수행될 수 없습니다. 왜냐하면 두 개의 뷰 컨트롤러는 서로의 뷰 컨트롤러가 어떻게 작동되는지를 이해해야 할 필요가 있기 때문입니다. 이 경우 두 개발자 사이에는 관심사의 분리(separation of concerns)가 이뤄질 수 없습니다.

이제 여러분은 클래스, 인스턴스 및 메소드(함수)들을 위와 같이 직접적으로 참조하는 것을 피하고 싶어질 것입니다. 이와 같이 코딩해 둔다면 나중에 유지관리하는 데 악몽이 될 수도 있습니다. 이러한 방식은 종종 스파게티 코드로 이어지면서 여러분이 코드의 어느 일부분을 수정할 경우 이와 관련된 다른 부분들도 연쇄적으로 충돌이 발생하게 됩니다. 그렇다면 어떻게 하는 것이 좋을까요? 그건 델리게이션(delegation)을 사용하는 것입니다.

간단한 요령 뷰 컨트롤러 사이에 소속될 변수가 여러 개 있다면, 프로퍼티를 이에 맞춰 여러 개 만들려 하지 마세요. 그 대신 필요한 모든 데이터들을 담는 구조체(struct) 또는 클래스(class)를 선언 및 정의하고 이 형식으로 표현되는 하나의 프로퍼티를 만들어서 뷰 컨트롤러 사이에 전달을 수행하면 됩니다.

카테고리 “Language/Objective-C & Swift”
more...

“Language/Objective-C & Swift” (21건)