9월 4일에 넷스루 오재훈님께서 TDD 를 주제로 2시간동안 워크샵을 진행하신다고 해서 참석했다. 그동안 TDD를 해봐야지 하고 책도 봤지만 막상 실무에 적용 하려면 막막했었다.

과연 어떤 힌트를 얻을 수 있을까 기대를 품고 갔는데, 다녀오길 잘 했다는 생각이 든다.

가장 막막했던 첫 시작을 어떻게 해야 할까?

시작점 : 요구사항

TDD는 “1 실패 케이스 작성” -> “2 성공하는 케이스 작성” -> “2의 리팩토링” ->” 1의 리팩토링 “-> “1 실패 케이스 작성”의 반복이다.

그러면 “1 실패 케이스”는 무엇인가? 이것이 바로 요구 사항이다. 처음에는 하드 코딩으로 시작해도 좋다. 그러면 리팩토링 거리가 생기겠지. 그럼 리팩토링 하면 된다. 그리고 계속 반복 개선해나가면 되고.

테스트 드리븐

처음에는 아마 테스트 코드만 작성 했을 수도 있다. 그러나 우리가 원하는건 제품 코드이다. 현재 테스트 코드에 있지만 제품 코드에 사용되어야 할 부분을 분리하는 리팩토링 작업을 한다. 제품 코드가 테스트 코드에서 나왔기 때문에 테스트 드리븐인 것이다.

의존성 낮추기

제품도 테스트도 계속 발전하고 크기가 커져 복잡도가 높아진다. 그렇다고 해서 성공 테스트 케이스가 실패 테스트 케이스로 변하면 곤란한 상황에 처했다는 신호이다.

이것을 위해 제품코드와 테스트 코드의 연결 고리는 인터페이스여야 하며, 필요하다면 제품코드와 테스트 코드 사이의 헬퍼 클래스를 작성 해야 할 수도 있다.

코드 정제하기

처음 솔루션을 개발 할 때는 하드 코딩 처럼 특수한 케이스에 맞는 것을 작성하고 그것을 케이스에 맞춰 점차 일반화 시킨다. 하드 코딩에서 일반화된 코드로 가는 방법은 중복된 코드를 제거하는 것이다.

TDD의 끝

더이상 실패 케이스를 작성 할 수 없으면 모두 끝이 나게 된다.

나쁜 테스트 코드

if, for 문이 많이 들어간 테스트 코드는 안좋을 확률이 높다.

개인적으로 스위프트가 문법 구조가 맛깔나서 매력적인 언어임에는 틀림 없지만 쉬운 언어는 절대 아니라고 생각한다. 그 이유는 여러가지 지켜야 할 사항이 많기 때문이다. 어렵다는건 두가지 측면이 있는데, 논리적으로 복잡해서 어려운 것이 있고 양이 많아서 어려운 것이 있다.

스위프트는 특정한 부분에서 외워야 할 것이 많은데, 특히 생성자에 관한 규칙이 그러하다.

핵심 키워드 : Two-Phase Initialization, Designated Initializer, Convenience Initializer, Inheritance, Safety Check

상속

클래스는 다른 타입과 달리 상속을 할 수 있다는 것이 가장 큰 차이점이다. 서브 클래스는 슈퍼 클래스의 모든 메소드, 프로퍼티, 특징을 이어 받는다. 또한, 프로퍼티와 메소드의 추가, 변경이 가능하다.

기반 클래스(Base class = Root class)

다른 특정한 클래스를 상속하지 않은 클래스를 기반 클래스라고 한다. Objective-C 는 NSObject 와 NSProxy 라는 2개의 기반 클래스를 이용하지만, 스위프트는 전역적인 기반 클래스가 없다. 따라서 아무것도 상속하지 않고 클래스를 정의하면 그것이 바로 기반 클래스가 되는 것이다. 하지만 실질적으로 NSObject 를 상속하는게 foundation framework를 이용 할 수 있으니 상속하는게 일반적이다.

Cocoa Foundation Framework 의 일반적인 기능을 이용하려면 NSObject 를 상속하면 된다.

클래스 상속

클래스 상속은 정의한 클래스 이름 뒤에 “ : 수퍼 클래스”를 적으면 된다.

[code language=”swift”]

class Shape {
func draw() { … }
}
class CircleShape : Shape {
var radius:Float

}

[/code]

생성(Initialization)

생성은 클래스, 구조체, 열거형은 인스턴스 준비하는 과정이다. 이 메소드를 생성자(Initializer)라고 한다.

생성시 꼭 수행되야 하는 일

  • 모든 저장형 프로퍼티에 값이 정해져야 한다.

물론 옵셔널은 예외다. 하지만, 옵셔널이 아니면 강제사항이다. 생성자에서 값이 정해지던지, 언언부에서 기본값이 정해 지던지 해야한다.

  1. 기본 값 정의
* ex)

struct circle {
var radius = 10.0
}

  1. 생성자
* init() 메소드 정의를 통해 수행


* ex)

struc circle {
var radius
init () {
radius = 10.0
}
}

TIP) 기본 값 를 사용하는 것과, 생성자에서 값을 지정하는 것의 차이는 무엇일까? 어떤 방식을 왜 선택해야 할까?

값 타입(Value Type =Structure, Enumeration) 생성자 규칙

만약 값 타입에 새로운 생성자(Custom Initializer)를 만들면, 기본 생성자는 더 이상 사용 할 수 없게 된다.

또한, self.init 은 생성자 안에서만 호출 할 수 있다.

팁 : 만약 값 타입에서 커스텀 생성자와 기본 생성자를 같이 쓰고 싶다면, 자신의 선언에서 사용하지 말고 Extentions 를 활용하는게 좋다.

클래스 상속

이 부분이 현재 글에서 가장 중요한 부분이라 생각한다. 스위프트는 생성자가 논리적으로 무결하게 동작하도록 하기 위해 몇 가지 규칙을 만들었다.

지정 생성자(Designated Initializer)와 편의 생성자(Convenience Initializer)

지정 생성자는 생성자 중에서 가장 근원적인 생성자이다(꼭 한번은 호출된다). 클래스의 모든 프로퍼티가 초기화 되며, 정확한 생성자 프로세스를 수행하기 위해 가장 적합한 수퍼 클래스의 생성자를 호출하기도 한다.

여러개의 지정 생성자가 있을 수도 있지만, 일반적으로 1개만 만드는 것이 일반적이다.  지정 생성자는 상속 상하위 관계에서 초기화 작업의 종착지이기도 하다.(funnel point )

편의 생성자는 인스턴스를 생성 할 때 몇가지 편리한 방식을 제공하기 위한 것이다. 편의 생성자는 일반적으로 파라미터가 없거나 적고( init() ), 지정 생성자는 생성자 파라미터에 프로퍼티를 상세하게 지정하는 경우가 일반적이다. ( init(_ name:String, ……… ) )

지정 생성자

[code language=”swift”]

init(parameters) {
statements
}

[/code]

편의 생성자

[code language=”swift”]

convenience init(parameters) {
statements
}

[/code]

위 문법만 보면 init 앞에 convenience 한정자를 붙인 것 외에 다른 것을 못느꼈을 것이다.

생성자 규칙

  1. 지정 생성자는 직전 수퍼 클래스의 지정 생성자를 호출 해야 한다.

  2. 편의 생성자는 같은 클래스의 다른 생성자를 반드시 호출 해야 한다.

  3. 편의 생성자는 최종적으로 지정 생성자를 호출 해야 한다.

다시 말하면

  1. 지정 생성자는 하위 클래스에서 상위 클래스를 호출하며 (super.init 호출)

  2. 편의 생성자는 ‘같은 클래스’ 내에서 호출이 이루어 진다. (self.init 호출)

도식화하면 아래와 같다.

출처 : https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html#//apple_ref/doc/uid/TP40014097-CH18-ID203

생성자 2단계

인스턴스가 생성 될 때는 2 단계를 거친다.

  1. 현재 클래스의 모든 저장형 프로퍼티 초기화(선언부에서 초기값 지정)

  2. 1번이 끝난 후, 필요한 작업을 수행(customize)하고 인스턴스를 사용 할 수 있게 한다.

※ Objective-C 와 다른점은 Objective-C  는 기본적으로 0 이나 nil 로 초기화 하지만, 스위프트는 0이나 nil 이 아닌 적절한 초기화가 이루어 지도록 한다.

생성자 규칙 체크 (Safety Check)

