<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>tkppp-dev.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Fri, 28 Oct 2022 07:07:04 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>tkppp-dev.log</title>
            <url>https://images.velog.io/images/tkppp-dev/profile/23ea171b-6908-4299-bbbf-384b658d594c/KakaoTalk_20211209_223344473.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. tkppp-dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/tkppp-dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[왜 Node.js는 자바보다 빠른가?]]></title>
            <link>https://velog.io/@tkppp-dev/%EC%99%9C-Node.js%EB%8A%94-%EC%9E%90%EB%B0%94%EB%B3%B4%EB%8B%A4-%EB%B9%A0%EB%A5%B8%EA%B0%80</link>
            <guid>https://velog.io/@tkppp-dev/%EC%99%9C-Node.js%EB%8A%94-%EC%9E%90%EB%B0%94%EB%B3%B4%EB%8B%A4-%EB%B9%A0%EB%A5%B8%EA%B0%80</guid>
            <pubDate>Fri, 28 Oct 2022 07:07:04 GMT</pubDate>
            <description><![CDATA[<h2 id="알고-있었지만-외면한-것">알고 있었지만 외면한 것</h2>
<p>Node.js가 빠른 이유를 구글에 검색하면 수많은 결과가 나온다. 많은 글에서 싱글스레드 기반의 비동기 이벤트 루프를 사용하기 때문에 빠르다고 한다. 더 깊게 들어가면 싱글스레드인 메인 스택에서 로직을 처리하고 비동기 작업(특히 IO 관련 작업)을 처리하는 스레드 풀이 존재하기 때문에 비동기 작업을 <strong>멀티스레드</strong>로 처리한다는 점이다.</p>
<p>일반적으로 논블로킹과 블로킹 방식의 차이를 물어보면 음식점의 예를 많이 든다. 논블로킹은 주문 순서대로 처리하는게 아닌 빨리 처리되는 순으로 음식을 내오고 블로킹 방식은 주문 받은 순서대로 음식을 만들고 내오기 때문에 응답속도의 면에서 차이가 난다는 것을 강조한다.</p>
<p>이 그대로 블로킹과 논블로킹의 차이를 면접에서 대답했는데 바로 꼬리질문이 들어왔다. <code>스프링부트처럼 멀티스레드로 요청을 처리하면 응답속도면에서 노드와 차이가 없기 때문에 성능적으로 차이가 나지 않는게 아닌지?</code> 라는 질문을 받았다. 단순히 그렇다는 사실만 알고 깊게 생각하지 않았기 때문에 외면했을뿐 사실 응답속도 측면에서는 멀티스레드로 받으면 차이가 나는지 않는 것을 알고 있었다. 결국 대답을 하지 못했고 그렇게 알고 있다고만 대답하고 넘어갔다.</p>
<p>다행히 1차 기술면접은 통과했지만 의문이 생겨 이를 찾아 정리한다.</p>
<h2 id="비동기-처리란-무엇인가">비동기 처리란 무엇인가?</h2>
<h3 id="nodejs">Node.js</h3>
<p>노드가 싱글스레드 기반으로 처리된다고 하여 처음 배우는 사람들이 오해하는 것이 정말로 스레드 하나만 사용되는거고 비동기처리는 특수하게 처리되는 것이라는 것이다<em>(저만 그랬을 수도 있구요... 아니라면 ㅈㅅ)</em></p>
<p>사실 비동기 처리는 멀티 스레드로 이루어지는 작업이다. 아니 근데 노드는 싱글 스레드라면서 왜 멀티 스레드죠? 하고 반문할 수 있다. 그것은 이벤트루프에서 콜스택이라 부르는 메인 처리 스택이 한 개이기 때문에 싱글스레드라는 것이다. 다만 비동기 작업을 수행해야 할 때는 이를 담당하는 스레드 풀에 작업을 위임하고 싱글스레드는 계속 돌아가다 위임한 비동기 작업이 완료되면 다시 메인 스택에서 작업을 수행하기에 싱글 스레드라고 하는 것이다.</p>
<h3 id="spring">Spring</h3>
<p>마찬가지로 스프링이 다중 요청을 받으면 미리 생성해둔 스레드풀에서 스레드를 꺼내 할당해 작업을 수행한다. 즉 요청 할당은 멀티스레드를 활용해 비동기적으로 작동하는 것이기 때문에 들어온 순서대로 요청이 처리되지 않는다. 따라서 응답속도에 관한 위의 대답은 틀린 것이 된다.</p>
<h2 id="node와-spring의-차이">Node와 Spring의 차이</h2>
<p>요청을 받는 부분은 서로 비동기적으로 작동하기 때문에 동일하다면 어디서 성능 차이가 발생하는 것일까. 차이점은 노드는 IO 작업에 완전한 비동기 처리를 지원한다는 것이다.</p>
<p>요청을 비동기로 받는 것까진 동일하지만 Node는 하나의 요청 처리 과정 내에서 필요하다면 비동기로 작업을 위임하고 기다리지 않고 다른 작업을 수행한다. 따라서 기다리는 시간이 없거나 매우 적다!</p>
<p>하지만 스프링은 IO 작업과 같이 시간과 리소스를 필요로 하는 작업을 한다면 요청 처리를 위해 할당받은 스레드가 대기하게 된다. 요청받는 것은 비동기로 받지만 언어의 특성상 요청의 처리는 블로킹 방식으로 진행되는 것이다. 또 요청이 많아질수록 스레드의 개수가 많아질 것이고 이로 인한 컨텍스트 스위칭으로 인해 오버헤드가 발생하게 된다. 결국 스레드의 대기 시간과 컨텍스트 스위칭의 오버헤드가 성능 차이를 불러오게 된다</p>
<p>이를 그림으로 나타내면 아래와 같다
<img src="https://velog.velcdn.com/images/tkppp-dev/post/a94250b0-e973-4d0f-ab98-17cf74483311/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/tkppp-dev/post/cc930287-718e-4912-8781-606bac742b16/image.png" alt=""></p>
<p>이러한 차이 때문에 성능적 차이가 나타나는 것이다</p>
<h2 id="asyncawait">Async/Await</h2>
<p>노드에서 <code>async/await</code> 키워드를 사용하면 프로그램의 흐름을 마치 블로킹 방식처럼 돌아가는 것처럼 느껴진다. 비동기 함수를 기다리기 때문에 블로킹 방식 아니야? 라고 생각할 수 있을 것이다. 하지만 <code>async/await</code> 은 <code>Promise</code>를 사용하기 위한 문법적 설탕이라는 것을 기억해야 한다. async 함수는 실제적으로 프로미스를 반환하며 await 뒤의 코드는 프로미스의 <code>then</code> 블록이다. 따라서 <code>async/await</code> 은 논블로킹을 작동한다</p>
<h2 id="결론">결론</h2>
<p>노드의 싱글 스레드 방식이 효과적이려면 3가지를 만족해야한다</p>
<ol>
<li>다중처리를 동시에 처리하도록 요구된다.</li>
<li>많은 I/O 작업을 수행한다.</li>
<li>단순한 CPU 작업만을 진행한다.</li>
</ol>
<p>이는 현 웹서비스의 특징들이기 때문에 노드의 이점이 잘 드러나게 된다. 하지만 IO 작업이 주가 아닌 무거운 CPU 연산이 필요하다면 싱글 스레드의 단점이 드러나게 된다. 따라서 서비스의 특징에 따라서 선택해야 한다.</p>
<blockquote>
<h4 id="참고">참고</h4>
<p><a href="https://siyoon210.tistory.com/164">Node.js가 Spring MVC보다 성능상 유리할까?</a>
<a href="https://ojt90902.tistory.com/650">Spring MVC : 동시요청 멀티 쓰레드 간단한 개념</a>
<a href="https://strongloop.com/strongblog/node-js-is-faster-than-java/">What Makes Node.js Faster Than Java?
</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Express + Typeorm] 프로젝트 리팩토링과 테스트 코드를 작성하면서 이틀동안 겪은 삽질 정리]]></title>
            <link>https://velog.io/@tkppp-dev/Express-Typeorm-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%EA%B3%BC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%9E%91%EC%84%B1%ED%95%98%EB%A9%B4%EC%84%9C-%EC%9D%B4%ED%8B%80%EB%8F%99%EC%95%88-%EA%B2%AA%EC%9D%80-%EC%82%BD%EC%A7%88-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@tkppp-dev/Express-Typeorm-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%EA%B3%BC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%9E%91%EC%84%B1%ED%95%98%EB%A9%B4%EC%84%9C-%EC%9D%B4%ED%8B%80%EB%8F%99%EC%95%88-%EA%B2%AA%EC%9D%80-%EC%82%BD%EC%A7%88-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 07 Sep 2022 18:21:51 GMT</pubDate>
            <description><![CDATA[<h1 id="시작">시작</h1>
<p>곧 다가오는 롤드컵 시즌에 앞서 개인적으로 혼자 사용하던 <code>스포츠 결과 한눈에 보기</code> 서비스에 롤드컵 기능을 추가하기 전에 프로젝트 리팩토링와 미뤄왔던 테스트 코드를 작성하기로 하였다.</p>
<p>프로젝트 리팩토링은 <code>도메인 주도 개발론</code>을 토대로 프로젝트 구조를 변경하고 서비스 레이어에 모든 비즈니스 로직이 노출되어 있는 것을 개별의 도메인 서비스로 분리하고 코드의 추상화 레벨을 높이는데 주력했다. (사실 제대로 한건지는 모르겠지만...) </p>
<p>도메인 객체에 비즈니스 로직을 작성하는 것이 아니라 이렇게 진행한 이유는 <code>JPA</code>와 다르게 <code>Typeorm</code>은 더티 체킹을 지원하지 않기 때문에 엔티티를 변경하더라도 save() 메소드를 서비스 레이어에서 호출하는것이 맘에 들지 않아 이를 감싸는 비즈니스 로직을 추상화한 도메인 서비스를 만드는 방식을 선택했다</p>
<h1 id="문제-상황-1---테스트-db">문제 상황 1 - 테스트 DB</h1>
<p>첫번째 문제는 테스트 DB를 어떻게 하느냐였다. 도메인 모델부터 테스트 코드를 작성하려 했는데 노드에서는 자바 진영의 <code>H2 데이터베이스</code> 와 같은 다양한 DB 문법을 지원하는 메모리 DB가 없었다. 그렇다고 운영 DB를 사용할 순 없으니 로컬 DB를 테스트 용으로 사용할까 했지만 배포환경에서도 테스트가 실행될텐데 로컬 DB를 사용하는 것은 불가하다고 생각했다 </p>
<p>결국 고민과 구글링 끝에 <code>도커</code>를 사용해 테스트 DB 환경을 구축해 테스트 시에만 컨테이너를 올리고 테스트 종료 시 컨테이너를 내리는 방식으로 테스트 DB를 세팅했다. 자세한 내용은 <a href="https://jaehoney.tistory.com/114">여기</a>를 참조</p>
<h1 id="문제-상황-2---날짜와-시간이-안맞아">문제 상황 2 - 날짜와 시간이 안맞아!</h1>
<p>이전에는 경기 날짜와 경기 시작 시간을 각각의 컬럼으로 나누었는데 이를 위해 컬럼 정의 시 컬럼 타입을 각각 <code>Date</code> 와 <code>Time</code>을 사용했다. 문제는 이렇게 사용할 시 <code>typeorm</code>은 엔티티의 타입은 <code>Date</code> 인데 DB에서 가져올 시 <code>string</code> 타입의 값을 가져온다는 것이다. 타입 불일치를 해결하기 위해 <code>moment</code> 라이브러리로 <code>Date</code> 객체로 변환해서 사용하고 있었는데 이럴 바에 차라리 자동으로 <code>Date</code> 객체로 직렬화/역직렬화 되는 <code>datetime</code> 컬럼 타입으로 변경하기로 하였다</p>
<h2 id="재앙의-시작">재앙의 시작</h2>
<p>이렇게 바꾸고 간단하게 CRUD 테스트를 진행했다. DB에는 제대로 잘 저장된 것을 확인했다. 그리고 생각없이 엔티티 객체를 출력해 보았다.</p>
<pre><code class="language-ts">const match = await Match.find()
console.log(match)    // 엔티티의 경기 시간 프로퍼티 값: UTC 시간대 출력</code></pre>
<p>현재 시간보다 9시간이 이른 UTC 시간이 출력되었다. 실제 프론트에는 KST 기준의 시간을 넘겨야 했기 때문에 매번 이를 보정해주는건 문제라고 생각했다. 따라서 삽질을 시작했다</p>
<h3 id="삽질-1---node-타임존-설정">삽질 1 - Node 타임존 설정</h3>
<p>기본적으로 노드는 타입존이 설정되어 있지 않다. 따라서 환경 변수를 통해 타임존을 설정했다. 하지만 문제가 해결되지 않았다. 이 부분에 대한 내용은 뒤에서 따로 설명</p>
<pre><code class="language-ts">// .dotenv
TZ=&#39;Asia/Seoul&#39;</code></pre>
<h3 id="삽질-2---typeorm-타임존-설정">삽질 2 - typeorm 타임존 설정</h3>
<p>열심히 구글링 해보니 <code>typeorm</code>은 로컬 시간대에 맞춰 UTC 시간대와의 차이만큼 보정해준다는 것을 알아내었다. 따라서 <code>Date</code> 객체가 UTC 시간이니 <code>typeorm</code>의 타임존을 UTC로 설정하고 Date 객체를 생성할 때 9시간을 더해주는 팩토리 함수를 만들어 사용하면 되지 않을까 생각하여 <code>Datesource</code> 설정의 <code>timezone</code> 옵션을 &#39;Z&#39;(UTC)로 설정했다</p>
<pre><code class="language-ts">export const MysqlDateSource = new DataSource({
  // options...
  timezone: &#39;Z&#39;    // &#39;local&#39;, &#39;Z&#39;(UTC), &#39;+HH:MM&#39;(오프셋 직접 설정), &#39;-HH:MM&#39;(오프셋 직접 설정)
})</code></pre>
<p>이렇게 문제를 해결하나 싶었지만... 경기 스케줄 저장 시 시간을 Date 객체의 <code>setXX</code> 메소드를 이용해 직접 설정하는데 이렇게 할 시 UTC 시간으로 되돌아갔다. DB에도 UTC로 저장이 되버려 &#39;Z&#39; 옵션은 사용하지 못하게 되었다. 여기서 문제를 찾기 위해 반나절을 삽질했던것 같다</p>
<h1 id="해결---date-객체의-특징">해결 - Date 객체의 특징</h1>
<p>삽질을 계속하다 Date 객체의 MDN 문서를 보다가 이런 <code>setXX, getXX</code> 메소드에서 이런 문구를 발견했다. <strong>주어진 날짜의 현지 시간 기준</strong>.</p>
<p>결론은 이렇다. Date 객체를 <code>toString</code>, <code>toLocaleString</code> 등의 메소드를 사용하지 않고 출력하면 로컬 타임존과 관련없이 UTC 시간으로 출력된다. 하지만 <code>setXX, getXX</code> 메소드는 현지 시간이 기준이다. 즉 출력이 UTC로 되었다고 해서 실제 Date 객체의 시간이 UTC 시간이 아니었던 것이었다</p>
<p>어이없게도 실제로 문제가 아닌데 문제라고 생각해서 발생한 해프닝이었다. 이 해프닝으로 인해 하루 가까이를 해맸지만 자바스크립트에서 날짜를 다루는데 이해가 높아지는 나쁘지 않은 해프닝이라 생각한다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Node] Express + Typescript에서 Jest로 테스트하기 - 모킹]]></title>
            <link>https://velog.io/@tkppp-dev/Node-Express-Typescript%EC%97%90%EC%84%9C-Jest%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0-%EB%AA%A8%ED%82%B9</link>
            <guid>https://velog.io/@tkppp-dev/Node-Express-Typescript%EC%97%90%EC%84%9C-Jest%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0-%EB%AA%A8%ED%82%B9</guid>
            <pubDate>Sun, 04 Sep 2022 07:28:19 GMT</pubDate>
            <description><![CDATA[<p>IO 작업을 수행한다거나 API 호출 제한이 걸리는 종류의 함수들은 실제 테스트에서 호출하기 부담스럽다. 따라서 테스트하기 어려운 함수들의 경우 모의 함수(Mock Function)으로 만들어(이를 <code>Mocking</code>이라고 한다) 테스트를 진행할 수 있다.</p>
<p>이번 포스트에서는 Jest에서 모킹하는 방법을 알아본다</p>
<h1 id="모킹mocking">모킹(Mocking)</h1>
<h2 id="jestfncallback--undefined">jest.fn(callback | undefined)</h2>
<pre><code class="language-ts">const mockFn = jest.fn()</code></pre>
<p>이렇게 생성된 모의 함수는 호출 시 <code>undefined</code>를 반환한다. 만약 인자로 함수를 넘긴다면 해당 함수가 실행된 결과를 반환한다</p>
<h2 id="mock-프로퍼티">.mock 프로퍼티</h2>
<p>모의 함수는 특별한 <code>.mock</code> 프로퍼티를 가지고 함수가 호출된 방법과 반환된 함수에 대한 데이터가 담긴 객체가 저장되어 있다</p>
<h3 id="calls">calls</h3>
<p>모의 함수가 호출될 때 넘겨진 매개변수들이 저장 배열에 저장되어 있다</p>
<pre><code class="language-ts">mockFn(1, 2)
mockFn(&#39;1&#39;, &#39;2&#39;)</code></pre>
<p>위와 같은 순서로 모의 함수를 실행시켰을 때 calls 의 값은 아래와 같다</p>
<pre><code class="language-ts">mock.calls = [[1, 2], [&#39;1&#39;, &#39;2&#39;]]</code></pre>
<p>따라서 모의 함수에 어떤 매개변수가 넘겨져 실행되었는지를 확인할 수 있고, 배열이기 때문에 <code>length</code> 프로퍼티를 이용해 모의 함수가 실행되었는지를 확인할 수도 있다</p>
<h3 id="results">results</h3>
<p>모의 함수가 반환한 결과를 담은 객체가 저장된 배열이다. 결과 객체는 <code>{ type: &#39;return&#39; | &#39;throw&#39;, value: any }</code> 이와 같다</p>
<pre><code class="language-ts">[
  {
    type: &#39;return&#39;,
    value: &#39;result1&#39;,
  },
  {
    type: &#39;throw&#39;,
    value: {
      /* Error instance */
    },
  },
  {
    type: &#39;return&#39;,
    value: &#39;result2&#39;,
  },
];</code></pre>
<h3 id="contexts">contexts</h3>
<p>모의 함수가 호출되었던 컨텍스트들이 배열에 저장되어 있다</p>
<pre><code class="language-ts">  const context1 = {
    a: 1
  }

  const context2 = {
    a: 2
  }

  const mockFn = jest.fn(function(this: any){    
    console.log(this.a)
  })
  mockFn.call(context1)        // 1
  mockFn.call(context2)        // 2

  expect(mockFn.mock.contexts[0]).toEqual(context1)
  expect(mockFn.mock.contexts[1]).toEqual(context2)</code></pre>
<h3 id="instances">instances</h3>
<p>모의 함수가 <code>new</code> 키워드로 초기화(인스턴스화) 되었을 떄의 객체를 배열에 담고 있다</p>
<pre><code class="language-ts">const mockFn = jest.fn()

const a = new mockFn()
const b = new mockFn()

mockFn.mock.instances[0] === a; // true
mockFn.mock.instances[1] === b; // true</code></pre>
<h3 id="lastcall">lastCall</h3>
<p>모의 함수가 마지막으로 호출될 때의 매개변수를 저장한 배열이다. 즉,<code>calls[-1]</code> </p>
<h1 id="stub">stub</h1>
<p>모의 함수가 특정한 동작을 하도록 로직을 전달할 수도 있지만 단순히 특정한 결과만 반환하게 하는 것이 필요할 때가 있다. 이를 <code>stub</code> 이라고 하단</p>
<h2 id="mockreturnvalue">mockReturnValue</h2>
<p>동기 함수를 <code>stub</code> 하는데 사용한다. <code>mockReturnValueOnce</code> 를 사용하면 첫 호출 시에만 지정된 결과를 반환하고 체이닝을 통해 n번째 호출 결과를 지정할 수 있다</p>
<pre><code class="language-ts">test(&#39;stub&#39;, () =&gt; {
  const mockFn = jest.fn()
  const mockFnReturnOnce = jest.fn()

  mockFn.mockReturnValue(&#39;return value&#39;)
  mockFnReturnOnce.mockReturnValueOnce(&#39;return once1&#39;)  // 체이닝을 통해 지정 가능
    .mockReturnValueOnce(&#39;return once2&#39;)
    .mockReturnValueOnce(&#39;return once3&#39;)

  console.log(mockFn())             // return value
  console.log(mockFnReturnOnce())   // return once1
  console.log(mockFnReturnOnce())   // return once2
  console.log(mockFnReturnOnce())   // return once3
})
</code></pre>
<h2 id="mockresolvedvalue">mockResolvedValue</h2>
<p>비동기 함수를 <code>stub</code> 하는데 사용한다. 마찬가지로 <code>mockResolvedValueOnce</code> 함수가 존재한다. 프로미스를 반환하기 때문에 비동기 처리가 필요하다</p>
<pre><code class="language-ts">test(&#39;stub&#39;, () =&gt; {
  // 타입스크립트에서는 jest.fn()의 반환값을 명시해주어야 에러 발생 X
  const asyncMockFn = jest.fn&lt;() =&gt; Promise&lt;string&gt;&gt;().mockResolvedValue(&#39;async return&#39;)
  const result = await asyncMockFn()

  expect(result).toBe(&#39;async return&#39;)
})</code></pre>
<h1 id="모듈-모킹">모듈 모킹</h1>
<p>테스트를 작성할 때 외부 모듈이나 모듈 전체를 모킹할 필요할 때가 있다. <code>jest.mock()</code> 함수를 통해 모듈을 한번에 모킹할 수 있다</p>
<pre><code class="language-ts">import { jest } from &#39;@jest/globals&#39;
import axios from &quot;axios&quot;;

jest.mock(&#39;axios&#39;)

test(&#39;module mocking&#39;, async () =&gt; {
  axios.get.mockResolvedValue(&#39;mock return&#39;)

  const resp = await mockAxios.get(&#39;/&#39;)
  expect(resp).toBe(&#39;mock return&#39;)
})</code></pre>
<p>자바스크립트라면 문제가 생기지 않지만 타입스크립트에서 위의 코드는 에러가 발생한다. 그 이유는 <code>jest.mock</code> 을 통해 외부 모듈을 모킹하기는 했지만 타입 캐스팅이 일어나지 않아 타입스크립트는 원래의 타입으로 알고 있기 때문에 <code>mockResolvedValue</code> 함수가 존재하지 않다는 에러를 발생시킨다</p>
<p>이러한 문제를 해결하기 위해서는 별도의 변수에 모킹한 모듈을 타입 캐스팅해주어야만 테스트가 정상적으로 진행된다</p>
<pre><code class="language-ts">import { jest } from &#39;@jest/globals&#39;
import axios from &quot;axios&quot;;

jest.mock(&#39;axios&#39;)
const mockAxios = axios as jest.Mocked&lt;typeof axios&gt;    // 타입 캐스팅

test(&#39;module mocking&#39;, async () =&gt; {
  axios.get.mockResolvedValue(&#39;mock return&#39;)    // 에러 발생 X

  const resp = await mockAxios.get(&#39;/&#39;)
  expect(resp).toBe(&#39;mock return&#39;)
})</code></pre>
<p>하지만 매번 모듈을 타입캐스팅해주는 것은 불편하고 번거롭다. <code>jest-mock</code> 라이브러리의 <code>mocked(module, { shallow?: boolean })</code> 함수를 사용하면 편하게 문제를 해결할 수 있다</p>
<pre><code class="language-bash">$ npm install -D jest-mock</code></pre>
<pre><code class="language-ts">import { jest } from &#39;@jest/globals&#39;
import { mocked } from &#39;jest-mock&#39;
import axios from &quot;axios&quot;;

jest.mock(&#39;axios&#39;)
const mockAxios = mocked(axios)

test(&#39;module mocking&#39;, async () =&gt; {
  axios.get.mockResolvedValue(&#39;mock return&#39;)

  const resp = await mockAxios.get(&#39;/&#39;)
  expect(resp).toBe(&#39;mock return&#39;)
})</code></pre>
<blockquote>
<p>mocked() 함수는 원래 ts-jest 라이브러리의 27.x &lt;= version 에 존재하는 함수였다. 하지만 28버전으로 업데이트 되면서 삭제되었고 jest-mock 라이브러리로 옮겨졌다. 만약 27버전 이하의 ts-jest를 사용하고 있다면 <code>import { mocked } from &#39;ts-jest/utils&#39;</code>를 사용하면 된다</p>
</blockquote>
<blockquote>
<h4 id="추가">추가</h4>
<p><code>jest.mocked(moduleName: string, deep: boolean | undefined = false)</code> 라는 메소드가 <code>jest-mock</code> 라이브러리의 <code>mocked</code> 와 같은 역할을 한다. 굳이 라이브러리를 설치할 필요가 없으므로 <code>jest.mocked()</code>를 사용하는 것이 나아보인다</p>
</blockquote>
<h2 id="부분적-모듈-모킹">부분적 모듈 모킹</h2>
<p>모듈의 모든 부분을 모킹하는 것이 아니라 일부만 모킹하고 싶을 수 있다. 이 때도 마찬가지로 <code>jest.mock(moduleName, callback)</code> 함수를 사용하면 된다</p>
<pre><code class="language-ts">// foo.ts
export const foo = &#39;foo&#39;;
export const bar = () =&gt; &#39;bar&#39;;
export default () =&gt; &#39;baz&#39;;

// module.test.ts
import exModule, { foo, bar } from &#39;./foo&#39;

jest.mock(&#39;./foo&#39;, () =&gt; {
  const original = jest.requireActual(&#39;./foo&#39;) as object

  return {
    __esModule: true,
    ...original,
    default: () =&gt; &#39;mock baz&#39;,
    foo: &#39;mocked foo&#39;,
  }
})

test(&#39;mock partial&#39;, () =&gt; {
  expect(exModule()).toBe(&#39;mock baz&#39;)
  expect(foo).toBe(&#39;mocked foo&#39;)
  expect(bar()).toBe(&#39;bar&#39;)
})</code></pre>
<blockquote>
<p>commonJS 모듈이 아닌 ES 모듈을 사용할 경우 <code>export default</code> 로 내보낸 것은 
<code>jest.mock(moduleName)</code> 함수로 모킹되지 않는다. 테스트를 해야한다면 모듈의 기본 
export 를 사용하기 보단 개별 export 하는 것이 좋을 것 같다.</p>
<p>하지만 기본 export의 모킹이 필요하다면 위의 부분 모듈 모킹처럼 <code>__esModule</code>을 
<code>true</code>로 설정하고 default 프로퍼티로 모의 함수나 구현을 만들어 넘기면 된다.</p>
</blockquote>
<h1 id="spy">Spy</h1>
<p>테스트에 모킹하는 것이 아니라 실제 로직이 실행되면서 함수가 단순히 어떻게, 몇번 호출되었는지만 확인하고 싶은 경우가 있다. 그런 경우에는 특정 함수를 추적하는 <code>spyInstance</code>를 만들어 해결할 수 있다.</p>
<h2 id="spyonobject-methodname-string">spyOn(object, methodName: string)</h2>
<p><code>spyOn</code> 함수는 <code>mock</code> 객체를 반환하지만 특정 함수를 추적한다는 차이점이 있다.</p>
<pre><code class="language-ts">import * as moduleObj, { bar } from &#39;./foo&#39;
import { bar } from &#39;./foo&#39;;

test(&#39;spyOn es6 module&#39;, () =&gt; {
  const spyBar = jest.spyOn(moduleObj, &#39;bar&#39;)

  console.log(bar())
  expect(spyBar).toHaveBeenCalledTimes(1)
}) </code></pre>
<blockquote>
<p><code>spyOn</code>의 첫번째 매개변수는 객체이다. 일반적인 ES 모듈의 import 는 객체없이 함수만 반환하기 때문에 첫번째 매개변수에 넘길 객체가 없다. 이런 경우에는 <code>import * as moduleName</code> 을 사용하여 모듈을 객체로 감싸 import 하고 매개변수로 넘기면 된다</p>
</blockquote>
<h2 id="spyonobject-methodname-string-access-get--set">spyOn(object, methodName: string, access: &#39;get&#39; | &#39;set&#39;)</h2>
<p>일반 메소드가 아닌 게터와 세터를 <code>spy</code> 하는데 사용한다</p>
<pre><code class="language-ts">// foo.ts
export class Foo{
    _bar: string
      get bar() {
      return this._bar
    }
}

// test.ts
import { Foo } from &#39;./foo&#39;

test(&#39;spyOn - getter/setter&#39;, () =&gt; {
  const spyFooGetter = jest.spyOn(Foo, &#39;bar&#39;, &#39;get&#39;)

  console.log(Foo.bar)
  expect(spyFooGetter).toHaveBeenCalledTimes(1)
}) 
</code></pre>
<h1 id="모의-함수-초기화">모의 함수 초기화</h1>
<p>매 테스트 수트마다 모킹 및 스텁을 다르게 해주어야 하는 경우가 많다. 그러한 경우 매 테스트 실행시 마다 모의 함수를 초기화 해주는 것이 필요하다. <code>Jest</code>는 이러한 경우에 필요한 메소드를 제공한다</p>
<h2 id="mockclear">mockClear()</h2>
<p>&#39;.mock&#39; 프로퍼티에 담긴 객체를 초기화한다. 단 객체 값을 덮어 씌우는 것이 아니라 새로운 객체로 대체하는 것이기 때문에 초기화 전 변수에 <code>.mock</code> 프로퍼티에 저장된 객체를 저장해놨다면 이전의 모의 함수의 정보에 접근할 수 있기 때문에 주의가 필요하다</p>
<h2 id="mockreset">mockReset()</h2>
<p><code>mockClear</code> + <code>mockReturnValues</code> 등 <code>stub</code> 한 결과들을 기본 상태인 <code>jest.fn()</code> 으로 초기화한다</p>
<h2 id="mockrestore">mockRestore()</h2>
<p>공식 문서에 따르면 <code>mockRestore()</code>는 <code>mockReset()</code>에 더해 원래 구현을 되돌린다고 설명한다. 일반적인 모의 함수는 원래 구현이 없기 때문에 <code>mockReset()</code>과 다를바가 없으나 <code>spyOn()</code>을 통해 만들어진 목 객체의 기본 구현을 되돌리는 역할을 한다</p>
<pre><code class="language-ts">// foo.ts
export const bar = () =&gt; &#39;bar&#39;;

// test.ts
import * as moduleObj from &#39;./foo&#39;
import { bar } from &#39;./foo&#39;;

test(&#39;spyOn - restore&#39;, () =&gt; {
  const spy = jest.spyOn(moduleObj, &#39;bar&#39;)
  console.log(bar())    // &#39;bar&#39;

  spy.mockImplementation(() =&gt; &#39;mocking spy bar&#39;)
  console.log(bar())    // &#39;mocking spy bar&#39;

  spy.mockRestore()
  console.log(bar())    // &#39;bar&#39;
})</code></pre>
<h2 id="정적-초기화-함수">정적 초기화 함수</h2>
<p><code>jest</code> 객체에 접근해 모든 목 객체에 영향을 주는 초기화 함수가 존재한다</p>
<ul>
<li>jest.clearAll()</li>
<li>jest.resetAll()</li>
<li>jest.restoreAll()</li>
</ul>
<p>상황에 따라 훅 함수와 연계해 사용하면 된다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Node] Express + Typescript에서 Jest로 테스트하기 - 셋업 & 해제]]></title>
            <link>https://velog.io/@tkppp-dev/Node-Express-Typescript%EC%97%90%EC%84%9C-Jest%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0-%EC%85%8B%EC%97%85-%ED%95%B4%EC%A0%9C</link>
            <guid>https://velog.io/@tkppp-dev/Node-Express-Typescript%EC%97%90%EC%84%9C-Jest%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0-%EC%85%8B%EC%97%85-%ED%95%B4%EC%A0%9C</guid>
            <pubDate>Thu, 01 Sep 2022 17:12:14 GMT</pubDate>
            <description><![CDATA[<p>테스트 이전 DB 연결과 같은 작업이 필요할 수 있다. <code>Jest</code>는 훅 함수를 통해 <code>Setup</code>과 <code>TearDown</code> 작업을 지원한다</p>
<h1 id="훅-함수">훅 함수</h1>
<h2 id="beforeeach-aftereach">beforeEach, afterEach</h2>
<p>매 테스트 마다 반복하는 작업을 지정할 때 사용</p>
<pre><code class="language-ts">beforeEach(() =&gt; {
  initializeCityDatabase();
});

afterEach(() =&gt; {
  clearCityDatabase();
});

test(&#39;city database has Vienna&#39;, () =&gt; {
  expect(isCity(&#39;Vienna&#39;)).toBeTruthy();
});

test(&#39;city database has San Juan&#39;, () =&gt; {
  expect(isCity(&#39;San Juan&#39;)).toBeTruthy();
});</code></pre>
<p>비동기 작업에 대해서는 <a href="https://velog.io/@tkppp-dev/Node-Express-Typescript%EC%97%90%EC%84%9C-Jest%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%85%8C%EC%8A%A4%ED%8A%B8">이전 포스트</a>와 같은 방법으로 셋업과 해제 콜백을 작성하면 된다</p>
<h2 id="beforeall-afterall">beforeAll, afterAll</h2>
<p>테스트 시 단 한번 수행되는 작업을 지정할 때 사용</p>
<h1 id="훅-함수의-스코프와-순서">훅 함수의 스코프와 순서</h1>
<p>상위 스코프에서 정의된 <code>beforeEach, afterEach</code>는 하위 스코프의 테스트에서도 실행되고 영향을 준다</p>
<p>같은 레벨의 훅 함수의 실행 우선 순위는 아래와 같다</p>
<ul>
<li>beforeAll &gt; beforeEach &gt; afterEach &gt; afterAll</li>
</ul>
<p>같은 레벨의 훅 함수의 실행 우선 순위는 아래와 같다</p>
<ol>
<li>상위 스코프의 beforeAll</li>
<li>하위 스코프의 beforeAll</li>
<li>상위 스코프의 beforeEach</li>
<li>하위 스코프의 beforeEach</li>
<li>하위 스코프의 afterEach</li>
<li>상위 스코프의 afterEach</li>
<li>하위 스코프의 afterAll</li>
<li>상위 스코프의 afterAll</li>
</ol>
<pre><code class="language-ts">beforeAll(() =&gt; console.log(&#39;1 - beforeAll&#39;));
afterAll(() =&gt; console.log(&#39;1 - afterAll&#39;));
beforeEach(() =&gt; console.log(&#39;1 - beforeEach&#39;));
afterEach(() =&gt; console.log(&#39;1 - afterEach&#39;));

test(&#39;&#39;, () =&gt; console.log(&#39;1 - test&#39;));

describe(&#39;Scoped / Nested block&#39;, () =&gt; {
  beforeAll(() =&gt; console.log(&#39;2 - beforeAll&#39;));
  afterAll(() =&gt; console.log(&#39;2 - afterAll&#39;));
  beforeEach(() =&gt; console.log(&#39;2 - beforeEach&#39;));
  afterEach(() =&gt; console.log(&#39;2 - afterEach&#39;));

  test(&#39;&#39;, () =&gt; console.log(&#39;2 - test&#39;));
});

// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll</code></pre>
<h1 id="describe와-test의-실행-순서">describe와 test의 실행 순서</h1>
<p>describe 내 test를 제외한 부분이 먼저 실행된 후 test가 작성된 순서대로 테스트가 실행된다</p>
<pre><code class="language-ts">describe(&#39;describe outer&#39;, () =&gt; {
  console.log(&#39;describe outer-a&#39;);

  describe(&#39;describe inner 1&#39;, () =&gt; {
    console.log(&#39;describe inner 1&#39;);

    test(&#39;test 1&#39;, () =&gt; console.log(&#39;test 1&#39;));
  });

  console.log(&#39;describe outer-b&#39;);

  test(&#39;test 2&#39;, () =&gt; console.log(&#39;test 2&#39;));

  describe(&#39;describe inner 2&#39;, () =&gt; {
    console.log(&#39;describe inner 2&#39;);

    test(&#39;test 3&#39;, () =&gt; console.log(&#39;test 3&#39;));
  });

  console.log(&#39;describe outer-c&#39;);
});

// describe outer-a
// describe inner 1
// describe outer-b
// describe inner 2
// describe outer-c
// test 1
// test 2
// test 3</code></pre>
<h1 id="약간의-팁---skip-only">약간의 팁 - skip, only</h1>
<p><code>describe, test</code> 뒤에 <code>skip(), only()</code>를 추가함으로서 테스트를 스킵하거나 해당 테스트만 실행할 수 있다</p>
<p><code>only</code>를 통해 실패하는 테스트만 실행하는 것이 가능하고 반대로 <code>skip</code>을 통해 통과하는 테스트를 테스트에서 제외하고 실패하는 테스트만 실행하는 것이 가능하다</p>
<pre><code class="language-ts">test.only(&#39;this will be the only test that runs&#39;, () =&gt; {
  expect(true).toBe(false);
});

test(&#39;this test will not run&#39;, () =&gt; {
  expect(&#39;A&#39;).toBe(&#39;A&#39;);
});</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Node] Express  + Typescript에서 Jest로 테스트하기 - 비동기 테스트 ]]></title>
            <link>https://velog.io/@tkppp-dev/Node-Express-Typescript%EC%97%90%EC%84%9C-Jest%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@tkppp-dev/Node-Express-Typescript%EC%97%90%EC%84%9C-Jest%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Thu, 01 Sep 2022 07:45:33 GMT</pubDate>
            <description><![CDATA[<p>자바스크립트는 비동기 함수가 자주 사용된다. 만약 비동기 함수를 일반적으로 테스트한다면 해당 테스트 수트가 완료되기전에 테스트는 종료되고 다음 테스트 수트가 실행된다</p>
<pre><code class="language-ts">test(&#39;async&#39;, () =&gt; {
  setTimeout(() =&gt; {
    console.log(&#39;async&#39;)
  }, 1000)
})

test(&#39;sync&#39;, () =&gt; {
  console.log(&#39;sync&#39;)
})</code></pre>
<p>위의 테스트 코드를 실행하면 문제 없이 통과한다. 하지만 1초후 비동기로 실행된 함수가 실행되면서 <code>Cannot log after tests are done. Did you forget to wait for something async in your test?</code> 라는 에러메세지가 나타난다.</p>
<p>위의 예제는 값 검증이 없기 때문에 성공했지만 만약 값을 받아 검증한다면 비동기 함수에서 프로미스 객체가 반환되기 때문에 테스트가 실패할 것이다</p>
<p>이런 문제를 막기위해 비동기 테스트를 위한 몇가지 방법이 있다</p>
<h2 id="프로미스-반환">프로미스 반환</h2>
<pre><code class="language-ts">// fetchData
test(&#39;the data is peanut butter&#39;, () =&gt; {
  return fetchData().then(data =&gt; {
    expect(data).toBe(&#39;peanut butter&#39;);
  });
});</code></pre>
<p>프로미스가 <code>reject</code> 된다면 테스트가 실패한다. 단 프로미스를 반환하도록 해야 실패함을 인지한다.</p>
<h2 id="asyncawait-사용">async/await 사용</h2>
<p>프로미스를 사용할 경우 프로미스 객체를 반환해야만 제대로 작동하므로 여러개의 비동기 함수를 테스트하게 된다면 사용하기 어렵다. 이러한 경우 테스트 콜백을 <code>aync</code> 함수로 만들고 <code>await</code> 키워드를 통해 비동기 함수가 완료되길 기다릴 수 있다.</p>
<pre><code class="language-ts">test(&#39;the data is peanut butter&#39;, async () =&gt; {
  const data = await fetchData();
  expect(data).toBe(&#39;peanut butter&#39;);
});

test(&#39;the fetch fails with an error&#39;, async () =&gt; {
  expect.assertions(1);
  try {
    await fetchData();
  } catch (e) {
    expect(e).toMatch(&#39;error&#39;);
  }
});</code></pre>
<h3 id="resolves-rejects">resolves, rejects</h3>
<p><code>expect()</code> 함수의 인자로 비동기 함수를 넘길 경우 비동기 함수를 기다리기 위해 <code>resolves, rejects</code> 를 매처로 사용한다. 단 이 방법 또한 테스트 콜백 내에서 반환해야만 제대로 작동한다</p>
<p>마찬가지로 async 테스트 콜백 내에서 await 키워드와 같이 사용하는 것도 가능하다</p>
<pre><code class="language-ts">// sync
test(&#39;the data is peanut butter&#39;, () =&gt; {
  return expect(fetchData()).resolves.toBe(&#39;peanut butter&#39;);
});

test(&#39;the fetch fails with an error&#39;, () =&gt; {
  return expect(fetchData()).rejects.toMatch(&#39;error&#39;);
});

// async
test(&#39;the data is peanut butter&#39;, async () =&gt; {
  await expect(fetchData()).resolves.toBe(&#39;peanut butter&#39;);
});

test(&#39;the fetch fails with an error&#39;, async () =&gt; {
  await expect(fetchData()).rejects.toMatch(&#39;error&#39;);
});</code></pre>
<h2 id="콜백-사용">콜백 사용</h2>
<p><a href="https://jestjs.io/docs/28.x/asynchronous#callbacks">공식 문서</a> 참조</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Node] Express + Typescript에서 Jest로 테스트하기 - Matcher]]></title>
            <link>https://velog.io/@tkppp-dev/Node-Express-Typescript%EC%97%90%EC%84%9C-Jest%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0-Matcher</link>
            <guid>https://velog.io/@tkppp-dev/Node-Express-Typescript%EC%97%90%EC%84%9C-Jest%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0-Matcher</guid>
            <pubDate>Thu, 01 Sep 2022 07:07:30 GMT</pubDate>
            <description><![CDATA[<h1 id="common-matcher">Common Matcher</h1>
<p><code>JUnit</code>에서 테스트할 때 주로 <code>AssertJ</code>가 제공하는 <code>Matcher</code>를 통해 테스트 결과를 검증했다. <code>Jest</code>에서 주로 사용되는 매처를 알아보자</p>
<h2 id="expectvalue">expect(value)</h2>
<p><code>expect()</code> 는 값을 검증하기 위해 항상 사용하는 함수이다. 이 함수를 단일로 사용할 일은 거의 없고 반환하는<code>Expect&lt;T&gt;</code> 객체와 <code>Matcher</code> 함수와 연계하여 사용한다. <code>AssertJ</code>의 <code>assertThat()</code> 함수와 유사하다</p>
<h2 id="tobevalue">toBe(value)</h2>
<p>주로 자바스크립트의 원시 타입을 검증하는데 사용한다. 객체에는 사용할 시 테스트가 실패한다. 또한 부동소수점 값을 테스트 시 실패하므로 <code>toBeCloseTo()</code> 를 사용해야한다</p>
<h2 id="toequalvalue">toEqual(value)</h2>
<p>주로 객체를 검증하는데 사용한다. 모든 프로퍼티를 재귀적으로 탐색하므로 깊은 부분까지 객체를 비교할 수 있다</p>
<h1 id="truthiness-matcher">Truthiness Matcher</h1>
<h2 id="tobedefined">toBeDefined()</h2>
<p>값이 할당되었는지 검증. <code>not</code>과 연계해 <code>toBeUndefined()</code>처럼 사용 가능</p>
<h2 id="tobeundefined">toBeUndefined()</h2>
<p>값이 undefined 인지 검증. <code>not</code>과 연계해 <code>toBeDefined()</code>처럼 사용 가능</p>
<h2 id="tobenull">toBeNull()</h2>
<p>값이 null 인지 검증</p>
<h2 id="tobetruely">toBeTruely()</h2>
<p>조건 또는 값이 참인지 검증. <code>not</code>과 연계해 <code>toBeFalsy()</code>처럼 사용 가능</p>
<h2 id="tobefalsy">toBeFalsy()</h2>
<p>조건 또는 값이 거짓인지 검증. <code>not</code>과 연계해 <code>toBeTruely()</code>처럼 사용 가능</p>
<pre><code class="language-js">// 테스트 통과
test(&#39;false&#39;, () =&gt; {
  expect(false).toBeFalsy()
  expect(undefined).toBeFalsy()
  expect(null).toBeFalsy()
  expect(0).toBeFalsy()
  expect(2 &lt; 1).toBeFalsy()
})</code></pre>
<h1 id="string-matcher">String Matcher</h1>
<h2 id="tomatchregex">toMatch(regex)</h2>
<p>문자열이 완전히 일치하는지의 여부는 <code>toBe, toEqual</code> 함수를 통해 확인할 수 있지만 <code>toMatch(regex)</code> 함수는 정규 표현식과 일치하는지 검증한다</p>
<h1 id="array-iterable-matcher">Array, Iterable Matcher</h1>
<h2 id="containingvalue">containing(value)</h2>
<p>배열이나 이터러블한 객체가 특정 값을 가지고 있는지 검증</p>
<h1 id="exception">Exception</h1>
<h2 id="tothrowmessage--undefined">toThrow(message | undefined)</h2>
<p>에러가 던져지는 것을 검증하기 위한 매처. 사용할 때 <code>expect()</code>에 넘기는 검증하려는 함수가 wrapper 함수로 감싸져야지만 성공하므로 유의 </p>
<h1 id="more">More</h1>
<p>이외에 더 많은 Matcher가 존재하니 <a href="https://jestjs.io/docs/28.x/expect">공식 문서</a>를 참조</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Node] Express  + Typescript에서 Jest로 테스트하기 - 설정 ]]></title>
            <link>https://velog.io/@tkppp-dev/Node-Express-Typescript%EC%97%90%EC%84%9C-Jest%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@tkppp-dev/Node-Express-Typescript%EC%97%90%EC%84%9C-Jest%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Thu, 01 Sep 2022 05:50:39 GMT</pubDate>
            <description><![CDATA[<p><code>Kotlin + SpringBoot + Junit5</code> 구성의 기존 프로젝트를 <code>Express + Typescript</code> 로 마이그레이션 하는 과정에서 최대한 빠르게 진행하기 위해 테스트 코드 작성을 하지 않았다. 최근 프로젝트에 <code>DDD</code> 를 적용하는 과정에서 테스트 코드의 필요성을 느껴 <code>Jest</code> 를 통해 <code>Node</code> 진영의 테스트 프레임워크와 방법에 대해 공부한 내용을 정리한 포스트이다</p>
<h1 id="의존성-설치">의존성 설치</h1>
<pre><code class="language-bash">$ npm install -D jest @types/jest ts-jest</code></pre>
<blockquote>
<p>포스트를 작성하는 현재 <code>jest</code>와 <code>ts-jest</code>의 최신 버전이 맞지 않아 오류 발생했다. <code>ts-jest</code>와 <code>jest</code>의 버전을 맞추어 설치하여 문제를 해결할 수 있다</p>
<pre><code class="language-bash">$ npm install -D jest@28.1.2 ts-jest</code></pre>
</blockquote>
<blockquote>
<p><a href="https://jestjs.io/docs/getting-started#type-definitions">공식 문서</a>에 따르면 <code>@types/jest</code>와  <code>jest</code> 의 버전을 맞추는 것을 권장하고 있으므로 버전이 다르다면 맞춰주도록 하자</p>
</blockquote>
<h1 id="설정-파일">설정 파일</h1>
<p>프로젝트 루트에 <code>jest.config.js</code> 파일을 직접 생성하여 설정 파일을 작성하거나</p>
<pre><code class="language-bash">$ npx jest --init</code></pre>
<p>명령어를 통해 설정 파일을 생성할 수 있다</p>
<p>필자는 생성 파일을 직접 만들었다</p>
<pre><code class="language-js">module.exports = {
  preset: &quot;ts-jest&quot;,
  testEnvironment: &quot;node&quot;, 
  testMatch: [&quot;**/*.spec.[jt]s?(x)&quot;, &quot;**/*.test.[jt]s?(x)&quot;], // 테스트 파일 위치 및 형식
};</code></pre>
<h1 id="테스트-옵션">테스트 옵션</h1>
<p>의존성 설치와 설정이 끝났으면 잘 작동하는지 간단한 테스트를 진행해보자. </p>
<pre><code class="language-bash">$ npx jest</code></pre>
<p>위의 명령어를 통해 프로젝트 내에 존재하는 모든 테스트를 한번에 실행시킬 수 있다. 또는</p>
<pre><code class="language-bash">$ npx jest [pattern | path]</code></pre>
<p>특정 디렉토리 내, 특정 패턴을 가진 테스트의 테스트만 진행하는 것도 가능하다</p>
<pre><code class="language-bash">$ npx -t [test suit pattern]</code></pre>
<p><code>-t</code>옵션을 주면 파일 내의 테스트 수트(describe, test, it)와 일치하는 특정 테스트 수트를 실행할 수 있다. 또 VSCode의 <code>jest</code>, <code>jest runner</code> 플러그인을 통해 명령어 대신 클릭 한번으로 손쉽게 테스트를 실행할 수 있다</p>
<blockquote>
<h4 id="참조">참조</h4>
<p><a href="https://mong-blog.tistory.com/entry/jest%EB%A1%9C-typescript-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0-1%EA%B8%B0%EB%B3%B8%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0">jest로 typescript 테스트하기, (1)기본설정하기
</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Typescript] 공식문서 정리]]></title>
            <link>https://velog.io/@tkppp-dev/Typescript-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@tkppp-dev/Typescript-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 29 Aug 2022 09:41:39 GMT</pubDate>
            <description><![CDATA[<h2 id="tsctypescript-compiler">TSC(TypeScript Compiler)</h2>
<h3 id="설치">설치</h3>
<pre><code class="language-bash">$ npm install -g typescript</code></pre>
<h3 id="사용">사용</h3>
<pre><code class="language-bash">$ tsc app.ts
$ tsc app    # 확장자 생략 가능

# JS로 컴파일된 app.js 파일 생성</code></pre>
<h3 id="컴파일-옵션">컴파일 옵션</h3>
<h4 id="--noemitonerror">--noEmitOnError</h4>
<pre><code class="language-js">// class.ts
class JsObject {
  constructor(name: string) {
    this.name = name    // 클래스 몸체에 프로퍼티 선언이 없어 에러 발생
  }
}

// class.js
class JsObject {
    constructor(name) {
        this.name = name;    // 생성자 내에 프로퍼티 선언을 하는 것이 정상이므로 에러 발생 X
    }
}</code></pre>
<p>컴파일중 에러가 발생한더라도 컴파일된 JS 파일이 생긴다. 그 이유는 TS에서는 문제가 있는 코드라도 JS에서는 문제가 없는 경우가 있기 때문이다. 위의 예제가 그 예시이다</p>
<p><code>--noEmitOnError</code> 옵션을 통해 컴파일 중 에러 발생시 결과물이 생성되지 않게할 수 있다. </p>
<pre><code class="language-bash">$ tsc --noEmitOnError class.ts</code></pre>
<h4 id="--target">--target</h4>
<p>위의 예제를 <code>tsc class.ts</code> 명령어를 통해 컴파일한 결과는 사실 다르다</p>
<pre><code class="language-js">// 실제 결과물
var JsObject = /** @class */ (function () {
    function JsObject(name) {
        this.name = name;
    }
    return JsObject;
}());</code></pre>
<p>기본적으로 tsc는 es3로 컴파일되도록 되어있다. 하지면 현재 대다수의 브라우저는 es3 이상의 ECMA 표준을 지원하므로 <code>--target</code> 명령어를 통해 표준을 지정할 수 있다</p>
<pre><code class="language-bash">$ tsc --target es2015 class.ts </code></pre>
<p>위의 예제의 클래스 문법을 사용한 JS 파일이 생성되는 것을 볼 수 있다</p>
<h4 id="--noimplicitany">--noImplicitAny</h4>
<p>암묵적으로 <code>any</code> 타입으로 추론되는 경우 에러를 발생시킨다</p>
<pre><code class="language-bash">$ tsc --noImplicitAny class.ts </code></pre>
<h4 id="--strictnullchecks">--strictNullChecks</h4>
<p>null 또는 undefined 검사를 강제하는 옵션이다</p>
<pre><code class="language-bash">$ tsc --strictNullChecks class.ts </code></pre>
<p><code>--noImplicitAny</code>, <code>--strictNullChecks</code> 옵션은 TS의 엄격도 레벨을 올리고 프로그래머의 실수를 미연에 방지할 수 있는 옵션이다</p>
<h2 id="함수">함수</h2>
<h3 id="매개변수-및-반환-타입-추론">매개변수 및 반환 타입 추론</h3>
<pre><code class="language-ts">function greet(name: string): void {
  console.log(`Hello, ${name}`)
}

function greetImplicit(name) {
  console.log(`Hello, ${name}`)
}</code></pre>
<p>명시적으로 매개 변수의 타입을 표기하지 않으면 <code>any</code> 로 타입을 추론한다. 단 <code>noImplictAny</code> 옵션이 활성화 되어있는 경우 에러가 발생하니 주의</p>
<p>반환 타입의 경우 return 문을 통해 타입을 추론한다. 반환이 없는 경우 <code>void</code> 타입으로 추론</p>
<h3 id="익명-함수">익명 함수</h3>
<p>익명 함수의 매개 변수 타입은 문맥을 통해 <code>any</code> 가 아닌 타입으로 추론하게된다</p>
<pre><code class="language-ts">const fruits = [&#39;apple&#39;, &#39;banana&#39;, &#39;tomato&#39;]    // string[] 타입

fruits.map((f) =&gt; {    // f를 string 타입으로 추론
  console.log(f.toUpperCase())
})</code></pre>
<p><code>map</code> 함수를 통해 넘겨받는 매개변수를 문맥을 통해 <code>string</code> 타입으로 추론해 IDE의 자동완성 기능이 작동하는 것을 볼 수 있다</p>
<h3 id="오버로딩">오버로딩</h3>
<p>이름만 같고 매개변수의 개수나 타입이 다른 여러개의 메소드가 존재하는 다른 언어의 오버로딩과 달리 자바스크립트는 이름이 같은 함수는 단 한개만 존재할 수 있다. 따라서 타입스크립트의 오버로딩은 이름이 같고 매개변수와 반환값등의 시그니처가 다른 함수를 정의만 해두고 모든 정의를 아우르는 구현부가 한개 존재하는 방식으로 오버로딩이 가능하다</p>
<p>따라서 함수별 로직이 서로 많이 다르다면 이름을 다르게 함수를 구현하는 것이 낫고 서로 로직이 겹칠 경우 오버로딩하여 구현할 수 있다.</p>
<pre><code class="language-ts">// 오버로딩 이전
function formatDate(date: Date, format = &quot;yyyyMMdd&quot;): string {
    const yyyy = date.getFullYear().toString();
    const MM = addZero(date.getMonth() + 1);
    const dd = addZero(date.getDate());
    return format.replace(&quot;yyyy&quot;, yyyy).replace(&quot;MM&quot;, MM).replace(&quot;dd&quot;, dd);
}

function formatDateString(dateStr: string, format = &quot;yyyyMMdd&quot;): string {
    const date = new Date(dateStr);
    const yyyy = date.getFullYear().toString();
    const MM = addZero(date.getMonth() + 1);
    const dd = addZero(date.getDate());
    return format.replace(&quot;yyyy&quot;, yyyy).replace(&quot;MM&quot;, MM).replace(&quot;dd&quot;, dd);
}

function formatDateTime(datetime: number, format = &quot;yyyyMMdd&quot;): string {
    const date = new Date(datetime);
    const yyyy = date.getFullYear().toString();
    const MM = addZero(date.getMonth() + 1);
    const dd = addZero(date.getDate());
    return format.replace(&quot;yyyy&quot;, yyyy).replace(&quot;MM&quot;, MM).replace(&quot;dd&quot;, dd);
}

// 오버로딩 적용 후
// 시그니처 정의
function formatDate(date: Date, format: string): string;
function formatDate(date: number, format: string): string;
function formatDate(date: string, format: string): string;
// 오버로딩 구현
function formatDate(date: Date | number | string, format = &quot;yyyyMMdd&quot;): string {
    const dateObj = new Date(date);
  const yyyy = dateObj.getFullYear().toString();
  const MM = addZero(dateObj.getMonth() + 1);
  const dd = addZero(dateObj.getDate());
  return format.replace(&quot;yyyy&quot;, yyyy).replace(&quot;MM&quot;, MM).replace(&quot;dd&quot;, dd);
}</code></pre>
<h2 id="인터페이스">인터페이스</h2>
<h3 id="인덱서블-타입indexable-type">인덱서블 타입(Indexable Type)</h3>
<p>인덱서블 타입은 인덱싱 할때 해당 반환 유형과 함께 객체를 인덱싱하는 데 사용할 수 있는 타입을 기술하는 인덱스 시그니처(index signature)를 가지고 있다</p>
<p>인덱스 시그니처의 타입은 <code>string</code>, <code>number</code> 만 가능하고 둘을 동시에 사용하는 것도 가능하다. 단 두개의 인덱스 시그니처를 같이 정의할 경우 <code>number</code> 시그니처의 타입은 <code>string</code> 시그니처 타입과 같거나 서브타입이어야 한다.</p>
<p><code>number</code> 타입의 프로퍼티는 인덱싱시 <code>string</code> 으로 변환되어 사용되기 때문에 사실 같다고 볼 수 있다. 따라서 타입의 일관성이 필요</p>
<pre><code class="language-ts">class Animal { }
class Dog extends Animal { }
interface Foo {
      [a: string]: Dog,
     [b: number]: Animal // 에러 발생. Animal은 Dog의 서브 타입이 아닌 부모 타입이기 때문
}</code></pre>
<p>마찬가지로 인덱스 시그니처가 존재하는 경우 인덱스 시그니처의 키 타입과 같은 일반 프로퍼티의 타입도 인덱스 시그니처와 일치해야한다</p>
<pre><code class="language-ts">interface Foo {
  [key: number]: string
  10: number    // 에러 발생. 인덱스 시그니처의 값 타입과 일치하지 않기 때문 
}</code></pre>
<h3 id="초과-프로퍼티-검사excess-property-check">초과 프로퍼티 검사(Excess Property Check)</h3>
<p>객체 리터럴을 다른 변수에 할당할 때나 인수로 전달할 때, 특별한 처리를 받고 <code>초과 프로퍼티 검사</code> 를 받는다. 초과 프로퍼티 검사란 인터페이스의 정의되지 않은 프로퍼티를 허용하지 않고 에러를 발생하도록 하는 검사이다</p>
<pre><code class="language-ts">interface LabeledValue {
  label: string;
}

function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj);
}
const myObj = {label: &#39;size 10 object&#39;, size: 10}
printLabel(myObj)</code></pre>
<p><code>printLable()</code> 함수의 인자로 <code>LabeledValue</code> 인터페이스보다 범위가 큰 객체를 넘겼다. 이 코드는 에러가 발생하지 않는다. 객체 리터럴이 아닌 변수를 인수로 전달하였기 때문에 초과 프로퍼티 검사가 일어나지 않기 때문이다</p>
<pre><code class="language-ts">interface LabeledValue {
  label: string;
}

function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj);
}

const myObj: LabledValue = {label: &#39;size 10 object&#39;, size: 10}    // 변수 할당 시 초과 프로퍼티 검사에 따른 에러 발생
printLabel(myObj)

// 또는
printLabel({label: &#39;size 10 object&#39;, size: 10})    // 객체 리터럴을 인자로 넘겨 초과 프로퍼티 검사에 따른 에러 발생</code></pre>
<p>위의 예제와 같이 변수 할당시 타입을 지정하거나 객체 리터럴을 인자로 전달하면 초과 프로퍼티 검사가 작동하여 에러가 발생한다</p>
<p>타입 단언을 사용하면 초과 프로퍼티 검사를 무력화할 수 있다</p>
<pre><code class="language-ts">interface LabeledValue {
  label: string;
}

function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj);
}
printLabel({label: &#39;size 10 object&#39;, size: 10} as LabeledValue)    // 에러 발생 X</code></pre>
<p>하지만 초과 프로퍼티 검사를 무력화하는 것은 좋지 않다. 전달된 매개변수를 통해서는 인터페이스에 정의된 프로퍼티 외에 다른 변수에 접근하는 것은 불가능하지만 여러 방법을 통해 접근할 수 있기 때문이다. 즉 문제를 불러올 수 있으므로 가능한 사용하지 않거나, 인덱스 시그니처를 통해 정의된 프로퍼티 외의 프로퍼티를 정의하는 것이 좋다</p>
<pre><code class="language-ts">interface LabeledValue {
  label: string;
}

function printLabel(labeledObj: LabeledValue) {
  for(let key in labeledObj) {
    console.log(key, labeledObj[key])
  }
}
const myObj = {label: &#39;size 10 object&#39;, size: 10}
printLabel(myObj)

/** 출력
* label size 10 object
* size 10    // 인터페이스 외 프로퍼티에 접근
*/</code></pre>
<p>위의 예제를 보면 <code>--noImplicitAny</code> 옵션이 활성화 되어 있지 않다면 에러가 발생하지 않는다. 이러한 문제점이 있음을 생각해야한다</p>
<h3 id="클래스를-확장한-인터페이스">클래스를 확장한 인터페이스</h3>
<pre><code class="language-ts">class Control {
    private state: any;
}

interface SelectableControl extends Control {
    select(): void;
}

class Button extends Control implements SelectableControl {
    select() { }
}

// Error: &#39;Foo&#39; 클래스가 &#39;SelectableControl&#39; 인터페이스를 잘못 구현합니다.
// 형식에 별도의 프라이빗 속성 &#39;state&#39; 선언이 있습니다.
class Foo1 implements SelectableControl {
    private state: any;
    select() { }
}

// Error: &#39;Foo2&#39; 클래스가 &#39;SelectableControl&#39; 인터페이스를 잘못 구현합니다.
// &#39;Foo2&#39; 형식에 &#39;SelectableControl&#39; 형식의 state 속성이 없습니다.
class Foo2 implements SelectableControl {
  select() { }</code></pre>
<p>타입스크립트의 인터페이스는 클래스를 확장할 수 있다. 일반적인 인터페이스는 프로퍼티가 읽기 전용인지 정도만 설정할 수 있지만 클래스를 확장할 경우 <code>private</code>, <code>public</code> 과 같은 접근 지정자까지 상속받는다. 또한 클래스를 확장한 인터페이스를 구현하는 경우 해당 클래스를 상속하지 않는 경우 에러가 발생하므로 클래스의 상속을 강제할 수 있다.</p>
<p>왜 이렇게 쓰는지는 이해하지 못했지만 이런 방식의 사용이 가능하다고 알고 있으면 될 것 같다</p>
<h2 id="모듈">모듈</h2>
<p>모듈은 전역 스코프가 아닌 자체 스코프 내에서 실행된다, 즉 모듈 내에서 선언된 변수, 함수, 클래스, 인터페이스, 타입 별칭 등은 명시적으로 <code>export</code> 하지 않는 한 모듈 외부에서 보이지 않습니다. 반대로 다른 모듈에서 <code>export</code> 한 변수, 함수, 클래스, 인터페이스 등을 사용하기 위해서는 <code>import</code> 를 통해 가져와야 한다.</p>
<p>만약 파일 내에 <code>export</code>, <code>import</code> 가 존재하지 않으면 해당 파일은 전역 스코프를 가지게 되므로 유의해햐 한다</p>
<h3 id="export">export</h3>
<h4 id="선언과-동시에-export">선언과 동시에 export</h4>
<pre><code class="language-ts">export class Foo { }
export const bar = &#39;Bar&#39;</code></pre>
<h4 id="export-문">export 문</h4>
<pre><code class="language-ts">class Foo { }

export { Foo }
export { Foo as Bar }    // 이름을 바꿔 export</code></pre>
<h3 id="import">import</h3>
<h4 id="단일-export-import">단일 export import</h4>
<pre><code class="language-ts">import { Foo } from &#39;./foo.ts&#39;
import { Bar as Baz } from &#39;./bar.ts&#39;</code></pre>
<h4 id="전체-모듈을-단일-변수로-import">전체 모듈을 단일 변수로 import</h4>
<pre><code class="language-ts">import * as Foo from &#39;./foo.ts&#39;</code></pre>
<h4 id="부수-효과만-import">부수 효과만 import</h4>
<p>모듈의 export와 관계없이 발생하는 부수 효과만을 가져오는 import</p>
<pre><code class="language-ts">// side.ts
console.log(&#39;side module&#39;)

// runtime.ts
import &#39;./side.ts&#39;
console.log(&#39;runtime&#39;)

/** runtime.ts 실행 결과
*    side module        // 부수 효과 실행
*    runtime
*/</code></pre>
<h4 id="타입-import">타입 import</h4>
<pre><code class="language-ts">// 암시적 import
import { Foo } from &quot;./foo.ts&quot;;

// 명시적으로 import type을 사용하기
import type { Foo } from &quot;./foo.ts&quot;;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] 도커로 Node 웹 애플리케이션 배포하기 - 3. 가상 서버 호스팅 서비스에 서비스 배포하기]]></title>
            <link>https://velog.io/@tkppp-dev/Docker-%EB%8F%84%EC%BB%A4%EB%A1%9C-Node-%EC%9B%B9-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-3.-%EA%B0%80%EC%83%81-%EC%84%9C%EB%B2%84-%ED%98%B8%EC%8A%A4%ED%8C%85-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@tkppp-dev/Docker-%EB%8F%84%EC%BB%A4%EB%A1%9C-Node-%EC%9B%B9-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-3.-%EA%B0%80%EC%83%81-%EC%84%9C%EB%B2%84-%ED%98%B8%EC%8A%A4%ED%8C%85-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 27 Jul 2022 02:55:13 GMT</pubDate>
            <description><![CDATA[<p>저번 포스트에 이어서 가상서버 호스팅 서비스인 <code>Vultr</code> 에 서비스를 배포해보는 과정을 알아보겠습니다.</p>
<h1 id="가상-서버란">가상 서버란?</h1>
<p>VPS는 하나의 물리서버를 여러 개의 가상 서버로 쪼개어 사용하는 것을 의미한다. 그렇게 쪼개어진 가상 서버를 여러 명의 클라이언트가 나누어 쓰는 것이다. 따라서 하나의 물리서버의 리소스를 다른 이들과 공유하지만 각자 독립적인 서버 공간을 가지는 것이 가능하다.</p>
<h2 id="가상-서버-호스팅-서비스vps">가상 서버 호스팅 서비스(VPS)</h2>
<p>말 그대로 가상 서버를 호스팅해주는 클라우드 서비스이다. <code>AWS Lightsail</code>, <code>Vultr</code>, <code>Digital Ocean</code> 등등 VPS 서비스가 있다. 가장 익숙한 것은 아마존 웹 서비스가 제공하는 <code>Lightsail</code> 일것이다. 프리티어 기간에는 무료이지만 이후에는 월정액으로 과금된다.</p>
<p>VPS 서비스를 선택하는 기준은 여러가지가 있는데 필자는 서울 리전이 존재하고 한달 무료 크레딧 100$를 제공하는 <code>Vultr</code> 를 선택하였다.</p>
<p>추천 링크를 통해서 가입해야만 100$ 크레딧을 제공하므로(그냥 가입하면 50$로 알고 있음) 필요하다면 구글링하여 추천인 링크를 통해 가입하면 된다.</p>
<h1 id="vultr-인스턴스-생성">Vultr 인스턴스 생성</h1>
<p>회원가입 후 로그인을 하고 인스턴스를 생성하는 것은 아래 포스트 참조
<a href="https://velog.io/@hunjison/%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0Vultr-hosting.kr-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95">서버 구축하기(Vultr + hosting.kr) + 초기 설정</a> </p>
<h2 id="인스턴스-접속">인스턴스 접속</h2>
<p>인스턴스를 생성했다면 <code>Vultr</code> 가 제공하는 웹 콘솔을 사용하는 것도 방법이지만 해상도라던지 클립보드 사용이라던지 불편한점이 많다. 그래서 ssh 이용한 원격 접속으로 콘솔에 접속할 것이다.</p>
<pre><code class="language-bash">$ ssh [user]@[ip]</code></pre>
<p>ssh 설정을 따로 하지 않았다면 위와 같은 명령어로 쉽게 가상 서버 콘솔에 접속할 수 있다. 비밀번호를 누르고 접속에 성공했다면 아래와 같은 화면이 나타날 것이다.
<img src="https://velog.velcdn.com/images/tkppp-dev/post/21704f7f-e268-4390-b151-98e2d452a6fe/image.png" alt=""></p>
<h1 id="도커-설치">도커 설치</h1>
<p>ssh로 가상서버에 원격접속을 완료했으면 도커를 설치해야한다. 설치방법은 <a href="https://docs.docker.com/engine/install/ubuntu/">공식 문서</a> 를 참조하자. 리눅스 배포판 별로 설치 방법이 상이하니 자신의 가상 서버 OS에 맞는 설치법을 찾아 설치하자.</p>
<p>도커 설치가 완료되었다면 <code>docker --version</code> 명령어로 잘 설치되었는지 확인하자.
<img src="https://velog.velcdn.com/images/tkppp-dev/post/9d6e3a56-bd76-4626-b937-c71021223aaa/image.png" alt=""></p>
<h1 id="이미지-받아오기">이미지 받아오기</h1>
<p>도커를 가상 서버에 설치했다면 이전 포스트에서 만들었던 이미지와 DB 이미지를 도커 허브에서 받아온다</p>
<pre><code class="language-bash">$ docker pull [docker-id]/[image-name]
$ docker pull mysql</code></pre>
<p><code>docker images</code> 명령어로 이미지를 잘 받아왔는지 확인할 수 있다.</p>
<h1 id="컨테이너-실행">컨테이너 실행</h1>
<p>이전 포스트에서 했던것과 같이 컨테이너를 백그라운드로 실행해주면 배포가 완료된다. 유의할 점은 앱이 DB 연결에 의존하기 때문에 DB 컨테이너를 앱 컨테이너보다 먼저 실행해야한다는 것이다.</p>
<pre><code class="language-bash">$ docker run -d --name mysql-container \
-e MYSQL_ROOT_PASSWORD=tiffndla0423 \
-e LC_ALL=C.UTF-8 \
-p 3306:3306 \
-v mysql-vol:/var/lib/mysql \
mysql

$ docker run -d --name [container-name] \
-e DB_HOST=mysql-container \
--link mysql-container \
-p 80:8080 \
[image-name]</code></pre>
<p><code>docker ps -a</code> 명령어로 컨테이너가 잘 실행되었는지 확인할 수 있다.</p>
<h1 id="마무리">마무리</h1>
<p>배포가 끝났다. 도커 없이 배포한다면 가상서버에 필요한 환경 설정을 하고 mysql을 설치하고 또 db 설정을 진행한다음 <code>git</code> 을 설치하고 소스 코드를 클론하여 앱을 실행해야한다. 이 과정 속에서 문제가 생긴다면 아주 골치아파질 것이다. 이와 달리 로컬에서 배포 환경의 이미지를 만들었기 때문에 배포 환경에 도커를 설치하고 이미지를 받아 컨테이너를 실행하기만 하면 배포가 완료된다. 컨테이너와 호스트 OS는 격리되어 있기 때문에 호스트의 환경 설정은 불필요하다.</p>
<p>도커로 배포를 진행하는 것이 간편하긴 하지만 매 실행마다 긴 도커 명령어를 쳐야하는 점은 불편할 것이다. 이를 해결해주는 것이 <code>Docker Compose</code> 이다. 필요하다면 도커 컴포즈에 대해 알아보면 좋을 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] 도커로 Node 웹 애플리케이션 배포하기 - 2. 개발환경 세팅 및 이미지 생성과 푸시]]></title>
            <link>https://velog.io/@tkppp-dev/Docker-%EB%8F%84%EC%BB%A4%EB%A1%9C-Node-%EC%9B%B9-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B02</link>
            <guid>https://velog.io/@tkppp-dev/Docker-%EB%8F%84%EC%BB%A4%EB%A1%9C-Node-%EC%9B%B9-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B02</guid>
            <pubDate>Wed, 27 Jul 2022 02:10:58 GMT</pubDate>
            <description><![CDATA[<p>저번 포스트에 이어서 도커를 활용해 가상 서버에 배포하는 과정을 알아보겠습니다.</p>
<h1 id="개발-환경-세팅">개발 환경 세팅</h1>
<p>이미지를 통해 컨테이너를 생성하면 해당 컨테이너 내의 변경점은 이미지에 반영되지 않는다. 모든 변경점은 사라지게 된다. 배포 환경에서는 코드가 변할 일이 없고 업데이트가 있다면 새로 이미지를 만들어 받아 실행하면 그만이기 때문에 문제가 없다. 하지만 실시간으로 코드가 변하는 개발 환경에서 코드가 변경될 때마다 일일이 이미지를 만들고 컨테이너를 실행하고 테스트하는 것은 비효율적이다. 따라서 도커가 제공하는 볼륨을 통해 컨테이너와 로컬 파일 시스템을 연결하여 사용하는 개발 환경을 구축할 수 있다.</p>
<h2 id="볼륨">볼륨</h2>
<p>컨테이너와 로컬 파일 시스템을 연결하는 방식에는 두가지 방식이 있다. 도커 볼륨을 생성하고 컨테이너와 마운트하는 방식과 로컬 디렉토리를 직접 컨테이너에 마운트하는 방식이 있다.</p>
<h3 id="볼륨-생성">볼륨 생성</h3>
<pre><code class="language-bash">$ docker volume create --name [volume-name]</code></pre>
<p><code>docker volume create</code> 명령어를 통해 볼륨을 생성할 수 있다. 이름을 지정하지 않으면 이름 없는 볼륨이 생성된다. 이렇게 생성한 볼륨은 <code>docker volume ls</code> 명령어를 통해 조회할 수 있다.
<img src="https://velog.velcdn.com/images/tkppp-dev/post/68babda9-b04e-4e9f-8050-a94514705346/image.png" alt=""></p>
<pre><code class="language-bash"> $ docker volume inspect [volume-name]</code></pre>
<p><code>docker volume inspect</code> 명령어를 통해 볼륨의 상세 정보를 조회할 수 있고 볼륨이 사용하는 로컬 디렉토리 위치를 알 수 있다.
<img src="https://velog.velcdn.com/images/tkppp-dev/post/2f39710c-21bd-4924-b50e-f7f680d0965f/image.png" alt=""></p>
<h2 id="도커파일-작성">도커파일 작성</h2>
<p>프로젝트 루트에 Dockerfile을 생성해 작성한다.</p>
<pre><code>FROM node:16-alpine

# Korean Fonts
RUN apk --update add fontconfig
RUN mkdir -p /usr/share/fonts/nanumfont
RUN wget http://cdn.naver.com/naver/NanumFont/fontfiles/NanumFont_TTF_ALL.zip
RUN unzip NanumFont_TTF_ALL.zip -d /usr/share/fonts/nanumfont
RUN fc-cache -f &amp;&amp; rm -rf /var/cache/*

# update pkg manager
RUN apk update

# bash install
RUN apk add bash

# Language
ENV LANG=ko_KR.UTF-8 \
    LANGUAGE=ko_KR.UTF-8

# Set the timezone in docker
RUN apk --no-cache add tzdata &amp;&amp; \
        cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime &amp;&amp; \
        echo &quot;Asia/Seoul&quot; &gt; /etc/timezone

# Create Directory for the Container
WORKDIR /user/src/app

# add chromium
RUN apk add chromium

# Only copy the package.json file to work directory
COPY package.json .
RUN npm install

# Docker Demon Port Mapping
EXPOSE 80

# RUN SERVER
CMD [&quot;npm&quot;, &quot;run&quot;, &quot;dev&quot;]</code></pre><blockquote>
<p>베이스 이미지는 lts 버전인 <code>node:16-alpine</code> 을 사용했다. 이 이미지는 경량 리눅스인 <code>알파인 리눅스</code> 에 노드 환경이 구성된 베이스 이미지로 기본 패키지 매니저로 <code>apk</code> 를 사용한다</p>
</blockquote>
<blockquote>
<p>\ 을 사용하면 여러줄 입력을 사용할 수 있다. &amp;&amp; 와 \ 을 사용하여 여러 명령어를 가독성있게 작성할 수 있다</p>
</blockquote>
<blockquote>
<p>크로미움 설치는 필자의 애플리케이션 실행에 필요하기 때문에 필요한 것이다. 크로미움 브라우저가 필요한 것이 아니라면 생략한다</p>
</blockquote>
<blockquote>
<p>컨테이너의 <code>/usr/src/app</code> 디렉토리를 WORKDIR로 지정하고 로컬의 <code>package.json</code> 을 복사해 노드 관련 의존성을 설치한다</p>
</blockquote>
<blockquote>
<p>CMD npm run dev 명령어는 개발시 사용하는 nodemon 으로 앱을 실행하라는 스크립트이다. 스크립트 내용은 아래와 같다.</p>
<p>&quot;dev&quot;: &quot;export NODE_OPTIONS=&#39;--trace-deprecation --abort-on-uncaught-exception&#39; &amp;&amp; export NODE_ENV=development &amp;&amp; nodemon --exec ts-node ./bin/<a href="http://www.ts&quot;">www.ts&quot;</a>,</p>
</blockquote>
<p>도커파일 작성을 완료했다면 도커파일이 존재하는 위치에서 이미지를 빌드한다.</p>
<pre><code>$ docker build -t [image-tag-name] .</code></pre><p>정상적으로 이미지를 빌드했다면 컨테이너를 실행한다.</p>
<pre><code>$ docker run -it --name sport-result \
-p 80:8080 \
[image-tag-name]</code></pre><p>이렇게 도커를 실행하면 에러가 발생할 것이다. 왜냐하면 컨테이너 내에서는 노드 의존성만 설치되어 있지 소스 코드가 존재하지 않기 때문이다. 따라서 로컬 디렉토리를 컨테이너에 마운트해서 컨테이너가 소스 코드에 접근할 수 있도록 해야한다.</p>
<p>수정된 명령어는 아래와 같다.</p>
<pre><code>$ docker run -it --name sport-result \
-p 80:8080 \
-v $(pwd):/usr/src/app/
[image-tag-name]</code></pre><p>로컬 디렉토리가 컨테이너와 연결되어 잘 실행되는 모습을 볼 수 있다. 코드가 수정되면 실시간으로 재시작되는 모습도 확인할 수 있다. </p>
<p>하지만 데이터베이스 설정이 로컬호스트로 되어있어 에러가 발생한다. 컨테이너 DB에 연결해보자.</p>
<blockquote>
<h4 id="docker-run-시-유의점">docker run 시 유의점</h4>
<p>-d 옵션을 사용하지 않을시 기본적으로 포그라운드로 도커 컨테이너를 실행한다.
만약 도커 컨테이너 내에서 포그라운드로 프로세스를 실행하지 않으면 도커 컨테이너가 종료되며 다시 재시작 시켜주어야 한다.</p>
<p>-d 옵션을 사용하여 백그라운드로 실행할 경우 마찬가지로 컨테이너 내에서 포그라운드로 프로세스를 실행해야지만 컨테이너가 종료되지 않고 유지된다.</p>
</blockquote>
<h2 id="mysql-연동">MySQL 연동</h2>
<p>웹 애플리케이션이라면 필연적으로 데이터베이스를 사용하게 된다. 도커 허브에서 Mysql 이미지를 받아 Mysql 컨테이너를 실행해보자</p>
<pre><code class="language-bash">$ docker pull mysql        # mysql 이미지 받아오기
$ docker run -d --name mysql-container \    # mysql 컨테이너 실행
-e MYSQL_ROOT_PASSWORD=[root-password] \
-e LC_ALL=C.UTF-8 \
-p 3306:3306 \
-v mysql-vol:/var/lib/mysql \
mysql</code></pre>
<blockquote>
<p>환경변수로 LC_ALL=C.UTF-8 을 지정해주었다. 설정하지 않으면 mysql 컨테이너 내에서 한글 사용이 제대로 되지 않는다. 필자는 데이터를 덤프하는 과정에서 한글이 제대로 입력되지 않아 설정했다.</p>
</blockquote>
<blockquote>
<p>mysql 공식 이미지는 oracle-linux를 사용하고 해당 이미지는 <code>microdnf</code> 패키지 매니저가 기본으로 사용된다. 또 텍스트 편집기인 vi와 vim도 깔려있지 않다. 만약 필요하다면 <code>microdnf install yum</code> 후 yum을 사용해 vim을 설치해야한다</p>
</blockquote>
<blockquote>
<p>볼륨을 연결하여 mysql 컨테이너를 삭제하더라도 데이터를 보존할 수 있도록 하였다. 컨테이너의 /var/lib/mysql 를 로컬 또는 도커 볼륨에 마운트하여 데이터를 저장할 수 있다.</p>
</blockquote>
<blockquote>
<p>컨테이너에 접속하여 쉘 스크립트 작업을 하고 싶다면 <code>docker exec -it [container-name] | [container-id] cmd</code> 명령어로 접속하자. cmd에는 <code>bash</code> 같은 쉘을 사용한다</p>
</blockquote>
<p><code>docker ps -a</code> 명령어로 <code>mysql-container</code> 이름의 컨테이너가 잘 실행된것이 보일것이다. 이제 아까 실행한 노드 컨테이너를 삭제하고 mysql 컨테이너과와 연동해보자.</p>
<pre><code class="language-ts">// datasoure.ts
import { DataSource } from &quot;typeorm&quot;;
import { KboMatch } from &quot;./domain/kbo/kboMatch&quot;;
import { KboRank } from &#39;./domain/kbo/kboRank&#39;;
import { LckMatch } from &#39;./domain/lck/lckMatch&#39;;

const host = process.env.DB_HOST
const username = process.env.DB_USER
const password = process.env.DB_PASSWORD
let synchronize, logging
if(process.env.NODE_ENV === &#39;production&#39;) {
  synchronize = false,
  logging = false
} else if (process.env.NODE_ENV === &#39;development&#39;) {
  synchronize = true
  logging = true
}

export const MysqlDateSource = new DataSource({
  type: &#39;mysql&#39;,
  host,
  port: 3306,
  username,
  password,
  database: &#39;sport_result&#39;,
  entities: [KboMatch, KboRank, LckMatch],
  synchronize,
  logging
})</code></pre>
<p>DB 커넥션을 위한 Datasource 파일을 보면 로컬 호스트가 아닌 환경변수 DB_HOST를 사용하게 변경한다. 그리고 <code>docker run</code> 명령어를 실행한다.</p>
<pre><code class="language-bash">$ docker run -it --name sport-result \
-e DB_HOST=mysql-container \
--link mysql-container \
-v $(pwd):/usr/src/app/ \
-p 80:8080 \
[image-tag-name]</code></pre>
<blockquote>
<p>환경변수로 mysql 컨테이너의 이름을 전달하고 --link 옵션을 통해 컨테이너를 연결하면 DB 연결이 완료된다.</p>
</blockquote>
<p>DB 연결까지 완료하여 개발환경 구축이 완료되었다.</p>
<h1 id="배포-이미지-만들기">배포 이미지 만들기</h1>
<p>실시간으로 변하는 코드에 대응하기 위해 로컬 디렉토리를 컨테이너에 마운트하여 개발환경을 구축하였다. 하지만 실제 배포 환경에서는 코드가 변할 일이 없는 정적인 환경이므로 굳이 로컬 디렉토리를 마운트해서 사용할 필요가 없다.</p>
<p>따라서 이미지를 만들 때 프로젝트 소스파일(혹은 빌드된 파일)을 복사하여 이미지를 생성한다. 노드는 빌드된 단일 파일이 아니기 때문에 소스파일을 통째로 복사한다.</p>
<h2 id="도커파일-수정">도커파일 수정</h2>
<pre><code>FROM node:16-alpine

# Korean Fonts
RUN apk --update add fontconfig
RUN mkdir -p /usr/share/fonts/nanumfont
RUN wget http://cdn.naver.com/naver/NanumFont/fontfiles/NanumFont_TTF_ALL.zip
RUN unzip NanumFont_TTF_ALL.zip -d /usr/share/fonts/nanumfont
RUN fc-cache -f &amp;&amp; rm -rf /var/cache/*

# update pkg manager
RUN apk update

# bash install
RUN apk add bash

# Language
ENV LANG=ko_KR.UTF-8 \
    LANGUAGE=ko_KR.UTF-8

# Set the timezone in docker
RUN apk --no-cache add tzdata &amp;&amp; \
        cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime &amp;&amp; \
        echo &quot;Asia/Seoul&quot; &gt; /etc/timezone

# Create Directory for the Container
WORKDIR /user/src/app

# add chromium
RUN apk add chromium

# Copy source code
COPY . .
RUN npm install
RUN npm install -g pm2

# Docker Demon Port Mapping
EXPOSE 80

# RUN SERVER
CMD [&quot;npm&quot;, &quot;run&quot;, &quot;docker&quot;]</code></pre><blockquote>
<p>개발 환경에서는 package.json만 복사하였지만 프로젝트 루트 폴더의 모든 파일을 복사한다. 만약 제외하고 싶은 폴더나 파일이 존재한다면 .dockerignore 파일에 지정하여 복사를 막을 수 있다. 폴더나 파일 지정 방식은 <code>Go</code> 언어 방식으로 지정한다.</p>
<pre><code># .dockerignore
.git
*Dockerfile*
*docker-compose*
node_modules```</code></pre></blockquote>
<blockquote>
<p>CMD 명령어를 배포에 사용하는 명령어로 변경하였다. 스크립트 내용은 아래와 같다.</p>
<p>&quot;docker&quot;: &quot;export NODE_ENV=production &amp;&amp; pm2-runtime start ts-node -- -r tsconfig-paths/register ./bin/<a href="http://www.ts&quot;">www.ts&quot;</a>,</p>
<p>배포 환경에는 포그라운드 방식이 아니라 백그라운드 방식으로 도커를 실행해야한다. -d 옵션을 활용해 컨테이너를 백그라운드로 실행할 경우 도커 컨테이너 내부에서 포그라운드로 실행되는 프로세스가 존재해야지만 컨테이너가 종료되지 않는다.</p>
<p>따라서 일반적으로 사용하는 pm2가 아니라 pm2-runtime을 이용해 앱을 실행했다</p>
</blockquote>
<p>이제 <code>docker build</code> 로 이미지를 빌드한다. 성공적으로 빌드가 됐다면 배포환경에 전달하기 위해 도커 허브에 이미지를 업로드한다.</p>
<blockquote>
<h4 id="빌드시-유의사항">빌드시 유의사항</h4>
<p>일반적으로 배포 서버는 amd64 아키텍처를 사용한다. 하지만 최근 맥북은 arm 기반이므로 빌드시 linux/arm 으로 빌드된다.
만약 arm 기반의 m1, m2 맥북을 사용한다면 빌드 옵션으로 --platform linux/amd64 를 명시해야만 한다.</p>
</blockquote>
<h2 id="도커-허브에-이미지-업로드하기">도커 허브에 이미지 업로드하기</h2>
<p><img src="https://velog.velcdn.com/images/tkppp-dev/post/7eac0479-e4f2-4143-90e2-273e03ce9baf/image.png" alt=""></p>
<p>도커 데스크탑을 사용하면 빌드한 이미지를 <code>push to hub</code> 버튼을 통해 간단하게 푸시할 수 있다. 유의할 점은 이미지 이름을 &lt;도커 id&gt;/&lt;이미지 이름&gt; 형식으로 지정해야지만 푸시가 된다.</p>
<pre><code class="language-bash"># 이미지 태그 작업
$ docker tag [image-name or Tag] [docker-hub-id | private-registry-ip:port]/[push-image-name]

# 이미지 푸시
$ docker push [push-image-name]</code></pre>
<p>cli를 사용할 경우 이미지를 만들고 위와 같은 커맨드를 통해 푸시할 수 있다. 로그인이 되어 있지 않다면 <code>docker login</code> 명령어로 로그인을 수행한다.</p>
<p>이제 도커 허브에 이미지 업로드까지 완료하였다. 다음 포스트에는 가상 서버 호스팅 서비스인 <code>Vultr</code> 에 서비스를 배포하는 과정을 알아보도록 하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] 도커로 Node 웹 애플리케이션 배포하기 - 1. 도커의 기본]]></title>
            <link>https://velog.io/@tkppp-dev/Docker-%EB%8F%84%EC%BB%A4%EB%A1%9C-Node-%EC%9B%B9-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-1.-%EB%8F%84%EC%BB%A4%EB%9E%80</link>
            <guid>https://velog.io/@tkppp-dev/Docker-%EB%8F%84%EC%BB%A4%EB%A1%9C-Node-%EC%9B%B9-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-1.-%EB%8F%84%EC%BB%A4%EB%9E%80</guid>
            <pubDate>Tue, 26 Jul 2022 06:59:34 GMT</pubDate>
            <description><![CDATA[<h1 id="동기">동기</h1>
<p>그동안 잘 써왔던 AWS 프리티어가 이번달부로 종료되었다. 현재 EC2 인스턴스위에 올라가 있는 서비스를 종료하던지 <code>Lightsail</code>, <code>Vultr</code> 와 같은 가상서버 호스팅 서비스(VPS)로 서비스를 마이그레이션하던지 해야했다.</p>
<p>결론은 나 혼자 쓰는 서비스지만 평소에 잘 쓰고 있었기 때문에 한달에 5천원 정도의 비용을 들여 <code>Vultr</code> 로 서비스를 옮기기로 했다. </p>
<p>처음 EC2에 서비스를 배포했을때 환경 세팅 때문에 고생한 경험이 있었기 때문에 도커를 이용해 배포를 진행하기로 했다.</p>
<h1 id="도커란">도커란?</h1>
<blockquote>
<p>도커는 애플리케이션을 신속하게 구축, 테스트 및 배포할 수 있는 소프트웨어 플랫폼이다. 소프트웨어를 컨테이너라는 표준화된 유닛으로 패키징하며, 컨테이너는 라이브러리, 시스템 도구, 코드 등 소프트웨어 실행에 필요한 모든 것이 포함되어 있다. 즉, 도커는 컨테이너 환경에서 독립적으로 애플리케이션을 실행할 수 있도록 컨테이너를 만들고 관리하는 것을 도와주는 도구이다. 도커를 통해 애플리케이션을 실행하면 독립적인 환경에서 일관된 결과를 보장한다.</p>
</blockquote>
<p>도커는 컨테이너 기술을 사용하여 프로세스를 호스트OS와 독립된 격리된 컨테이너 환경에서 실행할 수 있게 해주는 운영체제 수준의 가상화 기술이다. 초기에는 컨테이너 구현에 리눅스 컨테이너(LXC)를 사용했지만 현재는 자체 컨테이너 기술을 사용한다. 자세한 내용은 <a href="https://techblog.lotteon.com/docker%EB%A5%BC-%EC%99%9C-%EC%8D%A8%EC%95%BC%EB%90%98%EB%8A%94%EA%B0%80-2310117b4dea">이 포스트</a>을 참조</p>
<h2 id="도커를-사용했을때의-이점">도커를 사용했을때의 이점</h2>
<p>도커는 호스트OS와 격리된 환경을 만들어주기 때문에 로컬 개발 환경을 도커 컨테이너로 구축한다 배포 환경과 개발 환경이 동일해진다. 즉, 같은 컨테이너 환경에서 동작하기 때문에 로컬에서 제대로 동작한다면 배포 환경에서도 제대로 동작함을 보장할 수 있다는 것이다.</p>
<p>EC2 서버에 배포할 때마다 로컬 개발 환경과 달라 항상 제대로 동작하지 않았던 경험이 있었는데 이런 짜증나고 귀찮은 점을 해결할 수 있다.</p>
<h2 id="의문점">의문점</h2>
<p>분명 도커와 같은 컨테이너 기술은 가상 머신과 달리 게스트 OS가 없고 호스트 OS의 리눅스 커널을 공유한다고 했다. 그런데 왜 컨테이너 내에서도 OS가 포함되는 것일까? 그 이유는 리눅스에 대한 이해가 필요하다.</p>
<p>흔히 우리가 아는 <code>Ubuntu</code>, <code>CentOS</code> 와 같은 리눅스 운영체제를 <code>리눅스 배포판</code> 이다.
리눅스 배포판은 <code>리눅스 커널</code> + <code>자유소프트웨어(GNU 소프트웨어 등)</code> 로 구성된다. 즉 리눅스 배포판 버전에 따라에 리눅스 커널의 버전은 다를 수 있으나 리눅스 커널을 동일하게 사용한다. </p>
<p>컨테이너 내에 OS를 세팅하는 것은 해당 배포판과 같은 환경을 제공하는 것이지 OS를 구동하는 핵심인 리눅스 커널은 호스트 OS의 커널을 사용한다는 것이다.</p>
<p>더 자세한 내용은 아래 포스트들을 참조</p>
<blockquote>
<ul>
<li><a href="https://www.44bits.io/ko/post/is-docker-container-a-virtual-machine-or-a-process">도커 컨테이너는 가상머신인가요? 프로세스인가요?</a></li>
<li><a href="https://bcho.tistory.com/805">Docker 소개</a></li>
<li><a href="https://mosei.tistory.com/entry/Docker-Container%EC%9D%98-OS-vs-VM%EC%9D%98-OS">[Docker] Container의 OS vs VM의 OS</a></li>
</ul>
</blockquote>
<h1 id="도커의-구성">도커의 구성</h1>
<p><img src="https://velog.velcdn.com/images/tkppp-dev/post/3893c3cb-8b63-4bc9-ac4d-39711b8cfb11/image.jpeg" alt="docker-flow">
<a href="https://gngsn.tistory.com/128">Docker Engine, 제대로 이해하기</a> 참조</p>
<h2 id="이미지">이미지</h2>
<p>도커에서 서비스 운영에 필요한 서버 프로그램, 소스코드 및 라이브러리, 컴파일된 실행 파일을 묶는 형태를 이미지라한다. 다시 말해, 컨테이너를 실행하기 위한 모든 파일과 설정값(환경)을 지닌 것으로, 더 이상의 의존성 파일을 컴파일하거나 이것저것 설치할 필요 없는 상태의 파일을 의미한다. 예를 들어 Ubuntu이미지는 Ubuntu를 실행하기 위한 모든 파일을 가지고 있으며, Oracle 이미지는 Oracle을 실행하는데 필요한 파일과 실행명령어, port 정보 등을 모두 가지고 있다.</p>
<p>이미지는 읽기 전용이며 이미지로 만든 컨테이너의 내부가 변하더라도 변경되지 않는다. </p>
<h2 id="컨테이너">컨테이너</h2>
<p>이미지를 실행한 상태로, 응용프로그램의 종속성과 함께 응용프로그램 자체를 패키징 or 캡슐화하여 격리된 공간에서 프로세스를 동작시키는 기술이다.</p>
<p>컨테이너는 호스트의 자원을 공유하며 컨테이너를 삭제할 경우 컨테이너 내에서 변경된 모든 사항은 삭제된다.</p>
<h1 id="도커-사용법">도커 사용법</h1>
<h2 id="도커-파일dockerfile">도커 파일(Dockerfile)</h2>
<p>컨테이너를 만들기 위한 이미지를 만들기 위해서는 <code>Ubuntu</code>, <code>CentOS</code> 같은 베이스 이미지를 도커 허브에서 받아 컨테이너를 실행해 컨테이너 내에서 환경을 셋업한 후 <code>docker commit</code> 명령어로 이미지를 만드는 방법도 있지만 직접 컨테이너 터미널에서 작업하는 건 불편하고 문제가 생길 수 도 있다. 그렇기 때문에 이미지를 빌드하는 일종의 스크립트인 <code>Dockerfile</code> 을 만들어 이미지를 만든다.</p>
<h3 id="주요-명령어">주요 명령어</h3>
<ul>
<li><strong>FROM</strong> : 베이스 도커 이미지를 지정한다. 보통 OS 나 프로그래밍 언어 이미지를 지정하고 docker hub에서 이미지를 찾을 수 있다</li>
<li><strong>RUN</strong> : 쉘 커맨드를 도커 이미지에서 실행한다</li>
<li><strong>EXPOSE</strong> : 도커 컨테이너 외부에 노출할 포트를 지정한다. 단, 컨테이너에서 포트를 자동으로 오픈하지 않기 때문에 컨테이너 실행 시 지정된 포트를 열어주어야 한다</li>
<li><strong>ENV</strong> : 컨테이너 내의 환경 변수를 지정할때 사용</li>
<li><strong>ADD</strong> : 파일과 디렉토리를 호스트에서 지정한 도커이미지 디렉토리 안으로 복사한다. 만약 디렉토리가 없다면 새로 생성해서 복사한다. 디렉토리를 ADD하려면 끝이 “/”로 끝나야하고 파일 이름과 디렉토리 이외에도 URL도 가능하다. ADD 할려고 하는 파일이 tar 압축파일 이면 docker가 자동으로 압축을 풀어서 ADD 한다. ADD 할려고 하는 파일이나 디렉토리와 같은 이름의 파일이나 디렉토리가 벌써 image 상에 존재 한다면 덮어 씌우지 않는다<ul>
<li><strong>COPY</strong> : ADD와 기본적으로 동일하나 URL지정이 불가하며 압축파일을 자동으로 풀어주지 않는다.</li>
<li><strong>CMD</strong> : 도커 컨테이너가 시작될 때 실행할 커맨드를 지정한다</li>
<li><em>ENTRYPOINT*</em> : 도커 이미지가 실행될 때 기본 커맨드를 지정합니다</li>
</ul>
</li>
<li><strong>VOLUME</strong> : 호스트의 폴더를 도커 컨테이너에 연결 시킬 수 있습니다. 즉, 도커 내부에서 호스트 컴퓨터에서 지정한 곳의 파일을 읽거나 쓰거나 할 수 있다. 보통 로그 저장에 사용</li>
<li><strong>WORKDIR</strong> : 작업 폴더를 지정한다. cd 명령어와 유사하지만 디렉토리가 존재하지 않는다면 해당 디렉토리를 만들고 이동한다<ul>
<li><strong>SHELL</strong> : 디폴트로 지정되어 있는 shell 타입을 바꿀 수 있게 해줍니다. 디폴트 쉘은 [“/bin/sh”, “-c”] </li>
</ul>
</li>
</ul>
<blockquote>
<h4 id="cmd와-entrypoint의-차이">CMD와 ENTRYPOINT의 차이</h4>
<p>CMD와 ENTRYPOINT 모두 컨테이너 실행시 수행하는 명령어이지만 CMD는 덮어씌워질 수 있고 ENTRYPOINT는 덮어씌워지지 않고 항상 실행된다는 차이가 있다.
예를 들어 docker run 명령어로 컨테이너를 실행할 때 도커 이미지 뒤에 커맨드를 입력하면 CMD는 실행되지 않고 docker run에 쓰인 커맨드가 실행된다.
이와 다르게 ENTRYPOINT로 작성된 명령어는 docker run에 커맨드를 입력하더라도 실행되고 그 후에 docker run으로 전달된 커맨드가 실행된다.</p>
<p>컨테이너 실행 후 항상 실행되어야 하는 커맨드라면 ENTRYPOINT를 사용하고 그렇지 않다면 CMD를 사용하면 된다.</p>
</blockquote>
<h3 id="예시">예시</h3>
<pre><code class="language-bash">FROM node:16-alpine

# Korean Fonts
RUN apk --update add fontconfig
RUN mkdir -p /usr/share/fonts/nanumfont
RUN wget http://cdn.naver.com/naver/NanumFont/fontfiles/NanumFont_TTF_ALL.zip
RUN unzip NanumFont_TTF_ALL.zip -d /usr/share/fonts/nanumfont
RUN fc-cache -f &amp;&amp; rm -rf /var/cache/*

# update pkg manager
RUN apk update

# bash install
RUN apk add bash

# Language
ENV LANG=ko_KR.UTF-8 \
    LANGUAGE=ko_KR.UTF-8

# Set the timezone in docker
RUN apk --no-cache add tzdata &amp;&amp; \
        cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime &amp;&amp; \
        echo &quot;Asia/Seoul&quot; &gt; /etc/timezone

# Create Directory for the Container
WORKDIR /user/src/app

# add chromium
RUN apk add chromium

# Only copy the package.json file to work directory
COPY package.json .
RUN npm install
RUN npm install -g pm2

# Docker Demon Port Mapping
EXPOSE 80

# RUN SERVER
CMD [&quot;npm&quot;, &quot;run&quot;, &quot;docker&quot;]</code></pre>
<h1 id="마무리">마무리</h1>
<p>지금까지 도커의 기본적인 부분을 알아보았는데 다음 포스트에는 도커를 이용해 가상 서버에 배포하는 과정을 다뤄보도록 하겠습니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin]디자인 패턴 정리]]></title>
            <link>https://velog.io/@tkppp-dev/Kotlin%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@tkppp-dev/Kotlin%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 21 Jun 2022 18:22:49 GMT</pubDate>
            <description><![CDATA[<h1 id="어댑터-패턴">어댑터 패턴</h1>
<p>어댑터 패턴은 기존 인터페이스와 호환되지 않는 클래스를 기존 코드를 변경하지 않고 기존 인터페이스를 구현하는 중간 어댑터 클래스를 통해 작동하도록 하는 패턴이다</p>
<h2 id="예시">예시</h2>
<pre><code class="language-kotlin">// 기존 코드
interface JdbcDriver {
    fun findAll()
}

class MysqlDriver : JdbcDriver {
    private val name = &quot;MySQL&quot;

    override fun findAll() {
        println(&quot;$name - Find all&quot;)
    }
}

class JdbcApi(private val driver: JdbcDriver) {
    fun findAll() {
        driver.findAll()
    }
}

fun main() {
    var api = JdbcApi(MysqlDriver())
    api.findAll()
}</code></pre>
<p><code>findAll()</code> 메소드를 가지는 <code>JdbcDriver</code> 인터페이스를 구현하는 클래스를 <code>JdbcAdapter</code> 클래스에 주입해 사용하고 있는 상태이다.</p>
<p>여기서 <code>PostgresqlDriver</code> 라는 외부 라이브러리를 사용해야하는 상황이라고 가정하자. 이 클래스는 외부 라이브러리이기 때문에 <code>JdbcDriver</code> 를 구현하지 않아 <code>JdbcAdapter</code> 에 의존성을 주입할 수 없다. 이럴 경우 어댑터 패턴을 사용할 수 있다</p>
<pre><code class="language-kotlin">// 외부 라이브러리 코드
class PostgresqlDriver {
    fun findAll() {
        println(&quot;PostgreSQL - Find all&quot;)
    }
}

// 어댑터 패턴으로 구현한 어댑터 클래스
class PostgresqlAdapter : JdbcDriver {
    val driver = PostgresqlDriver()

    override fun findAll() {
        driver.findAll()
    }
}

fun main() {
    var api = JdbcApi(MysqlDriver())
    api.findAll()

    api = JdbcApi(PostgresqlAdapter())
    api.findAll()
}

/* 출력
MySQL - Find all
PostgreSQL - Find all
*/</code></pre>
<p><code>PostgresqlAdapter</code> 클래스에서 외부 라이브러리 의존성을 주입하고 <code>JdbcDriver</code> 를 구현해 어댑터 패턴으로 구현하였다.</p>
<h1 id="프록시-패턴">프록시 패턴</h1>
<p><strong>Proxy</strong>는 대리자, 대변인의 의미를 갖는 단어로 누군가를 대신해 그 역할을 수행하는 존재를 뜻한다. 프록시 패턴은 어떤 클래스의 동작을 대신 수행하는 프록시 클래스를 통해 동작한다. </p>
<p>위의 어댑터 패턴에 사용된 <code>JdbcApi</code> 클래스도 프록시 패턴을 사용한 클래스이다</p>
<h2 id="쓰임">쓰임</h2>
<ul>
<li>인터페이스를 구현한 클래스들의 동작에 실행 제어가 필요한 경우(예: 로깅, 권한에 따른 실행 제어 등)이 필요한 경우 사용할 수 있다</li>
<li>리소스가 많이 드는 객체를 항상 생성하지 않는다면 객체의 로딩을 연기하여 리소스의 낭비를 줄일 수 있다(예: JPA의 Lazy Loding)</li>
</ul>
<h2 id="예시-1">예시</h2>
<pre><code class="language-kotlin">interface FooService {
    fun runSomething()
}

class ServiceA : FooService {
    override fun runSomething() {
        println(&quot;ServiceA running&quot;)
    }
}

class ServiceB : FooService {
    override fun runSomething() {
        println(&quot;ServiceB running&quot;)
    }
}

class Proxy() : FooService {
    lateinit var service: FooService

    override fun runSomething() {
        println(&quot;Proxy Running...&quot;)
        service.runSomething()
    }
}

fun main() {
    // no proxy
    var service: FooService = ServiceA()
    service.runSomething()
    service = ServiceB()
    service.runSomething()

    // with proxy
    val proxy = Proxy()
    proxy.service = ServiceA()
    proxy.runSomething()
    proxy.service = ServiceB()
    proxy.runSomething()
}

/* 출력
ServiceA running
ServiceB running
Proxy Running...
ServiceA running
Proxy Running...
ServiceB running
*/</code></pre>
<h1 id="데코레이터-패턴">데코레이터 패턴</h1>
<p>데코레이터 패턴은 프록시 패턴과 유사하지만 프록시 패턴과 다르게 메소드 호출의 결과에 변화를 주기 위해 사용하는 패턴이다.</p>
<h2 id="예시-2">예시</h2>
<pre><code class="language-kotlin">interface FooService {
    fun runSomething(): String
}

class ServiceA : FooService {
    override fun runSomething(): String = &quot;Service A&quot;
}

class Decorator : FooService {
    lateinit var service: FooService

    override fun runSomething(): String
        = &quot;With decorator &quot; + service.runSomething()
}

fun main() {
    val service: FooService = ServiceA()
    val decorator = Decorator()
    decorator.service = service

    println(service.runSomething())
    println(decorator.runSomething())
}

/* 출력
Service A
With decorator Service A
*/</code></pre>
<h1 id="싱글톤-패턴">싱글톤 패턴</h1>
<p>싱글톤 패턴은 인스턴스를 하나만 만들어 사용하기 위한 패턴으로 다수의 객체가 불필요한 커넥션 풀, 스레드 풀과 같은 경우 인스턴스를 하나만 만들고 이를 재사용하여 불필요한 리소스의 낭비를 줄인다.</p>
<h2 id="예시-3">예시</h2>
<pre><code class="language-kotlin">object Foo {
    var name = &quot;foo&quot;
}

class Bar private constructor(var name: String) {
    companion object {
        private var instance: Bar? = null
        fun getInstance(): Bar = instance ?: Bar(&quot;bar&quot;).also { instance = it }
    }
}

fun main(){
    val s1 = Foo
    val s2 = Bar.getInstance()

    s1.name = &quot;new Foo&quot;
    s2.name = &quot;new Bar&quot;

    println(Foo.name)
    println(Bar.getInstance().name)
}

/* 출력
new Foo
new Bar
*/</code></pre>
<p>자바에서는 생성자를 private으로 만들고 정적 메소드를 통해 객체를 반환하게 하여 싱글톤 패턴을 구현한다. 하지만 코틀린은 언어 레벨에서 <code>object</code> 키워드로 하여금 싱글톤을 지원한다.</p>
<p>또 <code>companion object</code> 를 통해 자바와 같이 싱글톤을 구현할수도 있다</p>
<h1 id="스트레티지-패턴">스트레티지 패턴</h1>
<p>스트레티지 패턴은 객체의 행위를 전략이라 부르는 캡슐화한 알고리즘에 의존하는 패턴이다. 캡슐화된 알고리즘은 인터페이스를 구현한 객체로 다형성을 활용한다. 필요한 전략을 그때 그때 선택하여 객체의 행위를 결정한다.</p>
<h2 id="예시-4">예시</h2>
<pre><code class="language-kotlin">interface PaymentStrategy {
    fun pay(price: Int)
}

class KakaoPay: PaymentStrategy {
    override fun pay(price: Int) {
        println(&quot;$price Pay completed with KakaoPay&quot;)
    }
}

class NaverPay: PaymentStrategy {
    override fun pay(price: Int) {
        println(&quot;$price Pay completed with NaverPay&quot;)
    }
}

class ShoppingCart(
    val items: MutableList&lt;Int&gt; = mutableListOf()
) {

    fun getTotalPrice(): Int = items.fold(0) { total, price -&gt; total + price }

    fun pay(paymentMethod: PaymentStrategy) {
        paymentMethod.pay(getTotalPrice())
    }
}

fun main() {
    val cart = ShoppingCart()
    cart.items.addAll(listOf(100, 500, 700))

    cart.pay(KakaoPay())
    cart.pay(NaverPay())
}

/* 출력
1300 Pay completed with KakaoPay
1300 Pay completed with NaverPay
*/</code></pre>
<h1 id="옵저버-패턴">옵저버 패턴</h1>
<p>옵저버 패턴은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다. 주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다.</p>
<h2 id="예시-5">예시</h2>
<pre><code class="language-kotlin">interface Leader {
    fun subscribe(follower: Follower)
    fun unsubscribe(follower: Follower)
    fun notifyMember(msg: String)
}

interface Follower {
    fun update(msg: String)
}

class Member(val name: String, private val followers: MutableList&lt;Follower&gt; = mutableListOf()) : Leader, Follower {
    override fun subscribe(follower: Follower) {
        followers.add(follower)
    }

    override fun unsubscribe(follower: Follower) {
        followers.remove(follower)
    }

    override fun notifyMember(msg: String) {
        followers.forEach { it.update(msg) }
    }

    override fun update(msg: String) {
        println(&quot;$name - $msg&quot;)
    }

    fun writeTwit(twit: String){
        notifyMember(&quot;$name twit: $twit&quot;)
    }
}

fun main() {
    val m1 = Member(&quot;Harry&quot;)
    val m2 = Member(&quot;Potter&quot;)
    val m3 = Member(&quot;Ron&quot;)
    val m4 = Member(&quot;Wisely&quot;)

    m1.subscribe(m2)
    m1.subscribe(m3)
    m1.subscribe(m4)

    m1.writeTwit(&quot;twit!&quot;)
}

/* 출력
Potter - Harry twit: twit!
Ron - Harry twit: twit!
Wisely - Harry twit: twit!
*/</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] JUnit5 테스트 순서 지정하기]]></title>
            <link>https://velog.io/@tkppp-dev/SpringBoot-JUnit5-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%88%9C%EC%84%9C-%EC%A7%80%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@tkppp-dev/SpringBoot-JUnit5-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%88%9C%EC%84%9C-%EC%A7%80%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 06 Jun 2022 15:12:04 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-kotlin">package com.tkppp.sportresult.Integration

import com.ninjasquad.springmockk.SpykBean
import com.tkppp.sportresult.kbo.controller.KboRankController
import com.tkppp.sportresult.kbo.domain.KboRankRepository
import com.tkppp.sportresult.kbo.service.KboRankService
import io.mockk.verify
import org.junit.jupiter.api.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
class KboRankIntegrationTest(
    @Autowired private val kboRankRepository: KboRankRepository,
    @Autowired private val kboRankController: KboRankController
) {

    @SpykBean
    private lateinit var kboRankService: KboRankService

    @Test
    @Order(1)
    @DisplayName(&quot;KBO 랭크 DB 초기화 테스트&quot;)
    fun insertKboRank() {
        // when
        kboRankController.updateKboRanking()

        // then
        val rank = kboRankRepository.findAll()
        verify(exactly = 1) { kboRankService.insertKboRank(any()) }
        rank.map{
            println(&quot;${it.rank} ${it.name.fullName} ${it.win} ${it.draw} ${it.defeat} ${it.winRate} ${it.gameDiff}&quot;)
        }
    }

    @Test
    @Order(2)
    @DisplayName(&quot;KBO 랭크 DB 업데이트 테스트&quot;)
    fun updateKboRank() {
        // when
        kboRankController.updateKboRanking()

        // then
        val rank = kboRankRepository.findAll()
        verify(exactly = 0) { kboRankService.insertKboRank(any()) }
        rank.map{
            println(&quot;${it.rank} ${it.name.fullName} ${it.win} ${it.draw} ${it.defeat} ${it.winRate} ${it.gameDiff}&quot;)
        }
    }
}</code></pre>
<p><code>@TestMethodOrder(MethodOrderer.OrderAnnotation::class)</code> 어노테이션과 <code>@Order()</code> 어노테이션으로 지정 </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] Kotlin 환경에서 JSON 직렬화/역직렬화]]></title>
            <link>https://velog.io/@tkppp-dev/SpringBoot-Kotlin-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-JSON-%EC%A7%81%EB%A0%AC%ED%99%94%EC%97%AD%EC%A7%81%EB%A0%AC%ED%99%94</link>
            <guid>https://velog.io/@tkppp-dev/SpringBoot-Kotlin-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-JSON-%EC%A7%81%EB%A0%AC%ED%99%94%EC%97%AD%EC%A7%81%EB%A0%AC%ED%99%94</guid>
            <pubDate>Mon, 06 Jun 2022 06:06:23 GMT</pubDate>
            <description><![CDATA[<h1 id="직렬화와-역직렬화">직렬화와 역직렬화</h1>
<p>Java에서 말하는 직렬화(Serialization)는 객체를 직렬화하여 전송 가능한 형태로 만드는 것을 의미한다. 객체들의 데이터를 연속적인 데이터로 변형하여 Stream을 통해 데이터를 읽도록 해준다.
이것은 주로 객체들을 통째로 파일로 저장하거나 전송하고 싶을 때 주로 사용된다.</p>
<p>역직렬화(Deserialization)는 직렬화된 파일 등을 역으로 직렬화하여 다시 객체의 형태로 만드는 것을 의미한다. 저장된 파일을 읽거나 전송된 스트림 데이터를 읽어 원래 객체의 형태로 복원한다.</p>
<blockquote>
<h4 id="예시">예시</h4>
<p>요청 본문으로 들어온 JSON 데이터를 자바 객체로 파싱하는 것 =&gt; 역직렬화
응답 본문에 자바 객체를 JSON 으로 파싱하는 것 =&gt; 직렬화</p>
</blockquote>
<h1 id="스프링부트의-json-파싱">스프링부트의 JSON 파싱</h1>
<p>요청 본문과 응답 본문과 같은 부분은 Jackson 라이브러리를 기본으로 JSON 파싱을 수행</p>
<h1 id="javakotlin-json-파싱-라이브러리">Java/Kotlin JSON 파싱 라이브러리</h1>
<h2 id="jackson">Jackson</h2>
<h3 id="설정">설정</h3>
<p>스프링부트는 기본적으로 Jackson 라이브러리가 포함되어 있으므로 의존성 설정은 필요하지 않다.</p>
<p>사용시 <code>ObjectMapper</code> 객체를 생성해서 사용한다. 코틀린 환경에서는 <code>registerKotlinModule()</code> 메소드를 사용해야만 <code>Pair</code> 같은 코틀린 객체를 파싱할 수 있다.</p>
<pre><code class="language-kotlin">val mapper = ObjectMapper().registerKotlinModule()</code></pre>
<h3 id="직렬화">직렬화</h3>
<p><code>writeValueAsString(object)</code> 메소드를 사용해 객체를 직렬화 한다.</p>
<h3 id="역직렬화">역직렬화</h3>
<p><code>readValue&lt;T&gt;(jsonString)</code> 메소드를 사용해 Json을 객체로 역직렬화 한다.</p>
<h3 id="테스트">테스트</h3>
<pre><code class="language-kotlin">
data class TripleJson (
    val fullname: Triple&lt;String, String, Int&gt;
)

@Test
@DisplayName(&quot;JSON 직렬화/역직렬화 테스트 - Jackson&quot;)
fun jsonTestWithJackson() {
    val mapper = ObjectMapper().registerKotlinModule()
    val me = TripleJson(Triple(&quot;Park&quot;, &quot;Taekyeong&quot;, 26))
    val serialize = mapper.writeValueAsString(me)
    println(serialize)

    val deserialize = mapper.readValue&lt;TripleJson&gt;(serialize)
    println(deserialize)
}

/* 테스트 결과
{&quot;fullname&quot;:{&quot;first&quot;:&quot;Park&quot;,&quot;second&quot;:&quot;Taekyeong&quot;,&quot;third&quot;:26}}
PairJson(fullname=(Park, Taekyeong, 26))
*/</code></pre>
<h1 id="kotlin-json-파싱-라이브러리">Kotlin JSON 파싱 라이브러리</h1>
<h2 id="kotlinxserialization">Kotlinx.Serialization</h2>
<p>kotlin serialization 은 단순 json 라이브러리가 아니다. 라이브러리 이름에서부터 느껴지듯이 kotlin 객체 직렬화를 위한 라이브러리다.</p>
<p>또 단순 라이브러리가 아니라 컴파일러 레벨에서 동작하는 컴파일러 플러그인이기 때문에 serialization 을 이용할때는 gradle 에 플러그인 설정을 해줘야한다.</p>
<p>플러그인 설정 시 JSON 직렬화 / 역직렬화가 필요할 시 기본으로 설정되있는 <code>Jackson</code> 라이브러리가 아니라 <code>Kotlinx.Serialization</code> 으로 파싱이 이루어진다. 단 <code>Kotlinx.Serialization</code> 로 파싱에 실패할 경우 <code>Jackson</code> 라이브러리로 파싱이 이루어지므로 파싱이 성공하더라도 <code>Kotlinx.Serialization</code> 으로 파싱된 것이 아닐 수 있음에 유의</p>
<h3 id="설정-1">설정</h3>
<ul>
<li>플러그인 및 의존성 설정<pre><code class="language-kotlin">plugins {
  kotlin(&quot;jvm&quot;) version &quot;1.6.21&quot; // or kotlin(&quot;multiplatform&quot;) or any other kotlin plugin
  kotlin(&quot;plugin.serialization&quot;) version &quot;1.6.21&quot;
}
</code></pre>
</li>
</ul>
<p>dependencies {
    implementation(&quot;org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3&quot;)
}</p>
<pre><code>더 자세한 내용은 [공식 문서](https://github.com/Kotlin/kotlinx.serialization) 참조

- 직렬화 / 역직렬화 할 객체에 `@Serilazable` 어노테이션 작성
``` kotlin
@Serializable
data class TripleJson (
    val fullname: Triple&lt;String, String, Int&gt;
)</code></pre><h3 id="직렬화-1">직렬화</h3>
<p><code>Json.encodeToString(object)</code> 확장 함수를 사용해 객체를 직렬화 한다.</p>
<h3 id="역직렬화-1">역직렬화</h3>
<p><code>Json.decodeFromString&lt;T&gt;(jsonString)</code> 확장 함수를 사용해 Json을 객체로 역직렬화 한다.</p>
<h3 id="테스트-1">테스트</h3>
<pre><code class="language-kotlin">@Test
@DisplayName(&quot;JSON 직렬화/역직렬화 테스트 - Kotlinx.serialization&quot;)
fun jsonTest() {
    val me = TripleJson(Triple(&quot;Park&quot;, &quot;Taekyeong&quot;, 26))
    val serialize = Json.encodeToString(me)
    println(serialize)

    val deserialize = Json.decodeFromString&lt;TripleJson&gt;(serialize)
    println(deserialize)
}

/* 테스트 결과
{&quot;fullname&quot;:{&quot;first&quot;:&quot;Park&quot;,&quot;second&quot;:&quot;Taekyeong&quot;,&quot;third&quot;:26}}
PairJson(fullname=(Park, Taekyeong, 26))
*/</code></pre>
<h1 id="enum-파싱">enum 파싱</h1>
<p>열거형 클래스의 name과 JSON의 문자열 데이터가 일치한다면 별도의 설정없이 파싱이 된다.</p>
<p>문제는 열거형 클래스의 기본 name이 아닌 다른 변수와 JSON의 문자열 데이터를 매칭하려하는 경우에는 별도의 설정이 필요하다</p>
<h2 id="문제상황">문제상황</h2>
<pre><code class="language-kotlin">enum class KboTeam(val fullName: String, val code: String){
    SS(&quot;삼성&quot;, &quot;SS&quot;), KW(&quot;키움&quot;, &quot;WO&quot;), NX(&quot;넥센&quot;, &quot;WO&quot;),
    HH(&quot;한화&quot;,&quot;HH&quot;), LG(&quot;LG&quot;, &quot;LG&quot;), LT(&quot;롯데&quot;,&quot;LT&quot;),
    SSG(&quot;SSG&quot;, &quot;SK&quot;), SK(&quot;SK&quot;, &quot;SK&quot;), HT(&quot;KIA&quot;, &quot;HT&quot;),
    OB(&quot;두산&quot;, &quot;OB&quot;), NC(&quot;NC&quot;, &quot;NC&quot;), KT(&quot;KT&quot;, &quot;KT&quot;);
}</code></pre>
<p>팀 코드는 같지만 구단명 변경 같은 이유로 해서 구단 이름이 다른 경우가 있고 한글은 enum 기본 이름으로 사용할 수 없기 때문에 별도의 변수에 JSON 문자열 데이터를 매핑해야되는 상황이었다.</p>
<h2 id="해결">해결</h2>
<pre><code class="language-kotlin">enum class KboTeam(@JsonValue val fullName: String, val code: String){
    SS(&quot;삼성&quot;, &quot;SS&quot;), KW(&quot;키움&quot;, &quot;WO&quot;), NX(&quot;넥센&quot;, &quot;WO&quot;),
    HH(&quot;한화&quot;, &quot;HH&quot;), LG(&quot;LG&quot;,  &quot;LG&quot;), LT(&quot;롯데&quot;, &quot;LT&quot;),
    SSG(&quot;SSG&quot;, &quot;SK&quot;), SK(&quot;SK&quot;, &quot;SK&quot;), HT(&quot;KIA&quot;, &quot;HT&quot;),
    OB(&quot;두산&quot;, &quot;OB&quot;), NC(&quot;NC&quot;, &quot;NC&quot;), KT(&quot;KT&quot;, &quot;KT&quot;);

    companion object {
        @JvmStatic
        @JsonCreator
        fun set(team: String) = values().find { it.fullName == team }
    }
}</code></pre>
<p><code>@JsonValue</code> 어노테이션으로 매핑할 변수를 지정하고 companion object 와 <code>@JvmStatic</code> 어노테이션을 통해 정적 메소드를 정의한다. 여기서 <code>@JsonCreator</code> 어노테이션을 사용해 매핑에 사용될 메소드를 정의하면 원하는 대로 파싱이 이루어진다.</p>
<h1 id="pair-triple-과-같은-kotlin-특화-객체-파싱">Pair, Triple 과 같은 Kotlin 특화 객체 파싱</h1>
<p>최근 파이썬을 주로 사용했다보니 튜플과 리스트가 자연스럽게 구조분해할당이 되는 것처럼 Pair과 Triple도 리스트와 매핑될거라 생각하고 코드를 작성했지만 Pair과 Triple은 first, second, third(Triple 한정)를 프로퍼티로 가지는 객체와 매핑되기 때문에 실패했다. 결국 리스트로 타입을 변경하여 문제를 해결하였다.</p>
<blockquote>
<h4 id="참조">참조</h4>
<p><a href="https://tourspace.tistory.com/357">https://tourspace.tistory.com/357</a>
<a href="https://smelting.tistory.com/68">https://smelting.tistory.com/68</a>
<a href="https://multifrontgarden.tistory.com/285">https://multifrontgarden.tistory.com/285</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[네트워크 정리]]></title>
            <link>https://velog.io/@tkppp-dev/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@tkppp-dev/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 26 May 2022 05:29:29 GMT</pubDate>
            <description><![CDATA[<h1 id="인터넷이란">인터넷이란?</h1>
<p>네트워크 주변부(Network edge)와 네트워크 중심부(Network core)가 엑세스 네트워크(Access Network)로 하여금 연결된 것</p>
<ul>
<li><code>Network edge</code> : 사용자가 인터넷에 접속하기 위한 부분으로 인터넷에 연결되는 모든 장치를 뜻함. 다른 말로 <code>End System</code> 또는 <code>Host</code> 라 함</li>
<li><code>Access Network</code> : 네트워크에 접근하기 위한 네트워크로 ISP(Internet Service Provider)가 제공하며 네트워크 엣지의 기기를 네트워크 코어에 연결되도록 하는 역할을 함</li>
<li><code>Network core</code> : <code>End System</code> 사이에서 데이터를 전달하는 네트워크로 <code>Mesh of interconnected routers</code> 즉, 수많은 라우터 또는 스위치들이 그물처럼 얽혀있는 구조이다</li>
</ul>
<h2 id="network-core">Network core</h2>
<p>데이터는 <code>패킷</code> 이라는 단위로 전달되며 네트워크 코어에서 패킷이 이동하는 방식을 <code>Packet Switching</code> 이라고 한다. 패킷은 비트 단위로 전달되며 노드(호스트 또는 라우터) 사이를 이동할 때 한 패킷의 모든 비트가 전달되면 다음 노드로 전달된다. 이러한 방식의 패킷 전송을 <code>Store and Forward</code> 방식이라 한다.</p>
<blockquote>
<h4 id="packet-switching-과-circuit-switching">Packet Switching 과 Circuit Switching</h4>
<p>패킷 교환은 라우팅과 포워딩의 과정으로 이루어져있는데 라우팅이란 라우터에서 목적지까지의 최단경로를 찾는 과정이다. 라우팅을 통해 네트워크 내에 모든 라우터와 호스트의 최단 경로를 계산해 라우팅 테이블에 저장해놓고 패킷이 들어왔을때 최단경로와 연결된 노드로 패킷을 내보내는데 이 내보내는 과정을 포워딩이라 한다.</p>
<p>반면 회선 교환은 데이터가 전송될 경로를 처음부터 찾아놓고 해당 경로의 회선을 예약한 다음 데이터을 전송한다. 즉 예약된 회선은 데이터가 다 전달되기 전까지 다른 데이터를 전송하는데 사용할 수 없다.</p>
<p>인터넷은 패킷 교환 방식을 사용한다. 그 이유는 패킷 교환 방식은 많은 사용자를 빠른 시간 내에 인터넷에 연결할 수 있기 때문이다. 회선 교환은 경로를 찾고 회선을 예약하는데 오버헤드가 존재하기 때문이다. 하지만 패킷 교환 방식은 네트워크의 혼잡도에 따라 전송이 불안정해지거나 전송에 실패하게 되는 문제가 있다. 반면 회선 교환은 패킷 교환 방식에 비해 한번에 사용할 수 있는 사람이 한정되어 있지만 실제로 그런 경우는 별로 발생하지 않는다고 한다. 따라서 전화와 같이 연결이 안정적으로 유지되어야 하는 서비스에 경우 회선 교환을 사용한다.</p>
</blockquote>
<h2 id="대역폭과-전송-속도">대역폭과 전송 속도</h2>
<p>네트워크에서의 대역폭은 초당 전송될 수 있는 비트의 최대량을 뜻한다.
네트워크에서 전송 속도는 초당 현재 전송되는 비트들의 평균 속도를 의미한다.</p>
<p>예를 들면 대역폭은 고속도로에서의 최대 제한 속도라면 전송 속도는 현재 고속도로에 있는 자동차들의 평균 속도라고 생각할 수 있다. 대역폭은 고정된 값인 반면 전송 속도는 네트워크 상태에 따라 달라진다</p>
<h2 id="throughput-처리량">Throughput (처리량)</h2>
<p>End to End를 고려했을 때 받는 쪽의 단위 시간당 데이터량을 의미한다. 일반적인 처리량은 End to End의 포함되는 링크 중 최소 전송률이 되고 그 링크를 <strong>병목 링크(Bottleneck Link)</strong> 라고 한다.</p>
<p>만약 여러 호스트가 병목 링크를 공유한다면 어떻게 될까? 10개의 클라이언트가 각각 서로 다른 서버에 데이터를 전달하고 1mbps의 전송속도를 가진 병목 링크를 공유한다고 가정해보자. 이러할 경우 링크의 전송 속도는 한정되어 있고 패킷량에 따라 나눠지므로 각각의 처리량은 0.1mbps가 된다.</p>
<h2 id="네트워크-지연">네트워크 지연</h2>
<p>네트워크의 지연은 4가지 요소로 결정된다.</p>
<ol>
<li><code>Processing delay</code> : 전송된 패킷의 오류 검사 및 포워딩 테이블에 맞는 Output 링크로 패킷을 이동하는 시간</li>
<li><code>Queuing delay</code> : 패킷이 나가는 속도보다 들어오는 속도가 빠르면 패킷을 큐라는 버퍼에 저장한다. 이때 패킷이 큐에서 나가기까지의 시간</li>
<li><code>Transmission delay</code> : 라우터가 패킷을 링크로 내보내는데 걸리는 시간</li>
<li><code>Propagation delay</code> : 하나의 비트가 물리적 링크를 따라 전송되는 시간</li>
</ol>
<p>이 중 Processing delay는 매우 작아서 일반적으로 무시되고 <code>Transmission delay</code> 와 <code>Propagation delay</code> 는 각각 노드의 성능이나 링크의 성능에 따라 고정된 값이고 큐잉 딜레이는 네트워크의 혼잡도에 따라 없거나 매우 클 수 있다. 결론적으로 노드에서의 전체 지연 시간은 위의 4가지를 합친 것과 같다.</p>
<h1 id="네트워크-분류">네트워크 분류</h1>
<h2 id="lan-local-area-network">LAN (Local Area Network)</h2>
<p>근거리 통신망으로 구내 정보 통신망은 네트워크 매체를 이용하여 집, 사무실, 학교 등의 건물과 같은 가까운 지역을 한데 묶는 컴퓨터 네트워크이다.</p>
<h2 id="man-metropolitan-area-network">MAN (Metropolitan Area Network)</h2>
<p>도시권 통신망은 큰 도시 또는 캠퍼스에 퍼져 있는 컴퓨터 네트워크이다. LAN과 WAN의 중간 크기를 갖는다. DSL 전화망, 케이블 TV 네트워크를 통한 인터넷 서비스 제공이 대표적인 예이다.</p>
<h2 id="wan-wide-area-network">WAN (Wide Area Network)</h2>
<p>광역 통신망은 드넓은 지리적 거리/장소를 넘나드는 통신 네트워크 또는 컴퓨터 네트워크이다. 광역 통신망은 종종 전용선과 함께 구성된다.</p>
<h2 id="범위">범위</h2>
<p>간단하게 LAN은 건물 단위, MAN은 도시 단위, WAN은 국가 또는 대륙단위의 네트워크라고 생각할 수 있다.</p>
<h1 id="osi-7계층과-tcpip-4계층">OSI 7계층과 TCP/IP 4계층</h1>
<p>OSI 7계층은 네트워크 프로토콜이 통신하는 구조를 7개의 계층으로 분리하여 각 계층간 상호 작동하는 방식을 정해 놓은 것이다. 컴퓨터 통신 구조의 모델과 앞으로 개발될 프로토콜의 표준적인 뼈대를 제공하기 위해 개발된 참조 모델이어서 OSI 7 계층 모델을 알면 네트워크 구성을 예측하고 이해할 수 있다. 네트워크에서 트래픽의 흐름을 꿰뚫어 볼 수 있으며, 각 계층은 독립되어 있으며 서로 관여할 수 없다. 7단계 중 특정한 곳에 이상이 생기면 다른 단계의 장비 및 소프트웨어를 건드리지 않고도 이상이 생긴 단계만 고칠 수 있다.</p>
<p>TCP/IP 4계층은 오늘날 흔히 쓰이는 TCP/IP 프로토콜이 미 국방성 통신 표준으로 채택되면서, 해당 프로토콜의 통신 과정을 크게 4개의 계층 구조로 나눠 설명한 것이다.</p>
<p><img src="https://velog.velcdn.com/images/tkppp-dev/post/439feccf-3dfa-4f54-9efc-c1f0ecc6bf5d/image.png" alt="">
<img src="https://velog.velcdn.com/images/tkppp-dev/post/7302cef3-3eb0-4cfc-b3da-95727a4c0fbd/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>계층</th>
<th>이름</th>
<th>단위(PDU)</th>
<th>예시</th>
<th>프로토콜(Protocols)</th>
<th>디바이스(Device)</th>
</tr>
</thead>
<tbody><tr>
<td>7</td>
<td>응용 계층(Application Layer)</td>
<td>Data</td>
<td>텔넷(Telnet), 구글 크롬, 이메일, 데이터베이스 관리</td>
<td>HTTP, SMTP, SSH, FTP, Telnet, DNS, modbus, SIP, AFP, APPC, MAP</td>
<td></td>
</tr>
<tr>
<td>6</td>
<td>표현 계층(Presentation Layer)</td>
<td>Data</td>
<td>인코딩, 디코딩, 암호화, 복호화</td>
<td>ASCII, MPEG, JPEG, MIDI, EBCDIC, XDR, AFP, PAP</td>
<td></td>
</tr>
<tr>
<td>5</td>
<td>세션 계층 (Session Layer)</td>
<td>Data</td>
<td></td>
<td>NetBIOS, SAP, SDP, PIPO, SSL, TLS, NWLink, ASP, ADSP, ZIP, DLC</td>
<td></td>
</tr>
<tr>
<td>4</td>
<td>전송 계층 (Transport Layer)</td>
<td>TCP-Segment, UDP-datagram</td>
<td>특정 방화벽 및 프록시 서버</td>
<td>TCP, UDP, SPX, SCTP, NetBEUI, RTP, ATP, NBP, AEP, OSPF</td>
<td>게이트웨이</td>
</tr>
<tr>
<td>3</td>
<td>네트워크 계층 (Network Layer)</td>
<td>Packet</td>
<td>라우터</td>
<td>IP, IPX, IPsec, ICMP, ARP, NetBEUI, RIP, BGP, DDP, PLP</td>
<td>라우터</td>
</tr>
<tr>
<td>2</td>
<td>데이터링크 계층 (DataLink Layer)</td>
<td>Frame</td>
<td>MAC 주소, 브리지 및 스위치</td>
<td>Ethernet, Token Ring, AppleTalk, PPP, ATM, MAC, HDLC, FDDI, LLC, ALOHA</td>
<td>브릿지, 스위치</td>
</tr>
<tr>
<td>1</td>
<td>물리 계층 (Physical Layer)</td>
<td>Bit</td>
<td>전압, 허브, 네트워크 어댑터, 중계기 및 케이블 사양, 신호 변경(디지털,아날로그)</td>
<td>10BASE-T, 100BASE-TX, ISDN, wired, wireless, RS-232, DSL, Twinax</td>
<td>허브, 리피터</td>
</tr>
</tbody></table>
<h2 id="tcpip-4계층---애플리케이션-계층">TCP/IP 4계층 - 애플리케이션 계층</h2>
<h3 id="7---애플리케이션application-계층">7 - 애플리케이션(Application) 계층</h3>
<p><code>Application Layer</code> 는 OSI 7계층 모델에서 최상위 계층으로 사용자가 네트워크 자원에 접근하는 방법을 제공한다. 그리고 7계층은 최종적으로 <strong>사용자가 볼 수 있는 유일한 계층으로 모든 네트워크 활동의 기반이 되는 인터페이스를 제공하는데, 즉 사용자가 실행하는 응용 프로그램들이 7계층에 속한다고 보면 된다</strong>. 예를 들면 가상 터미널인 텔넷(telnet), 구글의 크롬(chrome), 이메일(전자우편), 데이터베이스 관리 등의 서비스를 제공한다. 사용자와 가장 가까운 계층이다.</p>
<h3 id="6---표현presentation-계층">6 - 표현(Presentation) 계층</h3>
<p><code>Presentation Layer</code> 는 응용 계층으로부터 전달받은 데이터를 읽을 수 있는 형식으로 변환하는데 표현 계층은 응용 계층의 부담을 덜어주는 역할이 되기도 한다. 응용 계층으로부터 전송받거나 응용 계층으로 전달해야 할 데이터의 인코딩과 디코딩이 이 계층에서 이루어진다. 그리고 표현 계층은 데이터를 안전하게 사용하기 위해서 암호화와 복호화를 하는데 이 작업도 표현 계층에서 이루어진다. 예를 들면 유니코드(UTF-8)로 인코딩 되어있는 문서를 ASCII로 인코딩 된 문서로 변환하려 할 때 이 계층에서 변환이 이루어진다. </p>
<h3 id="5---세션session-계층">5 - 세션(Session) 계층</h3>
<p><code>Session Layer</code> 는 두 컴퓨터 간의 대화나 세션을 관리하며, 포트(Port)연결이라고도 한다. 모든 통신 장치 간에 연결을 설정하고 관리 및 종료하고 또한 연결이 <code>양방향(Full duplex)</code>인지 <code>단방향(half duplex)</code>인지 여부를 확인하고 체크 포인팅과 유휴, 재시작 과정 등을 수행하며 호스트가 갑자기 중지되지 않고 정상적으로 호스트를 연결하는 데 책임이 있다. 즉 이 계층에서는 TCP/IP 세션을 만들고 없애고 통신하는 사용자들을 동기화하고 오류 복구 명령들을 일괄적으로 다루며 통신을 하기 위한 세션을 확립, 유지, 중단하는 작업을 수행한다.</p>
<h2 id="tcpip-3계층">TCP/IP 3계층</h2>
<h3 id="4---전송transport-계층">4 - 전송(Transport) 계층</h3>
<p><code>Transport Layer</code> 의 주목적은 하위 계층에 신뢰할 수 있는 데이터 전송 서비스를 제공하는 것이다. 컴퓨터와 컴퓨터 간에 신뢰성 있는 데이터를 서로 주고받을 수 있도록 해주어 상위 계층들이 데이터 전달의 유효성이나 효율성을 생각하지 않도록 부담을 덜어주는데, 이때 시퀀스 넘버 기반의 오류 제어 방식을 사용한다. 흐름 제어, 분할/분리 및 오류 제어를 통해 전송 계층은 데이터가 오류 없이 점-대-점으로 전달되게 하는데 신뢰할 수 있는 데이터 전송을 보장하는 것은 매우 번거롭기에 OSI 모델은 전체 계층을 사용한다. 전송 계층은 연결형 프로토콜과 비 연결형 프로토콜을 모두 사용한다. 전송 계층의 예로는 특정 방화벽이나 프록시 서버가 있다.</p>
<h2 id="tcpip-2계층">TCP/IP 2계층</h2>
<h3 id="3---네트워크network-계층">3 - 네트워크(Network) 계층</h3>
<p><code>Network Layer</code> 에서는 2홉 이상의 통신(멀티 홉 통신)을 담당한다. OSI 7 계층에서 가장 복잡한 계층 중 하나로서 실제 네트워크 간에 데이터 라우팅을 담당한다. 이때 라우팅이란 어떤 네트워크 안에서 통신 데이터를 짜여진 알고리즘에 의해 최대한 빠르게 보낼 최적의 경로를 선택하는 과정을 라우팅이라고 한다. 네트워크 계층은 네트워크 호스트의 논리 주소 지정(ex : ip 주소 사용)을 확인한다. 또한 데이터 스트림을 더 작은 단위로 분할하고 경우에 따라 오류를 감지해 처리한다. 그리고 여러 개의 노드를 거칠 때마다 경로를 찾아주는 역할을 하는 계층으로서 다양한 길이의 데이터를 네트워크들을 통해 전달하고 그 과정에서 전송 계층이 요구하는 서비스 품질을 제공하기 위한 기능적, 절차적 수단을 제공한다. 네트워크 계층은 라우팅, 흐름 제어, 세그멘테이션, 오류제어, 인터네트워킹 등을 수행한다. 라우터가 3계층에서 동작하고, 3계층에서 동작하는 스위치도 있다.</p>
<h2 id="tcpip-1계층">TCP/IP 1계층</h2>
<h3 id="2---데이터링크datalink-계층">2 - 데이터링크(Datalink) 계층</h3>
<p><code>DataLink Layer</code> 은 물리적인 네트워크를 통해 데이터를 전송하는 수단을 제공한다. 1홉 통신을 담당한다고도 말한다. 홉(hop)은 컴퓨터 네트워크에서 노드에서 다음 노드로 가는 경로를 말한다. 1홉 통신이면 한 라우터에서 그다음 라우터까지의 경로를 말한다. 주목적은 물리적인 장치를 식별하는 데 사용할 수 있는 주소 지정 체계를 제공하는 것이다. 데이터 링크 계층은 포인트 투 포인트 간의 신뢰성 있는 전송을 보장하기 위한 계층으로 CRC 기반의 오류 제어와 흐름 제어가 필요하다. 네트워크 위의 개체들 간 데이터를 전달하고 물리 계층에서 발생할 수 있는 오류를 찾아내고 수정하는 데 필요한 기능적, 절차적 수단을 제공한다. 이 계층의 예시를 들자면 브리지 및 스위치 그리고 이더넷 등이 있다.</p>
<h3 id="1---물리physical-계층">1 - 물리(Physical) 계층</h3>
<p><code>Physical Layer</code> 은 OSI 모델의 맨 밑에 있는 계층으로서, 네트워크 데이터가 전송되는 물리적인 매체이다. 데이터는 0과 1의 비트열로 ON, OFF의 전기적 신호 상태로 이루어져 있다. 이 계층은 전압, 허브, 네트워크 어댑터, 중계기 및 케이블 사양을 비롯해 사용된 모든 하드웨어의 물리적 및 전기적 특성을 정의한다. 물리 계층은 연결을 설정 및 종료하고 통신 자원을 공유하는 수단을 제공하며 디지털에서 아날로그로 또는 그 반대로 신호를 변환하는 역할을 한다. OSI 모델에서 가장 복잡한 계층으로 간주된다.</p>
<h1 id="application-layer">Application Layer</h1>
<h2 id="http-protocol">HTTP Protocol</h2>
<p>HTTP는 HTML 문서와 같은 리소스들을 가져올 수 있도록 해주는 프로토콜으로 웹에서 이루어지는 모든 데이터 교환의 기초이며, 클라이언트-서버 방식으로 동작한다.  하나의 완전한 문서는 텍스트, 레이아웃 설명, 이미지, 비디오, 스크립트 등 불러온(fetched) 하위 문서들로 재구성됩니다.</p>
<h1 id="transport-layer">Transport Layer</h1>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] 스택]]></title>
            <link>https://velog.io/@tkppp-dev/Kotlin-%EC%8A%A4%ED%83%9D</link>
            <guid>https://velog.io/@tkppp-dev/Kotlin-%EC%8A%A4%ED%83%9D</guid>
            <pubDate>Sun, 01 May 2022 10:08:57 GMT</pubDate>
            <description><![CDATA[<h1 id="스택-사용하기">스택 사용하기</h1>
<p>코틀린 컬렉션에는 스택이 따로 구현되어 있지 않다.</p>
<p>스택을 사용하기 위해서는 java.util.Stack 클래스를 사용하거나 MutableList, ArrayList의 add()와 removeLast()를 사용해 스택처럼 사용한다.</p>
<h2 id="궁금중">궁금중</h2>
<p>자바로 구현된 Stack과 코틀린 네이티브인(결국 Java로 컴파일되서 JVM에 올라가긴 하지만) Mutable, ArrayList를 사용하는 경우의 성능차이가 궁금해서 push, pop의 시간을 측정해보았다.</p>
<pre><code class="language-kotlin">fun main() {
    val repetitions = 10000000
    val stack1 = Stack&lt;Int&gt;()
    val stack2 = mutableListOf&lt;Int&gt;()
    val stack3 = arrayListOf&lt;Int&gt;()

    val pushTime1 = measureNanoTime { repeat(repetitions) { stack1.push(it) } }
    val pushTime2 = measureNanoTime { repeat(repetitions) { stack2.add(it) } }
    val pushTime3 = measureNanoTime { repeat(repetitions) { stack3.add(it) } }
    val mid1 = listOf(pushTime1, pushTime2, pushTime3).sorted()[1]

    println(&quot;${pushTime1 - mid1} ${pushTime2 - mid1} ${pushTime3 - mid1}&quot;)

    val popTime1 = measureNanoTime { repeat(repetitions) { stack1.pop() } }
    val popTime2 = measureNanoTime { repeat(repetitions) { stack2.removeLast() } }
    val popTime3 = measureNanoTime { repeat(repetitions) { stack3.removeLast() } }
    val mid2 = listOf(popTime1, popTime2, popTime3).sorted()[1]

    println(&quot;${popTime1 - mid2} ${popTime2 - mid2} ${popTime3 - mid2}&quot;)
}</code></pre>
<p>결과적으로 push는 Stack, MutableList, ArrayList 순으로 빨랐고 pop은 ArrayList, MutableList, Stack 순으로 빨랐다.</p>
<p>Stack의 경우 Vector로 구현되어 있어 pop시 동기화가 필요해 pop에서 성능이 상대적으로 낮은 것으로 나타났다.</p>
<p>신기한 점은 ArrayList는 MutableList를 구현한 컬렉션이고 mutableListOf()는 ArrayList를 반환하는데도 성능차이가 나타났다는 것이다. nano 단위 측정이기에 오차라고 볼 수도 있으나 같은 ArrayList지만 성능차이가 나타난 점이 신기했다.</p>
<h2 id="결론">결론</h2>
<p>ArrayList나 MutableList를 스택 대용으로 사용하는 것은 문제가 없다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[PostgreSQL을 Spring Data JPA 로 다루기]]></title>
            <link>https://velog.io/@tkppp-dev/PostgreSQL%EC%9D%84-Spring-Data-JPA-%EB%A1%9C-%EB%8B%A4%EB%A3%A8%EA%B8%B0</link>
            <guid>https://velog.io/@tkppp-dev/PostgreSQL%EC%9D%84-Spring-Data-JPA-%EB%A1%9C-%EB%8B%A4%EB%A3%A8%EA%B8%B0</guid>
            <pubDate>Thu, 14 Apr 2022 17:24:16 GMT</pubDate>
            <description><![CDATA[<h2 id="postgresql의-특이-데이터-타입">PostgreSQL의 특이 데이터 타입</h2>
<p>일반적으로 많이 사용하는 Mysql과 다르게 PostgreSQL은 다양한 데이터 타입을 지원한다. 예를들면 배열 타입, JSON 타입, JSON Binary 타입 등을 지원한다.</p>
<p>하지만 JPA의 구현체인 Hibernate에서 해당 데이터 타입에 대한 컬럼 타입을 기본으로 지원하지 않기 때문에 사용하기 위해서는 컬럼 타입을 정의하여 사용할 컬럼 타입과 매핑해주어야 한다.</p>
<h2 id="하이버네이트-사용자-정의-데이터-타입-매핑">하이버네이트 사용자 정의 데이터 타입 매핑</h2>
<p>컬럼 타입을 정의하려면 UserType 인터페이스를 구현하면 된다. 하지만 이는 매우 귀찮은 일이므로 라이브러리를 사용한다. 직접 UserType을 구현해보고 싶으면 <a href="https://www.baeldung.com/hibernate-custom-types">여기</a>를 참조하자.</p>
<h3 id="의존성">의존성</h3>
<pre><code class="language-kotlin">implementation(&quot;com.vladmihalcea:hibernate-types-52:2.16.0&quot;)</code></pre>
<h3 id="구현">구현</h3>
<pre><code class="language-kotlin">import com.vladmihalcea.hibernate.type.array.ListArrayType
import org.hibernate.annotations.TypeDef
import javax.persistence.*
import org.hibernate.annotations.Type

@Entity
@TypeDef(name=&quot;list-int&quot;, typeClass = ListArrayType::class)
class Summoner(
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    val id: Long? = null,

    @Column(nullable = false)
    val puuid: String,

    @Column(nullable = false)
    var name: String,

    @Column(nullable = true)
    var recentMatchId: String,

    @Type(type = &quot;list-int&quot;)
    @Column(columnDefinition = &quot;integer[]&quot;)
    var analysisResults: List&lt;Int&gt;
)</code></pre>
<p>먼저 <code>@TypeDef</code> 어노테이션으로 컬럼 타입을 정의하고 지원하지 않는 타입의 컬럼을 정의할 때 <code>@Type</code> 어노테이션으로 위에서 정의한 컬럼 타입을 지정하고 <code>@Column</code> 어노테이션으로 데이터베이스에 들어갈 컬럼의 타입을 정의함으로써 하이버네이트가 컬럼 타입으로 지원하지 않는 <code>List&lt;Int&gt;</code> 와 PostgreSQL의 <code>integer[]</code> 을 매핑했다.</p>
<h3 id="테스트">테스트</h3>
<pre><code class="language-kotlin">@SpringBootTest
class SummonerRepositoryTest(
    @Autowired private val summonerRepository: SummonerRepository
) {

    @AfterEach
    fun tearDown(){
        summonerRepository.deleteAll()
    }

    @Test
    @DisplayName(&quot;Create 테스트&quot;)
    fun createTest() {
        // given
        val puuid = &quot;puuid&quot;
        val name = &quot;쳇바퀴 속 다람쥐&quot;
        val recentMatchId = &quot;match_id&quot;
        val analysisResult = listOf(1,4,5,6)
        val summoner = Summoner(
            puuid = puuid, name = name, recentMatchId = recentMatchId, analysisResults = analysisResult
        )

        // when
        val result = summonerRepository.save(summoner)

        // then
        println(result.id)
        assertThat(result.puuid).isEqualTo(puuid)
        assertThat(result.name).isEqualTo(name)
        assertThat(result.recentMatchId).isEqualTo(recentMatchId)
        assertThat(result.analysisResults).isEqualTo(analysisResult)
    }
}</code></pre>
<p>테스트를 실행해보면 새 엔티티의 삽입이 정상적으로 되는 것을 확인할 수 있다.</p>
<blockquote>
<h4 id="참고">참고</h4>
<p><a href="https://vladmihalcea.com/postgresql-array-java-list/">https://vladmihalcea.com/postgresql-array-java-list/</a>
<a href="https://www.baeldung.com/hibernate-custom-types">https://www.baeldung.com/hibernate-custom-types</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] 수신 객체 지정 람다와 let]]></title>
            <link>https://velog.io/@tkppp-dev/Kotlin-%EC%88%98%EC%8B%A0-%EA%B0%9D%EC%B2%B4-%EC%A7%80%EC%A0%95-%EB%9E%8C%EB%8B%A4%EC%99%80-let</link>
            <guid>https://velog.io/@tkppp-dev/Kotlin-%EC%88%98%EC%8B%A0-%EA%B0%9D%EC%B2%B4-%EC%A7%80%EC%A0%95-%EB%9E%8C%EB%8B%A4%EC%99%80-let</guid>
            <pubDate>Mon, 04 Apr 2022 05:50:42 GMT</pubDate>
            <description><![CDATA[<h1 id="수신-객체-지정-람다">수신 객체 지정 람다</h1>
<p>어떤 객체의 이름을 반복하지 않고도 객체에 대해 여러 연산을 수행할 수 있다면 좋을 것이다. 이를 지원하는 도구로 코틀린은 with과 apply를 지원한다.</p>
<pre><code class="language-kotlin">fun alphabet(): String {
    val result = StringBuilder()
    for(letter in &#39;A&#39;..&#39;Z&#39;) {
        result.append(letter)
    }

    result.append(&quot;\nNow I know the alphabet!&quot;)
    return result
}</code></pre>
<h2 id="with">with</h2>
<pre><code class="language-kotlin">fun alphabet(): String =
    with(StringBuilder()) {
        for(letter in &#39;A&#39;..&#39;Z&#39;) {
            this.append(letter)        // this로 수신 객체 명시 가능
        }    
        append(&quot;\nNow I know the alphabet!&quot;)    // this 생략 가능
          toString()
    }</code></pre>
<p>with는 로직을 적용할 수신객체를 지정해 넘겨주고 로직을 수행한다.</p>
<h2 id="apply">apply</h2>
<p>수신객체를 반환할 필요가 있다면 with의 반환값으로 this를 지정해도 되지만 코틀린은 수신객체를 반환하는 apply를 지원한다.</p>
<pre><code class="language-kotlin">fun alphabet(): String =
    StringBuilder().apply {
        for(letter in &#39;A&#39;..&#39;Z&#39;) {
            this.append(letter)        // this로 수신 객체 명시 가능
        }    
        append(&quot;\nNow I know the alphabet!&quot;)    // this 생략 가능
    }.toString()</code></pre>
<p>apply는 <strong>적용하다</strong> 라는 뜻 그대로 수신 객체에 로직을 적용하고 수신 객체를 반환한다.</p>
<blockquote>
<h4 id="유의점">유의점</h4>
<p>코틀린에서 람다식을 밖으로 빼거나 괄호를 생략할 수 있다는 점에서 착각하기 쉬운데
apply와 with은 코틀린에서 제공하는 언어적 기능이 아니라 표준 라이브러리 함수이다.
apply와 with 뒤에 오는 중괄호는 사실 람다식이며 it이 this인 특수한 람다식이다.</p>
<p>실제 형태 : apply(logic = { ... }), with(Object, logic = { ... })</p>
</blockquote>
<h2 id="let">let</h2>
<p>let 함수도 수신 객체 지정 람다의 일종이지만 특수한 쓰임을 가지고 있다.</p>
<pre><code class="language-kotlin">fun sendEmailTo(email: String){
    println(email)
}

fun getEmail() = when((1..2).random()){
    2 -&gt; &quot;test@test.com&quot;
    else -&gt; null
}

fun main(){
    getEmail()?.let {
        sendToEmail(it)
    } ?: println(&quot;email is null!&quot;)
}</code></pre>
<p>어떤 함수를 수행할 때 넘겨지는 인자가 널인지 아닌지 검사할 필요가 있을 수 있다. let 함수는 이것을 간단하게 해준다.</p>
<pre><code class="language-kotlin">// if 분기
val result = getEmail()
if(result != null) {
    sendEmailTo(result)
}
else {
    println(&quot;email is null&quot;)
}

// let 사용
getEmail()?.let {
    sendEmailTo(it)
} ?: println(&quot;email is null!&quot;)</code></pre>
<p>let 함수는 널 검사를 수행하고 넘겨지는 수신 객체가 널인 경우 아무일도 일어나지 않는다. 널이 아닌 경우 람다로 넘겨진 로직을 수행한다.
if 분기를 통해 따로 널 검사를 수행하는 것보다 코드가 간결해지기 때문에 익숙해지면 좋을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] CSR 방식에서response.sendRedirect() 사용시 유의점]]></title>
            <link>https://velog.io/@tkppp-dev/SpringBoot-CSR-%EB%B0%A9%EC%8B%9D%EC%97%90%EC%84%9Cresponse.sendRedirect-%EC%82%AC%EC%9A%A9%EC%8B%9C-%EC%9C%A0%EC%9D%98%EC%A0%90</link>
            <guid>https://velog.io/@tkppp-dev/SpringBoot-CSR-%EB%B0%A9%EC%8B%9D%EC%97%90%EC%84%9Cresponse.sendRedirect-%EC%82%AC%EC%9A%A9%EC%8B%9C-%EC%9C%A0%EC%9D%98%EC%A0%90</guid>
            <pubDate>Thu, 31 Mar 2022 18:25:27 GMT</pubDate>
            <description><![CDATA[<h1 id="문제-발생">문제 발생</h1>
<p>로컬에서 테스트 할 때 전혀 문제없던 로그인 기능이 AWS에 배포하고 나서는 작동하지 않았다. 클라이언트에서는 404도 아닌 아예 응답이 오지않는다는 에러 로그를 확인했고 Postman으로 테스트 시 신기하게 요청 도메인이 127.0.0.1, 로컬 호스트와 연결할 수 없다는 에러가 발생했다. 로컬에서 서버를 실행하고 Postman으로 요청을 보내면 로컬로 요청이 와서 에러가 발생하지 않았다.</p>
<p>Redis가 문제인가 싶어 테스트용 컨트롤러를 만들어서 테스트해봤더니 잘되었다. 로그인 수행은 정상적으로 되는것을 로그를 통해 확인했으니 어딘가 로컬 환경으로 요청을 보내는 부분이 있을 거라 생각하고 스프링 시큐리티의 로그인 과정을 다시 살펴보면서 수상적은 부분을 발견했다. 바로 response.sendRedirect()이다</p>
<h1 id="원인">원인</h1>
<pre><code class="language-kotlin">// 문제의 코드
@Component
class CustomAuthenticationFailureHandler : AuthenticationFailureHandler{
    override fun onAuthenticationFailure(
        request: HttpServletRequest?,
        response: HttpServletResponse?,
        exception: AuthenticationException?
    ) {
        println(exception?.message)
        response?.sendRedirect(&quot;http://localhost:8080/api/login/fail&quot;)
    }
}</code></pre>
<p>로그인을 구현하면서 스프링 시큐리티가 제공하는 로그인 전략을 사용하다보니 필터 레벨에서 요청을 JWT 토큰을 담아 클라이언트에 전달했어야 했는데 서블릿 Req, Res는 잘 알지 않아, 토큰을 반환하는 컨트롤러를 만들고 response.sendRedirect() 를 통해 생성된 토큰을 쿼리에 담아 컨트롤러로 리다이렉트했다. 문제는 sendRedirect 메소드는 클라이언트에 리다이렉트 주소를 전달한 다음 다시 서버에 요청하는 방식으로 동작한다는 것이였다.
<img src="https://images.velog.io/images/tkppp-dev/post/1c33b247-483f-4bec-98ba-8bf1338ef1b9/image.png" alt=""></p>
<p>즉, 바로 서버내 요청 처리 컨트롤러로 전달되는 것이 아니라 클라이언트에서 해당 요청을 받고 다시 전달하기 때문에 localhost는 EC2 서버가 아닌 클라이언트의 도메인을 의미하게 되어 로컬로 요청이 전달된 것이다.</p>
<h1 id="해결">해결</h1>
<p>결국 localhost로 되어 있는 부분을 EC2 서버의 IP 주소로 변경하는 것으로 문제를 해결하였다. 이 문제로 여러가지 많은 방법을 시도해봤으나 결국 어느정도 서블릿에 대한 공부가 좀 더 필요하다고 생각되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] MockK로 단위테스트하기 ]]></title>
            <link>https://velog.io/@tkppp-dev/SpringBoot-MockK%EB%A1%9C-%EB%8B%A8%EC%9C%84%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@tkppp-dev/SpringBoot-MockK%EB%A1%9C-%EB%8B%A8%EC%9C%84%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 24 Mar 2022 04:42:56 GMT</pubDate>
            <description><![CDATA[<h1 id="문제-발생">문제 발생</h1>
<p>지금까지 <code>Kotlin</code> + <code>JUnit5</code> + <code>Mockito</code> 를 사용하여 단위테스트를 작성하였다. <code>Mockito</code> 와 <code>Kotlin</code> 을 같이 쓰기엔 불편한점이 있긴했지만 어쨋든 되긴하니 사용하고 있었다.</p>
<p>하지만 테스트 중에 <code>repository.findByIdOrNull()</code> 을 스텁하려하니 NPE 에러가 발생했다.</p>
<blockquote>
<h4 id="에러-로그">에러 로그</h4>
<p>cannot be returned by findById()
findById() should return Optional</p>
</blockquote>
<p>분명 <code>findByIdOrNull</code> 을 스텁했는데 <code>findById()</code> 는 Optional 객체를 반환해야한다는 에러 로그는 이해하기 힘들다.</p>
<p>구글링을 해보니 <code>Mockito</code> 는 정적 메소드에 대해서는 스텁할 수 없다고 한다. <code>findByIdOrNull</code> 는 코틀린에서 정의된 확장함수 이기 때문에 자바 코드로 디컴파일시 정적 메소드이기 때문에 에러가 발생한 것 같다.</p>
<p>이 문제를 해결하기 위해 findById 메소드를 사용하도록 코드를 바꾸는 것은 주객전도라 생각되어 모킹 프레임워크를 <code>Mockito</code> 에서 코틀린 프로젝트인 <code>MockK</code> 로 변경했다.</p>
<h1 id="사용법">사용법</h1>
<h2 id="extenstion">Extenstion</h2>
<pre><code class="language-kotlin">@ExtendWith(MockKExtension::class)
class TestClass {
    ...
}</code></pre>
<p>MockK를 사용하기 위해서는 <strong>MockKExtension</strong> 확장 기능을 추가해야한다.</p>
<h2 id="mock">Mock</h2>
<p>목 객체를 생성하는 것은 Mockito와 크게 다르지 않다.</p>
<pre><code class="language-kotlin">// Mockito Mock
val mock1 = mock(MockClass::class.java)

// MockK Mock
val mock = mockk&lt;MockClass&gt;()</code></pre>
<h2 id="stub">Stub</h2>
<pre><code class="language-kotlin">// stub
every { mock.method() } returns &quot;OK&quot;
every { mock.methodWithArgs(any()) } returns &quot;OK&quot;

// mockito stub
`when`(mock.method()).thenReturn(&quot;OK&quot;)
`when`(mock.methodWithArgs(anyString())).thenReturn(&quot;OK&quot;)</code></pre>
<p>stubbing 은 <code>every</code> 메소드를 사용한다. 유의할 점은 <code>Mockito</code>는 목 객체의 메소드를 stub 하지 않고 사용하면 null을 반환했지만 <code>MockK</code> 객체는 에러가 발생한다.</p>
<p>이것을 방지하려면 목 객체 생성시 relax 옵션으로 기본값을 설정해 RelaxedMock으로 만들어야 한다.</p>
<pre><code class="language-kotlin">val mock = mockk&lt;MockClass&gt;(relax=true)

mock.method() // true 반환</code></pre>
<h2 id="verify">Verify</h2>
<p>목 객체의 함수 호출 여부 검사는 <code>verify</code> 메소드를 사용한다.</p>
<pre><code class="language-kotlin">public fun verify(
    ordering: Ordering,
    inverse: Boolean,
    atLeast: Int,
    atMost: Int,
    exactly: Int,
    timeout: Long,
    verifyBlock: MockKVerificationScope.() -&gt; Unit
): Unit

// 정확히 한번 실행
verify(exactly = 1) { mock.method(1) }

// 1000ms 내 실행
verify(timeout = 1000L) { mock.method(1) }</code></pre>
<h2 id="mockbean-spybean">@MockBean, @SpyBean</h2>
<p><code>Mockk</code>는 <code>MockBean</code>, <code>SpyBean</code> 과 같은 기능을 제공하지 않는다. 따로 사용하고 싶다면 <code>Ninja-Squad/springmockk</code> 의존성을 추가하고 <code>@MockkBean</code>, <code>@SpykBean</code> 어노테이션을 사용해야 한다. </p>
<blockquote>
<h4 id="참조">참조</h4>
<p><a href="https://javacan.tistory.com/entry/kotlin-mock-framework-mockk-intro">코틀린 mock 프레임워크 MockK 소개</a>
<a href="https://kapentaz.github.io/test/Kotlin%EC%97%90%EC%84%9C-mock-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%98%EA%B8%B0/#">Kotlin에서 mock 테스트 하기</a>
<a href="https://techblog.woowahan.com/5825/">https://techblog.woowahan.com/5825/</a></p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>