<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Ryan-Son.devlog</title>
        <link>https://velog.io/</link>
        <description>합리적인 해법 찾기를 좋아합니다.</description>
        <lastBuildDate>Sun, 10 Oct 2021 07:43:20 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Ryan-Son.devlog</title>
            <url>https://velog.velcdn.com/images/ryan-son/profile/a78296c6-7636-43b7-bd0f-0ff50a43fb41/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Ryan-Son.devlog. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ryan-son" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[WWDC17] Behind the Scenes of the Xcode Build Process]]></title>
            <link>https://velog.io/@ryan-son/WWDC17-Behind-the-Scenes-of-the-Xcode-Build-Process</link>
            <guid>https://velog.io/@ryan-son/WWDC17-Behind-the-Scenes-of-the-Xcode-Build-Process</guid>
            <pubDate>Sun, 10 Oct 2021 07:43:20 GMT</pubDate>
            <description><![CDATA[<p>Xcode의 빌드 과정을 알아보겠습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/2070f0ec-4add-40a1-9128-c017643aec33/image.png" alt=""></p>
<h1 id="다룰-내용">다룰 내용</h1>
<ol>
<li>빌드 과정의 구조</li>
</ol>
<ul>
<li>Xcode가 빌드 과정을 모델링하고 조직하기 위해 프로젝트 파일 정보를 사용하는 방법</li>
</ul>
<ol start="2">
<li>컴파일러 영역</li>
</ol>
<ul>
<li>Clang과 Swift가 소스 코드를 오브젝트 파일로 빌드하는 방법</li>
<li>헤더와 모듈의 작동 방법</li>
<li>Swift 컴파일 모델이 기초적으로 C, C++, Objective-C와 다른 점</li>
</ul>
<ol start="3">
<li>링커</li>
</ol>
<ul>
<li>심볼들이 작성된 소스코드와 어떻게 연관되어 있는지</li>
<li>링커가 컴파일러가 생성한 오브젝트 파일을 받아 애플리케이션 또는 프레임워크와 같이 최종적으로 실행가능하게 이어 붙이는 방법</li>
</ul>
<p><code>PetWell</code>이라는 샘플 앱을 사용하여 이 과정을 알아봅니다. 이 앱은 애완동물 사진을 보여주는 앱입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/e5622455-58cb-4439-bf84-d3a1cc96b22b/image.png" alt=""></p>
<h1 id="build-process">Build process</h1>
<p>아래 <code>PetWall</code> 프로젝트에는 앱 타겟, 프레임워크와 Swift 및 Objective-C로 작성된 소스코드 파일이 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/3d73c789-5ede-4d70-9e7c-9ffa0bdce6f4/image.png" alt=""></p>
<p>앱을 빌드할 때 소스 크드와 리소스에서 사용자에게 배포하기 위해 앱 스토어에 업로드하는 패키지로 변환하기까지 여러 단계를 거쳐야합니다. </p>
<ol>
<li>소스코드를 컴파일하고 링크</li>
<li>Headers, asset catalogues, storyboards와 같은 리소스들을 복사하고 처리</li>
<li>코드사인 또는 API 문서화, 코드 린팅, 검증 도구 사용을 위해 shell script 작업</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/5a22ae39-b666-4c02-84b7-ee0aea715975/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ryan-son/post/485ef008-db01-40c3-af63-7f6491383bd9/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ryan-son/post/02e06cce-3fb5-4dbe-8a60-5855efe22a76/image.png" alt=""></p>
<p>대부분의 이러한 작업들 Clang, LD, AC tool, IB tool, Code sign 등과 같은 커맨드 라인 도구들이 수행합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/dc3d527a-554d-41c3-aac8-4066acc95753/image.png" alt=""></p>
<h2 id="빌드-과정">빌드 과정</h2>
<blockquote>
<p>빌드 작업들은 종속성 순 (dependency order)으로 실행된다.</p>
</blockquote>
<p>이러한 작업들은 Xcode 프로젝트에 따라 특정한 순서로 처리되어야 하지만, 빌드 시스템이 빌드를 수행할 때마다 조직화 작업을 자동으로 수행해줍니다.</p>
<p>빌드 작업이 실행되는 순서는 작업이 소비하는 인풋들과 작업이 생산하는 아웃풋들과 같이 작업의 종속정 정보들로 결정됩니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/f83fd1a9-fde7-4af4-b767-1accd6d1610f/image.png" alt=""></p>
<p>예를 들어, 컴파일 작업은 <code>PetController.m</code>과 같은 소스코드 파일을 소비하여 <code>PetController.o</code>와 같은 오브젝트 파일을 아웃풋으로 만듭니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/201ec1c4-06a4-41e1-959f-a16904a28b8d/image.png" alt=""></p>
<p>유사하게 링커 작업은 이전 작업으로 컴파일러가 만든 다수의 오브젝트 파일을 소비하여 .app 번들에 들어갈 <code>PetWall</code> 과 같은 실행 가능한 파일 (executable)이나 라이브러리를 아웃풋으로 만듭니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/1785ddb7-e199-4be4-91e4-2b533a75d8ba/image.png" alt=""></p>
<p>여기에서 패턴이 등장하는 것을 알 수 있습니다. 궁극적인 실행 순서를 나타내는 아래의 그래프 구조를 통해 종속성 정보가 흐르는 방법을 확인하실 수 있습니다. </p>
<p>이렇게 그래프에 나타낸 컴파일 작업은 차선과 유사합니다. 컴파일 작업들이 그들의 차선 안에서 완전히 독립적이기에 병렬적으로 실행될 수 있음을 알 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/4a00d40c-a3e8-4bbe-846d-3fceb549d73e/image.png" alt=""></p>
<p>그리고 링커 작업이 다른 모든 것들을 인풋으로써 받아들이기에 마지막에 와야하는 것도 알 수 있습니다. 이렇게 빌드 시스템이 종속성 정보를 통해 작업이 실행되어야 하는 순서와 어떤 작업들이 병렬적으로 실행되어야 하는지 결정하는 것을 종속성 순서 (dependency order)라 합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/53d27283-3ee9-46ad-af06-b6135a111387/image.png" alt=""></p>
<h2 id="빌드-과정의-작동-방법">빌드 과정의 작동 방법</h2>
<p>빌드를 누르면 Xcode 프로젝트 파일로부터 빌드 상세 (build description)을 받아 구문 해석 (parse)하고 프로젝트의 모든 파일, 타겟들, 종속성 관계, 빌드 설정들을 고려합니다. 그리고는 이를 directed graph라 부르는 트리와 유사한 구조로 변환합니다. Directed graph는 프로젝트에 있는 인풋과 아웃풋 파일들의 종속성들과 이들을 처리하기 위해 실행될 작업들을 나타냅니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/3c3c333b-ac87-462f-b577-bada38cbad81/image.png" alt=""></p>
<p>다음으로 Low-level execution engine이 이 그래프를 처리하여 종속성 상세사항 (dependency specifications)을 살펴보고 실행할 작업을 알아냅니다. 실행될 순서나 병렬적으로 실행될 작업들을 파악하여 이들을 실행합니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/77ab4b14-85c7-4c58-8bb5-0c6b0e21a0cd/image.png" alt=""></p>
<p>빌드 시스템의 Low-level build execution engine은 <code>llbuild</code>라 하며, 오픈 소스 프로젝트이기에 GitHub에서 소스코드를 확인하실 수 있습니다.</p>
<h2 id="discovered-dependencies">Discovered dependencies</h2>
<p>현재로써는 종속성 정보가 많지는 않기에 빌드 시스템이 작업 실행 과정 동안 더 많은 정보들을 찾아야 합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/803e8100-1a68-4c5f-a514-41e8b3b15af9/image.png" alt=""></p>
<p>예를 들어, Clang이 Objective-C 파일을 컴파일하면 예상하는대로 오브젝트 파일을 만듭니다. 하지만 해당 소스 파일에 포함된 헤더 파일 목록이 포함된 다른 파일을 생성할 수도 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/20b70657-a698-4d47-8650-5d686f43b468/image.png" alt=""></p>
<p>그러면 다음에 빌드할 때 빌드 시스템은 이 파일의 정보를 사용하여 포함된 헤더 파일이 변경되었는지 확인하여 필요하면 소스 파일을 다시 컴파일합니다. </p>
<p>그리고 <code>PetController.h</code>, <code>PetController.d</code>, <code>.n</code>을 통해 <code>.o</code> 파일 까지의 종속성 경로를 볼 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/bdc2b800-7cb1-4985-908c-a8fa9e6d47a3/image.png" alt=""></p>
<h2 id="증분-빌드-incremental-builds">증분 빌드 (Incremental Builds)</h2>
<p>지금까지 빌드 시스템의 주요 업무인 작업을 실행하는 방법에 대해 다루었습니다. 당연히 프로젝트가 커질수록 빌드 과정이 더 길어집니다. 하지만 빌드할 때마다 이런 작업들을 수행하지는 않습니다.</p>
<p>대신에 빌드 시스템은 그래프 상의 작업들의 일부만을 실행합니다. 이전 빌드로부터 변경한 사항에 따라 빌드가 실행되는 것이죠. 이를 증분 빌드 (Incremental build)라 하는데, 증분 빌드가 올바르고 효율적으로 작동하기 위해서는 정확한 종속성 정보를 가지고 있는 것이 매우 중요합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/a19dc71f-9dc4-4cdc-ac25-c0f71bf5dda7/image.png" alt=""></p>
<h3 id="change-detections-and-task-signatures">Change Detections and Task Signatures</h3>
<ul>
<li>빌드 그래프의 각 작업은 signature를 가지고 있음 (해시값)</li>
<li>signature은 인풋들의 상태 정보 (파일 경로, 수정 시간)와 기타 작업 메타데이터 (사용된 컴파일러의 버전) 로부터 계산됨</li>
<li>빌드 시스템은 작업의 현재와 이전 빌드의 시그니처를 추적</li>
<li>빌드가 수행될 때 재수행 여부를 결정 (signature가 다르면 재수행, 같으면 건너뜀)</li>
</ul>
<h2 id="빌드-시스템을-돕기-위한-방법">빌드 시스템을 돕기 위한 방법</h2>
<p>빌드 프로세스는 특정한 순서로 이루어진다고 했지만 이 순서를 조직하는 일은 빌드 시스템이 할 일이므로 관심 대상에서 제외하겠습니다.</p>
<p>대신에 개발자로서 작업들 간의 종속성을 고려하여 빌드 시스템이 그래프의 구조에 따라 최선의 방법으로 실행하는 방법을 알려주는데 집중해봅시다. 이를 통해 빌드 시스템이 작업의 순서를 올바르게 결정하고 멀티코어 하드웨어를 최대한 활용하기 위해 작업을 병렬화할 수 있을 것입니다.</p>
<h3 id="종속성">종속성</h3>
<ol>
<li><p>빌드 시스템 내에서의 빌드 방식 (Built in)
빌드 시스템은 컴파일러, 링커, 애셋 카탈로그 및 스토리보드 프로서스 등에 대한 규칙을 가지고 있습니다. 그리고 이러한 규칙은 어떤 종류의 파일이 인풋으로 허용되고 어떤 출력이 생성되는지 정의합니다.
<img src="https://images.velog.io/images/ryan-son/post/9427d737-e4a5-4eb7-8f94-e95f4c901a61/image.png" alt=""></p>
</li>
<li><p>타겟 종속성 (Target dependencies)
타겟 종속성은 타겟들이 빌드될 대략적인 순서를 결정합니다. 어떤 경우에 빌드 시스템은 다른 타겟들과 병렬 소스들을 컴파일할 수 있습니다. 이전에 Xcode에서 타겟이 빌드될 때 전체 종속 대상의 컴파일을 완료해야 시작될 수 있었던 점과 대조됩니다.</p>
</li>
</ol>
<p>현재의 Xcode 빌드 시스템에서 타겟은 더 빨리 빌드를 시작할 수 있습니다. 이는 소스 컴파일 단계가 비용 없이 일부 병렬화를 제공하기 위해 더 일찍 시작할 수 있음을 의미합니다. 하지만 실행 스크립트 단계 (run script phases)를 사용하는 경우 이 병렬화가 적용되기 전에 해당 스크립트 단계를 완료합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/c3049fff-98f8-4867-bf3b-1f8c49c16d23/image.png" alt=""></p>
<ol start="3">
<li><p>암시적 종속성 (Implicit dependencies)
암시적 종속성은 타겟 종속성과 다소 관련이 있습니다. 예를 들어, 바이너리 빌드 단계가 있는 링크 라이브러리의 타겟을 나열하고 스키마 편집기에서 암시적 종속성이 활성화된 경우 (기본값이 활성화 상태), 빌드 시스템은 타겟 종속성에 나열되지 않은 경우에도 해당 타겟에 대한 암시적 종속성을 설정합니다.</p>
</li>
<li><p>빌드 단계 종속성 (Build phase dependencies)
타겟 편집기에는 헤더 복사, 소스 컴파일, 번들 리소스 카피 등과 같이 수많은 빌드 단계가 있습니다. </p>
</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/615cf4a2-6edc-4477-8079-f1b35ef799bd/image.png" alt=""></p>
<p>이러한 단계와 관련된 작업은 일반적으로 단계가 나열된 순서에 따라 실행되는 그룹입니다. 그러나 빌드 시스템이 더 잘 알고 있는 경우 해당 순서를 무시할 수 있습니다. <code>Link Binary With Libraries</code>를 <code>Compile Sources</code> 전에 위치시킨다든지 하는 경우에요. 빌드 단계 순서가 잘못되면 빌드 문제나 실패가 발생할 수 있으므로 종속성을 이해하고 빌드 단계가 올바른 순서가 되어 있는지 확인해야 합니다.</p>
<ol start="5">
<li>스킴 순서 종속성 (Scheme order dependencies)
스킴 설정에서 병렬 빌드 체크 박스를 활성화하면 더 나은 빌드 성능을 얻을 수 있으며 스킴에 있는 타겟 순서는 중요하지 않게 됩니다. 그러나 병렬 빌드를 비활성화하면 Xcode는 스킴의 빌드 작업에 나열된 순서대로 대상을 하나씩 빌드합니다. 타겟 종속성은 먼저 빌드할 대상을 결정할 때 여전히 더 높은 우선 순위를 갖고 있습니다. 그러나 그렇지 않으면 Xcode는 그 순서를 존중합니다. 이제 종속성을 올바르게 설정하지 않은 경우에도 예측 가능한 빌드 순서를 제공하므로 이를 사용하기를 희망할 수 있습니다. 그러나 이렇게하면 병렬화가 되지 않기에 빌드 속도가 느려집니다. 따라서 빌드 병렬화 체크 발스를 활성화된 상태로 두고 타겟 종속성을 올바르게 설정하여 순서에 의존하지 않게 만드는 것이 좋습니다.</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/2875982d-a198-4005-a7d7-3fc64cb3390b/image.png" alt=""></p>
<ol start="6">
<li>커스텀 빌드 스크립트 단계
마지막 종속성 정보는 개발자가 설정한 내용이 있을 수 있습니다. 커스텀 셸 스크립트 빌드 단계나 빌드 규칙을 만든다면 빌드 시스템에게 인풋과 아웃풋이 무엇인지 알려주어야 합니다. 이는 빌드 시스템이 불필요한 작업 스크립트를 재실행하는 것을 피할 수 있도록 해주고 올바른 순서로 실행했는지를 판단하게 해줄 것입니다. 이러한 파일들의 경로는 스크립트에서 환경 변수로 사용할 수 있습니다.</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/19b62c8b-eb24-4e7e-891e-1c538f99b7ad/image.png" alt=""></p>
<hr>
<h2 id="avoid-auto-link-for-project-dependencies">Avoid Auto-Link for Project Dependencies</h2>
<p>프로젝트의 타겟 종속성 auto-link에 의존하지 마세요. 이 설정을 사용하면 링크 라이브러리의 빌드 단계에서 명시적으로 링크하지 않고도 가져온 모듈에 해당하는 프레임워크에 컴파일러가 자동으로 링크할 수 있습니다. 그러나 자동 링크는 빌드 시스템 수준에서 해당 프레임워크에 대한 종속성을 설정하지 않는다는 점에 유의하여야 합니다. 따라서 연결을 시도하기 전에 의존하는 대상이 실제로 구축된다는 보장은 없습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/0a15f7b1-b8b4-4932-823c-852832a2ad39/image.png" alt=""></p>
<h2 id="add-explicit-dependencies">Add Explicit Dependencies</h2>
<p>따라서 플랫폼 SDK의 프레임워크에 대해서만 이 기능에 의존해야 합니다. Foundation 및 UIKit과 마찬가지로 빌드가 시작되기 전에 이미 존재한다는 것을 알고 있기 때문입니다. 자체 프로젝트의 타겟에 대해 명시적으로 라이브러리의 종속성을 추가해야 합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/0b82bbef-3597-40b3-9a82-d3f438998d66/image.png" alt=""></p>
<h2 id="create-workspace-and-project-references">Create Workspace and Project References</h2>
<p>의존하는 다른 프로젝트의 타겟을 나타내기 위해 다른 Xcode 프로젝트를 프로젝트의 파일 탐색기로 끌어다 놓아 프로젝트 참조를 생성할 수도 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/db75add4-691a-4c5a-b3b6-618c1fd97884/image.png" alt=""></p>
<p>결론적으로, 정확한 종속성 정보를 사용하면 빌드 시스템이 빌드를 더 잘 병렬화할 수 있고 매번 일관된 결과를 얻을 수 있으므로 빌드 시간을 줄이고 개발에 더 많은 시간을 할애할 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/633d09b5-afad-4bfd-b500-ee0a38e5ffb0/image.png" alt=""></p>
<h1 id="clang-builds">Clang Builds</h1>
<p>살펴볼 두 가지 기능들:</p>
<ol>
<li>Header maps (Xcode 빌드 시스템과 Clang 컴파일러 간의 정보 소통을 위해 사용)</li>
<li>Clang modules (빌드 가속을 위해 사용)</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/612c03b6-684b-4414-8663-a40296a16597/image.png" alt=""></p>
<p>현재 Swift만 사용하시는 분들도 계시겠지만, Swift는 내부적으로 Clang을 사용합니다. </p>
<h2 id="clang">Clang</h2>
<p>Clang은 Apple의 공식 C 컴파일러이자 C, C++ 및 대부분의 프레임워크에 사용되는 Objective-C와 같은 C 언어군을 위한 컴파일러입니다. 앞서 말씀드린 바와 같이 컴파일러는 여러 인풋 파일을 받아 하나의 출력 파일을 생성한 다음 추후 링커가 이를 사용합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/9780d40b-5dbd-4182-9e56-5fd7f723d660/image.png" alt=""></p>
<p>iOS API들에 접근하거나 구현한 코드에 접근하고자 한다면 헤더 파일을 포함해야 합니다. 헤더 파일은 약속이라고 할 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/e0c3a56d-936e-4903-b3c6-c4792fa706f3/image.png" alt=""></p>
<p>어딘가에 이것의 구현부가 존재한다는 것을 약속하는 것입니다. 물론 구현부 파일만 업데이트하고 헤더 파일을 업데이트하지 않는다면 약속을 깨뜨리는 것입니다. 컴파일러가 약속을 신뢰하기 때문에 컴파일 시에는 break가 일어나지 않습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/3fbe3328-adf4-4b6b-a368-4dc965ef40da/image.png" alt=""></p>
<p>이는 링크 중에 break가 일어납니다. 컴파일러는 하나 이상의 헤더 파일을 가지고 있으며 이는 모든 컴파일러의 호출을 야기합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/df6c6e25-08a7-42d0-8274-e15a48e272a1/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ryan-son/post/65240bf0-c52f-4730-b5ca-033dbda82aa0/image.png" alt=""></p>
<p>예시 앱을 통해 헤더 파일을 다루는 방법을 알아보겠습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/5c5359d0-4ae7-43e7-b9e2-986b14efc0d0/image.png" alt=""></p>
<p>PetWall은 여러 언어가 공존하는 애플리케이션입니다. 애플리케이션 자체는 Swift로 작성되어 있으며 Objective-C로 작성된 프레임워크를 사용하고 있습니다. 그리고 C++로 작성된 support library를 가지고 있습니다. 시간이 지나 애플리케이션이 성장하여 파일을 찾기 용이하도록 다시 조직하였다고 해봅시다. cat과 연관된 파일들을 모두 하위 폴더에 옮기는거죠. 그래도 구현부 파일을 변경할 필요는 없습니다.</p>
<h3 id="clang이-헤더-파일을-찾는-방법">Clang이 헤더 파일을 찾는 방법</h3>
<p>Clang은 헤더 파일을 어떻게 찾을까요? 간단한 예시를 보시겠습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/97611e53-d6e7-4e7e-b31d-80300e9fcda9/image.png" alt=""></p>
<p>아래 코드는 구현부 파일 중 하나로 <code>cat.h</code>라 부르는 헤더 파일을 포함하였습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/2403af8b-f23a-4cc7-a6ab-d54bdef34db4/image.png" alt=""></p>
<p>Clang이 무엇을 하는지 파악하려면 빌드 로그를 살펴보면 됩니다. 빌드 로그를 살펴보면 특정한 파일을 컴파일할 때 Xcode 빌드 시스템이 무엇을 하였는지 알 수 있습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/052487e4-0e7f-4b13-a641-69de58fad0ca/image.png" alt=""></p>
<p>이를 verbose를 의미하는 <code>-v</code> 옵션을 붙여 터미널에 붙여넣으면 Clang이 많은 정보를 알려줄 것입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/16d3badd-262c-4d3c-90e8-3617aea42db9/image.png" alt=""></p>
<p>이제 헤더맵(headermaps)을 살펴보겠습니다.</p>
<p>헤더맵은 헤더 파일들의 위치를 알기 위해 Xcode 빌드 시스템이 사용하는 것입니다. 이제 가장 중요한 두 헤더맵 파일을 보겠습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/80b9acd2-264d-45fa-ae3c-3c95eb0dd2fa/image.png" alt=""></p>
<p>처음 두 엔트리들은 프레임워크 이름을 헤더에 넣어줍니다. 이 두 헤더들은 공개 (public) 헤더들입니다. 이 기능에 의존하면 안됩니다. 그 이유는 기존 프로젝트가 계속 작동하도록 유지하고 있을 때 Clang 모듈에 문제가 있을 수 있으므로 자체 프레임워크에서 공개 또는 비공개 헤더 파일을 포함할 때 항상 프레임워크 이름을 지정하는 것이 좋습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/c087eca8-2484-4c90-96f6-c045753a34af/image.png" alt=""></p>
<p>세 번째 엔트리는 프로젝트 헤더입니다. 이 경우에 필수는 아닙니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/88062360-9516-4499-817f-5e34110a11d2/image.png" alt=""></p>
<p>헤더맵의 목적은 소스코드로 포인트 백 (point back) 해주는 것이니까요. 공개 및 비공개 헤더들로 동일한 작업을 수행합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/44d8e5c9-bf41-42e1-9f3a-1d886b81a4cf/image.png" alt=""></p>
<p>Clang이 소스코드 경로에 있는 파일들이 유용한 에러와 경고 메시지들을 만들고 빌드 디렉토리의 다른 위치에 있을 수 있는 잠재적 복사본이 아닌 파일을 생성할 수 있도록 소스코드로 포인트 백 시킵니다.</p>
<h3 id="헤더맵과-관련된-흔한-프로젝트-이슈">헤더맵과 관련된 흔한 프로젝트 이슈</h3>
<ul>
<li>프로젝트에 헤더를 넣지 않음 (헤더는 프로젝트의 일부가 아님)<ul>
<li>이는 소스 경로에 있지만 프로젝트 자체는 아니라 프로젝트에 추가하여야 함</li>
</ul>
</li>
<li>헤더를 같은 이름으로 지음<ul>
<li>같은 이름이면 서로를 가리게 되므로 겹치지 않는 고유의 이름을 지어야 함 </li>
</ul>
</li>
</ul>
<p>이는 시스템 헤더에도 적용됩니다. 프로젝트에 시스템 헤더와 동일한 이름을 가진 로컬 헤더가 있으면 시스템 헤더를 가리게 되므로 이 상황을 피해야 합니다.</p>
<h3 id="시스템-헤더를-찾는-방법">시스템 헤더를 찾는 방법</h3>
<p>아래는 PetWall의 다른 예시인데, 여기에 SDK에 있는 <code>Foundation.h</code> 헤더 파일을 넣었습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/40f5ddab-0a68-4019-8e12-e0f4ab0f6924/image.png" alt=""></p>
<p>자신의 헤더 파일을 찾을 때 이전에 했던 것과 같은 일을 할 수 있지만, 현재 우리는 시스템 헤더를 찾고 있습니다. 헤더맵은 자체적인 헤더만을 대상으로 하기에 이를 무시할 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/1b1f18df-436f-445c-8435-62c104f71e2a/image.png" alt=""></p>
<p>포함하고 있는 경로에 초점을 맞춰보면, 기본적으로 SDK에 있는 두 개의 경로를 볼 수 있습니다. 첫 번째는 사용자가 넣은 것, 시스템 라이브러리 프레임워크가 넣은 것입니다. 이는 일반적인 포함 경로입니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/ba178ce0-9e0f-4642-912f-82c46a17470c/image.png" alt=""></p>
<p>사용자측에 <code>Foundation/Foundation.h</code> search term을 넣어서는 결과가 없어 찾을 수 없습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/f7f695f1-c96b-49f2-a428-dc8e5e462b14/image.png" alt=""></p>
<p>시스템 라이브러리 프레임워크에서는 작동 방식이 사용자측과 달리 프레임워크의 종류와 존재 여부를 식별한 후 헤더 파일의 헤더 경로를 찾습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/fb3db105-f9cf-47b0-afcf-228e8bd11677/image.png" alt=""></p>
<p>이 때 헤더 파일이 존재하지 않는다면 비공개 헤더 경로에서 헤더 파일을 찾습니다. Apple은 SDK에 비공개 헤더를 넣지 않지만, 다른 프레임워크들은 공개 및 비공개 헤더들을 넣을 수 있습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/9c405aee-fe96-4038-8d61-6870e7a6406b/image.png" alt=""></p>
<p>헤더 파일을 찾는데 실패했지만 더 이상 경로를 탐색하게 하지 않습니다. 프레임워크를 이미 찾은 상태이기 때문이죠. 프레임워크를 찾았으므로 프레임워크 경로에 헤더가 있을 것이라 예상합니다. 하지만 이를 찾지 못하면 탐색을 중지합니다. </p>
<p>모든 헤더들이 넣어지고 전처리가 된 후 구현부 파일의 생김새가 궁금하다면 Xcode가 전처리 파일을 만들게 하면 됩니다. 이는 매우 큰 아웃풋 파일을 만들 것입니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/61c51e3b-a501-432e-a4ef-e02b75950d23/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ryan-son/post/b4ff0ca6-51e4-4b9d-971f-290bcd1a4f74/image.png" alt=""></p>
<p><code>Foundation.h</code>는 시스템에서 매우 기초적인 헤더입니다. 직접적으로 넣거나 다른 헤더 파일을 통해 간접적으로 넣는 방식으로 많이 적용되고 있습니다. 이는 컴파일러가 매번 이 헤더를 찾기 위해 호출한다는 것을 의미합니다.</p>
<p>Clang은 한 줄의 include 구문을 위해 800 개 이상의 헤더 파일을 탐색하여 처리해야 합니다. 이는 구문 분석되고 검증되어야할 소스코드가 9 MB가 넘는다는 것입니다. 그리고 이러한 일이 컴파일러가 호출될 때마다 일어난다는 것이죠. 이는 많은 일이며 불필요한 일입니다.</p>
<p>이를 개선시킬 수 있는 방법 중 하나는 미리 컴파일된 (precompiled) 헤더 파일을 사용하는 것입니다. 하지만 더 좋은 것이 있는데, 이것이 Clang modules입니다.</p>
<h2 id="clang-modules">Clang Modules</h2>
<p>Clang 모듈은 프레임워크당 헤더를 한 번 찾아 구문분석한 후 이 정보를 디스크에 저장하여 캐시된 상태로 재사용될 수 있도록 만들어 빌드 시간을 개선합니다.</p>
<p>이를 위해 Clang 모듈은 특정 속성을 가져야 합니다. </p>
<h3 id="context-free">Context-free</h3>
<p>그 중 하나가 context-free입니다. 아래 두 코드들에 <code>PetKit</code> 모듈을 임포트하였지만 그 전에 두 가지 다른 매크로 정의가 넣었습니다. 이 헤더들을 임포트하는데 기존 모델을 사용했다면 이들이 포함되었다는 것을 의미합니다. 전처리기는 이 정의를 따라 헤더 파일을 적용할 것입니다. 하지만 이렇게 하면 모듈들은 각 헤더 케이스에 따라 다를 것이므로 재사용할 수 없습니다. 그래서 모듈들을 사용하려면 이렇게 해서는 안됩니다. 대신에 모듈이 context와 관련된 모든 정보들을 무시하도록하여 모든 구현부 파일에 재사용될 수 있도록 합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/97ee1a82-b048-45c8-a006-b8e63a3052ae/image.png" alt=""></p>
<h3 id="self-contained">Self-contained</h3>
<p>또 다른 하나의 요건은 모듈이 self-contained하여야 한다는 것입니다. 이는 모든 종속성을 나타내야 한다는 것입니다. 임포트 작업을 위해 어떠한 추가 헤더 파일도 추가할 걱정을 하지 않아도 되며 한 번 모듈을 임포트하면 작동한다는 의미입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/f62266a0-705f-406f-b5ef-5690006dcba3/image.png" alt=""></p>
<p>그럼 Clang은 모듈을 빌드 여부를 어떻게 알까요? <code>NSString.h</code>의 사례를 살펴봅시다. 먼저 Clang은 프레임워크에서 특정 헤더를 찾습니다. <code>Foundation.framwork</code> 경로죠. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/f3a69355-1855-4bc2-b731-55fffa68397a/image.png" alt=""></p>
<p>다음으로 Clang 컴파일러는 헤더의 경로에 상대적인 모듈 경로와 모듈 맵을 찾습니다. 모듈 맵은 특정 헤더 파일들의 집합이 모듈로 변환되는 방식을 나타냅니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/83529cc7-2779-4324-8ed5-3bcc941b3ca5/image.png" alt=""></p>
<p>Foundation이라는 모듈 이름과 어떤 헤더들이 이 모듈의 일부인지 나타내고 있습니다. 여기에는 <code>Foundation.h</code> 헤더라는 하나의 모듈만 가지고 있는데 이는 <code>umbrella</code> 키워드를 가진 특수한 헤더입니다. 이는 <code>NSString.h</code>이 모듈의 일부인지 알아내기 위해 Clang이 이 특정한 헤더 파일을 살펴보아야 한다는 것입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/b40906c3-4ddd-4c17-8f24-36ccfacffae1/image.png" alt=""></p>
<p>이제 <code>NSString.h</code>가 foundation 모듈의 일부임을 알았습니다. Clang은 텍스트 임포트를 모듈 임포트로 업그레이드할 수 있으며 이를 위해 foundation 모듈을 빌드해야 합니다. 그렇다면 Foundation 모듈을 어떻게 빌드할까요? 우선 이를 위해 별도의 Clang 위치를 만듭니다. 그리고 이 Clang 위치는 foundation 모듈의 모든 헤더 파일들을 가지고 있습니다. 우리는 원래 (original) 컴파일러 호출에서 기존 context를 전달하지 않았으므로 context-free입니다.</p>
<p>실제로 전달해야할 것은 Clang에 전달한 커맨드 라인 전달인자입니다. foundation 모듈을 빌드하는 동안 프레임워크 자체는 추가적인 프레임워크를 포함하므로 이 모듈 또한 빌드해야 합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/97db6e9c-7a5c-419a-aff2-37181a452e9e/image.png" alt=""></p>
<p>하지만 여기에서 이점을 찾을 수 있습니다. 임포트한 것들 중 일부는 동일할 수 있으므로 해당 모듈을 재사용하면 되는 것입니다. 이러한 모듈은 모듈 캐시라고 하는 디스크에 저장됩니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/0e5758e2-a2b5-44ec-be1b-2fd44e220a97/image.png" alt=""></p>
<p>앞서 언급하였듯이 커맨드 라인 전달인자들은 모듈을 만들 때 전달되는데, 이는 이 전달인자들이 모듈의 내용에 영향을 미칠 수 있다는 것을 의미합니다.</p>
<p>결과적으로 이 모든 전달인자들을 해시하고 이러한 특정 컴파일러 호출을 위해 생성한 모듈을 해당 해시와 일치하는 경로에 저장해야 합니다. 다른 제한 파일에 대한 컴파일러 전달인자를 변경하는 경우, 예를 들어 enable cat이라고 하면 이는 다른 해시이며, Clang이 해당 해시와 일치하는 해당 디렉토리에 모든 모듈의 인풋들을 다시 빌드해야 합니다. 따라서 모듈 캐시를 최대한 재사용하기 위해서는 전달인자를 동일하게 유지해야 합니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/d958fe58-db2a-40b1-919d-2801003be98c/image.png" alt=""></p>
<p>시스템 프레임워크의 모듈을 찾아 빌드하는 방법은 알았는데 자체적인 프레임워크는 어떨까요? 기존 cat 예시로 돌아가봅시다. 이번에는 모듈을 켰습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/b2878057-102e-4c9d-a862-263f6e6a77b7/image.png" alt=""></p>
<p>만약 헤더맵을 다시 사용한다면, 헤더맵은 소스 경로로 포인트 백 시킬 것입니다. 하지만 소스 경로를 보면 문제가 있습니다. 모듈 경로가 없거든요. 프레임워크 같아 보이지도 않기 때문에 Clang은 이 경우 어떻게 해야할지 모를 것입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/9207a0ca-f563-4284-bd7a-1a9686a3e849/image.png" alt=""></p>
<p>이 문제를 해결하기 위해 Clang의 Virtual File System이라 부르는 새로운 개념을 소개합니다. 이는 Clang이 모듈을 빌드할 수 있도록 프레임워크의 가상 추상화결과물을 만듭니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/04595ade-3824-437e-b324-dadb7c885292/image.png" alt=""></p>
<p>하지만 추상화 결과물은 경로로 파일을 포인팅시키기 때문에 다시 Clang은 소스코드에 대한 에러를 만들 것입니다. 앞서 말씀 드렸듯이 프레임워크 이름을 명시하지 않으면 이슈가 발생할 수 있습니다. 어떤 문제가 발생할 수 있는지 예시를 보여 드리겠습니다. 두 개의 임포트가 있습니다. 첫 번째는 <code>PetKit</code> 모듈이며 두 번째는 <code>PetKit</code> 모듈의 일부이지만 프레임워크 이름을 명시하지 않았기에 Clang은 알아채지 못할 것입니다. 이 경우 중복 정의 에러가 발생할 수 있습니다. 동일한 헤더를 임포트하면 발생하는 것이죠.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/5877208e-e417-4acf-80bc-c03845b54b1d/image.png" alt=""></p>
<p>Clang은 이러한 이슈를 해결하기 위해 내부적으로 바쁘게 작동합니다. 하지만 고칠 수 없죠. 이제 context를 바꾸는 조그마한 변화를 주어봅시다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/3bc003ba-1396-4361-8388-e31ee4231f89/image.png" alt=""></p>
<p>모듈 임포트는 context를 무시하기 때문에 이에 영향받지 않을 것입니다. cat 임포트는 여전히 이 변경 사항을 관찰할 헤더의 텍스트 임포트일 뿐입니다. </p>
<p>중복 정의가 없을 수 있지만 이제 모순된 정의를 가질 수 있습니다. 이 문제는 Clang이 해결할 수 없는 일이죠. 그래서 앞서 말씀 드렸듯이 항상 공개 또는 비공개 헤더를 임포트할 때 프레임워크 이름을 명시하기를 권장 드립니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/e3def3be-3121-457d-a88e-6ec58ed930cf/image.png" alt=""></p>
<h1 id="swift-builds">Swift Builds</h1>
<p>Swift와 빌드 시스템이 프로젝트에서 선언한 것들을 찾기 위해 협업하는 모습을 살펴보겠습니다.</p>
<p>이전 내용을 잠시 돌이켜보면, Clang은 각 Objective-C 파일을 따로 컴파일한다고 했습니다. 이 말은 다른 파일에 있는 클래스를 참조하고 싶다면 해당 클래스가 선언된 헤더를 임포트 해야 한다는 것입니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/e708bbac-caf6-4735-9e7f-bd7c768d901a/image.png" alt=""></p>
<p>하지만 Swift는 헤더를 작성할 필요가 없도록 설계되었습니다. 이는 초보자들이 언어를 시작하는 것을 용이하게 해주고 별도의 파일에 동일한 선언을 반복하는 것을 방지하게 해주죠. 하지만 이는 컴파일러가 추가적으로 장부 관리 작업 (bookkepping)을 수행해야 한다는 것이기도 합니다. 이제 장부 관리 작업이 이루어지는 방식을 살펴보도록 하겠습니다.</p>
<p><code>PetWall</code> 앱으로 돌아가 봅시다. 앱에는 ViewController에 Swift로 작성된 view, Objective-C AppDelegate, Swift unit test들이 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/8674cded-0266-430e-92cd-31afadf9def7/image.png" alt=""></p>
<p><code>PetViewController</code>라는 최상단에 위치한 파일을 컴파일하기 위해 컴파일러는 네 개의 다른 작업을 수행하여야 합니다.</p>
<ul>
<li>Swift 타겟과 Objective-C로부터 오는 두 가지에서 선언문들을 찾아야 합니다. </li>
<li>Objective-C와 다른 Swift 타겟들에서 선언문들을 찾아 사용할 수 있게끔 파일의 내용을 설명하는 인터페이스를 만들어야 합니다.</li>
</ul>
<p><img src="https://images.velog.io/images/ryan-son/post/377c8906-5a33-48d4-ab0f-88318aa51ca6/image.png" alt=""></p>
<p>예시를 통해 더 자세히 살펴보겠습니다. <code>PetViewController.swift</code>를 컴파일할 때, 컴파일러는 호출 가능 여부를 판단하기 위해 PetView의 이니셜라이저의 타입을 탐색합니다. 하지만 이 작업을 하기 전에 이니셜라이저 선언이 제대로 되어 있는지 확인하기 위해<code>PetView.swift</code>를 구문 분석하여 검증할 필요가 있습니다. 컴파일러는 이니셜라이저의 구현부 (body)까지 확인할 필요는 없다는 것을 알기에 이 부분을 체크하지는 않지만 파일의 인터페이스 부분을 처리하기 위해 몇 가지 작업을 더 수행하여야 합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/be9f1cbf-6c5b-4308-962c-67c76e83e1a8/image.png" alt=""></p>
<p>Clang과는 달리 Swift 파일을 컴파일할 때 컴파일러는 타겟 안의 모든 Swift 파일을 구문 분석합니다. 이 파일들 중 일부가 인터페이스와 관련이 있는지를 판단하기 위해서죠.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/b1250286-eadc-4904-9389-342456d4ce58/image.png" alt=""></p>
<p>Xcode 9에서는 컴파일러가 각 파일을 따로 컴파일하여 증분 빌드 시 빌드를 반복했을 때 반복되는 작업이 있었습니다. 이는 파일들이 병렬적으로 컴파일 될 수 있도록 했지만 컴파일러가 반복적으로 각 파일을 파싱하도록 강요했습니다. 구현한 것으로 <code>.o</code>를 만들려고 하면 선언문을 찾기 위해 여러 번 인터페이스를 찾아야 했죠.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/6e1293e7-861d-43ad-b78a-287198a3a6ef/image.png" alt=""></p>
<p>Xcode 10에서는 이러한 간접 비용 (overhead)를 줄였습니다. 가능한 한 많은 작업을 공유하는 그룹으로 파일을 결합하여 수행합니다. 최대한의 병렬 처리도 여전히 사용할 수 있습니다. 이는 그룹 내에서 파싱 결과를 재사용하고 그룹 간에서만 반복 작업이 일어납니다. 그룹의 수는 일반적, 상대적으로 적으므로 증분 디버그 빌드를 상당히 빠르게 만들어 줄 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/0255a850-27ea-4125-ad9c-11cd2016a9dd/image.png" alt=""></p>
<p>이제 Swift 코드는 Swift 코드만을 호출하지 않습니다. Objective-C도 호출할 수 있죠. PetWall 예시 앱으로 돌아가보면, Objective-C로 작성된 UIKit과 같은 시스템 프레임워크도 있으니까요.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/b1d01cb0-21a6-476b-8f18-cbd83722467c/image.png" alt=""></p>
<p>Swift는 다른 많은 언어들과는 다른 접근 방식을 취합니다. 외부의 다른 함수 인터페이스를 제공할 필요도 없죠. 예를 들어 Objective-C API를 사용하려면 일반적으로 이에 대한 Swift 선언을 작성해야 하지만 Swift 컴파일러인 swiftc는 내부에 Clang의 많은 부분을 포함하고 이를 라이브러리로 사용하고 있기 때문에 이를 통해 Objective-C 프레임워크를 직접 가져올 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/ce55fb61-3e7f-4346-bf3d-2b1bd4d85246/image.png" alt=""></p>
<p>그럼 Objective-C 선언은 어디에서 올까요? 아래와 같이 임포터가 타겟의 타입에 따라 헤더를 찾습니다.</p>
<ul>
<li>어떤 타겟이라도 Objective-C 프레임워크를 임포트하면 해당 프레임워크의 Clang 모듈 맵을 통해 드러난 선언문을 찾습니다.</li>
<li>Swift와 Objective-C 코드가 혼합된 프레임워크에서는 임포터는 umbrella 헤더에서 선언문을 찾습니다.<ul>
<li>Umbrella 헤더는 공개 인터페이스를 정의합니다. 이 방식으로 프레임워크 내부의 Swift 코드는 동일한 프레임워크의 공개된 Objective-C 코드를 호출할 수 있습니다.</li>
</ul>
</li>
<li>애플리케이션과 unit test 번들에서는 타겟의 브리징 헤더 (bridging header)에 임포트들을 추가하여 선언이 Swift로 호출되도록 합니다. 이제 임포터가 선언을 가져오면 이들을 더 관용적으로 만들기 위해 변경합니다.</li>
</ul>
<p>예를 들어, Swift의 내장 에러 처리 언어 기능을 사용하기 위해 쓰로잉 메서드들로 <code>NSError</code>를 사용하는 Objective-C 메서드를 임포트할 수 있습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/7a78fe4e-746a-441e-93c6-ed7359041bc7/image.png" alt=""></p>
<p>이 과정에서 동사와 전치사 다음에 오는 매개변수 타입 이름을 제거합니다. 예를 들어, <code>drawPet atPoint</code> 메서드에서는 pet이라는 단어가 있습니다. 동사 draw 다음에 오는 Pet 타입의 매개변수, 그리고 마찬가지로 전치사 at 다음에 오는 CGPoint 타입의 매개변수에 대한 단어 point가 있습니다. 이 메서드를 Swift로 가져올 때 단순히 <code>draw(_:at:)</code>으로 생략되어 표현됩니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/88b97d2a-6472-4dae-b913-2185e74a6456/image.png" alt=""></p>
<p>이것은 어떻게 작동할까요? 컴파일러가 흔한 영어 동사와 전치사 목록을 가지고 있다는 것을 아시면 놀라실지도 모르겠습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/fe5990e4-a96d-4945-9563-ff3904476a6f/image.png" alt=""></p>
<p>하드 코딩된 목록이며 인간이 사용하는 언어이기에 지저분하여 종종 단어를 놓칠 때도 있습니다. 더욱이 Swift의 명명 규칙 (컨벤션)에 맞추기 위해 임포터는 품사를 기반으로 단어를 제거하여 메서드의 이름도 바꿉니다. 종종 목록에 단어가 없어 놓치는 경우도 있는데, 이 경우에는 <code>NS_SWIFT_NAME</code> 어노테이션을 이용하여 컴파일러에게 원하는 메서드를 임포트 해달라고 할 수 있습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/39a01e38-af9b-4a13-b800-66e0d71ff89b/image.png" alt=""></p>
<p>Objective-C 헤더가 Swift에 임포트 되는 방법을 알고 싶다면 Xcode의 related items 팝업을 보시면 됩니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/c514a0e0-f061-4a31-ab8a-8d8a5e83c90c/image.png" alt=""></p>
<p>이건 좌측 상단에서 찾을 수 있는데, generated interfaces를 선택하시면 다른 Swift 버전에서의 인터페이스를 볼 수 있습니다.</p>
<p>반대로 Objective-C가 Swift를 임포트하는 상황에서는 Swift가 임포트한 Objective-C의 헤더를 만듭니다. 이 방법으로 Swift에서 클래스를 작성한 것을 Objective-C가 호출해서 사용할 수 있는 것이죠.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/8c7b6c6a-c581-4662-9a8e-0630c0682306/image.png" alt=""></p>
<ul>
<li>컴파일러는 <code>NSObject</code>를 확장한 Swift 클래스와 <code>@objc</code> 어노테이션이 있는 요소들로부터 Objective-C 선언을 만듭니다.</li>
<li>유닛 테스트에 있는 앱의 경우 헤더는 공용 및 internal 선언 모두 포함하고 있게 됩니다. 이것이 앱의 Objective-C 부분으로부터 Swift를 사용할 수 있게 해줍니다.</li>
<li>프레임워크는 빌드 프로덕트에 포함되어 있고 프레임워크의 공용 인터페이스 부분이기 때문에 생성된 헤더가 공용 선언문만을 제공합니다.</li>
</ul>
<p><img src="https://images.velog.io/images/ryan-son/post/994ca01d-4b5a-4a55-a788-ba7071e8102a/image.png" alt=""></p>
<p>오른쪽에서는 컴파일러가 모듈 이름인 <code>PetWall</code>을 포함하는 Swift 클래스에 약간 변형된 이름에 Objective-C 클래스를 바인딩하는 것을 볼 수 있습니다. 
두 모듈이 동일한 이름의 클래스를 정의했을 때 런타임 충돌을 방지하기 위해 <code>@objc</code> 어노테이션을 통해 Objective-C 클래스에 다른 이름을 사용하도록 Swift에게 요청할 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/3af4a1e2-d22d-4d16-8e05-8d9fc649c33f/image.png" alt=""></p>
<p>이름이 충돌되지 않도록 하는 것은 개발자의 책임입니다. <code>@objc(name)</code>처럼 새로운 이름을 전달하여 충돌을 방지할 수 있습니다. </p>
<p><img src="https://images.velog.io/images/ryan-son/post/d7e4969b-cdec-49a9-833b-41edb19c0bad/image.png" alt=""></p>
<p>컴파일러는 다른 Swift 타겟들의 인터페이스를 만들 때도 유사한 접근 방식을 취합니다.</p>
<p>Swift에서 모듈은 배포 가능한 선언 단위를 의미합니다. 그리고 이러한 선언문을 이용하려면 모듈을 임포트해야 합니다. 예를 들어, Objective-C 모듈이나 XCTest 모듈을 가져올 수 있겠죠. </p>
<p>Xcode에서 각 Swift 타겟은 앱 타겟을 포함하여 별도의 모듈을 생성합니다. 이것이 유닛 테스트에서 테스트하기 위해 앱의 메인 모듈을 임포트해야하는 이유입니다. 모듈을 가져올 때 컴파일러는 사용할 때 타입을 확인하기 위해 특수한 Swift 모듈 파일을 역직렬화(deserialize)합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/f9251d8d-3e08-48b6-b8c6-5a821d2d4b4c/image.png" alt=""></p>
<p>예를 들어 아래의 유닛 테스트에서 컴파일러는 컨트롤러를 제대로 생성하는지 확인하기 위해 <code>PetWall</code> Swift 모듈의 일부인 <code>PetViewController</code>를 로드할 것입니다. 이것은 앞서 보여 드렸던 컴파일러가 타겟 내 선언문을 찾는 방식과 유사합니다. 컴파일러는 Swift 파일을 직접 파싱하지 않고 모듈을 요약하는 파일을 로드합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/800563e1-8491-465a-bc47-2f41faea120b/image.png" alt=""></p>
<p>컴파일러는 생성된 Objective-C 헤더처럼 많은 Swift 모듈 파일을 생성합니다. 하지만 텍스트가 아니라 바이너리 표현 방식을 사용합니다. 여기에는 Objective-C의 정적 인라인 함수 또는 C++의 헤더 구현과 같은 인라인이 가능한(inlineable) 함수의 구현부 (bodies)가 포함됩니다. 하지만 이는 비공개 선언문들의 이름과 타입을 포함하고 있습니다. 이렇게 하면 디버거에서 참조할 수 있어 정말 편리하지만 비공개 변수를 아무렇게나 지어서는 안된다는 것을 시사합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/d215eb8e-f163-4c29-99eb-c41b22a1514b/image.png" alt=""></p>
<p>증분 빌드의 경우 컴파일러는 부분적인 Swift 모듈 파일을 생성한 다음 전체 모듈의 내용을 나타내는 단일 파일로 병합합니다. 이 병합 프로세스를 통해 단일 Objective-C 헤더를 생성할 수도 있습니다. 이는 링커가 개체 파일을 단일 실행 파일로 합칠 때 수행하는 작업과 유사합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/9a240e82-19bf-49ad-910a-1f6dd4afc86b/image.png" alt=""></p>
<h1 id="linking">Linking</h1>
<p>Xcode 빌드 프로세스의 마지막 단계인 링커를 살펴봅시다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/95280922-179d-494b-8131-fc298def9f94/image.png" alt=""></p>
<h2 id="링커">링커</h2>
<ul>
<li>실행가능한 Mach-O를 빌드하는 마지막 작업</li>
<li>모든 컴파일러의 결과물(<code>.o</code> 파일)을 합쳐 하나의 파일로 만듦<ul>
<li>컴파일러가 생성한 코드를 이동하거나 수정함</li>
</ul>
</li>
<li>두 종류의 인풋 파일을 받음<ul>
<li>오브젝트 파일 (<code>.o</code>)</li>
<li>라이브러리 (<code>.dylib</code>, <code>.tbd</code>, <code>.a</code>)</li>
</ul>
</li>
</ul>
<h2 id="심볼">심볼</h2>
<ul>
<li>코드나 데이터의 조각을 참조하기 위한 이름</li>
<li>코드 조각들은 다른 심볼들을 참조할 수 있음 (다른 함수를 부르는 함수를 작성 등)</li>
<li>심볼은 링커의 작동 방식에 영향을 주는 속성을 가질 수 있음 (weak, availability symbol 등)</li>
<li>언어들은 데이터를 &quot;mangling&quot;하여 심볼로 인코딩함 (C++, Swift)</li>
</ul>
<h2 id="오브젝트-파일">오브젝트 파일</h2>
<ul>
<li>개별 컴파일러 액션의 결과물</li>
<li>코드와 데이터 조각을 가진 실행할 수 없는 Mach-O 파일 (missing된 일부분을 링커가 이어 붙여 고쳐야 함)<ul>
<li>각 조각은 심볼로 표현됨</li>
<li>조각들은 &quot;정의되지 않은&quot; 심볼들을 참조할 수 있음 (한 <code>.o</code> 파일의 함수가 다른 <code>.o</code> 파일의 함수를 참조할 때)</li>
</ul>
</li>
</ul>
<h2 id="라이브러리">라이브러리</h2>
<ul>
<li>타겟의 일부로서 빌드되지 않은 심볼들을 정의<ul>
<li>Dylibs: 동적 라이브러리<ul>
<li>실행 파일이 사용할 코드와 데이터 조각을 노출하는 Mach-O 파일</li>
</ul>
</li>
<li>TBDs: 텍스트 기반 동적 라이브러리 (Text Based Dylib Stubs)<ul>
<li>심볼만 가지고 있음</li>
</ul>
</li>
<li>정적 아카이브<ul>
<li>&quot;AR&quot; 툴으로 빌드된 .o 파일의 모음</li>
<li>참조하는 심볼이 있는 .o 파일만 앱에 포함됨</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="예시">예시</h2>
<p>아래 그림에서 AAC 사운드 파일이 될 &quot;purr.aac&quot;를 찾아볼 수 있지만 <code>purrFile</code>은 나타나지 않음. 이는 static이기 때문입니다 (nonexported name).</p>
<p><img src="https://images.velog.io/images/ryan-son/post/bf1e3c18-3cf9-4acf-bd7b-cba2e0713b77/image.png" alt=""></p>
<p>Cat purr는 아래와 같이 심볼이 만들어집니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/af367c78-7a4e-4e3b-820a-ea88309bb2c2/image.png" alt=""></p>
<p>이제 변수를 <code>playSound(_:)</code>에 전달합니다. 어셈블리어의 명령어가 두 개인데, 이는 &quot;purr.aac&quot;는 문자열 이름이라 실제 주소를 가지고 있지 않기 때문에 이후에 찾기 위함입니다. 링커가 들어와서 이후에 고칠 심볼릭 오프셋, 심볼릭 값 페이지와 페이지 오프를 남겨둡니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/0b62b06b-a6cf-4db1-85e7-5ba608567311/image.png" alt=""></p>
<p>마지막으로 이 문자열을 x0에 로드했으므로 <code>playSound(_:)</code>를 호출할 수 있습니다. <code>playSound(_:)</code>를 호출하지 않고 <code>__Z9playSoundPKc</code>를 호출하죠. 이는 &quot;mangle&quot;된 심볼을 나타내는건데, Objective-C++이면서 C++에도 동일한 playSound 함수가 있기 때문입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/83ca3b09-3837-4b0b-8ad7-d2fcd38336ae/image.png" alt=""></p>
<p>이제 <code>.o</code> 파일이 있습니다. 이를 통해 빌드 시스템은 모든 <code>.o</code> 파일들을 링커에 인풋으로 전달할 것입니다. 그리고 링커는 파일을 생성하기 시작합니다. 이 경우 <code>PetWall</code> 내부에 포함된 프레임워크인 <code>PetKit</code>을 빌드합니다. Text segment라 부르는 것을 만들 것입니다. Text segment는 애플리에키션에 대한 모든 코드를 보관하는 곳인데, 이번에는 <code>cat.o</code>를 가져와서 복사합니다. 두 부분으로 나눌 건데, 하나는 해당 문자열용이고, 또 다른 하나는 실행 코드용입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/2121a061-e477-4b98-b88f-23d0aec5aa99/image.png" alt=""></p>
<p>이제 이들의 절대 주소를 알고 있으므로 링커가 특정 오프셋에서 로드하도록 <code>cat.o</code>를 다시 작성할 수 있습니다. 명령어를 삭제하거나 만들 수 는 없기 때문에 두 번째 명령어를 아무것도 하지 않는 null로 대체합니다. 그런 다음 분기합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/7a45e693-eb38-4d86-bdcc-27e907dca0b1/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ryan-son/post/ea544a25-efdf-4ed3-b7e5-16453aa6026c/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ryan-son/post/b03c5566-27e9-442a-b775-f7e8cb39a8e3/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ryan-son/post/7d66c889-ec42-44ed-91c2-ad7723910d7a/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ryan-son/post/ebe3bb23-ca4c-48bf-bf53-d7b53e32d534/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[multipart/form-data 알아보기 (vs application/json)]]></title>
            <link>https://velog.io/@ryan-son/multipartform-data-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-vs-applicationjson</link>
            <guid>https://velog.io/@ryan-son/multipartform-data-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-vs-applicationjson</guid>
            <pubDate>Sun, 26 Sep 2021 12:03:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/c0cc4062-217c-49d4-8744-6d83b161f4b1/image.png" alt=""></p>
<h1 id="overview">Overview</h1>
<blockquote>
<p>*&quot;multipart/form-data&quot; content type은 파일, ASCII 형식이 아닌 데이터와 바이너리 데이터 제출(submit)에 사용된다.* - HTML 4.01 Specification (W3C)</p>
</blockquote>
<p><a href="https://www.ietf.org/rfc/rfc2045.txt">RFC2045</a>에 소개된 모든 multipart Multipurpose Internet Mail Extensions (MIME) 데이터 스트림의 규칙을 준수합니다.</p>
<p>하나의 multipart/form-data 메시지는 <a href="https://www.w3.org/TR/html401/interact/forms.html#successful-controls">successful control</a>이라 표현하는 여러 파트로 이루어져 있으며 각 파트는 기본값을 <code>text/plain</code>으로하는 <code>Content-Type</code>을 가질 수 있습니다.</p>
<p>각 파트는 아래 내용을 가져야 합니다.</p>
<ol>
<li><code>&quot;form-data&quot;</code> 값을 가지는 <code>&quot;Content-Dispoisition&quot;</code> 헤더</li>
<li>상응하는 control을 지칭하기 위한 control name</li>
</ol>
<pre><code class="language-swift">Content-Disposition: form-data; name=&quot;mycontrol&quot;</code></pre>
<h1 id="예시">예시</h1>
<p>HTML 4.01 Specification에서 설명한 내용은 어떻게 이루어져 있는지 아래 예시를 통해 확인하실 수 있습니다.</p>
<p>배경지식으로 각 파트에서 등장하는 <code>\r\n</code>는 Carriage Return (CR), Line Feed (LF)를 의미하는<code>CRLF</code>이며 아래를 의미합니다.</p>
<ol>
<li>Carriage Return (CR, <code>\r</code>): 현재 라인에서 가장 앞자리로 위치 이동</li>
<li>Line Feed(LF, <code>\n</code>): 커서의 위치를 바꾸지 않은 상태로 종이를 한 라인 위로 올림</li>
</ol>
<h2 id="initial-and-encapsulated-part">Initial and encapsulated part</h2>
<p>multipart/form-data의 첫 파트와 중간 파트는 고유 메시지임을 나타내는 <code>boundary</code>와 control name을 가진 <code>content-disposition</code>, 그리고 값을 가지고 있습니다.</p>
<pre><code class="language-swift">--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;price\&quot;\r\n\r\n

12000\r\n</code></pre>
<h2 id="final-part">Final part</h2>
<p>마지막 파트는 메시지의 종료를 나타내기 위해 <code>--</code> 문자로 끝을 냅니다.</p>
<pre><code class="language-swift">--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA--\r\n</code></pre>
<h2 id="entire-message">Entire message</h2>
<p>전체 메시지는 아래와 같이 구성되어 있습니다.</p>
<pre><code class="language-swift">--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;price\&quot;\r\n\r\n

12000\r\n
--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;currency\&quot;\r\n\r\n

KRW\r\n
--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;descriptions\&quot;\r\n\r\n

상품 상세 정보입니당~\r\n
--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;discounted_price\&quot;\r\n\r\n

10000\r\n
--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;password\&quot;\r\n\r\n

1234\r\n--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;images[]\&quot;; filename=\&quot;BD9B2CA6-F70D-46C5-AD02-540528441574.jpeg\&quot;\r\n
Content-Type: image/jpeg\r\n\r\n

(data)\r\n
--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;images[]\&quot;; filename=\&quot;1CA0A2CF-BBEC-49AC-946A-D9A0D4014DE2.webp\&quot;\r\n
Content-Type: image/webp\r\n\r\n

(data)\r\n
--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\nContent-Disposition: form-data; name=\&quot;title\&quot;\r\n\r\n

상품 이름이에요~!\r\n
--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA\r\n
Content-Disposition: form-data; name=\&quot;stock\&quot;\r\n\r\n

100\r\n
--ryanmarket.boundary-D5ECC632-707B-4D97-B046-5F69C4B342CA--\r\n
</code></pre>
<h1 id="vs-applicationjson">vs <code>application/json</code></h1>
<p><code>multipart/form-data</code>를 이용하는 주된 이유는 가장 먼저 말씀드린 바와 같이 <em>파일, ASCII 형식이 아닌 데이터와 바이너리 데이터</em>를 업로드할 수 있는 형태이기 때문인 것으로 판단됩니다. 그렇다면 주로 활용되는 <code>application/json</code> 형식으로는 파일을 업로드할 수 없을까요?</p>
<p>파일의 컨텐츠를 <code>base64</code> 인코딩한 후 아래와 같이 구성하여 메시지를 보내면 업로드할 수 있습니다.</p>
<pre><code class="language-json">EXAMPLE 9: Files
&lt;form enctype=&#39;application/json&#39;&gt;
  &lt;input type=&#39;file&#39; name=&#39;file&#39; multiple&gt;
&lt;/form&gt;

// assuming the user has selected two text files, produces:
{
    &quot;file&quot;: [
        {
            &quot;type&quot;: &quot;text/plain&quot;,
            &quot;name&quot;: &quot;dahut.txt&quot;,
            &quot;body&quot;: &quot;REFBQUFBQUFIVVVVVVVVVVVVVCEhIQo=&quot;
        },
        {
            &quot;type&quot;: &quot;text/plain&quot;,
            &quot;name&quot;: &quot;litany.txt&quot;,
            &quot;body&quot;: &quot;SSBtdXN0IG5vdCBmZWFyLlxuRmVhciBpcyB0aGUgbWluZC1raWxsZXIuCg==&quot;
        }
    ]
}</code></pre>
<h2 id="applicationjson"><code>application/json</code></h2>
<ul>
<li>장점:<ul>
<li>파일과 함께 전달된 데이터의 연관 관계를 표현하기 용이하다.</li>
</ul>
</li>
<li>단점: <ul>
<li>인코딩 방식의 특성으로 인해 바이너리 데이터보다 33-37 % 많은 용량을 차지한다.
(base64 인코딩 방식이 인코딩 시 바이트 당 8 비트를 차지하는 바이너리 방식과 달리 6 비트를 차지하여 이론상 8/6 = 1.33, 약 33 % 많은 용량을 차지하지만 실제로는 패딩 등의 이유로 3~4 % 더 차지함)</li>
<li>서버는 디코딩, 클라이언트는 인코딩을 위한 프로세싱 오버헤드가 부가된다.</li>
<li>인코딩한 파일 컨텐츠의 문자열이 길어져 서버측에서 파일을 읽지 못하는 경우가 발생할 수 있다.</li>
</ul>
</li>
</ul>
<h2 id="multipartform-data"><code>multipart/form-data</code></h2>
<ul>
<li>장점:<ul>
<li>파일 업로드 시 <code>application/json</code> 방식에서 발생하는 단점을 감수하지 않을 수 있다.</li>
</ul>
</li>
<li>단점:<ul>
<li>파일과 함께 전달된 데이터의 연관 관계 표현이 제한된다.</li>
</ul>
</li>
</ul>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2">HTML 4.01 Specification</a> - W3C</li>
<li><a href="https://stackoverflow.com/questions/4526273/what-does-enctype-multipart-form-data-mean">What does enctype=&#39;multipart/form-data&#39; mean?</a> - Stack overflow</li>
<li><a href="https://www.w3.org/TR/html-json-forms/">W3C HTML JSON form submission</a> - W3C Working Group Note</li>
<li><a href="https://lena-chamna.netlify.app/post/http_multipart_form-data/">HTTP multipart/form-data 이해하기</a> - 레나 참나 블로그</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Storyboard의 컴파일 과정 알아보기]]></title>
            <link>https://velog.io/@ryan-son/Storyboard-%EC%BB%B4%ED%8C%8C%EC%9D%BC-%EA%B3%BC%EC%A0%95-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@ryan-son/Storyboard-%EC%BB%B4%ED%8C%8C%EC%9D%BC-%EA%B3%BC%EC%A0%95-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 26 Sep 2021 11:57:19 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/ba06c8e3-0c25-41ec-b769-f365e12bd4b4/image.png" alt=""></p>