컴파일러는 위 규칙을 정확히 적용 했는지 알려준다. 아래 규칙을 위반하면 컴파일러 에러가 발생한다. 이 규칙들을 살펴보면 한 프로퍼티가 최종적으로 내가 의도한 값이 아닌 다른 값을 가지게 되는 것을 방지하기 위함이다.

  1. 수퍼클래스의 지정 생성자를 호출하기 전에(super.init) 해당 클래스의 모든 프로퍼티가 초기화 되어야 한다.
    = 지정 생성자에서 해당 클래스의 모든 프로퍼티가 초기화 되어야 한다.

  2. 수퍼 클래스에서 상속받은 프로퍼티의 값을 서브 클래스(의 지정 생성자)에서 변경하기 전에, 이미 수퍼 클래스의 지정 생성자가 호출 되었어야 한다. 왜냐하면 서브 클래스의 지정 생성자에서 상속받은 프로퍼티의 값을 변경한 후 1번 규칙에 따라 수퍼 클래스의 생성자를 호출하면 서브 클래스에서 호출한 부분이 덮어 씌여질 수 있기 때문이다.
    올바른 순서 : super.init() …. -> 상속받은 프로퍼티 수정
    잘못된 순서 : 상속받은 프로퍼티 수정 -> super.init()

  3. 편의 생성자의 가장 처음 동작은, 다른 생성자를 호출하는 것이다.

  4. 생성자 2단계 중 1단계가 지나기 전에는 다음과 같은 일을 하지 못한다. (그 후에 가능하다는 이야기)

1. 인스턴스의 다른 메소드 호출


2. 인스턴스의 다른 프로퍼티 읽기


3. self 이용하기

이 부분이 어찌나 중요한지 또 설명이 있다.

Phase 1

  • 지정 생성자나 편의 생성자가 클래스에서 호출 됨

  • 새로운 인스턴스를 위한 메모리가 할당 된다. 아직 초기화는 안된 상태.

  • 지정 생성자는 클래스에서 선언된 모든 저장형 프로퍼티가 값을 갖도록 한다.

  • 지정 생성자는 수퍼 클래스에 대해서도 바로 위의 동작을 수행하게 한다.  즉, 수퍼 클래스에서 정의한 저장형 프로퍼티도 초기 값을 갖게 한다.

  • 이와 같은 동작이 상속 연결 고리에 따라 쭉 이어서 발생하게 한다.

  • 최종적으로 가장 최상의 수퍼 클래스의 초기화가 끝나면 모든 저장형 프로퍼티가 초기값을 갖게 된다. 이로써 1단계가 완결된다.

Phase 2

  • 1단계가 완료되면 드디어 다른 작업을 생성자에서 할 수 있다. 프로퍼티의 값을 바꿀 수 있다. self 를 이용 할 수 있고, 다른 메소드도 호출 가능해진다.

  • 최종적으로 편의 생성자에서 self 를 이용 할 수 있게 되고, 필요한 작업 또한 수행 할 수 있다.

요약하면 수퍼 클래스에서 서브 클래스 방향으로 지정 생성자가 실행되어야 하고, 그 다음에 편의 생성자에서 자신만의 기능을 수행 할 수 있게 된다.

생성자 상속과 오버라이드

Objective-C 와는 다르게 스위프트는 자동으로 생성자를 상속하지 않는다. 앞서 설명했듯이 스위프트에서는 상속 고리에서 발생 할 수 있는 의도치 않은 초기값이 정해지는 것을 방지하기 위해서이다.

하지만, 자동 상속이 되는 규칙을 지정해 두었다. 특히, Objective-C 와 연동해서 사용 할 때 이규칙을 모르면 혼동이 올 수 있으니 꼭 숙지해야 한다.

Swift 에서 프로퍼티, 메소드, subscript 를 서브 클래스에서 재정의 할 때는 꼭 override 한정자로 수식해야 한다. 그러면 위에 적은 규칙들에 의해 정확히 구현되었는지 컴파일러가 체크해준다.

또 하나의 규칙은 지정 생성자를 재정의 할 때는 꼭 override 키워드를 붙여야 한다. 서브 클래스에서 지정 생성자를 편의 생성자로 변경 할 수도 있지만 이때도 꼭 override 를 붙여야 한다.

만약 수퍼 클래스의 편의 생성자를 서브 클래스에서 지정 생성자로 오버라이드 하면, 수퍼 클래스의 편의 생성자는 서브 클래스에서 바로 호출 할 수 없을 것이다. 왜냐면 규칙에 의해 편의 생성자는 해당 클래스의 생성자 내부에서만 호출되어야 한다. 그러므로 편의 생성자와 지정 생성자가 서브 클래스에서 바뀌게 되면 위 규칙에 맞춰서 적절하게 모든 생성자들을 재구성 해주어야 한다.

생성자 자동 상속 규칙

위에 언급했듯이 생성자는 자동 상속되지 않는다. 하지만 특정 조건을 만족하면 자동 상속한다. (이 부분이 Objective-C 와 다른 부분이다. Objective-C 는 모두 자동 상속 한다.)

자동 상속 규칙 1

  • 서브 클래스에서 지정 생성자를 새로이 만들지 않았다면, 수퍼 클래스의 모든 지정 생성자가 상속 된다.

자동 상속 규칙 2

  • 서브 클래스에서 수퍼 클래스의 모든 지정 생성자를 상속했다면 서브 클래스는 수퍼 클래스의 모든 편의 생성자를 상속하게 된다. 이것은 지정 생성자는 자동 상속 규칙 1에 의해 상속 되는 경우와, 직접 모든 지정 생성자를 구현 했을 경우 둘 다 포함한다.

이 규칙은 서브 클래스에 편의 생성자를 추가 했을 때도 유효하다.

2번 규칙에 의해서 수퍼 클래스의 지정 생성자를 서브 클래스에서는 편의 생성자로 구현 하는 것이 가능하다.

실패 가능한 생성자

여러 사정에 의해, 가령 파라미터가 잘 못 됐다던지 리소스가 없다던지 어딘가에 문제가 발생 해서 인스턴스를 생성 할 수 없는 경우가 있을 것이다. 이 때 유용하게 쓰이는 것이 실패 가능한 생성자이다. 클래스, 구조체, 열거형 모두 적용 가능하다.

  1. init 대신 init? 을 사용한다.

  2. 실패 시 nil 을 반환한다.

실패 시 “ return nil “ 을 한다고 해서, 성공 했을 때 “return” 을 하면 안된다. Swift 생성자는 인스턴스를 반환하는 형식이 아니기 때문이다.

optional 변수와 같이 사용하는게 좋을 것이다.

실패 가능한 생성자는 생성 프로세스 전체에 영향을 주게 된다. 그래서 생성자 체인의 한 부분에서 실패가 일어나면 결과적으로 실패로 끝나게 된다.

실패 가능한 생성자는 실패 없는 생성자를 대리 할 수 있다. 실패하지 않는 기존 초기화 프로세스에 잠재적 장애 상태를 추가하기 위해서 이 방법을 이용 할 수 있다.

실패 가능한 생성자 오버라이드

실패 가능한 생성자를 서브 클래스에서 실패 없는 생성자로 오버라이드 가능하다. 반대는 안된다.

다른 규칙으로는, 실패 없는 생성자에서 실패 가능한 생성자를 사용하려면 ‘Force unwrapping’ 을 해야 한다. ( super.init()! )

이 때, Force Unwrapping 에 실패하면 런타임 오류가 발생한다.

init!

실패 가능한 생성자인데, implicitly unwrapped optional 인스턴스를 만드는 생성자이다.

  • init! -> init? 호출 가능

  • init? -> init! 호출 가능

  • init -> init! 호출 가능. 다만 실패하면 런타임 에러 발생

필수 생성자 (Required Initializer)

required 키워드를 사용하면 서브 클래스에서 해당 생성자를 꼭 구현해야 한다. 또한 서브 클래스의 생성자마다 다시 required 한정자를 적어야 한다.

필수 지정 생성자를 오버라이드 할 때는 override 키워드를 붙이지 않는다.

예외적으로, 자동 상속 조건을 만족한다면 필수 생성자를 꼭 구현해야 할 필요는 없다.

클로저와 함수를 이용한 기본값 지정

아래와 같은 방식으로 기본값을 클로저나 글로벌 함수를 이용해 생성 할 수 있다.

class SomeClass {
let someProperty: SomeType = {
return someValue
}()

let otherProperty:OtherType = createOtherFunction()
}

이 때 유의할 점은 이것은 초기화가 끝난 상태가 아니다. 즉 self 를 호출해도 안되고, 다른 멤버를 이용하는 것도 안된다.

규칙 다시 보기

생성자 규칙

  1. 지정 생성자는 직전 수퍼 클래스의 지정 생성자를 호출 해야 한다.

  2. 편의 생성자는 같은 클래스의 다른 생성자를 반드시 호출 해야 한다.

  3. 편의 생성자는 최종적으로 지정 생성자를 호출 해야 한다.

생성자 자동 상속 규칙

  1. 자동 상속 규칙 1
