<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>1yongs_.log</title>
        <link>https://velog.io/</link>
        <description>BackEnd Developer</description>
        <lastBuildDate>Mon, 20 Sep 2021 09:12:53 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>1yongs_.log</title>
            <url>https://images.velog.io/images/1yongs_/profile/33586768-9f87-441d-bdf4-f558fae7c02f/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 1yongs_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/1yongs_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[NestJS TDD (입고 알림) - 6. Result]]></title>
            <link>https://velog.io/@1yongs_/NestJS-TDD-%EC%9E%85%EA%B3%A0-%EC%95%8C%EB%A6%BC-6.-Result</link>
            <guid>https://velog.io/@1yongs_/NestJS-TDD-%EC%9E%85%EA%B3%A0-%EC%95%8C%EB%A6%BC-6.-Result</guid>
            <pubDate>Mon, 20 Sep 2021 09:12:53 GMT</pubDate>
            <description><![CDATA[<h2 id="what-i-do">What I Do?</h2>
<p>Used Skill Set</p>
<ul>
<li><code>NestJS</code>, <code>Jest</code>, <code>Scheduler</code>, <code>slack-webhook</code>, <code>docker</code></li>
</ul>
<p>Jest 테스트 툴로 NestJS의 &quot;품절난 모니터의 입고 알림&quot; 애플리케이션 구현을 테스트 주도 개발론을 도입하여 개발하였다.</p>
<p>최근 <code>프로그래머스:백엔드 웹 데브코스</code> 교육과정을 들으면서 <strong>OOP</strong>, <strong>TDD</strong> 등 다양한 개념들을 접하면서 직장에서 NestJS를 사용하면서 그냥 넘겨 짚었던 개념들을 사이드 프로젝트를 만들면서 녹여내고 싶었다.</p>
<p>마침 모니터가 필요했는데 가성비가 좋은 V28UE 모니터를 보게 되었고 구입할려고 했는데 매일 재고가 없다고 하여 &quot;재고 입고 알림 애플리케이션을 만들면 좋겠다.&quot;고 생각했다. 
<img src="https://images.velog.io/images/1yongs_/post/3cfed150-8779-4f80-b1a5-61c208b4b632/image.png" alt="">
그리고 여기에 TDD를 곁들인...</p>
<p><img src="https://images.velog.io/images/1yongs_/post/4a140ecc-6cc5-4538-ba41-0e155460e7e1/image.png" alt=""></p>
<h3 id="jest">Jest</h3>
<p><a href="https://github.com/dlfdyd96/be-G-book-real-world-software-development-study">실전 자바 소프트웨어 개발</a> 책을 읽고 OOP 설계 5대 원칙, TDD 등 JAVA 뿐 만 아니라 다른 언어에서도 사용할 수 있는 개념들을 배웠고 이를 NestJS에 녹여보고자 하였다.</p>
<p>책에서 소개된 Given-When-Then 패턴을 적용하여 Test Code 스타일을 구현하였다. </p>
<ul>
<li><p><strong>Given</strong>
테스트에서 구체화하고자 하는 행동을 시작하기 전에 테스트 상태를 설명하는 부분</p>
</li>
<li><p><strong>When</strong>
구체화하고자 하는 그 행동</p>
</li>
<li><p><strong>Then</strong>
어떤 특정한 행동 때문에 발생할거라고 예상되는 변화에 대한 설명</p>
</li>
</ul>
<p>(참고: <a href="https://kchanguk.tistory.com/40">Given-When-Then 패턴</a>)</p>
<p>개발하면서 다양한 모듈을 개발하고 이를 프레임워크에 Dependency Injection 하게된다. 이제 각 모듈들에 대해 유닛 테스트를 해야하는데 여러 모듈들이 의존성에 의해 연결되어 있어서 이를 모킹해야하는데 이 부분이 많이 골칫거리였다.</p>
<p>하지만 <a href="https://darrengwon.tistory.com/998?category=915252">HotHandCoding님의 Nest + Jest unit test</a> 글들을 참고하게 되었고 많은 고민들을 해소할 수 있게 되었다. 모듈을 Mocking을 했는데 모듈에 대한 메서드를 spyOn하는 방법이나, TestModule에서 원래의 모듈을 주입 대신 목 객체 모듈을 주입하는 방법 등 많은 부분을 배웠다. (감사합니다)</p>
<h3 id="scheduler">Scheduler</h3>
<p>이제 요청들을 매 5초 마다 실행하는 Scheduler를 실행해야 하는데, 이를 NestJS의 Cron Job을 통해서 해결 할 수 있었다. </p>
<p>예전에 회사에서 동료분께서 Batch 작업을 위해 Cron Job을 사용하신 것을 보았는데 이번 기회에 사용하는 기회가 와서 적용할 수 있었다.</p>
<p>단순히 스케쥴링 작업 뿐만아니라 Dynamic Scheduler를 통해 스케쥴러를 runtime에 등록하거나 해제할 수 있다는 점도 굉장히 인상적이었다.</p>
<h3 id="docker">Docker</h3>
<p>도커는 책을 정독을 해야 할 것 같다. <del>(그만 구글링 하고 싶다.)</del> 
<img src="https://images.velog.io/images/1yongs_/post/74d55e17-8d00-44cc-9e2a-310c633379f3/image.png" alt=""></p>
<p>MacOS에서 개발한 NestJS Application을 다른 환경 (ubuntu, window) 서버에 올려야 하므로 Docker Image로 빌드해야 하는 작업 필수적이라고 생각하였다.</p>
<p>그래서 DockerFile을 작성하고, Docker 작업(명령어 등)을 더 손 쉽게 쓸 수 있도록 docker-compose 파일을 작성하여 git repository에 같이 포함하여 올렸다.</p>
<p>그래서 개발한 어플리케이션은 나의 노트북에서 돌아가지 않고, 집에 있는 데스크탑(windows)에서 잘 돌아갈 수 있게 되었다.</p>
<h3 id="result">Result</h3>
<p>(예정)</p>
<hr/>

<h2 id="느낀점">느낀점</h2>
<p>개발은 하고싶은 것을 하고 만들고 싶은 것을 개발하는게 정말 매력적이라고 생각한다.</p>
<p>아직 못 한 부분들이 많다. 교육과정의 과제들과 회사 업무들이 거의 정리되는 시점에 다시 적용해 보도록 해야겠다.</p>
<ul>
<li><input disabled="" type="checkbox"> e2e test</li>
<li><input disabled="" type="checkbox"> CI / CD</li>
<li><input disabled="" type="checkbox"> domain 기능 확장 (다른 품절 제품 얻어 오는 기능)</li>
<li><input disabled="" type="checkbox"> 예외 처리</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS TDD (입고 알림) - 4. Scheduler]]></title>
            <link>https://velog.io/@1yongs_/NestJS-TDD-%EC%9E%85%EA%B3%A0-%EC%95%8C%EB%A6%BC-4.-Scheduler</link>
            <guid>https://velog.io/@1yongs_/NestJS-TDD-%EC%9E%85%EA%B3%A0-%EC%95%8C%EB%A6%BC-4.-Scheduler</guid>
            <pubDate>Mon, 20 Sep 2021 08:39:34 GMT</pubDate>
            <description><![CDATA[<p>5초마다 재고 상태를 확인해주는 스케쥴링을 구해봅시다. </p>
<h3 id="4-1-setting-up-task-scheduler">4-1. Setting Up Task Scheduler</h3>
<p>우선 필요한 패키지를 설치 합시다.</p>
<pre><code class="language-sh">&gt; npm install --save @nestjs/schedule
&gt; npm install --save-dev @types/cron</code></pre>
<blockquote>
<p><strong>참고</strong></p>
<p><a href="https://docs.nestjs.com/techniques/task-scheduling">NestJS Docs - Task Scheduling</a></p>
</blockquote>
<h3 id="4-2-scheduler">4-2. Scheduler</h3>
<p>매 5초마다 request를 요청하는 scheduler를 만든다. <del>(넘 심한가...?)</del></p>
<pre><code class="language-ts">@Injectable()
export class TaskSchedulerService {
  private static readonly logger = new Logger(TaskSchedulerService.name);

  constructor(
    private readonly watcherService: WatcherService,
    private readonly schedulerRegistry: SchedulerRegistry,
  ) {}

  @Cron(CronExpression.EVERY_5_SECONDS, {
    name: &#39;V28UE_WATCHING&#39;,
  })
  async handleWatchingStock() {
    const requestUrl =
      &#39;https://www.jooyonshop.co.kr/goods/goods_view.php?goodsNo=1000000165&#39;;
    // TaskSchedulerService.logger.debug(&#39;아이고난!&#39;);
    const resultRequest = await this.watcherService.getHttpRequest(requestUrl);
    const isSoldOut =
      this.watcherService.parseHtmlAndCheckIsSoldOut(resultRequest);

    if (!isSoldOut) {
      const job = this.schedulerRegistry.getCronJob(&#39;V28UE_WATCHING&#39;);
      job.stop();
      TaskSchedulerService.logger.debug(&#39;떳따!&#39;);
      this.watcherService.notify({ text: &#39;Buy It! Hurry Up!&#39; });
    } else {
      TaskSchedulerService.logger.debug(&#39;아직 안떴따&#39;);
    }
  }
}</code></pre>
<blockquote>
<h3 id="참고">참고</h3>
<p><a href="https://docs.nestjs.com/techniques/task-scheduling">NestJS Docs - Scheduler</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS TDD (입고 알림) - 5. Run application on Docker]]></title>
            <link>https://velog.io/@1yongs_/NestJS-TDD-%EC%9E%85%EA%B3%A0-%EC%95%8C%EB%A6%BC-5.-Run-application-on-Docker</link>
            <guid>https://velog.io/@1yongs_/NestJS-TDD-%EC%9E%85%EA%B3%A0-%EC%95%8C%EB%A6%BC-5.-Run-application-on-Docker</guid>
            <pubDate>Mon, 20 Sep 2021 08:39:09 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/1yongs_/post/119f151f-bc2d-4f45-8a59-59deb69062b7/image.png" alt=""></p>
<h3 id="5-1-dockerfile-작성">5-1. Dockerfile 작성</h3>
<p>dockerfile을 작성한다.</p>
<pre><code class="language-dockerfile"># First stage: build and test
FROM node:10-alpine as nodebuild    # Define the base image
WORKDIR /app                        # Define where we put the files
COPY . .                            # Copy all files from local host folder to image
RUN npm install &amp;&amp; \                # Install dependencies
    npm run build &amp;&amp; \              # Build the solution
    npm run test &amp;&amp; \               # Run the tests
    npm run coverage                # Report on coverage

# Second stage: assemble the runtime image
FROM node:10-alpine as noderun      # Define base image
WORKDIR /app                        # Define work directory
COPY --from=nodebuild /app/dist/src/ ./ # Copy binaries resulting from stage build
COPY package*.json ./               # Copy dependency registry
RUN npm install --only=prod         # Install only production dependencies
EXPOSE 8000
ENTRYPOINT node /app/index.js       # Define how to start the app.</code></pre>
<h3 id="5-2-image-build">5-2. image build</h3>
<p>test를 돌릴 image와 runtime 이미지를 따로 build한다.</p>
<pre><code class="language-sh">&gt; docker build . -t noti
&gt; docker build --target nodebuild . -t noti-test:latest</code></pre>
<h3 id="5-3-run-on-container">5-3. run on container</h3>
<p>그리고 test Image를 container로 실행하고, 결과 파일들을 받아온다.</p>
<pre><code class="language-sh">&gt; docker run --name noti-test noti-test
&gt; docker cp noti-test:/app/coverage ./results</code></pre>
<p>그리고 runtime image를 container로 실행한다.</p>
<pre><code class="language-sh">&gt; docker run -d --name noti-run noti</code></pre>
<blockquote>
<h3 id="참고">참고</h3>
<p><a href="https://www.feval.ca/posts/multistage-docker/">Using Docker Multi-Stage Builds To Build And Test Applications</a></p>
</blockquote>
<h3 id="5-4-이러한-도커-수행-과정을-docker-compose로-구성해보자">5-4. 이러한 도커 수행 과정을 docker-compose로 구성해보자.</h3>
<pre><code class="language-yaml">version: &#39;3.8&#39;

services:
  test:
    container_name: noti-test
    image: noti-test:latest
    build:
      context: .
      target: nodebuild
      dockerfile: ./Dockerfile
  run:
    container_name: noti
    image: noti:latest
    build:
      context: .
      dockerfile: ./Dockerfile
    ports:
      - 3000:3000
    networks:
      - noti-network
    restart: unless-stopped

networks:
  noti-network:</code></pre>
<p>실행은 background에서 하도록 하자.</p>
<pre><code class="language-sh">&gt; docker-compose up -d test
&gt; docker-compose up -d run</code></pre>
<hr/>

<h3 id="궁금한-점">궁금한 점</h3>
<p>❓ 아니 그런데 test를 하고 test result를 복사하려는데 어떻게 자동화 하지..?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS TDD (입고 알림) - 3. Slack Webhook 연동
]]></title>
            <link>https://velog.io/@1yongs_/NestJS-TDD-%EC%9E%85%EA%B3%A0-%EC%95%8C%EB%A6%BC-3.-Slack-Webhook-%EC%97%B0%EB%8F%99</link>
            <guid>https://velog.io/@1yongs_/NestJS-TDD-%EC%9E%85%EA%B3%A0-%EC%95%8C%EB%A6%BC-3.-Slack-Webhook-%EC%97%B0%EB%8F%99</guid>
            <pubDate>Mon, 20 Sep 2021 08:31:00 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/1yongs_/post/c7ad9fb6-a254-4520-9c83-ff5f72eb5d32/image.png" alt=""></p>
<p>재고가 들어 왔다고 알림을 줘야하는데 slack을 사용하여 채널에 등록된 사용자들에게 알려줍시다.</p>
<blockquote>
<h3 id="참고">참고</h3>
<p><a href="https://velog.io/@1yongs_/NestJS-sentry-slack-error%EB%A1%9C%EA%B7%B8-%EC%88%98%EC%A7%91">NestJS sentry + slack error로그 수집</a></p>
</blockquote>
<h3 id="3-1-setting-up">3-1. Setting Up</h3>
<pre><code class="language-sh">&gt; npm i nestjs-slack-webhook
&gt; npm i @slack/client @nestjs/config</code></pre>
<ul>
<li><p>SlackConfig 등록 (webhook)</p>
<ul>
<li><code>.env</code> file
  우선 환경 변수 파일을 만들어 발급받은 Slack wehook URL을 저장합니다.<pre><code class="language-env">SLACK_WEBHOOK_URL=https://hooks.slack.com/services/{비밀입니다!}</code></pre>
</li>
<li><code>src/config/slack.config.ts</code> file
  NestJS에 주입할 Slack 전용 Config를 만들어 줍시다.</li>
</ul>
</li>
</ul>
<pre><code class="language-ts">    import { registerAs } from &#39;@nestjs/config&#39;;
    import { SlackOptions } from &#39;nestjs-slack-webhook&#39;;

    export default registerAs(
      &#39;slack&#39;,
      (): SlackOptions =&gt; ({
        url: process.env.SLACK_WEBHOOK_URL,
      }),
    );</code></pre>
<ul>
<li><p>SlackModule Global로 등록</p>
<ul>
<li><code>src/app.module.ts</code> file
  환경변수를 등록하고, 설치한 SlackModule을 AppModule에 주입합니다. </li>
</ul>
</li>
</ul>
<pre><code class="language-ts">    // ...
    import { ConfigModule, ConfigService } from &#39;@nestjs/config&#39;;
    import slackConfig from &#39;./config/slack.config&#39;;
    import { WatcherModule } from &#39;./watcher/watcher.module&#39;;

    @Module({
      imports: [
        ConfigModule.forRoot({
          load: [slackConfig],
        }),
        SlackModule.forRootAsync({
          imports: [ConfigModule],
          inject: [ConfigService],
          useFactory: (config) =&gt; config.get(&#39;slack&#39;),
        }),
        // ...
      ],
      // ...
    })
    export class AppModule {}</code></pre>
<ul>
<li>NotifyService
인자로 받은 메세지를 slack 채널에 발송하는 서비스를 만들어 줍니다.</li>
</ul>
<pre><code class="language-ts">  import { Injectable } from &#39;@nestjs/common&#39;;
  import { IncomingWebhook, IncomingWebhookSendArguments } from &#39;@slack/client&#39;;
  import { InjectSlack } from &#39;nestjs-slack-webhook&#39;;

  @Injectable()
  export class NotifyService {
    constructor(
      @InjectSlack()
      private readonly slack: IncomingWebhook,
    ) {}

    async notify(args: IncomingWebhookSendArguments) {
      return await this.slack.send(args);
    }
  }</code></pre>
<h3 id="3-2-watcherservice-test-code-작성">3-2. WatcherService Test Code 작성</h3>
<p><code>WatcherService</code>에 <code>NotifyService</code>를 사용할 겁니다.</p>
<p>우선 <code>NotifyService</code> <strong>Mock 객체</strong>를 만들어 주자</p>
<pre><code class="language-ts">const mockNotifyService = {
  notify: jest.fn(),
};</code></pre>
<p>그리고 <code>NotifyService</code>를 TestModule에 주입합시다.</p>
<pre><code class="language-ts">
describe(&#39;WatcherService&#39;, () =&gt; {
  // ...
  let notifyService: NotifyService;

  beforeEach(async () =&gt; {
    const module: TestingModule = await Test.createTestingModule({
      // ...
      providers: [
        // ...
        { provide: NotifyService, useValue: mockNotifyService }, // &lt;- 주입
      ],
    }).compile();</code></pre>
<p>test 코드를 작성할때 <code>NotifyService</code>를 jest의 <code>spyOn</code> 의 기능으로 메서드 구라를 칩시다.</p>
<p>또한 <code>toHaveBeenCalledTimes</code>, <code>toHaveBeenCalledWith</code> 메서드로 해당 method가 수행 되었는지 체크합시다.</p>
<pre><code class="language-ts">describe(&#39;notify()&#39;, () =&gt; {
  it(&#39;should notify to slack&#39;, async () =&gt; {
    // given
    const requestNotify: IncomingWebhookSendArguments = {
      text: &#39;(test-code) Buy It! Hurry Up!&#39;,
    };
    const resultNotify: IncomingWebhookResult = {
      text: &#39;ok&#39;,
    };
    jest
      .spyOn(notifyService, &#39;notify&#39;)
      .mockImplementation(
        async (arg: IncomingWebhookSendArguments) =&gt; resultNotify,
      );

    // when
    const result = await watcherService.notify(requestNotify);

    // then
    // expect(notifyService.notify).toHaveBeenCalledTimes(1);
    expect(result).toEqual(resultNotify);

    expect(notifyService.notify).toHaveBeenCalledTimes(1);
    expect(notifyService.notify).toHaveBeenCalledWith(requestNotify);
  });
});</code></pre>
<h3 id="3-3-watcherservice-notify-method-작성">3-3. WatcherService notify method 작성</h3>
<p>테스트코드를 작동 할 수 있도록 소스 코드를 구현합니다.</p>
<pre><code class="language-ts">@Injectable()
export class WatcherService {
  constructor(
    // ...
    private readonly notifyService: NotifyService,
  ) {}

  // ...

  async notify(requestNotifyToSlack: IncomingWebhookSendArguments) {
    return await this.notifyService.notify(requestNotifyToSlack);
  }
}</code></pre>
<p>그럼 결과로 test를 통과하게 할 수 있게 되었습니다.</p>
<pre><code class="language-shell"> PASS  src/crawler/watcher.service.spec.ts
  WatcherService
    ✓ should be defined (9 ms)
    getHTTPRequest()
      ✓ should request http given url (4 ms)
    parseHtmlAndCheckIsSoldOut()
      ✓ should parse Html And Check Is SoldOut (8 ms)
      ✓ should parse Html And Check Is SoldOut is false (4 ms)
    notify()
      ✓ should notify to slack (2 ms) # &lt;====== 테스트 통과</code></pre>
<hr/>

<h3 id="참고-1">참고</h3>
<p>NotifyService에 대한 테스트코드도 미리 작성해야합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS TDD (입고 알림) - 2. 재고 상태 얻기]]></title>
            <link>https://velog.io/@1yongs_/NestJS-TDD-%EC%9E%85%EA%B3%A0-%EC%95%8C%EB%A6%BC-1.-%EC%9E%AC%EA%B3%A0-%EC%83%81%ED%83%9C-%EC%96%BB%EA%B8%B0</link>
            <guid>https://velog.io/@1yongs_/NestJS-TDD-%EC%9E%85%EA%B3%A0-%EC%95%8C%EB%A6%BC-1.-%EC%9E%AC%EA%B3%A0-%EC%83%81%ED%83%9C-%EC%96%BB%EA%B8%B0</guid>
            <pubDate>Mon, 20 Sep 2021 08:17:37 GMT</pubDate>
            <description><![CDATA[<p>재고상태를 요청하는 watcher service를 만들어 봅시다.</p>
<h3 id="2-1-watcher-service를-만들어-줍니다">2-1. watcher service를 만들어 줍니다.</h3>
<pre><code>&gt; nest generate module watcher
&gt; nest gnereate service watcher</code></pre><p>그러면 <code>watcher.service.spec.ts</code> 파일이 만들어지는데 이 파일이 <code>watcher.service.ts</code> 에 대한 테스트를 담당합니다.</p>
<h3 id="2-2-우선-시나리오를-통해-watcherservice-에서-어떤-것을-해야할-지-파악해보죠">2-2. 우선 시나리오를 통해 <code>WatcherService</code> 에서 어떤 것을 해야할 지 파악해보죠.</h3>
<ol>
<li><p>Get URL HTTP Request</p>
</li>
<li><p>Parse HTML And Check Sold Out</p>
</li>
<li><p>Notify Slack Notifier</p>
</li>
</ol>
<h3 id="2-3-그리고-test-코드를-먼저-작성합시다-tdd">2-3. 그리고 Test 코드를 먼저 작성합시다. (TDD)</h3>
<pre><code class="language-ts">describe(&#39;getHTTPRequest()&#39;, () =&gt; {
  it.todo(&#39;should request http given url&#39;);
  it.todo(&#39;should throw exception cant request http&#39;); // TODO:
});

describe(&#39;parseHtmlAndCheckIsSoldOut()&#39;, () =&gt; {
  it.todo(&#39;should parse Html And Check Is SoldOut&#39;);
  it.todo(&#39;should parse Html And Check Is SoldOut is false&#39;);
});

describe(&#39;notify()&#39;, () =&gt; {
  it.todo(&#39;should notify to SlackNotifer&#39;);
  it.todo(&#39;should throw exception cant notify slacknotifier&#39;); // TODO:
});</code></pre>
<p>하나 하나씩 Test들을 만들어 나갑시다.</p>
<h3 id="2-4-get-url-http-request">2-4. Get URL HTTP Request</h3>
<p>구현하기 전에 테스트 코드를 먼저 작성합시다.</p>
<pre><code class="language-ts">it(&#39;should request http given url&#39;, async () =&gt; {
  // given
  const givenUrl = &#39;https://www.naver.com&#39;;

  // when
  const result = await watcherService.getHttpRequest(givenUrl);

  // then
  expect(result).not.toBeNull();
});</code></pre>
<p>이렇게 작성하고 돌리시면 당연히 에러가 나겠죠. 당연합니다.</p>
<h3 id="2-5-이제-이-코드를-잘-돌아가게끔-수정합시다">2-5. 이제 이 코드를 잘 돌아가게끔 수정합시다.</h3>
<p>(요즘 axios는 rxjs를 써서 하는게 대세인가봅니다... 배울게 너무 많아 🥲)</p>
<pre><code class="language-ts">// carwler.service.ts
async getHttpRequest(givenUrl: string) {
  const result = await firstValueFrom(
    this.httpService.get(givenUrl).pipe(map((response) =&gt; response.data)),
  );

  WatcherService.logger.log(result);
  return result;
}</code></pre>
<h3 id="2-6-결과">2-6. 결과</h3>
<p>그럼 결과 메세지로 이렇게 뜨고 <code>passed</code> 되었다고 합니다.</p>
<p><img src="https://images.velog.io/images/1yongs_/post/4b94cffd-4c4a-4101-bc22-57579d9033ee/result.png" alt=""></p>
<p>그럼 나머지 테스트 코드도 작성해봅시다. (<a href="./src/watcher/watcher.service.spec.ts">링크</a>)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS TDD (입고 알림) - 1. Intro ]]></title>
            <link>https://velog.io/@1yongs_/NestJS-TDD-notificator</link>
            <guid>https://velog.io/@1yongs_/NestJS-TDD-notificator</guid>
            <pubDate>Mon, 20 Sep 2021 08:11:15 GMT</pubDate>
            <description><![CDATA[<p>최근 Java Spring 공부하다가 TDD, DI 등 NestJS를 하면서 얼렁뚱땅 넘어간 개념들을 다시 톺아보는 기회를 가졌었다.
최근에 테크 유튜브 잇섭님이 주연테크 <a href="https://www.youtube.com/watch?v=1uzMtHt1QBI&amp;t=11s">V28UE 모니터 제품을 리뷰</a>하면서 해당 제품이 한달 째 재고가 없다... 🥲</p>
<p>그래서 이번 기회에 NestJS에 Jest 테스트 도구로 테스트 주도 개발론을 직접 적용 해봄으로써 TDD에 한 걸음 더 다 가보자.</p>
<h2 id="1-overview">1. Overview</h2>
<h3 id="1-1-architecture">1-1. Architecture</h3>
<p><img src="https://images.velog.io/images/1yongs_/post/9436dad9-4c99-48d0-adc9-3d5913f83fa0/overview.png" alt=""></p>
<h3 id="1-2-flowchart">1-2. FlowChart</h3>
<p><img src="https://images.velog.io/images/1yongs_/post/347a3de4-67bc-45e6-8852-d72980f49ebb/flowchart.png" alt=""></p>
<ul>
<li><input checked="" disabled="" type="checkbox"> CrawlingService</li>
<li><input checked="" disabled="" type="checkbox"> Scheduler</li>
<li><input checked="" disabled="" type="checkbox"> Slack Notifier</li>
<li><input checked="" disabled="" type="checkbox"> Run on Docker Container</li>
</ul>
<h3 id="1-3-setting-up-package">1-3. Setting Up Package</h3>
<p>우선 필요한 패키지들을 설치해줍니다.</p>
<blockquote>
<p><a href="https://velog.io/@1yongs_/NestJS-Testing-Jest">NestJS Testing</a></p>
</blockquote>
<pre><code class="language-sh">&gt; npm i --save-dev @nestjs/testing
&gt; npm i --save @nestjs/axios</code></pre>
<p>그리고 package.json에서 <strong>jest setting</strong> 에 <code>verbose: true</code> 를 줘야지 테스트 돌리고나서 좀 자세하게 나온다. <del>(어쩐지 console.log도 안보이더라...)</del> 😒 
그리고 jest에서 rootDir를 src로 쓸려면 <code>moduleNameMapper</code>를 jest config에서 설정해줘야한다.</p>
<pre><code class="language-json">{
  // ...

  &quot;jest&quot;: {
    // ...

    &quot;verbose&quot;: true, // test 할 때 자세히 보기 설정
    &quot;moduleNameMapper&quot;: {
      // rootDir를 src로 설정.
      &quot;^src/(.*)$&quot;: &quot;&lt;rootDir&gt;/$1&quot;
    }
  }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS Testing (Jest)]]></title>
            <link>https://velog.io/@1yongs_/NestJS-Testing-Jest</link>
            <guid>https://velog.io/@1yongs_/NestJS-Testing-Jest</guid>
            <pubDate>Mon, 05 Apr 2021 16:47:25 GMT</pubDate>
            <description><![CDATA[<h1 id="nestjs-testing">NestJS Testing</h1>
<h2 id="index">Index</h2>
<ul>
<li>Testing</li>
<li>Set up<ul>
<li>Create New Project</li>
<li>Generate User Resource</li>
<li>installation</li>
<li>Setting up Project</li>
</ul>
</li>
<li>Unit Testing<ul>
<li>install</li>
<li>Setting Up Tests</li>
<li>Unit Test</li>
</ul>
</li>
<li>Result<ul>
<li>Test Coverage</li>
</ul>
</li>
<li>End-to-End(e2e) Testing<ul>
<li>Setting Up</li>
</ul>
</li>
</ul>
<h2 id="test-code">Test Code?</h2>
<p>최근 급하게 만들어야할 프로젝트가 생기면서 테스트 코드를 만들지 않고 진행하였습니다. 급하게 마무리가 되고 여러 이슈들이 발생하여 코드를 고치는 순간 다른부분에서 에러가 발생합니다. 이럴 때 정말 난처합니다. 😭</p>
<p>Application이 점점 커져갈수록, 수정사항도 많아집니다. 하지만 수정으로이한 부작용(Side-effect)가 발생하죠. <strong>만약</strong> 귀찮더라도 Test Code를 작성했더라면..? 에러가 어디에서 발생하는지 쉽게 Catch할 수 있을 것 이고, 디버깅 편리 및 유지보수가 편리해지는 등 코드에 대해 <code>유연한</code> 대처를 할 수 있습니다.</p>
<p><strong>Test Code</strong>에 대한 자세한 내용은 <a href="https://ssowonny.medium.com/%EC%84%A4%EB%A7%88-%EC%95%84%EC%A7%81%EB%8F%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%9E%91%EC%84%B1-%EC%95%88-%ED%95%98%EC%8B%9C%EB%82%98%EC%9A%94-b54ec61ef91a">설마 아직도 테스트 코드를 작성 안 하시나요?</a> 글에서 참고하시면 좋을 것 같습니다.</p>
<p>이 프로젝트에서는 <strong>실제 Database</strong>와 연결하여 <em>게시글 CRUD</em> 작업을 해보는 <code>Unit Test</code>와 <code>End-to-end</code> Test를 진행해볼 예정입니다. </p>
<hr>

<h2 id="set-up">Set up</h2>
<h3 id="create-new-project">Create New Project</h3>
<pre><code>nest new nestjs-test</code></pre><h3 id="generate-user-resource">Generate User Resource</h3>
<pre><code>nest generate resource post 
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes</code></pre><h3 id="installation">installation</h3>
<pre><code>npm i @nestjs/config @nestjs/mapped-types @nestjs/typeorm typeorm mysql2 joi
npm i class-validator class-transformer</code></pre><h3 id="setting-up-project">Setting Up Project</h3>
<h4 id="maints">Main.ts</h4>
<pre><code class="language-ts">async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  await app.listen(3000);
}
bootstrap();</code></pre>
<h4 id="app-module">App Module</h4>
<pre><code class="language-ts">@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: &#39;.env&#39;,
      isGlobal: true,
      validationSchema: Joi.object({
        NODE_PORT: Joi.string().required(),
        MYSQL_HOST: Joi.string().required(),
        MYSQL_PORT: Joi.string().required(),
        MYSQL_USERNAME: Joi.string().required(),
        MYSQL_PASSWORD: Joi.string().required(),
        MYSQL_DATABASE: Joi.string().required(),
      }),
    }),
    TypeOrmModule.forRoot({
      type: &#39;mysql&#39;,
      host: process.env.MYSQL_HOST,
      port: +process.env.MYSQL_PORT,
      username: process.env.MYSQL_USERNAME,
      password: process.env.MYSQL_PASSWORD,
      database: process.env.MYSQL_DATABASE,
      synchronize: true,
      logging: true,
      entities: [Posts],
      charset: &#39;utf8mb4_unicode_ci&#39;,
      timezone: &#39;+09:00&#39;,
    }),
    PostModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}</code></pre>
<h4 id="post-entity">Post Entity</h4>
<pre><code class="language-ts">@Entity({ name: &#39;post&#39; })
export class Post {
  @PrimaryGeneratedColumn(&#39;uuid&#39;)
  id: string;

  @Column()
  title: string;

  @Column()
  contents: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  @DeleteDateColumn()
  deletedAt: Date;

  @VersionColumn()
  version: number;
}</code></pre>
<h4 id="post-module">Post Module</h4>
<pre><code class="language-ts">@Module({
  imports: [TypeOrmModule.forFeature([Posts])],
  controllers: [PostController],
  providers: [PostService],
})
export class PostModule {}</code></pre>
<h4 id="post-controller">Post Controller</h4>
<pre><code class="language-ts">@Controller(&#39;post&#39;)
export class PostController {
  constructor(private readonly postService: PostService) {}

  @Post()
  create(@Body() createPostDto: CreatePostDto) {
    return this.postService.create(createPostDto);
  }

  @Get()
  findAll() {
    return this.postService.findAll();
  }

  @Get(&#39;:id&#39;)
  findOne(@Param(&#39;id&#39;) id: string) {
    return this.postService.findOne(id);
  }

  @Put(&#39;:id&#39;)
  update(@Param(&#39;id&#39;) id: string, @Body() updatePostDto: UpdatePostDto) {
    return this.postService.update(id, updatePostDto);
  }

  @Delete(&#39;:id&#39;)
  remove(@Param(&#39;id&#39;) id: string) {
    return this.postService.remove(id);
  }
}</code></pre>
<h4 id="post-service">Post Service</h4>
<pre><code class="language-ts">@Injectable()
export class PostService {
  private static readonly logger = new Logger(PostService.name);

  constructor(
    @InjectRepository(Posts)
    private readonly postRepository: Repository&lt;Posts&gt;,
  ) {}

  async create(createPostDto: CreatePostDto): Promise&lt;Posts&gt; {
    try {
      const result = await this.postRepository.save(createPostDto);
      PostService.logger.debug(result);
      return result;
    } catch (error) {
      PostService.logger.debug(error);
      throw error;
    }
  }

  async findAll(): Promise&lt;Posts[]&gt; {
    try {
      const posts = await this.postRepository.find();
      PostService.logger.debug(posts);
      return posts;
    } catch (error) {
      PostService.logger.debug(error);
      throw error;
    }
  }

  async findOne(id: string): Promise&lt;Posts&gt; {
    try {
      const post = await this.postRepository.findOne({
        id,
      });
      PostService.logger.debug(post);
      return post;
    } catch (error) {
      PostService.logger.debug(error);
      throw error;
    }
  }

  async update(id: string, updatePostDto: UpdatePostDto) {
    try {
      const post = await this.postRepository.findOne({
        id,
      });
      if (!post) {
        throw new EntityNotFoundError(Posts, id);
      }
      PostService.logger.debug(post);
      const result = await this.postRepository.save({
        ...post,
        ...updatePostDto,
      });
      return result;
    } catch (error) {
      PostService.logger.debug(error);
      throw error;
    }
  }

  async remove(id: string) {
    try {
      const post = await this.postRepository.findOne({
        id,
      });
      if (!post) {
        throw new EntityNotFoundError(Posts, id);
      }
      PostService.logger.debug(post);
      const result = await this.postRepository.softDelete({
        id,
      });
      return result;
    } catch (error) {
      PostService.logger.debug(error);
      throw error;
    }
  }
}</code></pre>
<hr>

<h2 id="unit-testing">Unit Testing</h2>
<h3 id="install">install</h3>
<p>Install <code>Jest</code> Testing Tool package.</p>
<pre><code class="language-sh">npm i --save-dev @nestjs/testing

rm ./src/post/post.controller.spec.ts    // controller test는 나중에 진행하기 위함합니다.</code></pre>
<h3 id="setting-up-tests">Setting Up Tests</h3>
<p>NestJS의 Testing Tool은 <code>Jest</code> 입니다. 기본적으로 제공하고 있기 때문에 <code>npm run test</code>를 하면 Nest가 <code>.spec</code> 등 test 파일들을 자동으로 검사하여 Test를 진행합니다.</p>
<h4 id="error">Error!</h4>
<p>하지만 <code>npm run test</code>하면 Error가 나옵니다.</p>
<pre><code class="language-sh">Cannot find module &#39;src/jwt/jwt.service&#39; from &#39;users/users.service.ts&#39;</code></pre>
<p>Testing Tool(Jest)이 src 경로를 찾지 못하는 경우입니다. 우리는 TypeScript를 사용하고 있기 때문에 <code>../../</code>이런 식으로 쓸 필요가 없습니다. 하지만 <code>Jest</code>는 그렇지 못합니다 👶.</p>
<h4 id="solve">Solve</h4>
<p><code>packge.json</code>에서 Jest가 파일을 찾는 방식을 수정합니다.</p>
<pre><code class="language-json">{
  // ...
  &quot;jest&quot;: {
    // ...
    &quot;moduleNameMapper&quot;: {
      &quot;^src/(.*)$&quot;: &quot;&lt;rootDir&gt;/$1&quot;
    }
  }
}</code></pre>
<h4 id="error또-발생">Error또 발생!</h4>
<pre><code class="language-sh">$ npm run test

FAIL  src/post/post.service.spec.ts (7.18 s)
● PostService › should be defined

  Nest can&#39;t resolve dependencies of the PostService (?). Please make sure that the argument PostsRepository at index [0] is available in the RootTestModule context.

  Potential solutions:
  - If PostsRepository is a provider, is it part of the current RootTestModule?
  - If PostsRepository is exported from a separate @Module, is that module imported within RootTestModule?
    @Module({
      imports: [ /* the Module containing PostsRepository */ ]
    })</code></pre>
<p>이게 무슨말이면, PostService는 repository가 필요한데, test Module에서 repository를 제공하지 않아 생기는 문제입니다.</p>
<p>그렇다고 TypeORM Module의 Repository를 제공하지 않습니다. 저희는 <strong>Mock Repository</strong>를 제공할 것 입니다.</p>
<p>Mock에 대해서 모르신다면 밑에 참고 게시글을 꼭 참고하세요.</p>
<blockquote>
<p><strong>Mock 🔍</strong></p>
<p>참고사이트: <a href="http://www.incodom.kr/Mock">Mock이란? - 人 CoDOM</a></p>
</blockquote>
<h4 id="solve-1">Solve</h4>
<ul>
<li>post.service.spec.ts</li>
</ul>
<pre><code class="language-ts">import { Test, TestingModule } from &#39;@nestjs/testing&#39;;
import { getRepositoryToken } from &#39;@nestjs/typeorm&#39;;
import { Repository } from &#39;typeorm&#39;;
import { Posts } from &#39;./entities/post.entity&#39;;
import { PostService } from &#39;./post.service&#39;;

const mockPostRepository = () =&gt; ({
  save: jest.fn(),
  find: jest.fn(),
  findOne: jest.fn(),
  softDelete: jest.fn(),
});

type MockRepository&lt;T = any&gt; = Partial&lt;Record&lt;keyof Repository&lt;T&gt;, jest.Mock&gt;&gt;;

describe(&#39;PostService&#39;, () =&gt; {
  let service: PostService;
  let postRepository: MockRepository&lt;Posts&gt;;

  beforeEach(async () =&gt; {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        PostService,
        {
          provide: getRepositoryToken(Posts),
          useValue: mockPostRepository(),
        },
      ],
    }).compile();

    service = module.get&lt;PostService&gt;(PostService);
    postRepository = module.get&lt;MockRepository&lt;Posts&gt;&gt;(
      getRepositoryToken(Posts),
    );
  });

  it(&#39;should be defined&#39;, () =&gt; {
    expect(service).toBeDefined();
  });
});</code></pre>
<blockquote>
<p><strong>MockRepository 🔍</strong></p>
<pre><code class="language-ts">type MockRepository&lt;T = any&gt; = Partial&lt;
  Record&lt;keyof Repository&lt;T&gt;, jest.Mock&gt;
&gt;;</code></pre>
<p><strong>Repository</strong>를 Mocking 하기위해 Repository Type을 정의한 것</p>
<ol>
<li><code>Partial</code> : 타입 T의 모든 요소를 optional하게 한다.</li>
<li><code>Record</code> : 타입 T의 모든 K의 집합으로 타입을 만들어준다.</li>
<li><code>keyof Repository&lt;T&gt;</code> : Repository의 모든 method key를 불러온다.</li>
<li><code>jest.Mock</code> : 3번의 key들을 다 가짜로 만들어준다.</li>
<li><code>type MockRepository&lt;T = any&gt;</code> : 이를 type으로 정의해준다.</li>
</ol>
</blockquote>
<ul>
<li>Result</li>
</ul>
<pre><code class="language-sh">PASS  src/post/post.service.spec.ts
PostService
  ✓ should be defined (12 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.729 s, estimated 5 s
Ran all test suites related to changed files.

Watch Usage: Press w to show more.
</code></pre>
<h3 id="unit-test">Unit Test</h3>
<p>Unit Test는 코드의 <strong>각 줄</strong>에 문제가 있나 없나를 검사합니다. (이 함수가 제대로 동작하냐 안하냐는 e2e에 가깝습니다.)</p>
<p>Unit Test는 우리가 의도한대로</p>
<ol>
<li>잘 작동되는지 테스트를 확인하고,</li>
<li>원하는 출력물이 나오며,</li>
<li>고립된 결과를</li>
</ol>
<p>원합니다.</p>
<h4 id="test-create-method">Test <code>create()</code> method</h4>
<ul>
<li>post.service.spec.ts</li>
</ul>
<pre><code class="language-ts">// ...

describe(&#39;PostService&#39;, () =&gt; {
  // ...
  describe(&#39;create()&#39;, () =&gt; {
    it.todo(&#39;should fail on exception&#39;);
    it.todo(&#39;should create Posts&#39;);
  });
});</code></pre>
<p>여기서 <code>describe(&#39;create()&#39;, () =&gt; {...})</code> 는 테스트할 <code>create()</code> method의 큰 범주라고 생각하시면 됩니다.</p>
<p><code>it.todo(...)</code>는 모든 경우의 수에 대해 test를 하는 것이고 <code>todo()</code> 는 test를 나중에 만들거라고 jest에게 알려줍니다.</p>
<ul>
<li>result</li>
</ul>
<pre><code>PASS  src/post/post.service.spec.ts (5.055 s)
  PostService
    ✓ should be defined (14 ms)
    create()
      ✎ todo should fail on exception
      ✎ todo should create Posts

Test Suites: 1 passed, 1 total
Tests:       2 todo, 1 passed, 3 total
Snapshots:   0 total
Time:        5.315 s
Ran all test suites related to changed files.

Watch Usage: Press w to show more.</code></pre><ul>
<li>post.service.spec.ts</li>
</ul>
<pre><code class="language-ts">describe(&#39;PostService&#39;, () =&gt; {
  describe(&#39;create()&#39;, () =&gt; {
    const createArgs = {
      title: &#39;제목&#39;,
      contents: &#39;글&#39;,
    };
    it(&#39;should fail on exception&#39;, async () =&gt; {
      // postRepository.save() error 발생
      postRepository.save.mockRejectedValue(&#39;save error&#39;); // 실패할꺼라고 가정한다.
      const result = await service.create(createArgs);
      expect(result).toEqual(&#39;save error&#39;); // 진짜 에러 발생했넴
    });

    it(&#39;should create Posts&#39;, async () =&gt; {
      postRepository.save.mockResolvedValue(createArgs); // 성공할꺼라고 가정한다.
      const result = await service.create(createArgs); //

      expect(postRepository.save).toHaveBeenCalledTimes(1); // save가 1번 불러졌니?
      expect(postRepository.save).toHaveBeenCalledWith(createArgs); // 매개변수로 createArgs가 주어졌니?

      expect(result).toEqual(createArgs); // 이 create() method의 결과가 `createArgs`와 똑같니?
    });
  });
});</code></pre>
<ul>
<li>result</li>
</ul>
<pre><code>PASS  src/post/post.service.spec.ts
  PostService
    ✓ should be defined (11 ms)
    create()
      ✓ should fail on exception (19 ms)
      ✓ should create Posts (9 ms)

[Nest] 38755   - 2021. 03. 05. 오후 6:16:19   [PostService] Object:
{
  &quot;title&quot;: &quot;제목&quot;,
  &quot;contents&quot;: &quot;글&quot;
}

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        4.761 s</code></pre><h4 id="test-findall-method">Test <code>findAll()</code> method</h4>
<pre><code class="language-ts">describe(&#39;findAll()&#39;, () =&gt; {
  it(&#39;should be find All&#39;, async () =&gt; {
    postRepository.find.mockResolvedValue([]);

    const result = await service.findAll();

    expect(postRepository.find).toHaveBeenCalledTimes(1);

    expect(result).toEqual([]);
  });
  it(&#39;should fail on exception&#39;, async () =&gt; {
    postRepository.find.mockRejectedValue(&#39;find error&#39;);
    const result = await service.findAll();
    expect(result).toEqual(&#39;find error&#39;);
  });
});
</code></pre>
<h4 id="test-findone-method">Test <code>findOne()</code> method</h4>
<pre><code class="language-ts">describe(&#39;findOne()&#39;, () =&gt; {
  const findOneArgs = { id: &#39;1&#39; };

  it(&#39;should be findOne&#39;, async () =&gt; {
    const mockedPost = {
      id: &#39;1&#39;,
      title: &#39;음&#39;,
      description: &#39;힘드노&#39;,
    };
    postRepository.findOne.mockResolvedValue(mockedPost);

    const result = await service.findOne(findOneArgs.id);

    expect(postRepository.findOne).toHaveBeenCalledTimes(1);
    expect(postRepository.findOne).toHaveBeenCalledWith(findOneArgs);

    expect(result).toEqual(mockedPost);
  });
  it(&#39;should fail if no post is found&#39;, async () =&gt; {
    postRepository.findOne.mockResolvedValue(null);

    const result = await service.findOne(findOneArgs.id);

    expect(postRepository.findOne).toHaveBeenCalledTimes(1);
    expect(postRepository.findOne).toHaveBeenCalledWith(findOneArgs);

    expect(result).toEqual(new EntityNotFoundError(Posts, findOneArgs.id));
  });
  it(&#39;should fail on findOne exception&#39;, async () =&gt; {
    postRepository.findOne.mockRejectedValue(&#39;find error&#39;);
    const result = await service.findOne(findOneArgs.id);
    expect(result).toEqual(&#39;find error&#39;);
  });
});</code></pre>
<h4 id="test-update-method">Test <code>update()</code> method</h4>
<pre><code class="language-ts">describe(&#39;update()&#39;, () =&gt; {
  const findOneArgs = { id: &#39;1&#39; };
  const updateArgs = {
    title: &#39;new&#39;,
  };

  it(&#39;should be update post&#39;, async () =&gt; {
    const oldPosts = {
      id: &#39;1&#39;,
      title: &#39;old&#39;,
      description: &#39;description&#39;,
    };
    const newPosts = {
      id: &#39;1&#39;,
      title: &#39;new&#39;,
      description: &#39;description&#39;,
    };

    postRepository.findOne.mockResolvedValue(oldPosts);
    postRepository.save.mockResolvedValue(newPosts);

    const result = await service.update(findOneArgs.id, updateArgs);

    expect(postRepository.findOne).toHaveBeenCalledTimes(1);
    expect(postRepository.findOne).toHaveBeenCalledWith(findOneArgs);

    expect(postRepository.save).toHaveBeenCalledTimes(1);
    expect(postRepository.save).toHaveBeenCalledWith({
      ...oldPosts,
      ...updateArgs,
    });

    expect(result).toEqual(newPosts);
  });
  it(&#39;should fail if no post is found&#39;, async () =&gt; {
    postRepository.findOne.mockResolvedValue(null);

    const result = await service.findOne(findOneArgs.id);

    expect(postRepository.findOne).toHaveBeenCalledTimes(1);
    expect(postRepository.findOne).toHaveBeenCalledWith(findOneArgs);

    expect(result).toEqual(new EntityNotFoundError(Posts, findOneArgs.id));
  });
  it(&#39;should fail on findOne exception&#39;, async () =&gt; {
    postRepository.findOne.mockRejectedValue(&#39;find error&#39;);
    const result = await service.findOne(findOneArgs.id);
    expect(result).toEqual(&#39;find error&#39;);
  });
  it(&#39;should fail on save exception&#39;, async () =&gt; {
    postRepository.save.mockResolvedValue(&#39;find error&#39;);
    const result = await service.update(findOneArgs.id, updateArgs);
    expect(result).toEqual(&#39;find error&#39;);
  });
});</code></pre>
<h4 id="test-remove-method">Test <code>remove()</code> method</h4>
<pre><code class="language-ts">describe(&#39;remove()&#39;, () =&gt; {
  const removeArgs = &#39;1&#39;;
  const findOneArgs = { id: &#39;1&#39; };
  const softDeleteArgs = { id: &#39;1&#39; };

  it(&#39;should be remove post&#39;, async () =&gt; {
    postRepository.findOne.mockResolvedValue(findOneArgs);
    postRepository.softDelete.mockResolvedValue(softDeleteArgs);

    await service.remove(removeArgs);

    expect(postRepository.findOne).toHaveBeenCalledTimes(1);
    expect(postRepository.findOne).toHaveBeenCalledWith(findOneArgs);

    expect(postRepository.softDelete).toHaveBeenCalledTimes(1);
    expect(postRepository.softDelete).toHaveBeenCalledWith(softDeleteArgs);
  });

  it(&#39;should fail if no post is found&#39;, async () =&gt; {
    postRepository.findOne.mockResolvedValue(null);

    const result = await service.remove(findOneArgs.id);

    expect(postRepository.findOne).toHaveBeenCalledTimes(1);
    expect(postRepository.findOne).toHaveBeenCalledWith(findOneArgs);

    expect(result).toEqual(new EntityNotFoundError(Posts, findOneArgs.id));
  });
  it(&#39;should fail on findOne exception&#39;, async () =&gt; {
    postRepository.findOne.mockRejectedValue(&#39;find error&#39;);
    const result = await service.findOne(findOneArgs.id);
    expect(result).toEqual(&#39;find error&#39;);
  });
  it(&#39;should fail on remove exception&#39;, async () =&gt; {
    postRepository.findOne.mockRejectedValue(&#39;remove error&#39;);
    const result = await service.findOne(findOneArgs.id);
    expect(result).toEqual(&#39;remove error&#39;);
  });
});</code></pre>
<hr>

<h2 id="result">Result</h2>
<h3 id="test-coverage">Test Coverage</h3>
<pre><code class="language-sh">npm run test:cov

---------------------|---------|----------|---------|---------|-------------------
File                 | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
---------------------|---------|----------|---------|---------|-------------------
All files            |   58.33 |      100 |   56.25 |   58.49 |                   
 src                 |   41.94 |      100 |      75 |      36 |                   
  app.controller.ts  |     100 |      100 |     100 |     100 |                   
  app.module.ts      |       0 |      100 |     100 |       0 | 1-42              
  app.service.ts     |     100 |      100 |     100 |     100 |                   
  main.ts            |       0 |      100 |       0 |       0 | 1-18
 src/post            |   63.89 |      100 |      50 |   66.67 | 
  post.controller.ts |       0 |      100 |       0 |       0 | 1-40
  post.module.ts     |       0 |      100 |     100 |       0 | 1-12
  post.service.ts    |     100 |      100 |     100 |     100 | 
  # post.service.ts 가 coverage 100% 를 달성했습니다! 👏👏👏
 src/post/dto        |       0 |      100 |     100 |       0 | 
  create-post.dto.ts |       0 |      100 |     100 |       0 | 1-4
  update-post.dto.ts |       0 |      100 |     100 |       0 | 1-4
 src/post/entities   |     100 |      100 |     100 |     100 |
  post.entity.ts     |     100 |      100 |     100 |     100 |
---------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 failed, 16 passed, 17 total
Snapshots:   0 total
Time:        8.027 s
Ran all test suites.</code></pre>
<p>coverage 로 부터 Unit Test의 어떤 부분이 Test가 빠졌는지 확인이 가능합니다. </p>
<p>100%를 다 채우면 <strong>기분이 너무 좋습니다.</strong> <del>(INTJ)</del></p>
<hr>

<h2 id="end-to-ende2e-testing">End-to-End(e2e) Testing</h2>
<p>(공사중 🛠)</p>
<h3 id="setting-up">Setting up</h3>
<ul>
<li>project RootDir의 <code>test/post.e2e-spec.ts</code> 를 만들어줍니다.<pre><code class="language-ts">import { INestApplication } from &#39;@nestjs/common&#39;;
import { Test, TestingModule } from &#39;@nestjs/testing&#39;;
import { AppModule } from &#39;src/app.module&#39;;
import { getConnection } from &#39;typeorm&#39;;
</code></pre>
</li>
</ul>
<p>describe(&#39;PostController (e2e)&#39;, () =&gt; {
  let app: INestApplication;</p>
<p>  // Test 전
  beforeAll(async () =&gt; {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();</p>
<pre><code>app = moduleFixture.createNestApplication();</code></pre><p>  });</p>
<p>  // Test 후
  afterAll(async () =&gt; {
    await getConnection().dropDatabase();
    app.close();
  });</p>
<p>  describe(&#39;create&#39;, () =&gt; {
    it.todo(&#39;should create Post&#39;);
  });
  describe(&#39;findAll&#39;, () =&gt; {
    it.todo(&#39;should findAll Posts&#39;);
  });
  describe(&#39;findOne&#39;, () =&gt; {
    it.todo(&#39;should findOne Post.&#39;);
  });
  describe(&#39;update&#39;, () =&gt; {
    it.todo(&#39;should update Post.&#39;);
  });
  describe(&#39;remove&#39;, () =&gt; {
    it.todo(&#39;should remove Post.&#39;);
  });
});</p>
<p>```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS sentry + slack error로그 수집]]></title>
            <link>https://velog.io/@1yongs_/NestJS-sentry-slack-error%EB%A1%9C%EA%B7%B8-%EC%88%98%EC%A7%91</link>
            <guid>https://velog.io/@1yongs_/NestJS-sentry-slack-error%EB%A1%9C%EA%B7%B8-%EC%88%98%EC%A7%91</guid>
            <pubDate>Mon, 05 Apr 2021 16:20:15 GMT</pubDate>
            <description><![CDATA[<p>Sentry는 코드를 진단하고 고치며 최적화하여 개발자를 돕는 모니터링 플랫폼 애플리케이션(오픈소스) 입니다.</p>
<p>NestJS에서 에러가 발생했을때 <code>sentry</code>에 전달하고, sentry client인 <code>raven</code>을 통한 <a href="https://docs.nestjs.com/interceptors"><strong>인터셉터</strong></a>로 slack에 알리는 애플리케이션을 구축해봅니다.</p>
<blockquote>
<p><strong>참고 🔍</strong>
(이 포스팅의 주요 핵심 개념은 NestJS의 <em>Interceptor</em> 기능 입니다.)
<a href="https://docs.nestjs.com/interceptors">NestJS - Interceptor</a>
<a href="https://sentry.io/">Sentry.io</a>
<a href="https://victorydntmd.tistory.com/176">🙈[Spring] Interceptor (1) - 개념 및 예제🐵</a></p>
</blockquote>
<h1 id="nestjs-프로젝트-생성-및-페키지-관련-설치">NestJS 프로젝트 생성 및 페키지 관련 설치</h1>
<pre><code class="language-yaml">$ nest new sentry-test
$ cd sentry-test
$ npm i @sentry/node nest-raven</code></pre>
<hr>
<h1 id="sentry-설정">Sentry 설정</h1>
<h2 id="sentry-app">Sentry App</h2>
<p>Sentry 웹 → Project → Create a new Project → Node 선택 → 프로젝트 생성후 <code>DSN</code> 복사</p>
<h2 id="sentry-연결-및-초기화">Sentry 연결 및 초기화</h2>
<p><code>main.ts</code>에 sentry 연결</p>
<pre><code class="language-tsx">import { NestFactory } from &#39;@nestjs/core&#39;;
import { init as SentryInit } from &#39;@sentry/node&#39;;**
import { AppModule } from &#39;./app.module&#39;;

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

    // connect Sentry
  SentryInit({
    dsn:
      &#39;https://940633ab27b94fdd8afc6ae73b986030@o561269.ingest.sentry.io/이히히&#39;,
  });

  await app.listen(3000);
}
bootstrap();</code></pre>
<blockquote>
<p><strong>NOTION</strong>
sentry는 무료버전으로 request 수가 제한되어있습니다. 되도록 <code>NODE_ENV == &#39;production&#39;</code> 환경에서 적용하도록 합시다.</p>
</blockquote>
<h2 id="nestjs-interceptor-설정">NestJS Interceptor 설정</h2>
<pre><code class="language-tsx">// app.module.ts

import { HttpException, Module } from &#39;@nestjs/common&#39;;
import { ConfigModule } from &#39;@nestjs/config&#39;;
import { APP_INTERCEPTOR } from &#39;@nestjs/core&#39;;
import { RavenInterceptor, RavenModule } from &#39;nest-raven&#39;;
import { AppController } from &#39;./app.controller&#39;;
import { AppService } from &#39;./app.service&#39;;

@Module({
  imports: [
    RavenModule,
    ConfigModule.forRoot({
      envFilePath: &#39;.env&#39;,
    }),
  ],
  controllers: [AppController],
  providers: [
    AppService,

    {
      provide: APP_INTERCEPTOR, // 전역 인터셉터로 지정
      useValue: new RavenInterceptor({
        filters: [
          {
            type: HttpException,
            // Filter exceptions of type HttpException.
            // Ignore those that have status code of less than 500
            filter: (exception: HttpException) =&gt; {
              return 500 &gt; exception.getStatus();
            },
          },
        ],
      }),
    },

  ],
})
export class AppModule {}</code></pre>
<hr>
<h1 id="slack-설정">Slack 설정</h1>
<h2 id="slack-api-incoming-webhooks-생성">Slack API Incoming Webhooks 생성</h2>
<p><code>Slack API 홈페이지</code> → <code>Create New App</code> → 이름 및 workspace 지정 → <code>Feature/Incoming Webhooks</code> 메뉴 → Activate Incoming Webhooks 를 <code>On</code> → Webhook URL <code>.env</code> 파일에 저장</p>
<pre><code class="language-tsx">// .env
SLACK_WEBHOOK=https://hooks.slack.com/services/T01SZ44S95J/B01TGPASYDN/이히히</code></pre>
<h2 id="slack-메세징-솔루션-사용">Slack 메세징 솔루션 사용</h2>
<pre><code class="language-sh">$ npm i @slack/client</code></pre>
<h2 id="nestjs-interceptor-적용">NestJS Interceptor 적용</h2>
<ol>
<li>webhook.interceptor.ts</li>
</ol>
<pre><code class="language-tsx">import {
    CallHandler,
    ExecutionContext,
    Injectable,
    NestInterceptor,
} from &#39;@nestjs/common&#39;;
import { captureException } from &#39;@sentry/minimal&#39;;
import { Observable } from &#39;rxjs&#39;;
import { catchError } from &#39;rxjs/operators&#39;;
import { IncomingWebhook } from &#39;@slack/client&#39;;

@Injectable()
export class WebhookInterceptor implements NestInterceptor {
    intercept(_: ExecutionContext, next: CallHandler) /** : Observable&lt;any&gt;*/ {
    return next.handle().pipe(
        catchError((error) =&gt; {
        const webhook = new IncomingWebhook(**process.env.SLACK_WEBHOOK**);
        webhook.send({
            attachments: [
            {
                color: &#39;danger&#39;,
                text: &#39;회사 드가자~ 드가자~!&#39;,
                fields: [
                {
                    title: `Request Message: ${error.message}`,
                    value: error.stack,
                    short: false,
                },
                ],
                ts: Math.floor(new Date().getTime() / 1000).toString(), // unix form
            },
            ],
        });
        return null;
        }),
    );
    }
}</code></pre>
<ol start="2">
<li>app.module.ts<pre><code class="language-ts">import { HttpException, Module } from &#39;@nestjs/common&#39;;
import { ConfigModule } from &#39;@nestjs/config&#39;;
import { APP_INTERCEPTOR } from &#39;@nestjs/core&#39;;
import { RavenInterceptor, RavenModule } from &#39;nest-raven&#39;;
import { AppController } from &#39;./app.controller&#39;;
import { AppService } from &#39;./app.service&#39;;
import { WebhookInterceptor } from &#39;./webhook.interceptor&#39;;
</code></pre>
</li>
</ol>
<p>@Module({
    // ...</p>
<pre><code>providers: [
// ...

  // Webhook Interceptor 적용
{
    provide: APP_INTERCEPTOR,
    useClass: WebhookInterceptor,
},

],</code></pre><p>})
export class AppModule {}</p>
<pre><code># Test
1. app.controller.ts

```ts
import { Controller, Get } from &#39;@nestjs/common&#39;;
import { AppService } from &#39;./app.service&#39;;

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Get(&#39;error&#39;)
  getError() {
    throw new Error(&#39;this is error!!&#39;);
  } 
}</code></pre><ol start="2">
<li>App 실행 후, error request<pre><code class="language-sh">$ curl -o http://127.0.0.1:3000/error</code></pre>
</li>
</ol>
<h1 id="result">Result</h1>
<p><img src="https://images.velog.io/images/1yongs_/post/c35f2b17-808b-4185-960f-4e45e9710bd1/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS Auth - Authorization]]></title>
            <link>https://velog.io/@1yongs_/NestJS-Auth-Authorization</link>
            <guid>https://velog.io/@1yongs_/NestJS-Auth-Authorization</guid>
            <pubDate>Sun, 14 Feb 2021 14:33:57 GMT</pubDate>
            <description><![CDATA[<h2 id="index">Index</h2>
<p>이전까지 <code>AuthGuard</code>를 만들어 <code>@UseGuard()</code> 데코레이터로 적용한 Authentication 과정을 진행하였습니다. 이번 글에서는 <a href="https://docs.nestjs.com/security/authorization#basic-rbac-implementation">Role-based access control</a> 메커니즘을 적용하여 역할과 권한에 대해 정의하여 액세스를 제어해 봅시다.</p>
<h2 id="role-based-authorization">Role-Based Authorization</h2>
<h3 id="add-role">Add Role</h3>
<p>먼저 User Entity에 <code>Role</code> 이라는 권한(역할) Column을 추가해줍니다.</p>
<ul>
<li>User Entity<pre><code class="language-ts">// src/user/entity/user.entity.ts
</code></pre>
</li>
</ul>
<p>export enum UserRole {
  CLIENT = &#39;CLIENT&#39;,
  ADMIN = &#39;ADMIN&#39;,
}</p>
<p>@Entity({ name: &#39;users&#39; })
export class User {
  // ....</p>
<p>  @Column({
    type: &#39;enum&#39;,
    enum: UserRole,
    default: UserRole.CLIENT,
  })
  role: UserRole;</p>
<p>  // ...
}</p>
<pre><code>### Role Guards
`@Role()` 이라는 Decorator를 만듭니다. 이 데코레이터는 지정한 특정 리소스에 대해 필요한 role(권한)을 허가합니다. 
예를 들어 `admin`이 필요한 리소스에 접근하기 위해 사용자는 `role` column에 `admin`으로 지정이 되어야 하죠. 
- Role Guard
```ts
// src/auth/role.decorator.ts

import { SetMetadata } from &#39;@nestjs/common&#39;;
import { UserRole } from &#39;src/user/entity/user.entity&#39;;

// &#39;Any&#39; : 모든 role에 대해 허용
export type AllowedRole = keyof typeof UserRole | &#39;Any&#39;;

export const Role = (roles: AllowedRole[]) =&gt; SetMetadata(&#39;roles&#39;, roles);</code></pre><blockquote>
<p><strong>참고 🔍</strong></p>
<ul>
<li>Nest 는 <code>@SetMetadata()</code> decorator를 통해 route handler에 <strong>custom metadata</strong>를 붙일 수 있도록 기능을 제공합니다.</li>
<li><a href="https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata">NestJS Docs - Excution context(Reflection and metadata)</a></li>
</ul>
</blockquote>
<ul>
<li>Auth Guard
route의 <code>roles</code> custom metadata에 접근하기 위해, framework 밖에서 제공하는 <strong><code>Reflector</code></strong> 헬퍼 클레스를 사용합니다. <pre><code class="language-ts">// src/auth/auth.guard.ts
</code></pre>
</li>
</ul>
<p>@Injectable()
export class AuthGuard implements CanActivate {</p>
<p>  constructor(private reflector: Reflector) {} // <code>roles</code> custom metadata에 접근하기 위해 reflector 사용</p>
<p>  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
      // 
    const requiredRoles = this.reflector.get<AllowedRole>(
      &#39;roles&#39;,
      context.getHandler(),
    );</p>
<pre><code>if (!requiredRoles) { // role이 필요없는 resource면 그냥 넘어감 
  return true;
}

const request = context.switchToHttp().getRequest();
const user: User = request[&#39;user&#39;];
if (!user) 
  return false;

if (requiredRoles.includes(&#39;Any&#39;)) 
  return true; // `Any` 권한은 로그인된 사용자 중 아무나 접근이 가능하다는 뜻.
return requiredRoles.includes(user.role); // 사용자의 role이 resource가 필요한 권한에 포함되어있는지 검사!</code></pre><p>  }
}</p>
<pre><code>- User Role Guards
이제 Controller에 Role Decorator를 적용해봅시다.
```ts
// src/user/user.controller.ts

@Controller(&#39;user&#39;)
export class UserController {
  // ...

  @Get(&#39;/me&#39;)
  @Role([UserRole.CLIENT])
  @UseGuards(AuthGuard)
  getMe() {
    return &#39;Get Me!&#39;;
  }

  @Get(&#39;/admin&#39;)
  @Role([UserRole.ADMIN])
  @UseGuards(AuthGuard)
  getAdmin() {
    return `U R Admin`;
  }
}</code></pre><h2 id="custom-decorator">Custom Decorator</h2>
<p>만약 현재 방식으로 swagger나 다른 decorator를 Auth 과정을 함께 쓴다면 controller는 골뱅이 무침이 될 것 같습니다.</p>
<p>그래서 우리는 Auth에 필요한 Decorator를 모~두 묶어 제공해봅시다.</p>
<h3 id="auth에-필요한-decorator-묶기">Auth에 필요한 Decorator 묶기</h3>
<p>Custom Decorator를 만들어 봅시다.</p>
<ul>
<li>Auth Decorator
현재는 2개만 적용했지만, 더 많은 데코레이터를 나열하여 적용시킬 수 있습니다.<pre><code class="language-ts">// src/auth/auth.decorator.ts
</code></pre>
</li>
</ul>
<p>import { applyDecorators, UseGuards } from &#39;@nestjs/common&#39;;
import { AuthGuard } from &#39;./auth.guard&#39;;
import { AllowedRole, Role } from &#39;./role.guard&#39;;</p>
<p>export function Auth(roles: AllowedRole[]) {
  return applyDecorators(Role(roles), UseGuards(AuthGuard));
}</p>
<pre><code>&gt; **참고 🔍**
&gt; 
&gt; * [NestJS Docs - Custom decorators(decorator composition)](https://docs.nestjs.com/custom-decorators#decorator-composition)

- user controller
Controller에 적용시켜 봅시다.
```ts
@Controller(&#39;user&#39;)
export class UserController {

  // ...

  @Get(&#39;/me&#39;)
  // @Role([UserRole.CLIENT])
  // @UseGuards(AuthGuard) 밑에꺼로 단축 가능
  @Auth([UserRole.CLIENT])
  getMe() {
    return &#39;Get Me!&#39;;
  }

  @Get(&#39;/admin&#39;)
  @Auth([UserRole.ADMIN])
  getAdmin() {
    return `U R Admin`;
  }
}
</code></pre><blockquote>
<p><strong>참고 🔍</strong></p>
<p><a href="https://github.com/dlfdyd96/nestjs-auth">Github Repository</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS Auth - Authentication]]></title>
            <link>https://velog.io/@1yongs_/NestJS-Auth-Authentication</link>
            <guid>https://velog.io/@1yongs_/NestJS-Auth-Authentication</guid>
            <pubDate>Sun, 14 Feb 2021 14:29:41 GMT</pubDate>
            <description><![CDATA[<p>이전 글까지는 Authentication을 준비하는 단계였습니다. 이번글에서는 <strong><code>JWT</code>를 이용한 회원가입과 로그인 과정 및 사용자 **인증(Authentication)</strong> 과정을 다루어 보도록 하겠습니다.</p>
<blockquote>
<p><strong>참고 🔍</strong></p>
</blockquote>
<ul>
<li><a href="https://velopert.com/2350">[JWT] 토큰(Token) 기반 인증에 대한 소개</a></li>
</ul>
<h2 id="index-📋">Index 📋</h2>
<ul>
<li><p>Sign Up (회원가입)</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> User Module</li>
<li><input checked="" disabled="" type="checkbox"> User Service</li>
<li><input checked="" disabled="" type="checkbox"> User Controller</li>
<li><input checked="" disabled="" type="checkbox"> Hashing Password</li>
</ul>
</li>
<li><p>Sign In (로그인)</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> Verify User</li>
<li><input checked="" disabled="" type="checkbox"> Token Generate</li>
<li><input checked="" disabled="" type="checkbox"> Verify Token</li>
<li><input checked="" disabled="" type="checkbox"> Guard Decorator</li>
</ul>
</li>
</ul>
<h2 id="sign-up-회원가입-📝">Sign Up (회원가입) 📝</h2>
<h3 id="user-module">User Module</h3>
<ul>
<li><strong>Register Repository to use User entity</strong>
User Module에서 TypeOrmModule의 <code>forFeature()</code> 메서드를 사용하여 현재 scope(User)에서 어떤 repository를 등록할 것 인지 결정합니다.</li>
</ul>
<pre><code class="language-ts">// src/user/user.module.ts

import { Module } from &#39;@nestjs/common&#39;;
import { UserService } from &#39;./user.service&#39;;
import { UserController } from &#39;./user.controller&#39;;
import { TypeOrmModule } from &#39;@nestjs/typeorm&#39;;
import { User } from &#39;./entity/user.entity&#39;;

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UserService],
  controllers: [UserController],
})
export class UserModule {}</code></pre>
<h3 id="user-service">User Service</h3>
<ul>
<li>Inject Repository
Module에 등록하면 우리는 현재의 범위(User)에서 <code>UsersRepository</code>를 <code>@InjectRepository()</code> decorator를 사용하여 <code>UserService</code>안에 <strong>inject</strong> 할 수 있습니다.</li>
</ul>
<pre><code class="language-ts">// src/user/user.service.ts

import { Injectable } from &#39;@nestjs/common&#39;;
import { InjectRepository } from &#39;@nestjs/typeorm&#39;;
import { Repository } from &#39;typeorm&#39;;
import { User } from &#39;./entity/user.entity&#39;;

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepository: Repository&lt;User&gt;,
  ) {}
}
</code></pre>
<ul>
<li>먼저 필요한 package를 설치하고,  <strong>DTO</strong>(Data Transfer Object)를 통해 Validation 작업을 합니다.</li>
</ul>
<blockquote>
<p><strong>참고 🔍</strong></p>
</blockquote>
<ul>
<li><a href="https://ko.wikipedia.org/wiki/%EB%8D%B0%EC%9D%B4%ED%84%B0_%EC%A0%84%EC%86%A1_%EA%B0%9D%EC%B2%B4">데이터 전송 객체(DTO)</a></li>
</ul>
<pre><code class="language-sh">$ npm i class-validator class-transformer</code></pre>
<pre><code class="language-ts">// src/main.ts

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  await app.listen(3000);
}
bootstrap();</code></pre>
<ul>
<li>Define <strong>DTO</strong>(Data Transfer Object)</li>
</ul>
<pre><code class="language-ts">// src/user/dtos/create-user.dto.ts

import { IsEmail, IsString } from &#39;class-validator&#39;;

export class CreateUserRequestDto {
  @IsEmail()
  username: string;

  @IsString()
  password: string;

  @IsString()
  name: string;
}</code></pre>
<ul>
<li>Insert User Object to Database
user repository의 <code>save()</code> method를 통해 database에 object를 insert 해줍니다. 그전에 등록할 계정이 이미 존재한다면 error를 반환합니다.<pre><code class="language-ts">// src/user/user.service.ts
</code></pre>
</li>
</ul>
<p>import { ForbiddenException, HttpException, Injectable } from &#39;@nestjs/common&#39;;
import { InjectRepository } from &#39;@nestjs/typeorm&#39;;
import { Repository } from &#39;typeorm&#39;;
import { CreateUserRequestDto } from &#39;./dtos/create-user.dto&#39;;
import { User } from &#39;./entity/user.entity&#39;;</p>
<p>@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepository: Repository<User>,
  ) {}</p>
<p>  async create(data: CreateUserRequestDto) {
    const isExist = await this.usersRepository.findOne({
      username: data.username,
    });
    if (isExist) {
      throw new ForbiddenException({
        statusCode: HttpStatus.FORBIDDEN,
        message: [<code>이미 등록된 사용자입니다.</code>],
        error: &#39;Forbidden&#39;,
      });
    }
    try {
      await this.usersRepository.save(data);
    } catch (error) {
      return {
        ...error,
      };
    }
    return {
      statusCode: HttpStatus.CREATED,
    };
  }
}</p>
<pre><code>
### User Controller
- Create User End Point
  - UserController가 `POST` 요청으로 body에 사용자를 생성할 데이터를 `http://localhost:{PORT}/user`로 받습니다.
  - 추가로 생성자로 `UserService` instance를 inject 합니다.
```ts
// src/user/user.controller.ts

import { Body, Controller, HttpCode, HttpStatus, Post } from &#39;@nestjs/common&#39;;
import { CreateUserRequestDto } from &#39;./dtos/create-user.dto&#39;;
import { UserService } from &#39;./user.service&#39;;

@Controller(&#39;user&#39;)
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body() data: CreateUserRequestDto) {
    return this.userService.create(data);
  }
}</code></pre><h3 id="hashing-password-🧶">Hashing Password 🧶</h3>
<p>사용자 등록하기 위해 DB에 Insert 되기 전, 비밀번호를 hashing 하는 작업을 해보도록 하겠습니다.</p>
<ul>
<li><p>Install bcrypt package</p>
<pre><code class="language-sh">$ npm install bcrypt</code></pre>
</li>
<li><p>TypeORM Listeners and Subscribers
<a href="https://typeorm.io/#/listeners-and-subscribers/beforeinsert">TypeORM - Listeners and Subscribers</a> 를 참고하시면 TypeORM에 특별한 기능이 있습니다.</p>
<blockquote>
<p>Any of your entities can have methods with custom logic that listen to specific entity events. You must mark those methods with special decorators depending on what event you want to listen to.</p>
</blockquote>
<p> 모든 Entity들은 특정 entity event를 기다리고 있는 사용자 정의 로직 메서드를 가질 수 있습니다.</p>
<p> 저는 <code>@BeforeInsert()</code> 라는 데코레이터를 사용하여, 사용자를 등록하여 DB에 Insert 되기 전, 비밀번호를 hashing 하는 작업을 해보도록 하겠습니다.</p>
</li>
</ul>
<pre><code class="language-ts">// src/user/entity/user.entity.ts

// ...
@Entity({ name: &#39;users&#39; })
export class User {
  // ...

  @BeforeInsert()
  async hashPassword(): Promise&lt;void&gt; {
    try {
      this.password = await bcrypt.hash(this.password, 10);
    } catch (e) {
      console.log(e);
      throw new InternalServerErrorException();
    }
  }
}
</code></pre>
<hr>

<h2 id="sign-in-로그인-🔐">Sign In (로그인) 🔐</h2>
<h3 id="token-generate-🎫">Token Generate 🎫</h3>
<p>아이디와 비밀번호로 로그인을 받으면 우리의 서버에서 <strong>계정 정보를 검증</strong>하고 맞다면 <strong>sigined 토큰</strong>을 발급 해줍니다. (토큰에는 중요한 개인정보가 담기면 안됩니다! 그저 발급한 토큰이 우리의 서버에서 정상적으로 발급된 토큰임을 증명하기 위함입니다.) </p>
<p>(나중에 사용자는 인증이 필요한 End point에 요청하기 위해 발급받은 토큰을 이용하게 됩니다.)</p>
<ul>
<li>Set Up<pre><code>$ npm i jsonwebtoken
</code></pre></li>
</ul>
<p>$ nest generate module jwt
$ nest generate service jwt</p>
<pre><code>그리고 JWT Module을 option으로 privateKey를 받는 동적모듈로 만들기 위해 option interface와 di token 값(`jwt constant`)을 만들어줍니다.

&gt; **참고 🔍**
- [NestJS Docs - Dynamic Modules](https://docs.nestjs.com/fundamentals/dynamic-modules)
- [NestJS 메일 발송 Dynamic Module](https://velog.io/@1yongs_/NestJS-%EB%A9%94%EC%9D%BC-%EB%B0%9C%EC%86%A1-Dynamic-Module)

- JWT Interface
```ts
export interface JwtModuleOptions {
  privateKey: string;
}</code></pre><ul>
<li><p>JWT Constant</p>
<pre><code class="language-ts">export const CONFIG_OPTIONS = &#39;CONFIG_OPTIONS&#39;;</code></pre>
</li>
<li><p>JWT Module
JWT Module을 <code>@Global()</code> decorator로 전역에서 사용할 수 있도록 하고, <code>forRoot()</code> static method로 Dynamic Module을 반환하도록 합니다.</p>
</li>
</ul>
<pre><code class="language-ts">@Module({})
@Global()
export class JwtModule {
  static forRoot(options: JwtModuleOptions): DynamicModule {
    return {
      module: JwtModule,
      exports: [JwtService],
      providers: [
        {
          provide: CONFIG_OPTIONS,
          useValue: options,
        },
        JwtService,
      ],
    };
  }
}</code></pre>
<ul>
<li>App Module
<code>AppModule</code>에 <code>JWTModule</code>을 import 하도록 합시다. <code>privateKey</code>는 JWT 생성과 검증에 필요한 비밀키를 지정합니다. (예: mYsEcReTkEy0011)</li>
</ul>
<pre><code class="language-ts">// src/app.module.ts

@Module({
  imports: [

    JwtModule.forRoot({
      privateKey: process.env.PRIVATE_KEY, // JWT private Key 아무거나. 
    }),
  ]
})
// ...</code></pre>
<ul>
<li>JWT Service (토큰 발행)
<code>JwtService</code>에는 <strong>토큰 발행</strong>을 담당하는 <code>sign</code> method를 정의해줍니다. 토큰에는 <code>userId</code> (uuid)를 담아서 줍니다.</li>
</ul>
<pre><code class="language-ts">import { Inject, Injectable } from &#39;@nestjs/common&#39;;
import * as jwt from &#39;jsonwebtoken&#39;;
import { CONFIG_OPTIONS } from &#39;./jwt.constants&#39;;
import { JwtModuleOptions } from &#39;./jwt.interface&#39;;

@Injectable()
export class JwtService {
  constructor(
    @Inject(CONFIG_OPTIONS) private readonly options: JwtModuleOptions,
  ) {}

  // 로그인 성공하면 token을 만들어 보냄
  sign(userId: string): string {
    return jwt.sign({ id: userId }, this.options.privateKey);
  }
}</code></pre>
<h3 id="verify-user">Verify User</h3>
<p>id와 password를 받아 사용자를 확인하는 기능을 만듭니다.</p>
<ul>
<li>user entity
User Entity에 비밀번호를 검증하는 helper method를 추가합니다.</li>
</ul>
<blockquote>
<p><strong>참고 🔍</strong></p>
</blockquote>
<ul>
<li><a href="https://gist.github.com/jtushman/673f5cd9c0225ee50951fcf46be24df5">Gist - add helper methods on TypeORM modles Example</a></li>
</ul>
<pre><code class="language-ts">// src/user/entity/user.entity.ts

@Entity({ name: &#39;users&#39; })
export class User {
  // ...

  async checkPassword(inputPassword: string): Promise&lt;boolean&gt; {
    try {
      return await bcrypt.compare(inputPassword, this.password);
    } catch (error) {
      console.log(error);
      throw new InternalServerErrorException({
        ...error.response,
      });
    }
  }
}</code></pre>
<ul>
<li>DTO<pre><code class="language-ts">export class SignInRequestDto {
  username: string;
    password: string;
}
</code></pre>
</li>
</ul>
<p>export class SignInResponseDto {
    statusCode: number;
      token?: string;
      error?: string;
    message?: string;
}</p>
<pre><code>
- User Service
사용자임을 확인하고 token을 발행해줍니다.
```ts
// src/user/user.service.ts

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository&lt;User&gt;,
    private readonly jwtService: JwtService,
  ) {}

  // ...

  async signIn({
    username,
    password,
  }: SignInRequestDto): Promise&lt;SignInResponseDto&gt; {
    try {
      const user = await this.userRepository.findOne({ username });
      if (!user) {
        throw new NotFoundException({
          error: &#39;Not Found&#39;,
          message: [&#39;사용자를 찾지 못했습니다.&#39;],
        });
      }
      const passwordCorrect = await user.checkPassword(password);
      if (!passwordCorrect) {
        throw new BadRequestException({
          error: &#39;Bad Request&#39;,
          message: [&#39;비밀번호가 틀렸습니다.&#39;],
        });
      }

      const token = this.jwtService.sign(user.id);
      return {
        statusCode: 201,
        token,
      };
    } catch (error) {
      return {
        statusCode: error.status,
        ...error.response,
      };
    }
  }
}</code></pre><ul>
<li>User Controller</li>
<li><em>POST*</em> 요청으로 request body로는 <code>username</code>과 <code>password</code>를 받습니다.<pre><code class="language-ts">@Controller(&#39;users&#39;)
export class UserController {
 constructor(private readonly userService: UserService) {}

</code></pre>
</li>
</ul>
<p>  @Post(&#39;/sign-in&#39;)
  signIn(@Body() data: SignInRequestDto): Promise<SignInResponseDto> {
    return this.userService.signIn(data);
  }
}</p>
<pre><code>
## Authentication
로그인된 사용자가 인증이 필요한 서버의 어느 한 End point로 요청하기위해 발행된 토큰을 request header의 `x-jwt`라는 이름으로 담아 요청합니다.

이 때 우리는 모든 요청에 대해 header를 검사하는 `JwtMiddleware`를 만들어 줍니다.
### Verify Token

- jwt service (토큰 검증)
`verify` method를 추가해줍니다.
```ts
// src/jwt/jwt.service.ts

@Injectable()
export class JwtService {
  // ...

  verify(token: string) {
    return jwt.verify(token, this.options.privateKey);
  }
}</code></pre><ul>
<li>user service (사용자 찾기)<pre><code class="language-ts">// src/user/user.service.ts

</code></pre>
</li>
</ul>
<p>@Injectable()
export class UserService {
  // ...</p>
<p>  async findById(id: string): Promise<User> {
    return await this.userRepository.findOne(id);
  }
}</p>
<pre><code>

- JWT Middleware
 JWT Middleware에서는 header의 `x-jwt`토큰을 검증하여 request object에 `&#39;user&#39;: User`를 추가합니다.
다음으로 `next()` method를 호출하여 다음 함수로 넘어갑니다. (header에 `x-jwt`이 없다면 user를 추가하지 않고 다음으로 넘어갑니다.)

```ts
// src/jwt/jwt.middleware.ts

@Injectable()
export class JwtMiddleware implements NestMiddleware {
  constructor(
    private readonly userService: UserService,
    private readonly jwtService: JwtService,
  ) {}

  async use(req: Request, res: Response, next: NextFunction) {
    if (&#39;x-jwt&#39; in req.headers) {
      const token = req.headers[&#39;x-jwt&#39;];
      const decoded = this.jwtService.verify(token.toString());
      if (typeof decoded === &#39;object&#39; &amp;&amp; decoded.hasOwnProperty(&#39;id&#39;)) {
        try {
          const user = await this.userService.findById(decoded[&#39;id&#39;]);
          req[&#39;user&#39;] = user;
        } catch (err) {
          console.log(err);
        }
      }
    }
    next();
  }
}</code></pre><ul>
<li>App Module
App Module에 JWT Middleware를 모든 요청으로 적용시켜줍니다.<pre><code class="language-ts">// src/app.module.ts
</code></pre>
</li>
</ul>
<p>@Module({
  // ...
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(JwtMiddleware)
      .forRoutes({ path: &#39;/&#39;, method: RequestMethod.ALL });
  }
}</p>
<pre><code>
&gt; **참고 🔍**
- [NestJS Docs - Middleware](https://docs.nestjs.com/middleware)

### Auth Guard
- Set up</code></pre><p>$ nest generate guard auth auth</p>
<pre><code>- Auth Guard
```ts
@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise&lt;boolean&gt; | Observable&lt;boolean&gt; {

    const request = context.switchToHttp().getRequest();
    const user: User = request[&#39;user&#39;];

    if (!user) return false;
    return true;
  }
}</code></pre><h3 id="use-guards">Use Guards</h3>
<ul>
<li>Auth Guard 적용
<code>@UserGuards()</code> decorator를 사용하여 우리가 만든 <code>AuthGuard</code>를 적용해줍니다. 
token검증을 성공하면 <code>Get Me!</code>를 받고, 검증에 실패하면 <code>UnAuthorized Error</code>를 받습니다.</li>
</ul>
<pre><code class="language-ts">// src/user/user.controller.ts

@Controller(&#39;users&#39;)
export class UserController {
  // ...

  @Get(&#39;/me&#39;)
  @UseGuards(AuthGuard)
  getMe() {
    return &#39;Get Me!&#39;;
  }
}</code></pre>
<blockquote>
<p><strong>참고 🔍</strong></p>
</blockquote>
<ul>
<li><p><a href="https://docs.nestjs.com/guards">NestJS Docs - Guards</a></p>
</li>
<li><p><a href="https://docs.nestjs.com/security/authentication">NestJS Docs - Security: Authentication</a></p>
</li>
<li><p>Custom Decorator
이전에 Middleware에서 request object에 <code>user</code>를 넣었었습니다. 이를 <code>next()</code> method로 다음으로 넘어온 함수에서 사용하기 위해 Custom Decorator로 정의한 <code>@AuthUser()</code>를 사용합니다.</p>
</li>
</ul>
<pre><code class="language-ts">// src/auth/auth-user.decorator.ts

import { createParamDecorator, ExecutionContext } from &#39;@nestjs/common&#39;;

export const AuthUser = createParamDecorator(
  (data: unknown, context: ExecutionContext) =&gt; {
    const request = context.switchToHttp().getRequest();
    const user = request[&#39;user&#39;];
    return user;
  },
);</code></pre>
<blockquote>
<p><strong>참고 🔍</strong></p>
</blockquote>
<ul>
<li><a href="https://docs.nestjs.com/custom-decorators">NestJS Docs - Custom Decorators</a></li>
</ul>
<hr>

<h3 id="정리">정리</h3>
<p>사용자 인증 / 인가 부분은 많은 기술 내용들과 작업들을 담고 있기 때문에 글을 쓰면서도 아직 부족하다는 느낌을 많이 받았습니다. 추후에 수정을 하면서 채워나가도록 하겠습니다...  
༼;´༎ຶ + ༎ຶ༽ </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS Auth - Setup]]></title>
            <link>https://velog.io/@1yongs_/NestJS-Auth-Setup</link>
            <guid>https://velog.io/@1yongs_/NestJS-Auth-Setup</guid>
            <pubDate>Sun, 14 Feb 2021 12:53:18 GMT</pubDate>
            <description><![CDATA[<h2 id="index">Index</h2>
<ul>
<li>MySQL Setup<ul>
<li><input checked="" disabled="" type="checkbox"> docker-compose</li>
</ul>
</li>
<li>NestJS Setup<ul>
<li><input checked="" disabled="" type="checkbox"> Create Project</li>
<li><input checked="" disabled="" type="checkbox"> TypeORM Setup (Connect DB)</li>
<li><input checked="" disabled="" type="checkbox"> User Entity Model</li>
</ul>
</li>
</ul>
<h2 id="mysql-docker-compose">MySQL docker-compose</h2>
<h3 id="docker-compose">Docker Compose</h3>
<p>MySQL Container 환경을 구성합니다. 기존 MySQL를 사용하기 원하시는 사용자는 <code>(Optinal)</code>이라고 적힌 부분을 Skip 하셔도 됩니다.</p>
<ul>
<li><code>docker-compose.yml</code>이란 파일을 생성합니다. (Optinal)</li>
</ul>
<pre><code class="language-yml">services:
  mysql:
    image: mysql:5.7
    container_name: dev-mysql
    restart: always
    ports:
      - 10310:3306
    environment:
      TZ: Asia/Seoul
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: dev
      MYSQL_USER: dev
      MYSQL_PASSWORD: dev
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
      - --skip-character-set-client-handshake
    volumes:
      - dev-mysql:/var/lib/mysql

volumes:
  dev-mysql:</code></pre>
<ul>
<li><p><code>docker-compose up -d</code> 명령어를 사용하여 MySQL Docker Container를 생성해줍니다. (Optinal)</p>
</li>
<li><p>MySQL WorkBench에서 Container가 생성되었는지 확인하고, 아래에 적힌 ID, Password로 Database에 Connect 합니다. (Optinal)</p>
</li>
</ul>
<pre><code>id: root
pw: root</code></pre><p><img src="https://images.velog.io/images/1yongs_/post/181ad1cb-7414-4e58-b659-9beb852cdd12/workbench_connect.png" alt=""></p>
<ul>
<li><p>Schema(Database)를 만들어 줍니다.
<img src="https://images.velog.io/images/1yongs_/post/edf11208-b567-4f18-937f-ddadb4644226/workbench_schema.png" alt=""></p>
</li>
<li><p>왼쪽 위 <code>Administration</code> 탭의 <code>Users and Privileges</code>에서 새로운 account를 등록해줍니다.</p>
<pre><code># 사용자 마음대로
</code></pre></li>
</ul>
<p>id: ilyong
pw: ilyong </p>
<pre><code>![](https://images.velog.io/images/1yongs_/post/50f3afd4-8aa7-41f4-8f18-fb286732efbf/workbench_add_account.png)

![](https://images.velog.io/images/1yongs_/post/a78705ab-5eb8-4832-9afb-94face6f1fb6/workbench_add_account2.png)


## NestJS Setup
### Create Project
- `@nestjs/cli` Package를 통해 프로젝트를 만들어 줍니다.
```sh
$ nest new nestjs-auth</code></pre><blockquote>
<p><strong>참고 🔍</strong></p>
</blockquote>
<ul>
<li><a href="https://docs.nestjs.com/cli/overview">NestJS Docs - CLI</a></li>
</ul>
<h3 id="typeorm-setup-connect-db">TypeORM Setup (Connect DB)</h3>
<ul>
<li><p>필요한 Package를 설치해줍니다.</p>
<pre><code class="language-sh">$ npm i joi @nestjs/config
$ npm i @nestjs/typeorm typeorm mysql2</code></pre>
</li>
<li><p>우선 Database 연결에 필요한 환경변수가 정의된 파일을 만들어줍니다.</p>
<pre><code class="language-env"># File Should exist on Project Root Directory
# File Name : `.env`
</code></pre>
</li>
</ul>
<p>NODE_PORT=3000</p>
<h1 id="mysql-database-hostname">MySQL Database Hostname</h1>
<p>MYSQL_HOST=127.0.0.1</p>
<h1 id="mysql-database-port">MySQL Database Port</h1>
<p>MYSQL_PORT=10310</p>
<h1 id="mysql-database-username">MySQL Database username</h1>
<p>MYSQL_USERNAME=ilyong</p>
<h1 id="mysql-database-password">MySQL Database Password</h1>
<p>MYSQL_PASSWORD=ilyong</p>
<h1 id="mysql-database-schema-name">MySQL Database Schema Name</h1>
<p>MYSQL_DATABASE=ilyong</p>
<pre><code>
- `ConfigModule`을 통해 Nest Project에 환경변수를 불러옵니다.
```ts
// src/app.module.ts

import { Module } from &#39;@nestjs/common&#39;;
import { ConfigModule } from &#39;@nestjs/config&#39;;
import { AppController } from &#39;./app.controller&#39;;
import { AppService } from &#39;./app.service&#39;;
import * as Joi from &#39;joi&#39;;

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: &#39;.env&#39;,
      isGlobal: true,
      validationSchema: Joi.object({
        NODE_PORT: Joi.string().required(),
        MYSQL_HOST: Joi.string().required(),
        MYSQL_PORT: Joi.string().required(),
        MYSQL_USERNAME: Joi.string().required(),
        MYSQL_PASSWORD: Joi.string().required(),
        MYSQL_DATABASE: Joi.string().required(),
      }),
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}</code></pre><blockquote>
<p><strong>참고 🔍</strong></p>
</blockquote>
<ul>
<li><p><a href="https://docs.nestjs.com/techniques/configuration#schema-validation">NestJS Docs - Schema validation</a></p>
</li>
<li><p><a href="https://docs.nestjs.com/techniques/configuration#configuration">NestJS Docs - Configuration</a></p>
</li>
<li><p><code>TypeOrmModule</code>을 사용하여 MySQL Database에 연결합니다. </p>
<pre><code class="language-ts">// src/app.module.ts

</code></pre>
</li>
</ul>
<p>@Module({
  imports: [
    // ...</p>
<pre><code>TypeOrmModule.forRoot({
  type: &#39;mysql&#39;,
  host: process.env.MYSQL_HOST,
  port: +process.env.MYSQL_PORT,
  username: process.env.MYSQL_USERNAME,
  password: process.env.MYSQL_PASSWORD,
  database: process.env.MYSQL_DATABASE,
  // Application이 실행(변경)될 때마다 database schema가 자동으로 생성(DB에 바로 반영)됩니다. 
  // (주의! 배포 환경에서 쓰지마시오)
  synchronize: true,
  // Query를 할때마다 Console Log를 찍어줍니다.
  logging: true,
  // The charset for the connection. (default: &#39;UTF8_GENERAL_CI&#39;)
  charset: &#39;utf8mb4_unicode_ci&#39;,
  // The timezone configured on the MySQL server.
  timezone: &#39;+09:00&#39;,
  // Database 연결에 대해 불러올 Entity를 정의합니다.
  // Entity는 데이터베이스 테이블에 매핑되는 Class입니다.
  entities: [],
}),

// ...
,</code></pre><p>})
export class AppModule {}</p>
<pre><code>

&gt; **참고 🔍**
- [NestJS Docs - Database](https://docs.nestjs.com/techniques/database)
- [NestJS Docs - TypeORM](https://docs.nestjs.com/recipes/sql-typeorm)
- [TypeORM Docs - Connection Options](https://typeorm.io/#/connection-options)
- [TypeORM Docs - What Is Entities?](https://typeorm.io/#/entities)

### User Entity Model

**TypeORM**은 [Repository Design Pattern](https://typeorm.io/#/working-with-repository)을 지원합니다. 따라서 각 Entity는 자신의 Repository를 가집니다. 이러한 Repository들은 database 연결로부터 얻어질 수 있습니다.

- 우선 사용자 관련 로직을 처리하기 위해 `Module`, `Controller`, `Service`를 생성합니다.
```sh
$ nest generate module user
$ nest generate service user
$ nest generate controller user</code></pre><ul>
<li>그리고 <code>User</code> Entity를 정의해줍니다.<pre><code class="language-ts">// src/user/entity/user.entity.ts
</code></pre>
</li>
</ul>
<p>import { Column, CreateDateColumn, DeleteDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn, VersionColumn } from &quot;typeorm&quot;;</p>
<p>@Entity({ name: &#39;users&#39; })
export class User {
  @PrimaryGeneratedColumn(&#39;uuid&#39;)
  id: string; // Primary Key</p>
<p>  @Column()
  username: string; // id(email 형식)</p>
<p>  @Column()
  name: string; // 사용자명</p>
<p>  @Column()
  password: string; // 비밀번호</p>
<p>  @Column({ nullable: true })
  avatar: string; // 프로필 이미지 (nullable)</p>
<p>  @CreateDateColumn()
  createdAt: Date;</p>
<p>  @UpdateDateColumn()
  updatedAt: Date;</p>
<p>  @DeleteDateColumn()
  deletedAt: Date;</p>
<p>  @VersionColumn()
  version: string;
}</p>
<pre><code>
- `User entity` 사용을 시작하기 위해서 `TypeOrmModule`의 `forRoot()` method 옵션 중 하나인 `entities` Array에 EnInserting 함으로써 `TypeORM`에 이를 알려야 합니다.

```ts
// src/app.module.ts

// ...
import { User } from &#39;./user/entity/user.entity&#39;;

@Module({
  imports: [
    // ...
    TypeOrmModule.forRoot({
      // ...

      entities: [User],

      // ...</code></pre><ul>
<li>저장하고 Nest Application을 실행하여 Database에 적용되었는지 확인해봅시다.<pre><code class="language-sh">$ npm run start:dev</code></pre>
</li>
</ul>
<p>실행하면 TypeORM이 MySQL Database에 연결하고 Table을 생성하기 위한 Query문을 실행하고 NestJS Server가 실행됩니다. 그리고 아래 사진과 같이 <code>users</code> 라는 Table이 된 것을 확인할 수 있습니다.</p>
<p><img src="https://images.velog.io/images/1yongs_/post/8de1c253-6431-4299-a005-d1cbc011b957/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS Auth - Intro]]></title>
            <link>https://velog.io/@1yongs_/NestJS-Auth-Intro</link>
            <guid>https://velog.io/@1yongs_/NestJS-Auth-Intro</guid>
            <pubDate>Sat, 13 Feb 2021 17:47:16 GMT</pubDate>
            <description><![CDATA[<p><strong>인증</strong>은 대부분의 애플리케이션에서 필수적인 부분입니다. 인증을 다루는 방식과 전략은 다양합니다. </p>
<p>이 예제에서는 <a href="https://jwt.io/">Json Web Token</a>을 이용한 토큰 기반 인증을 사용할 것 입니다.</p>
<h2 id="index">Index</h2>
<ul>
<li><input disabled="" type="checkbox"> Authentication 이란?</li>
<li><input disabled="" type="checkbox"> Authorization 이란?</li>
<li><input disabled="" type="checkbox"> NestJS Guard 란?</li>
</ul>
<h2 id="authentication-이란">Authentication 이란?</h2>
<p>Authentication(인증)은 사용자의 신원을 증명 하는 프로세스입니다. 이 예제에서는 ID/PW로 서비스의 사용자를 증명하고, 서버에서 받은 JSON Web Token을 가지고 필요한 리소스에 접근하여 인증을 합니다.</p>
<ul>
<li>네이버 로그인 예제
<img src="https://images.velog.io/images/1yongs_/post/3d944a53-6864-4ecb-b41b-d246b3506bb4/image.png" alt="네이버 로그인"></li>
</ul>
<h2 id="authorization-이란">Authorization 이란?</h2>
<p>Authorization(<em>인가</em> 또는 <em>권한 부여</em>)란 인증된 주체에게 작업을 수행할 수 있는 사용 권한을 부여하는 작업입니다. 액세스가 허용된 데이터 및 해당 데이터로 할 수 있는 작업을 지정합니다. </p>
<p>사용자를 추방하거나, 특정 데이터(게시글)에 대한 CRUD 권한을 부여 및 지정하는 작업입니다.</p>
<ul>
<li>네이버 카페 글쓰기 등급제한 예제
<img src="https://images.velog.io/images/1yongs_/post/9e1c0d2e-6b16-4a95-ab55-e6003b23bcd0/image.png" alt="네이버 카페 글쓰기 등급제한 예제"></li>
</ul>
<h2 id="nestjs-guard-란">NestJS Guard 란?</h2>
<p>NestJS의 <a href="https://docs.nestjs.com/guards"><strong>Guard</strong></a>는 run-time 중에 주어진 요청을 특정 조건(허가, 역할, ACLs 등)에 따라 <code>route handler</code>가 허가할지 안할지를 결정합니다.</p>
<p>일반적인 Express Application에서 인증은 <strong>미들웨어</strong>에서 처리되었습니다. 미들웨어는 토큰 유효성 검사 및 <code>request</code> 객체에 속성을 붙이는 것 등이 특정 route context(그리고 그것의 metadata)와 강하게 연결되지 않기 때문에, 미들웨어는 인증에서 괜찮은 선택입니다.</p>
<p>하지만 미들웨어는 <code>next()</code> 함수가 불린 후 어떤 handler가 실행되었는지 모릅니다. 하지만 <code>Guard</code>는 <code>ExecutionContext</code> instance로 접근하여 다음에 실행될 것이 무엇인지 정확히 알고있습니다. 이는 코드를 <a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself"><strong>DRY</strong></a>원칙과 <strong>선언적</strong>으로 만드는데 도움을 줍니다.</p>
<p>이 예제에서 <code>ExecutionContext</code>를 다룰때 다시 한번 언급하겠습니다.</p>
<blockquote>
<h3 id="reference">Reference</h3>
<p><a href="https://docs.nestjs.com/guards">NestJS Documents - Guards</a>
<a href="https://jwt.io/">JSON Web Tokens</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis + Socket.IO + NestJS Chat App]]></title>
            <link>https://velog.io/@1yongs_/Redis-Clustering-NestJS-Chat-App</link>
            <guid>https://velog.io/@1yongs_/Redis-Clustering-NestJS-Chat-App</guid>
            <pubDate>Fri, 05 Feb 2021 09:03:57 GMT</pubDate>
            <description><![CDATA[<p>최근 회사 프로젝트를 진행하면서 Chatting 모듈을 만들어야했는데</p>
<p>운영하는 서버는 사용량에 따라 서버의 대수를 늘리는 Scale Out을 하여 능력을 향상시킵니다. 이로 인해 서버가 여러대로 늘어감에 따라 소켓 서버들간에 데이터를 주고받을 수 있는 중간 서버가 필요해졌습니다.</p>
<p>해결책으로 <code>Redis</code>의 <code>PUB/SUB</code> 기능을 사용하여 소켓간의 메시지를 관리합니다.</p>
<p>서버는 Node.js 프레임워크 <code>NestJS</code>를 사용하고, 채팅에 필요한 소켓은 <code>Socket.io</code> 라이브러리를 사용합니다.</p>
<p>이는 나중에 Redis와 Socket를 Adapter Pattern으로 적용하기 편리해지기 때문입니다. 그리고 각 서버들은 <code>Docker</code>를 이용하여 실행합니다.</p>
<br>

<h2 id="📋-todo">📋 TODO</h2>
<ul>
<li><input checked="" disabled="" type="checkbox"> Set up Docker</li>
<li><input checked="" disabled="" type="checkbox"> Set up Chatting Application</li>
<li><input checked="" disabled="" type="checkbox"> Apply Websocket Adapter</li>
<li><input checked="" disabled="" type="checkbox"> Test</li>
</ul>
<br>

<h2 id="1-set-up-docker">1. Set up Docker</h2>
<p><code>Docker-Compose</code> 로 Redis 환경을 구성합니다.</p>
<pre><code class="language-yml">// docker-compose.yml

version: &#39;3&#39;

services:
  redis:
    image: redis
    restart: always
    container_name: ilyong-redis
    ports:
      - 10300:6379</code></pre>
<p><code>$ docker-compose up -d</code> 명령어를 사용하여 Redis Docker Image를 실행합니다.</p>
<pre><code class="language-powershell">$ docker-compose up -d
Creating network &quot;nestjs-socketio_default&quot; with the default driver
Pulling redis (redis:)...
latest: Pulling from library/redis
a076a628af6f: Pull complete
f40dd07fe7be: Pull complete
ce21c8a3dbee: Pull complete
ee99c35818f8: Pull complete
56b9a72e68ff: Pull complete
3f703e7f380f: Pull complete
Digest: sha256:0f97c1c9daf5b69b93390ccbe8d3e2971617ec4801fd0882c72bf7cad3a13494
Status: Downloaded newer image for redis:latest
Creating ilyong-redis ... done</code></pre>
<p><code>$ docker ps</code> 로 상태를 확인합니다.</p>
<pre><code class="language-sh">$ docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS                     NAMES
959e05e10871   redis     &quot;docker-entrypoint.s…&quot;   7 seconds ago   Up 6 seconds   0.0.0.0:10300-&gt;6379/tcp   ilyong-redis</code></pre>
<br>

<h2 id="2-set-up-chatting-application">2. Set up Chatting Application</h2>
<h3 id="generate-nestjs">Generate NestJS</h3>
<pre><code class="language-sh">$ nest new nestjs-socketio
$ nest generate module chat
$ nest generate gateway chat chat</code></pre>
<h3 id="install-package">Install package</h3>
<ul>
<li><p>NestJS WebSocket 관련 package를 설치해줍니다.</p>
<pre><code class="language-sh">$ npm i --save @nestjs/websockets @nestjs/platform-socket.io
$ npm i --save-dev @types/socket.io</code></pre>
</li>
</ul>
<h3 id="setting-up-chatting-application">Setting Up Chatting Application</h3>
<ul>
<li>환경 변수를 사용하기위해 <code>@nestjs/config</code> package를 설치해줍니다.</li>
</ul>
<pre><code class="language-sh">$ npm i --save @nestjs/config joi</code></pre>
<pre><code class="language-ts">// src/app.module.ts

@Module({
imports: [
  ConfigModule.forRoot({
    envFilePath: `.env`,
    validationSchema: Joi.object({
      NODE_PORT: Joi.string().required(),
      REDIS_PORT: Joi.string().required(),
      REDIS_HOST: Joi.string().required(),
    }),
  }),
  ChatModule,
],
controllers: [AppController],
providers: [AppService],
})</code></pre>
<ul>
<li>Gateway를 사용하여 WebSocket 통신 모듈을 구현합니다.</li>
</ul>
<pre><code class="language-ts">// src/chat.gateway.ts

import { Logger } from &#39;@nestjs/common&#39;;
import {
  OnGatewayConnection,
  OnGatewayDisconnect,
  OnGatewayInit,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from &#39;@nestjs/websockets&#39;;
import { Server, Socket } from &#39;socket.io&#39;;

@WebSocketGateway({ namespace: &#39;chat&#39; }) // namespace는 optional 입니다!
export class ChatGateway
  implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
  private static readonly logger = new Logger(ChatGateway.name);

  @WebSocketServer()
  server: Server;

  afterInit() {
    ChatGateway.logger.debug(`Socket Server Init Complete`);
  }

  handleConnection(client: Socket) {
    ChatGateway.logger.debug(
      `${client.id}(${client.handshake.query[&#39;username&#39;]}) is connected!`,
    );

    this.server.emit(&#39;msgToClient&#39;, {
      name: `admin`,
      text: `join chat.`,
    });
  }

  handleDisconnect(client: Socket) {
    ChatGateway.logger.debug(`${client.id} is disconnected...`);
  }

  @SubscribeMessage(&#39;msgToServer&#39;)
  handleMessage(
    client: Socket,
    payload: { name: string; text: string },
  ): void {
    this.server.emit(&#39;msgToClient&#39;, payload);
  }
}</code></pre>
<ul>
<li>NestJS 서버에서 정적 파일을 사용하도록 설정하고, VueJS로 간단히 만든 채팅 페이지를 불러오도록 합니다.</li>
</ul>
<pre><code class="language-ts">// src/main.ts

async function bootstrap() {
  const app = await NestFactory.create&lt;NestExpressApplication&gt;(AppModule);
  app.useStaticAssets(join(__dirname, &#39;..&#39;, &#39;static&#39;));
  await app.listen(process.env.NODE_PORT);
}
bootstrap();</code></pre>
<pre><code class="language-html">&lt;!-- assets/3000/index.html --&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot; /&gt;
    &lt;link
      rel=&quot;stylesheet&quot;
      href=&quot;https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css&quot;
      integrity=&quot;sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ&quot;
      crossorigin=&quot;anonymous&quot;
    /&gt;
    &lt;title&gt;Nestjs SocketIO&lt;/title&gt;
    &lt;link rel=&quot;stylesheet&quot; href=&quot;styles.css&quot; /&gt;
    &lt;script src=&quot;https://cdn.jsdelivr.net/npm/vue/dist/vue.js&quot;&gt;&lt;/script&gt;
    &lt;script
      type=&quot;text/javascript&quot;
      src=&quot;https://cdn.socket.io/socket.io-1.4.5.js&quot;
    &gt;&lt;/script&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id=&quot;app&quot; class=&quot;container&quot;&gt;
      &lt;div class=&quot;row&quot;&gt;
        &lt;div class=&quot;col-md-6 offset-md-3 col-sm-12&quot;&gt;
          &lt;h1 class=&quot;text-center&quot;&gt;{{ title }}&lt;/h1&gt;
          &lt;br /&gt;
          &lt;div id=&quot;status&quot;&gt;&lt;/div&gt;
          &lt;div id=&quot;chat&quot;&gt;
            &lt;input
              type=&quot;text&quot;
              v-model=&quot;name&quot;
              id=&quot;username&quot;
              class=&quot;form-control&quot;
              placeholder=&quot;Enter name...&quot;
            /&gt;
            &lt;br /&gt;
            &lt;div class=&quot;card&quot;&gt;
              &lt;div id=&quot;messages&quot; class=&quot;card-block&quot;&gt;
                &lt;ul&gt;
                  &lt;li v-for=&quot;message of messages&quot;&gt;
                    {{ message.name }}: {{ message.text }}
                  &lt;/li&gt;
                &lt;/ul&gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;br /&gt;
            &lt;textarea
              id=&quot;textarea&quot;
              class=&quot;form-control&quot;
              v-model=&quot;text&quot;
              placeholder=&quot;Enter message...&quot;
            &gt;&lt;/textarea&gt;
            &lt;br /&gt;
            &lt;button id=&quot;send&quot; class=&quot;btn&quot; @click.prevent=&quot;sendMessage&quot;&gt;
              Send
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;script src=&quot;main.js&quot;&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<pre><code class="language-js">// assets/3000/main.js

const app = new Vue({
  el: &#39;#app&#39;,
  data: {
    title: &#39;Nestjs Websockets Chat&#39;,
    name: &#39;&#39;,
    text: &#39;&#39;,
    messages: [],
    socket: null,
  },
  methods: {
    sendMessage() {
      if (this.validateInput()) {
        const message = {
          name: this.name,
          text: this.text,
        };
        this.socket.emit(&#39;msgToServer&#39;, message);
        this.text = &#39;&#39;;
      }
    },
    receivedMessage(message) {
      this.messages.push(message);
    },
    validateInput() {
      return this.name.length &gt; 0 &amp;&amp; this.text.length &gt; 0;
    },
  },
  created() {
    this.socket = io(&#39;http://localhost:3000/chat&#39;); // assets/3001 폴더에서 3001 포트로 수정해줍니다.
    this.socket.on(&#39;msgToClient&#39;, (message) =&gt; {
      this.receivedMessage(message);
    });
  },
});</code></pre>
<pre><code class="language-css">&lt;!-- assets/3000/style.css -- &gt; #messages {
  height: 300px;
  overflow-y: scroll;
}

#app {
  margin-top: 2rem;
}</code></pre>
<p>  <img src="https://github.com/dlfdyd96/nestjs-redis-socketio/blob/master/images/chat1.png?raw=true" alt="chat1"></p>
<blockquote>
<p><strong>참고</strong> 💡</p>
<p>VueJS 정적 페이지가 있는 <code>assets</code>에서 <code>3000</code>폴더와 <code>3001</code>폴더를 따로 만들어 준 이유는, 서로 다른 port에서 적용하는 모습을 보여주기 위함입니다.</p>
<p>현재는 <code>3000</code> 포트로 연결된 기본적인 소켓 통신 모습입니다.</p>
</blockquote>
<br>

<h2 id="3-apply-websocket-adapter">3. Apply Websocket Adapter</h2>
<h3 id="redis-pubsub">Redis PUB/SUB</h3>
<p>같은 서버가 Scale Out 하여 서버대수가 늘어나면 연결된 socket들도 분리가 됩니다. 이 때 Socket들을 관리해줄 중간 서버가 필요한데, 이는 Redis의 <code>PUB/SUB</code> 기능을 사용하여 관리합니다.</p>
<ul>
<li><p>NestJS 서버의 <a href="https://docs.nestjs.com/websockets/adapter">WebSocket Adapter</a>를 적용합시다.</p>
<p>WebSocket 모듈은 <code>platform-agnostic</code> 입니다.</p>
<blockquote>
<p><strong>platform-agnostic 이란?</strong></p>
<p>작동 시스템에 대한 아무런 지식이 없더라도 기능을 수행할 수 있도록 하는 기술.
예를 들어, 플랫폼 애그노스틱(platform-agnostic) 소프트웨어 기술은 어떠한 운영 체제나 프로세서의 조합인지에 대한 아무런 지식이 없더라도 상관없이 기능을 수행할 수 있는 소프트웨어 기술을 의미한다.</p>
<p>(<a href="http://www.tta.or.kr/data/weeklyNoticeView.jsp?pk_num=5179">한국정보통신기술협회-애그노스틱기술</a>)</p>
</blockquote>
<p>따라서 사용자의 라이브러리나 소스를 컴파일하는 데 필요한 종속성들을 <code>WebSocketAdpater</code> 인터페이스를 만들어 사용함으로써 같이 사용할 수 있습니다.</p>
<p>NestJS의 <code>socket.io</code> package는 <code>IoAapter</code> 클래스안에 래핑되었습니다. 현재 개발할려는 채팅 어플리케이션은 인스턴스가 가변적인 예제이기 때문에, <code>IoAdapter</code>를 상속받고 새로운 socket.io 서버를 인스턴스로 만드는 <code>createIoServer</code> method를 override 할 수 있습니다.</p>
</li>
<li><p>그전에 필요한 package를 설치합시다.</p>
</li>
</ul>
<pre><code class="language-sh">$ npm i --save socket.io-redis</code></pre>
<ul>
<li>설치를 완료했다면 <code>RedisIoAdapter</code> Class를 만듭시다.</li>
</ul>
<pre><code class="language-ts">// src/chat/redis.adapter.ts

export class RedisIoAdapter extends IoAdapter {
  createIOServer(port: number, options?: ServerOptions): any {
    const server = super.createIOServer(port, options);
    const redisAdapter = redisIoAdapter({
      host: process.env.REDIS_HOST,
      port: process.env.REDIS_PORT,
    });

    server.adapter(redisAdapter);
    return server;
  }
}</code></pre>
<ul>
<li>그 후에, <code>userWebSocketAdapter()</code> method로 새롭게 만들어진 Redis adapter를 적용합시다.</li>
</ul>
<pre><code class="language-ts">// src/main.ts
// ...
const app = await NestFactory.create(ApplicationModule);
app.useWebSocketAdapter(new RedisIoAdapter(app));
// ...</code></pre>
<blockquote>
<p><strong>참고</strong> 💡</p>
<p><code>socket.io-redis</code>는 <code>Socket.IO</code> server version과 대응해줘야합니다. 현재 예제에서, <code>socket.io-redis</code>의 version은 <code>5.4.0</code>을, <code>socket.io</code> version는 package-lock.json을 확인한 결과, <code>@nestjs/platform-socket.io</code> package를 설치하면 <code>2.4.1</code> version의 <code>socket.io</code>가 설치됩니다.</p>
<p><a href="https://github.com/socketio/socket.io-redis#compatibility-table">참고사이트</a></p>
</blockquote>
<br>

<h2 id="4-test">4. Test</h2>
<ul>
<li><p>NestJS Server를 각각 3000, 3001 server로 실행합니다.</p>
<ul>
<li>scripts에서 환경변수 세팅을 위해 <code>cross-env</code> package를 설치해줍니다. (<a href="https://www.npmjs.com/package/cross-env">cross-env</a>)</li>
</ul>
</li>
</ul>
<pre><code class="language-sh">$ npm i cross-env</code></pre>
<ul>
<li><p><code>assets</code>에서도 3000, 3001 로 구분하여 만들어 줍니다</p>
<ul>
<li>서로 다른 포트에서 소켓이 통신을 확인하기 위함입니다.</li>
<li>자세한 내용은 <a href="https://github.com/dlfdyd96/nestjs-redis-socketio">Github Repository</a> 의 <code>assets</code> directory에서 확인할 수 있습니다.</li>
</ul>
</li>
<li><p>package.json에서 scripts를 추가해줍니다.</p>
</li>
</ul>
<pre><code class="language-json">&quot;start:3000&quot;: &quot;cross-env NODE_PORT=3000 nest start&quot;,
&quot;start:3001&quot;: &quot;cross-env NODE_PORT=3001 nest start&quot;,</code></pre>
<ul>
<li>서로 다른 포트에서 채팅이 통신이 되는지 확인합니다.
<img src="https://github.com/dlfdyd96/nestjs-redis-socketio/blob/master/images/chat2.png?raw=true" alt="chat2">
ㄴ 채팅 app page
<img src="https://github.com/dlfdyd96/nestjs-redis-socketio/blob/master/images/chat3.png?raw=true" alt="chat3">
ㄴ NestJS의 다른 port에서 실행한 모습</li>
</ul>
<br>

<h2 id="references">References</h2>
<ul>
<li><a href="https://github.com/dlfdyd96/nestjs-redis-socketio">Project Github Repository</a></li>
<li><a href="https://docs.nestjs.com/websockets/gateways">NestJS - Gateway</a></li>
<li><a href="https://docs.nestjs.com/websockets/adapter">NestJS - WebSockets Adapter</a></li>
<li><a href="https://github.com/socketio/socket.io-redis#compatibility-table">socket.io-redis - compatibility table</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS 메일 발송 Dynamic Module]]></title>
            <link>https://velog.io/@1yongs_/NestJS-%EB%A9%94%EC%9D%BC-%EB%B0%9C%EC%86%A1-Dynamic-Module</link>
            <guid>https://velog.io/@1yongs_/NestJS-%EB%A9%94%EC%9D%BC-%EB%B0%9C%EC%86%A1-Dynamic-Module</guid>
            <pubDate>Thu, 04 Feb 2021 02:58:17 GMT</pubDate>
            <description><![CDATA[<p>NestJS Dynamic Module을 이용하여 Naver Cloud Platform 메일 발송 📨 서비스 모듈 예제를 만들어 봅니다.</p>
<h2 id="index">Index</h2>
<ul>
<li>Dynamic Module 이란,</li>
<li>설정</li>
<li>코드</li>
<li>느낀점</li>
</ul>
<hr>

<h2 id="dynamic-module-이란">Dynamic Module 이란</h2>
<h3 id="module이란">Module이란</h3>
<p>NestJS에서 <code>Module</code>은 애플리케이션 구조를 구성(organize)하는데 사용됩니다.
NestJS의 모듈은 기본적으로 싱글톤 패턴입니다. 같은 프로바이더 인스턴스(Service, Gateway, ...)를 많은 모듈에서 쉽게 공유되어 사용할 수 있습니다.
NestJS의 <code>Module</code>은 전체 Application의 모듈형 부분으로, 적합한 Providers 및 Controllers와 같은 구성요소 그룹을 정의합니다.
NestJS 모듈은 1) 정적 모듈과 2) 동적 모듈 두 가지로 분류됩니다.</p>
<br>

<h3 id="정적모듈">정적모듈</h3>
<p>정적 모듈은 NestJS가 필요한 모든 정보를 미리 호스트 및 consuming 모듈에서 선언하여 사용합니다.</p>
<br>

<h3 id="동적모듈">동적모듈</h3>
<p>동적 모듈은 모듈 등록과 Provider를 동적으로 설정이 가능한 커스텀 가능한 모듈을 쉽게 만들 수 있게 합니다.</p>
<p>정적 모듈 바인딩에서 불가능했던 상황들이 있습니다. 개발환경에 맞춰 서버 포트번호를 다르게 부여해야하는 등 다양한 상황에서 동적 바인딩은 말그대로 <strong>동적</strong>으로 상황에 맞게 Module을 커스텀할 수 있게 해줍니다.</p>
<hr>

<h2 id="example">Example</h2>
<p>우리의 예제에서는 메일 발송을 위한 모듈을 만들어 볼 것 입니다. API Key를 발급받아 환경변수에 설정하고, 이에 맞춰 Module을 동적으로 설정해봅시다.</p>
<h3 id="todo-list-📋">TODO List 📋</h3>
<ul>
<li><input checked="" disabled="" type="checkbox"> Naver Cloud Platform - Cloud Outbound Mailer API 발급</li>
<li><input checked="" disabled="" type="checkbox"> Set up NestJS Mail Module<ul>
<li><input checked="" disabled="" type="checkbox"> install package</li>
<li><input checked="" disabled="" type="checkbox"> Configure Enviornment Variables</li>
<li><input checked="" disabled="" type="checkbox"> Make Dynamic Module</li>
<li><input checked="" disabled="" type="checkbox"> Mail Service</li>
</ul>
</li>
<li><input checked="" disabled="" type="checkbox"> Test</li>
<li><input checked="" disabled="" type="checkbox"> 느낀점</li>
</ul>
<br>

<h4 id="1-naver-cloud-platform---cloud-outbound-mailer-api-발급">1. Naver Cloud Platform - Cloud Outbound Mailer API 발급</h4>
<ul>
<li>Naver Cloud Platform에 접속하여 Cloud Out bound Mailer 이용신청을 합니다.
<img src="https://github.com/dlfdyd96/send-grid/blob/master/images/ncp.png?raw=true" alt="ncp"></li>
<li><code>마이페이지</code> - <code>인증키 관리</code> 로 API 인증키를 발급합니다.
<img src="https://github.com/dlfdyd96/send-grid/blob/master/images/ncp2.png?raw=true" alt="ncp2"></li>
</ul>
<h4 id="2set-up-nestjs-mail-module">2.Set up NestJS Mail Module</h4>
<ul>
<li><code>@nestjs/cli</code> 페키지를 설치하여 프로젝트를 생성해줍니다.<pre><code class="language-sh"> $ npm i -g @nestjs/cli
 $ nest new mail-service-project
 $ npm i</code></pre>
</li>
<li>Mail module 하나를 생성해줍니다.<pre><code class="language-sh">$ nest generate module mail</code></pre>
</li>
</ul>
<h4 id="3install-package">3.install package</h4>
<ul>
<li>Cloud Outbound Mailer API 서비스는 RESTful 형태로 제공되기 때문에, <code>axios</code> package를 설치하여 api를 요청해봅니다.<pre><code class="language-sh">$ npm i axios # 다른 http package를 써도 됩니다.</code></pre>
</li>
<li>또한 발급받은 API키를 환경변수로 사용하기 위해 <code>ConfigModule</code>를 설치하고, <code>Joi</code> data validator를 사용합니다. (<a href="https://www.npmjs.com/package/joi">Joi</a>는 Config와 써보고 싶었는데, 이번에 같이 사용해보겠습니다.)<pre><code class="language-sh">$ npm i --save @nestjs/config
$ npm i joi</code></pre>
</li>
</ul>
<h4 id="4configure-enviornment-variables">4.Configure Enviornment Variables</h4>
<ul>
<li>환경 변수를 설정해줄 <code>.env</code> 파일을 <code>package.json</code> 파일이 있는 디렉토리에 생성해줍니다.</li>
</ul>
<pre><code class="language-env">  # .env

  # NAVER CLOUD PLATFORM
  ACCESS_KEY_ID=퍼블릭키
  SECRET_KEY=쉿!비밀키ㅎ
  SENDER_ADDRESS=1yongs_@naver.com
  MAIL_API_DOMAIN=https://mail.apigw.ntruss.com/</code></pre>
<ul>
<li><code>app.module.ts</code>에 <code>ConfigModule</code>을 <code>.forRoot()</code> static method를 통해 root로 import 해줍니다. (<a href="https://docs.nestjs.com/techniques/configuration#service">공식문서</a>)</li>
</ul>
<pre><code class="language-ts">  // src/app.module.ts
  import { Module } from &#39;@nestjs/common&#39;;
  import { ConfigModule } from &#39;@nestjs/config&#39;;
  import { AppController } from &#39;./app.controller&#39;;
  import { AppService } from &#39;./app.service&#39;;
  import * as Joi from &#39;joi&#39;;

  @Module({
    imports: [
      ConfigModule.forRoot({
        isGlobal: true,
        envFilePath: `.env`,
        validationSchema: Joi.object({
          ACCESS_KEY_ID: Joi.string().required(),
          SECRET_KEY: Joi.string().required(),
          SENDER_ADDRESS: Joi.string().required(),
          MAIL_API_DOMAIN: Joi.string().required(),
        }),
      }),
    ],
    controllers: [AppController],
    providers: [AppService],
  })
  export class AppModule {}</code></pre>
<h4 id="5-make-dynamic-module">5. Make Dynamic Module</h4>
<p>다음으로 Dynamic Module을 만들어 봅시다.</p>
<ul>
<li>Module을 만들기 전에, Mail Module을 Dynamic하게 만들어 주게해 줄 Module Option을 정의해봅시다. (<a href="https://docs.nestjs.com/fundamentals/custom-providers">Custom Provider</a>는 시간이 되면 리뷰를 하겠습니다.)</li>
</ul>
<pre><code class="language-ts">  // src/common/common.constants.ts

  export const CONFIG_OPTIONS = &#39;CONFIG_OPTIONS&#39;;</code></pre>
<p>  <code>CONFIG_OPTIONS</code>변수는 목적(모듈 설정 옵션이라는..)을 상술하기 위한 DI 토큰으로 쓰기위해 상수로 정의하였습니다.</p>
<pre><code class="language-ts">  // src/mail/mail.interface.ts

  export interface MailModuleOptions {
    apiKey: string; // 네이버 클라우드 플랫폼 포털에서 발급받은 Access Key ID 값
    secret: string; // Access Key ID 값 과 Secret Key 로 암호화한 서명
    senderAddress: string; // 발송자 Email 주소. 임의의 도메인 주소 사용하셔도 됩니다만, 가능하면 발신자 소유의 도메인 Email 계정을 사용하실 것을 권고드립니다.
    language: string; // API 응답 값의 다국어 처리를 위한 값. (입력 값 예시: ko-KR, en-US, zh-CN, 기본 값:en-US)
  }</code></pre>
<p>  <code>MailModuleOptions</code> 인터페이스를 만들어 사용할 옵션의 틀을 만들어줍니다.</p>
<br>

<ul>
<li>MailModule</li>
</ul>
<pre><code class="language-ts">  // src/mail/mail.module.ts

  import { DynamicModule, Module } from &#39;@nestjs/common&#39;;
  import { CONFIG_OPTIONS } from &#39;src/common/common.constants&#39;;
  import { MailModuleOptions } from &#39;./mail.interface&#39;;

  @Module({})
  export class MailModule {
    static forRoot(options: MailModuleOptions): DynamicModule {
      return {
        module: MailModule,
        providers: [
          {
            provide: CONFIG_OPTIONS,
            useValue: options,
          },
        ],
        exports: [],
      };
    }
  }</code></pre>
<p>  드디어 대망의 <code>MailModule</code> 입니다.</p>
<ol>
<li><p>MailModule을 Dynamic Module로 만들어 주기 위해 <code>DynamicModule</code> static method 인 <code>forRoot</code>를 정의 해줍니다.</p>
</li>
<li><p><code>forRoot</code>에 옵션을 주기위해 <code>options</code>를 매개변수로 주고,</p>
</li>
<li><p>DynamicModule을 return 해주는데, <code>providers</code>를 유심히 살펴봅시다.</p>
</li>
<li><p>옵션 값(<code>options</code>)을 Depedency Inject하기 위해 <code>CONFIG_OPTIONS</code>을 <code>provide</code>에 DI token 값으로 주고, 매개변수 <code>options</code>를 useValue에 주면 Dynamic Module를 정의할 수 있습니다.</p>
<br>

</li>
</ol>
<ul>
<li>AppModule에 DynamicModule을 import해줍시다!<pre><code class="language-ts">// src/app.module.ts
  //...
  MailModule.forRoot({
    apiKey: process.env.ACCESS_KEY_ID,
    secret: process.env.SECRET_KEY,
    senderAddress: process.env.SENDER_ADDRESS,
    language: &#39;ko-KR&#39;, // 한국어
  }),
  //...</code></pre>
</li>
</ul>
<h4 id="6-make-mail-service">6. Make Mail Service</h4>
<ul>
<li>Service를 generate 해줍시다.<pre><code class="language-sh">$ nest generate service mail # windows는 npx nest generate service mail</code></pre>
</li>
<li>DTO들을 만들어 줍니다.
메일을 발송하기 위해 Request / Response DTO를 만들어 줍니다.</li>
</ul>
<pre><code class="language-ts">  // src/mail/dto/send-email.dto.ts

  import { Type } from &#39;class-transformer&#39;;
  import { IsOptional, IsString, ValidateNested } from &#39;class-validator&#39;;
  import { CommonResponseDto } from &#39;src/common/dto/common.dto&#39;;

  export class Recipients {
    @IsString()
    address: string;
    @IsString()
    name: string;
    @IsString()
    type: string;
  }

  export class SendEmailRequestDto {
    @IsString()
    senderName: string;
    @IsString()
    title: string;
    @IsString()
    body: string;
    @ValidateNested({ each: true })
    @Type(() =&gt; Recipients)
    recipients: Recipients[];
  }

  export class SendEmailResponseDto extends CommonResponseDto {
    @IsOptional()
    @IsString()
    requestId?: string;
    @IsOptional()
    @IsString()
    count?: number;
  }</code></pre>
  <br>

<pre><code class="language-ts">  // src/common/dto/common.dto.ts
  import { IsBoolean, IsOptional, IsString } from &#39;class-validator&#39;;

  export class CommonResponseDto {
    @IsBoolean()
    status: boolean;
    @IsOptional()
    @IsString()
    error?: string;
    @IsOptional()
    @IsString()
    message?: string;
  }</code></pre>
<ul>
<li><p>MailService</p>
<p>메일을 발송하는데 api url로 POST 방식으로 전송합니다. 이때 headers에는 Cloud Outbound Mailer에 기재된 내용들을 넣어주고, 필요한 Body Data를 넣어 전송합니다.</p>
<p><code>x-ncp-apigw-signature-v2</code>는 Secret Key로 HmacSHA256 알고리즘으로 암호화한 후 Base64로 인코딩하여 담아 줍니다.</p>
</li>
</ul>
<pre><code class="language-ts">  import { Inject, Injectable } from &#39;@nestjs/common&#39;;
  import axios from &#39;axios&#39;;
  import { createHmac } from &#39;crypto&#39;;
  import { CONFIG_OPTIONS } from &#39;src/common/common.constants&#39;;
  import {
    SendEmailRequestDto,
    SendEmailResponseDto,
  } from &#39;./dto/send-email.dto&#39;;
  import { MailModuleOptions } from &#39;./mail.interface&#39;;

  @Injectable()
  export class MailService {
    constructor(
      @Inject(CONFIG_OPTIONS) private readonly options: MailModuleOptions,
    ) {}

    async sendEmail(
      reqData: SendEmailRequestDto,
    ): Promise&lt;SendEmailResponseDto&gt; {
      const url = `/api/v1/mails`;
      const method = `POST`;
      try {
        const { data } = await axios.post&lt;{ requestId: string; count: number }&gt;(
          `${process.env.MAIL_API_DOMAIN}${url}`,
          {
            senderAddress: this.options.senderAddress,
            ...reqData,
          },
          {
            headers: {
              &#39;Content-Type&#39;: &#39;application/json&#39;,
              &#39;x-ncp-apigw-timestamp&#39;: new Date().getTime().toString(10),
              &#39;x-ncp-iam-access-key&#39;: this.options.apiKey,
              &#39;x-ncp-apigw-signature-v2&#39;: this.makeSignature(
                method,
                url,
                new Date().getTime().toString(),
                this.options.apiKey,
                this.options.secret,
              ),
              &#39;x-ncp-lang&#39;: this.options.language,
            },
          },
        );

        return {
          ...data,
          status: true,
        };
      } catch (error) {
        console.log(error);
        return {
          status: false,
          error: error.response.data,
          message: `메일 발송에 실패하였습니다.`,
        };
      }
    }

    private makeSignature(
      method: string,
      url: string,
      timestamp: string,
      accessKey: string,
      secretKey: string,
    ): string {
      const space = &#39; &#39;; // 공백
      const newLine = &#39;\n&#39;; // 줄바꿈

      const hmac = createHmac(&#39;sha256&#39;, secretKey);

      hmac.write(method);
      hmac.write(space);
      hmac.write(url);
      hmac.write(newLine);
      hmac.write(timestamp);
      hmac.write(newLine);
      hmac.write(accessKey);

      hmac.end();

      return Buffer.from(hmac.read()).toString(&#39;base64&#39;);
    }
  }</code></pre>
<h4 id="7-test">7. Test</h4>
<ul>
<li><p>AppController</p>
<p>메일 발송을 위한 endpoint를 <code>app.controller.ts</code>에 열어줍시다.</p>
</li>
</ul>
<pre><code class="language-ts">// src/app.controller.ts

@Controller()
export class AppController {
  constructor(private readonly mailService: MailService) {}

  @Post(&#39;/mail&#39;)
  sendToClient(
  @Body() reqData: SendEmailRequestDto,
  ): Promise&lt;SendEmailResponseDto&gt; {
    return this.mailService.sendEmail(reqData);
  }
}</code></pre>
<ul>
<li>Request</li>
</ul>
<pre><code class="language-ts">// request url
&quot;http://localhost:3000/mail&quot;

// post request body
{
  &quot;senderName&quot;: &quot;황 일용&quot;,
    &quot;title&quot;: &quot;안녕하세요 테스트 메일입니다.&quot;,
      &quot;body&quot;: &quot;안녕하세요 Naver Cloud Platform - Cloud Outbound mailer 서비스 테스트 메일 입니다. &quot;,
        &quot;recipients&quot;: [
          {
            &quot;address&quot;: &quot;iyhwang@hnmcorp.kr&quot;,
            &quot;name&quot;: &quot;황일용&quot;,
            &quot;type&quot;: &quot;R&quot;
          }
        ]
}</code></pre>
<ul>
<li>Response</li>
</ul>
<pre><code class="language-ts">// response
{
  &quot;requestId&quot;: &quot;20210203000054051502&quot;,
    &quot;count&quot;: 1,
      &quot;status&quot;: true
}</code></pre>
<p>  <img src="https://github.com/dlfdyd96/send-grid/blob/master/images/result.png?raw=true" alt="result"></p>
<br>

<h3 id="8-느낀점">8. 느낀점</h3>
<p>Dynamic Module를 적용해봄으로써 NestJS 프레임워크의 <code>IoC</code>, <code>DI</code>에 대한 이해에 도움이 많이 되었습니다. 다음번엔 <code>Custom Provider</code>를 리뷰를 해보도록 하겠습니다.</p>
<p>Custom Provider 부분에서 Factory function과 Dependency Injection에 대한 이야기를 깊이 있게 다뤄보겠습니다.</p>
<h3 id="reference">Reference</h3>
<ul>
<li><a href="https://github.com/dlfdyd96/send-grid">Github Repository</a></li>
<li><a href="https://www.ncloud.com/product/applicationService/cloudOutboundMailer">Naver Cloud Platform - Cloud Outbound Mailer</a></li>
<li><a href="https://docs.nestjs.com/fundamentals/dynamic-modules">NestJS Dynamic Module</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Factory Pattern]]></title>
            <link>https://velog.io/@1yongs_/Factory-Pattern</link>
            <guid>https://velog.io/@1yongs_/Factory-Pattern</guid>
            <pubDate>Wed, 13 Jan 2021 18:42:42 GMT</pubDate>
            <description><![CDATA[<h3 id="팩토리-메서드-패턴"><a href="https://ko.wikipedia.org/wiki/%ED%8C%A9%ED%86%A0%EB%A6%AC_%EB%A9%94%EC%84%9C%EB%93%9C_%ED%8C%A8%ED%84%B4">팩토리 메서드 패턴</a></h3>
<p><img src="https://images.velog.io/images/1yongs_/post/887fa200-7877-48fc-af83-4b6ebfd3de2d/image.png" alt=""></p>
<p>팩토리 메서드 패턴(Factory method pattern)은 객체지향 디자인 패턴이다. Factory method는 부모(상위) 클래스에 알려지지 않은 구체 클래스를 생성하는 패턴이며. 자식(하위) 클래스가 어떤 객체를 생성할지를 결정하도록 하는 패턴이기도 하다. 부모(상위) 클래스 코드에 구체 클래스 이름을 감추기 위한 방법으로도 사용한다.</p>
<p>Factory Method라는 패턴 이름이 적절하지 못한데, 이름으로 인해 객체를 생성하는 메소드를 Factory method라 오해하는 개발자가 많이 있다(Allen Holub의 말을 인용.) 이런 생성 메소드가 모두 Factory method 패턴을 사용하는 것은 아니다. Template Method의 생성 패턴 버전으로 볼 수 있는데 Template Method를 알지 못한다면 그 패턴을 먼저 이해하는 것이 Factory Method를 이해하기 수월할 것이다.</p>
<p>Factory Method가 중첩되기 시작하면 굉장히 복잡해 질 수 있다. 또한 상속을 사용하지만 부모(상위) 클래스를 전혀 확장하지 않는다. 따라서 이 패턴은 extends 관계를 잘못 이용한 것으로 볼 수 있다. extends 관계를 남발하게 되면 프로그램의 엔트로피가 높아질 수 있으므로 Factory Method 패턴의 사용을 주의해야 한다.</p>
<pre><code class="language-js">//Our pizzas
function HamAndMushroomPizza(){
  var price = 8.50;
  this.getPrice = function(){
    return price;
  }
}

function DeluxePizza(){
  var price = 10.50;
  this.getPrice = function(){
    return price;
  }
}

function SeafoodPizza(){
  var price = 11.50;
  this.getPrice = function(){
    return price;
  }
}

//Pizza Factory
function PizzaFactory(){
  this.createPizza = function(type){
     switch(type){
      case &quot;Ham and Mushroom&quot;:
        return new HamAndMushroomPizza();
      case &quot;DeluxePizza&quot;:
        return new DeluxePizza();
      case &quot;Seafood Pizza&quot;:
        return new SeafoodPizza();
      default:
          return new DeluxePizza();
     }
  }
}

//Usage
var pizzaPrice = new PizzaFactory().createPizza(&quot;Ham and Mushroom&quot;).getPrice();
alert(pizzaPrice);</code></pre>
<hr>

<p>NestJS의 Module에서 <strong>동적 Provider</strong>를 생성하기 위해 <code>useFactory</code> 문법을 통해 생성한다.</p>
<pre><code class="language-ts">const connectionFactory = {
  provide: &#39;CONNECTION&#39;,
  useFactory: (optionsProvider: OptionsProvider) =&gt; {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
};

@Module({
  providers: [connectionFactory],
})
export class AppModule {}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS Controller]]></title>
            <link>https://velog.io/@1yongs_/NestJS-Controller</link>
            <guid>https://velog.io/@1yongs_/NestJS-Controller</guid>
            <pubDate>Wed, 13 Jan 2021 18:34:14 GMT</pubDate>
            <description><![CDATA[<h2 id="controller">Controller</h2>
<p><strong>NestJS</strong>의 <strong>컨트롤러</strong>는 들어오는 Request를 처리하고 Client에게 Response를 반환하는 전형적인 <strong>MVC</strong> 모델의 <code>Controller</code> 역할을 하고 있습니다.
<img src="https://images.velog.io/images/1yongs_/post/e1d9629c-0a45-483b-a841-cc5afefabba3/image.png" alt=""></p>
<p>컨트롤러의 목적은 애플리케이션에 대한 특정 요청을 수신하는 것입니다. 라우팅 체계는 어느 Request를 받는 어느 컨트롤러를 제어합니다. 종종 각 컨트롤러에는 둘 이상의 경로가 있으며 다른 경로는 다른 작업을 수행 할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS Introduction]]></title>
            <link>https://velog.io/@1yongs_/NestJS-Introduction</link>
            <guid>https://velog.io/@1yongs_/NestJS-Introduction</guid>
            <pubDate>Wed, 13 Jan 2021 18:23:23 GMT</pubDate>
            <description><![CDATA[<h2 id="소개">소개</h2>
<blockquote>
<p><strong>Nest(NestJS)</strong> 는 효율적이고 확장 가능한 <code>Node.js</code> 서버 측 애플리케이션 을 구축하기위한 프레임 워크입니다 . 프로그레시브 JavaScript를 사용하고 <code>TypeScript</code>로 빌드되고 완전히 지원 되며 (하지만 여전히 개발자가 순수 JavaScript로 코딩 할 수 있음) <strong>OOP</strong> (Object Oriented Programming), <strong>FP</strong> (Functional Programming) 및 <strong>FRP</strong> (Functional Reactive Programming) 요소를 결합합니다.</p>
</blockquote>
<p><code>NestJS</code>는 프레임워크로써 IoC(Inversion of Control) 기술로 DI(Dependency Injection) 사용을 하기 때문에 <strong>OOP</strong>, <strong>FP</strong>, <strong>FRP</strong> 요소를 결합했다고 생각한다.</p>
<blockquote>
<p>내부적으로 Nest는 <strong>Express</strong> (기본값)와 같은 강력한 HTTP 서버 프레임 워크 를 사용하며 선택적으로 <strong>Fastify</strong> 를 사용하도록 구성 할 수도 있습니다 !</p>
</blockquote>
<p>기술적으로 Nest는 어댑터가 생성되면 모든 Node HTTP 프레임 워크와 함께 작동 할 수 있다고 한다. </p>
<pre><code class="language-ts">// main.ts
const app = await NestFactory.create&lt;NestExpressApplication&gt;(AppModule);
</code></pre>
<hr>

<p><code>IoC</code>, <code>DI</code>, <code>OOP</code>, <code>FP</code>, <code>FRP</code> 등 어려운 단어들이 많다. 추후에 사용예제를 통해 의미를 알아봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[BackEnd 공부 Velog 시작 ]]></title>
            <link>https://velog.io/@1yongs_/BackEnd-%EA%B3%B5%EB%B6%80-Velog-%EC%8B%9C%EC%9E%91</link>
            <guid>https://velog.io/@1yongs_/BackEnd-%EA%B3%B5%EB%B6%80-Velog-%EC%8B%9C%EC%9E%91</guid>
            <pubDate>Mon, 12 Oct 2020 06:18:52 GMT</pubDate>
            <description><![CDATA[<h1 id="끈기와-노력">끈기와 노력</h1>
<blockquote>
<p align="center">인생에서의 <strong>성공</strong>은 어떤 지위에 올랐느냐가 아니라,</p>
<p align="center">장애물을 극복하며 성공하려고 <strong>노력하는 과정</strong>에 있다</p>
<p align="center"><i>- 보커 T. 워싱턴 -</i></p>
</blockquote>
<hr>

]]></description>
        </item>
    </channel>
</rss>