<blockquote>
<ul>
<li>ibtool이 storyboard 파일을 storyboardc 파일로 컴파일 (c: compiled).</li>
<li>storyboardc 파일은 view와 view의 관계 (IBAction, Segue 등)가 정의된 스토리보드를 바이너리 파일인 nib로 컴파일하며 스토리보드 내 view들의 고유 NIB 식별자 등의 정보를 Information Property List에 기록</li>
</ul>
</blockquote>
<h2 id="스토리보드-소스-파일">스토리보드 소스 파일</h2>
<p>Xcode를 통해 확인할 수 있는 스토리보드 파일의 구성은 아래와 같습니다.</p>
<h3 id="interface-builder---storyboard">Interface Builder - Storyboard</h3>
<p>스토리보드 파일을 클릭하면 볼 수 있는 흔한 화면 구성입니다. 화면과 화면 간의 관계를 시각적으로 표현하고 있어 화면 구성을 이해하기 용이합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/c21395b9-595e-42f1-a9d9-c3efb8e7bde6/image.png" alt=""></p>
<h3 id="source-code">Source Code</h3>
<p>스토리보드 파일을 우클릭하여 <code>Open As</code>, <code>Source Code</code>을 순차적으로 클릭하며 진입하면 다음과 같이 <code>XML</code> 형식으로 표현된 스토리보드를 확인하실 수 있습니다. 스토리보드의 내용을 수정하여 커밋하면 이 소스코드가 변경된다는 것을 gif diff 명령을 통해 확인하신 적이 있으실 것입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/4d1e07f3-f155-4662-87d5-fbd8da5d50e7/image.png" alt=""></p>
<h3 id="번외---xib">번외 - XIB</h3>
<p><code>XML Interface builder</code> 또는 <code>Xcode Interface builder</code>라고 불리는 XIB 파일도 마찬가지로 <code>XML</code> 형식으로 작성되어 있습니다. 스토리보드의 document type이 <code>&quot;com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB&quot;</code>, XIB 파일의 document type이 <code>&quot;com.apple.InterfaceBuilder3.CocoaTouch.XIB&quot;</code>으로 둘 모두 유사한 파일 형식임을 유추할 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/7b83389b-896d-40fe-b29a-a1fa55e2ed2d/image.png" alt=""></p>
<h2 id="build-phase">Build phase</h2>
<p>빌드 완료 시 <code>XIB</code> 파일은 바이너리 형식으로 변환되어 Next step Interface Builder를 의미하는 <code>NIB</code> 파일로 컴파일됩니다. <code>NIB</code> 형식으로는 사람이 읽고 이해할 수 있는 형태를 벗어나서 git과 같은 방식으로 소스 컨트롤을 하기 제한되어 <code>XML</code> 형식의 <code>XIB</code>가 도입되었다고 하네요.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/076e775b-86dd-4bd8-b3f8-cfe52a3cf2ba/image.png" alt=""></p>
<p>출처: <a href="https://dalgonakit.tistory.com/82">달망이 개발일기</a></p>
<p>빌드 후 앱 번들 패키지를 열어보시면 아래와 같이 구성되어 있는 것을 확인하실 수 있습니다. 개별 XIB로 작업한 내용은 스토리보드와 관계없이 NIB 파일로 컴파일되어 앱 번들 최상위 폴더에서 확인할 수 있네요.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/171c3096-da0e-459c-8c68-68e5a01c8faa/image.png" alt=""></p>
<p>다음은 메인 소스가 위치할 것으로 예상되는 <code>Base.lproj</code>를 열었을 때 확인할 수 있는 구성입니다. 스토리보드 파일이었던 <code>LaunchScreen</code>과 <code>Main</code>이 있네요.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/fafedbb8-9305-4186-b4ed-29804a57913e/image.png" alt=""></p>
<p>미리 말씀드렸듯이 컴파일된 <code>storyboardc</code> 패키지는 view와 view의 관계 (IBAction, Segue 등)로 이루어진 NIB 파일들을 가지고 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/db398ff4-4c53-4f9c-8d22-03ca09c22d08/image.png" alt=""></p>
<p>의미 없어보이는 <code>BYZ-38-t0r-view-8bC-Xf-vdC</code>와 같은 문자열도 의미가 있습니다. 다음 코드 일부를 통해 특정 ViewController의 최상위 view의 id를 지칭하는 것임을 파악할 수 있습니다.</p>
<p>마찬가지로 스토리보드의 요소인 <code>UINavigationController</code>, <code>UIViewController</code>들도 모두 무작위처럼 보이는 <code>id</code>를 키값을 가지고있으며 이러한 내용이 <code>storyboardc</code> 파일에 동봉된 <code>info.plist</code> 프로퍼티 리스트를 통해 확인할 수 있습니다.</p>
<pre><code class="language-swift">// Main.storyboard
...
&lt;viewController id=&quot;BYZ-38-t0r&quot; customClass=&quot;OpenMarketItemListViewController&quot; customModule=&quot;OpenMarket&quot; customModuleProvider=&quot;target&quot; sceneMemberID=&quot;viewController&quot;&gt;
    &lt;view key=&quot;view&quot; contentMode=&quot;scaleToFill&quot; id=&quot;8bC-Xf-vdC&quot;&gt;