* 서브 클래스에서 지정 생성자를 새로이 만들지 않았다면, 수퍼 클래스의 모든 지정 생성자가 상속 된다.
  1. 자동 상속 규칙 2
* 서브 클래스에서 수퍼 클래스의 모든 지정 생성자를 상속했다면 서브 클래스는 수퍼 클래스의 모든 편의 생성자를 상속하게 된다. 이것은 지정 생성자는 자동 상속 규칙 1에 의해 상속 되는 경우와, 직접 모든 지정 생성자를 구현 했을 경우 둘 다 포함한다.

요즘 박준표님의 도움으로 작은 실습을 하고 있다. 최종 목표는 실습한 것을 바탕으로 교육 프로그램을 만들어 보는 것이다.

오늘은 트렐로에 할 일 목록을 각자 작성했다. 각자 작성한것을 살펴보니 나는 화면과 그 화면에 어떤 컴포넌트가 들어갈지 적었다. 준표님은 ‘누가 어떤일을 할 수 있다(왜)’는 식으로 적었다.

내가 작성한 방식은 정밀하게 적지 않으면 마음이 답답해지는 단점이 있다. 또한, 어디까지 자세히 적을 것인가 하는 고민도 생긴다. 빠르게 실행하지 못하고 머리속에서 복잡도만 높아지는 단점이 있다.

반면 준표님처럼 서술하면 행동이 바로 떠오르고 그에 맞춰 상상 할 수 있는 만큼만 하면 되니 자유도가 높아진다. 또한 비어있는 만큼 비어있는 채 실행하면 되는 것이다. 빠른 실행 후 변경하는게 더 속도가 빠르니까.

처음  명세를 작성 할 때, 사용자 스토리 방식이 유용하다는 것을 느꼈다.

Objective-C 에서 XCode 의 메소드 탐색 기능을 활용 할 때 #pragma mark 를 사용했다.

#pragma mark - Start

라고 했을 때 아래 처럼 나온다.

AppDelegate_m_—_Edited.jpg

Swift 에서는 보통 2가지 방식을 이용하는 것 같다. extension 을 쓰는 것과, 주석을 이용하는 것이다.

1. Extension

Extension 을 이용해서 구분을 하는 방식이 있다.

[code language=”swift”]

class PTSummaryViewController: UIViewController{
……….
}

extension PTSummaryViewController : UITableViewDataSource{
………
}

[/code]

Item-0_및_Item-1_및_Item-0.jpg

이렇게 하면 위와 같이 Extension 을 기준으로 탐색을 할 수 있다. 그러나 그것에 관한 설명이 없기 때문에 #pragma mark 처럼 강력 하지는 않다.

2. 주석 이용하기

3가지 주석을 이용 할 수 있다. XCode 버전 몇 부터 지원했던건지 잘 모르겠지만, 여튼 Objective-C 사용 할 적에는 #pragma mark 를 썼으므로 알 필요가 없었고, swift 는 extension 을 많이 이용해서 잘 모르는 사람도 있는 것 같다. 중요한 점은 대문자로 써야 한다는 것과 콜론을 꼭 붙여 써야 한다는 점이다.

  1. MARK:          설명

  2. FIXME:         고쳐야 할 것

  3. TODO:           할 일

    •                      구분선 추가

예제

PTSummaryViewController_swift.jpg

밑줄 친 데로 하면 아래와 같이 나온다. MARK, TODO, FIXME 마다 아이콘이 다름을 볼 수 있다. 예제에서는 모두 - 를 추가하여 구분선이 들어가있고, - 를 빼면 구분선이 없어진다.

PTSummaryViewController_swift.jpg

Extension 방식보다는 XCode 주석을 통해 구분하는 방식이 설명을 적을 수 있어서 더 나아보인다.

출처 : Modernizing Grand Central Dispatch Usage WWDC 2017

GCD 최적화 하기

결론부터 말하자면, GCD Queue (다른 동시성 프로그래밍 기법도 동일한 원리가 적용될 것이다)를 이용 할 때 1)너무 잦은 Context Switching 이 일어나게 프로그래밍 하지 말라는 것이다. 또한 2)GCD 큐의 계층화를 최적화 하라. 이것만 개선해도 1.3배의 속도 증가가 나왔다.

이런 견지에서 애플은 iOS, Mac OSX 양측 모두에서 성능 개선을 계속해 나가고 있고 iOS 11, High Sierra 에서 확연히 성능이 증가했다고 말하고 있다.

1. 병렬 프로그래밍

**병렬(Parallel) 프로그래밍**

밀접히 관련된 연산들이 동시에 실행 되는 것

동시성(Concurrency) 프로그래밍

독립적으로 실행되는 동작들의 조합

아래 슬라이드를 보면 이해가 바로 갈것이다. 이미지에 필터를 처리 하는건 1가지 필터 함수가 있을 테고, 그것을 이미지 전체에 걸쳐 계산하는 것이다. 그런데 만약 코어가 8개라면 이미지를 8의 배수로 등분하여 각각의 코어에 할 당하면 이론상 8배의 속도 향상이 있을 것이다.

706_modernizing_grand_central_dispatch_usage.jpg

706_modernizing_grand_central_dispatch_usage2.jpg

애플에서는 병렬  프로그래밍에 도움을 주는 프레임워크를 제공하고 있습니다.

Accelerate , Metal 2, Core ML , Core Animation 이 그것들이죠.

2. GCD 로 병렬 프로그래밍하기

DispatchQueue.concurrentPerform은 명시적으로 병렬 프로그래밍을 하는 방식입니다.

[code language=”objc”]
<div class=”page” title=”Page 18”><div class=”section”><div class=”layoutArea”><div class=”column”>
DispatchQueue.concurrentPerform(1000) { i in /* iteration i */ } // swift

<div class=”page” title=”Page 19”><div class=”section”><div class=”layoutArea”><div class=”column”>
dispatch_apply(DISPATCH_APPLY_AUTO, 1000, ^(size_t i){ /* iteration i */ }) //Objective-C
</div></div></div></div></div></div></div></div>
[/code]

반복 횟수 최적화

그런데, 어떻게 하면 병렬 프로그래밍의 이득을 얻을 수 있을가요? 다음과 같은 상황을 생각해 봅시다.

이렇게 동작을 3등분 했습니다.

DispatchQueue.concurrentPerform(3) { i in /* iteration i */ }

그럼 아마 이런식으로 배분이 될 것입니다. (코어가 3개라면 말이죠)  그러면 2번째 코어는 놀게 되네요. 효율이 약간 떨어지게 됩니다.

706_modernizing_grand_central_dispatch_usage_pdf_22_245페이지_.jpg

코드를 바꿔서 이터레이션이 11 이 되도록 해봅시다.

DispatchQueue.concurrentPerform(11) { i in /* iteration i */ }

706_modernizing_grand_central_dispatch_usage_pdf_26_245페이지_.jpg

그냥 봐도 좀 더 효율적으로 바뀌었지요. 코어가 쉬고 있지 않으니까요.

그러면 이터레이션 단위를 크게 할 수록(작업 단위를 잘게 쪼갤수록) 더욱 코어 활용도가 높아지겠네요. 맞나요? (아닌가요? 왜 그럴까요?)

동시성 프로그래밍

동시성은 독립된 태스크들의 집합입니다.  UI, Networking, Database 같은 시스템들이 각기 독립적으로 존재하며 실행되지요.

아래 그림은 UI, Database, Networking 컴포넌트가 독립적으로 동작하고 코어에서 이것들을 어떻게 할당하는지 대략적으로 보여준 것입니다. UI는 우선순위가 높으므로 터치 이벤트가 발생 했을 때 데이터베이스가 잠쉬 쉬고 UI가 실행되는 것을 나타내고 있습니다.

706_modernizing_grand_central_dispatch_usage_pdf_38_245페이지_.jpg

※ 시스템 코어가 어떻게 돌아가는지 눈으로 확인하고 싶다면 System trace in depth 세션을 보세요. Instruments 를 통해 아래 그림처럼 볼 수 있습니다.

706_modernizing_grand_central_dispatch_usage_pdf_40_245페이지_.jpg

컨텍스트 스위칭

OS는 어느 순간에 어떤 부분이 실행 될지(어떤 쓰레드가 CPU를 점유 할 지) 선택 할 수 있습니다.  그럼 어느 순간에 새로운 쓰레드가 선택 될 까요?

  • 우선순위가 높은 쓰레드가 CPU를 점유합니다.

  • 현재 작업이 끝났을 때

  • 자원을 획득하길 기다릴 때

  • 비동기 요청이 끝났을 때

이렇게 필요와 우선 순위마다 활성화되는 쓰레드가 바뀜으로써 반응성이 좋아질 수 있다는 것이 동시성 프로그래밍의 힘이죠.

