• Stability vs Real World Conditions In Test


    최근 내 iTime프로젝트에서 쓰고있던 swift-clocks 라이브러리의 Testclock을 사용한 clock unit test들이 random하게 터지는 일이 발생을 했다. 원인을 찾아보니 TestClock에서 제공하는 advance(to:) 함수가 flaky한것이 원인이었다.
    당시 내 생각은 Test에서 쓰이는 Clock의 인터페이스가 deterministic하지 못한건 잘못 되었다 생각하고 swift-concurrency-extra의 withMainSerialExecutor 함수를 사용해, TestClock의 advance(to:) 구현부를 감싸 Test시에 동작을 좀 더 deterministically 하게 바꾸어 PR을 올렸다. PR 코드 수정 부분



    그러다 몇시간 만에 바로 poinfreeco 메인테이너인 stephencelis 에게서 답변이 달렸다. (기본적으로 PR 피드백이 굉장히 빠르신거 같다👍)



    stephencelis1



    요약:

    • 너의 코드가 Test를 less flaky하게 만드는건 맞다.

    • 하지만, 실제 환경을 테스트하려는 사용자들이 TestClock을 사용하는 것을 방해 또는 예상치못한 effect를 발생시킬수 있다.


    Test의 Flakiness에만 신경을 쓰고 있던터라, Test의 실제 환경 반영 여부는 잘 고려하지 못했었다. 또한 executor를 바꾸게 되면 기존 사용자들이 영향을 받을 수 있다는 점도 간과하였다.

    그럼에도 불구하고, less flaky한 코드를 내버려두는것에 의구심이 남아 좀 더 디테일한 설명요청과 함께 질문을 하였더니 금방 정성스런 답변을 받을 수 있었다.



    stephencelis2



    요약:

    • 사용자들은 default executor와 함께 TestClock을 사용하여 실제 환경과 유사하게 동작하도록 해야 한다.
    • withMainSerialExecutor를 사용하면 비동기 코드가 메인 스레드에서 예측 가능하게 연속적으로 실행되어 TestClock을 사용하는 코드가 현실과 다르게 동작하게 된다.
    • 따라 실제 환경을 테스트하려는 사용자들은 여전히 default excecutor에서 테스트가 가능해야 하며, flaky한 현상들은 다른 방법으로 각자 handling 되어야 하는게 적절하다.


    즉, 애초에 이 라이브러리에서 TestClock이 설계 될때, 우선적으로 고려했던 것은 “Clock이라는 도메인의 실제환경을 반영하는 TestClock”인 것이다. 그렇기에 내가 수정한 PR 코드의 변경으로 설계자가 의도한 “실제 환경 반영” 이라는 의도를 비틀 수 있는 것이다. 따라서 이 PR이 반영 되려면, TestClock이 실제 환경을 반영하는 특징을 깨뜨리지 않고 기존 사용자들에게 의도되지 않는 영향을 주지 않으며 기존 개선하고자 했던 부분인 테스트 시의 less flaky한 ` advance(to:)` 구현체를 구현하면 된다.



    다만 현재 처럼, Test 환경에서 Stability vs Real-World Conditions을 비교해보아야 하는 상황이면 어떻게 해야할까?


    둘의 정의를 통해 맥락을 비교해보자.


    Stability:

    • 테스트 시의 안정성은 동일한 조건에서 여러 번 실행될 때 동일한 결과를 얻을 수 있는 특성을 의미함. 즉, 동일한 테스트 데이터와 환경 설정에서 실행될 때 일관된 결과를 제공하는 것이 중요.
    • 테스트 시의 안정성은 주로 개발자들이 코드 변경사항을 확인하고 버그를 찾는 데 도움이 된다. 안정적인 테스트는 개발 주기를 빠르게 만들고, 코드 변경에 대한 신뢰성을 높일 수 있다.


    Real-world Conditions:

    • 테스트가 실제 조건에서 어떻게 동작하는지 확인하는 것은 애플리케이션이 실제 사용 환경에서 어떻게 동작할지에 대한 자신감을 제공.
    • 현실적인 조건에서 테스트를 실행하면 미처 고려하지 못한 환경에서 발생할 수 있는 문제를 미리 감지할 수 있다.


    두 측면은 프로젝트의 성격에 따라 어떤 측면이 더 중요한지 나뉘게 된다. 만약, 프로젝트가 안정성과 신뢰성을 중시한다면 Stability가 중요할 수 있다.

    반면에 실제 사용 환경에서 발생하는 다양한 조건을 반영해야 하는 경우 Real-world Conditions이 중요할 수 있다. 또는 개발 단계와 라이프 사이클 단계에 따라 우선시해야 하는 측면이 다를 수 있다. 예를 들어,안정성 있고 빠른 개발을 위해 초기에는 테스트 시의 안정성을 고려하고, 추후 개발 프로덕트가 실제 사용 환경에 가까워 질수록 테스트 시의 실제 환경을 구축하여 다양한 조건들을 테스트 하는게 중요할 수 있다. 종합적으로 프로젝트의 특성과 단계에 따라 두 측면을 균형 있게 고려하는 것이 중요.



    결론

    • 정답은 없다. “ Flakiness한 테스트? 잘못된거 아니야? “ 라고 바로 도달하기 보다는 한발자국 떨어져 관점을 바꾼 후 문제를 다각도로 관찰 해보도록 해보자.
    • 라이브러리의 코드를 변경 시 기존 사용자들을에게 어떤 영향을 미치는지 늘 고려하도록 하자.



    -자세한 PR 내용은 하단 참조-
    Ref:

    https://github.com/pointfreeco/swift-clocks/pull/27

  • Dependency Injection Pattern Tradeoff


    내가 iTime에서 needle을 걷어내고(라이브러리 의존성) 순수 DI를 내가 하게됨으로써 나에게 선택지는 크게 3가지가 있었다.

    • IoC Container 방식(SL): 특정 모듈(어셈블리)에 속하는 인터페이스 구현타입을 IoC 컨테이너로 등록하여, 사용자가 클래스 생성자를 직접 호출하는게 아니라, IoC 컨테이너에 의해 호출 된다. 인스턴스 생성방향이 역전되는게 특징이다.
    • Pure -DI 방식: 심플하게 말하자면 IoC Container 없이 의존성을 주입하는 방법이다.
    • Convention over Configuration 방식: 설정 대신 약속으로 DI를 한다는 것인데.. 사실 이번 iTime은 나혼자 하는거라 큰 의미가 없다.(지금 내상황에서의 장점도 크지 않다.) 그러므로 이번 옵션에서는 제외하겠다.



    Pure DI vs IoC Container


    image

    IoC Container


    IoC 컨테이너를 사용하면 의존성 등록 비용이 적다. 하나하나 생성자를 통해 의존성을 넣어주는 코드를 작성하지 않아도 되니 말이다. 앱이 커지면 커질수록 객체들도 많아 질테니 이런 수작업을 줄이고 코드를 줄일 수 있다는건 유지보수의 용이성을 올리는 일이니 장점이 분명하다. 하지만, 단점도 명확하다. 먼저 IoC Container를 도입하려면 별도의 container에 대한 학습 비용이 발생하게 된다. 무엇보다 가장 큰 문제는 의존성이 잘못 만들어졌을 경우, 컴파일 에러를 발생시키지 않고 런타임 에러 발생이 가능하다는 것이다.(Weakly typed) 그리고 이런 에러가 발생해버리면 특정 기능이 실행되지 않거나 앱이 크래쉬가 날수도 있는 가능성을 내포한다. 이는 앱 개발자가 가장 회피해야할 유저사용경험 중 하나이다.



    Pure-DI


    Pure-DI 방식의 단점은 명백하다. 바로 High maintenance. 즉, 일일이 손으로 의존성 주입 코드를 작성해주어야 하기 때문에 코드양이 늘어나게 되고 개발하는데 비용 또한 늘어나게 된다. 이는 결국 유지보수 비용의 증가로 연결된다. 그렇다면 IoC Container가 Pure DI보다 좋을까? 이는 상황마다 다르다. 이 질문을 상기시키며 Pure DI의 장점을 살펴보자. 먼저 IoC Container의 학습비용이 사라지게 된다. 또한 각 모듈의 Composition Root를 찾으면 개체 그래프가 어떻게 구성되는지 분명하게 파악할 수 있다. Pure DI의 가장 큰 장점은 IoC Container의 Weakly typed 문제가 Pure DI에서는 발생되지 않는다는 것이다. 이는 런타임 에러를 피할 수 있는 매우 강력한 장점이다.

    정리를 통해 여기서 우리가 고민해야하는 방향이 좁혀졌다. 흔히들, (이 아티클을 쓰기전 나를 포함) 의존성 등록 비용을 줄인다는 이유로 IoC Container의 학습 비용과 Weakly Typed 비용을 제대로 고려하지 못하는 것 같다. 모든 비용을 고려하여 내 iTime에는 어떤 DI 방식을 쓸지 고려해보자
    image

    먼저 선택에 앞서 현황파악을 먼저 시도해보았다.

    1. 현재 내 앱은 개발중이며 사용자 경험이 중요한 어플이다.
    2. 개인이 개발하는 앱이기 때문에 상당량 규모의 의존성 등록이 발생할 확률이 적다.



    이어 TradeOff를 고려해보자,

    • 현재 내 앱의 의존성 등록비용이 Weaky typed 비용 보다 큰가? -> NO

    • 앱 개발자로서 crash를 내포할 코드를 작성하는 리스크를 지면서까지 의존성 등록 비용을 줄여야하는 이유가 명확한가? -> NO

    Conclusion


    내 식견으로 앞의 질문들에 대한 답을 고려해보았을때, 현재 내 앱에 들어갈 DI 방식으로는 Pure-DI가 적절한것으로 판단된다.


    추후에 정말 앱이 커져 의존성 등록 비용이 기하급수적으로 커진다면 IoC Container를 고려해봄직하다.





    Ref:
    https://blog.ploeh.dk/2012/11/06/WhentouseaDIContainer/
    https://blog.ploeh.dk/2014/06/10/pure-di/
    https://jwchung.github.io/DI%EB%8A%94-IoC%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EC%95%8A%EC%95%84%EB%8F%84-%EB%90%9C%EB%8B%A4
    https://minsone.github.io/pure-dependency-injection

  • mvc, mvp, mvi, mvvm, viper, ribs 개요 및 탄생 이유



    설계 및 디자인 패턴, 아키텍쳐 패턴 등을 뭐라고 정의할까? 보통 이에 대해 "어떤 문제가 발생하고 다른 똑똑한 사람들이 우아하며 일관성 있게 정리하여 내놓은 해답" 정도로 생각했다. 하지만, 아키텍쳐 영역은 거의 예술의 영역이라 볼수 있을 만큼 심오하다. 애초에 모든 문제에 대응하는 실버불릿이라고 말할 것들이 없으며 형태 또한 환경, 시기에 따라 자꾸 변화한다.

    image


    이런의미에서 아키텍쳐를 “현재의 애플리케이션 개발을 위한 최선의 접근방식을 적용하려는 끊임없는 진화의 시도중 하나” 라고도 정의할 수 있을 것 같다. 여기서 잊지 말아야 하는건 그 접근 방식의 시작은 항상 문제로 부터 출발한다는 거다. (설계나 패턴이 먼저가 아니라는 소리)





    MVC


    image


    개요


    모델-뷰-컨트롤러(model–view–controller, MVC)

    Model: 객체가 행동과 프로퍼티로 실제 세계의 Entity와 process를 모델링하는데 사용되는 도메인 모델
    View: 모델의 시각적 표현, 주로 애플리케션의 위젯이나 스크린을 지칭
    Controller: 키보드나 마우스 같은 사용자의 입력에 응답하는 구성요소. 사람과 애플리케이션을 이어주고 사용자가 view 및 model가 상호작용 할 수 있도록 다리역할을 수행.



    흔한 오해


    “controller가 model과 view를 분리시킨다?” -> NO. MVC 패턴자체가 presentation Layer으로부터 Domain layer의 concern을 분리 시키는거다. 그리고 이 분리는 controller가 분리시키는게 아니라 observer pattern통해 달성이 된다. controller는 user와 application을 분리하는것 (view와 모델이 아니라..)



    탄생이유


    1979년, 제록스 팔로 알토 연구소
    라이브 린스케이지(Trygve Reenskaug)란 사람이 다이나북팀에서 GUI개발을 하던중, 생각을 떠올리게 됨. “사용자가 세상을 인식하는 방법(멘탈 모델)과, 컴퓨터가 정보를 인식하고 처리하는 방법이 달라보인다. 전혀 책임이 다른 이 두 부분을 잘 분리시켜서 설계하는 게 효율적이겠구나.” 그렇게 해서 MVC는 세상에 모습을 드러나게 되었고 결국 MVC의 존재여부는 다음과 같다. “GUI를 가진 소프트웨어를 객체 지향적으로 잘 구조화하기 위해서” 좀더 추상화 시키자면 “관심사의 분리(separation of concerns)”를 위해 탄생했다. 라고 봐도 된다.
    (또는 “도메인과 presentation의 영역 분리”라고도 말할 수 있다.)





    MVP


    MVP는 대표적으로 두가지 방식이 유래

    1. The Taligent Model-View-Presenter Pattern


     image


    개요


    MVC의 변형으로 유사하게 애플리케이션의 도메인, 프레젠테이션 및 사용자 입력의 concern을 구성요소로 분리함.

    Model: 애플리케이션의 코어기능이나 데이터
    Command: data에 대한 operation이 모여있는 곳. e.g) deleting, printing, saving
    View: 데이터의 시각화
    Interactor: 사용자 이벤트가 모델에서 수행되는 작업에 매핑되는 방식을 다룸 e.g) 키보드 입력, 체크박스
    Presenter: 어플리케이션 내 다른 구성 요소의 전반적인 상호 작용을 조정. (Coordinator 느낌..?)



    탄생이유


    MVC로부터 파생 Apple, IBM 및 Hewlett-Packard의 합작 투자 회사인 Taligent에서 1990년대 초에 시작됨 사용자 인터페이스 프레임워크의 기초로 1995년 IBM이 Taligent, Inc.를 인수한 후에 Mike Potel이 처음으로 Taligent programming model에서 발견되는 특징적인 패턴을 설명하기 위해 “Model-View-Presenter”라는 이름을 제안함.

    2. The Dolphin Smalltalk Model-View-Presenter Pattern


    image

    개요


    Dolphin Smalltalk 팀은 MVP 패턴의 공식 설명에서 인터랙터, 명령 및 선택 항목을 제거하여 Taligent MVP 패턴을 단순화시킴. Presenter의 역할을 단순화하여 하위 컨트롤러에서 뷰를 대신하여 모델 업데이트를 중재하는 구성요소로 변경시킴. (이게 현재 우리가 알고있는 MVP에 가까움)

    Model: 애플리케이션의 코어기능이나 데이터
    View: 데이터의 시각화
    Presenter: 모델과 상호작용하는 presentation 로직을 포함하는 곳



    탄생이유


    1. 복잡성 때문에 model에서 view로 다이렉트로 접근하고 싶은 유혹이 생김.. <- 이 유혹에 환멸을 느끼게 됨
    2. 그렇다고 MVC 쓰기에는 최신 플랫폼은 기본 위젯에서 바로 유저이벤트를 처리하는데 이부분에서 전통적인 MVC와 맞지 않다고 느낌 (controller가 이 역할을 하니깐)
    3. MVC의 진화라기보단 개발 하고 있던 VisualWorks MVC내의 리팩토링 과정에서 진행하다보니 MVP 라는 패턴이 나온 느낌

    swift에서 MVP라하면 Presenter layer를 추가하여 viewModel을 만들어 view로 내려주는 용도로 사용하는것이 흔하다.





    MVVM


     image


    개요


    Model: 애플리케이션의 코어기능이나 데이터
    View: 데이터의 시각화
    ViewModel: view 속성, 명령의 추상화(결국 로직짜라는 소리), binder를 사용함. MVP의 Presenter와 다르게 viewModel은 view를 모른다.
    Binder: viewModel과 view를 동기화 해줌



    탄생이유


    MVVM은 WPF의 Data-Binding기능을 사용해서 View layer에서 거의 모든 GUI코드를 제거(or 숨김)하도록 설계되었다. 이에 Microsoft WPF 및 Silverlight 설계자인 John Gossman은 2005년 자신의 블로그에서 MVVM을 발표했다. MVVM으로 역할 분리를 통해 당시 인터렉티브 디자이너는 비지니스 로직 프로그래밍 보다는 UX 요구사항에 집중할 수 있었다 한다. 따라 생산성 향상을 위해 애플리케이션의 계층을 나눠 여러 작업 흐름에 개발할 수 있다록 한 것이다. 설령 혼자 작업한다 해도 view 영역은 자주 변경되므로 모델과 뷰를 더 적절히 분리 한것으로 더 생산적인 결과를 낳았다.


    결과적으로 MVVM의 가이드 라인이 유도하는 바는 모델과 프레임워크가 가능한 한 많은 작업을 구동하고 View를 직접 조작하는 로직을 제거하거나 최소화 하는것 이다.





    MVI


     image


    개요


    • Unidirectional cycle of data
    • Non-blocking (뷰가 모델의 상태 변화에 의해 블로킹되지 않음)
    • Immutable state

    View: 사용자에게 제공되어 보여 지는 UI 부분으로 상태(state)를 입력 받아 화면에 출력
    Intent: 앱 상태를 변경 하려는 의도를 의미하며 사용자의 상호작용으로 발생한 상태를 변경 하는 동작
    Model(State): 앱의 상태를 의미하며 MVI에서는 하나의 상태만을 갖으며 imutable한 데이터 구조로 작성, intent를 트리거 하여 새로운 상태를 만드는 것만이 State를 바꾸는 유일한 방법



    탄생이유


    Hannes Dorfmann가 전통적인 MVP or MVVM의 문제점을 꼬집으며 내세운 리덕스기반 패턴 대표적으로는 MVVM이 가지고 있던 Mutiple Inputs (non-thread-safe) + Multiple State 특성으로 발생되는 side-effect문제와 더불어 앱의 상태관리 문제를 해결하려고 함.


    • 어떻게?
      • MVI에서 상태는 불변, 즉, View를 업데이트 하기위해선 intent()를 발생시켜 그에 따라 새로운 상태를 덧씌우는것이 유일한 방법
      • Model은 뷰 상태에 대한 Single Source Of Truth이므로 상태 중복 X + Immutable -> 따라 model자체를 copy() 함





    Viper


    image

    개요


    Clean Architecturer를 기반으로한 설계 -> 논리적 구조를 별개의 layer로 분리 -> easier to isolate dependencies & Test

    View: Presenter에게 받은 data를 보여주고 사용자에게 받은 input action을 다시 presenter에게 전달
    Interactor: usecase를 포함한, 비지니스 로직을 담당
    Presenter: display하는데 필요한 뷰 로직 + user input을 view에게 받으면 반응해줌 (interactor에게 데이터를 request하던가 or view logic)
    Entity: interactor가 사용하는 basic model
    Routing: 어느 순서, 어느 screen이 보여질껀지 등의 navigation logic 담당


    설계의 바탕은 Single Responsibility Principle.



    탄생이유


    글에서 말하는걸로보아.. 찾은 결과로 보아.. objc.io-Viper 가 ios진영의 viper 시작을 알리는 글로 추정이 된다.
    여기 나와있듯이 iOS진영에서 테스트는 중요한 부분으로 여겨지지 않았고 이를 못마땅하게 본(?) objc.io 팀원들은 테스트를 좀 더 짜기 쉽게 하기위해 새로운 설계방법인 Viper를 만들었다.





    RIBs


    image


    개요


    Riblet이라는 단위로 어플리케이션의 RIB Tree를 만들어 설계

    View(Controller): 최대한 “dump”하게 디자인 됨. 일반적으로 유닛테스트가 필요한 코드가 여기 포함되지 않음
    Interactor: 비지니스 로직을 담당, interactor lifecycle Rx Subscription이 일어나야함.
    Presenter: business Model 를 view Model로 바꿔줌.(그 반대도). 생략가능 -> 생략시 이 책임은 interactor or view
    Component: RIB dependencies를 관리함. 주로 dependencies들에 대한 엑세스제어, 제공 등을 담당, RIB을 만들때 builder와 함께 사용된다.



    탄생이유


    Cross-platform 아키텍쳐 프레임워크, 중첩된 상태들을 가진 거대한 모바일 어플리케이션을 위해 디자인 됨
    (여기서 부턴 사견입니다.)
    갈수록 앱들이 고도화 되고 도메인에 따라 (ex, uber) 한 화면에 많은 Nested 상태들 을 가지는 경우가 많아짐. uber 앱만 봐도 메인 맵 에서 정말 많은 일들이 일어남. 이런 앱의 상태들과 앱 내 책임들을 일관성있게 정리하려고 시도한게 바로 이 Ribs + Ribs Tree라고 생각. 추가로 이걸로도 상태관리가 부족하면 State-Machine 같은 객체를 아키텍쳐에 추가해보는 생각을 가져갈 수도 있음.






    Ref)
    https://hannesdorfmann.com/android/mosby3-mvi-1/
    https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel
    https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter
    http://aspiringcraftsman.com/2007/08/25/interactive-application-architecture/#the-model-view-presenter-pattern
    https://yoon-dailylife.tistory.com/117
    https://www.objc.io/issues/13-architecture/viper/
    





  • Swift Naming With Convention



    Naming


    애매함을 피해라.

    extension List {
        public mutating func remove(at position: Index) -> Element
    }
    


    필요없는 말은 생략해라

    public mutating func removeElement(_ member: Element) -> Element
    


    Name variables, parameters, and associated types 등 각자의 책임에 따라 네이밍, Not Type..

    protocol ViewControlller {
        associatedtype ContentView: View
    }
    class ProductionLine {
        func restock(from supplier: WidgetFactory)
    }
    


    하지만, 만약 타입이 역할인경우? -> associated typed , Protocol

    protocol Sequence {
        associatedtype Iterator: IteratorProtocol
    }
    
    protocol IteratorProtocol {}
    

    이런경우는 뒤에 Protocol이라 명시


    매개 변수의 역할을 명확히 하기 위해 약한 타입 설명 추가

    func addObserver(_ observer: NSObject, forKeyPath path: String)
    grid.addObserver(self, forKeyPath: graphics) // clear
    



    유창한 사용을 위한 노력


    영어 문법대로 말이되게 method명, 파라메터명 만들기

    x.insert(y, at: z)          x, insert y at z
    x.subViews(havingColor: y)  x's subviews having color y
    x.capitalizingNouns()       x, capitalizing nouns
    


    해당 메소드가 호출 중심이 아닌경우, 하나 또는 두개의 매개변수 이후 유창성이 떨어지는것은 괜찮다.

    AudioUnit.instantiate(
      with: description,
      options: [.inProcess], completionHandler: stopProgressBar)
    


    팩토리 메소드의 시작은 make 붙이기

    (ex) foctory.makeInterator())



    Side-Effect에 따라 함수와 메소드 이름 지정

    1. Side-Effect 없으면 명사구로 (ex) x.distance(to: y), i.successor())
    2. Side-Effect 있으면 명령형 동사로 (ex) x.sort(), x.append())
     더 나아가,해당 함수가 Nonmutating인 경우 “ed” or “ing” 붙이기
    3. 하지만, 메소드가 자연스럽게 명사구로 설명이 될때 --> 
    y.union(z)-> y.formUnion(z) j,
    c.successor(i)-> c.formSuccessor(&i)
    와 같이 “form” prefix해주기
    


    Boolean 메소드나 다른 프로퍼티는 Nonmutating일 때, 수신하는 쪽의 Assertion 처럼 읽혀야 함.

    (Ex) x.isEmpty, line1.intersects(line2))


    무엇인가 설명하는 Protocol은 명사로 읽혀야 함.


    기능을 설명하는 Protocol은 “able”, “ible”, “ing” 등을 뒤에 붙힘


    Other types, properties, variables, and constants의 네임들은 명사들로 읽혀야 함.



    용어를 잘 사용하기


    Term of Art : 특정 분야나 직업 내에서 정확하고 전문적인 의미를 갖는 단어나 문구.

    애매모호한 말들 피하기. Ex) 화려하고 겉만 번지르르한 단어들 말고 핵심적이고 본질적인 언어 써라..


    정해진 의미를 지켜라.


    1. Don’t surprise an expert. : 신조어 처럼 있던것 개조해서 말 만들지 말기
    2. Don’t confuse a beginner: 새로운 용어를 배우려는 사람은 본래 용어의 traditional한 의미를 먼저 배움. 그러니깐 건들이지 마..

    정해진 의미를 지켜라.약어, 축약어 피해라

    선례를 살펴보고 수용하기. 초보자에게 더 쉬운 용어를 제공하겠다고 선례 무시하고 신조어 만들지 말기. Ex) not list -> Array OK (list가 초보자에게 더 쉬워보일수는 있어도 no no)

    • 특정 도메인에서는 sin(x) 처럼 쓰이는것들이 있는데 이걸 위와 같은 이유로 verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x).와 같이 만들지 말기.



    Convention


    - General Conventions


    O(1)이 아닌 Computed Property의 접근 시 Complexity을 문서화합니다. (오.. 이건 새롭다)


    그냥 생 free functions 보다 메소드나 프로퍼티를 선호하자. (Free func는 거의 안써서.. 공감이 안됨)


    카멜 케이스 컨벤션 따르기


    같은 의미 이거나 같은 도메인일경우, Method들은 base name을 끼리끼리 공유 할수 있다.

    extension Shape {
      /// Returns `true` if `other` is within the area of `self`;
      /// otherwise, `false`.
      func contains(_ other: Point) -> Bool { ... }
    
      /// Returns `true` if `other` is entirely within the area of `self`;
      /// otherwise, `false`.
      func contains(_ other: Shape) -> Bool { ... }
    
      /// Returns `true` if `other` is within the area of `self`;
      /// otherwise, `false`.
      func contains(_ other: LineSegment) -> Bool { ... }
    }
    


    ❌❌ 이 케이스는 different sementic이므로 리네이밍 해야함.

    extension Database {
      /// Rebuilds the database's search index
      func index() { ... }
    
      /// Returns the `n`th row in the given table.
      func index(_ n: Int, inTable: TableID) -> TableRow { ... }
    }
    


    “overloading on return type”를 피해라

    ❌❌ 타입 추론에 있어 모호성을 유발

    extension Box {
      /// Returns the `Int` stored in `self`, if any, and
      /// `nil` otherwise.
      func value() -> Int? { ... }
    
      /// Returns the `String` stored in `self`, if any, and
      /// `nil` otherwise.
      func value() -> String? { ... }
    }
    



    Parameters


    Documentation에 알맞은 (어울릴만한) 파라메터 명 채택하기

    /// Return an `Array` containing the elements of `self`
    /// that satisfy `predicate`.
    func filter(_ predicate: (Element) -> Bool) -> [Generator.Element]
    
    /// Replace the given `subRange` of elements with `newElements`.
    mutating func replaceRange(_ subRange: Range, with newElements: [E])
    


    ❌❌ 어색하고 문법도 틀림

    /// Return an `Array` containing the elements of `self`
    /// that satisfy `includedInResult`.
    func filter(_ includedInResult: (Element) -> Bool) -> [Generator.Element]
    
    /// Replace the range of elements indicated by `r` with
    /// the contents of `with`.
    mutating func replaceRange(_ r: Range, with: [E])
    


    Default Parameter 활용 하기 (Simple을 위해)

    extension String {
      /// ...description...
      public func compare(
         _ other: String, options: CompareOptions = [],
         range: Range? = nil, locale: Locale? = nil
      ) -> Ordering
    }
    


    매개 변수 리스트 마지막에 보통 default 파라메터 값을 넣어주면 좋다. Default 값이 없는 초기 파라메터가 보통 sementic 측면에서 중요하며, 안정적임

    #fileID 쓰는거 선호하기. (Save space & protects developers privacy)



    Agument Labels


    인수를 유용하게 구분할수 없는 경우 라벨들을 다 없애라.

    (Ex)  min(number1, number2), zip(sequence1, sequence2) )


    상위 타입 캐스팅 하는 경우 첫번째 라벨 없애기

    Ex)Int64(someUInt32), String(veryLargeNumber, radix: 16)


    하위 타입 캐스팅 하는 경우는 라벨 살리기

    extension UInt32 {
      /// Creates an instance having the specified `value`.
      init(_ value: Int16)             Widening, so no label
      /// Creates an instance having the lowest 32 bits of `source`.
      init(truncating source: UInt64)
      /// Creates an instance having the nearest representable
      /// approximation of `valueToApproximate`.
      init(saturating valueToApproximate: UInt64)
    }
    


    첫번째 인자가 전치사구인 경우, 인자 라벨을 제공하기

    Ex) x.removeBoxes(havingLength: 12).



    ❌❌ 하지만 여러개의 인수들이 한가지의 추상화를 하고 있다면 Exeption

    a.move(toX: b, y: c)
    a.fade(fromRed: b, green: c, blue: d)
    


    -> into

    a.moveTo(x: b, y: c)
    a.fadeFrom(red: b, green: c, blue: d)
    


    첫번째 인자가 문법적인 표현을 구사하는 경우, 라벨 생략 -> 대신 func base name에 포함시키기 Ex) x.addSubview(y)



    **반대로 말하면 첫번째 인자가 문법적으로 말하고 있지 않으면 라벨이 필요하단거임 <중요>**

    view.dismiss(animated: false)
    let text = words.split(maxSplits: 12)
    let studentsByName = students.sorted(isOrderedBefore: Student.namePrecedes)
    


    말했던 라벨 안붙이는 특이케이스 제외하고 나머지는 모두 다 인자에 라벨 붙여라.



    Special Instructions


    튜플이나 클로져 파라메터에 라벨을 지정해라

    /// - Returns:
    ///   - reallocated: `true` if a new block of memory
    ///     was allocated; otherwise, `false`.
    ///   - capacityChanged: `true` if `capacity` was updated;
    ///     otherwise, `false`.
    mutating func ensureUniqueStorage(
      minimumCapacity requestedCapacity: Int,
      allocate: (_ byteCount: Int) -> UnsafePointer<Void>
    ) -> (reallocated: Bool, capacityChanged: Bool)
    


    top-level-func가 있으면 그 함수의 parameter 네이밍 하듯이 지어라.



    Unconstrained 한 다형성을 다룰때는 오버로드 묶음들 중에서 애매함을 피하기 위해 좀더 신경써라


    ❌❌ 뭐가 실행되는지 애매함

    struct Array {
      /// Inserts `newElement` at `self.endIndex`.
      public mutating func append(_ newElement: Element)
    
      /// Inserts the contents of `newElements`, in order, at
      /// `self.endIndex`.
      public mutating func append(_ newElements: S)
        where S.Generator.Element == Element
    }
    
    
    var values: [Any] = [1, "a"]
    values.append([2, 3, 4]) // [1, "a", [2, 3, 4]] or [1, "a", 2, 3, 4]?
    


    명확하게 메소드 시그니쳐 추가

    struct Array {
      /// Inserts `newElement` at `self.endIndex`.
      public mutating func append(_ newElement: Element)
    
      /// Inserts the contents of `newElements`, in order, at
      /// `self.endIndex`.
      public mutating func append(contentsOf newElements: S)
        where S.Generator.Element == Element
    }
    




    코코아 터치 기반 Bool 케이스 정리


    is + 명사 “(무엇)인가?”

    func isDescendant(of view: UIView) -> Bool //UIView: "view의 자식인가?"
    


    is + 현재진행형(~ing)

    “~하는 중인가?”

    var isExecuting: Bool { get } // operation: 오퍼레이션이 진행중인가?
    var isPending: Bool { get } // MSMessage: “메세지가 보내기전 대기중인가?”
    


    is + 형용사

    case 1: 단어 자체가 형용사인것 : opaque, readable, visible 등 case 2: 과거분사 형태 (수동태라고 생각): hidden, selected, highlighted, completed

    var isOpaque: Bool { get set }
    var isSelected: Bool { get set }
    var  isHighlighted: Bool { get set }
    var isHidden: Bool { get set }
    


    ❌ is + 동사원형은 피하기 ❌

    // 모호함
    var isEdit 
    
    // 명확함
    var isEditable: Bool // 편집 가능한가?
    var isEditing: Bool // 편집 중인가?
    var canEdit: Bool // 편집할 수 있는가?
    


    (다만 상황과 컨벤션에 따라 조정 가능)



    조동사

    can

    “~ 할수 있는가?”

    var canBecomeFirstResponder: Bool { get }
    


    should, will

    “~해야 하는가?” or “~ 할 것인가?”

    var shouldRefreshRefetchedObjects: Bool { get }
    



    has

    has로 시작하는 Bool 유형 2가지, 서로 뜻이 다름

    has + 명사

    “~를 가지고 있는가?”

    var hasiCloudAccount: Bool { get }
    var hasVideo: Bool { get }
    


    has + 과거분사

    “~가 현재까지 유지되고 있는가?”

    var hasConnected: Bool { get }
    var hasEnded: Bool { get }
    



    동사원형

    3인칭 단수 써라.


    • supports: ~을 지원하는가?
    • includes: ~을 포함하는가?
    • shows: ~을 보여줄것인가?
    • accepts: ~을 받아 주는가?
    • contains: ~을 포함하고 있는가?
    • returns: 리턴하는가?
    • preserves: 보존하는가?
    var supportsVideo: Bool //CXProviderConfiguration: 비디오를 지원하는가?
    var includesCallsInRecents: Bool //CXProviderConfiguration
    var showsBackgroundLocationIndicator: Bool //CLLocationManager
    var allowsEditing: Bool //CNContactViewController: 편집을 허용하는가?
    var acceptsFirstResponder: Bool //NSResponder
    var preservesSuperviewLayoutMargins: Bool //UIView
    var returnsObjectsAsFaults: Bool //NSFetchRequest
    


    (예외적으로 3인칭 단수 안 쓰는 경우도 있음) -> 주체에 따라


    변수 네이밍


    swift getter

    어떤 인스턴스를 리턴하는 함수나 메소드에 get 안씀 -> 바로 타입이름(명사)로

    func distance(from location: CLLocation) -> CLLocationDistance 
    


    결과를 바로 리턴하는 ’fetch’

    (오래 걸리지 않는 동기적 작업, 요청이 실패하지 않음)

    class func fetchAssets(withLocalIdentifiers identifiers: [String], options: PHFetchOptions?) -> PHFetchResult<PHAsset>
    


    유저에게 요청하거나 작업이 실패할 수 있을 때 ‘Request’

    func requestData(for resource: PHAssetResource, options: PHAssetResourceRequestOptions?, dataReceivedHandler handler: @escaping (Data) -> Void, completionHandler: @escaping (Error?) -> Void) -> PHAssetResourceDataRequestID
    


    작업의 단위가 클로져나 Request로 래필 되어있으면 ‘Perform’ or ‘excute’

    //NSManagedObjectContext
    func perform(_ block: @escaping () -> Void)
    
    //CNContactStore
    func execute(_ saveRequest: CNSaveRequest) throws
    


    XXXError

    // public error인경우
    enum [모듈이름]Error {} 
    
    // internal error인경우
    [구체적]error
    




    전치사

    전치사 어떻게 사용할까


    전치사 의미 예시
    to 입력으로 제공된 데이터를 출력으로 반환합니다. add(x: Int, y: Int) -> Int
    from 입력으로 제공된 데이터를 가져옵니다. load(data: Data, from: URL)
    with 입력으로 제공된 데이터를 사용하여 작업을 수행합니다. sort(numbers: [Int], with: <#SortAlgorithm#>)
    on 입력으로 제공된 데이터에 작업을 수행합니다. draw(shape: Shape, on: Canvas)
    in 입력으로 제공된 데이터 내에서 작업을 수행합니다. iterate(over: [Int], in: { (number: Int) in ... })
    into 입력으로 제공된 데이터를 다른 데이터로 변환합니다. convert(number: Int, into: String)
    as 입력으로 제공된 데이터의 타입을 변경합니다. cast(value: Any, as: Int)
    by 입력으로 제공된 데이터를 사용하여 작업을 수행합니다. filter(numbers: [Int], by: { (number: Int) in number % 2 == 0 })
    for 입력으로 제공된 데이터의 모든 항목에 대해 작업을 수행합니다. forEach(numbers: [Int], for: { (number: Int) in ... })



    전치사 사용 전에 끊어라

    func chapterMetadataGroups(withTitleLocale locale: Locale, containingItemsWithCommonKeys)
    func determineCompatibleFileTypes(withCompletionHandler handler: ([String]) -> Void)
    





    Ref)
    (개인 블로그 내용은 원작자의 허락을 구했습니다.)
    https://soojin.ro/blog/naming-boolean-variables
    https://soojin.ro/blog/english-for-developers-swift
    https://forums.swift.org/t/guidelines-first-argument-labels-prepositions-inside-the-parens/1369/18
    https://github.com/apple/swift-3-api-guidelines-review/commit/da7e512cf75688e6da148dd2a8b27ae9efcb8821?diff=split

  • Hash Collection Deep Dive



    Swift의 Dictionary, Set 등 HashCollection이 실제로 어떻게 구현되어있을까? 내가 알고있던 cs 지식들과 똑같이 구현되어 있을까 아니면 다르게 구현되어 있을까. 좀 더 Swift의 정확한 이해를 위해 오늘은 실제 Swift 구현부들을 조사해 보았다. 조사한 방법은 내 평소 코드에서 단순 HashCollection에 대해 읽고 쓰는 부분들이 어떻게 타고 타고 가서 실행되는지 구현 코드들을 살펴보고 이해했다.



    Dictionary



    1. updateValue()를 하면 무슨일이 일어날까



    var dict: [Int: String] = [1: "2"]
    
    let oldValue = dict.updateValue("3", forKey: 1)
    print(oldValue) // Optional("2")
    


    평소에 updateValue 함수를 쓰지 않더라도 dictionary를 업데이트 하는 경우는 많을 것이다. 첫번째로는 이부분 들이 어떻게 실행되고 있는지 한번 이해해보자.



    -> Dictionary._Variant로 이동
    ( _Variant는 Dictionary가 가지고 있는 구조체를 뜻한다. 안에 변수로 _NativeDictionary를 가지고 있다. )
    -> _NativeDictionary.updateValue()로 이동

     @inlinable
      internal mutating func updateValue(
        _ value: __owned Value,
        forKey key: Key,
        isUnique: Bool
      ) -> Value? {
        let (bucket, found) = mutatingFind(key, isUnique: isUnique) // 키에 해당하는 버킷을 찾기
        if found {
          let oldValue = (_values + bucket.offset).move() // 예전값은 deinitialize 후
          (_values + bucket.offset).initialize(to: value) // 새로운 값을 업데이트
          return oldValue // 이전 값 반환
        }
        _insert(at: bucket, key: key, value: value) // 존재하지 않을 시 해당 버킷 삽입
        return nil
      }
    



    그리고서는 위의 주석과 같이 코드를 실행 해준다. If found {} 부분을 보면 왜 Dictionary에서 쓰던 updateValue() 리턴값이 OldValue였는지 이해할 수 있다. 지금은 key에 해당하는 버킷을 못찾았다 가정하고 _insert 구현부로 바로 이동해 보겠다.

     @inlinable
      internal func _insert(
        at bucket: Bucket,
        key: __owned Key,
        value: __owned Value) {
        _internalInvariant(count < capacity)
        hashTable.insert(bucket) // hash table에 butcket bit 삽입
        uncheckedInitialize(at: bucket, toKey: key, value: value) // 메모리에 key value 저장
        _storage._count += 1 // storage count 하나 늘리기
      }
    



    _insert에서는 hashTable에 bucket bit를 하단 코드와 같이 uncheckedInsert()해주어 hashTable이 가지고 있는 words라는 메모리값 array에 bit를 삽입한다.



    -> HashTable

     @inlinable
      @inline(__always)
      internal func insert(_ bucket: Bucket) {
        _internalInvariant(!isOccupied(bucket)) // 버켓이 이미 테이블에 있는지 확인
        words[bucket.word].uncheckedInsert(bucket.bit) // 버켓의 비트위치에 해당하는 비트를 해시 테이블에 삽입
      }
    


    word: 8바이트(64비트)로 이루어진 행


    그리고서는 uncheckedInitialize()를 통해 Bucket의 offset에 key 또는 value의 값을 더한 주소에 값을 넣어준다.

    @inlinable // FIXME(inline-always) was usableFromInline
      @inline(__always)
      internal func uncheckedInitialize(
        at bucket: Bucket,
        toKey key: __owned Key,
        value: __owned Value) {
        defer { _fixLifetime(self) }
        _internalInvariant(hashTable.isValid(bucket))
        (_keys + bucket.offset).initialize(to: key)
        (_values + bucket.offset).initialize(to: value)
      }
    




    2. 그럼 우리가 저장한 이 값을 어떻게 찾을까?


    먼저 Dictionary에 subscript로 get {}이 발생했을 경우를 제목과 같다고 가정하고 살펴보겠다.
    앞에서와 같이 _variant란 객체를 통해 lookup을 실행한다.

    @inlinable
      public subscript(key: Key) -> Value? {
        get {
          return _variant.lookup(key)
        }
        set(newValue) {
          if let x = newValue {
            _variant.setValue(x, forKey: key)
          } else {
            removeValue(forKey: key)
          }
        }
        _modify {
          defer { _fixLifetime(self) }
          yield &_variant[key]
        }
      }
    



    -> Dictionary._Varian
    -> _NativeDictionary : DictionaryBuffer

    이부분도 역시 방금봤던 케이스와 같이 _NativeDictionary 이동한다. 여기서 눈여겨봐야할점은 _NativeDictionary가 DictionaryBuffer라는 프토코콜을 채택하고 있다는 점이다. DictionaryBuffer에는 lookup()이라는 함수가 정의되어있어. _NativeDictionary에 구현부가 있다.



    • Lookup implementation


     @inlinable
      @inline(__always)
      func lookup(_ key: Key) -> Value? {
        if count == 0 {
          // Fast path that avoids computing the hash of the key.
          return nil
        }
        let (bucket, found) = self.find(key) // << find key 로 이동 
        guard found else { return nil }
        return self.uncheckedValue(at: bucket)
      }
    



    @inlinable
      @inline(__always)
      internal func find(_ key: Key) -> (bucket: Bucket, found: Bool) {
        return _storage.find(key)
      }
    



    -> __RawDictionaryStorage

    타고타고 들어온 __RawDictionaryStorage 에서는 key와 key가 가지고 있는 hash value에 이 딕셔너리 인스턴스에 사용되는 seed를 넣어 find를 돌린다.


    @_alwaysEmitIntoClient
      @inline(never)
      internal final func find<Key: Hashable>(_ key: Key) -> (bucket: _HashTable.Bucket, found: Bool) {
        return find(key, hashValue: key._rawHashValue(seed: _seed))
      }
    


    그리고 드디어 여기서 임시 버켓을 하나 만들어 이 버켓이 hashTable에 속하는지 + bucket의 key가 내가 찾으려고 하는 key와 일치하는지 판별한 후 값을 결과에 따라 버켓과 함께 리턴한다.



    @_alwaysEmitIntoClient
      @inline(never)
      internal final func find<Key: Hashable>(_ key: Key, hashValue: Int) -> (bucket: _HashTable.Bucket, found: Bool) {
          let hashTable = _hashTable
          var bucket = hashTable.idealBucket(forHashValue: hashValue) /// hashvalue를 가지고 있는 bucket하나를 임시로 만들고
          while hashTable._isOccupied(bucket) { // hashtable이 이 버켓을 점유하고 있다면 while문을 타게 한다.
            if uncheckedKey(at: bucket) == key { // 메모리에서 버켓을 통해 동일한 키값을 찾는다면 리턴한다.
              return (bucket, true)
            }
            bucket = hashTable.bucket(wrappedAfter: bucket) // &+ 1로 버켓 돌려주기
          }
          return (bucket, false)
      }
    



    ++) uncheckedKey(): 구현부 keys 저장되어 있는 메모리 지역 접근 후 offset으로 key 리턴

    @_alwaysEmitIntoClient
      @inline(__always)
      internal final func uncheckedKey<Key: Hashable>(at bucket: _HashTable.Bucket) -> Key {
        defer { _fixLifetime(self) }
        _internalInvariant(_hashTable.isOccupied(bucket))
        let keys = _rawKeys.assumingMemoryBound(to: Key.self)
        return keys[bucket.offset]
      }
    
    




    Set



    Set에는 어떻게 구현이 되어있을까?



    신기한 점은 Set 또한 hashTable을 사용하여 lookup을 하기 때문에 유사하게 구현되어 있는걸 발견할 수 있다.

      @inlinable
      public func contains(_ member: Element) -> Bool {
        return _variant.contains(member)
      }
    


    여기서는 해당 메소드를 정의하는 (DictionaryBuffer 대신) _SetBuffer가 존재하고 (_NativeDictionary 대신) _NativeSet이 이걸 구현하고 있다.

    @inlinable
      @inline(__always)
      internal func find(_ element: Element) -> (bucket: Bucket, found: Bool) {
        return find(element, hashValue: self.hashValue(for: element))
      }
    


    하단 메소드를 보면, 어디서 많이본 메소드 형태이다. 방금 Dictionary에서 쓰던 find함수 구현 내용과 동일하게 _NativeSet에서도 구현할 수 있는것을 알 수가 있다.
    여기서 알 수 있는건 set도 이렇게 “Hash lookup을 통해 값을 find할 수있구나” 와 그래서 “contain이 O(1)에 실행이 되었구나.” 를 알 수가 있다.

     @inlinable
      @inline(__always)
      internal func find(
        _ element: Element,
        hashValue: Int
      ) -> (bucket: Bucket, found: Bool) {
        let hashTable = self.hashTable
        var bucket = hashTable.idealBucket(forHashValue: hashValue)
        while hashTable._isOccupied(bucket) {
          if uncheckedElement(at: bucket) == element {
            return (bucket, true)
          }
          bucket = hashTable.bucket(wrappedAfter: bucket)
        }
        return (bucket, false)
      }
    




    +) Hash Table , hash value 저장


    해쉬테이블을 검색하면 해당 그림이 가장 먼저 나온다. 개인적으로 이 그림은 조금 오해의 소지가 있다고 본다.

    image



    당연히 이론적으로는 해쉬테이블의 인덱싱원리를 잘 표현하였다. 하지만, 이번에 Hash Collection들을 까보면서 느끼는 것은 조금 다르다. 이전에는 그림과 같이 내가 dictionary에 key를 대입하면 hash func가 돌아서 hash value가 나와서 그값으로 인덱싱을 하는걸로 이해했었지만, 애초에 swift에서는 Hashable한 값들만을 key로 받는다. 즉, 애초에 HashValue를 가진 키를 가지고 인덱싱을 하는거다.



    그리고 좀 더 구체적으로 보면 hash value를 만들때도 해쉬 함수자체 로만 값을 만드는 것이 아니라 swift에서는 충돌성을 낮춰주기 위해 seed라는 값을 받아 xor을 돌려 엄청난 난수를 만든 후 그 값으로 해쉬함수를 돌린다.


    internal struct _State {
        // "somepseudorandomlygeneratedbytes"
        private var v0: UInt64 = 0x736f6d6570736575
        private var v1: UInt64 = 0x646f72616e646f6d
        private var v2: UInt64 = 0x6c7967656e657261
        private var v3: UInt64 = 0x7465646279746573
        // The fields below are reserved for future use. They aren't currently used.
        private var v4: UInt64 = 0
        private var v5: UInt64 = 0
        private var v6: UInt64 = 0
        private var v7: UInt64 = 0
    
        @inline(__always)
        internal init(rawSeed: (UInt64, UInt64)) {
          v3 ^= rawSeed.1
          v2 ^= rawSeed.0
          v1 ^= rawSeed.1
          v0 ^= rawSeed.0
        }
      }
    




    +)Ref
    https://github.com/apple/swift