...</code></pre>
<p><img src="https://images.velog.io/images/ryan-son/post/b95aeace-9be6-4552-86c4-b03644bac92e/image.png" alt=""></p>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://stackoverflow.com/questions/42043047/what-does-ibtool-do-in-the-compiling-and-linking-storyboards-phase-and-how-the-r">What does ibtool do in the compiling and linking storyboards phase and how the result of linked storyboards work with executable?</a> - Stack overflow</li>
<li><a href="https://dalgonakit.tistory.com/82">iOS의 화면을 그리기 위해선 이것이 필요합니다! XIB, NIB, Storyboard</a> - 달망이 개발일기</li>
<li><a href="https://soooprmx.com/nib-%ED%8C%8C%EC%9D%BC%EB%A1%9C%EB%B6%80%ED%84%B0-ui-%EA%B4%80%EB%A0%A8-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EB%A1%9C%EB%94%A9%ED%95%98%EA%B8%B0/#fn-7226-2-2">Nib 파일로부터 UI 관련 객체를 로딩하기</a> - Wireframe blog</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[nib과 xib의 차이]]></title>
            <link>https://velog.io/@ryan-son/nib%EA%B3%BC-xib%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@ryan-son/nib%EA%B3%BC-xib%EC%9D%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Sun, 26 Sep 2021 11:51:43 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/215799ef-5a85-42ed-b21f-34e96830c059/image.png" alt=""></p>
<blockquote>
<p>XIB를 바이너리 형식으로 컴파일하여 파일로 만든 것을 NIB라고 합니다.</p>
</blockquote>
<p>빌드 완료 시 <code>XIB</code> 파일은 바이너리 형식으로 변환되어 Next step Interface Builder를 의미하는 <code>NIB</code> 파일로 컴파일됩니다. 사람이 읽고 이해할 수 있는 형태가 아니라 <code>NIB</code> 형식으로는 git과 같은 방식으로 소스 컨트롤을 하기 제한된다는 단점이 있어 <code>XML</code> 형식의 <code>XIB</code>가 도입되었다고 하네요.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/076e775b-86dd-4bd8-b3f8-cfe52a3cf2ba/image.png" alt=""></p>
<p>이미지 출처: <a href="https://dalgonakit.tistory.com/82">https://dalgonakit.tistory.com/82</a></p>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://dalgonakit.tistory.com/82">iOS의 화면을 그리기 위해선 이것이 필요합니다! XIB, NIB, Storyboard</a> - 달망이 개발일기</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Safe area 정리]]></title>
            <link>https://velog.io/@ryan-son/Safe-area-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@ryan-son/Safe-area-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 26 Sep 2021 11:41:21 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/85083f5e-d478-4db9-a9fe-dadccba6f01c/image.png" alt="">
이미지 출처: <a href="https://wit.nts-corp.com/2019/10/24/5731">https://wit.nts-corp.com/2019/10/24/5731</a></p>
<blockquote>
<p>View controller의 view를 가리는 내비게이션 바, 탭 바, 툴바, 기타 요소를 제외한 영역</p>
</blockquote>
<h1 id="소개">소개</h1>
<p><code>Safe area</code>는 모든 인터페이스의 가시적인 부분에 view를 위치시키는 것을 도와줍니다. <code>UIKit</code>이 정의한 <code>View controller</code>는 컨텐츠 위에 내비게이션 컨트롤러의 내비게이션 바와 같이 특별한 view들을 컨텐츠 위에 위치시킬 수 있습니다. 이러한 view들이 일부 투명할 수 있지만, 그래도 view 아래에 일부 컨텐츠들을 가립니다. tvOS에서는 <code>safe area</code>가 화면 베젤을 의미하는 <code>overscan inset</code>을 포함합니다.</p>
<p>컨텐츠의 레이아웃을 결정할 때 각 view에서 접근할 수 있는 <code>safeAreaLayoutGuide</code>를 통해 <code>safe area</code>의 도움을 받을 수 있습니다. AutoLayout을 통해 제약을 설정하는 상황이 아니라면 <code>safeAreaInsets</code> 프로퍼티를 통해 raw inset values를 얻을 수 있습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/9dc18a39-6470-4f5d-af2e-5ead191872e5/image.png" alt=""></p>
<h1 id="custom-view를-포함하기-위해-safe-area를-확장하기">Custom View를 포함하기 위해 safe area를 확장하기</h1>
<p><code>Container view controller</code>는 <code>embedded child view controller</code>의 view를 통해 컨텐츠를 보여줄 수 있습니다. 이 상황에서 <code>container view</code>의 컨텐츠 view들이 이미 커버하고 있는 영역을 <code>child view controller</code>의 <code>safe area</code>에서 제외해주기 위해 업데이트하여야 하는데, UIKit <code>container view controller</code>는 컨텐츠 view들을 고려한 <code>child view controller</code>의 <code>safe area</code>를 미리 조정합니다. 예를 들어, 내비게이션 컨트롤러는 내비게이션 바를 고려하여 <code>child view controller</code>의 <code>safe area</code>을 확장합니다.</p>
<p><code>Embedded child view controller</code>의 <code>safe area</code>를 확장하려면 <code>additionalSafeAreaInsets</code> 프로퍼티를 수정하면 됩니다. 아래 그림과 같이 화면 아래와 오른쪽 끝에 커스텀 view들을 보여주겠다고 정의하였다면 <code>child view controller</code>의 컨텐츠가 커스텀 view들 아래에 위치하기 때문에 해당 view들을 고려하여 <code>child view controller</code>의 <code>safe area</code> 아래와 오른쪽의 inset을 설정하여야 합니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/733617d1-1d98-4a87-afed-5e974cea5b82/image.png" alt=""></p>
<p>아래 코드를 통해 <code>container view controller</code>의 <code>viewDidAppear(_:)</code> 메서드에서 커스텀 view들을 고려하여 <code>child view controller</code>의 <code>safe area</code>를 늘리는 방법을 확인하실 수 있습니다. view 계층에 view가 추가되기 까지 <code>safe area insets</code>가 정확하지 않으니 해당 메서드에서 수정 작업이 이루어져야 합니다.</p>
<pre><code class="language-swift">override func viewDidAppear(_ animated: Bool) {
   var newSafeArea = UIEdgeInsets()
   // Adjust the safe area to accommodate 
   //  the width of the side view.
   if let sideViewWidth = sideView?.bounds.size.width {
      newSafeArea.right += sideViewWidth
   }
   // Adjust the safe area to accommodate 
   //  the height of the bottom view.
   if let bottomViewHeight = bottomView?.bounds.size.height {
      newSafeArea.bottom += bottomViewHeight
   }
   // Adjust the safe area insets of the 
   //  embedded child view controller.
   let child = self.childViewControllers[0]
   child.additionalSafeAreaInsets = newSafeArea
}</code></pre>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://developer.apple.com/documentation/uikit/uiview/positioning_content_relative_to_the_safe_area">Positioning Content Relative to the Safe Area</a> - Apple Developer</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Design patterns in Swift]]></title>
            <link>https://velog.io/@ryan-son/Design-patterns-in-Swift</link>
            <guid>https://velog.io/@ryan-son/Design-patterns-in-Swift</guid>
            <pubDate>Mon, 20 Sep 2021 07:50:56 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/8296ec55-037b-41df-9cf6-d40e1a55721a/image.png" alt=""></p>