과도한 컨텍스트 스위칭

그러나 과도한 컨텍스트 스위칭이 일어나는 것은  경계해야 합니다. 아래 그림에서 흰색은 컨텍스트 스위칭을 나타내는데 이것도 다 비용입니다. 너무 낮은 컨텍스트 스위칭은 CPU 자원을 소모해버립니다.

706_modernizing_grand_central_dispatch_usage_pdf_49_245페이지_.jpg

분명 컨텍스트 스위칭은 필요한 것이지만 아래와 같은 행위들이 너무 많이 일어나면 효율이 떨어지게 됩니다.

  • 자원의 독점적 접근을 위한 대기 반복

  • 독립적 동작들 사이의 스위칭 반복

  • 쓰레드 사이에 오퍼레이션이 왔다 갔다 하는것이 반복 되는 것

너무 잦은 컨텍스트 스위칭이 발생 하게 하는 것 보다는 자원 점유를 직렬화 하거나 순차적으로 진행되도록 함으로써 컨텍스트 스위칭을 적게 하는게 나을 수 있습니다.

자원 획득 경쟁(Lock Contention)

자원 획득에 관한 정책은 매우 유용합니다. 어떤 자원이 필요한 쓰레드가 해당 자원을 소유하고 있지 못하면 CPU는 그 쓰레드를 실행시킬 필요가 없지요. 불필요하게 CPU 자원을 소모하는 일을 방지해줍니다.

자원 획득에 관한 정책은 Unfair, Fair 두가지가 있습니다.

**Unfair**

Fair

**가능한**** ****타입** os_unfair_lock pthread_mutext_t , NSLock

DispatchQueue.sync

**자원**** ****독점**** ****재획득**** Contented lock re-acquisition** 독점 훔치기 가능 다음 대기자에 컨텍스트 스위칭이 발생
**대기자**** ****기아**** ****상태****(****자원**** ****획득**** ****방지****) Subject to waiter starvation** 원함 원치 않음
일반적으로 unfair 타입이 객체, 전역 상태, 프로퍼티에 적당할겁니다.

잠금 소유권(Lock Ownership)

잠금 소유권은 CPU가 어떤 쓰레드를 선택해야 할지 도움을 줍니다. 높은 순위의 쓰레드가 대기중인 경우나, 낮은 순위의 쓰레드가 오너쉽을 가지고 있어서 발생하는 문제점을 해결해줍니다.

잠금에 대해 어떤 정책을 사용할 것인지에 따라 아래와 같이 결정하는게 도움이 됩니다.

706_modernizing_grand_central_dispatch_usage_pdf_76_245페이지_.jpg

3. GCD 로 동시성 프로그래밍하기

그동안 GCD 세션이 WWDC 에서 다뤄졌습니다. 더 관심이 있다면 아래 아티클을 찾아보세요.

  • Simplifying iPhone App Development with Grand Central Dispatch, 2010

  • Asynchronous Design Patterns with Blocks, GCD, and XPC, 2012

  • Power, Performance, and Diagnostics: What’s new in GCD and XPC, 2014

  • Building Responsive and Efficient Apps with GCD, 2015

  • Concurrent Programming with GCD in Swift 3, 2016

letmecompile.com : GCD 튜토리얼

직렬 디스패치 큐(Serial Dispatch Queue)

  • 상호 배제(Mutual Exclusion)

  • FIFO 순서

  • 원자성을 보존하며 큐에 삽입(Concurrent atomic enqueue)

  • 큐에 삽입 할 때와 마찬가지로, 원자적으로 큐에서 제거 됨(Single dequeuer)

큐 계층도 구성하기

S : 자원, Q : 큐 , EQ : 상호 배제 큐(Mutual Exclusive Queue)

706_modernizing_grand_central_dispatch_usage_pdf_99_245페이지_.jpg

이런식으로 큐를 구성 할 수 있습니다. 그러면 큐 Q1, Q2를 EQ에서 총체적으로 관리하게 됩니다. 누가 먼저 실행 될지 EQ 에서 선택하는 것이지요. 직렬 큐라면 특정 순서를 만들어서 순서대로 실행될겁니다.

QOS (Quality Of Service)

그래서 이런 식으로 시스템 큐가 구성되어 있습니다. 상위에 있는 것이 우선 순위가 높습니다.

Power, Performance and Diagnostics: What’s new in GCD and XPC, 2014 에서 다뤄졌던 내용도 있으니 먼저 보고 오는게 좋겠습니다.

706_modernizing_grand_central_dispatch_usage_pdf_107_245페이지_.jpg

비디오 21분 50초쯤 부터 나오는 내용인데, 애플 OS의 QoS는 이렇게 구성했고, 큐 우선순위를 통해서 이벤트 발생 순서에  상관 없이 UI 처럼 높은 우선순위의 동작부터 처리하게 되어 있다고 합니다.

※ 좋다고 남발하는 것 피하기

  • 반복적인 자원에 독점적 접근을 위해 대기

  • 반복적인 독립된 동작 스위칭

  • 반복적인 쓰레드간의 오퍼레이션 이동

GCD  잘 구성하기

네트워크 연결을 한다고 가정합시다. 아마 한번에 여러 네트워크 연결이 생길 수 있을 것이고, 한 커넥션마다 하나의 큐를 생성해서 처리한다고 칩시다. 그러면 아래의 그림과 같은 상태가 될 것입니다. 하나의 큐마다 하나의 쓰레드가 생성 됩니다.

706_modernizing_grand_central_dispatch_usage_pdf_131_245페이지_.jpg

그러면 앞에서 얘기한데로 컨텍스트 스위칭이 많이 발생 할 것입니다. 좀 더 개선해보죠.

706_modernizing_grand_central_dispatch_usage_pdf_135_245페이지_.jpg

이렇게 단일한 상호 배제 큐로 바꾸어  봤습니다. 그러면 쓰레드가 한개만 생성되어 컨텍스트 스위칭이 줄어들게 됩니다. 오버헤드가 사라지는 것이죠.

실제로 애플에서 이런 방식을 통해서 처음에 언급했던 1.3배의 성능 향상을 이끌어 냈습니다.

경계 없는 동시성을 피하기

= 반복적인 독립된 오퍼레이션간의 스위칭

만약 전역 큐에 많은 아이템들이 할당 된다면

  • 만약 일감들이 블록되면, 더 많은 쓰레드가 생성 될 것이고

  • 이건 쓰레드 폭발로 이어질 수 있다 (Thread Explosion)

더 자세한 내용은 다음을 참조
Building Responsive and Efficient Apps with GCD

좋은 동시성 프로그래밍 전략

  • 고정된 갯수의 직렬 큐 계층 만들기

  • 각 계층간에는 발생하는 일은 큰 덩어리로 만들어지게 하라

  • 한 계층 내의 일은 크기가 작으면 좋다

4. 단일한 큐 구분자Unified queue identity

Mac OSX Sierra, iOS 10 이전에는 아래 그림과 같이 큐가 동작했습니다. 어떤 동작을 하고 있을 때 높은 순위의 작업이 발생하면 다른 쓰레드가 하나 생겨났죠. S1 -> S2 그리고 큐 동작은 그림처럼 진행 됐을 겁니다. 그런데, 이게 대체 무슨 이점이 있을까요? 컨텍스트 스위칭으로 비용만 들 뿐이었습니다.

706_modernizing_grand_central_dispatch_usage_pdf_191_245페이지_.jpg

그리고 High Sierra, iOS 11 에서는 이렇게 바꾸었습니다. (Unified queue)

706_modernizing_grand_central_dispatch_usage_pdf_195_245페이지_.jpg

EQ (Exculusive Queue) 가 CPU 를 점유 하는 것이라는 것을 알게 됐으며, 어떤 이벤트가 발생해서 어떤 일이 일어나던지 신경쓰지 않게 되었습니다. 그냥 큐를 실행하기만 하면 되니까요.

그럼 어떻게 두번째 이벤트가 방해 없이 발생 할 수 있을건지 의문이 생길 겁니다.

706_modernizing_grand_central_dispatch_usage_pdf_198_245페이지_.jpg그림에서 보듯이 새로운 이벤트(S2)가 생기면 다음 이벤트가 E2에 있다는 것만 표시해줍니다. 그러면 순차적으로 실해되는 것이죠.

또한 이 방식을 이용함으로써 런타임 도구를 통해서 최적화에 대한 힌트를 얻을 수 있게 되었습니다.

5. 최신 코드로 교체 (Modernizing Existing Code)

  1. 활성화 후 디스패치 오브젝트를 변경금지

  2. 큐 계층을 보호한다

1. 활성화 후 디스패치 오브젝트를 변경 금지

오브젝트의 프로퍼티는 활성화 되기 전에 지정한다. 1. 소스 핸들러, 2. 타겟 큐

