<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>leejm_dev</title>
        <link>https://velog.io/</link>
        <description>자유로워지고 싶다면 기록하라.</description>
        <lastBuildDate>Tue, 02 Jan 2024 06:01:09 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>leejm_dev</title>
            <url>https://velog.velcdn.com/images/leejm_dev/profile/d07b052e-ff0f-4d42-bf60-3ee5647ae218/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. leejm_dev. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/leejm_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[providers instance 생성자에 주입된 객체 들을 건들면 안되는 이유]]></title>
            <link>https://velog.io/@leejm_dev/providers-instance-%EC%83%9D%EC%84%B1%EC%9E%90%EC%97%90-%EC%A3%BC%EC%9E%85%EB%90%9C-%EA%B0%9D%EC%B2%B4-%EB%93%A4%EC%9D%84-%EA%B1%B4%EB%93%A4%EB%A9%B4-%EC%95%88%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@leejm_dev/providers-instance-%EC%83%9D%EC%84%B1%EC%9E%90%EC%97%90-%EC%A3%BC%EC%9E%85%EB%90%9C-%EA%B0%9D%EC%B2%B4-%EB%93%A4%EC%9D%84-%EA%B1%B4%EB%93%A4%EB%A9%B4-%EC%95%88%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Tue, 02 Jan 2024 06:01:09 GMT</pubDate>
            <description><![CDATA[<h2 id="overview">overview</h2>
<p>nestjs 에서 class 를 인스턴스화 시켜서 언제 어디서 사용해도 동일한 값을 이용하기 위해 <code>constructor()</code> 안에 사용하고 싶은 객체를 주입해서 주로 사용한다. </p>
<p>여러 예시가 있지만, 이 글에서는 db 에 접근하는 layer 인 repository 에서 어떻게 주입해서 사용하는지 코드를 통해 확인해보자.</p>
<h2 id="code">code</h2>
<pre><code class="language-ts">// user.repository.ts
/**
 * @description default db connection : main db
 */
export class UserFunctions extends ReadRepo implements IUsersReadRepo {
  constructor(
    @InjectEntityManager() private readonly EntityManager: EntityManager,
    @InjectEntityManager(&#39;bo&#39;) private readonly boEntityManager: EntityManager,
  ) {
    super(EntityManager);
  }</code></pre>
<p>우리는 읽는 repository 와 쓰는 repository 를 구분해서 사용하는데, 각 repository 에서 상속받는 클래스가 다르다. 
read repository 에서는 <code>ReadRepo</code> 를 상속받는데, ReadRepo 는 다음과 같이 생겼다. </p>
<pre><code class="language-ts">export abstract class ReadRepo extends Repo {
  constructor(entityManager: EntityManager) {
    super(entityManager);
  }

  protected async queryMany&lt;T extends object&gt;(
    query: string,
    parameters?: any[],
    classConstructor?: ClassConstructor&lt;T&gt;,
  ): Promise&lt;T[]&gt; {
    const queryResult = columnToCamel(await this.query(query, parameters));
    if (!classConstructor) {
      return queryResult;
    }

    return Promise.all(
      queryResult.map((r) =&gt; this.validateReturnType(r, classConstructor)),
    );
  }</code></pre>
<p><code>UserFunctions</code> 의 생성자에서 </p>
<pre><code class="language-ts">super(EntityManager);</code></pre>
<p>를 통해서 <code>ReadRepo</code> 의 생성자로 <code>entityManager</code> 를 주입시켜 주고, </p>
<pre><code class="language-ts">export class Repo {
  constructor(protected entityManager: EntityManager) {}

  protected query&lt;T&gt;(query: string, parameters?: any[]): Promise&lt;T&gt; {
    return this.entityManager.query(query, parameters);
  }
}</code></pre>
<p>주입받는 <code>entityManager</code> 를 다시 </p>
<pre><code class="language-ts">super(EntityManager);</code></pre>
<p>를 통해서 상속받는 <code>Repo</code> 로 주입시켜서 쿼리를 실행하는 구조이다. </p>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/1871885a-4058-4e9e-83ad-16e67d4e858d/image.png" alt=""></p>
<h2 id="problem">problem</h2>
<p>하나의 repository 에서 하나의 <code>entityManager</code> 만 사용한다면 위 코드는 정말 잘 짜여진 코드라고 생각된다. 모듈화도 잘 시켜놨고, 코드의 재사용성 및 유지보수에도 좋다고 생각이 들지만... 
<strong>문제는 여러 <code>entityManager</code> 를 사용하는 경우에 발생했다.</strong></p>
<pre><code class="language-ts">// user.repository.ts
/**
 * @description default db connection : main db
 */
export class UserFunctions extends ReadRepo implements IUsersReadRepo {
  constructor(
    @InjectEntityManager() private readonly EntityManager: EntityManager,
    @InjectEntityManager(&#39;bo&#39;) private readonly boEntityManager: EntityManager,
  ) {
    super(EntityManager);
  }

  async func1(): Promise&lt;any&gt; {
    this.entityManager = this.EntityManager
    this.queryMany(&#39;select ~&#39;); // 1
    this.queryMany(&#39;select ~&#39;); // 2
    this.queryMany(&#39;select ~&#39;); // 3
  }

  async func2(): Promise&lt;any&gt; {
    this.entityManager = this.boEntityManager
    return this.queryMany(&#39;select ~&#39;);
  }</code></pre>
<h3 id="상황설명">상황설명</h3>
<p>func1 함수에서는 일반 디비 entityManager 를 사용하고, func2 함수에서는 bo 디비 entityManager 를 사용하고 있었고, 다음과 같이 각 함수에서 생성자에 주입된 entityManager 를 다른 entityManager 로 재할당 해줘서 사용하고 있었다.
그리고 func1 은 실행되는데 5초가 걸린다고 하고 호출 순서는 func1 -&gt; func2 라고 해보자.</p>
<p>그렇게 된다면 일반 디비 entityManager 를 주입해줘서 func1 을 실행하는 도중에 func2 가 실행된다면 주입되는 entityManager 는 bo entityManager 로 재할당이 되어 버려서 <code>원하는 테이블 혹은 sp 를 찾을 수 없다라는 에러</code> 가 발생한다.</p>
<p>func1 에서 사용하는 entityManager 가 function scope 안에서 선언되어서 사용된 객체가 아니기 때문에, func2 가 실행되는 도중에 재할당을 시켜버리면 func1 에서도 bo entityManager 를 사용하게 되는 것이었다. </p>
<p>쿼리 결과에 대한 validator 체크를 해주는 <code>queryMany()</code> 함수를 사용하고자 하면, 주입된 entityManager 를 바꾸지 말고 하나만 사용을 해야 한다. </p>
<h2 id="해결방법">해결방법</h2>
<p>하나의, 수정되지 않는 entityManager 를 사용하기 위해서는 기존에 사용하고 있던 <code>queryMany()</code> 를 수정하는 것 보다는 새로 만드는 것이 더 효율적이라고 판단했다. </p>
<p>애초에 기존에 사용하고 있는 <code>queryMany()</code> 를 수정하면 해당 함수를 사용하고 있는 모든 부분을 찾아서 수정을 해양하고, 하나의 repository 에서 두개 이상의 entityManager 를 주입받아서 사용하는 경우도 흔치 않기 때문이다. </p>
<p>그래서 새로 만든 함수는 다음과 같다. </p>
<pre><code class="language-ts">protected async queryManyForPickDB&lt;T extends object&gt;(
    entityManager: EntityManager, // 인스턴스의 생성자에 종속된 DB 말고 다른 DB 를 사용하고자 하는 함수가 있다면 해당 entityManager 를 주입시킴. ex) getDashboard
    query: string,
    parameters?: any[],
    classConstructor?: ClassConstructor&lt;T&gt;,
  ): Promise&lt;T[]&gt; {
    const queryResult = columnToCamel(
      await entityManager.query(query, parameters),
    );
    if (!classConstructor) {
      return queryResult;
    }

    return Promise.all(
      queryResult.map((r: object) =&gt;
        this.validateReturnType(r, classConstructor),
      ),
    );
  }</code></pre>
<p>특정 원하는 디비에 접근하기 위해서 인자값으로 entityManager 를 받았고, 이것으로 쿼리를 실행하는 함수이다. <code>queryManyForPickDB</code> 호출부를 보면</p>
<pre><code class="language-ts">// user.repository.ts
/**
 * @description default db connection : main db
 */
export class UserFunctions extends ReadRepo implements IUsersReadRepo {
  constructor(
    @InjectEntityManager() private readonly EntityManager: EntityManager,
    @InjectEntityManager(&#39;bo&#39;) private readonly boEntityManager: EntityManager,
  ) {
    super(EntityManager);
  }

  async func1(): Promise&lt;any&gt; {
    return this.queryManyForPickDB(
      this.boEntityManager,
      &#39;select ~&#39;,
    );
  }</code></pre>
<p>다음과 같이 <code>super(EntityManager);</code> 를 통해 기존 default 로 연결된 디비 말고 다른 디비 entityManager 를 인자값으로 넘겨줘서 쿼리를 실행시킬 수 있다. </p>
<blockquote>
<p><strong>생성자에 주입된 객체를 바꾸려고 하지 말자!</strong></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[ELK Stack (4) - kibana
]]></title>
            <link>https://velog.io/@leejm_dev/ELK-Stack-4-kibana</link>
            <guid>https://velog.io/@leejm_dev/ELK-Stack-4-kibana</guid>
            <pubDate>Fri, 22 Dec 2023 02:38:47 GMT</pubDate>
            <description><![CDATA[<p>자 마지막 kibana 이다. 
kibana 는 간단하게 설명하면 elasticsearch 에 저장된 데이터들을 시각화해서 직접 쿼리도 짤 수 있게 해주고, 대시보드도 구성할 수 있게 해주는 시각화 툴이다.</p>
<h2 id="how-to-install">how to install</h2>
<ol>
<li><code>wget https://artifacts.elastic.co/downloads/kibana/kibana-8.6.2-x86_64.rpm</code></li>
<li><code>rpm -Uvh kibana-8.6.2-x86_64.rpm</code></li>
<li><code>sudo systemctl daemon-reload</code>
<code>sudo systemctl enable logstash.service</code>
<code>sudo systemctl start logstash.service</code>
<code>sudo systemctl status logstash.service</code></li>
</ol>
<h2 id="config-file">config file</h2>
<p><code>/etc/kibana/kibana.yml</code></p>
<pre><code class="language-yaml">## monitoring 
monitoring.ui.container.elasticsearch.enabled: true
monitoring.ui.container.logstash.enabled: true

# Specifies whether Kibana should rewrite requests that are prefixed with
# `server.basePath` or require that they are rewritten by your reverse proxy.
# Defaults to `false`.
#server.rewriteBasePath: false

# Specifies the public URL at which Kibana is available for end users. If
# `server.basePath` is configured this URL should end with the same basePath.
#server.publicBaseUrl: &quot;&quot;

# The maximum payload size in bytes for incoming server requests.
#server.maxPayload: 1048576

# The Kibana server&#39;s name. This is used for display purposes.
server.name: &quot;kibana&quot;

# =================== System: Kibana Server (Optional) ===================
# Enables SSL and paths to the PEM-format SSL certificate and SSL key files, respectively.
# These settings enable SSL for outgoing requests from the Kibana server to the browser.
#server.ssl.enabled: false
#server.ssl.certificate: /path/to/your/server.crt
#server.ssl.key: /path/to/your/server.key

# =================== System: Elasticsearch ===================
# The URLs of the Elasticsearch instances to use for all your queries.
elasticsearch.hosts: [&quot;http://localhost:9200&quot;]</code></pre>
<p>kibana 와 elasticsearch 는 같은 ec2 에 설치되어 있기 때문에, <code>[&quot;http://localhost:9200&quot;]</code> 을 통해 elasticsearch 에 접근할 수 있다. </p>
<p>자 이제 <code>localhost:5601</code> 에 접속해서 확인해보면, 다음과 같이 필터도 걸고 검색을 통해서 원하는 로그를 확인할 수 있다.!</p>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/5c783a15-107a-4a56-bae9-60c3d204002e/image.png" alt=""></p>
<hr>
<h3 id="ref">REF</h3>
<p><a href="https://www.elastic.co/kr/kibana">https://www.elastic.co/kr/kibana</a>
사진: <a href="https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%EB%85%B8%ED%8A%B8%EB%B6%81-%ED%99%94%EB%A9%B4%EC%9D%98-%EC%84%B1%EB%8A%A5-%EB%B6%84%EC%84%9D-%EA%B7%B8%EB%9E%98%ED%94%84-JKUTrJ4vK00?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>의<a href="https://unsplash.com/ko/@lukechesser?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Luke Chesser</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ELK Stack (3) - elasticsearch]]></title>
            <link>https://velog.io/@leejm_dev/ELK-Stack-3-elasticsearch</link>
            <guid>https://velog.io/@leejm_dev/ELK-Stack-3-elasticsearch</guid>
            <pubDate>Fri, 22 Dec 2023 02:32:04 GMT</pubDate>
            <description><![CDATA[<h2 id="elasticsearch">elasticsearch</h2>
<p>elasticsearch 는 apahce lucene 기반의 java 로 만든 오픈소스 검색엔진이다. 엄청나게 빠른 검색 기능과 효율을 자랑하기 때문에, 흔히 사용하는 rdbms 에서 full-text-search 검색을 하는 것 보다 더 좋은 결과를 보인다. 
그렇기 때문에, 일반적인 검색 기능은 웬만하면 elasticsearch 를 이용한다고 보면 된다. </p>
<h2 id="elasticsearch-vs-opensearch">elasticsearch vs opensearch</h2>
<p>elasticsearch 와 용호상박을 이루는 opensearch 가 있다. 
opensearch 는 aws에서 만든 elasticsearch 와 아주 유사한 검색엔진인데, 한때 elasticsearch 와 분쟁이 있었다. </p>
<h5 id="결국은-elasticsearch-가-이긴것으로-알고-있다">결국은 elasticsearch 가 이긴것으로 알고 있다.</h5>
<p>아래는 간단하게 두 검색엔진을 비교한 표이다. 우리는 비용 및 여러가지 이유로 인해 elasticsearch 를 이용하기로 했다. 
<img src="https://velog.velcdn.com/images/leejm_dev/post/39c160cc-5ab8-4557-9c01-2bcbff4b0715/image.png" alt=""></p>
<h2 id="how-to-install">how to install</h2>
<ol>
<li><code>wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.6.2-x86_64.rpm</code></li>
<li><code>rpm -Uvh elasticsearch-8.6.2-x86_64.rpm</code></li>
<li><code>sudo systemctl daemon-reload</code>
<code>sudo systemctl enable logstash.service</code>
<code>sudo systemctl start logstash.service</code>
<code>sudo systemctl status logstash.service</code></li>
</ol>
<h2 id="config-file">config file</h2>
<p>elasticsearch 도 filebeat 와 마찬가지로 java 로 만들었기 때문에 heap 메모리 관리를 해줘야 한다. 기본적으로 아주 낮게 설정이 되어 있기 때문에, 이를 늘려주는 것이 좋다. <code>Xms4g -&gt; Xmx512m</code></p>
<p><code>/etc/elasticsearch/jvm.options</code></p>
<pre><code class="language-yaml">################################################################
## IMPORTANT: JVM heap size
################################################################
##
## The heap size is automatically configured by Elasticsearch
## based on the available memory in your system and the roles
## each node is configured to fulfill. If specifying heap is
## required, it should be done through a file in jvm.options.d,
## which should be named with .options suffix, and the min and
## max should be set to the same value. For example, to set the
## heap to 4 GB, create a new file in the jvm.options.d
## directory containing these lines:
##
## -Xms4g
## -Xmx4g
-Xms512m
-Xmx512m</code></pre>
<p><code>/etc/elasticsearch/elasticsearch.yml</code></p>
<pre><code class="language-yaml">Xpack.security.transport.ssl:
  enabled: true
  verification_mode: certificate
  keystore.path: certs/transport.p12
  truststore.path: certs/transport.p12
# Create a new cluster with the current node only
# Additional nodes can still join the cluster later
cluster.initial_master_nodes: [&quot;[ec2 ip where elasticsearch installed]&quot;]</code></pre>
<p>다음글은 마지막으로 kibana 에 대해서 알아보자. </p>
<hr>
<h3 id="ref">REF</h3>
<p><a href="https://www.elastic.co/kr/elasticsearch">https://www.elastic.co/kr/elasticsearch</a>
사진: <a href="https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%EC%BD%94%EC%99%80-%EC%84%A0%EA%B8%80%EB%9D%BC%EC%8A%A4%EC%9D%98-%EB%AA%A8%EC%96%91%EC%9D%84-%ED%99%95%EB%8C%80%ED%95%98%EB%8A%94-%EB%8F%8B%EB%B3%B4%EA%B8%B0%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%82%AC%EB%9E%8C-uAFjFsMS3YY?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>의<a href="https://unsplash.com/ko/@laughayette?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Marten Newhall</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[API 로그를 어떻게 Filebeat 가 긁어갈 수 있는가]]></title>
            <link>https://velog.io/@leejm_dev/API-%EB%A1%9C%EA%B7%B8%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-Filebeat-%EA%B0%80-%EA%B8%81%EC%96%B4%EA%B0%88-%EC%88%98-%EC%9E%88%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@leejm_dev/API-%EB%A1%9C%EA%B7%B8%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-Filebeat-%EA%B0%80-%EA%B8%81%EC%96%B4%EA%B0%88-%EC%88%98-%EC%9E%88%EB%8A%94%EA%B0%80</guid>
            <pubDate>Fri, 22 Dec 2023 02:04:09 GMT</pubDate>
            <description><![CDATA[<p>회사에서 ELK stack 에서 로그파일을 긁어오기 위해서는 filebeat 를 사용한다. 
filebeat 가 에러파일을 수집할 수 있다고 <a href="https://velog.io/@leejm_dev/ELK-Stack">여기서</a> 언급했었다. </p>
<p>그렇다면 실제로 어떻게 로그를 쌓고, 어떻게 연결이 되어서 긁어가는지 알아보자.  </p>
<p>우리는 어디서든 에러가 발생하면 <code>ExceptionFilter</code> 를 통해서 에러를 캐치한다. 이를 실제로 구현한 코드는 아래와 같다. </p>
<pre><code class="language-ts">// all-exception.filter.ts
import { LoggerService } from &#39;@common/utility/logger.service&#39;;
import { ResponseObj } from &#39;@core/class/response/response-obj&#39;;
import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpAdapterHost,
  HttpException,
  HttpStatus,
} from &#39;@nestjs/common&#39;;

@Catch()
export class AllExceptionFilter implements ExceptionFilter {
  constructor(
    private readonly httpAdapterHost: HttpAdapterHost,
    private readonly logger: LoggerService,
  ) {}


  catch(exception: any, host: ArgumentsHost) {
    const { httpAdapter } = this.httpAdapterHost;
    const ctx = host.switchToHttp();

    const hostArgs = host.getArgs();
    const req = host.getArgByIndex(0);
    const incommingMessage = hostArgs[0];
    const serverIp = req.connection.localAddress.split(&#39;:&#39;)[3];
    // host port 
    const serverPort = req.connection.localPort;
    const hostUrl = `${req.headers.host}::${serverPort}`;

    const clientIp =
      incommingMessage.headers[&#39;x-forwarded-for&#39;]?.split(&#39;,&#39;)[0] ||
      req.connection.remoteAddress.split(&#39;:&#39;)[3];

    const { url, method } = incommingMessage;
    const appName = url.split(&#39;/&#39;)[2];
    let httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
    const errorCode = Number(exception.errorCode)
      ? exception.errorCode
      : Number(exception.message)
      ? exception.message
      : &#39;0000&#39;;

    exception.url = url;
    exception.method = method;
    exception.appName = appName;
    exception.serverIp = serverIp;
    exception.clientIp = clientIp;
    exception.body = JSON.stringify(req.body);
    exception.hostUrl = hostUrl;

    httpStatus = exception.status
      ? exception.status
      : exception.statusCode
      ? exception.statusCode
      : httpStatus;

    switch(errorCode){
      case ...:
        this.logger.info(exception);
        this.logger.warn(exception);
        this.logger.error(exception):
    }

    let exceptionInfo: {
      statusCode?: number;
      error?: string;
      message?: string;
    } = {
      statusCode: exception?.status,
      message: exception?.message,
      error: exception?.message,
    };
   /**
    exceptionInfo 에 어떻게 값을 할당하는지는 생략하겠다.
   */

    const responseObj: ResponseObj&lt;any&gt; = new ResponseObj&lt;any&gt;(
      false,
      null,
      exceptionInfo,
      httpAdapter.getRequestUrl(ctx.getRequest()),
    );

    httpAdapter.reply(ctx.getResponse(), responseObj, httpStatus);
  }
}</code></pre>
<pre><code class="language-ts">// logger.service.ts
import { LoggerService, LogLevel } from &#39;@nestjs/common&#39;;
import * as winston from &#39;winston&#39;;
import * as winstonDaily from &#39;winston-daily-rotate-file&#39;;

const logDir = `${process.cwd()}/logs`;
const { errors, combine, timestamp, printf, colorize, label } = winston.format;

export class LoggerService implements LoggerService {
  private logger: winston.Logger;

  constructor(service: string) {
    this.logger = winston.createLogger({
      exitOnError: true,
      format: combine(
        timestamp({ format: &#39;YYYY-MM-DD HH:mm:ss.SSSZ&#39; }),
        errors({ stack: true }),
        label({ label: service }),
        printf((msg) =&gt; {
          return `#${msg.timestamp}#${msg.level}#${msg.message.appName}#${msg.message.method}#${msg.message.hostUrl}#${msg.message.url}#${msg.message.error}#${msg.message.body}#${msg.message.clientIp}#${msg.message.serverIp}#Exception ${msg.message.stackTrace}`;
        }),
      ),
      transports: [new winstonDaily(this.dailyOptions(&#39;info&#39;, service))],
      // handleExceptions: true,
    });

  }
  public log(message: any, ...optionalParams: any[]) {
    throw new Error(&#39;Method not implemented.&#39;);
  }
  public setLogLevels?(levels: LogLevel[]) {
    throw new Error(&#39;Method not implemented.&#39;);
  }

  console(): object {
    return this.logger;
  }

  public error(message: string, meta?) {
    this.logger.error(message, meta);
  }

  public warn(message: string) {
    this.logger.warn(message);
  }

  public info(message: string) {
    this.logger.info(message);
  }

  public debug(message: string) {
    this.logger.debug(message);
  }

  private dailyOptions(level: string, service: string) {
    return {
      level,
      datePattern: &#39;YYYY-MM-DD&#39;,
      dirname: logDir + `/${service}`,
      filename: `%DATE%.log`,
      maxFiles: &#39;14d&#39;,
      maxSize: &#39;256m&#39;,
      zippedArchive: false,
    };
  }
}
</code></pre>
<p>생략한 부분이 많긴 하지만, 중요하게 볼 부분은 아래 부분이다.</p>
<pre><code class="language-ts">switch(errorCode){
  case ...:
    this.logger.info(exception);
    this.logger.warn(exception);
    this.logger.error(exception):
}</code></pre>
<p><strong>LoggerService</strong> 를 주입받아서 사용한 <code>this.logger.</code> 를 통해서 
<code>#${msg.timestamp}#${msg.level}#${msg.message.appName}#${msg.message.method}#${msg.message.hostUrl}#${msg.message.url}#${msg.message.error}#${msg.message.body}#${msg.message.clientIp}#${msg.message.serverIp}#Exception ${msg.message.stackTrace}</code> 형태로 <code>${process.cwd()}/logs</code> 파일에 저장할 수 있다.</p>
<p>이렇게 로컬 파일에 저장을 하게 되면 우리는 도커 이미지를 통해 배포를 하고 서비스를 운영하기 때문에 실제로는 ec2 instance 에 로그파일이 쌓이는 것이 아니고, 도커 이미지 안에 위치하게 된다. 
그렇기 때문에, 도커 volume 을 통해서 파일을 공유해야 하는데, 아래와 같이 공유할 수 있다. </p>
<pre><code class="language-yaml">version: &#39;3.7&#39;
services:
  gateway:
    image: gateway
    container_name: gateway
    restart: always
    ports:
      - 3000:3000
      - 3051:3051
    working_dir: /app
    volumes:
      - /var/elk-log:/app/logs
    entrypoint:
      - node
      - ./dist/apps/gateway/main.js</code></pre>
<p>도커 이미지 안에 위치한 <code>/app/logs</code> 이 부분을 실제 ec2 instance 의 <code>/var/elk-log</code> 로 이동시켜줄 수 있다. 이를 <a href="https://docs.docker.com/storage/volumes/">volumne mounting</a> 이라고 한다. </p>
<p>이렇게 공유된 로그 파일은 <code>/var/elk-log</code> 에서 확인할 수 있다. 
<img src="https://velog.velcdn.com/images/leejm_dev/post/1245880a-be5d-4f43-87a9-325f8c3a2d26/image.png" alt=""></p>
<p>이렇게 공유가 되면 filebeat 에서는 input -&gt; paths 를 통해서 파일을 긁어올 수 있게 된다. </p>
<pre><code class="language-yaml">filebeat.inputs:

# filestream is an input for collecting log messages from files.
- type: log

  # Unique ID among all inputs, an ID is required.
  #id: my-filestream-id

  # Change to true to enable this input configuration.
  enabled: true

  # Paths that should be crawled and fetched. Glob based paths.
  paths:
    - /var/elk-log/*/*.log</code></pre>
<p>다음에는 이렇게 긁어온 파일을 받은 logstash 에서 어떻게 처리하는지 확인해보자.</p>
<hr>
<h3 id="ref">REF</h3>
<p>사진: <a href="https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%EB%82%AE-%EB%8F%99%EC%95%88-%EB%88%88-%EB%8D%AE%EC%9D%B8-%EB%95%85%EC%97%90-%EC%A3%BC%ED%99%A9%EC%83%89%EA%B3%BC-%EA%B2%80%EC%9D%80-%EC%83%89-%EC%A0%9C%EC%84%A4%EA%B8%B0-LDmDjb_cjkY?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>의<a href="https://unsplash.com/ko/@ar_graphics_?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Anis Rahman</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[disk 가 부족한지 어떻게 알 수 있을까 (feat. linux)]]></title>
            <link>https://velog.io/@leejm_dev/linux-disk-%EB%B6%80%EC%A1%B1</link>
            <guid>https://velog.io/@leejm_dev/linux-disk-%EB%B6%80%EC%A1%B1</guid>
            <pubDate>Fri, 22 Dec 2023 01:21:37 GMT</pubDate>
            <description><![CDATA[<p>ec2 intance(linux) 를 사용하면서 elasticsearch 혹은, prometheus 등 <code>APM(Application Performance Management)</code> 를 위한 서비스를 이용하게 되면 인스턴스 내부에 자체적으로 로그가 쌓이길 마련이다. 
회사에서도 elasticsearch 와 prometheus 를 사용하면서 엄청나게 로그가 많이 쌓여서 kibana 가 정상적으로 기능을 못하는 바람에 
<img src="https://velog.velcdn.com/images/leejm_dev/post/a848170f-97c9-4254-bb32-51807c1e510f/image.png" alt="">
다음과 같은 알람이 계속 울리고 있었다. </p>
<p>모든 기능이 정상적으로 돌아가고 있는 가운데, 
<img src="https://velog.velcdn.com/images/leejm_dev/post/70d00717-dd02-47b4-ab06-08238e903d44/image.png" alt=""></p>
<p>다음과 같은 에러가 발생했다. 의역하면, <code>요청이 너무 많아서 가용가능한 worker 가 정상적으로 일을 할 수 없다.</code> 라는 뜻 인 것 같았다. 
요청은 기존 그대로 보낼것 같은데, 혹시 디스크가 부족한건가 싶어서 디스크를 확인해봤다. </p>
<h3 id="디스크-용량-확인하기">디스크 용량 확인하기</h3>
<p><code>$ df -h</code>
<img src="https://velog.velcdn.com/images/leejm_dev/post/7cb73e4b-b56b-4ef4-9245-bbaf2f0b7eba/image.png" alt=""></p>
<p>/dev/xvda1 용량을 보면 거의 다 사용한것을 알 수 있다. </p>
<h3 id="가장-많이-사용된-용량-확인하기">가장 많이 사용된 용량 확인하기</h3>
<p><code>$ cd /</code>
<code>$ sudo du -ckx | sort -n &gt; /tmp/duck-root</code>
<code>$ sudo tail /tmp/duck-root</code>
<img src="https://velog.velcdn.com/images/leejm_dev/post/7d2ed180-0616-4ece-b93e-719d5045fccd/image.png" alt=""></p>
<p>/var/log 와 /var/lib 에서 엄청나게 사용하는 것을 확인할 수 있다. 
/var/log/elasticsearch 와 /var/lib/promethus 에 가서 필요없는 파일들을 삭제하여 용량을 늘려줄 수 있다.</p>
<h3 id="특정-디렉토리에서-많이-쓰이는-용량-확인하기">특정 디렉토리에서 많이 쓰이는 용량 확인하기</h3>
<p><code>$ du -ah | sort -n -r | head -n 10</code>
<img src="https://velog.velcdn.com/images/leejm_dev/post/aab1b665-b03e-43d2-9b41-fd19ac3fb022/image.png" alt="">
용량을 꽤나 차지 하고 있는 <code>/var/log/elasticsearch</code> 에 들어가서 어떤 파일이 실제로 용량을 차지하고 있는지 확인하려면, 위 명령어를 통해서 확인할 수 있다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ELK Stack (2) - logstash]]></title>
            <link>https://velog.io/@leejm_dev/ELK-Stack-2-logstash</link>
            <guid>https://velog.io/@leejm_dev/ELK-Stack-2-logstash</guid>
            <pubDate>Thu, 21 Dec 2023 02:25:09 GMT</pubDate>
            <description><![CDATA[<p>이전 글에 이어서... 긁어온 로그 데이터를 어떻게 가공하는지에 대해서 알아보자. </p>
<h2 id="logstash">logstash</h2>
<blockquote>
<p>무료 개방형 서버의 데이터 처리 파이프라인인 Logstash는 다양한 소스에서 데이터를 수집하여 변환한 후 자주 사용하는 저장소로 전달합니다.
<a href="https://www.elastic.co/kr/logstash">logstash 공식문서</a></p>
</blockquote>
<p>데이터를 수집해서 원하는 stash 로 전송한다. 
데이터를 가공할 필요가 없다면, logstash 를 사용하지 않고, 바로 elasticsearch 혹은 다른 stash 로 전송하면 되지만, 비즈니스 상황 및 로직에 따라 데이터를 가공해야 할 경우 logstash 를 사용하면 효용성을 높일 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/0bd60a0f-eebb-4041-bffd-80f84938c596/image.png" alt=""></p>
<h2 id="how-to-install">how to install</h2>
<ol>
<li><code>wget https://artifacts.elastic.co/downloads/logstash/logstash-8.6.2-x86_64.rpm</code></li>
<li><code>rpm -Uvh logstash-8.6.2-x86_64.rpm</code></li>
<li><code>sudo systemctl daemon-reload</code>
<code>sudo systemctl enable logstash.service</code>
<code>sudo systemctl start logstash.service</code>
<code>sudo systemctl status logstash.service</code></li>
</ol>
<h2 id="config-file">config file</h2>
<p>logstash 는 filebeat 가 설치된 ec2(서버가 돌아가고 있는) 에 같이 설치해도 되지만, 관리의 편의성과 logstash 때문에 서버에 문제가 발생하면 안되기 때문에 다른 ec2 에 설치를 해주었다. </p>
<p>logstash 도 filebeat 와 마찬가지로 기본적으로 <code>/etc/logstash</code> 에 설정파일이 있다. </p>
<h3 id="jvmoptions">jvm.options</h3>
<pre><code class="language-yaml">## JVM configuration

# Xms represents the initial size of total heap space
# Xmx represents the maximum size of total heap space

#-Xms1g
#-Xmx1g
-Xms256m
-Xmx256m</code></pre>
<p>기본적으로 logstash 는 자바로 만들어져있어 따로 메모리 관리를 해줘야 한다. 
*<em>처음에는 <code>Xms1g</code> 로 되어 있을텐데, 이건 용량이 너무 작아서 늘려주는 것(<code>Xms256m</code>)이 좋다. *</em></p>
<h3 id="pipelinesyml">pipelines.yml</h3>
<pre><code class="language-yaml"># This file is where you define your pipelines. You can define multiple.
# For more information on multiple pipelines, see the documentation:
#   https://www.elastic.co/guide/en/logstash/current/multiple-pipelines.html

- pipeline.id: main
  path.config: &quot;/etc/logstash/pipeline.conf&quot;</code></pre>
<p>pipeline.yml 파일에는 어떤 설정파일을 가지고 logstash 를 구동할지에 대한 정의가 들어있다. 만약에 본인이 설정파일의 이름을 바꾸던지 위치를 바꿨다면 여기도 같이 바꿔줘야 한다. </p>
<h3 id="pipelineconf">pipeline.conf</h3>
<pre><code class="language-yaml"># Beats -&gt; Logstash -&gt; Elasticsearch pipeline.


input {
  beats {
    port =&gt; 5044
  }
}

filter {
    grok {
        match =&gt; [ &quot;message&quot;, &quot;#(?&lt;current_time&gt;[^#]*)#(?&lt;log_level&gt;[^#]*)#(?&lt;app_name&gt;[^#]*)#(?&lt;http_method&gt;[^#]*)#(?&lt;host_url&gt;[^#]*)#(?&lt;url&gt;[^#]*)#(?&lt;error_message&gt;[^#]*)#(?&lt;body&gt;[^#]*)#(?&lt;client_ip&gt;[^#]*)#(?&lt;server_ip&gt;[^#]*)#(?&lt;stack_trace&gt;[^#]*)&quot; ]
    }
    #current_time에서 년월일 추출
    grok {
        match =&gt; [ &quot;current_time&quot;, &quot;(?&lt;py&gt;[^-]*)-(?&lt;pm&gt;[^-]*)-(?&lt;pd&gt;[^-]*) (?&lt;ph&gt;[^:]*)&quot; ]
    }

    mutate {
        convert =&gt; {
            # convert 구문에서 integer는 정수로 long, integer 같이 사용함
            &quot;server_type&quot; =&gt; &quot;integer&quot;
            &quot;server_id&quot; =&gt; &quot;integer&quot;
            &quot;thread_id&quot; =&gt; &quot;integer&quot;
        }
        rename =&gt; {
            &quot;[fields][instance_name]&quot; =&gt; &quot;instance_name&quot;
            &quot;[py]&quot; =&gt; &quot;[@metadata][py]&quot;
            &quot;[pm]&quot; =&gt; &quot;[@metadata][pm]&quot;
            &quot;[pd]&quot; =&gt; &quot;[@metadata][pd]&quot;
            &quot;[ph]&quot; =&gt; &quot;[@metadata][ph]&quot;
        }
        remove_field =&gt; [&quot;_id&quot;, &quot;_score&quot;, &quot;_type&quot;, &quot;beat&quot;, &quot;source&quot;, &quot;offset&quot;, &quot;prospector&quot;, &quot;message&quot;, &quot;tags&quot;, &quot;@version&quot;, &quot;fields&quot;, &quot;host&quot;, &quot;input&quot;, &quot;log&quot;, &quot;ecs&quot;, &quot;agent&quot;]
    }
    if [app_name] =~ &quot;undefined&quot; {
        mutate {
          replace =&gt; { &quot;log_level&quot; =&gt; &quot;info&quot; }
        }
    }
}

output {
  elasticsearch {
    hosts =&gt; [&quot;http://localhost:9200&quot;]
    index =&gt; &quot;log-%{+YYYY.MM.dd}&quot;
    user =&gt; &quot;elastic&quot;
    password =&gt; &quot;[password]&quot;
  }
  s3 {
    access_key_id =&gt; &quot;[access_key_id]&quot;
    secret_access_key =&gt; &quot;[secret_access_key]&quot;
    bucket =&gt; &quot;[bucket_name]&quot;
    codec =&gt; rubydebug
  }
}</code></pre>
<p>여기가 가장 중요하다. </p>
<p><strong>input</strong></p>
<ul>
<li><strong>beats</strong> : 
filebeat 로 부터 데이터를 받는다. elk 는 prometheus 와 다르게 데이터를 <code>pull</code> 해서 받는것이 아니라 <code>push</code> 해서 받는것이기 때문에 filebeat 가 설치되어 있는 ec2 의 ip를 따로 설정을 해줄 필요가 없다. 대신 port 번호(5044)는 있어야 한다. </li>
</ul>
<p><strong>filter</strong></p>
<ul>
<li><p><strong>grok</strong> : 
쿼리를 짤 수 있도록 raw data 를 변형시켜주는 플러그인이다. 
match 를 통해 특정 field를 정형화된 형태로 매칭시켜줄 수 있다.
<span style="color:orange">첫번째 match</span> 를 보면 <code>message</code> field를 두번쨰 인자값인 <code>#(?&lt;current_time&gt;[^#]*)#(?&lt;log_level&gt;[^#]*)#(?&lt;app_name&gt;[^#]*)#(?&lt;http_method&gt;[^#]*)#(?&lt;host_url&gt;[^#]*)#(?&lt;url&gt;[^#]*)#(?&lt;error_message&gt;[^#]*)#(?&lt;body&gt;[^#]*)#(?&lt;client_ip&gt;[^#]*)#(?&lt;server_ip&gt;[^#]*)#(?&lt;stack_trace&gt;[^#]*)&quot;</code> 형태로 매칭시켜줬다. <br>
잘 보면 <code>(?&lt;...&gt;[^#]*)</code> 와 같은 형태와 filebeat 설정파일에서 세팅한 <span style="color:#33B3EE">multiline pattern</span> 을 구분자로 나누는 것을 확인할 수 있다.</p>
<h5 id="-를-기준으로-current_time-과-같이-원하는-값을-구분지을-수-있다">(<code>#</code> 를 기준으로 <code>(?&lt;current_time&gt;[^#]*)</code> 과 같이 원하는 값을 구분지을 수 있다)</h5>
<p>current_time, log_level, app_name 이런 값들이 나중에 kibana 에서 쿼리를 짤때 쓰이는 field들이다. <br></p>
</li>
<li><p><em>대신 중요한 점이 있다. filebeat 에서 수집하는 로그 파일에도 저 형태와 동일하게 데이터가 쌓여야 한다. 즉, 반드시 <code>#</code> 를 구분자로 로그를 쌓아야 하다는 뜻이다.** <br>
<span style="color:orange">두번째 match</span> 를 보면 첫번째 match 에서 사용한 <code>current_time</code> field를 두번째 형태 `&quot;(?<py>[^-]</em>)-(?<pm>[^-]<em>)-(?<pd>[^-]</em>) (?<ph>[^:]<em>)&quot;` 로 매칭시켜줬다. *</em>년월일시간**으로 형태화 시키기 위한 정규식을 통해서 kibana 에서 정형화된 날짜 형태 데이터로 확인할 수 있다. </p>
</li>
<li><p><strong>mutate</strong> : 
mutate 는 형태를 정형화 시켜주는 match 와는 다르게 값을 <strong>바꿔주는 역할을 한다</strong>
mutate 에서 쓰이는 기능들로는
<code>coerce, rename, update, replace, convert, gsub, uppercase, capitalize, lowercase, strip, split, join, merge, copy</code> 들이 있다.
<span style="color:orange">convert</span>는 사용하는 field 타입을 변경해주는 <span style="color:#33B3EE">type casting</span> 을 할 수 있다.
<span style="color:orange">rename</span>은 field 의 이름을 바꿔주는 역할을 해준다. 
<a href="https://velog.io/@leejm_dev/ELK-Stack">filebeat 의 설정파일</a>에서 세팅한 fields 키 값을 보면 instance_name 을 설정했었다. </p>
<pre><code class="language-yaml">fields:
 instance_name: &quot;instance_1&quot;</code></pre>
<p>여기서 쓰이는 <code>instance_1</code> 을 <code>instance_name</code> field 로 사용하겠다라는 의미이다. 
아래 있는 <code>[@metadata][py]</code> 는 kibana 에서 쓰이는 metadata 로 사용하겠다라는 의미이다. 
<span style="color:orange">remove_field</span>는 kibana 에서 특정 field 들을 보이지 않게 할 수 있다. <br>
마지막으로 에러 로그중에서 app_name 이 undefined 는 우리 서비스 페이지에서 보낸 요청이 아닌, 외부에서 의도적으로 공격성을 띈 요청이라고 판단되어, log_level 을 info 로 전환시킬 수도 있다. </p>
<pre><code class="language-yaml">if [app_name] =~ &quot;undefined&quot; {
      mutate {
        replace =&gt; { &quot;log_level&quot; =&gt; &quot;info&quot; }
      }
  }</code></pre>
</li>
</ul>
<p><strong>output</strong></p>
<ul>
<li><strong>elasticsearch</strong> : 
logstash 를 통해서 데이터를 가공하고 나면 해당 데이터들을 elasticsearch 로 전송하는 역할이다. <h5 id="logstash-와-elasticsearch-를-같은-ec2-에-설치를-하였기-때문에-localhost9200-임을-알-수-있다">logstash 와 elasticsearch 를 같은 ec2 에 설치를 하였기 때문에 localhost:9200 임을 알 수 있다.</h5>
</li>
<li><strong>s3</strong> : 
가공된 데이터를 elasticsearch 뿐만 아니라, s3에도 저장을 하여, 추후에 분쟁이 있거나 혹시 모를 문제에 대응하기 위해 s3에 저장을 하게끔 설정한다. </li>
</ul>
<p>다음에는 elasticsearch 에서 어떻게 kibana 로 보내는 지 알아보자.</p>
<hr>
<h3 id="ref">REF</h3>
<p><a href="https://www.elastic.co/kr/logstash">https://www.elastic.co/kr/logstash</a>
<a href="https://www.elastic.co/guide/en/logstash/current/plugins-filters-grok.html">https://www.elastic.co/guide/en/logstash/current/plugins-filters-grok.html</a>
<a href="https://www.elastic.co/guide/en/logstash/current/plugins-filters-mutate.html">https://www.elastic.co/guide/en/logstash/current/plugins-filters-mutate.html</a>
사진: <a href="https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%EA%B0%88%EC%83%89-%EB%82%98%EB%AC%B4-%ED%85%8C%EC%9D%B4%EB%B8%94%EC%97%90-%EC%8B%A4%EB%B2%84-%EB%9D%BC%EC%9A%B4%EB%93%9C-%EB%8F%99%EC%A0%84-liak2l9DQiQ?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>의<a href="https://unsplash.com/ko/@cmapes?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Crystal Mapes</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RDBMS 정규화]]></title>
            <link>https://velog.io/@leejm_dev/RDBMS-%EC%A0%95%EA%B7%9C%ED%99%94</link>
            <guid>https://velog.io/@leejm_dev/RDBMS-%EC%A0%95%EA%B7%9C%ED%99%94</guid>
            <pubDate>Mon, 18 Dec 2023 02:57:12 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/leejm_dev/post/ab110628-d556-4fc2-aa51-fec26cab57a2/image.png" alt=""></p>
<p>디비 설계를 할때, 무결성을 지켜야 함 </p>
<h3 id="무결성">무결성</h3>
<p>데이터의 정확성, 일관성, 유효성을 유지하는 성질</p>
<h3 id="무결성-종류">무결성 종류</h3>
<ol>
<li><p>개체 무결성 (PK의 성질입니다)</p>
<ul>
<li>기본키는 null을 가질 수 없다. </li>
<li>기본키는 유일한 값이어야 한다. </li>
</ul>
</li>
<li><p>참조 무결성 (FK의 성질입니다)</p>
<ul>
<li>외래키는 null 이거나 참조테이블의 기본키여야 한다.</li>
</ul>
</li>
<li><p>도메인 무결성 (도메인: 칼럼 값이 가질 수 있는 데이터의 성질)</p>
<ul>
<li><p>데이터는 속해있는 필드의 속성에 부합해야 한다. </p>
<p>  <img src="https://velog.velcdn.com/images/leejm_dev/post/1ccdc166-81bf-4c22-97d1-b5e05a4d0c1c/image.png" alt=""></p>
</li>
</ul>
</li>
</ol>
<pre><code>→ s3_file_name 은 uuid 성질이어야 한다. </code></pre><ol start="4">
<li><p>null 무결성</p>
<ul>
<li>특정 속성(필드)값이 null 이 될 수 없다 </li>
</ul>
</li>
<li><p>key 무결성</p>
<ul>
<li>한 테이블에는 적어도 하나 이상의 key 가 있어야 한다.</li>
</ul>
</li>
<li><p>고유 무결성</p>
<ul>
<li><p>특정 필드가 identity 하면 같은 값이 있을 수 없다</p>
<p>  <img src="https://velog.velcdn.com/images/leejm_dev/post/8bb58a98-e141-4ecb-a36f-8ceff640e72c/image.png" alt=""></p>
</li>
</ul>
</li>
</ol>
<blockquote>
<p>무결성을 지키지 않으면 <code>이상현상</code>이 발생할 수 있음</p>
</blockquote>
<h3 id="이상현상-종류">이상현상 종류</h3>
<ol>
<li>삽입 이상</li>
</ol>
<pre><code>| 학번 | 학생 이름 | 학과 명 | 학과 코드 |
| --- | --- | --- | --- |
| 1 | 에디 | 수학과 | 001 |
| 2 | 제이크 | 경영학과 | 002 |
|  |  | 물리학과 | 003 |

물리학과를 추가하고자 하는데 물리학과에는 학생이 한명도 없다 → 물리학과 추가 불가능

→ 학번, 학생이름 / 학과 명, 학과 코드 분리 필요</code></pre><ol start="2">
<li>삭제 이상</li>
</ol>
<pre><code>| 학번 | 학생 이름 | 학과 명 | 학과 코드 |
| --- | --- | --- | --- |
| 1 | 에디 | 수학과 | 001 |
| 2 | 제이크 | 경영학과 | 002 |
| 3 | 올리 | 물리학과 | 003 |

올리가 자퇴를 해서 3번 row 를 지워야 한다 → 학과명과 학과 코드까지 한번에 지워지기 때문에 학과명, 학과 코드 정보가 사라진다

→ 학번, 학생이름 / 학과 명, 학과 코드 분리 필요</code></pre><ol start="3">
<li>수정 이상</li>
</ol>
<pre><code>| 학번 | 학생 이름 | 학과 명 | 학과 코드 |
| --- | --- | --- | --- |
| 1 | 에디 | 수학과 | 001 |
| 2 | 제이크 | 경영학과 | 002 |
| 3 | 올리 | 물리학과 | 003 |
| 4 | 파이 | 물리학과 | 003 |
| 5 | 랑 | 물리학과 | 003 |
| 6 | 조 | 물리학과 | 003 |

물리학과의 학과 코드가 004 로 바뀐다면 4개의 행을 수정해야 한다 → 여기서 실수로 3개만 수정한다면 데이터가 상이한 문제가 발생

→ 학번, 학생이름 / 학과 명, 학과 코드 분리 필요</code></pre><p><strong>다음과 같은 이상현상을 해결하려면 정규화가 필요하다 (정규화를 해야하는 이유 → 이상현상을 제거 하기 위해)</strong></p>
<h3 id="함수-종속성">함수 종속성</h3>
<p>하나의 값 A에 의해 다른 값 B가 하나의 값으로 결정되는 속성 (A가 B를 결정한다)</p>
<table>
<thead>
<tr>
<th>학번</th>
<th>학생 이름</th>
<th>학과 명</th>
<th>학과 코드</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>에디</td>
<td>수학과</td>
<td>001</td>
</tr>
<tr>
<td>2</td>
<td>제이크</td>
<td>경영학과</td>
<td>002</td>
</tr>
<tr>
<td>3</td>
<td>올리</td>
<td>물리학과</td>
<td>003</td>
</tr>
<tr>
<td>4</td>
<td>파이</td>
<td>물리학과</td>
<td>003</td>
</tr>
<tr>
<td>5</td>
<td>랑</td>
<td>물리학과</td>
<td>003</td>
</tr>
<tr>
<td>6</td>
<td>조</td>
<td>물리학과</td>
<td>003</td>
</tr>
</tbody></table>
<p>학번은 학생이름을 결정한다 학번 → 학생이름 (종속성), <code>학번</code> 은 <code>종속 결정자</code>이다</p>
<h2 id="정규화">정규화</h2>
<ol>
<li><p>제 1 정규화</p>
<ul>
<li><p>각 칼럼이 하나의 속성만을 가져야 한다 → 학과명에서 불만족</p>
</li>
<li><p>하나의 칼럼은 같은 타입의 값을 가져야 한다 → 학과명에서 불만족</p>
</li>
<li><p>각 칼럼이 유일한 이름을 가져야 한다 → 만족</p>
</li>
<li><p>칼럼의 순서가 상관없어야 한다 → 만족</p>
<p>수정전 </p>
<table>
<thead>
<tr>
<th>학번</th>
<th>학생 이름</th>
<th>학과 명</th>
<th>학과 코드</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>에디</td>
<td>수학과, 정치학과</td>
<td>001</td>
</tr>
<tr>
<td>2</td>
<td>제이크</td>
<td>경영학과</td>
<td>002</td>
</tr>
<tr>
<td>3</td>
<td>올리</td>
<td>물리학과</td>
<td>003</td>
</tr>
</tbody></table>
<p>수정후</p>
<table>
<thead>
<tr>
<th>학번</th>
<th>학생 이름</th>
<th>학과 명</th>
<th>학과 코드</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>에디</td>
<td>수학과</td>
<td>001</td>
</tr>
<tr>
<td>2</td>
<td>제이크</td>
<td>경영학과</td>
<td>002</td>
</tr>
<tr>
<td>3</td>
<td>올리</td>
<td>물리학과</td>
<td>003</td>
</tr>
<tr>
<td>1</td>
<td>에디</td>
<td>정치학과</td>
<td>004</td>
</tr>
</tbody></table>
</li>
</ul>
</li>
<li><p>제 2 정규화</p>
<ul>
<li><p>1 정규화를 만족해야 한다 → 만족</p>
</li>
<li><p>모든 칼럼이 <strong>부분 종속성(PK가 아닌 다른 키에 종속됨)</strong>이 없어야 한다, </p>
</li>
<li><p><em>완전 함수 종속(PK에만 종속될 수 있다)*</em>을 만족해야 한다.
해당 테이블의 PK는 (학번+학과명) 이라고 했을 떄,
점수는 PK에 종속되지만, 학과 코드는 학과명에 종속된다. </p>
<p>수정전</p>
<table>
<thead>
<tr>
<th>학번</th>
<th>학과 명</th>
<th>학과 코드</th>
<th>점수</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>수학과</td>
<td>001</td>
<td>100</td>
</tr>
<tr>
<td>2</td>
<td>경영학과</td>
<td>002</td>
<td>90</td>
</tr>
<tr>
<td>3</td>
<td>물리학과</td>
<td>003</td>
<td>80</td>
</tr>
</tbody></table>
<p>수정후</p>
<table>
<thead>
<tr>
<th>학번</th>
<th>학과 코드</th>
<th>점수</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>001</td>
<td>100</td>
</tr>
<tr>
<td>2</td>
<td>002</td>
<td>90</td>
</tr>
<tr>
<td>3</td>
<td>003</td>
<td>80</td>
</tr>
<tr>
<td><br></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th>학과 코드</th>
<th>학과 명</th>
</tr>
</thead>
<tbody><tr>
<td>001</td>
<td>수학과</td>
</tr>
<tr>
<td>002</td>
<td>경영학과</td>
</tr>
<tr>
<td>003</td>
<td>물리학과</td>
</tr>
</tbody></table>
</li>
</ul>
</li>
<li><p>제 3 정규화</p>
<ul>
<li>2 정규화를 만족해야 한다</li>
<li>기본키를 제외하고 모든 칼럼은 <strong>이행 종속성(X→Y, Y→Z ⇒ X→Z)</strong>이 없어야 한다.</li>
</ul>
</li>
</ol>
<pre><code>수정전

| 학번 | 학생 이름 | 전화번호 |
| --- | --- | --- |
| 1 | 에디 | 01055558888 |
| 2 | 제이크 | 01044447777 |

학번을 알면 학생이름을 알 수 있고, 학생이름을 알면 전화번호를 알 수 있다.

학번을 알면 전화번호를 알 수 있게 됨

수정후

| 학번 | 학생 이름 |
| --- | --- |
| 1 | 에디 |
| 2 | 제이크 |

| 학생 이름 | 전화번호 |
| --- | --- |
| 에디 | 01055558888 |
| 제이크 | 01044447777 |</code></pre><ol start="4">
<li><p>BCNF 정규화</p>
<ul>
<li><p>3 정규화를 만족해야 한다.</p>
</li>
<li><p><strong>후보키(PK가 될 수 있는 칼럼들)에 속하지 않은 칼럼이 종속 결정자가 될 수 없다.</strong></p>
<p>수정전</p>
<table>
<thead>
<tr>
<th>payment_id</th>
<th>rfq_id</th>
<th>project_name</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>4</td>
<td>test-1</td>
</tr>
<tr>
<td>2</td>
<td>4</td>
<td>test-1</td>
</tr>
<tr>
<td>3</td>
<td>6</td>
<td>test-3</td>
</tr>
</tbody></table>
<p>PK는 payment_id 이다</p>
<p>rfq_id 는 payment_id 에 의해 결정 되지만, project_name 은 rfq_id 에 의해 결정된다. </p>
<p>rfq_id 는 후보키가 아니기 때문에 만족하지 않는다.</p>
<p>수정후</p>
<table>
<thead>
<tr>
<th>payment_id</th>
<th>rfq_id</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>4</td>
</tr>
<tr>
<td>2</td>
<td>4</td>
</tr>
<tr>
<td>3</td>
<td>6</td>
</tr>
</tbody></table>
<br>

<table>
<thead>
<tr>
<th>rfq_id</th>
<th>project_name</th>
</tr>
</thead>
<tbody><tr>
<td>4</td>
<td>test-1</td>
</tr>
<tr>
<td>6</td>
<td>test-3</td>
</tr>
</tbody></table>
</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[grafana monitoring with nestjs]]></title>
            <link>https://velog.io/@leejm_dev/grafana-monitoring-with-nestjs</link>
            <guid>https://velog.io/@leejm_dev/grafana-monitoring-with-nestjs</guid>
            <pubDate>Fri, 15 Dec 2023 06:18:10 GMT</pubDate>
            <description><![CDATA[<p>서비스의 전체적인 상황들을 한눈에 파악하고 문제가 발생한 경우, 즉각적으로 트래킹 및 처리할 수 있도록 하는 것을 <code>모니터링 한다</code> 라고 한다. </p>
<p>모니터링 툴에는 여러가지가 있다. </p>
<ul>
<li>sentry</li>
<li>datadog</li>
<li>grafana</li>
<li>etc...</li>
</ul>
<p>이번에는 grafana 에 대해서 얘기를 해보려고 한다. </p>
<p>회사에서 아직 모니터링 대시보드 역할을 할 수 있는 것이 없었기 때문에, 
이를 구축하기 위해서 <code>prometheus + grafana stack</code> 으로 대시보드를 구성한 경험에 대해서 기록하자.</p>
<h1 id="어떤-것이-필요할까">어떤 것이 필요할까?</h1>
<p>일단 모니터링이 필요한 경우 어떻게 해야할지에 대해서 생각을 해보자. </p>
<p>모니터링을 한다는 것은, 모니터링 대상에 대한 정보를 실시간으로 긁어오거나, 모니터링 대상이 본인의 정보를 실시간으로 제공을 해주거나 하는 방법으로 지속적으로 데이터를 긁어와야 한다. 
그렇다면, 그 긁어오거나 제공을 해주는 툴이 있어야 한다. 
긁어오는 툴을 찾아보니, prometheus 라는 툴이 있었다. 
또한, 그런 데이터들을 수집하는 collector 중에 우리가 사용하는 것은 <code>node_exporter, postgres_exporter, cadvisor, nestjs-prometheus</code> 가 있다. </p>
<h2 id="✅-collector-1---node-exporter">✅ collector 1 - node exporter</h2>
<p>node exporter 는 하드웨어의 상태 및 정보와 커널관련 메트릭을 수집하는 collector 이다. 
node exporter 를 통해 현재 하드웨어의 cpu 및 메모리 등 하드웨어의 정보를 수집할 수 있다. </p>
<h3 id="how-to-install-node-exporter">how to install node exporter</h3>
<ol>
<li><p>download archive file
<code>$ wget https://github.com/prometheus/node_exporter/releases/download/v0.15.2/node_exporter-0.15.2.linux-amd64.tar.gz</code></p>
</li>
<li><p>extract archive file
<code>$ tar -xf node_exporter-0.15.2.linux-amd64.tar.gz</code></p>
</li>
<li><p>move binary file to <code>/usr/local/bin</code>
<code>$ sudo mv node_exporter-0.15.2.linux-amd64/node_exporter /usr/local/bin</code></p>
</li>
<li><p>create user only for node exporter
<code>$ sudo useradd -rs /bin/false node_exporter</code></p>
</li>
<li><p>make file that node exporter can be started at boot
<code>$ sudo vim /etc/systemd/system/node_exporter.service</code></p>
<pre><code class="language-yaml">[Unit]
Description=Node Exporter
After=network.target

[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/local/bin/node_exporter

[Install]
WantedBy=multi-user.target</code></pre>
</li>
</ol>
<h3 id="how-to-start-node-exporter">how to start node exporter</h3>
<p><code>$ sudo systemctl daemon-reload</code>
<code>$ sudo systemctl enable node_exporter</code>
<code>$ sudo systemctl start node_exporter</code></p>
<h2 id="✅-collector-2---postgres-exporter">✅ collector 2 - postgres exporter</h2>
<h3 id="how-to-install-postgres-exporter">how to install postgres exporter</h3>
<ol>
<li>make directory for postgres exporter
<code>$ mkdir /opt/postgres_exporter &amp;&amp;
cd /opt/postgres_exporter</code></li>
<li>download archive file
<code>% wget https://github.com/wrouesnel/postgres_exporter/releases/download/v0.5.1/postgres_exporter_v0.5.1_linux-amd64.tar.gz</code></li>
<li>extract archive file
<code>tar -xzvf postgres_exporter_v0.5.1_linux-amd64.tar.gz</code></li>
<li>copy to <code>/usr/local/bin</code>
<code>$ cd postgres_exporter_v0.5.1_linux-amd64</code>
<code>$ sudo cp postgres_exporter /usr/local/bin</code></li>
</ol>
<h3 id="env-file-for-postgres-exporter">env file for postgres exporter</h3>
<p><code>$ cd /opt/postgres_exporter</code>
<code>$ sudo vim postgres_exporter.env</code></p>
<p>특정 데이터베이스만 원하는 경우. database-name 치환</p>
<pre><code class="language-yaml">DATA_SOURCE_NAME=&quot;postgresql://username:password@localhost:5432/database-name?sslmode=disable&quot;</code></pre>
<p>모든 데이터베이스</p>
<pre><code class="language-yaml">DATA_SOURCE_NAME=&quot;postgresql://postgres:postgres@localhost:5432/?sslmode=disable&quot;</code></pre>
<h3 id="setup-postgres-exporter-user">setup postgres exporter user</h3>
<ol>
<li>create user
<code>$ sudo useradd -rs /bin/false postgres</code></li>
<li>make service file
<code>$ sudo vim /etc/systemd/system/postgres_exporter.service</code><pre><code class="language-yaml">[Unit]
Description=Prometheus exporter for Postgresql
Wants=network-online.target
After=network-online.target
[Service]
User=postgres
Group=postgres
WorkingDirectory=/opt/postgres_exporter
EnvironmentFile=/opt/postgres_exporter/postgres_exporter.env
ExecStart=/usr/local/bin/postgres_exporter --web.listen-address=:9187 --web.telemetry-path=/metrics
Restart=always
[Install]
WantedBy=multi-user.target</code></pre>
<h3 id="how-to-start-prometheus-exporter">how to start prometheus exporter</h3>
<code>$ sudo systemctl daemon-reload</code>
<code>$ sudo systemctl start postgres_exporter</code>
<code>$ sudo systemctl enable postgres_exporter</code>
<code>$ sudo systemctl status postgres_exporter</code></li>
</ol>
<h2 id="✅-collector-3---cadvisor">✅ collector 3 - cadvisor</h2>
<p>cadvisor 는 docker, kubernetes 의 시계열 데이터를 수집하는 collector 이다. 각 container 들의 cpu, memory, network 사용량 등 리소스의 사용량 및 잔여량을 알 수 있다. </p>
<h3 id="how-to-install-cadvisor">how to install cadvisor</h3>
<p><code>$ sudo apt-get update</code>
<code>$ sudo apt-get -y install cadvisor</code>
<code>$ systemctl status cadvisor</code></p>
<h2 id="✅-collector-4---nestjs-prometheus">✅ collector 4 - nestjs prometheus</h2>
<p>node_exporter 에서는 실제 어플리케이션의 http request 및 response time 등을 핸들링하거나 추적하기에 적합하지 않은 데이터를 주고 있다보니, 실제 http request 및 response 를 다루기 위해 새로운 라이브러리가 필요했다. </p>
<p>이를 해결하기 위해서 node 라이브러리인, <code>nestjs-prometheus</code> 를 이용했다.
<a href="https://github.com/willsoto/nestjs-prometheus">https://github.com/willsoto/nestjs-prometheus</a>
위 링크를 타고 들어가면 어떻게 설치하고 어떻게 사용하는지 나와있는데...</p>
<h5 id="이게-생각보다-어려웠다-기본적으로-제공하는-metric-들에-대한-개념도-있어야-하고-이를-우리가-운영하고-있는-코드에-녹이는-것도-어느정도-시행착오가-있었다-그냥-단순하게-연동하는-것-보다-최대한-현재-운영되고-있는-서비스에-영향을-가지-않도록-개발하는것이-포인트다">이게 생각보다 어려웠다. 기본적으로 제공하는 metric 들에 대한 개념도 있어야 하고, 이를 우리가 운영하고 있는 코드에 녹이는 것도 어느정도 시행착오가 있었다. 그냥 단순하게 연동하는 것 보다 최대한 현재 운영되고 있는 서비스에 영향을 가지 않도록 개발하는것이 포인트다.</h5>
<h3 id="구현방법">구현방법</h3>
<p>우리는 모든 요청에 대해서 기존 request flow 를 방해하지 않기 위해, interceptor 를 이용해서 데이터를 가져오도록 구현했다. </p>
<ol>
<li><p>우선 metric 모듈을 만들어주고, 모든 요청에 대해서 데이터를 수집하기 위해 gateway 모듈에 주입을 해준다. </p>
<pre><code class="language-ts">// metrics.module.ts
import { Global, Module } from &#39;@nestjs/common&#39;;
import {
 makeCounterProvider,
 makeHistogramProvider,
 PrometheusModule as Prometheus,
} from &#39;@willsoto/nestjs-prometheus&#39;;

@Module({
 imports: [
   Prometheus.register({
     path: &#39;/metrics&#39;,
     defaultMetrics: {
       enabled: true,
     },
   }),
 ],
})
export class MetricsModule {}</code></pre>
<pre><code class="language-ts">// gateway.module.ts
@Module({
 imports: [
   MetricsModule,
 ],
 controllers: [GatewayController],
 providers: [GatewayService],
})
export class GatewayModule {
... 이하 생략
}</code></pre>
</li>
<li><p>그 다음에 interceptor를 구현한다. </p>
<pre><code class="language-ts">import {
 CallHandler,
 ExecutionContext,
 Injectable,
 NestInterceptor,
 OnModuleInit,
} from &#39;@nestjs/common&#39;;
import { Counter, Gauge, Histogram } from &#39;prom-client&#39;;
import { Observable, tap } from &#39;rxjs&#39;;

@Injectable()
export class PrometheusInterceptor implements NestInterceptor, OnModuleInit {
 onModuleInit() {
   this.requestSuccessHistogram.reset();
   this.requestFailHistogram.reset();
   this.failureCounter.reset();
 }
 // status code 2XX
 private readonly requestSuccessHistogram = new Histogram({
   name: &#39;nestjs_success_requests&#39;,
   help: &#39;NestJs success requests - duration in seconds&#39;,
   labelNames: [&#39;handler&#39;, &#39;controller&#39;, &#39;method&#39;],
   buckets: [
     0.0001, 0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.09, 0.1, 0.25, 0.5, 1,
     2.5, 5, 10,
   ],
 });

 // status code != 2XX
 private readonly requestFailHistogram = new Histogram({
   name: &#39;nestjs_fail_requests&#39;,
   help: &#39;NestJs fail requests - duration in seconds&#39;,
   labelNames: [&#39;handler&#39;, &#39;controller&#39;, &#39;method&#39;],
   buckets: [
     0.0001, 0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.09, 0.1, 0.25, 0.5, 1,
     2.5, 5, 10,
   ],
 });

 private readonly failureCounter = new Counter({
   name: &#39;nestjs_requests_failed_count&#39;,
   help: &#39;NestJs requests that failed&#39;,
   labelNames: [&#39;handler&#39;, &#39;controller&#39;, &#39;error&#39;, &#39;method&#39;],
 });

 static registerServiceInfo(serviceInfo: {
   domain: string;
   name: string;
   version: string;
 }): PrometheusInterceptor {
   new Gauge({
     name: &#39;nestjs_info&#39;,
     help: &#39;NestJs service version info&#39;,
     labelNames: [&#39;domain&#39;, &#39;name&#39;, &#39;version&#39;],
   }).set(
     {
       domain: serviceInfo.domain,
       name: `${serviceInfo.domain}.${serviceInfo.name}`,
       version: serviceInfo.version,
     },
     1,
   );

   return new PrometheusInterceptor();
 }

   // metrics url 요청은 트래킹 필요 x
 private isAvailableMetricsUrl(url: string): boolean {
   const excludePaths = &#39;metrics&#39;;
   if (url.includes(excludePaths)) {
     return false;
   }
   return true;
 }

 intercept(context: ExecutionContext, next: CallHandler): Observable&lt;any&gt; {
   const originUrl = context.switchToHttp().getRequest().url.toString();

   const method = context.switchToHttp().getRequest().method.toString();
   const labels = {
     controller: context.getClass().name,
     handler: context.getHandler().name,
     method: method,
   };

   try {
     const requestSuccessTimer =
       this.requestSuccessHistogram.startTimer(labels);
     const requestFailTimer = this.requestFailHistogram.startTimer(labels);
     return next.handle().pipe(
       tap({
         next: () =&gt; {
           if (this.isAvailableMetricsUrl(originUrl)) {
             requestSuccessTimer();
           }
           // Handle the next event here
         },
         error: () =&gt; {
           if (this.isAvailableMetricsUrl(originUrl)) {
             requestFailTimer();
             this.failureCounter.labels({ ...labels }).inc(1);
           }

           // Handle the error event here
         },
       }),
     );
   } catch (error) {}
 }
}</code></pre>
<p>위 코드에서 보듯이, 
우선적으로 요청데이터를 저장할 histogram을 선언한다. <code>Histogram class</code> 에서 제공하는 <code>startTimer()</code> 함수가 모든 요청에 있어서 정보를 긁어올 수 있다. </p>
</li>
</ol>
<ul>
<li>요청의 response 가 성공하면 <code>requestSuccessTimer()</code> 를 통해 <code>requestSuccessHistogram Histogram</code> 에 정보를 저장하고,</li>
<li>요청의 response 가 실패하면 <code>requestFailTimer()</code> 를 통해 
<code>requestFailHistogram Histogram</code> 에 정보를 저장한다. 
또한, <code>failureCounter.labels({ ...labels }).inc(1);</code> 를 통해 실패 횟수를 1씩 증가시킬 수 있다.</li>
<li>해당 모듈이 실행될 때 마다 histogram 을 리셋해줌으로서(<code>onModuleInit()</code>), 메모리 용량을 관리해준다. </li>
</ul>
<ol start="3">
<li><p>main.ts 에 interceptor 를 등록해준다. </p>
<pre><code class="language-ts">// main.ts
async function bootstrap(): Promise&lt;void&gt; {
 const app = await NestFactory.create(GatewayModule);
 app.enableCors({
   origin: &#39;*&#39;,
   methods: &#39;*&#39;,
   allowedHeaders: &#39;*&#39;,
 });

 app.useGlobalInterceptors(new ResponseInterceptor());
 app.useGlobalInterceptors(new PrometheusInterceptor());

 await app.listen(3000);

}
bootstrap();</code></pre>
</li>
</ol>
<ol start="4">
<li>response interceptor 에서 메트릭 정보를 가공하지 않고 그대로 반환해주기 위해 조건을 추가한다. 
서비스내에서 클라이언트와 약속한 표준 응답으로 메트릭을 제공하면 데이터를 바인딩하는데 있어서 에러가 났다. 그래서 이를 약속한 response format 이 아닌 메트릭을 수집할 수 있도록 <code>raw 형태 그대로</code> 반환을 해줘야 한다. </li>
</ol>
<p><strong>혹시 response format 을 사용하고 있다면 메트릭 수집을 위해서는 꼭 약속된 format 을 이용하지 않고, 반환할 수 있도록!!!</strong></p>
<pre><code class="language-ts">  // response.interceptor.ts
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
  intercept(_context: ExecutionContext, next: CallHandler) {
    const req: Request = _context.switchToHttp().getRequest();

    const excludePaths = [&#39;/api/capa-metrics&#39;, &#39;/capa-metrics&#39;];

    return next
      .handle()
      .pipe(defaultIfEmpty(null))
      .pipe(
        map((result) =&gt; {
          if (excludePaths.includes(req.url)) {
            return result;
          }
          // CAPA4.0 표준 응답으로 변환하여 반환
          return new ResponseObj(true, result);
        }),
      );
  }
}</code></pre>
<h3 id="결과-확인">결과 확인</h3>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/b696d5fd-9a8f-4cfc-8258-a728db29df36/image.png" alt=""></p>
<p>다음과 같이 어떤 요청이 어떻게 들어오고 있는지 확인할 수 있다. </p>
<p>자 이제 이 collector 들을 한군데 모으기 위해 prometheus 를 이용해보자.</p>
<h2 id="✅-prometheus">✅ prometheus</h2>
<p>prometheus 는 SoundCloud 에서 만든 오픈소스 모니터링 툴이다.
prometheus 의 구조는 다음과 같다. 
<img src="https://velog.velcdn.com/images/leejm_dev/post/2f4fd3ca-697c-4a6e-9483-9f8836876a91/image.png" alt=""></p>
<p>jobs/exporter 가 실제로 메트릭을 수집하고 이를 prometheus server 가 해당 메트릭 정보를 <code>pull</code> 을 통해서 가져온다. 
해당 메트릭 정보는 <code>각 모니터링 대상의 수집기</code>로 부터 긁어오는데, 그 때 이제 http endpoint port 를 통해서 데이터를 가져온다.</p>
<h5 id="모니터링-수집기의-예로는-node_exporter-cadvisor-postgres_exporter-이-있다">모니터링 수집기의 예로는 node_exporter, cadvisor, postgres_exporter 이 있다.</h5>
<p>또한, prometheus 에서 취급하는 메트릭 데이터는 전부 time series data 들로, 이러한 시계열 매트릭 데이터를 수집하고 저장하는 데이터베이스 역할도 한다. </p>
<p>그리고 오른쪽에 보면 Alertmanager 와 Grafana 가 있다. </p>
<p><code>Alertmanager</code> 는 수집한 메트릭 정보가 어떠한 alert rule 에 위배가 되는 경우, email, teams, slack 으로 알림을 보낼 수 있는 기능이다. </p>
<p><code>Grafana</code> 는 수집한 메트릭 정보를 시각화 해줄 수 있는 visual dashboard 역할을 한다.</p>
<p>우리는 prometheus 에서 수집한 메트릭 정보를 grafana 에서 시각화하는 방법으로 모니터링을 구축했다. 약간 elasticSearch &amp; Kibana 같은 관계로 생각하면 된다. </p>
<h3 id="prometheus-install">prometheus install</h3>
<ol>
<li><p>download prometheus archive file
<code>$ wget https://github.com/prometheus/prometheus/releases/download/v2.1.0/prometheus-2.1.0.linux-amd64.tar.gz</code></p>
</li>
<li><p>extract from archieve file
<code>$  tar -xf prometheus-2.1.0.linux-amd64.tar.gz</code></p>
</li>
<li><p>move binary file to <code>/usr/local/bin</code>
<code>$  sudo mv prometheus-2.1.0.linux-amd64/prometheus prometheus-2.1.0.linux-amd64/promtool /usr/local/bin</code></p>
</li>
<li><p>make configuration file
<code>$ sudo mkdir /etc/prometheus /var/lib/prometheus</code>
<code>$ sudo mv prometheus-2.1.0.linux-amd64/consoles prometheus-2.1.0.linux-amd64/console_libraries /etc/prometheus</code></p>
</li>
<li><p>delete useless file
<code>$ rm -r prometheus-2.1.0.linux-amd64*</code></p>
</li>
</ol>
<h3 id="prometheus-configuration-file">prometheus configuration file</h3>
<pre><code class="language-yaml">global:
  scrape_interval: 10s

scrape_configs:
  - job_name: &#39;prometheus_metrics&#39;
    scrape_interval: 5s
    static_configs:
      - targets: [&#39;localhost:9090&#39;]
  - job_name: &#39;node_exporter_B/E&#39;
    scrape_interval: 5s
    static_configs:
      - targets: [&#39;xx:9100&#39;, &#39;xx:9100&#39;, &#39;xx:9100&#39;]
  - job_name: &#39;node_exporter_F/E&#39;
    scrape_interval: 5s
    static_configs:
      - targets: [&#39;xx:9100&#39;, &#39;xx:9100&#39;]
  - job_name: &#39;postgres_exporter&#39;
    static_configs:
      - targets: [&#39;localhost:9187&#39;]
  - job_name: &#39;container_B/E&#39;
    scrape_interval: 10s
    static_configs:
      - targets: [&#39;xx:8080&#39;, &#39;xx:8080&#39;, &#39;xx:8080&#39;]
  - job_name: &#39;container_F/E&#39;
    scrape_interval: 10s
    static_configs:
      - targets: [&#39;xx:8080&#39;, &#39;xx:8080&#39;]
  - job_name: &#39;nestjs-app&#39;
    scrape_interval: 10s
    metrics_path: /api/capa-metrics
    static_configs:
      - targets: [&#39;xx:3000&#39;, &#39;xx:3000&#39;]</code></pre>
<p>prometheus 설정파일을 보면, 각 collector 들의 할당된 port를 통해 긁어오게 세팅을 해줘야 한다. </p>
<h5 id="-xx-로-되어-있는-host-는-실제-frontend-backend--ec2-instance-ip-이다">* XX 로 되어 있는 host 는 실제 Frontend, Backend  ec2 instance IP 이다</h5>
<h2 id="✅-grafana-dashboard">✅ grafana dashboard</h2>
<p>grafana의 대시보드 datasource 를 prometheus 혹은 postgresql 와 연결하면 수집한 메트익 정보를 다음과 같이 시각화하여 대시보드를 구성할 수 있다. 
<img src="https://velog.velcdn.com/images/leejm_dev/post/8e8e9936-26f6-4dfe-be27-58c1ea2e5232/image.png" alt=""></p>
<p>collector 들로 수집한 메트릭 데이터를 prometheus 를 이용해서 grafana 에서 시각화 하려면 promql(prometheus query) 를 알아야 한다.
일반적으로 rdbms 혹은 nosql 에서 사용하는 언어와는 결이 다르기 때문에 따로 공부를 해야한다. 
<a href="https://prometheus.io/docs/prometheus/latest/querying/basics/">promql 공식 문서</a>
대시보드를 구성하는 방법은 여기서는 따로 언급하지는 않겠다.
<a href="https://grafana.com/docs/grafana/latest/dashboards/">대시보드 구성방법</a></p>
<h2 id="architecture">Architecture</h2>
<p>대시보드 파이프라인을 구축한 전체적인 아키텍처이다. 
<img src="https://velog.velcdn.com/images/leejm_dev/post/6a9fbef9-41ea-4c68-b8c8-7fe9d773fbca/image.png" alt=""></p>
<hr>
<h3 id="ref">Ref</h3>
<p><a href="https://medium.com/devops-dudes/install-prometheus-on-ubuntu-18-04-a51602c6256b">https://medium.com/devops-dudes/install-prometheus-on-ubuntu-18-04-a51602c6256b</a>
<a href="https://schh.medium.com/monitoring-postgresql-databases-using-postgres-exporter-along-with-prometheus-and-grafana-1d68209ca687">https://schh.medium.com/monitoring-postgresql-databases-using-postgres-exporter-along-with-prometheus-and-grafana-1d68209ca687</a>
<a href="https://github.com/willsoto/nestjs-prometheus">https://github.com/willsoto/nestjs-prometheus</a>
<a href="https://github.com/raphaabreu/nestjs-prometheus-requests/blob/main/src/prometheus.interceptor.ts">https://github.com/raphaabreu/nestjs-prometheus-requests/blob/main/src/prometheus.interceptor.ts</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[nestjs with graphql - ch6 (transaction)]]></title>
            <link>https://velog.io/@leejm_dev/nestjs-with-graphql-ch6-transaction</link>
            <guid>https://velog.io/@leejm_dev/nestjs-with-graphql-ch6-transaction</guid>
            <pubDate>Wed, 13 Dec 2023 00:27:46 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/leejm_dev/post/be1ea6f8-9bf2-4c63-bc59-cb4c4696e3b6/image.png" alt=""></p>
<p>3.0 이상의 typeorm 을 가지고 트랜잭션 관리를 해보자. 
레퍼런스가 너무 많아서 그냥 기록용으로 작성하려고 한다. </p>
<pre><code class="language-ts">// room.repository.ts
import { Injectable } from &#39;@nestjs/common&#39;;
import { RoomEntity } from &#39;lib/database/entity&#39;;
import { DataSource, Repository } from &#39;typeorm&#39;;

@Injectable()
export class RoomRepository extends Repository&lt;RoomEntity&gt; {
  constructor(private dataSource: DataSource) {
    super(
      RoomEntity, 
      dataSource.createEntityManager(),
      dataSource.createQueryRunner(),
    );
  }

  async createRoom(name: string): Promise&lt;RoomEntity&gt; {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();
    try {
      const result = await queryRunner.manager.save(RoomEntity, { name: name });
      await queryRunner.commitTransaction();
      return result;
    } catch (error) {
      await queryRunner.rollbackTransaction();
    } finally {
      await queryRunner.release();
    }
  }
}</code></pre>
<p>자세히 보면 생성자 부분을 잘 봐야 한다. 
상속받은 Repository class 에 </p>
<ul>
<li>접근하고자 하는 엔티티 (<code>RoomEntity</code>)</li>
<li>entityManager (<code>dataSource.createEntityManager()</code>)</li>
<li>queryRunner (<code>dataSource.createQueryRunner()</code>)</li>
</ul>
<p>를 주입시켜줘서 사용한다. </p>
<p>위와 같이 작성하는것이 일반적인데, repo 단에서 로직 구현할떄 마다 저렇게 할 수는 없으니, 코드를 줄여보자. </p>
<p>상속받은 Repository class 에는 EntityManager 에 접근할 수 있는 manager 라는 멤버변수가 있다. </p>
<pre><code class="language-ts">export declare class Repository&lt;Entity extends ObjectLiteral&gt; {
    /**
     * Entity target that is managed by this repository.
     * If this repository manages entity from schema,
     * then it returns a name of that schema instead.
     */
    readonly target: EntityTarget&lt;Entity&gt;;
    /**
     * Entity Manager used by this repository.
     */
    readonly manager: EntityManager;
    /**
     * Query runner provider used for this repository.
     */
    readonly queryRunner?: QueryRunner;
    /**
     * Entity metadata of the entity current repository manages.
     */
    get metadata(): import(&quot;..&quot;).EntityMetadata;
    constructor(target: EntityTarget&lt;Entity&gt;, manager: EntityManager, queryRunner?: QueryRunner);
</code></pre>
<p>manager 를 통해서 EntityManager class 에 접근해서 보면 transaction 함수가 보인다. </p>
<pre><code class="language-ts">export declare class EntityManager {
    /**
     * Wraps given function execution (and all operations made there) in a transaction.
     * All database operations must be executed using provided entity manager.
     */
    transaction&lt;T&gt;(runInTransaction: (entityManager: EntityManager) =&gt; Promise&lt;T&gt;): Promise&lt;T&gt;;
    /**
     * Wraps given function execution (and all operations made there) in a transaction.
     * All database operations must be executed using provided entity manager.
     */
    transaction&lt;T&gt;(isolationLevel: IsolationLevel, runInTransaction: (entityManager: EntityManager) =&gt; Promise&lt;T&gt;): Promise&lt;T&gt;;
</code></pre>
<p>설명을 잘 읽어보면, 
<strong><span style="color: #EE4933"><code>주어진 함수를 하나의 transaction 으로 감싸서 실행할 수 있다. 그리고 반드시 주어진  entityManager 를 이용해야 한다.</code></span></strong> 라고 나와있다. </p>
<p>이거를 사용하면 되겠다 생각해서, 다음과 같이 transaction 안에 실행시킬 함수의 구현체를 넣어주면 구현할 수 있다. 
<strong>대신, transaction 안에서는 에러가 발생하면 unhandled error 로 처리되기 때문에 반드시 안에서 try ~ catch 구문으로 에러를 핸들링해줘야 한다.</strong> </p>
<pre><code class="language-ts">try {
      await this.manager.transaction(async (entityManager) =&gt; {
        try {
          await entityManager.save(RoomEntity, { name: name });
        } catch (error) {
          throw new Error(&#39;duplicated key&#39;);
        }
      });
    } catch (error) {
      throw error;
    }</code></pre>
<p>그래서 다음과 같이 구현해냈다. </p>
<pre><code class="language-ts">// room.repository.ts
@Injectable()
export class RoomRepository extends Repository&lt;RoomEntity&gt; {
  constructor(private dataSource: DataSource) {
    super(
      RoomEntity,
      dataSource.createEntityManager(),
      dataSource.createQueryRunner(),
    );
  }

  async getAllRoomIds(): Promise&lt;RoomEntity[]&gt; {
    return await this.find();
  }

  async getRoom(id: string): Promise&lt;RoomEntity&gt; {
    return await this.findOne({
      where: {
        id: id,
      },
    });
  }

  async createRoom(name: any): Promise&lt;void&gt; {
    try {
      await this.manager.transaction(async (entityManager) =&gt; {
        try {
          await entityManager.save(RoomEntity, { name: name });
        } catch (error) {
          throw new Error(&#39;duplicated key&#39;);
        }
      });
    } catch (error) {
      throw error;
    }
  }
}</code></pre>
<hr>
<h3 id="ref">REF</h3>
<p>사진: <a href="https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%EB%85%B9%EC%83%89-%ED%91%9C%EC%8B%9C%EB%93%B1%EC%9D%B4-%EC%BC%9C%EC%A7%84-%EC%8B%A0%ED%98%B8%EB%93%B1-v4BScYmI7Pg?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>의<a href="https://unsplash.com/ko/@yeicdhd?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Yeik CD</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[nestjs with graphql - ch5 (dataloader with orm)]]></title>
            <link>https://velog.io/@leejm_dev/nestjs-with-graphql-ch5-dataloader-with-orm</link>
            <guid>https://velog.io/@leejm_dev/nestjs-with-graphql-ch5-dataloader-with-orm</guid>
            <pubDate>Mon, 11 Dec 2023 02:07:43 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/leejm_dev/post/a33709d0-5658-4359-bdc5-7c223817d1b2/image.png" alt=""></p>
<p>저번에는 실제 디비를 연결하지 않고, 고정 값으로 dataloader 를 구현해봤다면, 이번에는 실제로 디비에 연결해서 dataloader 가 어떻게 돌아가는지 확인해보자. </p>
<p>전체적인 플로우를 먼저 살펴보자
시나리오는 다음과 같다. </p>
<blockquote>
<p>&quot; 전체 채팅방에 대해서 각 채팅방에 속해있는 유저들의 정보를 긁어온다. &quot;
chat resolver → chat service → chat repository → user resolver</p>
</blockquote>
<h2 id="chat-resolver">chat resolver</h2>
<pre><code class="language-ts">@Resolver(() =&gt; RoomModel)
// 이 resolver 는 chatModel 을 뽑아내기 위한 리졸버이다
export class ChatResolver {
  constructor(
    private readonly chatService: ChatService,
    private readonly chatLoader: ChatLoader,
  ) {}

  @Query(() =&gt; [UserModel], { name: &#39;user&#39; })
  init() {}

  /**
   *
   * @description 전체 채팅방과 그 안에 포함된 유저들의 정보를 불러오자
   */

  @Query(() =&gt; [RoomModel])
  async getAllRoomInfo(): Promise&lt;RoomModel[]&gt; {
    return await this.chatService.getAllRoomIds();
  }

  /**
   * @description user resolver 로 필요한 정보를 얻기 위한 쿼리 전송
   * roomId 로 그 안에 속해있는 유저들의 정보를 긁어와야 하는데,
   * resolveField 호출은 여러번 해도 실제 디비에서 조회하는 로직은 한번만 할 수 있도록
   */
  @ResolveField(&#39;users&#39;, () =&gt; [UserModel])
  async getUsers(@Parent() room: RoomModel): Promise&lt;UserModel[]&gt; {
    try {
      const result = await this.chatLoader.findByUserId.load(room.roomId);
      return result;
    } catch (error) {
      this.chatLoader.findByUserId.clear(room.roomId);
    }
  }
}</code></pre>
<h2 id="chat-service">chat service</h2>
<pre><code class="language-ts">@Injectable()
export class ChatService {
  constructor(private readonly roomRepo: RoomRepository) {}
  findAll() {
    return `This action returns all chat`;
  }

  async getAllRoomIds(): Promise&lt;RoomModel[]&gt; {
    const rooms = await this.roomRepo.getAllRoomIds();
    const result: RoomModel[] = [];
    for (const r of rooms) {
      result.push({
        roomId: r.id.toString(),
      });
    }
    return result;
  }
}</code></pre>
<h2 id="chat-repository">chat repository</h2>
<pre><code class="language-ts">@Injectable()
export class RoomRepository extends Repository&lt;RoomEntity&gt; {
  constructor(private dataSource: DataSource) {
    super(RoomEntity, dataSource.createEntityManager());
  }

  async getAllRoomIds(): Promise&lt;RoomEntity[]&gt; {
    return await this.createQueryBuilder().getMany();
  }
}</code></pre>
<h2 id="user-resolver">user resolver</h2>
<pre><code class="language-ts">@Resolver(() =&gt; UserModel)
export class UserResolver {
  constructor(private readonly userService: UserService) {}

  @Query(() =&gt; [UserModel], { name: &#39;user&#39; })
  init() {}

  /**
   *
   * @description chat resolver 에서 처리하지 못하는 쿼리를 여기서 처리함 (find user)
   * resolverField 를 여기서 수신해서 처리한다.
   */
  @ResolveReference()
  async resolveReference(reference: {
    __typename: string;
    id: string;
  }): Promise&lt;UserModel&gt; {
    return await this.userService.getUser(reference.id);
  }
}</code></pre>
<h2 id="user-service">user service</h2>
<pre><code class="language-ts">@Injectable()
export class UserService {
  constructor(private readonly userRepo: UserRepository) {}
  async getUser(userId: string): Promise&lt;UserModel&gt; {
    const user = await this.userRepo.getUser(userId);
    const result: UserModel = {
      id: user.id,
      name: user.name,
    };
    return result;
  }
}</code></pre>
<h2 id="user-repository">user repository</h2>
<pre><code class="language-ts">@Injectable()
export class UserRepository extends Repository&lt;UserEntity&gt; {
  constructor(private dataSource: DataSource) {
    super(UserEntity, dataSource.createEntityManager());
  }

  async getUser(userId: string): Promise&lt;UserEntity&gt; {
    return await this.createQueryBuilder().where({ id: userId }).getOne();
  }
}</code></pre>
<h2 id="db-schema">db schema</h2>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/73e48a83-3077-49c8-bfc6-03ffce8746e2/image.png" alt=""></p>
<h2 id="play-ground-result">play ground result</h2>
<pre><code class="language-yaml">{
  &quot;data&quot;: {
    &quot;getAllRoomInfo&quot;: [
      {
        &quot;roomId&quot;: &quot;23bfddea-ef11-413f-940d-0101fc8ab23c&quot;,
        &quot;users&quot;: [
          {
            &quot;id&quot;: &quot;ab5e6db1-2054-4fd2-9066-4ad1e407e903&quot;,
            &quot;name&quot;: &quot;tom&quot;
          },
          {
            &quot;id&quot;: &quot;268311cb-66ce-486c-85c4-1bc6e0adbd6a&quot;,
            &quot;name&quot;: &quot;ciara&quot;
          },
          {
            &quot;id&quot;: &quot;323d22e7-0edd-4d4f-ae3b-cb8b7ce42a97&quot;,
            &quot;name&quot;: &quot;json&quot;
          },
          {
            &quot;id&quot;: &quot;ba176fc3-b860-4cae-9703-c9030212f5e7&quot;,
            &quot;name&quot;: &quot;daniel&quot;
          }
        ]
      },
      {
        &quot;roomId&quot;: &quot;68e75332-c884-4f20-b05a-2795bf03945a&quot;,
        &quot;users&quot;: [
          {
            &quot;id&quot;: &quot;9220a7f9-c958-48b7-80ff-15d1b462ae5a&quot;,
            &quot;name&quot;: &quot;jonny&quot;
          },
          {
            &quot;id&quot;: &quot;7c7c2b9d-337c-4da7-b0b1-3f88b1adc486&quot;,
            &quot;name&quot;: &quot;hardy&quot;
          }
        ]
      },
      {
        &quot;roomId&quot;: &quot;1ec747ab-b761-4889-89bb-6c6516f4874d&quot;,
        &quot;users&quot;: []
      }
    ]
  }
}</code></pre>
<h2 id="repository-">repository ???</h2>
<p>여기서 주의 깊게 봐야할 부분은 <code>repository</code> 이다
<strong>현재 나는 typeorm 버전이 3.0.0 이상이다</strong>
대부분의 사람들이 알겠지만, typeOrm 버전이 올라가면서 <code>@EntityRepository()</code> 가 없어졌다. 
구글링을 해보면 다들 custom repository 를 만들어서, <code>@InjectRepository()</code> 를 이용해서 각 모듈마다 주입을 해주는 방법을 구현하고 있는데, 생각보다 너무 복잡해서 굳이 이렇게까지 어렵게 구현하라고 nest 가 <code>@EntityRepository()</code> 를 없애진 않았을 것이다. </p>
<p>찾아보다가 좋은 방법을 찾았다. 
<a href="https://stackoverflow.com/questions/72549668/how-to-do-custom-repository-using-typeorm-mongodb-in-nestjs">https://stackoverflow.com/questions/72549668/how-to-do-custom-repository-using-typeorm-mongodb-in-nestjs</a></p>
<p>위에 나와있는 repository 코드가 다음 레퍼런스를 참고해서 만들었다. 
생성자 부분을 잘 보면, 필요한 엔티티를 부모 클래스인 Repository 로 넘겨주면 provider 로 사용할 수 있게 된다. </p>
<p>여기서 transaction 은 어떻게 처리하는지는 다음에 알아보겠다. </p>
<hr>
<h3 id="ref">REF</h3>
<p>사진: <a href="https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%EA%B3%B5%EA%B5%AC-%EC%84%A0%EB%B0%98%EC%97%90-%EC%9E%88%EB%8A%94-%EB%B6%84%EB%A5%98%EB%90%9C-%ED%9C%B4%EB%8C%80%EC%9A%A9-%EA%B3%B5%EA%B5%AC-t5YUoHW6zRo?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>의<a href="https://unsplash.com/ko/@barnimages?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Barn Images</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Rabbitmq unAcked message]]></title>
            <link>https://velog.io/@leejm_dev/Rabbitmq-unAcked-message</link>
            <guid>https://velog.io/@leejm_dev/Rabbitmq-unAcked-message</guid>
            <pubDate>Thu, 07 Dec 2023 06:48:59 GMT</pubDate>
            <description><![CDATA[<p>현재 서비스에서 결제 서비스와 대출 서비스를 운영하고 있다. 
결제 서비스, 대출 서비스 모두 결제 및 대출이 일어나면 해당 액션에 대한 트리거로 웹훅을 전송해주는 모듈이다. </p>
<h5 id="예를-들어서-결제가-일어났다-그러면-결제완료-혹은-대출을-신청했다-그러면-대출신청-이라는-이벤트를-웹훅으로-전송해준다">예를 들어서, 결제가 일어났다 그러면 &#39;결제완료&#39; 혹은 대출을 신청했다 그러면 &#39;대출신청&#39; 이라는 이벤트를 웹훅으로 전송해준다.</h5>
<p>우리 서비스에 녹여져있는 webhook flow 를 보기 전에 rabbitmq 에 대해서 간단하게 알아보자.</p>
<h2 id="✅-what-is-rabbitmq">✅ What is RabbitMQ</h2>
<p>비동기 작업을 하기 위해서 주로 사용하는 메세지큐의 종류 중 하나이다. 
ex) rabbitmq, kafka, etc...</p>
<pre><code class="language-sh">            ------------ broker ------------
producer -&gt; [exchange -&gt; queue(binding rule)] -&gt; comsumer</code></pre>
<p>producer 가 메세지를 발송하면, 무조건 모든 메세지는 broker의 exchange 로 전달된다. 
exchange 는 어떤 queue 로 보내줄지 binding rule 에 의거해서 queue 로 메세지를 복사하고 이를 consumer 가 수신한다. </p>
<h3 id="consumer-가-message-를-처리하는-방법">consumer 가 message 를 처리하는 방법</h3>
<ol>
<li><p>consumer 가 메세지를 받기만 하면 큐에서 메세지를 삭제한다.</p>
<ul>
<li>msa 안에서 MQ 를 사용해서 [gateway -&gt; microservivce] 같이 내부 api request 를 처리할 때 주로 사용할 수 있다.
내부 api에서 일어나기 때문에 언제든 다시 발송하게 할 수 있기 때문</li>
</ul>
</li>
<li><p>consumer 가 메세지를 받고 잘 받았다는 신호를 보내면 큐에서 삭제한다. 
 이 신호를 <strong><span style="color:orange"><code>consumer acknowledge</code></span></strong> 라고 한다.</p>
<ul>
<li>외부 모듈 혹은 외부 api 와 통신할 때 주로 사용한다. 
정상적으로 정해진 로직으로 처리를 하지 못하거나 시스템 적으로 문제가 발생하면 다시 수신을 해서 처리를 해야 하는 경우가 생기기 때문.</li>
</ul>
</li>
</ol>
<h3 id="exchange-종류">exchange 종류</h3>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/93f73a57-637a-42dd-905c-f46af07860d3/image.png" alt=""></p>
<h3 id="binding">binding</h3>
<p>각 exchange 에는 어떤 큐에 어떤 방식으로 메세지를 보낼지에 대한 규칙을 지정할 수 있다. 이 규칙은 binding 이라고 한다. </p>
<h2 id="✅-webhook-flow">✅ webhook flow</h2>
<p>해당 웹훅을 우리 서비스(api server)가 점검중이거나 혹은 예기치 못한 상황에 전송을 할 수도 있기 때문에, API Gateway 가 트리거로 걸린 람다 함수를 사용하고 있다. 아래는 해당 AWS lambda diagam 이다
<img src="https://velog.velcdn.com/images/leejm_dev/post/20610e0b-1f75-49e8-9cac-7bb450643d0b/image.png" alt=""></p>
<p>1.결제 및 모듈에서 우리가 제공한 API Gateway 의 엔드포인트로 웹훅을 전송하면 
2.그에 할당된 람다함수가 작동하여 <code>RabbitMQ</code> 로 메세지를 날린다. 
3.그러면 서비스 API 서버가 해당 MQ에서 메세지를 가져간다. 
4.메세지를 수신하고 로직을 처리하고 나서 <code>Consumer Acknowledge</code> 를 MQ 쪽으로 반환해준다. </p>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/2efc1727-520a-4472-a7f4-104ff5936a1d/image.png" alt=""></p>
<h2 id="✅-subscribe--publish-message">✅ subscribe &amp; publish message</h2>
<pre><code class="language-ts">@MessagePattern({ cmd: &#39;webHook&#39; })
  async webHook(@Ctx() context: RmqContext): Promise&lt;void&gt; {
    const content = JSON.parse(context.getMessage().content);
    const data = content?.data;
    const jsonData = data?.body;

    const mqChannel = context.getChannelRef();
    try {
      await this.service.webHook(jsonData);
      mqChannel.ack(context.getMessage());
    } catch (error) {
      mqChannel.ack(context.getMessage());
    }
  }</code></pre>
<p><code>mqChannel.ack()</code> 를 통해서 
&quot;나 메세지 잘 수신했고, 잘 처리했어~&quot; 라고 MQ 에 다시 알려준다. 
그러면 해당 메세지를 MQ 는 다시 publish 하지 않는다. 
이 말을 반대로 생각하면, 
*<em><code>mqChannel.ack()</code> 를 하지 않는다면, &quot;잘 못 받았나? 무슨 문제가 있나?&quot; 라고 간주해서 반복적으로 Publish 한다. *</em></p>
<h2 id="✅-publish-noack-message">✅ publish noAck message</h2>
<p>테스트로나 서비스로나 한번도 발송하지 않은 메세지들이 계속 위 코드를 통해서 수신하고 있던 것을 확인할 수 있었다. </p>
<pre><code class="language-yaml">[Nest] 1  - 12/07/2023, 4:58:48 AM   ERROR [RpcExceptionsHandler] Unexpected token o in JSON at position 1
SyntaxError: Unexpected token o in JSON at position 1 at JSON.parse (&lt;anonymous&gt;)</code></pre>
<p>원치 않은 형식의 message 가 날라와서 파싱하는데 있어서 계속 문제가 발생했다. 
근데 이게 30분 마다 로그에 반복적으로 찍혔다. 
혹시 noAck message 를 계속 전송하는건가?
생각해보니, 개발환경에 올리기 전에 로컬에서 테스트 했었는데, 이때 람다를 자체적으로 실행하기 위해서 input message 형태를 임의로 수정해서 테스트를 했었던 메세지가 아직도 수신되고 있었던 것이었다. 
<strong>2023.11.29 일에 publish 된 메세지가 2023.12.07 에도 수신하고 있었다.</strong>
aws console 창을 확인해보니 다음과 같았다. 
<img src="https://velog.velcdn.com/images/leejm_dev/post/e1721450-a48f-4bd4-ab32-2d803a574c34/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/97ac1020-3a43-434d-9629-db4ec0738357/image.png" alt=""></p>
<ul>
<li>첫번째 이미지를 보면, Redelivered 메세지가 반복적으로 날라오고 있었다. consumer가 ack 처리를 하지 않아 재전송한 것으로 보인다.</li>
<li>두번째 이미지를 보면 해당 메세지를 두번째 채널에서 Unacked 처리한 메세지가 하나 있다는 것을 알 수 있다.</li>
</ul>
<p>리소스 낭비를 막기 위해 해당 메세지를 큐에서 지우기 위해 찾아보니 아래 화면에서 가장 하단부에 위치한 <code>Purge messages</code> 를 통해서 특정 큐에 존재하는 메세지를 지울 수 있다고 한다. </p>
<h4 id="아래는-aws-rabbitmq-console-이다">아래는 aws rabbitmq console 이다.</h4>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/d9757833-d012-4a75-adec-1b062b2a6bc4/image.png" alt=""></p>
<p>하지만 그래도 계속 위 에러가 발생했다. 
에러가 발생하고 나서 콘솔창을 확인해보니 계속해서 <code>Unack</code> 처리된 메세지가 있고, <code>Redelivered</code> 를 하고 있었다.</p>
<h2 id="✅-how-to-solve">✅ how to solve</h2>
<p>unAck message 가 할당된 channel 을 먼저 close 하고 나서, 그 후에 purge message 를 하라고 나와있다. 개발환경에서 해당 channel에 연결중이고, 결제 docker container 만 잠시 내리고 purge 했다. </p>
<h5 id="다른-방법으로-jsonparse-가-실패해도-무조건-ack-처리를-하는것도-나쁘지-않을듯-하다-애초에-정해진-메세지가-아닌-이상한-형태의-메세지가-왔다는-것-자체가-해당-메세지로-로직을-처리할-필요가-없으니까">다른 방법으로 JSON.parse 가 실패해도 무조건 ack 처리를 하는것도 나쁘지 않을듯 하다. 애초에 정해진 메세지가 아닌 이상한 형태의 메세지가 왔다는 것 자체가 해당 메세지로 로직을 처리할 필요가 없으니까.</h5>
<p>해결.</p>
<hr>
<h3 id="ref">Ref</h3>
<p><a href="https://stackoverflow.com/questions/25114230/rabbitmq-purge-a-queue-from-all-of-its-unacked-messages">https://stackoverflow.com/questions/25114230/rabbitmq-purge-a-queue-from-all-of-its-unacked-messages</a>
<a href="https://jonnung.dev/rabbitmq/2019/02/06/about-amqp-implementtation-of-rabbitmq/#gsc.tab=0">https://jonnung.dev/rabbitmq/2019/02/06/about-amqp-implementtation-of-rabbitmq/#gsc.tab=0</a>
<a href="https://www.rabbitmq.com/tutorials/amqp-concepts.html">https://www.rabbitmq.com/tutorials/amqp-concepts.html</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[nestjs with graphql - ch4 (dataloader)]]></title>
            <link>https://velog.io/@leejm_dev/nestjs-with-graphql-ch4-dataloader</link>
            <guid>https://velog.io/@leejm_dev/nestjs-with-graphql-ch4-dataloader</guid>
            <pubDate>Tue, 05 Dec 2023 00:14:14 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/leejm_dev/post/53e41c09-ecac-4e53-a414-56c141b0a001/image.png" alt=""></p>
<p>graphql 에서 그냥 무작정으로 resolveField 를 사용하면 N+1 문제가 발생할 수 있는 경우가 있다. 
여기서 N+1 문제란, </p>
<blockquote>
<p>예를 들어, 채팅방에 여러 사람이 있는데 여러 사람의 정보를 긁어오기 위해 채팅방은 1개의 정보만 조회하지만, 그 후에 해당 채팅방의 사람 수(N) 만큼 정보를 조회해야 하는 경우를 뜻 한다.</p>
</blockquote>
<p>그래서 <code>@ResolveField</code> 를 잘~ 써야 하는데,</p>
<p>만약에 한 쿼리에서 join을 통해서 데이터를 가져오면 되지 않을까? 싶지만, 그렇게 되면 client 의 요청에서는 over fetching 은 일어나지 않지만, back단에서 over fetching 이 일어난다. client에서는 원하지 않을 수 있는 데이터를 back 단에서 미리 조인을 걸어서 데이터를 가져오기 때문에 그렇다. </p>
<p>즉, join 을 사용하지 않고, @ResolveField 를 영리하게 사용을 하려면 <code>dataloader</code> 를 사용해야 하는데, dataloader 에 대해서 좀 자세하게 알아보자</p>
<h2 id="dataloader">dataloader</h2>
<p>dataloader 는 데이터를 조회하는데 있어서 발생하는 N+1 문제를 <code>batching</code>, <code>caching</code> 을 통해서 1+1 로 해결해주는 모듈이다. </p>
<h3 id="✅-batching">✅ batching</h3>
<p>N번 실행하지 않기 위해서 여러 개의 key값을 배열로 받아서 해당 key 값의 value 를 매핑한 promise array object 을 생성한다. 그리고 각각의 value 들을 반환한다. 즉, promise array object 전체를 반환하는 것이 아닌, 각 value 들을 각각 반환한다. 
아래는 dataloader 공식 git repository 에서 가져온 글이다.</p>
<blockquote>
<h5 id="a-batch-loading-function-accepts-an-array-of-keys-and-returns-a-promise-which-resolves-to-an-array-of-values">A batch loading function accepts an Array of keys, and returns a Promise which resolves to an Array of values.</h5>
<h5 id="then-load-individual-values-from-the-loader-dataloader-will-coalesce-all-individual-loads-which-occur-within-a-single-frame-of-execution-a-single-tick-of-the-event-loop-and-then-call-your-batch-function-with-all-requested-keys">Then load individual values from the loader. DataLoader will coalesce all individual loads which occur within a single frame of execution (a single tick of the event loop) and then call your batch function with all requested keys.</h5>
</blockquote>
<p>여기서 지켜야 할 점들이 있다. </p>
<ul>
<li>value 들의 총 길이는 key 들의 총 길이와 같아야 한다. </li>
<li>value 들의 인덱스 순서는 key 들의 인덱스 순서와 같아야 한다.</li>
</ul>
<p>이 부분에 대해서는 아래 예시에서 확인해보겠다. </p>
<h3 id="✅-caching">✅ caching</h3>
<p>dataloader 는 key 를 바탕으로 캐싱처리를 해준다. 
<code>.load()</code> 를 통해서 캐싱처리를 해준다고 생각해준다. </p>
<p>주어진 key 값을 기반으로 캐싱으로 하기 때문에, 반복된 요청으로 부터 빠른 반환 시간을 보일 수 있지만, <strong>다른 요청으로 부터 캐싱된 데이터의 원본이 수정이 되더라도,</strong> 반복된 요청으로 부터 동일한 값이 반환될 수 밖에 없다. 그렇기 때문에 요청이 올때 마다 dataloader instance 를 생성해주는 것을 권장한다. </p>
<pre><code class="language-ts">const result = await this.chatLoader.findByUserId.load(room.roomId);
console.log(this.chatLoader.findByUserId);</code></pre>
<pre><code class="language-yaml">DataLoader {
  _batchLoadFn: [AsyncFunction (anonymous)],
  _maxBatchSize: Infinity,
  _batchScheduleFn: [Function (anonymous)],
  _cacheKeyFn: [Function (anonymous)],
  _cacheMap: Map(3) {
    4 =&gt; Promise { [Array] },
    5 =&gt; Promise { [Array] },
    6 =&gt; Promise { [Array] }
  },
  _batch: {
    hasDispatched: true,
    keys: [ 4, 5, 6 ],
    callbacks: [ [Object], [Object], [Object] ]
  },
  name: null
}
DataLoader {
  _batchLoadFn: [AsyncFunction (anonymous)],
  _maxBatchSize: Infinity,
  _batchScheduleFn: [Function (anonymous)],
  _cacheKeyFn: [Function (anonymous)],
  _cacheMap: Map(3) {
    4 =&gt; Promise { [Array] },
    5 =&gt; Promise { [Array] },
    6 =&gt; Promise { [Array] }
  },
  _batch: {
    hasDispatched: true,
    keys: [ 4, 5, 6 ],
    callbacks: [ [Object], [Object], [Object] ]
  },
  name: null
}
DataLoader {
  _batchLoadFn: [AsyncFunction (anonymous)],
  _maxBatchSize: Infinity,
  _batchScheduleFn: [Function (anonymous)],
  _cacheKeyFn: [Function (anonymous)],
  _cacheMap: Map(3) {
    4 =&gt; Promise { [Array] },
    5 =&gt; Promise { [Array] },
    6 =&gt; Promise { [Array] }
  },
  _batch: {
    hasDispatched: true,
    keys: [ 4, 5, 6 ],
    callbacks: [ [Object], [Object], [Object] ]
  },
  name: null
}</code></pre>
<p>위 결과를 보면, dataloder 는 자체적으로 <code>cacheMap</code> 을 가지고 있어 key 값을 바탕으로 캐시를 적용하는 것을 알 수 있다. 
room.roomId 가 4,5,6이 들어갈 수 있는데, 이를 키값으로 가지고 있고, 또한 value 값을 promise 배열 객체로 가지고 있는 것을 알 수 있다. 
즉, 4,5,6 의 value 를 모두 resolve 하고 나서의 결과가 콘솔로 찍히는 것을 볼 수 있다. </p>
<p><strong>dataloader 는 결국, 들어오는 모든 값들을 resolve 하고 난 결과를 뱉어내고, 이를 호출하는 부분에서 pending 상태로 대기하다가 resolve 되고 나서 결과를 받아볼 수 있다.</strong></p>
<h4 id="캐시-사용-안하기----cache-false">캐시 사용 안하기 - { cache: false}</h4>
<pre><code class="language-ts">@Injectable({ scope: Scope.REQUEST })
export class ChatLoader {
  findByUserId = new DataLoader&lt;number, UserModel[]&gt;(
    async (roomIds: number[]) =&gt; {
      // 여기 있는 roomIds 에 있는 아이템들이 key 가 된다.
      ... context
    { cache: false },
  );
}</code></pre>
<h4 id="캐시-전부-삭제하기---clear--clearall">캐시 전부 삭제하기 - clear &amp; clearAll</h4>
<pre><code class="language-ts">@ResolveField(&#39;users&#39;, () =&gt; [UserModel])
  async getUsers(@Parent() room: RoomModel): Promise&lt;UserModel[]&gt; {
    try {
      await this.chatLoader.findByUserId.clear(room.roomId);
      await this.chatLoader.findByUserId.clearAll(room.roomId);
    } catch (error) {
      this.chatLoader.findByUserId.clear(room.roomId);
    }
  }</code></pre>
<h3 id="✅-소스코드">✅ 소스코드</h3>
<pre><code class="language-ts">// chat.resolver.ts
@Query(() =&gt; [RoomModel])
  async getRoomInfo(
    @Args(&#39;roomId&#39;, { type: () =&gt; Int }) roomId: number,
  ): Promise&lt;RoomModel[]&gt; {
    return [
      {
        roomId: roomId + 1,
      },
      {
        roomId: roomId + 2,
      },
      {
        roomId: roomId + 3,
      },
    ];
  }

@ResolveField(&#39;users&#39;, () =&gt; [UserModel])
  async getUsers(@Parent() room: RoomModel): Promise&lt;UserModel[]&gt; {
    try {
      const result = await this.chatLoader.findByUserId.load(room.roomId);
      console.log(result);
      return result;
    } catch (error) {
      this.chatLoader.findByUserId.clear(room.roomId);
    }
  }


// chat.loader.ts
@Injectable({ scope: Scope.REQUEST })
export class ChatLoader {
  findByUserId = new DataLoader&lt;number, UserModel[]&gt;(
    async (roomIds: number[]) =&gt; {
      // 여기 있는 roomIds 에 있는 아이템들이 key 가 된다.
      const userModels = {
        4: [{ id: &#39;4&#39; }, { id: &#39;8&#39; }, { id: &#39;12&#39; }],
        5: [{ id: &#39;5&#39; }, { id: &#39;10&#39; }, { id: &#39;15&#39; }],
        6: [{ id: &#39;6&#39; }, { id: &#39;12&#39; }, { id: &#39;18&#39; }],
      };
      const userModelGroup: { [key: number]: UserModel[] } = {};
      for (const roomId of roomIds) {
        if (!userModelGroup[roomId]) {
          userModelGroup[roomId] = [];
        }
        userModelGroup[roomId] = userModels[roomId] ?? [];
      }
      const result = roomIds.map((roomId: number) =&gt; userModelGroup[roomId]);

      console.log(result);
      return result;
    },
    { cache: true },
  );
}

// user.resolver.ts
@ResolveReference()
  resolveReference(reference: { __typename: string; id: string }): UserModel {
    return {
      id: reference.id,
      name: `${reference.id} + name`,
    };
  }</code></pre>
<p>채팅방 4,5,6 에 속해있는 유저의 정보를 조회한다고 가정을 해보자.
resolveField 를 통해서 채팅방 하나에 속해있는 유저들의 정보를 가져와야 하는데, dataloader 를 사용하지 않는다고 하면, resolveField에서 
<code>roomId:4</code> 에 속한 유저들의 정보를 가져오고, 
<code>roomId:5</code> 에 속한 유저들의 정보를 가져오고, 
<code>roomId:6</code> 에 속한 유저들의 정보를 가져오고...
N+1 문제가 발생할 수 있다. </p>
<p>그래서 @ResolveField 쿼리가 3번 호출되더라도, 실제 디비에서 데이터를 조회하는 로직은 한번만 할 수 있도록 dataloader 를 사용해보자. </p>
<br>
여기서는 실제 디비는 조회하지 않고, 조회를 했다고 가정해보자.

<pre><code class="language-ts">await this.chatLoader.findByUserId.load(room.roomId)</code></pre>
<p>여기서 실제 인자값으로는 roomId 가 한개씩 들어가지만, dataloader 의 <strong><span style="color:orange">batching</span></strong> 특징으로 인해 </p>
<pre><code class="language-ts">async (roomIds: number[])</code></pre>
<p>으로 <code>[4,5,6]</code> 의 채팅방id 들의 배열이 들어간다. </p>
<p>그로인해, 
채팅방에 속한 유저들의 정보가 다음과 같다고 가정을 해보자
채팅방 4에 속한 유저들은 4, 8, 12 
채팅방 5에 속한 유저들은 5, 10, 15
채팅방 6에 속한 유저들은 6, 12, 18</p>
<pre><code class="language-ts">const userModels = {
  4: [{ id: &#39;4&#39; }, { id: &#39;8&#39; }, { id: &#39;12&#39; }],
  5: [{ id: &#39;5&#39; }, { id: &#39;10&#39; }, { id: &#39;15&#39; }],
  6: [{ id: &#39;6&#39; }, { id: &#39;12&#39; }, { id: &#39;18&#39; }],
};</code></pre>
<p>dataloader 를 통한 결과 값은 아래와 같다. </p>
<pre><code class="language-ts">const result = roomIds.map((roomId: number) =&gt; userModelGroup[roomId]);
console.log(result);
return result;</code></pre>
<pre><code class="language-yaml">[
  [ { id: &#39;4&#39; }, { id: &#39;8&#39; }, { id: &#39;12&#39; } ],
  [ { id: &#39;5&#39; }, { id: &#39;10&#39; }, { id: &#39;15&#39; } ],
  [ { id: &#39;6&#39; }, { id: &#39;12&#39; }, { id: &#39;18&#39; } ]
]</code></pre>
<p>하지만 해당 dataloader 를 호출한 부분에서 console을 찍어보면 다음과 같다. </p>
<pre><code class="language-ts">const result = await this.chatLoader.findByUserId.load(room.roomId);
console.log(result);</code></pre>
<pre><code class="language-yaml">[ { id: &#39;4&#39; }, { id: &#39;8&#39; }, { id: &#39;12&#39; } ]
[ { id: &#39;5&#39; }, { id: &#39;10&#39; }, { id: &#39;15&#39; } ]
[ { id: &#39;6&#39; }, { id: &#39;12&#39; }, { id: &#39;18&#39; } ]</code></pre>
<p>🔥🔥🔥
<strong>이걸 보면 결국 dataloader 에서 디비에서 조회는 한번만 해서 배열의 결과를 반환하지만, 호출부에서는 인자값으로 <code>room.roomId</code> 처럼 한개씩만 보냈기 때문에, 결과도 채팅방 하나의 속해있는 유저 정보를 조회하고 이를 @ResolveReference() 에서 수신해서 쿼리를 실행하는 것을 알 수 있다.</strong></p>
<p><strong>그래서 dataloader 의 키값이랑 반환값의 사이즈가 같아야 하고, 순서가 보장되어야 한다는 것을 여기서 알 수 있다</strong>
🔥🔥🔥</p>
<h3 id="✅-playground-결과">✅ playground 결과</h3>
<pre><code class="language-yaml">{
  &quot;data&quot;: {
    &quot;getRoomInfo&quot;: [
      {
        &quot;roomId&quot;: 4,
        &quot;users&quot;: [
          {
            &quot;id&quot;: &quot;4&quot;,
            &quot;name&quot;: &quot;4 + name&quot;
          },
          {
            &quot;id&quot;: &quot;8&quot;,
            &quot;name&quot;: &quot;8 + name&quot;
          },
          {
            &quot;id&quot;: &quot;12&quot;,
            &quot;name&quot;: &quot;12 + name&quot;
          }
        ]
      },
      {
        &quot;roomId&quot;: 5,
        &quot;users&quot;: [
          {
            &quot;id&quot;: &quot;5&quot;,
            &quot;name&quot;: &quot;5 + name&quot;
          },
          {
            &quot;id&quot;: &quot;10&quot;,
            &quot;name&quot;: &quot;10 + name&quot;
          },
          {
            &quot;id&quot;: &quot;15&quot;,
            &quot;name&quot;: &quot;15 + name&quot;
          }
        ]
      },
      {
        &quot;roomId&quot;: 6,
        &quot;users&quot;: [
          {
            &quot;id&quot;: &quot;6&quot;,
            &quot;name&quot;: &quot;6 + name&quot;
          },
          {
            &quot;id&quot;: &quot;12&quot;,
            &quot;name&quot;: &quot;12 + name&quot;
          },
          {
            &quot;id&quot;: &quot;18&quot;,
            &quot;name&quot;: &quot;18 + name&quot;
          }
        ]
      }
    ]
  }
}</code></pre>
<hr>
<h3 id="ref">REF</h3>
<p><a href="https://github.com/graphql/dataloader">https://github.com/graphql/dataloader</a>
<a href="https://y0c.github.io/2019/11/24/graphql-query-optimize-with-dataloader/">https://y0c.github.io/2019/11/24/graphql-query-optimize-with-dataloader/</a>
사진: <a href="https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%EB%82%AE-%EB%8F%99%EC%95%88-%EA%B0%88%EC%83%89-%EB%93%A4%ED%8C%90%EC%97%90-%EB%85%B8%EB%9E%80%EC%83%89%EA%B3%BC-%EA%B2%80%EC%9D%80%EC%83%89-%EC%A4%91%EC%9E%A5%EB%B9%84-N1LBcqLP9ec?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>의<a href="https://unsplash.com/ko/@zacedmo?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Zac Edmonds</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[nestjs with graphql - ch3 (ResolveField for array object)
]]></title>
            <link>https://velog.io/@leejm_dev/nestjs-with-graphql-ch3-ResolveField-for-array-object</link>
            <guid>https://velog.io/@leejm_dev/nestjs-with-graphql-ch3-ResolveField-for-array-object</guid>
            <pubDate>Sat, 02 Dec 2023 07:04:10 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/leejm_dev/post/e6567cfa-00de-4375-b96a-fb021295170f/image.png" alt=""></p>
<p>저번 글에서 <code>@ResolveField()</code> 를 통해서 추가 필드값을 받고, 해당 필드를 구현하기 위해서 <code>@ResolveReference()</code> 로 받아서 로직을 처리하는 방법을 구현했었다. </p>
<p>해당 내용을 공부해보니, 
<strong>&quot;이렇게 하면 N+1 문제가 발생해서 <code>DataLoader</code> 를 사용하면 문제를 해결할 수 있다~&quot;</strong>
라는 글을 많이 봤는데, N+1 문제로 인해 쿼리 지연이 발생하려면, resolveField 가 배열, 즉 여러 값이 나와야 그러한 문제가 발생할 수 있겠다~ 라고 생각해서, 저번에는 필드가 배열이 아닌 하나의 값으로 구현을 했는데 이번에는 배열로 한번 받아보자 라는 생각에 배열로 구현을 해보기로 했다. </p>
<p>그런데, 아무리 해도 계속 배열로 뭔가 받질 못하는 것 같아서 수 많은 구글링과 phind 및 gpt 를 찾아봐도 내가 원하는 결과를 도출해낼 수 없었다. </p>
<blockquote>
<p>구현하고자 하는 내용을 말하자면, 하나의 채팅방에 여러명의 유저가 있을 수 있고, 그러한 유저들의 정보를 채팅방 정보와 같이 조회를 하기 위한 기능이었다. </p>
</blockquote>
<p>그래서 근본적으로 생각을 해봤는데,</p>
<ol>
<li>일단 ! 필드 자체는 배열이긴 하다.(하나의 채팅방에 유저는 여러명이니까) 그러면 이 resolveReference 가 쿼리를 여러번 수신해야 하는건가?
👉 이건 아무리 생각해도 말이 안된다. 그래프큐엘을 만든 팀에서 과연 그렇게 비효율적으로 만들었을까</li>
<li>그렇다면 일단 resolveField 는 배열을 반환하는 것은 맞다. </li>
<li>그리고 이거를 resolveReference 도 결국에는 한번만 받고, 그 배열을 풀어내기 위한 로직을 여러번 하는건가</li>
<li>이거다 !</li>
</ol>
<p>결국에 구현해냈다. 물론 디비를 연결해서 실제로 데이터 커넥션을 일으킨건 아니지만, 전체적인 흐름을 파악할 수 있게 되었다. </p>
<pre><code class="language-ts">/// chat.resolver.ts
@ResolveField(&#39;users&#39;, () =&gt; [AccountModel])
  async getUsers(
    @Parent() chat: RoomModel,
  ): Promise&lt;{ __typename: string; id: number }[]&gt; {
    console.log(chat.roomId);
    const userIds: number[] = [1, 2, 3];
    return [
      { __typename: &#39;RoomModel&#39;, id: userIds[0] },
      { __typename: &#39;RoomModel&#39;, id: userIds[1] },
      { __typename: &#39;RoomModel&#39;, id: userIds[2] },
    ];
  }</code></pre>
<p>특정 채팅방의 roomId 를 이용해서 디비에서 조회를 한 후, 해당 채팅방에 존재 하는 유저들의 id 가 1,2,3 이라고 하자. 
이 유저들의 정보를 <code>chat.resolver</code> 에서 조회하는 것이 아닌, <code>account.resolver</code> 에서 조회하기 때문에, <code>account.resolver</code> 쪽으로 array object 을 보내준다. </p>
<pre><code class="language-ts">@ResolveReference()
  resolveReference(reference: {
    __typename: string;
    id: string;
  }): AccountModel {
    return {
      id: reference.id,
      name: `${reference.id} + name`,
    };
  }</code></pre>
<p>그러면 여기서 받아서 처리를 해주는데, 배열안에 아이템이 3개가 있기 때문에, <code>@ResolveReference</code> 를 3번 처리해줘야 한다. 
이렇게 하나를 처리하기 위해 여러번의 쿼리를 실행할 경우 N+1 문제가 발생할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/f11723c8-afce-4980-ad66-2914d2e8e3f8/image.png" alt=""></p>
<hr>
<h3 id="ref">REF</h3>
<p><a href="https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%ED%9A%8C%EC%83%89-%ED%83%9C%EC%96%91-%EC%A0%84%EC%A7%80%ED%8C%90-%EB%A1%9C%ED%8A%B8-dCx2xFuPWks">https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%ED%9A%8C%EC%83%89-%ED%83%9C%EC%96%91-%EC%A0%84%EC%A7%80%ED%8C%90-%EB%A1%9C%ED%8A%B8-dCx2xFuPWks</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[postgresql trigger]]></title>
            <link>https://velog.io/@leejm_dev/postgresql-trigger</link>
            <guid>https://velog.io/@leejm_dev/postgresql-trigger</guid>
            <pubDate>Fri, 01 Dec 2023 09:02:57 GMT</pubDate>
            <description><![CDATA[<p>소소한 내용이다. 
회사에서 cron-job 혹은 복잡한 로직들을 소스코드가 아닌 <code>postgresql procedure, function</code> 으로 관리를 하였다. </p>
<h3 id="procedure-버전관리를-어떻게-해야하지">procedure 버전관리를 어떻게 해야하지</h3>
<p>개발환경마다 디비를 다르게 해서 관리를 해서 배포를 하는 방법으로 진행했지만, 
소스코드는 github로 버전관리를 하면 되는데, procedure, function 은 버전관리를 깃허브와 완전 비슷하게 우리 입맛에 구현해주는 툴이 없었다. 
datagrip 에서 github repository 를 연동해서 하면 된다고 했는데, 예상보다 쉽지 않았고, 뭔가 맘에 들지 않았다.</p>
<p>그래서 팀원 모두들 찾아보다가 툴 말고 그냥 디비안에서 관리를 해야겠다는 생각을 했다. 
그 결과 <code>trigger</code>를 사용하기로 했다. 
trigger 는 어떠한 액션에 대해서 자체적으로 이벤트를 캐치해서 원하는 동작을 하게 해주는 기술이다. postgresql에만 있는건 아니고 다른 rdbms, nosql 에도 다~ 있다. 일반적인 트리거는 특정 테이블에서 발생하는 DML 을 캐치해서 그에 따른 처리를 해주는 것이 일반적이다. 
하지만 우리는 테이블의 상태 변화가 아닌, procedure, function 의 상태 변화를 캐치했어야 했다. 
그래서 구현한 방법이 일반적인 트리거가 아닌 <strong><span style="color:orange">event trigger</span></strong> 이다.</p>
<h3 id="event-trigger">Event trigger</h3>
<blockquote>
<p>특정 하나의 테이블에서 발생하는 DML 만을 캐치하는것이 아닌, 
<code>event trigger</code> 는 특정 한 테이블에 국한되는 것이 아닌, 전역적으로 모든 DDL 이벤트를 캐치하는 트리거이다. </p>
</blockquote>
<p>postgres 16 버전에서는 </p>
<ul>
<li>ddl_command_start</li>
<li>ddl_command_end</li>
<li>table_rewrite </li>
<li>sql_drop</li>
</ul>
<p>위 4가지 이벤트에 대해서 이벤트 트리거가 발동한다고 한다. 
그 외의 기능들은 추후에 추가될 예정이라고 한다. </p>
<p><strong>1. ddl_command_start</strong>
<code>CREATE</code>, <code>ALTER</code>, <code>DROP</code>, <code>SECURITY LABEL</code>, <code>COMMENT</code>, <code>GRANT</code>, <code>REVOKE</code>, <code>SELECT INTO(CREATE TABLE AS)</code>
위 명령어들이 시작 하기 전에 발동한다. </p>
<p><strong>2. ddl_command_end</strong>
ddl_command_start 와 같은 이벤트들에 대해서 명령어 후에 발동한다.
해당 명령어들이 끝났을 때 어떤 명령어가 끝났고, 그에 대한 추가 정보가 필요하면 <code>pg_event_trigger_ddl_commands()</code> 함수를 이용하면 정보를 알 수 있다. 예를들어, create procedure 후에 <code>pg_event_trigger_ddl_commands()</code> 함수를 실행하면, 어떤 프로시저가 생성되고, 생성시점, 추가 정보 등을 알 수 있다. 
<strong>대신에 해당 이벤트는 트랜잭션이 커밋되기전에 발동한다.</strong> </p>
<p><strong>3. sql_drop</strong>
sql_drop 은 ddl_command_end 바로 직전에 어떠한 객체가 drop 될 때 발동한다. 
어떤 객체들이 drop 되었는지 알기 위해서는 <code>pg_event_trigger_dropped_objects()</code> 함수를 이용하면 알 수 있다. 
이 이벤트는 시스템 카탈로그에서 객체가 사라지면 확인할 수 있다. </p>
<h5 id="system-catalog-데이터베이스의-모든-객체에-대한-정의-와-정보를-저장하고-있는-시스템-테이블이다-성능-평가를-위한-모든-통계정보도-저장하고-카탈로그에-저장된-데이터를-meta-data라고-한다">system catalog: 데이터베이스의 모든 객체에 대한 정의 와 정보를 저장하고 있는 시스템 테이블이다. 성능 평가를 위한 모든 통계정보도 저장하고, 카탈로그에 저장된 데이터를 meta data라고 한다.</h5>
<p><strong>4. table_rewrite</strong>
테이블의 상태가 변하는 <code>ALTER TABLE</code>, <code>ALTER TYPE</code> 명령어 바로 직전에 실행되는 이벤트이다. </p>
<br>

<h3 id="🔥-주의할-점-🔥">🔥 <strong>주의할 점</strong> 🔥</h3>
<p>*<em>이벤트 트리거는 트랜잭션이 롤백되면 실행되지 않는다. *</em></p>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/be996b43-c1e3-469f-a791-9108b2e4d52c/image.png" alt=""></p>
<p>그렇기 때문에, </p>
<ol>
<li>ddl_command_start 실패하면 event trigger 또한 실행되지 않는다. </li>
<li>ddl 이 실패하면 ddl_command_end 또한 실행되지 않는다. </li>
</ol>
<p><strong>3. ddl_command_end 이 실패하면 ddl 또한 롤백된다.</strong></p>
<p><span style="color: #F57929">ddl_command_end 이 commit 되기 전에 실행되기 때문에 3번 같은 문제가 발생하는데, 그렇기 때문에 ddl_command_end 이벤트에 따른 이벤트트리거는 신중하게 작성해야겠다. </span></p>
<h3 id="그래서-어떻게-설계를-했는가">그래서 어떻게 설계를 했는가</h3>
<p>procedure, function 이 <code>CREATE, ALTER, DROP</code> 중 하나의 상태가 되면 해당 이벤트를 잡아서 액션을 취하도록 하였다. </p>
<p>1) 로그를 저장할 테이블을 만들자</p>
<pre><code class="language-sql">create table tb_sp_fn_ddl_log
(
    name        text, // procedure, function 이름
    obj_id      integer, // trigger action id
    ddl         text, // procedure, function DDL
    create_date timestamp with time zone default now() not null
);</code></pre>
<p>2) 이벤트 트리거가 걸리면 작동할 function을 만들어준다. </p>
<pre><code class="language-sql">create function cr_sp_fn_ddl_log() returns event_trigger
    language plpgsql
as
$$

DECLARE
    r   RECORD;
    ddl text;
BEGIN
    IF tg_tag in
       (&#39;CREATE PROCEDURE&#39;, &#39;CREATE FUNCTION&#39;)
    THEN
        r := pg_event_trigger_ddl_commands();
        select * into ddl from pg_get_functiondef(r.objid);
        insert into tb_sp_fn_ddl_log (name, obj_id, ddl) values (r.object_identity, r.objid, ddl);
    END IF;
END;
$$;</code></pre>
<p><strong><span style="color: #F57929">이벤트 트리거로 작동할 함수는 반드시 event_trigger 를 반환하는 형식의 함수여야 한다.</span></strong></p>
<ul>
<li>tg_tag 는 postgresql event_trigger 를 반환하는 함수에서 자체적으로 수집할 수 있는 객체라고 생각된다. </li>
<li>tg_tag 가 <code>create procedure</code> 혹은 <code>create function</code> 일 경우에 이 함수가 실행되는데, <code>pg_event_trigger_ddl_commands()</code> 를 통해서 ddl_command_end 이벤트의 정보를 조회할 수 있다. </li>
<li><code>pg_get_functiondef()</code> 함수는 procedure, function 의 정의 DDL 를 조회할 수 있다. </li>
<li>조회할 결과를 토대로 사전에 생성한 테이블에 저장한다. </li>
</ul>
<p>3) 마지막으로 해당 function을 일으킬 트리거를 생성해준다.</p>
<pre><code class="language-sql">create event trigger tr_sp_fn_ddl_log
    on ddl_command_end
execute procedure public.cr_sp_fn_ddl_log();</code></pre>
<p>이렇게 하면 테이블에 그동안 특정 procedure 혹은 function 이 어떻게 변화했는가 모든 로그를 트래킹할 수 있다. 저장 시간까지 저장하기 때문에, 원하는 시점에 어떤 DDL이었는지 알 수 있기 때문에, 롤백 및 버전관리를 하기 수월해졌다. </p>
<hr>
<p>Reference
<a href="https://www.postgresql.org/docs/current/event-triggers.html">https://www.postgresql.org/docs/current/event-triggers.html</a>
<a href="https://www.postgresql.org/docs/current/event-trigger-definition.html">https://www.postgresql.org/docs/current/event-trigger-definition.html</a>
<a href="https://code-lab1.tistory.com/133">https://code-lab1.tistory.com/133</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[nestjs with graphql - ch2 (Federation)]]></title>
            <link>https://velog.io/@leejm_dev/nestjs-with-graphql-ch3-microservice</link>
            <guid>https://velog.io/@leejm_dev/nestjs-with-graphql-ch3-microservice</guid>
            <pubDate>Mon, 20 Nov 2023 01:06:48 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/leejm_dev/post/cae19561-abd1-430a-aaee-e83c21ef448f/image.png" alt=""></p>
<p>🔥 <strong>MSA 구조를 graphql 로 만들어보자</strong> 🔥</p>
<p>그전에 생각해보자...
<code>grapqhl 은 엔드포인트가 하나인데, MSA 를 어떻게 구현해야 하지...? 뭔가 msa 로 할 수 있을 것 같으면서도... 이게 restapi 처럼 msa 를 구성 가능할까...? 여러가지 생각이 든다. 음...각 app 별로 tcp 서버를 열어야 하나? tcp 통신이 필요한가? 어차피 엔드포인트 하나인데..?</code>
한번 해보자.</p>
<ol>
<li><p>각 도메인에 따른 app 을 만든다 
<code>nest g app gateway</code>, <code>nest g app account</code>, <code>nest g app chat</code></p>
</li>
<li><p>생성한 앱은 rest 기반으로 만들기 때문에 graphql 을 위한 resolver 생성이 필요하다. 
<code>nest g resource</code>
<img src="https://velog.velcdn.com/images/leejm_dev/post/ce4b6bf9-fbbc-4a77-9c8e-c463e907490c/image.png" alt=""></p>
<p>gateway 를 제외한 다른 app 들을 각자 선택해서 resolver 를 생성하면 기본 구조(resolver, service, dto, entity...) 및 로직(CRUD)을 만들어준다. 
<img src="https://velog.velcdn.com/images/leejm_dev/post/c0421791-9845-44cd-970a-c59aed5896da/image.png" alt=""></p>
</li>
</ol>
<ol start="3">
<li><p>이제 gateway 에서 각 앱들을 하나로 연결을 시켜줘야 한다. </p>
<p>여기서 중요한 개념이 있다. </p>
</li>
</ol>
<br>
---

<h2 id="span-stylecolororangefederationspan"><span style="color:orange">Federation</span></h2>
<p>Federation(직역하면 <code>연방</code>)의 의미는 graphql 서버를 각 microservice 로 주입을 해준다라는 의미이다.</p>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/f80090c8-eb50-428d-88f5-d1a57d5afe62/image.png" alt=""></p>
<p>요청한 쿼리를 하나의 리졸버에서 모두 해결하는 것이 아닌, 각 subgraph 에서 스키마를 처리해서 처리된 schema 를 superGraph 에서 취합해서 프론트로 던져준다.</p>
<p>Federation 을 구현하기 위해서는 하나의 gateway 와 한개 이상의 microservice(federated microservice) 가 필요하다. 
<code>이때 gateway 를 supergraph 라고 하고, 각각의 service 들을 subgraph 라고 이해하면 편하다.</code></p>
<p>federated microservice(subgraph) 는 각각의 schema 를 가지고 있고, gateway(supergraph) 는 그 schema 들을 한 곳으로 모을 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/50de6bfd-94a2-4eb8-90c5-529c455039ff/image.png" alt=""></p>
<p>🔥 물론 하나의 resolver 에서 모든 로직을 처리할 수는 있지만, 그렇게 하면 여러 테이블에 join 을 걸어야 하고 그렇게 되면 graphql 을 사용하고도 overFetching 을 할 수 밖에 없다. 
그렇기 때문에 원하는 값만 조회하기 위해 각 entity를 담당하는 resolver 에서 각각 처리하고 superGraph 에서 해당 entity 들을 모아서 반환하는 것이다. </p>
<p>그러면 각각의 subgraph 를 어떻게 한번에 모으는지 아래 코드를 통해 확인해보자.</p>
<pre><code class="language-ts">// gateway.module.ts
import { Module } from &#39;@nestjs/common&#39;;
import { GatewayService } from &#39;./gateway.service&#39;;
import { GraphQLModule } from &#39;@nestjs/graphql&#39;;
import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from &#39;@nestjs/apollo&#39;;
import { IntrospectAndCompose } from &#39;@apollo/gateway&#39;;
@Module({
  imports: [
    GraphQLModule.forRoot&lt;ApolloGatewayDriverConfig&gt;({
      driver: ApolloGatewayDriver,
      gateway: {
        supergraphSdl: new IntrospectAndCompose({
          // 각각의 subGraph 를 하나로 연결 시켜준다.
          subgraphs: [
            {
              name: &#39;account&#39;,
              url: &#39;http://localhost:3001/graphql&#39;,
            },
            {
              name: &#39;talk&#39;,
              url: &#39;http://localhost:3002/graphql&#39;,
            },
          ],
        }),
      },
    }),
  ],
  providers: [GatewayService],
})
export class GatewayModule {}
</code></pre>
<p>코드는 간단하다. <code>ApolloGatewayDriver, IntrospectAndCompose class</code> 를 이용해서 각각의 subgrapqh 를 통합해줄 수 있다.</p>
<p>각각의 federated microservice 의 schema 중 <code>@ObjectType</code> 간의 relation 을 활용하면 하나의 query 에서 데이터를 가져오는 것이 가능하다. 그러기 위해서는 <code>directive 지시어</code> 가 필요하다. </p>
<h2 id="span-stylecolororangedirectivespan"><span style="color:orange">Directive</span></h2>
<p><strong>graphql 에서는 동일한 이름의 <code>@ObjectType</code> 이 존재 하면 안된다</strong></p>
<p>하지만 여러 앱에서 동일한 모델을 쓰고 싶을 수 있다. 예를 들어, 계정 페이지에서 고객의 정보를 보여줄 수도 있고, 채팅방에서 고객 정보를 보여줄 수도 있다. 채팅방에서는 채팅 정보와 고객의 정보를 같이 보여줘야 하는 경우가 생긴다. 
MSA 구조에 따라서 각 앱에서 각 모델을 교차해서 종속시키는건 옳지 않은 방법이기 때문에, 각 앱에서 각 모델을 둘다 선언을 해준다. </p>
<pre><code class="language-ts">// chat &gt; chat.model.ts
import { Directive, Field, ID, ObjectType } from &#39;@nestjs/graphql&#39;;
@ObjectType()
@Directive(&#39;@key(fields: &quot;id&quot;)&#39;)
export class AccountModel {
  @Field(() =&gt; ID)
  id: string;
}



// account &gt; account.model.ts
import { Directive, Field, ID, ObjectType } from &#39;@nestjs/graphql&#39;;
@ObjectType()
@Directive(&#39;@key(fields: &quot;id&quot;)&#39;)
// 동일 이름의 objectType 이 있으면 안된다
// -&gt; 동일한 objectType 이 있다면 반드시 @Directive(&#39;@key(fields: &quot;id&quot;)&#39;) 로 obejct 의 pk 를 설정해줘야 함
export class AccountModel {
  @Field(() =&gt; ID)
  id: string;

  @Field(() =&gt; String)
  name: string;
}
</code></pre>
<p>이렇게 될 경우 schema 에 같은 이름의 모델이 생겨버리기 떄문에 graphql 철학에 위배된다. 이때, <code>@Directive(&#39;@key(fields: &quot;id&quot;)&#39;)</code> 로 문제를 풀어준다. 
저 데코레이터는 중복을 허용해준다는 의미를 포함하고 있다. <span style="color:skyblue">또한, 안에 있는 @Key 는 모델에도 테이블처럼 PK 를 지정해주는 것이다. 
저 PK로 정한 id 가 같은 모델이 있으면 두개는 같은것으로 판단해서 하나로 묶어준다.</span> 
결국 </p>
<pre><code class="language-ts">@ObjectType()
export class AccountModel {
  @Field(() =&gt; ID)
  id: string;

  @Field(() =&gt; String)
  name: string;
}

// schema.gql
type AccountModel {
  id: ID!
  name: String!
}</code></pre>
<p>다음과 같은 하나의 모델이 되는 것이다. </p>
<h2 id="span-stylecolororangeresolvefieldspan"><span style="color:orange">ResolveField</span></h2>
<p>처음에 이거 떄문에 애를 썩었다. 
아무리 구글링해서 나오는 설명을 봐도 이해가 안되고, 대신에 이거를 쓰면 dataLoader 뭐시기 뭐시기... 그래서 유튜브 영상 및 지인들을 통해서 알아냈다. </p>
<p>ResolveField... 일단 Field 다. resolve... 해결하다?</p>
<p>의역하면 필드를 하나 열어주는 것이다. 
<span style="color:#CF5B42">즉 ResolveField 가 속해있는 리졸버에서 반환하는 모델에 필드를 하나 더 추가하는 행위다.</span></p>
<pre><code class="language-ts">// chat.resolver.ts
import {
  Resolver,
  Query,
  Args,
  Int,
  ResolveField,
  Parent,
} from &#39;@nestjs/graphql&#39;;
import { ChatService } from &#39;./chat.service&#39;;
import { AccountModel, ChatModel } from &#39;./entities&#39;;

@Resolver(() =&gt; ChatModel)
// 이 resolver 는 chatModel 을 뽑아내기 위한 리졸버이다
export class ChatResolver {
  constructor(private readonly chatService: ChatService) {}

  @Query(() =&gt; ChatModel)
  async getChatInfo(
    @Args(&#39;roomId&#39;, { type: () =&gt; Int }) roomId: number,
  ): Promise&lt;ChatModel&gt; {
    return {
      chatId: roomId,
    };
  }

  @ResolveField(() =&gt; AccountModel)
  user(@Parent() chat: ChatModel) {
    return { __typename: &#39;UserEntity&#39;, id: chat.chatId };
  }
}



// chat.model.ts
@ObjectType()
export class ChatModel {
  @Field(() =&gt; Number)
  chatId: number;
}
</code></pre>
<p>위 리졸버는 ChatModel을 반환하는 리졸버이다. @ResolveField 를 통해서 ChatModel 에 <code>user</code> 라는 필드를 추가적으로 반환하겠다라는 뜻이다.</p>
<p>근데 User 는 AccountModel 이다. <strong>AccountModel 은 Chat 앱이 아니라, Account 앱에 종속되어있다.</strong> 어떻게 가져오지? 
<span style="color:#CF5B42">이때 <code>@Directive</code> 가 쓰인다!!!</span></p>
<p><code>@Query() getChatInfo</code> 쿼리는 ChatModel 을 반환한다. 
그러면 <code>@Parent()</code> 가 ChatModel 인 <code>user @ResolveField()</code> 가 이를 캐치한다. <code>user @ResolveField()</code> 가 <code>{ __typename: &#39;UserEntity&#39;, id: chat.chatId }</code> 를 반환하는데, 이를 누구한테 반환하는가..?</p>
<p>✅ <strong>앞서 말했듯이 graphql 에서는 동일한 ObjectType이 있을 수 없다. 
chat app 에 종속되어 있는 Account Model 와 
Account app 에 종속되어 있는 Account Model 이 
@Directive key 로 연결되어 있기 때문에, 
AccountModel 을 종속하고 있는 Account app 에서 이를 수신한다.</strong></p>
<blockquote>
<p>만약에 또다른 app 에서도 Account Model 을 사용하고 있다면? 어느쪽에서 수신할까?
이런 고민은 잘못됐다. 
애초에 Account Model 은 Account app 에서 다루는것이 옳은 방법이다. (관심사의 분리가 적용된다) 
그렇기 때문에 애초에 또 다른 app 에서는 <code>@ResolveReference()</code> 이 아니라, <code>@ResolveField() user</code> 를 사용하는 것이 맞다.
<span style="color:#CF5B42"><strong>즉, Account Model 은 Account app 에서만 로직을 풀어가자!</strong></span></p>
</blockquote>
<pre><code class="language-ts">// account.resolver.ts
/**
   *
   * @description chat resolver 에서 처리하지 못하는 쿼리를 여기서 처리함 (find account)
   * resolverField 를 여기서 수신해서 처리한다.
   */
  @ResolveReference()
  resolveReference(reference: {
    __typename: string;
    id: string;
  }): AccountModel {
    return {
      id: reference.id,
      name: `${reference.id} + name`,
    };
  }</code></pre>
<br>

<hr>
<p>이렇게 ResolveField -&gt; ResolveReference -&gt; ResolveField -&gt; ResolveReference -&gt; ... 계속 하면 가장 마지막에 수신하는 리졸버를 수만가지 요청을 처리해야 하는 경우가 생긴다. 
이를 <code>N+1 문제</code> 라고 하는데, 다음에 다뤄보자.</p>
<hr>
<h3 id="ref">Ref</h3>
<p><a href="https://blog.doctor-cha.com/integrating-graphql-services-with-graphql-federation">https://blog.doctor-cha.com/integrating-graphql-services-with-graphql-federation</a>
<a href="https://www.apollographql.com/docs/federation/">https://www.apollographql.com/docs/federation/</a></p>
<p><a href="https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%ED%8C%8C%EB%9E%80%EC%83%89-%ED%8E%98%EC%9D%B8%ED%8A%B8%EA%B0%80-%EC%B9%A0%ED%95%B4%EC%A7%84-%EC%82%AC%EB%9E%8C-%EB%B0%9C-EIyAz8blaAk">https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%ED%8C%8C%EB%9E%80%EC%83%89-%ED%8E%98%EC%9D%B8%ED%8A%B8%EA%B0%80-%EC%B9%A0%ED%95%B4%EC%A7%84-%EC%82%AC%EB%9E%8C-%EB%B0%9C-EIyAz8blaAk</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[nestjs singleton]]></title>
            <link>https://velog.io/@leejm_dev/nestjs-singleton</link>
            <guid>https://velog.io/@leejm_dev/nestjs-singleton</guid>
            <pubDate>Tue, 14 Nov 2023 02:53:35 GMT</pubDate>
            <description><![CDATA[<h3 id="✅-용어설명">✅ 용어설명</h3>
<blockquote>
<p><strong>singleton</strong>
객체의 인스턴스가 오직 1개만 생성되는 패턴
메모리에 인스턴스를 한번만 만들어서 올려놓고 필요할 때 마다 생성하는 패턴이다.</p>
</blockquote>
<blockquote>
<p><strong>IOC(inversion of control)</strong>
제어의 권한을 바꾼다
인스턴스의 생성 및 할당과 해제를 개발자가 하는 것이 아니고, 프레임워크가 이를 맡아주는 것을 말한다. </p>
</blockquote>
<blockquote>
<p><strong>DI(dependency of injection)</strong>
의존성 주입
A가 B를 사용하기 위해 직접 B를 생성하는 것이 아니라, 외부로부터 가져와서 사용한다는 것을 말한다.</p>
</blockquote>
<p>❗️ 의존성 주입을 사용하지 않을 경우 단점 ❗️</p>
<ol>
<li>A를 B에서도 사용하고 C에서도 사용을 하는데 둘다 의존성 주입이 아니라 직접 인스턴스를 생성해서 사용한다고 가정을 해보자. (A-&gt;B / A-&gt;C)
이때 A가 변하면 B, C에서도 수정된 A를 다시 생성해야 한다. 
이를 <code>강한 결합</code> 이라고 한다.</li>
<li>또한, A의 인스턴스들의 생명주기를 B, C 가 직접 관리를 해주면 메모리 관리를 해야한다. 이때 <code>memory leak</code> 가 발생할 수 있다. </li>
</ol>
<br>

<p><code>여기서 잠깐! 근본적으로 생각해보자. 이런 의존성이니 주입이니 이런말이 왜 나온걸까?
아키텍처를 구성할때, 도메인에 따라서 서비스 레이어를 구분하도록 구성하면 구현체의 로직이 변경되도 호출부에서는 해당 내용을 알 필요가 없다. 
이로써, 생산성과 재사용성이 좋아지게 된다.</code> <br></p>
<p><span style="color:orange">nest는 내부적으로 <strong>IOC container</strong> 를 이용해서 DI를 관리해준다 </span>
ioc container는 <code>@Injectable(), @Module()</code> 데코레이터가 달린 클래스들을 DI대상으로 관리한다. nest 는 typescript 이기 때문에, 타입기준으로 DI를 관리하기 수월하다.</p>
<h3 id="✅-nestjs-싱글톤-패턴의-특징">✅ nestjs 싱글톤 패턴의 특징</h3>
<ul>
<li>공유된 자원을 사용하기 위해.<ul>
<li>하나의 인스턴스를 여러 곳에 사용했을 경우 동시성 이슈와 데드락 같은 문제를 방지할 수 있다.</li>
</ul>
</li>
<li>인스턴스를 필요할 때 마다 생성하고 필요없을 때, 제거 하면서 까지 메모리 관리를 신경 쓰지 않아도 된다. </li>
<li>하나의 인스턴스를 global 하게 사용할 수도 집약적으로 사용할 수 있다. <ul>
<li>global 하게 사용한다면 A클래스에서 B클래스에서 사용하는 인스턴스에 접근을 방지 하기 위해 private 을 선언해주는 방법도 있다.</li>
</ul>
</li>
</ul>
<h3 id="✅-구현방법">✅ 구현방법</h3>
<p>nest는 애초에 싱글스레드이기 때문에, 인스턴스간의 동시성이 발생하지 않는다고 한다. 
nestjs 에서는 docs 에서 알 수 있듯이, singleton 을 지향하고 있다. <span style="color:orange">하지만 무조건 싱글톤으로 구현되는 건 아니다. 아래서 다뤄보겠다.<span> 
<img src="https://velog.velcdn.com/images/leejm_dev/post/cec32e28-b246-4d30-a70e-66019c347a06/image.png" alt=""></p>
<p>싱글톤을 구현하기 위해서는 간단하게 설명을 하자면, <code>@Injectable(), @Module()</code> 데코레이터를 사용하면 된다.
<code>@Injectable(), @Module()</code> 데코레이터를 선언하면, 해당 인스턴스를 nest 내장 IOC container 가 관리한다 
<span style="color:grey">(데코레이터가 달린 클래스는 타입스크립트가 컴파일시 메타데이터로 어떤 서비스에 의존하고 있는지 명시를 해준다. 그러면 nestjs 가 어떤 의존관계가 있는지 알 수 있다.) </span><br>
👉 <span style="color:#C13011"><strong>nest 내부의 IOC container 가 @Injectable(), @Module() 데코레이터 클래스를 싱글톤으로 생성하여 DI의 대상으로 관리한다.</strong></span></p>
<h3 id="✅-뭐야-두개-생기는데">✅ 뭐야 두개 생기는데?</h3>
<p>socket adapter 를 공부하다가 소켓에 연결하면 즉각 실행하는 <code>afterInit()</code> 이라는 함수를 구현하다가 뭔가 이상함을 봤다.</p>
<pre><code class="language-ts">// ws.gateway.ts
@WebSocketGateway(80)
export class WsKorGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
  constructor(private readonly logger: Logger){
  }
  @WebSocketServer()
  wsServer: Server;
  /**
     * 
     * @param server 해당 gateway 가 실행되면 가장 먼저 실행되는 함수 -&gt; handleConnection 보다 먼저 실행된다
     */
  afterInit(server: any) {
    this.logger.log(&#39;kor ws gateway started!&#39;) 
    // 터미널을 보면 이게 두번 실행됨을 알 수 있다.
  }
}



// ws.module.ts
@Module({
    providers: [WsKorGateway, Logger]
})
export class WsKorModule {
}



// app.module.ts
@Module({
  imports: [WsKorModule, WsUsaModule],
  controllers: [AppController],
  providers: [AppService, WsKorGateway, Logger],
})
export class AppModule {}</code></pre>
<p>위와 같이 소스코드를 구현해 놓고, ws server 를 구동하면 다음과 같은 결과가 나온다. </p>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/4776f370-baf6-4eda-9039-3d99dd73103d/image.png" alt=""></p>
<p>위 사진을 보면 <code>kor connected!</code> 라는 로그가 두번찍혔는데 이로써 <code>afterInit()</code> 함수가 두번 실행됐음을 알 수 있다. </p>
<p>app module 에서 import 에서 한번 providers 에서 한번, 총 두번 주입을 해줬기 때문에, WsKorGateway 가 두번 실행되서 로그가 두번찍히는건 알겠는데</p>
<p>app module 에서 import 에서 한번, providers 에서 한번, 총 두번 주입을 하지만 한개의 인스턴스를 그냥 두번 주입하는게 아닌가?</p>
<p>nest 는 인스턴스를 싱글톤으로 만든다고 했는데..?</p>
<p>여기서 그냥 아무 decorator 로 싱글톤을 구현하는것이 아닌것을 알 수 있다. 
<code>@injectable() @Module()</code> 이 두개로만 싱글톤이 적용되는 것으로 추측된다. </p>
<h3 id="✅-주입된-놈이-수정되면-나도-컴파일-해야-하나">✅ 주입된 놈이 수정되면 나도 컴파일 해야 하나?</h3>
<pre><code class="language-ts">@Controller(&#39;payment&#39;)
export class PaymentController extends CommonController {
  constructor(
    @Inject(&#39;PAYMENT_SERVICE&#39;)
    private readonly paymentService: PaymentService,
  ) {
    super();
  }</code></pre>
<p>위 같은 경우는 PaymentService 가 변하면 PaymentController 도 같이 컴파일을 다시 해야한다. </p>
<pre><code class="language-ts">@Controller(&#39;talk&#39;)
export class ChatController extends BaseController {
  constructor(
    @Inject(CHAT_SERVICE)
    private readonly chatService: IChatService,
  ) {
    super();
  }</code></pre>
<p>하지만 다음과 같은 경우는 Interface 를 주입받았기 때문에, ChatService의 구현체가 변해도 ChatController 는 컴파일 하지 않아도 된다. 
이게 interface 를 사용하는 중요한 이유다 (일종의 추상화)</p>
<hr>
<p>  <strong>References</strong>
  <a href="https://jun-choi-4928.medium.com/nest-js-behind-the-curtain-712b39abd49c">reference 1</a>
 <a href="https://jay-ji.tistory.com/106">reference 2</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[nestjs with graphql - ch.1 (schema, resolver)]]></title>
            <link>https://velog.io/@leejm_dev/nestjs-with-graphql-ch.1-schema-resolver</link>
            <guid>https://velog.io/@leejm_dev/nestjs-with-graphql-ch.1-schema-resolver</guid>
            <pubDate>Sun, 12 Nov 2023 05:08:43 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/leejm_dev/post/22e6a133-26d6-4368-8802-193f3ca1a6c4/image.png" alt=""></p>
<p>입사했을 당시, 회사 백엔드 코드의 스택은 nestjs + graphql 이었다. </p>
<p>그 당시 graphql 에 대해서 잘 알지 못했고, 코드 구조를 보고 graphql 은 원래 이렇게 사용하는구나 싶었다. </p>
<p>새로 오신 시니어 개발자 분이 현재 코드 구조가 graphql 의 철학과 맞지 않는 것 같다 라는 의견과 함께, 
대규모 개편에 앞서 graphql 을 철학에 맞게 사용하거나 혹은 restapi 를 새롭게 도입하자 라는 의견을 제시해줘 고민을 하다가 시간이 촉박한 탓에 그래도 좀 더 익숙하고 레퍼런스가 많은 restapi 를 사용하자라는 결론이 났었다. </p>
<p>결과적으로 대규모 개편은 restapi 로 개발을 하였다. 
배포를 하고 나니, <code>그렇다면 정말 graphql 의 철학은 무엇인가?</code> 라는 의문이 들기 시작한다. </p>
<p>다시 초심의 마인드로 겸손하게 하나하나 차근차근 공부해보자.</p>
<hr>
<p>graphql 은 ql 에서 유추할 수 있듯이, 데이터를 가져오는 쿼리 언어이자, 서버측 런타임이다. </p>
<p>restapi 는 하나의 api 요청에 하나의 데이터를 가져올 수 있지만, 
graphql 은 하나의 api 요청에 여러 데이터 소스에서 데이터를 가져올 수 있다. </p>
<h3 id="✅-schema">✅ schema</h3>
<p>gql 에서 사용하는 모든 resolver(query, mutation, subscription), model 등을 설명 및 정리한 것이다. </p>
<pre><code class="language-gql"># ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------

type Message {
  id: String!
  message: String!
}

type Query {
  getMessages: Message!
}</code></pre>
<p><span style="color:orange">graphql 의 가장 큰 장점은 프레임워크와 상관없이 프론트에서 schema generate 를 통해서 schema.gql 전체를 한번에 긁어올 수 있다는 것 같다. 이는 정말 엄청난 생산성을 높일 수 있다. </span>
rest api 는 이것이 불가능해서 생각보다 불편했다. </p>
<h3 id="✅-resolver">✅ resolver</h3>
<p>schema 에 정리된 데이터를 호출하기 위한 일종의 controller 같은 것이다. 실제 데이터를 가져오는 로직이 들어가 있다.</p>
<h3 id="✅-schema-first-vs-code-fisrt">✅ schema first vs code fisrt</h3>
<ol>
<li>설계 방법 중에 스키마를 먼저 설계 하고 이를 바탕으로 코드를 짜는 방법이 있고, </li>
<li>코드를 먼저 생성하고 graphql module 의 <code>autoSchemaFile</code> 를 이용하여 스키마를 자동 생성하는 방법이 있다. </li>
</ol>
<p>검색을 해보니 말들이 많다. 각자의 장점과 단점이 있다. 
schema fisrt 가 레퍼런스가 더 많고, 이를 우선적으로 공부를 하라고 많이들 나와있다. 하지만, 이 방법이 현업에서 정말 더 좋을까?</p>
<p>schema 를 먼저 설계를 하고 배포를 하면 시간을 더 줄일 수 있는건 확실하다. schema 를 기반으로 프론트와 백엔드가 동시에 개발을 할 수 있기 때문이다. </p>
<p>하지만 결정적으로 <span style="color:orange">resolver 와 schema 간의 동기화</span>가 맹점이다.</p>
<p>nestjs 를 restapi 로 개발을 하다 보니, 동기화 작업이 여간 귀찮고 까탈스러운 일이 아니였다. swagger 를 도입한다고 해도, 불편한 점이 더러 있었다. </p>
<p>동기화를 위해서는 code-first 방식이 더 좋을 것 같다. 
(개발을 하면서 장단점을 좀 더 비교해보겠다)</p>
<h3 id="✅-graphql-장점">✅ graphql 장점</h3>
<p><span style="color:#FF5733"><strong>GraphQL 호출은 단일 왕복으로 처리되며 클라이언트는 오버페칭 없이 요청한 결과만 얻습니다</strong></span> 라는 말은 너무 매력적으로 다가온다. fetching 이 하나라면, 네트워크 비용을 줄일 수 있다는 말로 들리는데, 대규모 배포 전의 graphql 백엔드 코드는 단일 왕복이 아니였다. 이를 중점적으로 공부를 해보려고 한다. </p>
<p>REST API의 단점으로 많이 거론되는 바로 ‘오버패칭(필요한 정보 외에도 불필요한 정보까지 받게 되는 것)’과 ‘언더패칭(필요한 정보를 서버에서 가져오지 못해 추가 요청을 해야 하는 경우)’ 문제도 있다. </p>
<p>한 예로, 현재 운영중인 서비스에는 제조 파트너 정보를 보여주는 화면이 있다. 
해당 화면에는 파트너의 각 종 정보를 다 보여줘야 한다. (프로필, 포트폴리오, 리뷰, 소개서, 인증서, 통계, 장비, etc..)
이것들을 <strong>호출하기 위해 너무 많은 api 를 호출해야 한다</strong>고 생각했다. 이러한 문제도 graphql 로 어느정도 해결할 수 있지 않을까?</p>
<h3 id="✅-코드">✅ 코드</h3>
<pre><code class="language-ts">/// app.module.ts
@Module({
  imports: [
    GraphQLModule.forRoot&lt;ApolloDriverConfig&gt;({
      driver: ApolloDriver,
      playground: true,
      autoSchemaFile: &#39;src/schema.gql&#39;,
      path: &#39;v1/gql&#39;,
    }),
    ChatModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}</code></pre>
<pre><code class="language-ts">/// chat.resolver.ts
import { Resolver, Query } from &#39;@nestjs/graphql&#39;;
import { ChatService } from &#39;./chat.service&#39;;
import { Message } from &#39;./model&#39;;

@Resolver(&#39;Chat&#39;)
export class ChatResolver {
  constructor(private readonly chatService: ChatService) {}

  @Query(() =&gt; Message)
  async getMessages(): Promise&lt;Message&gt; {
    return await this.chatService.getMessages();
  }
}
</code></pre>
<pre><code class="language-ts">/// model/message.ts
import { Field, Int, ObjectType } from &#39;@nestjs/graphql&#39;;
import { randomUUID } from &#39;crypto&#39;;

@ObjectType()
export class Message {
  @Field(() =&gt; String)
  id: string;

  @Field(() =&gt; String)
  message: string;
}
</code></pre>
<p><code>ApolloDriver</code> 이 @Resolver(), @ObjectType(), @InputType() decorator 를 다 긁어와서 schema 를 자동 생성해주는 것 같다. </p>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/3ef1f900-06b9-4e99-8ca0-94bff113db3e/image.png" alt=""></p>
<p>다음과 같이 프론트는 백엔드에서 던져주는 값들 중에 원하는 값만 받을 수 있다. (message 필드는 무시하고, id 만 받은 것을 볼 수 있음)</p>
<p>다음에는 orm 및 mutation 에 대해서 알아보겠다.</p>
<hr>
<h3 id="ref">REF</h3>
<p><a href="https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%ED%8C%8C%EB%9E%80-%EA%B3%84%EB%8B%A8%EC%9D%84-%EB%B0%9F%EB%8A%94-%EC%82%AC%EB%9E%8C-7_kRuX1hSXM">https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%ED%8C%8C%EB%9E%80-%EA%B3%84%EB%8B%A8%EC%9D%84-%EB%B0%9F%EB%8A%94-%EC%82%AC%EB%9E%8C-7_kRuX1hSXM</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SocketIO Adapter (with. nestjs)]]></title>
            <link>https://velog.io/@leejm_dev/web-socket-nestjs</link>
            <guid>https://velog.io/@leejm_dev/web-socket-nestjs</guid>
            <pubDate>Sat, 04 Nov 2023 03:50:17 GMT</pubDate>
            <description><![CDATA[<p><em>&quot;소켓에 대해서 저 스스로 잘 모른다고 생각하여 정리한 글입니다&quot;</em>
 <br></p>
<p>sokcet 은 기본적으로, http 와 같이 프로토콜의 한 종류이며, http는 단방향 통신에 한번 통신을 하고 나면 연결을 끊어버리는 성격과 다르게, socket 은 양방향 통신에 한번 연결을 해도 끊어버리지 않고, 연결을 유지하기 때문에, 실시간으로 소통해야 하는 경우에 자주 사용된다. 예를 들면, 채팅 같이 실시간 통신을 하기 위한 기능에 자주 사용된다.</p>
<p>socket 을 사용되는 기술에는 websocket 과 socketio 가 있는데 차이점은 아래와 같다. </p>
<pre><code class="language-shell">WebSocket 과 socketIO 의 차이점

ws 는 프로토콜이다(웹소켓이 등장하기 전에는 http 로만 통신을 했기 때문에 주로 polling 을 사용함)
io 는 node로 만든 websocket 기반 양방향 통신 모듈(라이브러리)이다.
ws 를 지원하지 못하는 브라우저가 많아서 io가 생김.</code></pre>
<p>그러면 어떻게 구현할 수 있는지 알아보자. </p>
<h2 id="install-package">install package</h2>
<p><code>$ npm i --save @nestjs/websockets @nestjs/platform-socket.io</code>
<code>$ npm i --save-dev @types/socket.io</code></p>
<h2 id="architecture">architecture</h2>
<p>socket 서버를 열기 위해서는 일단, nest 서버가 있어야 한다. 
그리고 nest 서버에서 socket 서버를 주입받아서 사용한다. 
어떻게 되어 있는지 코드를 통해 알아보자. </p>
<pre><code class="language-ts">// ws.gateway.ts
@WebSocketGateway(80)
export class WsKorGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
    constructor(private readonly logger: Logger){}
  ... 이하 생략
}

// ws.module.ts
@Module({
    providers: [WsKorGateway, Logger]
})
export class WsKorModule {
}</code></pre>
<p>socket 서버만을 여는 socket gateway 모듈화 시켜준다.</p>
<pre><code class="language-ts">// app.module.ts
@Module({
  imports: [WsKorModule, WsUsaModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}</code></pre>
<p>socket 모듈(<code>WsKorModule</code>)을 app module 에 주입해준다.</p>
<p>app module 에서는 단순하게 socket 서버를 여는 방법으로 연결을 할 수도 있지만, socket 서버만 달랑 하나 여는 것 보다, <strong><code>adapter</code></strong> 를 이용하면, <strong>여러 socket server 를 통합하여 관리할 수 있다.</strong>
아래는 <code>1. socket io adapter</code> 또는 <code>2. web socket adapter</code> 로 관리하는 방법이다. </p>
<pre><code class="language-ts">// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableCors({
    origin: true,
    credentials: true,
  });

  // 1. io adapter init using socketIO
  const redisIoAdapter = new RedisIoAdapter(app);
  await redisIoAdapter.connectToRedis();
  app.useWebSocketAdapter(redisIoAdapter);

  // 2. ws adapter init using WS (faster then socketIO, lower skill then socketIO)
  app.useWebSocketAdapter(new WsAdapter(app));


  await app.listen(3000);
}
bootstrap();</code></pre>
<p>코드에 대한 설명은 아래에서 더 자세하게 다뤄보겠다.</p>
<hr>
<h2 id="socket-server-gateway">socket server gateway</h2>
<h3 id="gateway-lifecycle"><strong>Gateway lifeCycle</strong></h3>
<p><code>ws gateway 가 연결이 되고 끊어때까지의 생명주기</code>는 </p>
<pre><code class="language-ts">implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect</code></pre>
<p>를 통해서 알 수 있다.</p>
<pre><code class="language-ts">export interface OnGatewayInit&lt;T = any&gt; { 
  // ws gateway 시작되는 순간 가장먼저 실행됨
    afterInit(server: T): any;
}</code></pre>
<pre><code class="language-ts">export interface OnGatewayConnection&lt;T = any&gt; { 
  // 소켓 연결되면 실행됨
    handleConnection(client: T, ...args: any[]): any;
}</code></pre>
<pre><code class="language-ts">export interface OnGatewayDisconnect&lt;T = any&gt; { 
  // 소켓 연결 끊어지면 실행됨
    handleDisconnect(client: T): any;
}</code></pre>
<h3 id="ongatewayinit">OnGatewayInit</h3>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/9e44f468-fe7f-4e0c-b6d3-4939d8399c3f/image.png" alt="">
ws gateway 에 namespace 속성을 추가하면 해당 namespace 로 방을 만들었음을 알 수 있다. (nsps 는 namespaces 의 축약이다 ^^)</p>
<p>web socket 에서 말하는 namespace 는 하나의 방이라고 생각하면 된다.
(http 통신에서의 방은 엔드포인트라고 볼 수 있듯이)
namespace 이름에 따라 요청을 다르게 처리할 수 있다. 
<img src="https://velog.velcdn.com/images/leejm_dev/post/75a04977-a4f9-46d1-96b7-5f7225a733fa/image.png" alt=""></p>
<h3 id="ongatewayconnection--ongatewaydisconnect">OnGatewayConnection &amp; OnGatewayDisconnect</h3>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/9dd054d7-31a8-4ca2-b31c-50af152001a6/image.png" width="100%" height="50%"></img></p>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/bf60ed85-cea5-4bf0-8bcc-1295659e11cc/image.png" alt=""></p>
<p>동일 클라이언트가 연결이 됐다가 끊겼다는 것을 <code>id</code> 로 알 수 있다.
<br></p>
<h2 id="-socket-adapter">** socket adapter**</h2>
<p>adapter는 쉽게 말해, 여러 소켓 서버들을 연결해주는 모듈이라고 생각하면 된다. 
여러 서버들간의 데이터를 주고 받는 다거나 상태 값을 공유할 때 쓰인다. </p>
<p>nestjs 에서는 기본적으로 socket을 socketIO 로 연결한다.  ws로 연결시켜주기 위해서는 따로 세팅이 필요하다.</p>
<h3 id="1-socketio-with-not-adapter">1. socketio with not Adapter</h3>
<pre><code class="language-yml">Namespace {
  _events: [Object: null prototype] {},
  _eventsCount: 0,
  _maxListeners: undefined,
  sockets: Map(0) {},
  _fns: [],
  _ids: 0,
  server: Server {
    _events: [Object: null prototype] {},
    _eventsCount: 0,
    _maxListeners: undefined,
    _nsps: Map(1) { &#39;/&#39; =&gt; [Circular *1] },
    parentNsps: Map(0) {},
    parentNamespacesFromRegExp: Map(0) {},
    _path: &#39;/socket.io&#39;,
    clientPathRegex: /^\/socket\.io\/socket\.io(\.msgpack|\.esm)?(\.min)?\.js(\.map)?(?:\?|$)/,
    _connectTimeout: 45000,
    _serveClient: true,
    _parser: {
      protocol: 5,
      PacketType: [Object],
      Encoder: [class Encoder],
      Decoder: [class Decoder extends Emitter]
    },
    ... 이하 생략
  },
  name: &#39;/&#39;,
  adapter: Adapter {
    _events: [Object: null prototype] {},
    _eventsCount: 0,
    _maxListeners: undefined,
    nsp: [Circular *1],
    rooms: Map(0) {},
    sids: Map(0) {},
    encoder: Encoder { replacer: undefined },
    [Symbol(kCapture)]: false
  },
  [Symbol(kCapture)]: false
}</code></pre>
<p>그림과 같이 socketIO 로 연결하고 Adapter 또한 사용하고 있지 않는 것을 볼 수 있다. ** 하지만 redis 를 이용해 socketIO Adapter 를 이용한다면 ** (아래 코드가 있음)</p>
<h3 id="2-socketio-with-adapter">2. socketio with Adapter</h3>
<pre><code class="language-yml">Namespace {
  _events: [Object: null prototype] {},
  _eventsCount: 0,
  _maxListeners: undefined,
  sockets: Map(0) {},
  _fns: [],
  _ids: 0,
  server: Server {
    _events: [Object: null prototype] {},
    _eventsCount: 0,
    _maxListeners: undefined,
    _nsps: Map(1) { &#39;/&#39; =&gt; [Circular *1] },
    parentNsps: Map(0) {},
    parentNamespacesFromRegExp: Map(0) {},
    _path: &#39;/socket.io&#39;,
    clientPathRegex: /^\/socket\.io\/socket\.io(\.msgpack|\.esm)?(\.min)?\.js(\.map)?(?:\?|$)/,
   ... 이하 생략
  },
  name: &#39;/&#39;,
  adapter: RedisAdapter {
    _events: [Object: null prototype] {},
    _eventsCount: 0,
    _maxListeners: undefined,
    nsp: [Circular *1],
    rooms: Map(0) {},
    sids: Map(0) {},
    encoder: Encoder { replacer: undefined },
    pubClient: Commander {
      _events: [Object: null prototype],
      _eventsCount: 1,
      _maxListeners: undefined,
      commandOptions: [Function: commandOptions],
      select: [AsyncFunction: SELECT],
      subscribe: [Function: SUBSCRIBE],
      unsubscribe: [Function: UNSUBSCRIBE],
      pSubscribe: [Function: PSUBSCRIBE],
      pUnsubscribe: [Function: PUNSUBSCRIBE],
      sSubscribe: [Function: SSUBSCRIBE],
      sUnsubscribe: [Function: SUNSUBSCRIBE],
      ... 이하생략
    },
    subClient: Commander {
      _events: [Object: null prototype],
      _eventsCount: 1,
      _maxListeners: undefined,
      commandOptions: [Function: commandOptions],
      select: [AsyncFunction: SELECT],
      subscribe: [Function: SUBSCRIBE],
      unsubscribe: [Function: UNSUBSCRIBE],
      pSubscribe: [Function: PSUBSCRIBE],
      pUnsubscribe: [Function: PUNSUBSCRIBE],
      sSubscribe: [Function: SSUBSCRIBE],
      sUnsubscribe: [Function: SUNSUBSCRIBE],
      ... 이하생략
    },
    ... 이하생략
    channel: &#39;socket.io#/#&#39;,
    requestChannel: &#39;socket.io-request#/#&#39;,
    responseChannel: &#39;socket.io-response#/#&#39;,
    specificResponseChannel: &#39;socket.io-response#/#aVBDBs#&#39;,
    friendlyErrorHandler: [Function (anonymous)],
    [Symbol(kCapture)]: false
  },
  [Symbol(kCapture)]: false
}</code></pre>
<p>다음과 같이 socketIO 를 사용하면서 <code>Adapter 또한 사용함</code>을 알 수 있다.</p>
<h3 id="코드">코드</h3>
<pre><code class="language-ts">// io-adapter.ts
import { IoAdapter } from &#39;@nestjs/platform-socket.io&#39;;
import { ServerOptions } from &#39;socket.io&#39;;
import { createAdapter } from &#39;@socket.io/redis-adapter&#39;;
import { createClient } from &#39;redis&#39;;

export class RedisIoAdapter extends IoAdapter {
  private adapterConstructor: ReturnType&lt;typeof createAdapter&gt;;

  async connectToRedis(): Promise&lt;void&gt; {
    const pubClient = createClient({ url: `redis://localhost:6379` });
    const subClient = pubClient.duplicate();

    await Promise.all([pubClient.connect(), subClient.connect()]);

    this.adapterConstructor = createAdapter(pubClient, subClient); // publish client 와 subscribe client 끼리 연결?
  }

  createIOServer(port: number, options?: ServerOptions): any {
    const server = super.createIOServer(port, options);
    server.adapter(this.adapterConstructor);
    return server;
  }
}</code></pre>
<h3 id="3-web-socket-with-adapter">3. web socket with Adapter</h3>
<p>다음으로 ** ws adpater ** 를 사용하도록 설정하면 (아래 코드 있음) </p>
<pre><code class="language-yml">WebSocketServer {
  _events: [Object: null prototype] {
    connection: [Function (anonymous)],
    error: [Function (anonymous)]
  },
  _eventsCount: 2,
  _maxListeners: undefined,
  _server: &lt;ref *1&gt; Server {
    maxHeaderSize: undefined,
    insecureHTTPParser: undefined,
    requestTimeout: 300000,
    headersTimeout: 60000,
    keepAliveTimeout: 5000,
    connectionsCheckingInterval: 30000,
    joinDuplicateHeaders: undefined,
    ... 이하 생략
  _removeListeners: [Function: removeListeners],
  clients: Set(0) {},
  _shouldEmitClose: false,
  options: {
    ... 이하생략
    port: 80,
    WebSocket: &lt;ref *2&gt; [class WebSocket extends EventEmitter] {
      CONNECTING: 0,
      OPEN: 1,
      CLOSING: 2,
      CLOSED: 3,
      createWebSocketStream: [Function: createWebSocketStream],
      Server: [class WebSocketServer extends EventEmitter],
      Receiver: [class Receiver extends Writable],
      Sender: [class Sender],
      WebSocket: [Circular *2],
      WebSocketServer: [class WebSocketServer extends EventEmitter]
    }
  },
  _state: 0,
  [Symbol(kCapture)]: false
}</code></pre>
<h3 id="코드-1">코드</h3>
<pre><code class="language-ts">// ws-adapter.ts
import * as WebSocket from &#39;ws&#39;;
import { WebSocketAdapter, INestApplicationContext } from &#39;@nestjs/common&#39;;
import { MessageMappingProperties } from &#39;@nestjs/websockets&#39;;
import { Observable, fromEvent, EMPTY } from &#39;rxjs&#39;;
import { mergeMap, filter } from &#39;rxjs/operators&#39;;

export class WsAdapter implements WebSocketAdapter {
  constructor(private app: INestApplicationContext) {}

  create(port: number, options: any = {}): any {
    return new WebSocket.Server({ port, ...options });
  }


  bindClientConnect(server, callback: Function) {
    server.on(&#39;connection&#39;, callback);
  }

  bindMessageHandlers(
    client: WebSocket,
    handlers: MessageMappingProperties[],
    process: (data: any) =&gt; Observable&lt;any&gt;,
  ) {
    fromEvent(client, &#39;message&#39;)
      .pipe(
        mergeMap(data =&gt; this.bindMessageHandler(data, handlers, process)),
        filter(result =&gt; result),
      )
      .subscribe(response =&gt; client.send(JSON.stringify(response)));
  }

  bindMessageHandler(
    buffer,
    handlers: MessageMappingProperties[],
    process: (data: any) =&gt; Observable&lt;any&gt;,
  ): Observable&lt;any&gt; {
    const message = JSON.parse(buffer.data);
    const messageHandler = handlers.find(
      handler =&gt; handler.message === message.event,
    );
    if (!messageHandler) {
      return EMPTY;
    }
    return process(messageHandler.callback(message.data));
  }

  close(server) {
    server.close();
  }
}</code></pre>
<p>얼추 어떻게 다르고 역할을 하는지 알았으니, 이제 adapter (redis io apater) 를 이용해서 테스트를 해보자.
두개의 소켓 서버를 생성한다 </p>
<h5 id="port--80kor-81usa">port : 80(kor), 81(usa)</h5>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/74c03d7f-83b5-4a6f-ab21-944532f8969c/image.png" width="100%" height="50%"></img></p>
<p>테스트는 postman 으로 했다. </p>
<div class="video-container">
    <video autoplay muted controls src="https://velog.velcdn.com/images/leejm_dev/post/861f3174-2ee1-4561-ba61-8a2c74b109b2/image.mov" width="100%" height="30%"></video>
</div>

<p><a href="https://youtu.be/kVCnzHvUbT8">postman test video</a></p>
<p>영상을 보면 한국에서 보낸 메세지를 미국에서도 수신할 수 있는 것으로 보인다. </p>
<p>이 처럼 다른 서버간의 소켓 통신을 위해서는 <code>adapter</code> 로 통신할 수 있음을 확인할 수 있다. </p>
<hr>
<h3 id="github">github</h3>
<p><a href="https://github.com/jjmmll0727/potential-nestjs-memory/tree/main/socket-kafka">https://github.com/jjmmll0727/potential-nestjs-memory/tree/main/socket-kafka</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[결제 테이블 재설계]]></title>
            <link>https://velog.io/@leejm_dev/%EA%B2%B0%EC%A0%9C-%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%9E%AC%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@leejm_dev/%EA%B2%B0%EC%A0%9C-%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%9E%AC%EC%84%A4%EA%B3%84</guid>
            <pubDate>Sun, 01 Oct 2023 06:31:46 GMT</pubDate>
            <description><![CDATA[<p>작년 겨울에 처음으로 결제 모델을 붙였다. 
<code>결제</code> 라는 기능을 처음 만들다 보니, 편협한 사고와 시야로 결제 테이블 스키마를 설계 했었다. 
그때 당시 <code>나이스페이</code>라는 pg 사를 사용했기 때문에, 사실상 나이스페이에만 국한되는 테이블을 설계했었다. </p>
<p>이번에는 기존에 존재하는 결제 기능에 대출서비스, 마켓 기능을 추가로 붙이다 보니 추가해야 할 결제 관련 테이블들이 늘어났다. 
ex) 세금계산서, 거래명세서, 주문이력, 대출이력...</p>
<blockquote>
<p>물론 현재 있는 결제 테이블에 계속 더하거나 비정규화를 통해서 만들 수 있었지만, 이렇게 계속 개발하면 발전도 없고, 점점 시간이 지나서 다른 pg 사를 사용하던가 다른 방법으로 결제를 하게 되는 기능이 생긴다면 무결성은 위배될 것이고, 스키마 확장성은 줄어들 것으로 보였다. </p>
</blockquote>
<p>그래서 이번에 기존에 있던 결제 관련 스키마를 재설계 하면서 대출 기능을 추가하기로 하였다. 대출 기능 배포 기간도 맞춰야 하는 문제가 있고, <span style="color:orange"><strong>재설계를 한다면 기존에 구현되어 있던 기능들 테스트도 물론 해야하는 문제도 있다.</strong></span></p>
<hr>
<h3 id="🔥어떻게-진행했나🔥">🔥어떻게 진행했나🔥</h3>
<p>두번째 문제를 잘 해결하기 위해 우리는 이런 방법으로 진행했다. </p>
<p>1)  기존 테이블에 insert 하는 로직은 그대로 두고, 새롭게 설계된 테이블에도 insert 를 한다. 
기존 테이블이 <code>payment</code> 라는 테이블이라면 우리는 <code>payment_root</code> 와 <code>payment_rfq</code>, <code>payment_strategy_nicepay</code> 와 같이 분할했다. 
<code>insert into payment</code> 하는 로직에서 <code>insert into payment_root, insert into payment_rfq, insert into payment_strategy_nicepay</code> 를 같이 하는거다.</p>
<p>2) 조회하는 로직에서는 대출 관련 데이터는 새롭게 설계된 테이블을 대상으로 <strong>새롭게 로직을 짜고</strong>, 대출 아닌 데이터에 대해서는 기존에 사용하던 로직 그대로 이용
<span style="color:orange"><strong>대신 추후에 데이터를 이관했을 때에도 정상적으로 조회가 될 수 있게끔 쿼리를 짠다.</strong></span> 
👉 <em>이 부분이 쿼리를 짜는데 까다로웠다.</em></p>
<p>3) 수정하는 로직은 다행히도 대출관련 결제 기능에서는 수정하는 부분이 많지 않아서 2번과 비슷하게 기존에 사용하던 수정 로직은 사용하고, 대출관련 데이터들은 새롭게 만들었다. 수정하는 로직은 배포하고 나서 천천히 같이 바꿔줘도 되기 때문에 급하지 않았다.</p>
<p>물론 회사가 운영하고 있는 서비스의 상황이나 디비의 상황에 따라 방법은 다르겠지만, 기존에 잘 쓰고 있는 로직을 한꺼번에 다 바꾸는 것은 리스크가 따른다. 그렇기 때문에 <em>우리는 조회부터 바꿀꺼야 우리는 삽입부터 바꿀꺼야</em> 이런것들을 결정하는 건 회사 상황에 따라 결정하면 되지만, 이거 하나는 확실한 것 같다. 
<strong>새로 설계된 테이블에 삽입할때는 새로 설계한 테이블에도 같이 넣어주는 것이다.</strong></p>
<p>그리고 나중에 배포하기 전에 데이터 마이그레이션 했을 경우 새로 만든 조회 로직으로도 잘 나오면 땡큐고 아니면 일단 마이그레이션 롤백하고 배포 후에 다시 조회 로직 수정하면 되는 것이다. </p>
<hr>
<h3 id="📦-new-schema">📦 new schema</h3>
<img src="https://velog.velcdn.com/images/leejm_dev/post/2ec1b85e-0cd1-4889-bcc9-3ad66d1cd4cb/image.png">


<img src="https://velog.velcdn.com/images/leejm_dev/post/83886a80-a50d-431d-870e-5e3a7906657e/image.png" width="40%" height="60%" float="left">

<p>현재는 단건결제만 있지만, 대출이 들어오면서 분할결제도 생기고 추후에 있을 복합결제까지 고려해서 설계 하였다. </p>
<p><strong>&lt;단건 결제&gt;</strong>
 👉🏻 APP : 통합결제 : 결제서비스 : 수단  = 1 : 1 : 1 : 1
    ex) 발주서 1 : 통합결제 1 : RFQ 결제 1 : 나이스페이 1</p>
<p><strong>&lt;분할 결제&gt;</strong>
👉🏻 APP : 통합결제 : 결제 서비스 : 수단 = 1 : N : N : N
ex) 대출 결제 (선결제 10%, 대출 90%)
발주서 1 : 통합결제 2 : 결제서비스 2 : 수단 2 (나이스페이 1 + 대출 1)</p>
<p><strong>&lt;복합 결제&gt;</strong> 
 👉🏻 APP : 통합결제 : 결제서비스 : 수단 = 1 : 1 : N : N
ex) 포인트로 일부 결제
발주서 1 : 통합결제 1 : 결제서비스 2 : 수단 2 (나이스페이 1 + 포인트 1)</p>
<hr>
<h3 id="☠️-이거-생각보다-할일이-많은데">☠️ 이거 생각보다 할일이 많은데...?</h3>
<p>개발하면서 잘못 건들였다는 생각이 한두 번이 아니였다. 기존에 구현되어 있는 로직 수정하는 것도 일이었고, 기존에 비정규화되어 있던 테이블에 있던 칼럼들이 뿔뿔이 흩어지는 것 이기 때문에 체크할 것들도 너무 많았다. </p>
<p>괜히 뭐 하나 빠트리거나 중복되는 로직이 생긴다면 이대로 또 골치였다.</p>
<p><strong>그래도 이렇게 기존에 잘 쓰고 있는 테이블을 재설계 하는 경험은 어디서도 할 수 없는 값진 경험이었다.</strong></p>
<p>아직 대출 기능 개발중이지만, 이미 엎질러진 물이다. 문제가 생기더라도 어떻게 해서든지 반드시 해결해야 한다. 
2023.10.01</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[mirroring with hmac]]></title>
            <link>https://velog.io/@leejm_dev/mirroring-with-hmac</link>
            <guid>https://velog.io/@leejm_dev/mirroring-with-hmac</guid>
            <pubDate>Wed, 06 Sep 2023 23:30:08 GMT</pubDate>
            <description><![CDATA[<p><code>hmac : hash-based message authentication code
암호화 해시 함수와 암호화 키를 동반한 메세지 인증 방법이다.</code></p>
<h3 id="🔥-개발-계기">🔥 개발 계기</h3>
<p>CX 팀에서 VOC 를 듣고 핸들링할 때 BO사이트에서 대고객 사이트를 미러링 할 수 있는 기능이 있으면 좋겠다 라는 애로사항이 있어서 미러링 기능을 만들게 되었다. </p>
<p>실제 완벽한 미러링이라면, 원격지원처럼 실시간으로 마우스 포인터도 움직여지고 클릭 하나하나 전부 동시 확인 가능한 기능이겠지만, 우리는 그렇게 까지는 아니고, 고객의 동의하에 고객의 계정으로 로그인을 해서 접속하는 방식을 채택했다. </p>
<h3 id="🔥-시나리오">🔥 시나리오</h3>
<p>시나리오는 다음과 같다. </p>
<ol>
<li>BO 프론트에서 BO 백으로 미러링 요청을 한다.</li>
<li>BO 백에서 일련의 검증 후에 hmac signiture 을 만들어 반환한다.</li>
<li>BO 프론트에서 해당 hmac signiture 을 가지고 메인 사이트 프론트로 끌고간다. </li>
<li>메인 사이트 프론트에서 메인 사이트 백으로 hmac signiture 를 전달한다. </li>
<li>hmac signiture 를 다시 BO 백으로 전달후, hmac signiture 를 검증한다. </li>
<li>검증 후 이상 없으면 메인 사이트 토큰(authentication token)을 발급한다. </li>
</ol>
<h3 id="🔥-프로세스">🔥 프로세스</h3>
<p><img src="https://velog.velcdn.com/images/leejm_dev/post/a4362dd1-54e9-433b-9d72-1958a2a1f4a8/image.png" alt="capa-mirroring-flow-chart"></p>
<h3 id="🔥-질문">🔥 질문</h3>
<ol>
<li><p>왜 굳이 bo api 에서 발급한 <code>hmac signiture</code> 를 <strong>bo api -&gt; bo -&gt; capa -&gt; capa api -&gt; bo api</strong> 와 같이 복잡하게 돌아가는가 ? 
👉 bo api 로 토큰 발급 요청이 우리 서비스 capa api 로 부터 온 것인지 아닌지를 확인할 수 있다.</p>
</li>
<li><p>timeStamp(5s)는 무슨 역할인가 ?
👉 혹여나 누군가 hmac signiture 를 약탈해서 요청을 했을 경우를 대비해서 5초 라는 약간의 방어책을 만든 것이다. 동일한 환경에서 동일한 시그니처를 약탈해도 <strong>5초가 넘어가면 이상 요청</strong>이라고 판단하기 위함이다. </p>
</li>
<li><p>authCode 는 무슨 역할인가 ?
👉 timeStamp 와 마찬가지로, 방어책 역할이다. 동일 환경에서 동일 시그니처로 요청을 보냈는데 5초 안에 요청을 보냈다면 정상 요청이라고 판단할 수 있고, 혹시 이미 보냈던 요청을 그대로 복사해서 다시 요청을 보낼 수도 있다. 
또한, timeStamp 는 사실 요청 시간을 변조해서 보낼 수 있기 때문에 완벽한 방어책이라고는 볼 수 없다. 
그래서 authCode(uuid)를 만들어 한번 요청을 보내면 디비에 체크를 하고 동일 authCode 로 요청이 오면 이상 요청이라고 판단하기 위함이다. </p>
</li>
</ol>
<h3 id="🔥-코드">🔥 코드</h3>
<pre><code class="language-js">const hmac = crypto.createHmac(&#39;sha-256&#39;, accessKey);
const signiture = hmac.update(message.join(&#39;&#39;)).digest(&#39;base64&#39;);</code></pre>
<p>hmac 만드는 코드는 많이 나와있기 때문에 간단하게 넘어가겠다. 주고 받는 쪽에서 그들만의 규칙을 정해서 만들면 되기 때문에, 중요한 건 저 코드 두줄이 될 것으로 보인다. </p>
]]></description>
        </item>
    </channel>
</rss>