<p>Swift로 작성한 예시를 통해 디자인 패턴 적용 방법을 알아봅니다.</p>
<h1 id="contents">Contents</h1>
<table>
<thead>
<tr>
<th>Behavioral patterns</th>
<th>Creational patterns</th>
<th>Structural patterns</th>
</tr>
</thead>
<tbody><tr>
<td><a href="https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Strategy-pattern-in-Swift">Strategy</a></td>
<td><a href="https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Factory-pattern-in-Swift">Factory</a></td>
<td><a href="https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Adapter-pattern-in-Swift">Adaptor</a></td>
</tr>
<tr>
<td><a href="https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Template-method-pattern-in-Swift">Template Method</a></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<h1 id="참고자료">참고자료</h1>
<ol>
<li><a href="https://github.com/ochococo/Design-Patterns-In-Swift">Design Patterns in Swift</a> - ochococo GitHub Repository</li>
<li><a href="https://www.youtube.com/watch?v=lJES5TQTTWE">객체지향 디자인패턴 1</a> - 얄팍한 코딩사전</li>
<li><a href="https://www.youtube.com/watch?v=q3_WXP9pPUQ">객체지향 디자인패턴 2</a> - 얄팍한 코딩사전</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[디자인 패턴] Factory pattern in Swift]]></title>
            <link>https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Factory-pattern-in-Swift</link>
            <guid>https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Factory-pattern-in-Swift</guid>
            <pubDate>Mon, 20 Sep 2021 07:39:04 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/0f474073-80a3-40a0-aaf5-afeb6bf084b7/image.png" alt=""></p>
<p><a href="https://velog.io/@ryan-son/Design-patterns-in-Swift">목차로 돌아가기</a></p>
<h1 id="소개">소개</h1>
<p>팩토리 또는 팩토리 메서드 패턴 (Factory pattern or factory method pattern)은 생성 관점의 디자인 패턴 (Creational design patterns)의 일종으로, 인스턴스 생성을 팩토리 타입의 메서드에 위임하는 방식으로 개발자들에게 이용하는 타입에 대한 정보를 숨기고 인스턴스 생성을 용이하게 해줍니다.</p>
<h1 id="example">Example</h1>
<h2 id="가정">가정</h2>
<p>현재 개발 중인 앱에는 자주 사용되는 UI 요소들을 커스터마이징한 커스텀 UI 타입이 있습니다.</p>
<h2 id="패턴을-적용하지-않은-경우">패턴을 적용하지 않은 경우</h2>
<pre><code class="language-swift">import UIKit

final class MyAppLabel: UILabel {

    init(textColor: CGColor = UIColor.label.cgColor, backgroundColor: UIColor = .systemBackground) {
        self.textColor = textColor
        self.backgroundColor = backgroundColor
    }
}


final class MyAppButton: UIButton {

    init(textColor: CGColor, backgroundColor: UIColor = .systemBackground) {
        self.textColor = textColor
        self.backgroundColor = backgroundColor
    }
}

final class MyAppTextField: UITextField {

    init(textColor: CGColor = UIColor.label.cgColor, backgroundColor: UIColor) {
        self.textColor = textColor
        self.backgroundColor = backgroundColor
    }
}</code></pre>
<p>각 타입에는 이니셜라이저를 통해 기본으로 설정된 값도 있고, 그렇지 않은 경우도 있습니다. 개발자들은 UI를 개발하는 동안 모든 커스텀 타입에 대해 이러한 세부 사항을 기억하고 있어야 합니다. 현재는 <code>textColor</code>와 <code>backgroundColor</code>에 대한 사항만을 가정하고 있지만, 실제로는 크기 또는 여백 등 더 많은 커스터마이징 요소들이 포함되어 있을 수 있습니다. 팩토리 패턴이 적용되지 않은 현재는 아래와 같이 직접 UI 요소들을 초기화하여 사용하고 있습니다.</p>
<pre><code class="language-swift">let label = MyAppLabel()
let button = MyAppButton(textColor: UIColor.systemPink.cgColor)
let textField = MyAppTextField(backgroundColor: .systemTeal)</code></pre>
<p>하지만 개발자들이 모든 커스텀 타입에 대해 이러한 세부 사항을 기억하는 것은 쉽지 않은 일일 것입니다.</p>
<h2 id="패턴을-적용한-경우">패턴을 적용한 경우</h2>
<p>그래서 각 UI 요소들을 열거형을 통해 케이스를 정의하고, 프로토콜을 통해 외부에 제공해야할 인터페이스를 정의하도록 만들어 케이스별로 특정한 UI 요소를 지칭할 수 있는 환경을 만들어줍니다.</p>
<pre><code class="language-swift">import UIKit

enum UIComponent {
    case label, button, textField
}

protocol MyAppUIComponent {

    var component: UIComponent { get }
    var textColor: CGColor { get set }
    var backgroundColor: UIColor { get set }
}


final class MyAppLabel: UILabel, MyAppUIComponent {

    var component: UIComponent { .label }

    init(textColor: CGColor = UIColor.label.cgColor, backgroundColor: UIColor = .systemBackground) {
        self.textColor = textColor
        self.backgroundColor = backgroundColor
    }
}


final class MyAppButton: UIButton, MyAppUIComponent {

    var component: UIComponent { .button }

    init(textColor: CGColor, backgroundColor: UIColor = .systemBackground) {
        self.textColor = textColor
        self.backgroundColor = backgroundColor
    }
}

final class MyAppTextField: UITextField, MyAppUIComponent {

    var component: UIComponent { .textField }

    init(textColor: CGColor = UIColor.label.cgColor, backgroundColor: UIColor) {
        self.textColor = textColor
        self.backgroundColor = backgroundColor
    }
}</code></pre>
<p>계속해서 정의한 커스텀 UI 요소의 생성을 전문으로하는 팩토리 타입을 만듭니다.</p>
<pre><code class="language-swift">struct ComponentFactory {

    func make(_ component: UIComponent) -&gt; MyAppUIComponent {
        switch component {
        case .label:
            return MyAppLabel()
        case .button:
            return MyAppButton(textColor: UIColor.systemPink.cgColor)
        case .textField:
            return MyAppTextField(backgroundColor: .systemTeal)
        }
    }
}</code></pre>
<p>이제 팩토리 타입을 이용해 원하는 UI 요소를 케이스별로 생성하여 사용할 수 있게 되었습니다. 개발자들이 UI 요소를 생성할 때 많은 것을 알 필요가 없게 되었네요. </p>
<pre><code class="language-swift">let factory = ComponentFactory()

let label = factory.make(.label)
let button = factory.make(.button)
let textField = factory.make(.textField)</code></pre>
<p>새로운 커스텀 UI 타입을 정의한다고 하더라도 인스턴스 생성에 대한 세부 사항은 팩토리 타입에서 정의해주면 됩니다. 열거형의 케이스로 정의되어 있기 때문에 새로운 케이스에 대한 생성 방식을 팩토리 타입의 <code>make(_:)</code> 메서드에 정의하지 않으면 컴파일 에러가 발생하여 에러 발견이 더 용이해진다는 장점도 생겼습니다.</p>
<p><a href="https://velog.io/@ryan-son/Design-patterns-in-Swift">목차로 돌아가기</a></p>
<h1 id="참고자료">참고자료</h1>
<ol>
<li><a href="https://github.com/ochococo/Design-Patterns-In-Swift">Design Patterns in Swift</a> - ochococo GitHub Repository</li>
<li><a href="https://www.youtube.com/watch?v=q3_WXP9pPUQ">객체지향 디자인패턴 2</a> - 얄팍한 코딩사전</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[디자인 패턴] Template method pattern in Swift]]></title>
            <link>https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Template-method-pattern-in-Swift</link>
            <guid>https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Template-method-pattern-in-Swift</guid>
            <pubDate>Mon, 20 Sep 2021 06:31:04 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/7cd42ff7-f7ce-4aea-9b66-24f204b633c9/image.png" alt=""></p>
<p><a href="https://velog.io/@ryan-son/Design-patterns-in-Swift">목차로 돌아가기</a></p>
<h1 id="소개">소개</h1>
<p>템플릿 메서드 패턴 (Template method pattern)은 행동 관점의 디자인 패턴 (Behavioral design pattern)의 일종으로 실행 과정 전반이 동일한 타입의 세부 메서드들을 각 타입에서 구현하여 필요에 따라 유사하지만 다른 수행 로직을 가진 타입을 만드는 방식을 이릅니다.</p>
<h1 id="example">Example</h1>
<h2 id="가정">가정</h2>
<p>선택 모드에 따라 다른 지도 API를 사용하여 지도를 로드하는 경우를 살펴보겠습니다. 사용할 지도 API는 네이버, 카카오로 다르지만 지도를 불러올(초기화) 때 아래의 순서는 유지된다고 가정합니다.</p>
<ol>
<li>지도 서버 연결</li>
<li>지도를 화면에 표시</li>
<li>현재 위치로 이동</li>
</ol>
<h2 id="패턴-적용">패턴 적용</h2>
<p>지도를 초기화하는 메서드를 포함해 위에서 가정한 1, 2, 3에 맞는 기능을 외부에 제공할 인터페이스를 아래와 같이 프로토콜을 통해 정의합니다. 동시에 프로토콜 기본 구현 (Protocol default implementation)을 통해 구현할 기능이 의도한 순서대로 실행해주는 메서드를 구현합니다.</p>
<pre><code class="language-swift">protocol MapView {

    func connectMapServer()
    func showMapOnScreen()
    func moveToCurrentLocation()
    func initMap()
}

extension MapView {

    func initMap() {
        connectMapServer()
        showMapOnScreen()
        moveToCurrentLocation()
    }
}</code></pre>
<p>정의한 프로토콜을 준수하며 각 지도 API를 통해 지도를 불러올 타입을 정의합니다.</p>
<pre><code class="language-swift">struct NaverMapView: MapView {

    func connectMapServer() {
        print(&quot;네이버 지도 서버에 연결&quot;)
    }

    func showMapOnScreen() {
        print(&quot;네이버 지도 보여주기&quot;)
    }

    func moveToCurrentLocation() {
        print(&quot;네이버 지도에서 현재 좌표로 이동&quot;)
    }
}

struct KaKaoMapView: MapView {

    func connectMapServer() {
        print(&quot;카카오 지도 서버에 연결&quot;)
    }

    func showMapOnScreen() {
        print(&quot;카카오 지도 보여주기&quot;)
    }

    func moveToCurrentLocation() {
        print(&quot;카카오 지도에서 현재 좌표로 이동&quot;)
    }
}</code></pre>
<p>각 타입은 <code>MapView</code> 프로토콜이 기본 구현한 <code>initMap()</code> 메서드를 이미 가지고 있으므로 해당 메서드를 통해 지도를 불러오면 됩니다. 다른 지도 API를 통해 지도를 초기화한다고 하여도 이미 정의해둔 타입과 동일한 방식으로 <code>MapView</code> 프로토콜을 준수하는 타입을 정의하여 사용하면 됩니다.</p>
<p><a href="https://velog.io/@ryan-son/Design-patterns-in-Swift">목차로 돌아가기</a></p>
<h1 id="참고자료">참고자료</h1>
<ol>
<li><a href="https://github.com/ochococo/Design-Patterns-In-Swift">Design Patterns in Swift</a> - ochococo GitHub Repository</li>
<li><a href="https://www.youtube.com/watch?v=q3_WXP9pPUQ">객체지향 디자인패턴 2</a> - 얄팍한 코딩사전</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[디자인 패턴] Adapter pattern in Swift]]></title>
            <link>https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Adapter-pattern-in-Swift</link>
            <guid>https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Adapter-pattern-in-Swift</guid>
            <pubDate>Mon, 20 Sep 2021 05:59:09 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/050355ec-c611-43b2-ad2b-54c90a6d7e08/image.png" alt=""></p>
<p><a href="https://velog.io/@ryan-son/Design-patterns-in-Swift">목차로 돌아가기</a></p>
<h1 id="소개">소개</h1>
<p>어댑터 패턴은 구조형 디자인 패턴 (Structural design pattern)의 일종으로 인터페이스가 서로 다른 타입이 동일한 형식으로 작동할 수 있도록 만들어 줍니다.</p>
<h1 id="example">Example</h1>
<h2 id="가정">가정</h2>
<p>이전 포스트인 <a href="https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Strategy-in-Swift">전략 패턴</a>과 동일한 상황을 가정합니다. 검색 기능을 개발하고 있으며, 전체, 이미지, 뉴스, 지도 검색 기능이 이미 전략 패턴으로 구현되어 있습니다.</p>
<pre><code class="language-swift">final class SearchFeatureWithPattern {

    private var searchStrategy: SearchStrategy = SearchStrategyAll()

    func setSearchStrategy(to strategy: SearchStrategy) {
        searchStrategy = strategy
    }

    func searchButtonTapped() {
        searchStrategy.search()
    }
}

protocol SearchStrategy {
    func search()
}

struct SearchStrategyAll: SearchStrategy {
    func search() {
        print(&quot;전체 검색&quot;)
    }
}

struct SearchStrategyImage: SearchStrategy {
    func search() {
        print(&quot;이미지 검색&quot;)
    }
}

struct SearchStrategyNews: SearchStrategy {
    func search() {
        print(&quot;뉴스 검색&quot;)
    }
}

struct SearchStrategyMap: SearchStrategy {
    func search() {
        print(&quot;지도 검색&quot;)
    }
}</code></pre>
<p><a href="https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Strategy-in-Swift">전략 패턴 포스트</a>에서 확인하셨듯이 <code>SearchStrategy</code> 프로토콜을 준수하는 새로운 타입을 추가하는 방식으로 새로운 검색 전략을 추가할 수 있었습니다. 하지만 아래와 같이 다른 회사에서 작성된 검색 기능을 사용하고자 할 때는 어떻게 하여야 할까요?</p>
<pre><code class="language-swift">protocol FindAlgorithm {
    func find(global: Bool)
}

struct FindVideoAlgorithm: FindAlgorithm {
    func find(global: Bool) {
        print(&quot;\(global ? &quot;전체 &quot; : &quot;일부 &quot;)&quot; + &quot;동영상 찾기&quot;)
    }
}</code></pre>
<p>다른 회사에서 개발된 기능인 <code>FindVideoAlgorithm</code>은 해당 회사에서 사용했던 것으로 추정되는 <code>FindAlgorithm</code> 프로토콜을 따르고 있고, 전체 검색 범위에서 검색할 것인지 여부를 <code>global</code>이라는 매개변수에 <code>Bool</code> 타입의 인자를 전달하여 검색 작업을 수행합니다.</p>
<h2 id="패턴-적용">패턴 적용</h2>
<p><code>Adapter pattern</code>을 적용하면 이렇듯 기존 <code>SearchStrategy</code>와 다른 인터페이스를 가진 타입을 동일한 인터페이스를 사용하게끔 조정할 수 있습니다.</p>
<p>먼저, 콘센트 규격이 다른 해외에서 사용하는 어댑터와 같이 저희가 원하는 인터페이스를 제공해줄 수 있는 어댑터 타입을 정의합니다.</p>
<pre><code class="language-swift">struct SearchFindAdapter: SearchStrategy {
    private var findAlgorithm: FindAlgorithm

    init(findAlgorithm: FindAlgorithm) {
        self.findAlgorithm = findAlgorithm
    }

    func search() {
        // 내가 `search()`라고 말하면 너는 `find(global: true)`로 알아들으면 돼.
        findAlgorithm.find(global: true)
    }
}</code></pre>
<p>정의한 어댑터 타입은 이니셜라이저를 통해 변환 대상인 <code>FindAlgorithm</code> 타입의 인스턴스를 프로퍼티로 전달받아 <code>SearchStrategy</code> 인터페이스로 사용할 수 있도록 해줍니다.</p>
<p>어댑터를 활용하여 변환한 <code>FindVideoAlgorithm</code>은 아래와 같은 방식으로 사용할 수 있습니다.</p>
<pre><code class="language-swift">let searchFeatureWithPattern = SearchFeatureWithPattern()
let searchStrategyVideo = SearchFindAdapter(findAlgorithm: FindVideoAlgorithm())
searchFeatureWithPattern.setSearchStrategy(to: searchStrategyVideo)
searchFeatureWithPattern.searchButtonTapped()</code></pre>
<p><a href="https://velog.io/@ryan-son/Design-patterns-in-Swift">목차로 돌아가기</a></p>
<h1 id="참고자료">참고자료</h1>
<ol>
<li><a href="https://github.com/ochococo/Design-Patterns-In-Swift">Design Patterns in Swift</a> - ochococo GitHub Repository</li>
<li><a href="https://www.youtube.com/watch?v=lJES5TQTTWE">객체지향 디자인패턴 1</a> - 얄팍한 코딩사전</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[디자인 패턴] Strategy pattern in Swift]]></title>
            <link>https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Strategy-pattern-in-Swift</link>
            <guid>https://velog.io/@ryan-son/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Strategy-pattern-in-Swift</guid>
            <pubDate>Sat, 18 Sep 2021 16:33:22 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/290744f3-5a7b-4871-8be7-43f324939b8e/image.png" alt=""></p>
<p><a href="https://velog.io/@ryan-son/Design-patterns-in-Swift">목차로 돌아가기</a></p>
<h1 id="소개">소개</h1>
<p>전략 패턴은 Behavioral design pattern의 일종으로 모드가 바뀔 때마다 전략을 교체할 수 있도록 타입을 제공합니다. 전략 패턴을 적용하면 새로운 전략을 추가하더라도 기존 코드의 수정을 하지 않거나 최소화할 수 있습니다.</p>
<h1 id="example">Example</h1>
<h2 id="가정">가정</h2>
<p>검색 기능을 개발하는 상황이라 가정합니다. 먼저, 전체, 이미지, 뉴스, 지도 검색 기능을 구현하는데, 이 경우 패턴을 적용하지 않았을 때와 적용하였을 때를 비교해보겠습니다.</p>
<h2 id="패턴을-적용하지-않은-경우">패턴을 적용하지 않은 경우</h2>
<p>패턴을 고려하지 않고 검색 기능을 구현하면 아래와 같이 작성할 수 있을 것입니다. 검색 모드를 열거형의 케이스로 정의하고, 모드 변경을 위한 메서드를 만든 후, 버튼 탭 동작에 따라 검색 모드에 따라 검색 로직을 실행하는 것입니다.</p>
<pre><code class="language-swift">final class SearchFeatureWithoutPattern {

    enum Mode {
        case all, image, news, map
    }

    private var mode: Mode = .all

    func setMode(to mode: Mode) {
        self.mode = mode
    }

    func searchButtonTapped() {
        switch mode {
        case .all:
            print(&quot;전체 검색&quot;)
        case .image:
            print(&quot;이미지 검색&quot;)
        case .news:
            print(&quot;뉴스 검색&quot;)
        case .map:
            print(&quot;지도 검색&quot;)
        }
    }
}

let searchFeatureWithoutPattern = SearchFeatureWithoutPattern()
searchFeatureWithoutPattern.setMode(to: .news)
searchFeatureWithoutPattern.searchButtonTapped()</code></pre>
<p>하지만 이 경우에는 동영상 검색과 같이 새로운 검색 전략을 추가하고자 할 때 <code>Mode</code>에 새로운 케이스를 추가하고 <code>searchButtonTapped()</code> 메서드를 직접 수정하여야 합니다. 확장성이 떨어지는 것이죠. 또한 프로젝트 이해도가 떨어지는 상황에서 기존에 개발해둔 기능을 수정하려 한다면 리스크가 클 수 있습니다.</p>
<h2 id="패턴을-적용한-경우">패턴을 적용한 경우</h2>
<p>반면 전략 패턴을 적용하여 동일한 기능을 구현하면 아래와 같이 작성할 수 있습니다. 각 전략을 타입으로써 구성하고 전략 타입들이 가져야할 최소한의 인스턴스 메서드 인터페이스를 프로토콜로 정의하여 이를 준수하도록 하는 것입니다. 프로토콜에 의해 구현 상 다형성을 보장하기에 각 전략 타입의 상세 구현은 각 타입에서 해주면 됩니다. 검색 전략의 변경은 <code>setSearchStrategy(to:)</code> 메서드를 통해 어떤 검색 전략 타입을 따를지 설정해줄 수 있습니다.</p>
<pre><code class="language-swift">final class SearchFeatureWithPattern {

    private var searchStrategy: SearchStrategy = SearchStrategyAll()

    func setSearchStrategy(to strategy: SearchStrategy) {
        searchStrategy = strategy
    }

    func searchButtonTapped() {
        searchStrategy.search()
    }
}

protocol SearchStrategy {
    func search()
}

struct SearchStrategyAll: SearchStrategy {
    func search() {
        print(&quot;전체 검색&quot;)
    }
}

struct SearchStrategyImage: SearchStrategy {
    func search() {
        print(&quot;이미지 검색&quot;)
    }
}

struct SearchStrategyNews: SearchStrategy {
    func search() {
        print(&quot;뉴스 검색&quot;)
    }
}

struct SearchStrategyMap: SearchStrategy {
    func search() {
        print(&quot;지도 검색&quot;)
    }
}

let searchFeatureWithPattern = SearchFeatureWithPattern()
searchFeatureWithPattern.setSearchStrategy(to: SearchStrategyNews())
searchFeatureWithPattern.searchButtonTapped()</code></pre>
<p>이러한 상황에서 새로운 검색 전략을 추가하고자 한다면 단순히 <code>SearchStrategy</code> 타입을 준수하는 새로운 검색 전략 타입을 작성하기만 하면 됩니다. 기존에 존재했던 어떤 타입, 프로퍼티, 메서드의 구현부를 수정하지 않아도 되는 것이죠.</p>
<p><a href="https://velog.io/@ryan-son/Design-patterns-in-Swift">목차로 돌아가기</a></p>
<h1 id="참고자료">참고자료</h1>
<ol>
<li><a href="https://github.com/ochococo/Design-Patterns-In-Swift">Design Patterns in Swift</a> - ochococo GitHub Repository</li>
<li><a href="https://www.youtube.com/watch?v=lJES5TQTTWE">객체지향 디자인패턴 1</a> - 얄팍한 코딩사전</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vapor/Swift] Vapor와 Heroku를 이용한 REST API 구성 및 배포]]></title>
            <link>https://velog.io/@ryan-son/VaporSwift-Vapor%EC%99%80-Heroku%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-REST-API-%EA%B5%AC%EC%84%B1-%EB%B0%8F-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@ryan-son/VaporSwift-Vapor%EC%99%80-Heroku%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-REST-API-%EA%B5%AC%EC%84%B1-%EB%B0%8F-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Wed, 07 Jul 2021 19:29:21 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/373a67c4-7f4e-4954-80db-db3e2502ff11/image.png" alt=""></p>