let mySource = DispatchSource.makeReadSource(fileDescriptor: fd, queue: myQueue) mySource.setEventHandler(qos: .userInteractive) { ... }

mySource.setCancelHandler { close(fd) }
mySource.activate()

mySource.setTarget(queue: otherQueue) <- 활성 화 후 큐를 바꾸면 안된다.

만약 이것을 어긴다면 미래에 예측하고 있는 것들이 바뀌고, 최적화 했던 것들, 우선순위 뒤바뀜 정정 기능이 잘 못 될 것이며 다른 문제들도 야기 할 수 있다.

2. 큐 계층 보호

큐 계층이 변경 됐을 때 우선순위와 소유권 스냅샷이 더이상 유효하지 않을 수 있다.

  • 우선순위 뒤바뀜 정정의 파괴

  • Direct handoff 최적화 파괴

  • 이벤트 전달 최적화 파괴

때문에 큐 계층이 변경되는 것을 지양해야 하는데, 만약 여러 팀끼리 협업하거나 다른 회사 제품을 쓰거나 하면 이런 것을 지키기 힘들 수 있다. 이 때 작년에 소개된 “static queue hierarchy” 기법을 이용하여 계층을 보호 할 수 있다. 단, 이것은 Objective-C 에 해당하는 것이며 Swift 는 이미 적용되고 있는 부분이다.

기존코드

Q1 = dispatch_queue_create(“Q1”, DISPATCH_QUEUE_SERIAL)
dispatch_set_target_queue(Q1, EQ)

 새로운 코드 Q1 = dispatch_queue_create_with_target("Q1", DISPATCH_QUEUE_SERIAL, EQ)

45분 부터는 여러 기법을 이용해서 문제점을 찾고 최적화 하는 방법에 대해 소개하고 있다. XCode 9 이 정식 출시 되면 관련된 Instuments 도구도 같이 나올 것 같다.

요약

  • 모든 코어가 ‘잘 활용’되도록 하자

  • 작업 크기를 적당히 하자

  • 동시성 프로그래밍에 적절한 코드 전략(granularity)을 선택하자(계층, 큐의 수)

  • 최신 GCD 코드를 사용하자

  • 문제를 해결하기 위한 툴을 사용하자

소개

Undefined Behavior (이하 비정의 동작)을 이해하고 디버깅하는 방법을 소개하는 세션입니다. C 언어 계열(C/C++,ObjectiveC/C++)에 특히 유용한 세션이 되겠습니다.

바로 전에 관련된 글(WWDC 2017 – Finding Bugs Using XCode Runtime Tools)을 썼으니 먼저 읽고 오시면 더 좋겠습니다. 저번 포스트에도 몇가지 유형을 적었는데 실제로 200개 이상의 유형이 있다고 합니다.

출처 : Understanding Undefined Behavior

참고링크

사실 이런 비정의된 동작들은 언어 차원에서 줄일 수 있는데, 성능과 타협을 본 것입니다. OS와 애플리케이션은 성능을 갖고, 나머지 빚더미(비정의 동작)는 개발자가 떠안게 된거죠.

몇가지 예제가 나오는데  WWDC 2017 – Finding Bugs Using XCode Runtime Tools 의 비정의 동작 부분에 나오는 것과 동일합니다.

1. 비정의 동작

일단 컴파일러에게 비정의 동작은 어떤 의미일까요? 컴파일러는 코드가 비정의 동작을 포함하고 있지 않다고 가정합니다. 왜냐면 비정의 동작이 코드에 있다면 이건 의미적으로(Semantic) 잘 정의된 코드가 아니기 때문입니다. 한마디로 당신을 믿는다는 거죠.

컴파일러는 최적화 작업을 하는데 일단 예제를 보겠습니다.

첫번째는 불필요하게 중복된 널 체크 제거하기 입니다. 아래처럼 코드 최적화가 이루어 집니다.

407_understanding_undefined_behavior_pdf_43_171페이지_.jpg

다음은 쓸모 없는 코드 줄이기입니다. 407_understanding_undefined_behavior_pdf_46_171페이지_.jpg

그러면 결과적으로 이렇게 되겠네요.

407_understanding_undefined_behavior_pdf_48_171페이지_.jpg

그런데 여기서 우리가 파라미터로 NULL 을 보냈다고 칩시다. 그러면 버그가 발생하겠지요. 그런데, 최적화 한것과 최적화 하지 않은 버전은 서로 다른 곳에서 충돌이 발생합니다. 최적화 버전에서는 마지막 줄에서 충돌이 일어나고, 최적화 하지 않은 버전에서는 첫번째 줄에서 충돌이 일어납니다.

이것은 버그 발생 지점과 버그 원인 지점이 굉장히 멀리 떨어져 있을 수 있다는 의미입니다.

여기서 한번 최적화 옵션 순서를 바꿔볼까요.

407_understanding_undefined_behavior_pdf_52_171페이지_.jpg이번에는 Dead Code 제거부터 적용 했습니다. 그리고 불필요한 널포인트 체크를 했지요. 첫번째 최적화와 두번째 최적화는 분명 같은 코드인데 최적화 결과가 다릅니다. 아래와 같이 되지요. 보통은 2번째 컴파일러처럼 동작 할 것이지만 꼭 그런 보장이 없습니다. 그래서 1처럼 동작해서 파멸을 불러 올 수도 있지요.

무제.jpg

여기서 얻을 수 있는 교훈운 우리가 디버그 모드에서 릴리즈 모드로 바꿀 때, 최적화 옵션을 바꿀 때마다 서로 다른 결과를 얻을 수 있다는 사실입니다. 또한 컴파일러가 바뀔 때 다른 결과물이 나올 수 있습니다.

XCode 팀은 새로운 버전마다 더욱 작고 빠른 코드를 생성하는 컴파일러를 만듭니다. 때문에 새로운 컴파일러에서 그 전에 발견하지 못했던 비정의 동작들이 튀어 나올 수 있습니다. 어떤 옵션에서는 잘 되던것이 다른 옵션에선 버그가 될 수 있습니다. 심지어 어떤 버그는 해당 버그와 수천  라인 떨어진 곳에 원인이 있을 수 있고, 몇 시간 전에 실행했던 부분이 버그가 될 수도 있습니다.

비정의 동작과 관련된 이슈
  • 비정의 동작은 예측 불가능하다.
  • 특정 수행 결과가 전체 프로그램에 영향을 줄 수 있다.
  • 버그는 그저 숨어 있을 수도 있다.

2. 비정의 동작의 보안 문제

한때 Heart Bleed 라는 보안 이슈가 크게 세계를 강타했습니다. Open SSL 에서 발견된건데, 한  패킷을 보내면 그 서버의 수KB 의 힙 정보를 받아 볼 수 있었죠. 이렇게 버그는 보안 문제를 일으 킬 수도 있습니다.

  • 버퍼 오버플로

  • 초기화되지 않은 변수 사용

  • 해제된 변수를 사용하기

  • 두번 해제하기

  • 자원 경쟁

실제로 Yosemite 를 릴리즈 하기 한달 전에 굉장히 많은 버그가 발생 했었는데 재현하기 쉽지 않았습니다. CFString 을 C 문자열로 변경하는 경우였는데, 자세한건 비디오에서 확인하시고, 버퍼 오버플로 버그가 발생했죠. 이 때 Address Sanitizer 를 이용해서 문제를 찾을 수 있었습니다. 잠재적인 버그일 경우가 많으므로, 초기부터 Memory Sanitizer 를 이용해서 버그를 발견하는게 좋습니다.

메모리 주소에 대한 비정의된 행위를 찾기 위해서 XCode는 5가지 도구를 제공합니다.

  • Compiler

  • Static Analyzer

  • Address Sanitizer

  • Thread Sanitizer

  • Undefined Behavior Sanitizer

Compiler

일단 컴파일러에서 시작해 봅니다. 컴파일러 ‘Warning’ 을 무시하지 마세요. 프로젝트 세팅에서 Editor->Validate Settings 를 참조하세요. XCode 가 발전하면서 계속해서 경고에 대한 부분도 발전하고 있습니다. 꼭 이용하세요.

Analyzer

컴파일러는 한 부분에 집중하지만 분석기는 코드 전체를 살펴봅니다. 빌드 세팅에 ‘Analyzer During Build’  옵션을 켜면 빌드 할 때마다 같이 실행되니 더욱 좋습니다. CI와 같이 쓸 때는 Deep 모드로 사용해보세요. XCode 9 에서는  새로이 15개의 비정의 동작을 찾아 낼 수 있습니다.

무제.jpg

3. 언어

언어에서 안전한 구조를 제공한다면 그것을 이용하세요. ARC, C++ 스마트 포인터(std::shared_ptr, std::unique_ptr), 범위를 체크하는 컨테이너(NSArray) 같은 것을 말이죠.

그리고 Swift 를 써보세요. 앞서 얘기 했듯이 성능과 안정성에는 트레이드 오프가 있습니다. Swift는 다른 방식의 트레이드 오프를 취했고 기본적으로 더 안전하게 디자인 되었습니다.

Swift.org 에는 버그에 관한 내용들이 올라와 있으므로 참조 하십시요.

4. Swift

Swift 와 C 계열 언어와 아이점을 적어보았습니다. 아래와 같은 방식으로 Swift 는 몇몇 문제점을 극복했습니다.

무제.jpg

1. Optional Type

옵셔널 타입은 널포인터의 값을 참조하는 것을 방지합니다. 옵셔널 타입은 변수가 널일 수도 있다는 의미입니다. 그런데 강제로 값을 취하는 forced unwrap “ ! “ 을 남발하는 것을 권하지 않습니다.

강제 언랩은 다음 경우에만 사용 합니다

  • 해당 변수가 절대 nil 이 아님을 보증 할 때

  • 타입 시스템에서 인코딩이 불가능할 때

  • 예 ) App bundle 에서 이미지를 읽어 올 때

암묵적인 언랩드 옵셔널 (Cake!)

컴파일러는 사용전에 값을 체크하도록 강제하지 않습니다.

Swift 가 C 보다는 이런 면에서 안전(보안)합니다. 왜냐면 이건 정의된 행위이고, nil 을 마주치면 멈추도록 보장되어 있기 때문이죠.

“Now, this type should be used for properties that are guaranteed to have a value. However, they cannot be initialized in the constructor. Some of you might be using it for IB outlets.” 이런 얘길 했는데 정확한 문맥이 안들어오네요.

아무튼 다른 타입의 묵시적인 Unwrapped Optional 이 Objective-C (C) 때문에 생겼습니다.

[code language=”objc”]

  • (nullable NSView *)ancestorSharedWithView:(nonnull NSView *)aView; // Objective-C

func ancestorShared(with view: NSView) -> NSView? // Swift

[/code]

위 예제처럼 Objective-C 의 Annotation 을 이용해서 Swift 와 자연스럽게 섞일 수 있습니다. Swift 와 Objective-C 를 같이 사용한다면 꼭 Annotation 을 사용하세요.

Annotation 을 이용하면 정적 분석기나 Undefined Behavior Sanitizer 의 도움도 받을 수 있습니다.

2. 명확한 초기화(Definite Initialization)

Swift 의 특징 중 하나는 멤버 변수가 초기화 되기 전에는 사용 할 수 없습니다. 꼭 초기화 되도록 하죠. 강제적입니다. 컴파일러가 초기화 안된 것은 모두 경고해 줍니다.

3. 버퍼, 정수 오버플로

이것은 보안의 가장 큰 원인들입니다. 오버 플로는 오직 정수에서만 발생하고 스위프트는 그럴 경우 프로그램을 중단시킵니다. Array, Int 연산 후에 발생합니다. 아마 이런 질문을 하실겁니다. “왜 런타임에서 체크하는게 좋은거죠?” 왜냐면 이게 다른 대안들보다 좋기 때문입니다. 이런 문제가 생기면 프로그램이 중단되죠. 그러면 디버깅 할 것입니다. 이건 아주 큰 보안성을 제공하는 것입니다.(잠재적으로 오버플로를 가지는 것보다는 죽여버리는게 낫다는 의미인듯)

한편 Integer Wrapping Behavior 가 필요하면 이런 연산자를 사용 할 수도 있습니다. &+, &- , &*

4. 그럼 Swift 에서는 비정의 행위가 없나요?

아뇨. 존재 합니다. 그러나 훨씬 적습니다. 그리고 역시 XCode 의 디버깅 툴의 도움을 받을 수 있습니다.

C 와 같이 쓰기 위해 UnsafePointer 같은 클래스들을 사용 할 수도 있습니다. 또한, 자원 경쟁이 있을 수도 있죠.

결론

언어적으로 룰을 잘 지키는 것이 가장 좋습니다. 그런데 어렵죠. 런타임에서 잡는건 좀 더 쉽습니다. 그러나 실행 오버헤드가 있죠. 이 둘 사이에서 균형을 잡고 XCode 도구를 이용해서 버그를 잡는것이 중요합니다.

  • 안전한 방식으로 프로그램을 코딩하세요(Swift 강추)

  • 디버깅 툴을 적극 활용하세요

사고 회로 만들기 에 이어 쓰는 글이 되겠습니다.

지식을 밖에 두고 사용하는 습관에 대한 후회가 되겠습니다.

몇 군데 면접을 보면서 좌절하게 되고, 면접 때 했던 말들을 스스로 곱씹어 보면서 든 생각입니다. 일을 할 때 머릿속에서 길은 그려지는데 그 길에 무엇이 있는지는 하나하나 확인해야 하는 상태인거죠.

일단 면접이나 그런것에 관한 것은 지금 쓰고 싶은 주제는 아니고, 좋은(?) 뇌를 위해서 어떤 훈련을 해야 하나 생각 중입니다.

학창 시절부터 ‘암기 과목’은 질색이었지요. ‘이해, 논리’ 과목이 좋다는 착각을 하고 있었던거죠. 그러나 사실 암기 과목이 따로 있는게 아니고 이해가 우선인 과목이 따로 있는것도 아닌 것이었습니다.

왜냐면 어떤 복잡한 사고를 할 때 머리속에 어떤 지식에 대한 회로가 형성되어 있어서 쭉 사고를 이어가는 것과, 외부에서 지식을 확인하고 단기간 기억하고 이어서 사고하는 것과는 효율 면에서 아주 큰 차이가 날 것이기 때문입니다.

컴퓨터로 따지면 레지스터에서 데이터 참조하는 것과 하드 디스크에서 참조하는 것의 효율 차이가 될 것이기 때문이죠.

또한, 근거 없는 개인적인 생각이긴 하지만 지난 날들을 떠올려보면 사고는 지식의 총량도 중요하지만 ‘속도’도 중요합니다. 사고의 속도가 빠르기 위해선 신경전달물질의 속도가 빠르거나 해당 지식의 ‘뇌 회로’가 만들어져 있어야 하는거죠.

이런 측면에서 봤을 때, 똑똑해지려면 일단 암기를 잘 하는 훈련을 해야하고 그 다음에 그 지식들을 엮어서 논리를 만들어내는 훈련을 해야하는거죠. 개인적으로 암기를 잘 하는 훈련은 흥미가 떨어지는 것이었기 때문에 굉장히 등한시 했습니다. 그래서 결국 이꼴이 됐군요.

그동안 생성된 뇌 회로 때문에 원하는 방향으로 회로를 다시 만들어내는건 어려운 일인 것 같은데 그래도 해내야 늙어서 굶어죽진 않을테니 지금에라도 훈련을 해야겠네요.

원본 주소 https://developer.apple.com/videos/play/wwdc2017/406/

1. 소개

XCode Runtime Tools 를 통해서 버그를 찾는 기법을 소개하는 세션이다.
아래와 같은 내용을 다룰 예정이다.

 extract-22.jpg

2. Main Thread Checker

UI 업데이트처럼 메인 쓰레드에서만 실행되어야 하는 동작들이 있다. 이것을 위반하는지 Main Thread Checker 가 검토해준다.

메인 쓰레드에서 수행 되야 할 UI 함수가 백그라운드 쓰레드에서 실행되면 UI 업데이트가 안되거나, 화면이 깨지거나, 데이터가 오염되거나, 앱이 죽는 경우가 발생한다.

메인 쓰레드에 사용될 함수가 백그라운드에서 호출 될 경우 XCode 9 에서는 extract-17.png 아이콘을 표시해주고, 메인 쓰레드에서 실행하라는 경고문구를 보여준다.

OperationQueue(in Swift)에 관련된 쓰레드가 있다.

예를 들어 비동기식 호출에 클로저를 사용하는 경우가 많다. 이 클로저가 어느 쓰레드에서 실행되야 할지 만들 수 있게 하는 것이 좋다. 다르게 표현하면 파라미터로 OperationQueue 가 들어가야 할지 설계 할 때 숙고해보는 것이 좋다.

AppKit, UIKit, WebKit API 에 사용되며, Swift 와 C 언어도 지원한다.

3. Address Sanitizer

XCode 7.0 부터 소개 된 도구인데, 더 관심이 있다면 Advanced Debugging and the Address Sanitizer 세션을 먼저 보고 오는 것을 추천한다.