<blockquote>
<p>이 시리즈는 Swift로 작성된 Server-side Web Framework인 Vapor를 이용하여 REST API를 구성하고 Heroku 서비스를 통해 배포하는 과정을 다룹니다.</p>
</blockquote>
<h1 id="table-of-contents">Table of Contents</h1>
<h4 id="1-vapor를-이용하여-서버-구성하기"><a href="https://velog.io/@ryan-son/VaporSwift-Vapor%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0">1. Vapor를 이용하여 서버 구성하기</a></h4>
<h4 id="2-heroku를-이용하여-vapor-서버-배포하기"><a href="https://velog.io/@ryan-son/VaporSwift-Heroku%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-Vapor-%EC%84%9C%EB%B2%84-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0">2. Heroku를 이용하여 Vapor 서버 배포하기</a></h4>
<h4 id="3-fluent-모델-알아보기"><a href="https://velog.io/@ryan-son/VaporSwift-Fluent-%EB%AA%A8%EB%8D%B8-%ED%83%80%EC%9E%85-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0">3. Fluent 모델 알아보기</a></h4>
<h4 id="4-인코딩-및-디코딩을-위한-모델-타입-작성하기"><a href="https://velog.io/@ryan-son/VaporSwift-%EC%9D%B8%EC%BD%94%EB%94%A9-%EB%B0%8F-%EB%94%94%EC%BD%94%EB%94%A9%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%AA%A8%EB%8D%B8-%ED%83%80%EC%9E%85-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0">4. 인코딩 및 디코딩을 위한 모델 타입 작성하기</a></h4>
<h4 id="5-local에서-postgresql-db-구성하기"><a href="https://velog.io/@ryan-son/VaporSwift-Local%EC%97%90%EC%84%9C-PostgreSQL-DB-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0">5. Local에서 PostgreSQL DB 구성하기</a></h4>
<h4 id="6-배포-서버의-postgresql-db-구성하기"><a href="https://velog.io/@ryan-son/VaporSwift-%EB%B0%B0%ED%8F%AC-%EC%84%9C%EB%B2%84%EC%9D%98-PostgreSQL-DB-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0">6. 배포 서버의 PostgreSQL DB 구성하기</a></h4>
<h4 id="7-모델에-dto-추가하기"><a href="https://velog.io/@ryan-son/VaporSwift-%EB%AA%A8%EB%8D%B8%EC%97%90-DTO-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0">7. 모델에 DTO 추가하기</a></h4>
<h4 id="8-crud-기능-구현하기"><a href="https://velog.io/@ryan-son/VaporSwift-CRUD-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0">8. CRUD 기능 구현하기</a></h4>
<h4 id="9-request-contents-검증하기"><a href="https://velog.io/@ryan-son/VaporSwift-Request-%EC%BB%A8%ED%85%90%EC%B8%A0-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0">9. Request contents 검증하기</a></h4>
<h4 id="10-에러-커스터마이징하기"><a href="https://velog.io/@ryan-son/VaporSwift-%EC%97%90%EB%9F%AC-%EC%BB%A4%EC%8A%A4%ED%84%B0%EB%A7%88%EC%9D%B4%EC%A7%95%ED%95%98%EA%B8%B0">10. 에러 커스터마이징하기</a></h4>
<hr>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://docs.vapor.codes/4.0/">Vapor Docs</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vapor/Swift] 에러 커스터마이징하기]]></title>
            <link>https://velog.io/@ryan-son/VaporSwift-%EC%97%90%EB%9F%AC-%EC%BB%A4%EC%8A%A4%ED%84%B0%EB%A7%88%EC%9D%B4%EC%A7%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ryan-son/VaporSwift-%EC%97%90%EB%9F%AC-%EC%BB%A4%EC%8A%A4%ED%84%B0%EB%A7%88%EC%9D%B4%EC%A7%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 07 Jul 2021 19:04:53 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/8de4d7c1-71bd-4898-bf0a-57c76ba17495/image.png" alt=""></p>
<p>이전 포스팅의 말미에 잠시 언급 드렸듯이 검증 이외에도 클라이언트 요청에 따라 서버에서 작업을 수행하다 에러가 일어나는 경우가 있습니다. 이런 경우에 서버에서 기본적으로 지정된 HTTP status code와 에러 메시지를 response로 반환하게 되지만 이해하기 어려운 경우가 많습니다. 예를 들어 <code>&quot;Something went wrong!&quot;</code>, <code>&quot;Bad Request&quot;</code>와 같은 에러 메시지를 받으면 도대체 무엇이 잘못 되었는지 알 수 없는 경우가 많습니다. 그래서 이번 시간에도 이전 포스팅의 순서와 같이 Vapor의 에러와 관련한 내용을 먼저 알아보고 적용하는 모습을 살펴보도록 하겠습니다.</p>
<h1 id="에러">에러</h1>
<p>Vapor는 에러 처리를 위해 Swift의 <code>Error</code> 프로토콜을 사용합니다. 라우트(루트) 핸들러는 에러를 <code>throw</code> 하거나 실패한 <code>EventLoopFuture</code>를 반환할 수 있습니다. Swift의 <code>Error</code>를 던지거나 반환하면 <code>500</code> 상태 코드 응답과 함께 에러가 기록되는 결과를 낳습니다. <code>AbortError</code>과 <code>DebuggableError</code>를 통해 응답과 기록을 각각 변경할 수 있습니다. 에러 처리는 <code>ErrorMiddleware</code>에 의해 수행되는데, 이 middleware는 애플리케이션에 기본적으로 추가되어 있고 원할 경우 사용자 지정 로직으로 교체할 수 있습니다.</p>
<h2 id="abort">Abort</h2>
<p>Vapor는 <code>Abort</code>라는 이름의 구조체로 기본 에러를 제공합니다. 이 구조체는 <code>AbortError</code>과 <code>DebuggableError</code> 프로토콜을 모두 준수하고 있고, 필요에 따라 HTTP 상태와 failure에 대한 원인을 이니셜라이즈 할 수 있습니다.</p>
<pre><code class="language-swift">// 404 error, default &quot;Not Found&quot; reason used.
throw Abort(.notFound)

// 401 error, custom reason used.
throw Abort(.unauthorized, reason: &quot;Invalid Credentials&quot;)</code></pre>
<p><code>flatMap</code> 클로저와 같이 throwing이 지원되지 않는 비동기 상황에서는 failed future를 반환할 수 있습니다.</p>
<pre><code class="language-swift">guard let user = user else {
    req.eventLoop.makeFailedFuture(Abort(.notFound))    
}
return user.save()</code></pre>
<p>Vapor는 옵셔널 값을 가진 future를 언래핑하기 위한 도우미 익스텐션으로 <code>unwrap(or:)</code> 메서드를 가지고 있습니다.</p>
<pre><code class="language-swift">User.find(id, on: db)
    .unwrap(or: Abort(.notFound))
    .flatMap 
{ user in
    // Non-optional User supplied to closure.
}</code></pre>
<p>만약 <code>User.find</code>가 <code>nil</code>을 반환한다면, future는 실패하며 지정된 에러의 이유를 보여줄 것입니다. 그렇지 않은 경우에는 <code>flatMap</code>에게 옵셔널이 아닌 값을 제공해줄 것입니다.</p>
<h2 id="abort-error">Abort Error</h2>
<p>기본적으로 라우트(루트) 클로저가 던지거나 반환한 모든 Swift <code>Error</code>는 <code>500 Internal Server Error</code> 응답으로 처리됩니다. 디버그 모드로 빌드된 경우에는 <code>ErrorMiddleware</code>가 에러 설명을 포함해줍니다. 이는 프로젝트가 release mode로 빌드되면 보안 상의 이유로 완전히 제거됩니다.</p>
<p>특정 에러에 대해 HTTP 응답 상태 또는 원인을 구성하고자 한다면 <code>AbortError</code>를 준수하면 됩니다.</p>
<pre><code class="language-swift">import Vapor

enum MyError {
    case userNotLoggedIn
    case invalidEmail(String)
}

extension MyError: AbortError {
    var reason: String {
        switch self {
        case .userNotLoggedIn:
            return &quot;User is not logged in.&quot;
        case .invalidEmail(let email):
            return &quot;Email address is not valid: \(email).&quot;
        }
    }

    var status: HTTPStatus {
        switch self {
        case .userNotLoggedIn:
            return .unauthorized
        case .invalidEmail:
            return .badRequest
        }
    }
}</code></pre>
<hr>
<blockquote>
<p>Skip 하셔도 되는 구간입니다.</p>
</blockquote>
<h2 id="debuggable-error">Debuggable Error</h2>
<p><code>ErrorMiddleware</code>은 루트에서 던져진 에러를 기록하기 위해 <code>Logger.report(error:)</code> 메서드를 사용합니다. 이 메서드는 읽을 수 있는 메시지를 기록하기 위해 <code>CustomStringConvertible</code> 및 <code>LocalizedError</code>의 채택 여부를 체크할 것입니다.</p>
<p>에러 기록 작업을 커스터마이징 하기를 원한다면 에러 타입이 <code>DebuggableError</code>을 준수하게 만들면 됩니다. 이 프로토콜은 고유 식별자, 소스 위치, 스택 추적과 같은 유용한 프로퍼티들을 포함하고 있습니다. 대부분의 프로퍼티들은 준수를 용이하게 만들기 위해 선택적으로 사용할 수 있도록 설정되어 있습니다.</p>
<p><code>DebuggableError</code>를 잘 준수하면 </p>
<p><code>DebuggableError</code>를 가장 잘 준수하려면 에러 타입이 구조체여야 하며 필요한 경우에는 소스 및 스택 추적 정보를 저장할 수 있어야 합니다. 아래는 구조체 및 에러 소스 정보를 캡처하고 사용하기 위해 업데이트된 <code>MyError</code> 열거 타입의 예시입니다.</p>
<pre><code class="language-swift">import Vapor

struct MyError: DebuggableError {
    enum Value {
        case userNotLoggedIn
        case invalidEmail(String)
    }

    var identifier: String {
        switch self.value {
        case .userNotLoggedIn:
            return &quot;userNotLoggedIn&quot;
        case .invalidEmail:
            return &quot;invalidEmail&quot;
        }
    }

    var reason: String {
        switch self.value {
        case .userNotLoggedIn:
            return &quot;User is not logged in.&quot;
        case .invalidEmail(let email):
            return &quot;Email address is not valid: \(email).&quot;
        }
    }

    var value: Value
    var source: ErrorSource?

    init(
        _ value: Value,
        file: String = #file,
        function: String = #function,
        line: UInt = #line,
        column: UInt = #column
    ) {
        self.value = value
        self.source = .init(
            file: file,
            function: function,
            line: line,
            column: column
        )
    }
}</code></pre>
<p><code>DebuggableError</code>는 발생한 에러의 디버그 용이성을 향상시켜줄 수 있는 <code>possibleCauses</code> 및 <code>suggestedFixes</code>와 같은 몇 가지 다른 프로퍼티들을 가지고 있습니다. 더 많은 정보는 프로토콜 자체에 대해 살펴보시면 됩니다.</p>
<h3 id="스택-추적">스택 추적</h3>
<p>Vapor에는 정상적인 Swift 오류와 충돌에 대한 스택 추적를 볼 수 있는 기능이 포함되어 있습니다.</p>
<h4 id="swift-backtrace">Swift Backtrace</h4>
<p>Vapor는 SwiftBacktrace 라이브러리를 사용하여 Linux에서 치명적인 에러 또는 assertion이 발생한 후 스택 추적을 제공합니다. 이 기능이 작동하려면 아래와 같이 컴파일 중에 앱에 디버그 기호가 포함되어 있어야 합니다.</p>
<pre><code class="language-swift">swift build -c release -Xswiftc -g</code></pre>
<h4 id="에러-추적">에러 추적</h4>
<p>기본적으로 <code>Abort</code>는 이니셜라이즈 되었을 때 현재 스택 추적을 캡처합니다. 사용자 지정 에러 타입들은 <code>DebuggableError</code>을 채택하고 <code>StackTrace.capture()</code>를 가지게 함으로써 이 작업을 수행할 수 있습니다.</p>
<pre><code class="language-swift">import Vapor

struct MyError: DebuggableError {
    var identifier: String
    var reason: String
    var stackTrace: StackTrace?

    init(
        identifier: String,
        reason: String,
        stackTrace: StackTrace? = .capture()
    ) {
        self.identifier = identifier
        self.reason = reason
        self.stackTrace = stackTrace
    }
}</code></pre>
<p>애플리케이션의 <a href="https://docs.vapor.codes/4.0/logging/#level">log level</a>이 <code>.debug</code> 이하로 설정되어 잇는 경우 에러 스택 추적은 로그 결과에 포함될 것입니다.</p>
<p>스택 추적은 log level이 <code>.debug</code> 이상이라면 캡처되지 않습니다. 이 행동을 재정의하려면 <code>configure</code>에서 <code>StackTrace.isCaptureEnabled</code>를 수동으로 설정해주세요.</p>
<pre><code class="language-swift">// Always capture stack traces, regardless of log level.
StackTrace.isCaptureEnabled = true</code></pre>
<h2 id="error-middleware">Error Middleware</h2>
<p><code>ErrorMiddleware</code>는 기본적으로 애플리케이션에 추가된 유일한 미들웨어입니다. 이 미들웨어는 루트 핸들러가 던지거나 반환한 Swift 에러들을 HTTP 응답으로 변환해줍니다. 이 미들웨어가 없으면 에러가 발생하여도 응답 없이 연결이 끊어질 것입니다.</p>
<p><code>AbortError</code>과 <code>DebuggableError</code>가 제공하는 에러처리를 커스터마이징하고 싶다면 <code>ErrorMiddleware</code>를 자체적인 에러 처리 로직으로 교체하면 됩니다. 이렇게 하려면 먼저 <code>app.middleware</code>를 빈 구성으로 설정함으로써 기본 에러 미들웨어를 제거하고, 자체 에러 처리 미들웨어를 애플리케이션의 첫 번째 미들웨어로 추가하면 됩니다.</p>
<pre><code class="language-swift">// Remove all existing middleware.
app.middleware = .init()
// Add custom error handling middleware first.
app.middleware.use(MyErrorMiddleware())</code></pre>
<blockquote>
<p>스킵 구간 끝입니다.</p>
</blockquote>
<hr>
<h1 id="적용하기">적용하기</h1>
<p>저는 아래와 같이 크게 헤더의 Content-Type 검사와 검증 실패 시 에러 <code>unwrap(or:)</code> 메서드에서 <code>nil</code>을 발견하였을 때 에러를 정의하여 적용하였습니다.</p>
<pre><code class="language-swift">import Vapor

enum HTTPError {
    case progressNotFoundInURL
    case invalidProgressInURL
    case invalidContentType
    case validationFailedWhileCreating
    case validationFailedWhileUpdating
    case invalidID
}

extension HTTPError: AbortError {

    var reason: String {
        switch self {
        case .progressNotFoundInURL:
            return &quot;This path requires progress as path parameter. Please enter \&quot;/todo\&quot;, \&quot;/doing\&quot; or \&quot;/done\&quot; additively.&quot;
        case .invalidProgressInURL:
            return &quot;Progresses are classified as todo, doing and done. Please check entered URL.&quot;
        case .invalidContentType:
            return &quot;The Content-Type of the request is not application/json.&quot;
        case .validationFailedWhileCreating:
            return &quot;Validations are failed while creating data. Please check if the content is not exceed length of 1000, progress is in todo, doing and done.&quot;
        case .validationFailedWhileUpdating:
            return &quot;Validations are failed while updating data. Please check if the content has id, not to exceed length of 1000, and the progress is in todo, doing and done.&quot;
        case .invalidID:
            return &quot;You have entered invalid projectItem ID. The database does not have such item.&quot;
        }
    }

    var status: HTTPResponseStatus {
        switch self {
        case .progressNotFoundInURL:
            return .notFound
        case .invalidProgressInURL:
            return .notFound
        case .invalidContentType:
            return .badRequest
        case .validationFailedWhileCreating:
            return .badRequest
        case .validationFailedWhileUpdating:
            return .badRequest
        case .invalidID:
            return .notFound
        }
    }
}</code></pre>
<pre><code class="language-swift">func create(req: Request) throws -&gt; EventLoopFuture&lt;ProjectItem&gt; {
    // ⭐️ Header의 Content-Type이 application/json일 것
    guard req.headers.contentType == .json else {
        throw HTTPError.invalidContentType
    }

    do {
        try PostProjectItem.validate(content: req)
    } catch {
        // 검증에 대한 커스텀 에러 
        throw HTTPError.validationFailedWhileCreating
    }

    let exist = try req.content.decode(PostProjectItem.self)
    let newProjectItem = ProjectItem(exist)
    return newProjectItem.save(on: req.db).map{ (result) -&gt; ProjectItem in
        return newProjectItem
    }
}

func update(req: Request) throws -&gt; EventLoopFuture&lt;ProjectItem&gt; {
    guard req.headers.contentType == .json else {
        throw HTTPError.invalidContentType
    }

    do {
        try PatchProjectItem.validate(content: req)
    } catch {
        throw HTTPError.validationFailedWhileUpdating
    }

    let exist = try req.content.decode(PatchProjectItem.self)

    return ProjectItem.find(exist.id, on: req.db)
        .unwrap(or: HTTPError.invalidID)
        .flatMap { item in
            if let title = exist.title { item.title = title }
            if let content = exist.content { item.content = content }
            if let progress = exist.progress { item.progress = progress }
            if let deadlineDate = exist.deadlineDate { item.deadlineDate = deadlineDate }
            if let index = exist.index { item.index = index }

            return item.update(on: req.db).map { return item }
        }
}

func delete(req: Request) throws -&gt; EventLoopFuture&lt;HTTPStatus&gt; {
    guard req.headers.contentType == .json else {
        throw HTTPError.invalidContentType
    }

    let exist = try req.content.decode(DeleteProjectItem.self)
    return ProjectItem.find(exist.id, on: req.db)
        .unwrap(or: HTTPError.invalidID)
        .flatMap { $0.delete(on: req.db) }
        .transform(to: .ok)
}</code></pre>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://docs.vapor.codes/4.0/errors/">Vapor Docs - Errors</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vapor/Swift] Request contents 검증하기]]></title>
            <link>https://velog.io/@ryan-son/VaporSwift-Request-%EC%BB%A8%ED%85%90%EC%B8%A0-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ryan-son/VaporSwift-Request-%EC%BB%A8%ED%85%90%EC%B8%A0-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 07 Jul 2021 17:55:32 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/aebeccf7-6b95-442c-8382-aae92d880d99/image.png" alt=""></p>
<p>지난 시간까지 CRUD 기능을 구현해 보았습니다. 이번 시간에는 Request contents를 검증하는 방법을 다루어보겠습니다. </p>
<h1 id="request-contents를-왜-검증하나요">Request contents를 왜 검증하나요?</h1>
<p>클라이언트의 요청에 따라 CRUD 기능을 수행할 때, 특히 DB의 수정 작업이 이루어지는 Create, Update, Delete 작업을 수행할 때 특정 필드가 반드시 포함되어야 한다거나 해당 필드 입력값에 대한 제한이 필요한 경우 검증 (Validation)을 적용합니다.</p>
<p>Vapor Validation API를 사용하면 <code>Content</code> API를 통해 데이터를 디코딩하기 전에 받은 요청을 검증할 수 있습니다. 사실 Vapor는 Swift의 타입 안전성을 가진 <code>Codable</code> 프로토콜과 통합되어 있어 동적 타입 언어에 비해 데이터 검증에 대해 많이 걱정할 필요는 없기는 하지만, 아래 이유들로 인해 검증을 사용할 가치는 충분히 있습니다.</p>
<h2 id="에러가-발생한-이유를-쉽게-이해할-수-있다">에러가 발생한 이유를 쉽게 이해할 수 있다.</h2>
<p><code>Content</code> API를 이용하여 구조체를 디코딩하면 데이터가 적절하지 않을 경우에 에러가 발생합니다. 하지만 이런 에러 메시지는 때로는 이해하기 어려운 경우가 있죠. 예를 들어 아래 예시와 같은 상황이 있을 수 있습니다.</p>
<pre><code class="language-swift">enum Color: String, Codable {
    case red, blue, green
}</code></pre>
<p>만약 사용자가 <code>Color</code> 타입에 <code>&quot;purple&quot;</code>이라는 문자열을 전달하면 아래와 같은 에러를 받을 것입니다.</p>
<pre><code class="language-swift">Cannot initialize Color from invalid String value purple for key favoriteColor</code></pre>
<p>이러한 에러는 기술적으로는 잘못된 값을 입력받지 않음으로써 endpoint를 성공적으로 보호했지만 사용성은 떨어진다고 볼 수 있습니다. 이러한 방식 대신에 사용자에게 에러와 입력할 수 있는 옵션을 알리는 것이 더 나을 수 있습니다. Validation API를 사용하면 다음과 같은 에러를 보여줌으로써 사용자에게 안내 문구를 보여줄 수 있습니다.</p>
<pre><code class="language-swift">favoriteColor is not red, blue, or green</code></pre>
<h2 id="세부적인-내용을-검증할-수-있다">세부적인 내용을 검증할 수 있다.</h2>
<p><code>Codable</code>은 타입 검증을 잘 다루지만, 때로는 타입 검증 이상의 것을 하기를 원할 때도 있습니다. 예를 들어, 문자열의 컨텐츠를 검증하거나 정수의 크기를 검증하는 것이 있을 수 있죠. Validation API로 이메일, 문자 세트 (character sets), 정수 범위 이상의 것을 검증할 수 있습니다.</p>
<h1 id="validatable">Validatable</h1>
<p>요청을 검증하려면 <code>Validations</code> 컬렉션을 만들어야 합니다. 이는 기존의 타입에 <code>Validatable</code> 프로토콜을 준수하게 만들면 됩니다.</p>
<p>아래의 예시를 통해 <code>POST /users</code> endpoint에 검증을 추가하는 방법을 알아보겠습니다.</p>
<pre><code class="language-swift">enum Color: String, Codable {
    case red, blue, green
}

struct CreateUser: Content {
    var name: String
    var username: String
    var age: Int
    var email: String
    var favoriteColor: Color?
}

app.post(&quot;users&quot;) { req -&gt; CreateUser in
    let user = try req.content.decode(CreateUser.self)
    // Do something with user.
    return user
}</code></pre>
<h2 id="검증-추가하기">검증 추가하기</h2>
<p>첫 번째 해야할 일은 디코딩할 타입(이 경우 <code>CreateUser</code>)이 <code>Validable</code> 프로토콜을 준수하게 하는 것입니다. 이는 아래와 같이 extension을 통해 할 수 있습니다.</p>
<pre><code class="language-swift">extension CreateUser: Validatable {
    static func validations(_ validations: inout Validations) {
        // Validations go here.
    }
}</code></pre>
<p>스태틱 메서드인 <code>validations(_:)</code>는 <code>CreateUser</code>가 검증될 때 호출될 것입니다. 수행하기를 원하는 모든 검증을 <code>Validations</code> 컬렉션에 추가해주어야 합니다. 아래는 사용자가 입력한 이메일이 유효한지를 검증하는 간단한 검증을 추가하는 예시입니다.</p>
<pre><code class="language-swift">validations.add(&quot;email&quot;, as: String.self, is: .email)</code></pre>
<p>첫 번째 파라미터의 전달인자에는 값의 예상되는 키를 입력합니다. 이 경우에는 <code>&quot;email&quot;</code>입니다. 이는 검증될 타입의 프로퍼티 이름과 동일해야 합니다. 두 번째 파라미터인 <code>as</code>의 전달인자에는 예상되는 타입을 입력해야 합니다. 이 경우에는 <code>String</code>입니다. 타입은 대개는 프로퍼티의 타입과 동일하지만, 항상 그런 것은 아닙니다. 마지막으로, 세 번째 파라미터 <code>is</code>의 전달인자에는 하나 혹은 그 이상의 validator가 올 수 있습니다. 이 경우에는 입력된 값이 이메일 주소인지를 검증하는 하나의 validator만 추가한 모습을 확인하실 수 있습니다.</p>
<h2 id="요청-컨텐츠-검증하기">요청 컨텐츠 검증하기</h2>
<p>타입이 <code>Validatable</code> 프로토콜을 준수하게 만들었다면, 스태틱 함수인 <code>validate(content:)</code>를 통해 request content를 검증할 수 있습니다. 아래 내용을 라우트(루트) 핸들러의 <code>req.content.decode(CreateUser.self)</code> 이전에 위치시키면 됩니다.</p>
<pre><code class="language-swift">try CreateUser.validate(content: req)</code></pre>
<p>이제 유효하지 않은 이메일을 포함한 요청을 보내봅시다.</p>
<pre><code class="language-swift">POST /users HTTP/1.1
Content-Length: 67
Content-Type: application/json