Address Sanitizer 에 대해 위에 적은 것을 다시 서술하면 다음과 같다.   밑줄 친건 이번에 새로 추가된 것이다.

  • Heap, Stack, Global 변수의 Out-of-bound

  • 메모리 해제 후 사용 시도

  • 리턴 후 사용 시도

  • 접근 가능 범위 밖에서 사용 시도

  • 잘못된 메모리 해제(중복, 유효하지 않은 변수)

  • 메모리 누수

2년 전에 소개한것 처럼 다음과 같이 해제된 메모리를 다시 사용 하려 할 때 어떤 문제가 생기는지 잡아준다.

extract-29.jpg

Use of out of scope stack memory

[code language=”cpp”]

int *integer_pointer = NULL;
if (is_com_condition_true()) {
int value = cal_value();
integer_pointer = value;
}
*integer_pointer = 42; // Error

[/code]

단, 이건 성능저하가 꽤 심하기 때문에 선택사항으로 남겨뒀다. 필요하다면 옵션에서 켜야 한다.

Address Sanitizer and Swift

스위프트는 여러 제약 사항 때문에 훨씬 안전한 언어긴 하지만, Objective-C 와 섞어 쓰는 경우가 있다. 이 때도 Address Sanitizer 가 유용 하게 쓰일 수 있다.

또한 Swift 에서는 포인터를 위해서 UnsafePointer 와 다른 여러 포인터 클래스를 제공하는데 되도록이면 안쓰는것을 권장한다. (※ MetalKit 같은 포인터를 자주 사용하게 된다.)

Address Sanitizer 는 인스턴스가 어디에서 생성되고, 어디에서 소멸 됐는지 추적해 주기 때문에 많은 도움이 된다.

디버깅 중 변수를 오른 클릭해서 “View Memory of …” 을 실행하면

extract-35.jpg

이처럼 메모리에 관한 내용을 볼 수 있다. 오른쪽에서 검은 색은 아직 살아있는 내용들이고, 회색은 무효한 메모리 (Invalid memory, Poisoned Memory) 라고 한다.extract-37.jpg

그리고 디버깅 콘솔에서 “memory history” 명령을 이용하면 아래와 같이 정보를 얻을 수 있다.

IMG_0013.jpg

4. Thread Sanitizer

Thread Sanitizer 는 멀티 쓰레딩에서 발생 할  수 있는 문제를 해결 할 수 있다. 자원 경쟁 문제를 찾을 수 있다. 쓰레드에 관한 문제는 타이밍에 굉장히 민감하기 때문에 재현이 어렵다. 하지만 Thread Sanitizer 를 이용하면 이 문제를 해결 할 수 있다.

64비트 컴퓨터와 64비트 시뮬레이터에서만 사용 가능하다.

Thread Sanitizer and Static Analysis 도 같이 보는 것이 좋다.

자원 경쟁(Data Races)

  • 변경 가능한 공유 자원의 비동기적 접근

  • 메모리 오염과 충돌을 일으킨다

  • C, Objective-C 심지어 Swift 도 자유로울 순 없다

예제

https___devstreaming-cdn_apple_com_videos_wwdc_2017_406hi7pbvl7ez0j_406_406_finding_bugs_using_xcode_runtime_tools_pdf.jpg

위는 자원 경쟁이 일어난다. 따라서 아래 처럼 DispatchQueue를 이용해서 경쟁을 해결 할 수 있다.

https___devstreaming-cdn_apple_com_videos_wwdc_2017_406hi7pbvl7ez0j_406_406_finding_bugs_using_xcode_runtime_tools_pdf.jpg

GCD 에 대해 더 알고 싶다면 Concurrent Programming With GCD in Swift 3 를 참조하라.

WWDC 2017 에서도 GCD 에 관한 세션이 있으므로 같이 봐도 좋겠다. Modernizing Grand Central Dispatch Usage

Memory Sanitizer 는 Raw data 를 접근 할 때 이용 하는데 반해 동기화 문제는 콜렉션 같은 큰 데이터에서 발생하는 문제이다. Mutable 데이터를 다룰 때 문제가 자주 있어서 XCode 9에 추가되었다.  이런 문제로 곤란을 격었던 사람들에게 매우도움이 될 것이다.

다시 요약 하자면 일단 Scheme에서 Memory Sanitizer 옵션을 켜고, 직접 실행을 해서 문제를 발견한다. 그리고 Mutable 데이터를 수정 할 때 자원 경쟁이 일어나지 않기 위해 DispatchQueue를 하나 생성한다.  (비디오 26분부터 참조)

Swift 에서 접근 경쟁

  • 모든 구조체에서 발생.

  • 구조체의 내용을 변경하는 메소드(mutate method)는 ‘전체 구조체’ 인스턴스에 대해 독점권을 가져야 한다.

  • 클래스는 Mutate method 가 없으므로 위와 같이 하진 않지만, 프로퍼티에 대해서는 독점적으로 사용해야 한다.

  • 독점적 = 한번에 한 쓰레드

  • Swift 4 에서 추가

  • 컴파일 타임, 런타임 양측에서 모두 강제된다.

Swift 예제

IMG_0014.PNGIMG_0015.PNG

구조체와 Mutate Method 에 대한 예제는 아래와 같다.

IMG_0016.PNG

위에 언급 했듯이 구조체 인스턴스 전체에 대해 독점적으로 사용해야 하기 때문에 위처럼 하면 해결이 안된다. 구조체 인스턴스를 이용하는 부분에서 해줘야 한다.

그러나 클래스의 경우는 구조체와 다르므로 아래 처럼 내부 DispatchQueue를 만들어서 해결 할 수 있다.

IMG_0017.PNG

5. Undefined Behavior Sanitizer

XCode 9 에 새로 추가된 기능입니다. 앞서 나왔던 다른 도구와 다른 점은 C 언어 계열의 ‘안전하지 못한’ 구조를 체크해주는 기능입니다.

참고 : Understanding Undefined Behavior

위 링크는  Undefined Behavior (이하 비정의 동작)이 왜 있으며, 앱에 어떤 영향을 주는지 설명해줄 겁니다.

다음과 같은 오류들을 검출 할 수 있습니다.

  1. C++ Dynamic Type Violation

  2. Invalid Float Cast

  3. Integer Overflow

  4. Invalid Shift Exponent

  5. Invalid Boolean

  6. Invalid Variable-Length Array

  7. Invalid Integer Cast

  8. Reached Unreachable Code

  9. Missing Return Value

  10. Invalid Object Size

  11. Nonnull Assignment Violation

  12. Nonnull Parameter Violation 

  13. Nonnull Return Value Violation

  14. Alignment Violation 

  15. Invalid Enum

  16. Integer Division by Zero

  17. Invalid Shift Base 

  18. Null Dereference

  19. Out-of-Bounds Array Access

아이고 많기도 하네요. 일단 3가지 Integer Overflow, Alignment Violation, Nonnull Return value Violation 에 대해서만 살펴봅니다.

1. Integer Overflow

정수 넘침은 보안 문제도 일으 킬 수 있습니다. 모든 정수 넘침이 비정의 동작인 것은 아닙니다. Unsigned Integer 같은 경우가 예외입니다. (※ 좀 더 자세한 예제는 여기에선 나오지 않습니다)

아래와 같이 무엇이 잘못 되었는지 알 수 있게 되죠.

IMG_0018.PNG

2. Alignment Violation

모든 C 언어 변수들은 고유의 크기에 따라 저장되고 불러져야 되어야 하는데 이것을 위반 했을 때 발생한다. 이 문제는 아주 사소한 것이라 찾기 힘들다. 또한, 릴리즈 할 때 컴파일러가 최적화 옵션을 켠다. 이때 만약 메모리 정렬이 잘못되어 있다면 실행중에 죽는 문제를 발생 시킬 여지가 있다. 이런 종류의 버그는 저장 장치에 Serialize, Deserialize 할 때 자주 발생한다. 또한 소켓 통신에서….

예제는 소켓 통신인데, 아래처럼 처음에 “Hey Kuba!” 라 보내고, 그 다음에 “How’s ……” 을 보낸 경우, int 가 4바이트이기 때문에 3번째 이미지에서 보듯이 두번째 패킷에서 에러가 발생 하게 된다.IMG_0020IMG_0019IMG_0021

이를 해결하기 위한 방법은 비디오에 나와있다.

3. Nonnull return violation

“nonnull”이라 표시되어 있는데 어쨌든 nil 을 리턴 했을 때 발생하는 문제다. 이건 C 와 Swift 코드가 섞여 있을 때 문제를 유발 할 수 있다. 따라서 두개를 섞어 사용한다면 비정의 동작 탐지를 켜는것을 추천한다.

아래 예제를 보면 이해 할 수 있을 것이다.

406_finding_bugs_using_xcode_runtime_tools_pdf_178_192페이지_.jpg

아래 설정을 통해 켜고 끌 수 있다.406_finding_bugs_using_xcode_runtime_tools_pdf_180_192페이지_.jpg