{
    &quot;age&quot;: 4,
    &quot;email&quot;: &quot;foo&quot;,
    &quot;favoriteColor&quot;: &quot;green&quot;,
    &quot;name&quot;: &quot;Foo&quot;,
    &quot;username&quot;: &quot;foo&quot;
}</code></pre>
<p>그럼 아래와 같은 에러를 확인하실 수 있습니다.</p>
<pre><code class="language-swift">email is not a valid email address</code></pre>
<h2 id="요청-쿼리-검증하기">요청 쿼리 검증하기</h2>
<p><code>Validable</code>을 준수하는 타입에는 요청의 쿼리 문자열을 검증하는 데 사용할 수 있는 <code>validate(query:)</code>도 있습니다. 아래 코드를 라우트(루트) 핸들러에 넣으시면 됩니다.</p>
<pre><code class="language-swift">try CreateUser.validate(query: req)
req.query.decode(CreateUser.self)</code></pre>
<p>이제 아래와 같이 쿼리 문자열에 유효하지 않은 이메일을 포함해서 요청을 보내봅시다.</p>
<pre><code class="language-swift">GET /users?age=4&amp;email=foo&amp;favoriteColor=green&amp;name=Foo&amp;username=foo HTTP/1.1</code></pre>
<p>그럼 아래와 같은 메시지를 확인하실 수 있으실 것입니다.</p>
<pre><code class="language-swift">email is not a valid email address</code></pre>
<h2 id="정수-검증하기">정수 검증하기</h2>
<p>이제 <code>age</code>에 대한 검증을 추가해봅시다.</p>
<pre><code class="language-swift">validations.add(&quot;age&quot;, as: Int.self, is: .range(13...))</code></pre>
<p>age 검증은 age가 13 이상임을 요구하고 있습니다. 만약 위에서 했던 요청을 다시 시도한다면 아래와 같은 에러를 만나시게 될 것입니다.</p>
<pre><code class="language-swift">age is less than minimum of 13, email is not a valid email address</code></pre>
<h2 id="문자열-검증하기">문자열 검증하기</h2>
<p>다음으로 <code>name</code>과 <code>username</code>에 대한 검증을 추가해봅시다.</p>
<pre><code class="language-swift">validations.add(&quot;name&quot;, as: String.self, is: !.empty)
validations.add(&quot;username&quot;, as: String.self, is: .count(3...) &amp;&amp; .alphanumeric)</code></pre>
<p>name 검증은 <code>.empty</code> 검증을 반전시키기 위해 <code>!</code> 연산자를 사용했습니다. 이는 요청 시 문자열이 비어있지 않을 것을 요구하게 됩니다.</p>
<p>username 검증은 <code>&amp;&amp;</code>을 이용해 두 개의 validators를 결합하고 있습니다. 이는 문자열이 최소 3 개의 characters를 가지고 알파벳과 숫자만 포함(alphanumeric)할 것을 요구하게 됩니다.</p>
<h2 id="열거형-검증하기">열거형 검증하기</h2>
<p>마지막으로 <code>favoriteColor</code>이 유효한지 체크하기 위해 조금 더 어려운 검증을 살펴보겠습니다. </p>
<pre><code class="language-swift">validations.add(
    &quot;favoriteColor&quot;, as: String.self,
    is: .in(&quot;red&quot;, &quot;blue&quot;, &quot;green&quot;),
    required: false
)</code></pre>
<p>유효하지 않은 값으로부터 <code>Color</code>를 디코딩할 수 없기 때문에 이 검증은 <code>String</code>을 베이스 타입으로 사용합니다. 값이 유효한 옵션인 red, blue, green 중에 있는지 여부를 검증하기 위해 <code>.in</code> validator를 사용하고 있습니다. 이 값은 옵셔널이므로 요청 데이터에 키가 없더라도 검증이 실패하지 않도록 <code>required</code>가 false로 설정되어 있습니다.</p>
<p>위의 검증은 favorite color 키가 없다면 통과하지만 <code>null</code>을 넣으면 통과되지 않습니다. <code>null</code> 입력을 지원하려면 검증 타입을 <code>String?</code>으로 변경하고 <code>.nil ||</code>을 추가해주세요.</p>
<pre><code class="language-swift">validations.add(
    &quot;favoriteColor&quot;, as: String?.self,
    is: .nil || .in(&quot;red&quot;, &quot;blue&quot;, &quot;green&quot;),
    required: false
)</code></pre>
<h2 id="validators">Validators</h2>
<p>아래와 같은 Validators를 사용할 수 있습니다.</p>
<table>
<thead>
<tr>
<th>Validation</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>.ascii</code></td>
<td>ASCII 문자만 포함할 수 있음</td>
</tr>
<tr>
<td><code>.alphanumeric</code></td>
<td>알파벳 또는 숫자 문자만 포함할 수 있음</td>
</tr>
<tr>
<td><code>.characterSet(_:)</code></td>
<td><code>CharacterSet</code>에 작성된 문자들만 포함할 수 있음</td>
</tr>
<tr>
<td><code>.count(_:)</code></td>
<td>컬렉션 내 요소의 개수가 작성한 범위 내에 있어야 함</td>
</tr>
<tr>
<td><code>.email</code></td>
<td>유효한 이메일을 포함해야 함</td>
</tr>
<tr>
<td><code>.empty</code></td>
<td>컬렉션이 비어있어야 함</td>
</tr>
<tr>
<td><code>.in(_:)</code></td>
<td>값이 작성한 <code>Collection</code> 안에 있어야 함</td>
</tr>
<tr>
<td><code>.nil</code></td>
<td>값이 <code>null</code>이어야 함</td>
</tr>
<tr>
<td><code>.range(_:)</code></td>
<td>값이 제공된 <code>Range</code> 안에 있어야 함</td>
</tr>
<tr>
<td><code>.url</code></td>
<td>유효한 URL을 포함해야함</td>
</tr>
</tbody></table>
<p>Validators는 아래 연산자를 사용해서 복합적으로 검증을 구성할 수 있습니다.</p>
<table>
<thead>
<tr>
<th>Operator</th>
<th>Position</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>!</code></td>
<td>앞(prefix)</td>
<td>Validator를 뒤집어 반대의 경우를 요구함</td>
</tr>
<tr>
<td><code>&amp;&amp;</code></td>
<td>중간(infix)</td>
<td>두 개의 validators를 결합하여 두 가지 모두를 요구함</td>
</tr>
<tr>
<td>||</td>
<td>중간(infix)</td>
<td>두 개의 validators를 결합하여 두 가지 중에 하나를 요구함</td>
</tr>
</tbody></table>
<h1 id="적용하기">적용하기</h1>
<p>예시 프로젝트에는 <code>POST</code>, <code>PATCH</code> 메서드로 요청을 보낼 경우 아래와 같은 검증들을 하도록 구성했습니다.</p>
<pre><code class="language-swift">extension PostProjectItem: Validatable {
    static func validations(_ validations: inout Validations) {
        validations.add(&quot;title&quot;, as: String.self, required: true)
        validations.add(&quot;content&quot;, as: String.self, is: .count(...1000), required: true)
        validations.add(&quot;progress&quot;, as: String.self, is: .in(&quot;todo&quot;, &quot;doing&quot;, &quot;done&quot;), required: true)
        validations.add(&quot;index&quot;, as: Int.self, required: true)
        validations.add(&quot;deadlineDate&quot;, as: Date.self, required: true)
    }
}

extension PatchProjectItem: Validatable {
    static func validations(_ validations: inout Validations) {
        validations.add(&quot;id&quot;, as: String.self, required: true)
        validations.add(&quot;title&quot;, as: String.self, required: false)
        validations.add(&quot;content&quot;, as: String.self, is: .count(...1000), required: false)
        validations.add(&quot;progress&quot;, as: String.self, is: .in(&quot;todo&quot;, &quot;doing&quot;, &quot;done&quot;), required: false)
        validations.add(&quot;index&quot;, as: Int.self, required: false)
        validations.add(&quot;deadlineDate&quot;, as: Date.self, required: false)
    }
}</code></pre>
<p><code>POST</code>의 경우만 간략히 설명 드리겠습니다.</p>
<ol>
<li><code>&quot;title&quot;</code>은 <code>String</code> 타입이어야 하고 반드시 요청에 키를 포함하여야 한다.</li>
<li><code>&quot;content&quot;</code>는 <code>String</code> 타입이어야 하고 길이가 0 ~ 1000 자 사이어야 하지만 반드시 요청에 키를 포함하여야 할 필요는 없다.</li>
<li><code>&quot;progress&quot;</code>는 <code>String</code> 타입이어야 하고 <code>&quot;todo&quot;</code>, <code>&quot;doing&quot;</code>, <code>&quot;done&quot;</code> 중에 하나여야 하고, 반드시 요청에 키를 포함하여야 한다.</li>
<li><code>&quot;index&quot;</code>는 <code>Int</code> 타입이어야 하고 반드시 요청에 키를 포함하여야 한다.</li>
<li><code>&quot;deadlineDate</code>는 <code>Date</code> 타입이어야 하고 반드시 요청에 키를 포함하여야 한다.</li>
</ol>
<p>앞서 보셨듯이 위의 검증을 적용하려면 HTTP 메서드 처리에 사용되는 메서드에 request 디코딩 전에 <code>validate(content:)</code> 메서드를 추가하시면 됩니다.</p>
<pre><code class="language-swift">func create(req: Request) throws -&gt; EventLoopFuture&lt;ProjectItem&gt; {
    try PostProjectItem.validate(content: req)
    let exist = try req.content.decode(PostProjectItem.self)
    let newProjectItem = ProjectItem(exist)
    return newProjectItem.save(on: req.db).map { (result) -&gt; ProjectItem in
        return newProjectItem
    }
}

func update(req: Request) throws -&gt; EventLoopFuture&lt;ProjectItem&gt; {
    try PatchProjectItem.validate(content: req)
    let exist = try req.content.decode(PatchProjectItem.self)

    return ProjectItem.find(exist.id, on: req.db)
        .unwrap(or: HTTPError.invalidID)
        .flatMap { item in
            if let title = exist.title { item.title = title }
            if let content = exist.content { item.content = content }
            if let progress = exist.progress { item.progress = progress }
            if let deadlineDate = exist.deadlineDate { item.deadlineDate = deadlineDate }
            if let index = exist.index { item.index = index }
            return item.update(on: req.db).map { return item }
        }
}</code></pre>
<h1 id="다음은">다음은?</h1>
<p>검증을 통해 입력값에 대한 제한과 확인을 하였고, 검증 결과에 의해 요청을 받아들일 수 없는 경우 사용자에게 적절한 에러 메시지도 제공해주었습니다. 하지만 헤더의 <code>Content-Type</code>이 잘못되었다거나 기타 요청을 처리하는 도중 에러가 발생했을 때 우리 상황에 더 적절한 에러 메시지를 제공해줄 수도 있을 것입니다. 다음 시간에는 Vapor의 <code>AbortError</code> 프로토콜을 이용해 사용자가 받아보는 메시지를 커스터마이징 하는 방법에 대해 알아보겠습니다!</p>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://docs.vapor.codes/4.0/validation/">Vapor Docs - Validation</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vapor/Swift] CRUD 기능 구현하기]]></title>
            <link>https://velog.io/@ryan-son/VaporSwift-CRUD-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ryan-son/VaporSwift-CRUD-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 07 Jul 2021 10:27:36 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/234082d7-4c6b-4ddf-8936-d7d2f8057c9d/image.png" alt=""></p>
<p>데이터베이스 마이그레이션 작업에 모델 DTO 추가까지 진행해 보았습니다. 이제 실제로 만들고(Create), 읽고(Read), 업데이트하고(Update), 삭제할(Delete) 수 있는 API를 서버에서 구현해볼까요? </p>
<p>변경사항이 일어날 때마다 배포하여 테스트하기에는 시간이 많이 소요되니 로컬 환경에서 진행하도록 하겠습니다. 로컬 환경의 DB 구성 방법은 <a href="https://velog.io/@ryan-son/VaporSwift-Local%EC%97%90%EC%84%9C-PostgreSQL-DB-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0">이 포스팅</a>을 참고해주세요. </p>
<h1 id="crud-기능-구현">CRUD 기능 구현</h1>
<h2 id="controller-파일-생성-및-타입-작성">Controller 파일 생성 및 타입 작성</h2>
<p>먼저 클라이언트측에서 Request를 보내면 이를 처리할 메인 로직을 담당해줄 <code>Controller</code> 타입을 만듭니다. <code>App</code> 폴더 안에 <code>Controllers</code>라는 폴더를 만들고 <code>&lt;SchemaName&gt;Controller.swift</code> 파일을 생성해주세요. 저는 <code>ProjectItemController.swift</code>를 만들었습니다.</p>
<p>아래와 같이 <code>Fluent</code>와 <code>Vapor</code>를 import 해주세요.</p>
<pre><code class="language-swift">import Fluent
import Vapor</code></pre>
<p>이제 파일 이름과 동일한 구조체를 만들어보겠습니다. 이 컨트롤러는 <code>routes.swift</code>를 도와 묶음으로 일을 처리해주는 타입이므로 <code>RouteCollection</code> 프로토콜을 채택해줍니다.</p>
<p><code>RouteCollection</code> 프로토콜은 아래와 같이 <code>boot(routes:)</code> 메서드 구현을 요구하는데, 이 메서드는 이후 과정에서 <code>routes.swift</code>의 <code>routes(_:)</code>가 <code>register(collection:)</code>을 실행하여 컨트롤러를 사용해줄겁니다. 구조가 이해되시죠? 컨트롤러에서 이를 구현하지 않는다면 <code>routes(_:)</code> 함수 안에 컨트롤러의 모든 업무를 정의해주어야 할 것입니다. 아주 비대한 함수를 만들게 되겠지요. 지금은 <code>boot(routes:)</code> 메서드의 body를 빈 코드블럭으로 남겨주세요.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/9d73246b-ea35-414d-b5cf-983c1e50fdf6/image.png" alt=""></p>
<p>이 과정까지 완료하시면 아래와 같은 구조가 되었을 것입니다.</p>
<pre><code class="language-swift">struct ProjectItemController: RouteCollection {
    func boot(routes: RoutesBuilder) throws { }
}</code></pre>
<h2 id="create">Create</h2>
<p>초기 구성이 끝났으니 Controller 타입 안에 HTTP <code>POST</code> 메서드에 대응하는 Create 메서드를 작성해보겠습니다. 이전 포스팅에서 작성한 이니셜라이저를 이용해서 <code>PostProjectItem</code> 타입으로 디코딩된 인스턴스로부터 <code>ProjectItem</code> 타입의 인스턴스를 만들어서 저장해주시고 해당 내용을 response body로 반환해주시면 됩니다.</p>
<pre><code class="language-swift">    func create(req: Request) throws -&gt; EventLoopFuture&lt;ProjectItem&gt; {
        let exist = try req.content.decode(PostProjectItem.self)
        let newProjectItem = ProjectItem(exist)

        return newProjectItem.save(on: req.db).map{ (result) -&gt; ProjectItem in
            return newProjectItem
        }
    }</code></pre>
<p>여기에서 반환하실 때 <code>save(on:)</code> 메서드를 <code>create(on:)</code> 메서드로 대체하실 수 있습니다. <code>save(on:)</code> 메서드는 데이터베이스에 ID가 존재하지 않을 때는 <code>create(on:)</code> 메서드로, 존재할 때는 <code>update(on:)</code> 메서드로 작동하는 메서드입니다.</p>
<p>요청에 따라 모델 인스턴스를 DB에 생성하면 <code>map(_:)</code> 메서드의 클로저를 통해 response에 담아줄 body를 반환하여 클라이언트가 정상적으로 요청이 처리되었음을 확인할 수 있도록 해줍니다.</p>
<h2 id="read">Read</h2>
<p>다음으로 HTTP <code>GET</code> 메서드에 대응하는 간단한 read용 메서드를 구현해보겠습니다.</p>
<p>먼저 조건 없이 스키마 안의 모든 모델 인스턴스를 반환하는 메서드는 아래와 같이 쿼리 결과를 모두 가져오는 방식으로 작성하시면 됩니다.</p>
<pre><code class="language-swift">func read(req: Request) throws -&gt; EventLoopFuture&lt;[ProjectItem]&gt; {
    return ProjectItem.query(on: req.db).all()
}</code></pre>
<p>저는 클라이언트의 Request에서 <code>Path parameter</code>를 이용해 요청한 parameter가 포함된 모델 인스턴스를 필터하여 제공하는 메서드를 작성해보았습니다.</p>
<pre><code class="language-swift">func read(req: Request) throws -&gt; EventLoopFuture&lt;[ProjectItem]&gt; {
    let validProgress = [&quot;todo&quot;, &quot;doing&quot;, &quot;done&quot;]

    guard let progress = req.parameters.get(&quot;progress&quot;), validProgress.contains(progress) else {
        throw Abort(.badRequest)
    }

    return ProjectItem.query(on: req.db).filter(\.$progress == progress).all()
}</code></pre>
<p>필터 작업은 위와 같은 방식으로 수행하시면 됩니다.</p>
<h2 id="update">Update</h2>
<p>Update는 HTTP <code>PUT</code>과 <code>PATCH</code> 메서드에 대응하는 메서드입니다. 이미 데이터베이스에 존재하는 모델 인스턴스를 대상으로 Request하기 때문에 요청한 내용으로부터 <code>id</code>를 식별하고 DB에서 <code>id</code>를 기준으로 해당 인스턴스를 찾아 수정한 후 업데이트 해줍니다.</p>
<pre><code class="language-swift">func update(req: Request) throws -&gt; EventLoopFuture&lt;ProjectItem&gt; {
    let exist = try req.content.decode(PatchProjectItem.self)

    return ProjectItem.find(exist.id, on: req.db)
        .unwrap(or: Abort(.notFound))
        .flatMap { item in
            if let title = exist.title { item.title = title }
            if let content = exist.content { item.content = content }
            if let progress = exist.progress { item.progress = progress }
            if let deadlineDate = exist.deadlineDate { item.deadlineDate = deadlineDate }
            if let index = exist.index { item.index = index }

            return item.update(on: req.db).map { return item }
        }
}</code></pre>
<h2 id="delete">Delete</h2>
<p>Delete는 HTTP <code>DELETE</code> 메서드에 대응하는 메서드로, 아래와 같이 데이터베이스에 있는 데이터를 삭제할 수 있도록 구성해주시면 됩니다. </p>
<pre><code class="language-swift">func delete(req: Request) throws -&gt; EventLoopFuture&lt;HTTPStatus&gt; {
    guard req.headers.contentType == .json else {
        throw HTTPError.invalidContentType
    }

    let exist = try req.content.decode(DeleteProjectItem.self)
    return ProjectItem.find(exist.id, on: req.db)
        .unwrap(or: HTTPError.invalidID)
        .flatMap { $0.delete(on: req.db) }
        .transform(to: .ok)
}</code></pre>
<h1 id="url-지정-및-crud-메서드-적용">URL 지정 및 CRUD 메서드 적용</h1>
<p>CRUD 기능을 구현했다면 이제 우리 서버의 어떤 URL에 어떤 HTTP 메서드로 접근하면 해당 작업들을 할 수 있는지를 정의해야겠죠? 최초에 Controller를 만들며 빈 코드블럭으로 남겨두었던 <code>boot(routes:)</code> 메서드를 이용하면 이 작업을 하실 수 있습니다.</p>
<p>먼저 메서드가 전달인자로 받은 <code>routes</code>는 <code>RoutesBuilder</code> 타입으로, 내부의 <code>grouped(_:)</code> 메서드로 <code>https://some-server-name.domain-name.com/somePath</code>의 형태로 경로를 지정해주실 수 있고, <code>group(_:)</code> 메서드를 통해 Path parameter를 설정해주실 수 있습니다. 아래 예시를 보시죠.</p>
<pre><code class="language-swift">func boot(routes: RoutesBuilder) throws {
    // https://some-server-name.domain-name.com/projectItems
    let projectItems = routes.grouped(&quot;projectItems&quot;)
    // https://some-server-name.domain-name.com/projectItems/:progress
    // :progress에 작성되는 내용은 path parameter 형식으로 서버에 전달됨
    projectItems.group(&quot;:progress&quot;) { projectItem in
        // 위의 URL로 get 요청 가능
        projectItem.get(use: read)
    }
    // https://some-server-name.domain-name.com/projectItem
    let projectItem = routes.grouped(&quot;projectItem&quot;)
    // 위의 URL로 post, patch, delete 요청 가능
    projectItem.post(use: create)
    projectItem.patch(use: update)
    projectItem.delete(use: delete)
}</code></pre>
<p>위의 예시를 도식으로 나타내면 아래와 같습니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/d321a438-81ff-4951-b5a3-71cc6ac19a26/image.png" alt=""></p>
<h1 id="route에-controller-등록">route에 Controller 등록</h1>
<p>이제 각 HTTP 메서드를 통해 요청할 수 있는 URL을 구성(route)했으니 이 컨트롤러를 <code>routes.swift</code>에 있는 <code>routes(_:)</code> 메서드에 등록만 하면 서버가 정상 작동될 것입니다.</p>
<pre><code class="language-swift">// routes.swift
import Vapor
import Fluent

func routes(_ app: Application) throws {
    try app.register(collection: ProjectItemController())
}</code></pre>
<h1 id="기능-확인">기능 확인</h1>
<p>기능 확인을 하기 전에 아래 사항들이 잘 적용되어 있는지 확인해주세요.</p>
<h2 id="사전-작업">사전 작업</h2>
<ol>
<li>로컬 PostgreSQL을 사용하도록 DB 구성 (<code>configure.swift</code>) - <a href="https://velog.io/@ryan-son/VaporSwift-Local%EC%97%90%EC%84%9C-PostgreSQL-DB-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0">관련 포스팅</a>
해당 포스팅의 <code>등록 (Register)</code>과 같은 내용으로 작성되어 있으면  됩니다.</li>
<li>추가적인 모델 타입, 마이그레이션 타입 필드 변경이 있었다면 마이그레이션 작업 완료 - 위 관련 포스팅에서 revert 후 migration</li>
<li>Controller의 <code>boot(routes:)</code> 메서드 완성 및 <code>routes.swift</code> 내 <code>routes(_:)</code> 메서드에 컨트롤러 등록 (본 포스팅)</li>
<li>Postman과 같이 Request를 보낼 수 있는 툴 준비</li>
<li>Xcode vapor 프로젝트 Run (command + R)</li>
</ol>
<h2 id="기능-검증">기능 검증</h2>
<h3 id="create-1">Create</h3>
<ol>
<li><code>POST</code> 방식으로 요청할 수 있도록 옵션 선택 </li>
<li>Request 송신 툴의 URL에 <code>http://localhost:8080/projectItem</code>과 같이 <code>POST</code> 요청이 가능한 URL을 작성</li>
<li>Body에 JSON을 담아 보낼 수 있도록 설정</li>
<li>Body 내용 작성</li>
<li>요청 송신</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/bacdedc5-5367-4b12-a6a1-28cfdd6bd690/image.png" alt=""></p>
<p>Response의 HTTP Status Code가 200 번대인지, 서버가 요청한대로 응답하였는지 확인해보세요.</p>
<h3 id="read-1">Read</h3>
<ol>
<li><code>GET</code> 방식으로 요청할 수 있도록 옵션 선택 </li>
<li>Request 송신 툴의 URL에 <code>http://localhost:8080/projectItems/doing</code>과 같이 <code>GET</code> 요청이 가능한 URL을 작성</li>
<li>요청 송신</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/9e75f68e-4b80-4f6c-af98-4dd13479a5fb/image.png" alt=""></p>
<h3 id="update-1">Update</h3>
<p>Create와 동일한 방식으로 요청을 만들어주되 몇 가지 파라미터는 의도적으로 제외하고 요청을 송신해봅시다.</p>
<ol>
<li><code>PATCH</code> 방식으로 요청할 수 있도록 옵션 선택 </li>
<li>Request 송신 툴의 URL에 <code>http://localhost:8080/projectItem</code>과 같이 <code>PATCH</code> 요청이 가능한 URL을 작성</li>
<li>Body에 JSON을 담아 보낼 수 있도록 설정</li>
<li>Body 내용 작성</li>
<li>요청 송신</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/55837731-bd24-40a4-9b2a-ab9967af8801/image.png" alt=""></p>
<p>POST 요청과 마찬가지로 Response의 HTTP Status Code가 200 번대인지, 서버가 요청한대로 응답하였는지 확인해보세요.</p>
<h3 id="delete-1">Delete</h3>
<p>삭제 작업은 DB에 등록된 해당 아이템의 ID만 제공해주면 삭제 작업이 가능하도록 DTO를 작성했었습니다.</p>
<ol>
<li><code>DELETE</code> 방식으로 요청할 수 있도록 옵션 선택 </li>
<li>Request 송신 툴의 URL에 <code>http://localhost:8080/projectItem</code>과 같이 <code>DELETE</code> 요청이 가능한 URL을 작성</li>
<li>Body에 JSON을 담아 보낼 수 있도록 설정</li>
<li>Body 내용 작성</li>
<li>요청 송신</li>
</ol>
<p><img src="https://images.velog.io/images/ryan-son/post/ef279455-647f-43b4-a773-24418b2a0bfa/image.png" alt=""></p>
<p>Response의 HTTP Status Code가 200 번대인지 확인해보세요.</p>
<h1 id="다음은">다음은?</h1>
<p>이번 시간에는 CRUD 기능을 구현해보았습니다. 하지만 아직 DB를 사용할 수 있게끔 인터페이스만 구성한 상황인데요, 그래서 다음 포스팅에서는 <code>Validatable</code> 프로토콜을 이용해 클라이언트가 보낸 Body 데이터를 검증하는 방법을 알아보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vapor/Swift] 모델에 DTO 추가하기]]></title>
            <link>https://velog.io/@ryan-son/VaporSwift-%EB%AA%A8%EB%8D%B8%EC%97%90-DTO-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ryan-son/VaporSwift-%EB%AA%A8%EB%8D%B8%EC%97%90-DTO-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 07 Jul 2021 07:37:10 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/df06961e-83d0-4497-9613-a79196c1b149/image.png" alt=""></p>
<p>모델과 관련된 지난 포스팅 두 편(<a href="https://velog.io/@ryan-son/VaporSwift-Fluent-%EB%AA%A8%EB%8D%B8-%ED%83%80%EC%9E%85-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0">이론</a>, <a href="https://velog.io/@ryan-son/VaporSwift-%EC%9D%B8%EC%BD%94%EB%94%A9-%EB%B0%8F-%EB%94%94%EC%BD%94%EB%94%A9%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%AA%A8%EB%8D%B8-%ED%83%80%EC%9E%85-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0">적용</a>)을 통해서 앞으로 클라이언트와 서버 간의 Body 데이터를 주고 받는 양식을 Data Transfer Object (DTO)를 통해 정의할 수 있다고 말씀 드렸었습니다! 단적으로 말해 아래와 같은 상황들이 있을 수 있겠죠.</p>
<blockquote>
<ol>
<li>클라이언트: 저희는 DB에 있는 내용을 삭제하고 싶을 뿐이에요. ID 값만 드리면 되지 않나요?</li>
<li>서버: 이 필드값은 서버에서 데이터 관리용으로 사용하는 값이라 클라이언트가 받아가는 내용에서 제외해도 되는데.</li>
</ol>
</blockquote>
<p>이런 경우에 DTO를 활용해서 주고 받을 Body 데이터의 모양을 설정하실 수 있습니다. 그럼 계속해서 Fluent  프레임워크를 이용해 DTO를 작성하는 방법을 자세히 알아봅시다.</p>
<h1 id="data-transfer-object-dto">Data Transfer Object (DTO)</h1>
<p>DTO는 인코드, 디코드하고 싶은 데이터 구조를 나타내는 별도의 <code>Codable</code> 타입을 의미합니다. 예를 들어 아래와 같이 <code>User</code>라는 모델이 있다고 가정해 보겠습니다.</p>
<pre><code class="language-swift">// Abridged user model for reference.
final class User: Model {
    @ID(key: .id)
    var id: UUID?

    @Field(key: &quot;first_name&quot;)
    var firstName: String

    @Field(key: &quot;last_name&quot;)
    var lastName: String
}</code></pre>
<p><code>PATCH</code> 요청을 처리하는 경우에 흔히 DTO를 사용하게 됩니다. <code>PATCH</code> 요청에는 업데이트가 필요한 필드에 대해서만 요청에 포함시켜 보내기 때문이죠. 이 때 <code>User</code> 모델을 디코딩 타입으로 설정하면 몇 가지의 필드가 빠졌다고 하며 요청이 실패하게 될겁니다. 아래 예시를 통해 모델을 업데이트 할 때 요청 데이터를 디코딩할 때 사용할 DTO를 확인하실 수 있습니다.</p>
<pre><code class="language-swift">// Structure of PATCH /users/:id request.
struct PatchUser: Decodable {
    var firstName: String?
    var lastName: String?
}

app.patch(&quot;users&quot;, &quot;:id&quot;) { req in 
    // Decode the request data.
    let patch = try req.content.decode(PatchUser.self)
    // Fetch the desired user from the database.
    return User.find(req.parameters.get(&quot;id&quot;), on: req.db)
        .unwrap(or: Abort(.notFound))
        .flatMap 
    { user in
        // If first name was supplied, update it.
        if let firstName = patch.firstName {
            user.firstName = firstName
        }
        // If new last name was supplied, update it.
        if let lastName = patch.lastName {
            user.lastName = lastName
        }
        // Save the user and return it.
        return user.save(on: req.db)
            .transform(to: user)
    }
}</code></pre>
<p>DTO를 사용하는 또 다른 경우 중에 하나로 위의 2 번 상황과 같이 API 응답 형식을 정의해주고 싶을 때가 있습니다. 2 번에서는 특정 내용을 제외하고 보내고 싶다고 하였지만, 아래 예시에서는 <code>firstName</code>과 <code>lastName</code>을 조합하여 <code>name</code>이라는 하나의 키 값으로 응답하고 있음을 확인하실 수 있습니다.</p>
<pre><code class="language-swift">// Structure of GET /users response.
struct GetUser: Content {
    var id: UUID
    var name: String
}

app.get(&quot;users&quot;) { req in 
    // Fetch all users from the database.
    User.query(on: req.db).all().flatMapThrowing { users in
        try users.map { user in
            // Convert each user to GET return type.
            try GetUser(
                id: user.requireID(),
                name: &quot;\(user.firstName) \(user.lastName)&quot;
            )
        }
    }
}</code></pre>
<p>비록 DTO의 구조가 모델의 <code>Codable</code> 만족에 따른 모양과 동일하더라도 별도의 타입으로 만들어 두면 대형 프로젝트를 수행할 때 깔끔한 구조를 유지하실 수 있으실 것입니다. 이렇게 구조를 마련해두시면 모델의 프로퍼티를 변경해야 하는 경우에도 앱의 공용 API를 손상시킬 염려가 없게 됩니다. API 이용자 (클라이언트)와 공유할 수 있는 별도의 패키지에 DTO를 넣는 것도 고려해 볼 수 있습니다.</p>
<p>이러한 이유들로 인해 대형 프로젝트의 경우에는 가능한 경우 DTO를 되도록이면 사용할 것을 권장 드립니다.</p>
<h1 id="적용">적용</h1>
<p>기존 프로젝트에 아래와 같이 모델 타입에 DTO들을 추가해보았습니다. 타입 이름은 HTTP 메서드 이름을 기준으로 지어주었습니다. 만든 DTO들을 타입별로 파일을 분리해준다면 프로젝트를 처음 접하는 사람들도 타입의 위치를 찾기 더 쉬울 것 같습니다. 위에서 설명드리지 않았지만 한 가지 추가된 내용은 <code>PostProjectItem</code> 타입을 통해 디코딩된 인스턴스 입력으로 <code>ProjectItem</code> 타입의 인스턴스를 생성할 수 있는 이니셜라이저를 하나 작성해 주었다는 것입니다.</p>
<pre><code class="language-swift">import Fluent
import Vapor

struct PostProjectItem: Content {
    let id: UUID?
    let title: String
    let content: String
    let deadlineDate: Date
    let progress: String
    let index: Int
}

struct PatchProjectItem: Decodable {
    let id: UUID
    let title: String?
    let content: String?
    let deadlineDate: Date?
    let progress: String?
    let index: Int?
}

struct DeleteProjectItem: Decodable {
    let id: UUID
}

final class ProjectItem: Model, Content {
    static let schema = &quot;projectItems&quot;

    @ID(key: .id)
    var id: UUID?

    @Field(key: &quot;title&quot;)
    var title: String

    @Field(key: &quot;content&quot;)
    var content: String

    @Field(key: &quot;deadlineDate&quot;)
    var deadlineDate: Date

    @Field(key: &quot;progress&quot;)
    var progress: String

    @Field(key: &quot;index&quot;)
    var index: Int

    init() { }

    init(id: UUID? = nil, title: String, content: String, deadlineDate: Date, progress: String, index: Int) {
        self.id = id
        self.title = title
        self.content = content
        self.deadlineDate = deadlineDate
        self.progress = progress
        self.index = index
    }

    init(_ projectItem: PostProjectItem) {
        self.id = projectItem.id
        self.title = projectItem.title
        self.content = projectItem.content
        self.deadlineDate = projectItem.deadlineDate
        self.progress = projectItem.progress
        self.index = projectItem.index
    }
}</code></pre>
<p>이제 담당하시는 프로젝트에서 DTO를 작성하실 수 있으시겠죠? 다음 포스팅에서는 만들어 둔 DTO를 이용해 대망의 Create, Read, Update, Delete (CRUD) 기능을 함께 구현해 보겠습니다!</p>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://docs.vapor.codes/4.0/fluent/model/#data-transfer-object">Vapor Docs - Fluent - Model</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vapor/Swift] 배포 서버의 PostgreSQL DB 구성하기]]></title>
            <link>https://velog.io/@ryan-son/VaporSwift-%EB%B0%B0%ED%8F%AC-%EC%84%9C%EB%B2%84%EC%9D%98-PostgreSQL-DB-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ryan-son/VaporSwift-%EB%B0%B0%ED%8F%AC-%EC%84%9C%EB%B2%84%EC%9D%98-PostgreSQL-DB-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 06 Jul 2021 17:43:18 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/e899c523-bf0d-4279-953d-d4225fd11180/image.png" alt=""></p>
<p>이번 내용은 배포와 관련된 <a href="https://velog.io/@ryan-son/VaporSwift-Heroku%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-Vapor-%EC%84%9C%EB%B2%84-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0">이전 포스팅</a>에서 다룬 적이 있으므로 사전 작업은 해당 포스팅의 <code>Postgres</code> 섹션을 참고해주세요. 자세하게 다룰 부분은 해당 포스팅의 <code>데이터베이스 구성</code> 파트입니다.</p>
<h1 id="데이터베이스-구성">데이터베이스 구성</h1>
<p>직전 포스팅인 로컬 환경과 달리 구성해주셔야 하는 부분은 <code>configure.swift</code>입니다. 아래와 같이 작성해주시면 됩니다.</p>
<pre><code class="language-swift">import Vapor
import Fluent
import FluentPostgresDriver

public func configure(_ app: Application) throws {
    app.migrations.add(CreateProjectItem(), to: .psql)

    if let databaseURL = Environment.get(&quot;DATABASE_URL&quot;), var postgresConfig = PostgresConfiguration(url: databaseURL) {
        var clientTLSConfiguration = TLSConfiguration.makeClientConfiguration()
        clientTLSConfiguration.certificateVerification = .none
        postgresConfig.tlsConfiguration = clientTLSConfiguration
        app.databases.use(.postgres(configuration: postgresConfig), as: .psql)
    } else {
        throw Abort(.internalServerError)
    }

    try routes(app)
}</code></pre>
<p><code>TLSConfiguration</code>의 인스턴스를 생성하는 방식이 Vapor Docs와 다릅니다. 이는 Vapor Docs에서 TLSConfiguration 인스턴스 생성에 사용한 <code>TLSConfiguration.forClient(certificationVerification:)</code> 메서드가 deprecated 되었기 때문입니다. 이제는 <code>TLSConfiguration.makeClientConfiguration()</code>을 통해 인스턴스를 생성할 수 있지만 <code>certificationVerification</code>을 초기화 시점에 바로 <code>.none</code> 값으로 할당해줄 수는 없게 되었습니다. 만들어준 인스턴스의 <code>certificationVerification</code> 프로퍼티를 <code>.none</code>으로 설정해주시고 <code>postgresConfig.tlsConfiguration</code>에 할당해줍니다. 이를 통해 Heroku Postgres에서 발생할 수 있는 SSL Certification 관련 에러(Handshake Error)를 방지할 수 있습니다.</p>
<h1 id="commit--push">Commit &amp; Push</h1>
<p>커밋하시고 서버에 푸시해주세요! CD로 인해 자동 배포됩니다.</p>
<pre><code class="language-swift">git commit -m &quot;feat: Configure heroku postgres database&quot;
git push heroku master // 브랜치를 이용하고 있다면 git push heroku branchName:master</code></pre>
<h1 id="데이터베이스-마이그레이션">데이터베이스 마이그레이션</h1>
<p>마이그레이션은 아래 명령을 통해 할 수 있습니다.
<code>heroku run Run -- migrate --env production</code></p>
<h1 id="데이터베이스-되돌리기">데이터베이스 되돌리기</h1>
<p>데이터베이스를 되돌리려면 아래 명령을 입력하세요.
<code>heroku run Run -- migrate --revert --all --yes --env production</code></p>
<p>revert 작업에 실패하면 아래 명령어를 입력해보세요.
<code>heroku run Run -- migrate --revert</code></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vapor/Swift] Local에서 PostgreSQL DB 구성하기]]></title>
            <link>https://velog.io/@ryan-son/VaporSwift-Local%EC%97%90%EC%84%9C-PostgreSQL-DB-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ryan-son/VaporSwift-Local%EC%97%90%EC%84%9C-PostgreSQL-DB-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 06 Jul 2021 17:06:01 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/1f2ebf07-5eb4-4b45-a83b-654be81393b2/image.png" alt=""></p>
<p>서버를 배포하는 작업은 시간이 많이 소요되므로 로컬에서 작업한 후 원격 환경에 배포하는 방법을 고려해볼 수 있습니다. 따라서 이번에는 로컬 환경에서 PostgreSQL을 Vapor의 DB 드라이버로 설정하고 마이그레이션 하는 방법을 알아보겠습니다. 먼저 작성하기에 앞서 <a href="https://hururuek-chapchap.tistory.com/129">이 글</a>을 참고하여 작성하였음을 밝힙니다.</p>
<h1 id="local-환경-postgresql-db-구성">Local 환경 PostgreSQL DB 구성</h1>
<h2 id="postgresql-설치">PostgreSQL 설치</h2>
<p>PostgreSQL을 사용하기 위해 먼저 brew를 통해 PostgreSQL을 다운로드 받습니다. 터미널에 아래와 같이 명령하시면 됩니다.</p>
<pre><code class="language-swift">brew install postgresql</code></pre>
<p>설치가 완료되시면 터미널을 통해 아래와 같이 명령합니다.</p>
<pre><code class="language-swift">pg_ctl -D /usr/local/var/postgres start

// postgresql 서비스 시작
brew services start postgresql

// 재실행 시
brew services restart postgrsql</code></pre>
<h2 id="postgresql-기본-문법">PostgreSQL 기본 문법</h2>
<pre><code class="language-swift">// PostgreSQL 접속
psql postgres 

// 모든 데이터베이스 조회
postgres=# \l 

// 데이터베이스 생성
postgres=# create database my_database

    // 명령어를 입력했지만 데이터베이스가 생성되지 않은 경우 \q 명령을 통해 접속 해제하신 후
    // 터미널에 아래 명령을 입력하시고 다시 접속하여 db가 생성되었는지 확인해보시기 바랍니다.
    createdb (database_name)

// PostgreSQL 접속 해제
postgres=# \q

// 작성한 데이터베이스로 이동
postgres=# \c (database_name)

// 테이블 목록 조회
my_database=# \dt

// 테이블 내 모델 인스턴스 조회
my_database# select * from (table_name);
    // 테이블 이름을 대소문자를 혼용하여 지으셨다면 &quot;table_name&quot;과 같이 테이블 이름 전후로 쌍따옴표를 붙여 명령어를 입력해주세요.

// 테이블 삭제
my_database=# drop table (table_name)</code></pre>
<h2 id="데이터베이스-생성">데이터베이스 생성</h2>
<p>위 기본 문법을 참고하셔서 데이터베이스를 하나 생성해주세요. postgreSQL에 접속된 상태에서 <code>\l</code> 명령을 통해 데이터베이스가 생성되었는지 확인도 해주시면 좋습니다.
<img src="https://images.velog.io/images/ryan-son/post/14c03019-146d-42d6-a001-d1c3969246e0/image.png" alt=""></p>
<p>Owner 값도 이후 과정에서 사용될 예정이니 기억해두시면 좋습니다.</p>
<h2 id="spm-packageswift에-postgresql-의존성-추가">SPM Package.swift에 PostgreSQL 의존성 추가</h2>
<p>터미널에서 <code>vapor new (ProjectName)</code> 명령을 통해 Vapor 프로젝트를 생성하실 때 Fluent를 추가하시고 PostgreSQL 사용을 선택하시지 않은 경우 <code>Package.swift</code> 파일에서 아래와 같이 의존성을 추가해주세요. ⭐️로 표시한 내용을 참고하시면 됩니다.</p>
<pre><code class="language-swift">// swift-tools-version:5.2
import PackageDescription

let package = Package(
    name: &quot;ProjectManagerServer&quot;,
    platforms: [
       .macOS(.v10_15)
    ],
    dependencies: [
        // 💧 A server-side Swift web framework.
        .package(url: &quot;https://github.com/vapor/vapor.git&quot;, from: &quot;4.0.0&quot;),
        // ⭐️ Fluent 프레임워크 의존성을 추가하지 않으신 경우 아래 추가
        .package(url: &quot;https://github.com/vapor/fluent.git&quot;, from: &quot;4.0.0-rc&quot;),
        // ⭐️ Fluent Postgres 드라이버 의존성 추가
        .package(url: &quot;https://github.com/vapor/fluent-postgres-driver.git&quot;, from: &quot;2.0.0&quot;),
    ],
    targets: [
        .target(
            name: &quot;App&quot;,
            dependencies: [
                .product(name: &quot;Vapor&quot;, package: &quot;vapor&quot;),
                // ⭐️ Fluent 프레임워크 추가 시 아래 내용 추가
                .product(name: &quot;Fluent&quot;, package: &quot;fluent&quot;),
                // ⭐️ Fluent Postgres 드라이버 추가 시 아래 내용 추가
                .product(name: &quot;FluentPostgresDriver&quot;, package: &quot;fluent-postgres-driver&quot;)
            ],
            swiftSettings: [
                // Enable better optimizations when building in Release configuration. Despite the use of
                // the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
                // builds. See &lt;https://github.com/swift-server/guides#building-for-production&gt; for details.
                .unsafeFlags([&quot;-cross-module-optimization&quot;], .when(configuration: .release))
            ]
        ),
        .target(name: &quot;Run&quot;, dependencies: [.target(name: &quot;App&quot;)]),
        .testTarget(name: &quot;AppTests&quot;, dependencies: [
            .target(name: &quot;App&quot;),
            .product(name: &quot;XCTVapor&quot;, package: &quot;vapor&quot;),
        ])
    ]
)</code></pre>
<p>네트워크가 사용 가능한 환경에 계실 경우 바로 SPM이 의존성을 추가하기 시작할 것입니다.</p>
<p>의존성 추가 작업이 끝나면 <code>App</code> 폴더 내에 위치한 <code>configure.swift</code>로 이동하여 아래와 같이 <code>app.databases</code>를 이용해 DB 구성 작업을 해줍니다. 필요한 프레임워크가 아래처럼 모두 import 되어있는지도 확인해주세요.</p>
<pre><code class="language-swift">import Vapor
import Fluent
import FluentPostgresDriver

public func configure(_ app: Application) throws {
    app.migrations.add(CreateProjectItem()) // 마이그레이션 과정에서 설명할 내용 

    // ⭐️ 아래 내용을 추가하여 로컬 DB를 구성합니다.
    app.databases.use(
        .postgres(hostname: &quot;localhost&quot;, username: &quot;ryan-son&quot;, password: &quot;&quot;, database: &quot;database_name&quot;),
        as: .psql
    )

    try routes(app)
}</code></pre>
<p>매개변수의 전달인자로 사용되는 값은 위와 같이 넣어주시면 됩니다. <code>username</code>의 경우 PostgreSQL에 접속해서 확인하신 <code>owner</code>이름을, <code>password</code>는 기본 패스워드인 <code>&quot;&quot;</code>을 입력해주시면 됩니다. 데이터베이스 이름도 확인하셔서 기입해주세요.
<img src="https://images.velog.io/images/ryan-son/post/14c03019-146d-42d6-a001-d1c3969246e0/image.png" alt=""></p>
<h1 id="database-migration">Database Migration</h1>
<p><code>configure.swift</code>를 통해 로컬 환경의 PostgreSQL을 사용함을 알려주었다면, 저희가 이전 과정을 통해 만들어두었던 모델 타입을 담을 수 있는 표(스키마)를 만들어주어야 합니다. 이 과정이 마이그레이션입니다. 마이그레이션은 데이터베이스의 버전 관리 시스템 (Version Control System; VCS)와 같아서 생성할 수도 있지만 이전으로 되돌릴 수도 있습니다. 아래 과정을 계속해서 살펴보죠.</p>
<h2 id="migration-파일-생성">Migration 파일 생성</h2>
<p>먼저 <code>Migrations</code> 폴더를 만들고 내부에 <code>Create&lt;SchemaName&gt;.swift</code>와 같은 파일을 생성합니다. 저는 <code>CreateProjectItems.swift</code>로 만들었습니다.</p>
<p>이제 아래와 같이 데이터베이스에 스키마를 생성할 수 있도록 <code>prepare(on:)</code> 메서드를 작성합니다.</p>
<pre><code class="language-swift">import Fluent

struct CreateProjectItem: Migration {
    /// 데이터베이스에 변경을 만드는 메서드
    func prepare(on database: Database) -&gt; EventLoopFuture&lt;Void&gt; {
        return database.schema(&quot;projectItems&quot;)
            .id()
            .field(&quot;title&quot;, .string, .required)
            .field(&quot;content&quot;, .string, .required)
            .field(&quot;deadlineDate&quot;, .date, .required)
            .field(&quot;progress&quot;, .string, .required)
            .field(&quot;index&quot;, .int, .required)
            .create()
    }

    /// 가능한 경우 `prepare`에서 만든 변경사항을 되돌리는(undo) 메서드, 현재는 스키마 삭제로 설정됨
    func revert(on database: Database) -&gt; EventLoopFuture&lt;Void&gt; {
        return database.schema(&quot;projectItems&quot;).delete()
    }
}</code></pre>
<h2 id="등록-register">등록 (Register)</h2>
<p>위와 같이 마이그레이션 구조체를 작성하셨다면 아래와 같이 <code>configure.swift</code>에 <code>app.migrations</code>를 이용해 마이그레이션을 등록해주어야 합니다.</p>
<pre><code class="language-swift">import Vapor
import Fluent
import FluentPostgresDriver

public func configure(_ app: Application) throws {
    // ⭐️ 아래 내용을 추가하여 작성한 마이그레이션을 등록합니다.
    app.migrations.add(CreateProjectItem())

    app.databases.use(
        .postgres(hostname: &quot;localhost&quot;, username: &quot;ryan-son&quot;, password: &quot;&quot;, database: &quot;database_name&quot;),
        as: .psql
    )

    try routes(app)
}</code></pre>
<h2 id="migrate">Migrate</h2>
<p>사전 작업이 완료되었다면 터미널에 아래 명령을 입력하여 마이그레이트를 실시합니다.</p>
<pre><code class="language-swift">vapor run migrate</code></pre>
<p>마이그레이션 작업이 완료되었으면 PostgreSQL에 접속하여 테이블을 확인하실 수 있으실 것입니다. 제 테이블에는 넣어둔 내용이 있어 이렇게 보이지만, 여러 분들은 필드 이름만 있고 내용은 빈 테이블을 확인하실 수 있으실 것입니다.</p>
<p><img src="https://images.velog.io/images/ryan-son/post/5ba0f866-edbf-4600-b8ed-a68ecc3eaa48/image.png" alt=""></p>
<h2 id="revert">Revert</h2>
<p>앞서 말씀드렸듯이 마이그레이션 작업은 버전 관리 시스템과 같아서 마이그레이션 작업을 되돌릴 수 있습니다. <code>Migration 파일 생성</code> 섹션에서 다룬 <code>Migration</code>이 채택된 타입의 <code>revert(on:)</code> 메서드를 커스터마이징하셔서 사용하실 수 있는데, 저의 경우에는 스키마 삭제가 적용되어 있습니다. revert 작업은 터미널에 아래와 같이 명령하시면 수행하실 수 있습니다.</p>
<pre><code class="language-swift">vapor run migrate --revert</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vapor/Swift] 인코딩 및 디코딩을 위한 모델 타입 작성하기]]></title>
            <link>https://velog.io/@ryan-son/VaporSwift-%EC%9D%B8%EC%BD%94%EB%94%A9-%EB%B0%8F-%EB%94%94%EC%BD%94%EB%94%A9%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%AA%A8%EB%8D%B8-%ED%83%80%EC%9E%85-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ryan-son/VaporSwift-%EC%9D%B8%EC%BD%94%EB%94%A9-%EB%B0%8F-%EB%94%94%EC%BD%94%EB%94%A9%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%AA%A8%EB%8D%B8-%ED%83%80%EC%9E%85-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 06 Jul 2021 14:37:22 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/57822ce4-ab02-47a7-a0c5-c64c7955d099/image.png" alt=""></p>