Continuous Integration

품질을 높이기 위해 CI 도 이용하세요.

참고 : Continuous Integration and Code Coverage in Xcode

요약

앞서 꾸준히 얘기했듯이 Runtime Tool 은 오버헤드가 크긴한데, 매우 강력한 툴이고 코드 자체의 품질을 높이는데 큰 기여를 할 수 있기 때문에 꼭 사용하길 권합니다. CI 툴과도 같이 이용 할 수 있는데, 환경에 따라 켜고 끄고 테스트 해보길 원합니다.

출처 :https://developer.apple.com/videos/play/wwdc2017/404/

원격 디버깅, SprikteKit, SceneKit 디버깅에 대해 설명한 세션.

1. 원격 디버깅

드디어 원격 디버깅이 가능해졌다. 카메라, AR 개발, 노트북 AC 전원이 아닐 때, 애플 TV 개발 할 때, 그냥 USB 꼽기 싫을 때, TV OS 앱을 개발 할 때 아주 유용하다.

**요구사항 **

iPhone, iPad, or iPod Touch running iOS 11
Apple TV running tvOS 11
macOS 10.12.4+
XCode 9

원격 디버깅이라고 안되는건 없다고 보면 되겠다.

2. 준비하기

설정은 간단하다. 아래와  같이 디바이스 관리 창을 열고,

XCode - Windows - Devices and simulators (단축키 Shift + Command + 2)

“1” 번을 누르면 “2” 처럼 지구본 모양이 생긴다.

Devices_및_WMCamera_m_및_PageAlignedArray_swift.png

Apple TV 는 본인이 없어서 첨부된 키노트 이미지를 대신한다.

연결하면 이렇게 인증 코드를 한번 넣게 되는 듯.

USB, Wireless, Wired Network 모두 지원한다고.

https___devstreaming-cdn_apple_com_videos_wwdc_2017_404z7uj3xincdb0_404_404_debugging_with_xcode_9_pdf.png

보통은 다른 설정은 필요 없지만 특수한 경우 IP를 직접 입력해서 사용 할 수도 있다고 한다.

아이폰이 엄청 많은 환경에서나 사용하려나?

여튼, 원격으로 연결된 디바이스는 아래 그림처럼 XCode 실행 타겟에 지구본 아이콘이 표시 된다.

WMCamera_m_및_Devices.png

3. 디버거 향상

특정 조건을 만족 할 때 브레이크를 걸어주는 옵션이 디버깅 할 때 종종 쓰이는데 이 때 특정 조건을 만났을 때 수행하는 액션쪽에 변화가 생겼다. 밑의 그림처럼 코드 자동 완성 기능이 들어갔다.

또한 옵션이 지정된 브레이크 포인트는 빨간색 네모의 브레이크 포인트 아이콘처럼 뾰족한 부분이 흰색으로 표시된다.

extract-35.jpg

4. UI 계층도

원래부터 있던 기능이긴 한데 디버깅 하다가 아래 화살표로 표시한 버튼을 누르면 3D로 뷰 계층을 보여주고 각종 정보를 볼 수 있다.

extract-38.jpg

SpriteKit

SpriteKit 은 2D 에 쓰이는데, 이젠 여기도 잘 된다고. 멋진 UI 만들고 싶으면 SpriteKit 써보라는 얘기도.

extract-41.jpg

SceneKit

SceneKit 은 3D 용인데 물론 여기에도 잘 된다는 소개.

extract-46.jpg

이 세션은 기능 소개 위주 였고, 심각하게 고민해서 볼 부분은 없었던 것 같다.

5. One more 자랑

SceneKit 을 이용해서 디버그 시각화 툴도 만들었다고 소개하는 부분

Debug Navigation 을 고 아래 버튼을 누르면 여러 옵션을 볼 수 있다.

extract-52.jpg

Item-0_및_Item-1_및_Item-0_및_Item-0.png

메모리 시각화 한 부분은 참 좋은데 얼마나 잘 써먹으려면 여러 전략이 필요 할 것 같다.

클리앙에 썼던 글을 다시 옮겨와봅니다.
https://www.clien.net/service/board/park/10976812

최저임금 관련해서 자영업자와 일반인들간에 감정 싸움이 벌어지는 글이 종종 올라와

답답한 마음에 감히 한말씀 올립니다.

우리나라는 인건비에 대해 참 박하죠.

공임비 조차도 그거 왜 받냐고 하는 사람이 부지기수죠.

이건 사람의 가치에 대해 크게 고민해보지 않아서라고 생각합니다.

개개인은 얼마가 됐든 정해진 수명을 가지고 있습니다.

바꿔 말하면 개인이 가진 시간은 무엇과도 바꿀 수 없는 개인의 고유 자원이죠.

개개인으로 보면 가장 가치있는 자원입니다.

제 의견은 개인의 시간을 노동에 썼을 때 국가가 공인한 금액이 최저 임금이라고 생각합니다.

이건 생득적인거죠.

최저임금 이상 받는건 그 사람이 자연적으로 부여받은 시간, 그 이상의 부가가치를 생산해낸 것이고요.

이정도가 원론적인 저의 생각이고, 그 다음으로 실질 경제에 끼칠 영향을 생각해 봐야겠죠.

경제를 돈의 흐름이라고 치고, 1층(저소득,저재산)에서 100층(고소득,고재산)까지 있다고 칩시다.

(이하 하위,고위,저층,상층은 돈의 분포를 말씀드리는 것입니다)

돈은 1층~100층까지 자유로이 왔다갔다 해야 건전한 경제 활동이 이뤄질 겁니다.

그런데 우리나라의 문제는 저층에 돈이 없습니다.

그리고 저층의 돈은 고층으로 흘러들어가는데(대기업 제품, 프렌차이즈, 대형 마트 이용 등등)

고층으로 흘러간 돈이 저층으로 순환되지 않습니다. (자본주의  착취 구조, 일반적인 사람들의 사고방식에 기인)

소득, 재산 분포를 보면 알 수 있죠.

그렇다면 돈의 계층간의 이동이 좀 더 활발히 이루어 질 수 있도록 해야 한다고 생각합니다.

지금은 하위 자본이 상위로 가는게 상위 자본이 하위로 가는 것보다 훨씬 많습니다.

한편으로, 최저임금만 올리면 가뭄에 논밭에 말라버린 연못에서 물 퍼다 나르는 꼴 밖에 되지 않습니다.

아직 물이 풍부히 남아 있는 곳에서 물을 길어와야죠.

대기업, 최상위 자본가에 있는 돈이 그 다음 자본 계층인 중소 기업으로 충분히 흘러가야 중소 기업이 탄탄해지고

또한 중간 계층의 자본이 하위 경제 계층에 흘러가야 합니다.

최저 임금은 그동안 저 평가된 인건비를 정상화 시키는 단계 중 하나입니다.

다만, 이것만으로는 건강한 체질이 이루어 질 수 없습니다.

대기업과 중소기업의 불공정 구조, 얼마전에 얘기한 김상조씨의 중소기업의 영세 기업에 대한 갑질 문제,

프렌차이즈 가맹점의 본사-가맹점의 불공장한 관계 등 각층에서 동시에 큰 변화가 일어나야 합니다.

문재인 정부는 이를 잘 알고 있다고 생각합니다.

또한 제가 문재인 정부에 거는 기대가 사회 다방면에 걸쳐 부조리함, 불공정함을 해결 할 수 있는 시스템과 문화를 정착시켜 달라는 것이고요.

영세 자영업자와 노동자간의 감정싸움, 하지 않았으면 좋겠습니다.

“최저시급 지급 못하는 업자는 망해버려야 한다” ,”니가 못나서 최저시급 밖에 못받지”

이런 것 모두 담론에 해악 요소 입니다.

이런 얘기 하면 웃는 곳은 정해져 있습니다.

좀 더 중요한 것에 집중했으면 합니다.

우리가 해결하고 정부에 요청해야 할 것은 자본이 전체 계층에 잘 순환될 수 있도록 시스템을 만들고,

잘 행해지도록 감시 감독 하도록 만드는 것입니다.

지난 민주정부 10년동안에도 해결 하지 못했습니다.

그 이런 문제를 해결 하지 못한건 여러 원인이 있겠지만 시대의 공감도 큰 원인 중 하나였다고 생각합니다.

우리는 민주정부 10년, 극한의 8년을 보내면서 많은 것을 새롭게 깨달았습니다.

문재인 정부 5년 동안 해결 하기 힘들 것입니다. 그 뒤로 5년 더 ,  10년 더 필요 합니다.

불필요한 감정싸움은 지양하고 진짜 우리가 필요한 것을 위해

정치에 더 관심을 갖고 참여 하고 그 다음 주자를 만들어내는데 힘을 쓰면 좋겠습니다.