<p>이번에는 작성한 모델 타입을 통해 어떠한 방식으로 모델 타입을 작성할 수 있는지를 알아보겠습니다.</p>
<p>아래는 프로젝트 관리에 사용되는 아이템을 표현하는 타입으로 아직 Data Transfer Object (DTO)가 적용되지 않은 형태를 가져왔습니다. 클라이언트측과 협의하여 필드값들을 결정했지만, 이전 글에서 다루었듯이 <code>deadlineDate</code>의 경우 soft-delete를 지원할 수 있도록 <code>@Timestamp</code>로 정의하여도 되고, <code>id</code>를 사용자 정의해서 사용하실 수도 있습니다. 더 복잡한 내용을 담기 위해서 <code>@Group</code>을 통해 구조체를 저장할 수도 있고, <code>progress</code>와 같이 상태가 특정 케이스로 분류가 가능한 경우 <code>@Enum</code>을 이용해 필드를 정의할 수도 있습니다. 이전 글에서 기본적인 지식을 쌓고 오셔서 간단한 구조임을 한 눈에 파악하실 수 있으실 거라 생각합니다.</p>
<pre><code class="language-swift">import Fluent
import Vapor

final class ProjectItem: Model, Content {
    static let schema = &quot;projectItems&quot;

    @ID(key: .id)
    var id: UUID?

    @Field(key: &quot;title&quot;)
    var title: String

    @Field(key: &quot;content&quot;)
    var content: String

    @Field(key: &quot;deadlineDate&quot;)
    var deadlineDate: Date

    @Field(key: &quot;progress&quot;)
    var progress: String

    @Field(key: &quot;index&quot;)
    var index: Int

    init() { }

    init(id: UUID? = nil, title: String, content: String, deadlineDate: Date, progress: String, index: Int) {
        self.id = id
        self.title = title
        self.content = content
        self.deadlineDate = deadlineDate
        self.progress = progress
        self.index = index
    }
}</code></pre>
<p>다음에는 모델 타입을 담아줄 DB를 구성하는 내용을 다루어 보겠습니다. 먼저 Local 환경에서 구성하는 방법을 다루고 이후에 배포 서버에서 구성하는 방법을 알아볼 것입니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vapor/Swift] Fluent 모델 알아보기]]></title>
            <link>https://velog.io/@ryan-son/VaporSwift-Fluent-%EB%AA%A8%EB%8D%B8-%ED%83%80%EC%9E%85-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@ryan-son/VaporSwift-Fluent-%EB%AA%A8%EB%8D%B8-%ED%83%80%EC%9E%85-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 06 Jul 2021 14:21:29 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/ryan-son/post/85cfb449-d201-4c98-8ea9-b7dee7548d13/image.png" alt=""></p>
<p>지난 시간까지 로컬과 Heroku를 통해 원격에 배포한 서버에 접근하는 과정까지 다루었습니다. </p>
<p>이번에는 본격적으로 원하는 API를 구성하기 위해 request를 통해 전달 받은 데이터의 디코딩 타입을 만들고, DB를 구성하는 과정으로 넘어가볼텐데요, 이번에는 먼저 Fluent 모델에 대한 이론적인 내용을 다루어 보겠습니다.</p>
<h1 id="모델">모델</h1>
<p>모델은 데이터베이스의 테이블 또는 컬렉션에 저장된 데이터를 나타냅니다. 모델은 Codable 값을 저장할 수 있는 필드를 가질 수 있습니다. 모든 모델들은 고유 식별자가 있고, Property wrapper가 식별자, 필드와 관계를 나타내는데 사용됩니다.</p>
<p>아래 코드는 하나의 필드를 가지고 있는 예시 모델을 나타내고 있습니다. 한 가지 주의하여야 할 사항은 모델이 제약사항, 인덱스, foreign key와 같은 데이터베이스의 모든 스키마를 나타내고 있지 않다는 점입니다. 모델은 단지 데이터가 데이터베이스 스키마에 저장되는 모양을 나타내는 수단일 뿐이에요.</p>
<pre><code class="language-swift">final class Planet: Model {
    // Name of the table or collection.
    static let schema = &quot;planets&quot;

    // Unique identifier for this Planet.
    @ID(key: .id)
    var id: UUID?

    // The Planet&#39;s name.
    @Field(key: &quot;name&quot;)
    var name: String

    // Creates a new, empty Planet.
    init() { }

    // Creates a new Planet with all properties set.
    init(id: UUID? = nil, name: String) {
        self.id = id
        self.name = name
    }
}</code></pre>
<p>아래부터는 위의 문단에서 다룬 새로운 용어들에 대해서 다루어보겠습니다.</p>
<h2 id="스키마-schema">스키마 (Schema)</h2>
<p>모든 모델들은 <code>schema</code>라는 읽기 전용 프로퍼티를 필요로 합니다. 이 문자열은 모델이 나타내는 테이블 또는 컬렉션의 이름을 나타냅니다.</p>
<pre><code class="language-swift">final class Planet: Model {
    // Name of the table or collection.
    static let schema = &quot;planets&quot;
}</code></pre>
<p>이 모델에 쿼리(데이터 요청)를 할 때, <code>planets</code>라 이름 붙여진 스키마에 데이터를 저장하거나 스키마로부터 데이터를 불러옵니다.</p>
<blockquote>
<p>schema 이름은 통상적으로 클래스 이름을 소문자, 복수형으로 정의합니다.</p>
</blockquote>
<h2 id="식별자-identifier">식별자 (Identifier)</h2>
<p>모든 모델들은 <code>@ID</code> property wrapper를 이용해 정의된 <code>id</code>라는 프로퍼티를 가져야 합니다. 이 필드는 모델의 인스턴스를 고유하게 식별하는데 사용됩니다.</p>
<pre><code class="language-swift">final class Planet: Model {
    // Unique identifier for this Planet.
    @ID(key: .id)
    var id: UUID?
}</code></pre>
<p>기본적으로 <code>@ID</code> 프로퍼티는 사용하고 있는 데이터베이스 드라이버에 적절한 키를 사용할 수 있도록 조정해주는 <code>.id</code>라는 특별한 키를 사용해야 합니다. SQL에서는 <code>&quot;id&quot;</code>, NoSQL에서는 <code>&quot;_id&quot;</code>로 자동 적용됩니다.</p>
<p><code>@ID</code>는 또한 <code>UUID</code> 타입이어야 합니다. 이는 모든 데이터베이스 드라이버가 지원하는 유일한 식별값이며, Fluent는 모델이 새로 생성될 때 자동적으로 새로운 UUID 식별자를 생성하여 부여해줍니다.</p>
<p><code>@ID</code>는 저장되지 않은 모델들이 식별자를 아직 가지고 있지 않을 수 있기 때문에 optional로 정의되어 있습니다. 식별자를 가져오면서 식별자가 없는 경우 에러를 던져주고자 한다면 아래처럼 <code>requireID</code> 메서드를 사용하시면 됩니다.</p>
<pre><code class="language-swift">let id = try planet.requireID()</code></pre>
<h3 id="exists">Exists</h3>
<p><code>@ID</code>는 데이터베이스에 존재하는지 여부를 나타내는 <code>exists</code>라는 프로퍼티를 가지고 있습니다. 모델을 초기화할 때 이 값은 <code>false</code>이지만, 저장하거나 데이터베이스에서 모델을 가져온 후에는 <code>true</code>입니다. 이 값은 변경할 수 있습니다.</p>
<pre><code class="language-swift">if planet.$id.exists {
    // This model exists in database.
}</code></pre>
<h3 id="사용자-정의-식별자-custom-identifier">사용자 정의 식별자 (Custom Identifier)</h3>
<p>Fluent는 <code>@ID(custom:)</code>를 통해 사용자 정의 식별자 키와 타입을 정의할 수 있습니다.</p>
<pre><code class="language-swift">final class Planet: Model {
    // Unique identifier for this Planet.
    @ID(custom: &quot;foo&quot;)
    var id: Int?
}</code></pre>
<p>위의 예시는 <code>Int</code> 타입을 가진 <code>&quot;foo&quot;</code>라는 사용자 정의 식별자 키를 가지고 <code>@ID</code>를 정의했습니다. 이는 자동으로 증가하는 primary key를 이용하는 SQL 데이터베이스와 호환될 수 있습니다만 NoSQL과는 호환되지 않습니다.</p>
<p>사용자 정의된 <code>@ID</code>들은 <code>generatedBy</code> 파라미터를 통해 식별자가 생성되는 방식을 지정할 수 있습니다.</p>
<pre><code class="language-swift">@ID(custom: &quot;foo&quot;, generatedBy: .user)</code></pre>
<table>
<thead>
<tr>
<th>Generated By</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>.user</code></td>
<td>새로운 모델을 저장하기 전에 사용자가 <code>@ID</code> 프로퍼티를 세팅함</td>
</tr>
<tr>
<td><code>.random</code></td>
<td><code>@ID</code> 값 타입은 <code>RandomGeneratable</code> 프로토콜을 채택해야 함 (자동 생성)</td>
</tr>
<tr>
<td><code>.database</code></td>
<td>저장 시 데이터베이스가 값을 생성함</td>
</tr>
</tbody></table>
<p><code>generatedBy</code> 파라미터를 생략하면 Fluent는 <code>@ID</code> 값 타입에 따라 적절하게 추론하여 적용합니다. 예를 들어 별도로 지정되지 않으면 <code>Int</code>는 기본적으로 <code>.database</code> 설정으로 값을 생성합니다.</p>
<h2 id="생성자-initializer">생성자 (Initializer)</h2>
<p>모델은 반드시 빈 이니셜라이저 메서드를 가지고 있어야 합니다.</p>
<pre><code class="language-swift">final class Planet: Model {
    // Creates a new, empty Planet.
    init() { }
}</code></pre>
<p>Fluent는 쿼리의 결과로 반환되는 모델을 내부적으로 생성하기 위해 이 메서드를 필요로 합니다.</p>
<p>원한다면 모든 프로퍼티를 가지는 convenience initializer를 추가할 수 있습니다.</p>
<pre><code class="language-swift">final class Planet: Model {
    // Creates a new Planet with all properties set.
    init(id: UUID? = nil, name: String) {
        self.id = id
        self.name = name
    }
}</code></pre>
<p>Convenience initializer를 이용하면 향후 모델에 새로운 프로퍼티를 추가하는 작업을 편하게 만들어줄 수 있습니다.</p>
<h2 id="필드-field">필드 (Field)</h2>
<p>모델들은 데이터를 저장하기 위해 <code>@Field</code> 프로퍼티들을 가질 수 있습니다.</p>
<pre><code class="language-swift">final class Planet: Model {
    // The Planet&#39;s name.
    @Field(key: &quot;name&quot;)
    var name: String
}</code></pre>
<p>필드를 사용하려면 위의 예시와 같이 데이터베이스 키를 명시적으로 정의하여야 합니다. 다만 프로퍼티 이름과 반드시 같을 필요는 없습니다.</p>
<blockquote>
<p>Fluent는 데이터베이스 키를 <code>snake_case</code>식 이름으로, 프로퍼티 이름을 <code>camelCase</code>식 이름으로 짓는 것을 권장합니다.</p>
</blockquote>
<p>필드 값들은 <code>Codable</code>을 따르는 모든 타입으로 설정할 수 있습니다. <code>@Field</code>는 중첩 구조 (nested structures)와 배열을 저장할 수 있지만 이를 이용한 필터링 작업은 제한됩니다. 필터링이 필요하다면 <code>@Group</code>을 대체재로 알아보시면 됩니다.</p>
<p>옵셔널 값을 가지는 필드를 정의하고자 한다면 <code>@OptionalField</code>를 사용하세요.</p>
<pre><code class="language-swift">@OptionalField(key: &quot;tag&quot;)
var tag: String?</code></pre>
<h3 id="관계-relations">관계 (Relations)</h3>
<p>모델들은 <code>@Parent</code>, <code>@Children</code>, <code>@Siblings</code>와 같은 다른 모델들을 참조할 수 있는 관계 프로퍼티를 가질 수 있습니다. 관련한 내용은 <a href="https://docs.vapor.codes/4.0/fluent/relations/">Relation</a> 섹션을 확인해보세요.</p>
<h3 id="timestamp">Timestamp</h3>
<p><code>@Timestamp</code>는 <code>Foundation.Date</code>를 저장하는 <code>@Field</code>의 특수한 타입입니다. Timestamp는 선택된 트리거에 따라 Fluent가 자동적으로 세팅합니다.</p>
<pre><code class="language-swift">final class Planet: Model {
    // When this Planet was created.
    @Timestamp(key: &quot;created_at&quot;, on: .create)
    var createdAt: Date?

    // When this Planet was last updated.
    @Timestamp(key: &quot;updated_at&quot;, on: .update)
    var updatedAt: Date?
}</code></pre>
<p><code>@Timestamp</code>는 아래의 트리거를 지원합니다.</p>
<table>
<thead>
<tr>
<th>Trigger</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>.create</code></td>
<td>새로운 모델 인스턴스가 데이터베이스에 저장될 때 세팅.</td>
</tr>
<tr>
<td><code>.update</code></td>
<td>기존의 모델 인스턴스가 데이터베이스에 저장될 때 세팅.</td>
</tr>
<tr>
<td><code>.delete</code></td>
<td>모델이 데이터베이스에서 삭제될 때 세팅. soft delete 참조</td>
</tr>
</tbody></table>
<h4 id="timestamp-형식">Timestamp 형식</h4>
<p>기본적으로 <code>@Timestamp</code>는 사용하는 데이터베이스 드라이버에 따라 인코딩하기 효율적인 datetime을 사용합니다. <code>format</code> 파라미터를 통해 데이터베이스에 timestamp가 저장되는 방식을 설정할 수 있습니다.</p>
<pre><code class="language-swift">// Stores an ISO 8601 formatted timestamp representing
// when this model was last updated.
@Timestamp(key: &quot;updated_at&quot;, on: .update, format: .iso8601)
var updatedAt: Date?</code></pre>
<p>위 예시에 표현한 <code>.iso8601</code>은 마이그레이션 시 아래와 같이 <code>.string</code> 형식으로 저장되어야 한다는 것을 기억해주세요.</p>
<pre><code class="language-swift">.field(&quot;updated_at&quot;, .string)</code></pre>
<p>이용할 수 있는 timestamp 형식은 아래와 같습니다.</p>
<table>
<thead>
<tr>
<th>Format</th>
<th>Description</th>
<th>Type</th>
</tr>
</thead>
<tbody><tr>
<td><code>.default</code></td>
<td>데이터베이스에 따라 인코딩하기 효율적인 datetime을 사용함</td>
<td>Date</td>
</tr>
<tr>
<td><code>.iso8601</code></td>
<td>ISO 8601 문자열. <code>withMilliseconds</code> 파라미터 사용을 지원함</td>
<td>String</td>
</tr>
<tr>
<td><code>.unix</code></td>
<td>fraction을 포함한 Unix epoch 시점으로부터의 경과된 시간(초)</td>
<td>Double</td>
</tr>
</tbody></table>
<p><code>timestamp</code> 프로퍼티를 통해 raw timestamp 값에 직접 접근하실 수 있습니다.</p>
<pre><code class="language-swift">// Manually set the timestamp value on this ISO 8601
// formatted @Timestamp.
model.$updatedAt.timestamp = &quot;2020-06-03T16:20:14+00:00&quot;</code></pre>
<h3 id="soft-delete">Soft Delete</h3>
<p><code>.delete</code>를 <code>@Timestamp</code>에 추가하면 모델이 soft-deletion을 지원합니다.</p>
<pre><code class="language-swift">final class Planet: Model {
    // When this Planet was deleted.
    @Timestamp(key: &quot;deleted_at&quot;, on: .delete)
    var deletedAt: Date?
}</code></pre>
<p>Soft하게 삭제된 모델들은 삭제된 후에도 데이터베이스에 존재하지만 쿼리로부터 반환되지는 않습니다.</p>
<blockquote>
<p>삭제 시 timestamp를 미래의 시점으로 설정할 수 있습니다. 이는 실제로 데이터가 삭제되는 만료 시점으로 사용할 수 있습니다.</p>
</blockquote>
<p>데이터베이스로부터 soft-delete를 지원하는 모델을 강제로 삭제하고자 한다면 <code>delete</code> 시 <code>force</code> 파라미터를 사용하시면 됩니다.</p>
<pre><code class="language-swift">// Deletes from the database even if the model 
// is soft deletable. 
model.delete(force: true, on: database)</code></pre>
<p>soft하게 삭제된 모델을 복구할 때는 <code>restore</code> 메서드를 사용하시면 됩니다.</p>
<pre><code class="language-swift">// Clears the on delete timestamp allowing this 
// model to be returned in queries. 
model.restore(on: database)</code></pre>
<p>soft 삭제된 모델들을 쿼리에 포함하고 싶으시다면 <code>withDeleted</code>를 사용하시면 됩니다.</p>
<pre><code class="language-swift">// Fetches all planets including soft deleted.
Planet.query(on: database).withDeleted().all()</code></pre>
<h2 id="열거형-enum">열거형 (Enum)</h2>
<p><code>@Enum</code>은 문자열으로 표현할 수 있는 타입을 네이티브 데이터베이스 열거값으로 저장하기 위한 특수 타입 <code>@Field</code>입니다. 네이티브 데이터베이스 열거형은 데이터베이스에 타입 안전 계층을 추가하므로 raw 열거형보다 성능이 더 우수할 수 있습니다.</p>
<pre><code class="language-swift">// String representable, Codable enum for animal types.
enum Animal: String, Codable {
    case dog, cat
}

final class Pet: Model {
    // Stores type of animal as a native database enum.
    @Enum(key: &quot;type&quot;)
    var type: Animal
}</code></pre>
<p><code>RawRepresentable</code> (RawValue가 String인 경우)을 준수하는 타입만 <code>@Enum</code>과 호환됩니다. 문자열 String이 채택된 열거형은 기본적으로 이 요구 사항을 충족합니다.</p>
<p>옵셔널 형태의 열거형을 저장하고자 한다면 <code>@OptionalEnum</code>을 사용하세요.</p>
<p>마이그레이션을 통해 열거형을 다룰 수 있도록 데이터베이스를 준비해야 합니다. 자세한 내용은 <a href="https://docs.vapor.codes/4.0/fluent/schema/#enum">enum</a>을 참조하세요.</p>
<h3 id="raw-enums">Raw Enums</h3>
<p>String 또는 Int와 같이 <code>Codable</code>이 지원되는 모든 열거형을 <code>@Field</code>에 저장할 수 있습니다. 이 값은 데이터베이스에 raw value로 저장됩니다.</p>
<h2 id="group">Group</h2>
<p><code>@Group</code>을 사용하면 중첩된 필드 그룹을 모델의 단일 속성으로써 저장할 수 있습니다. <code>@Field</code>에 저장된 <code>Codable</code> 구조체들과 달리 <code>@Group</code>의 필드는 쿼리할 수 있습니다. Fluent는 데이터베이스에 <code>@Group</code>을 플랫한 구조로 저장하여 쿼리할 수 있도록 만듭니다.</p>
<p><code>@Group</code>을 사용하려면 먼저 저장하고자 하는 중첩 구조체를 작성하고 <code>Fields</code> 프로토콜을 채택합니다. 이는 식별자나 스키마 이름이 필요하지 않다는 것을 제외하고 <code>Model</code>과 매우 유사합니다. 이를 이용하여 <code>Model</code>이 지원하는 <code>@Field</code>, <code>@Enum</code>, <code>@Group</code>을 통해 많은 프로퍼티들을 저장할 수 있습니다.</p>
<pre><code class="language-swift">// A pet with name and animal type.
final class Pet: Fields {
    // The pet&#39;s name.
    @Field(key: &quot;name&quot;)
    var name: String

    // The type of pet. 
    @Field(key: &quot;type&quot;)
    var type: String

    // Creates a new, empty Pet.
    init() { }
}</code></pre>
<p>필드들을 만든 후에 <code>@Group</code> 프로퍼티를 값으로 사용할 수 있습니다.</p>
<pre><code class="language-swift">final class User: Model {
    // The user&#39;s nested pet.
    @Group(key: &quot;pet&quot;)
    var pet: Pet
}</code></pre>
<p><code>@Group</code>의 필드들에는 dot 문법으로 접근할 수 있습니다.</p>
<pre><code class="language-swift">let user: User = ...
print(user.pet.name) // String</code></pre>
<p>property wrapper에서 사용하던 dot 문법과 같이 중첩된 필드에 쿼리할 수 있습니다.</p>
<pre><code class="language-swift">User.query(on: database).filter(\.$pet.$name == &quot;Zizek&quot;).all()</code></pre>
<p>데이터베이스에서 <code>@Group</code>은 <code>_</code>로 연결된 키를 가진 플랫 구조로 저장됩니다. 아래는 데이터베이스에서 <code>User</code>가 어떻게 표시되는지 보여 주는 예시입니다.</p>
<table>
<thead>
<tr>
<th align="center">id</th>
<th align="center">name</th>
<th align="center">pet_name</th>
<th align="center">pet_type</th>
</tr>
</thead>
<tbody><tr>
<td align="center">1</td>
<td align="center">Tanner</td>
<td align="center">Zizek</td>
<td align="center">Cat</td>
</tr>
<tr>
<td align="center">2</td>
<td align="center">Logan</td>
<td align="center">Runa</td>
<td align="center">Dog</td>
</tr>
</tbody></table>
<h2 id="codable">Codable</h2>
<p>모델들은 기본적으로 <code>Codable</code>을 채택하고 있습니다. 모델에 Vapor의 <code>content</code>를 채택함으로써 Codable을 함께 채택하게 됩니다.</p>
<pre><code class="language-swift">extension Planet: Content { }

app.get(&quot;planets&quot;) { req in 
    // Return an array of all planets.
    Planet.query(on: req.db).all()
}</code></pre>
<p><code>Codable</code>을 통해 데이터를 인코딩/디코딩할 때 모델 프로퍼티들은 키 대신에 가지고 있는 변수 이름을 사용합니다.</p>
<h2 id="data-transfer-object-dto">Data Transfer Object (DTO)</h2>
<p>이 내용은 추후 Create, Read, Update, Delete (CRUD) 기능을 구현할 때 다시 다루도록 하겠습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Swift] 체육복 - 프로그래머스 Lv 1]]></title>
            <link>https://velog.io/@ryan-son/Swift-%EC%B2%B4%EC%9C%A1%EB%B3%B5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-Lv-1</link>
            <guid>https://velog.io/@ryan-son/Swift-%EC%B2%B4%EC%9C%A1%EB%B3%B5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-Lv-1</guid>
            <pubDate>Tue, 06 Jul 2021 05:36:05 GMT</pubDate>
            <description><![CDATA[<p>Swift로 프로그래머스 체육복 문제를 해결하며 얻은 지식을 정리합니다.</p>
<p><a href="url">문제로 이동</a></p>
<p><img src="https://images.velog.io/images/ryan-son/post/acbec7eb-c1e8-4db5-9b35-f34708760ae4/image.png" alt=""></p>
<h2 id="풀이">풀이</h2>
<pre><code class="language-swift">import Foundation

func solution(_ n: Int, _ lost: [Int], _ reserve: [Int]) -&gt; Int {
    var losts = Set(lost).subtracting(Set(reserve))
    var participants = Set(1...n).subtracting(losts)
    let reservables = reserve.filter { !lost.contains($0) }

    reservables.forEach {
        let previousNumber = $0 - 1 == 0 ? 1 : $0 - 1
        let followingNumber = $0 + 1 == 0 ? 1 : $0 + 1

        if losts.contains(previousNumber) {
            losts.remove(previousNumber)
            participants.insert(previousNumber)
        } else if losts.contains(followingNumber) {
            losts.remove(followingNumber)
            participants.insert(followingNumber)
        }
    }

    return participants.count
}</code></pre>
<h2 id="고찰">고찰</h2>
<p>체육복을 도난 당한 <code>lost</code> 배열에서 여분이 없어 수업에 참가할 수 없는 학생들을 <code>losts</code>로, 그 중 빌리지 않고도 현재 수업에 참가할 수 있는 학생을 <code>participants</code>로, 체육복의 여벌이 남아 실제로 빌려줄 수 있는 학생들을 <code>reservables</code>로 정의하였고, <code>reservables</code> 내부의 각 학생들이 자신의 전후 번호에게 넘겨주고 전달 받은 학생이 <code>losts</code>에서 제거하는 행동을 수행하였습니다.</p>
<h2 id="사용한-개념">사용한 개념</h2>
<ul>
<li>Set</li>
<li>filter(_:)</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>