<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발블로그</title>
    <link>https://geonbbang.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 6 Jun 2026 18:41:28 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>MIRACLE LIFE</managingEditor>
    <item>
      <title>NestJS에서 AsyncLocalStorage로 traceId 추적하기</title>
      <link>https://geonbbang.tistory.com/76</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;한 요청에서 발생하는 모든 로그에 같은 식별자(traceId)를 주입하여 추적하고 싶다.&lt;br /&gt;처음엔 NestJS의 Request Scope로 풀었지만, 왜 결국 AsyncLocalStorage로 바꿨는지에 대해 기록한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배경: 왜 traceId가 필요한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 한 번의 HTTP 요청이 처리되는 동안 여러 곳에서 로그가 찍힌다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;미들웨어: &quot;요청이 들어왔다&quot;&lt;/li&gt;
&lt;li&gt;컨트롤러/서비스: &quot;유저를 조회한다&quot;&lt;/li&gt;
&lt;li&gt;레포지토리: &quot;DB 쿼리를 날렸다&quot;&lt;/li&gt;
&lt;li&gt;인터셉터: &quot;응답을 보낸다 (XXms 걸림)&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 중 장애가 났을 때, 우리가 보고 싶은 건 &lt;b&gt;&quot;이 요청 하나가 만들어낸 로그들&quot;&lt;/b&gt; 이다. 다른 요청들의 로그와 뒤섞이면 디버깅이 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 요청이 들어오는 순간 식별자를 하나 발급하고, 그 요청 안에서 찍히는 모든 로그에 그 식별자를 박는다. 이걸 보통 &lt;b&gt;traceId&lt;/b&gt;(혹은 correlationId, requestId)라고 부른다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{ &quot;level&quot;: &quot;info&quot;, &quot;traceId&quot;: &quot;abc-123&quot;, &quot;msg&quot;: &quot;user fetched&quot; }
{ &quot;level&quot;: &quot;info&quot;, &quot;traceId&quot;: &quot;abc-123&quot;, &quot;msg&quot;: &quot;post fetched&quot; }
{ &quot;level&quot;: &quot;info&quot;, &quot;traceId&quot;: &quot;def-456&quot;, &quot;msg&quot;: &quot;user fetched&quot; }   // 다른 요청&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 로그 시스템(Datadog, ELK, NewRelic 등)에서 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;traceId=&quot;abc-123&quot;&lt;/span&gt;로 필터만 걸면 그 요청의 전체 흐름을 한 화면에 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NestJS Request Scope&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS는 모든 provider를 기본적으로 &lt;b&gt;싱글톤&lt;/b&gt;으로 만든다. 앱이 뜰 때 한 번 인스턴스화되고, 죽을 때까지 그 인스턴스를 재사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 NestJS는 두 가지 다른 스코프를 더 제공한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Scope&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;DEFAULT&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;싱글톤. 앱당 1개 인스턴스.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;REQUEST&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;요청당 1개 인스턴스. 매 HTTP 요청마다 새로 만들고, 응답이 끝나면 GC.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;TRANSIENT&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;주입할 때마다 1개. 거의 안 씀.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Scope.REQUEST&lt;/span&gt;는 요청 단위 컨텍스트를 다룰 때 매력적이다. 매 요청마다 새 인스턴스가 만들어지므로 &lt;b&gt;&quot;이 요청의 traceId는 이 인스턴스의 필드다&quot;&lt;/b&gt; 라고 단순하게 둘 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음엔 이렇게 짰다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Injectable({ scope: Scope.REQUEST })
export class AppLogger implements LoggerService {
  constructor(
    @Inject(REQUEST)
    private readonly request: (Request &amp;amp; { traceId?: string }) | undefined,
  ) { /* winston 인스턴스 생성 */ }

  log(message: unknown, context?: string) {
    this.logger.info(message, {
      traceId: this.request?.traceId ?? 'unknown',
      context,
    });
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미들웨어에서 traceId를 발급해 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;req.traceId&lt;/span&gt;에 넣어두면, 로거 인스턴스가 그 요청의 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;req&lt;/span&gt;를 들고 있으므로 자동으로 traceId가 메타데이터에 들어간다. 깔끔해 보였다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const traceId = req.header('x-correlation-id') ?? randomUUID();
    req.traceId = traceId;
    res.setHeader('x-correlation-id', traceId);
    next();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기대한 대로 작동은 한다. 그런데 이 방식에는 약간의 문제가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Request Scope의 문제&lt;/h2&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size23&quot;&gt;1. 미들웨어와 인터셉터에서 logger를 쓰지 못해 console.log로 우회&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS에서 클래스 미들웨어와 인터셉터는 기본적으로 &lt;b&gt;앱이 뜰 때 한 번 인스턴스화되는 싱글톤&lt;/b&gt;으로 동작한다. 부트스트랩 시점엔 아직 어떤 요청도 들어오지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;Scope.REQUEST&lt;/span&gt;인 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;AppLogger&lt;/span&gt;를 주입할 수 없다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;부트스트랩 시점에는 request 컨텍스트가 없어 request-scoped 의존성을 정상적으로 해소할 수 없다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;따라서 미들웨어와 인터셉터 안에서는 그냥 console.log를 사용하여 로그를 남겼다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// LoggerMiddleware
console.log(JSON.stringify({ traceId, type: '[REQUEST]', method, path }));

// LoggingInterceptor
console.log(JSON.stringify({ traceId, type: '[RESPONSE]', statusCode, latencyMs }));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 같은 로깅이라는 관심사를 두고 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;winston&lt;/span&gt; 기반 logger와 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;console.log&lt;/span&gt;가 섞여 있는 일관성 없는 코드가 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포맷도 다르고, transport도 다르고, 코드 베이스에서 로그를 찾을 때 두 군데를 봐야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 성능 비용 발생&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미들웨어/인터셉터에서 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;console.log&lt;/span&gt;를 사용한다고 해결된 게 아니었다. &lt;b&gt;logger를 주입받던 모든 일반 서비스&lt;/b&gt; 가 request scope의 영향권 안에 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS 공식 문서의 &lt;a href=&quot;https://docs.nestjs.com/fundamentals/injection-scopes#scope-hierarchy&quot;&gt;Scope hierarchy&lt;/a&gt; 섹션은 이렇게 동작을 명시한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;&lt;b&gt;The REQUEST scope bubbles up the injection chain.&lt;/b&gt; A controller that depends on a request-scoped provider will, itself, be request-scoped.&quot;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 동작은 한 번 request scope에 발을 들이면, 그 의존성을 쓰는 모든 클래스가 request scope로 끌려간다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 케이스에 그대로 대입해보면&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;SampleController &amp;lt;- SampleService &amp;lt;- AppLogger(REQUEST)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppLogger가 request-scoped이므로 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;SampleService&lt;/span&gt;는 자동으로 request-scoped가 되고, 그걸 주입한 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;SampleController&lt;/span&gt;도 request-scoped가 된다. 의존성 사슬을 따라 줄줄이 끌려간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS 공식 문서의 &lt;a href=&quot;https://docs.nestjs.com/fundamentals/injection-scopes#durable-providers&quot;&gt;Durable providers&lt;/a&gt; 섹션은 이 케이스를 거의 정확히 짚는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Having a common provider that most providers depend on (think of a database connection, or &lt;b&gt;a logger service&lt;/b&gt;), automatically converts all those providers to request-scoped providers as well.&quot;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매 요청마다 컨트롤러, 서비스, 그리고 그것들의 의존성 전부가 새로 인스턴스화되고 GC된다. 트래픽이 적을 땐 체감이 거의 없지만, 동시 요청이 많아질수록 비용이 누적된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;console.log 우회&lt;/b&gt;는 미들웨어/인터셉터를 건드리지 않으려고 한 회피책&lt;/li&gt;
&lt;li&gt;&lt;b&gt;진짜 성능 비용&lt;/b&gt;은 logger를 정상적으로 주입하던 일반 서비스 쪽에서 이미 발생하고 있었다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 매 요청마다 winston 인스턴스가 새로 만들어진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppLogger의 생성자는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;winston.createLogger({...})&lt;/span&gt;를 호출한다. request scope이기 때문에 이게 &lt;b&gt;요청마다 한 번씩 실행&lt;/b&gt; 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;transport 설정, 포맷터 조립, 메타데이터 초기화 등은 한 번만 하면 충분한 일이다. 굳이 매 요청 비용을 지불할 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결책: AsyncLocalStorage&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AsyncLocalStorage(이하 ALS)는 Node.js 12.17부터 표준 모듈(node:async_hooks)에 포함된 기능이다. 한 줄로 표현하면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비동기 호출 체인 전체에 걸쳐 살아남는 컨텍스트 저장소&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 다른 언어와 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 언어를 써본 사람이라면 &lt;b&gt;Thread-Local Storage(TLS)&lt;/b&gt; 를 떠올리면 가장 빠르다. &quot;이 스레드만의 변수&quot;를 두는 그 기능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, Node.js는 싱글스레드 이벤트 루프이기 때문에 &quot;스레드별&quot;이 아니라 &lt;b&gt;&quot;async chain별&quot;&lt;/b&gt; 이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 어떻게 동작하는지 코드로 보기&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage&amp;lt;{ traceId: string }&amp;gt;();

als.run({ traceId: 'abc-123' }, () =&amp;gt; {
  setTimeout(() =&amp;gt; {
    console.log(als.getStore()); // { traceId: 'abc-123' }
  }, 100);

  Promise.resolve().then(() =&amp;gt; {
    console.log(als.getStore()); // { traceId: 'abc-123' }
  });
});

console.log(als.getStore()); // undefined (run 콜백 밖)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;run(store, callback)&lt;/span&gt;으로 컨텍스트를 시작하면, 그 콜백 안에서 시작된 &lt;b&gt;모든 비동기 작업&lt;/b&gt;(Promise 체인, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;await&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;setTimeout&lt;/span&gt;, 이벤트 핸들러 등)에서 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;getStore()&lt;/span&gt;로 같은 store를 꺼낼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 해결&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;미들웨어에서 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;als.run({ traceId }, () =&amp;gt; next())&lt;/span&gt;로 컨텍스트를 연다.&lt;/li&gt;
&lt;li&gt;그 안에서 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;next()&lt;/span&gt;가 실행되면, 이후 모든 인터셉터/컨트롤러/서비스/레포지토리가 같은 컨텍스트에서 동작한다.&lt;/li&gt;
&lt;li&gt;로거는 그냥 ALS에서 traceId를 꺼내쓰면 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로거가 더 이상 request scope일 필요가 없다 = 싱글톤으로 회귀할 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스코프 전염 문제도, winston을 매번 생성하는 문제도 한 번에 해결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 리팩토링&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 1. ALS 래퍼 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ALS(AsyncLocalStorage)자체는 NestJS와 무관한 순수 Node API지만, NestJS DI에 태우면 테스트가 깔끔해진다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// src/logger/request-context.service.ts
import { Injectable } from '@nestjs/common';
import { AsyncLocalStorage } from 'node:async_hooks';

export type RequestContextStore = {
  traceId: string;
};

@Injectable()
export class RequestContextService {
  private readonly storage = new AsyncLocalStorage&amp;lt;RequestContextStore&amp;gt;();

  run&amp;lt;T&amp;gt;(store: RequestContextStore, callback: () =&amp;gt; T): T {
    return this.storage.run(store, callback);
  }

  getTraceId(): string | undefined {
    return this.storage.getStore()?.traceId;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 2. AppLogger를 싱글톤으로 되돌리기&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// Before
@Injectable({ scope: Scope.REQUEST })
export class AppLogger {
  constructor(@Inject(REQUEST) private readonly request: ...) {}

  private getRequestMeta() {
    return { traceId: this.request?.traceId ?? 'unknown' };
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// After
@Injectable() // = Scope.DEFAULT (싱글톤)
export class AppLogger {
  constructor(private readonly requestContext: RequestContextService) {}

  private getRequestMeta() {
    return { traceId: this.requestContext.getTraceId() ?? 'unknown' };
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Inject(REQUEST)&lt;/span&gt;도, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;Scope.REQUEST&lt;/span&gt;도 사라졌다. 인스턴스는 앱당 1개. winston도 앱당 1개.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 3. 미들웨어에서 ALS 컨텍스트 시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 이번 리팩토링의 핵심이다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  constructor(
    private readonly logger: AppLogger,
    private readonly requestContext: RequestContextService,
  ) {}

  use(req: Request, res: Response, next: NextFunction) {
    const traceId = req.header('x-correlation-id') ?? randomUUID();
    res.setHeader('x-correlation-id', traceId);

    this.requestContext.run({ traceId }, () =&amp;gt; {
      this.logger.log(`${req.method} ${req.url}`, '[REQUEST]', {
        method: req.method,
        path: req.url,
      });
      next(); // &amp;larr; 이 안에서 시작된 모든 비동기 체인은 같은 traceId 컨텍스트
    });
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요한 포인트는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;next()&lt;/span&gt;가 반드시 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;run()&lt;/span&gt; 콜백 안에서 호출되어야 한다는 것이다.&lt;/b&gt; 그래야 라우트 핸들링 전체가 ALS 컨텍스트 안에 들어간다.&lt;/p&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;// ❌ BAD - 컨텍스트 밖에서 next 호출
this.requestContext.run({ traceId }, () =&amp;gt; {});
next(); // 여기는 ALS 밖

// ✅ GOOD
this.requestContext.run({ traceId }, () =&amp;gt; {
  next();
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 4. 인터셉터는 그냥 logger를 받아쓰면 끝&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  constructor(private readonly logger: AppLogger) {} // 싱글톤이라 자유롭게 주입 OK

  intercept(context: ExecutionContext, next: CallHandler) {
    const httpContext = context.switchToHttp();
    const request = httpContext.getRequest&amp;lt;Request&amp;gt;();
    const response = httpContext.getResponse&amp;lt;Response&amp;gt;();
    const startedAt = Date.now();

    return next.handle().pipe(
      tap(() =&amp;gt; {
        this.logger.log(
          `${request.method} ${request.url} ${response.statusCode}`,
          '[RESPONSE]',
          { latencyMs: Date.now() - startedAt },
        );
        // traceId는 ALS에서 자동으로 채워짐
      }),
    );
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;console.log&lt;/span&gt; 우회가 사라지고, 모든 곳에서 같은 logger를 쓴다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 5. LoggerModule로 묶고 글로벌로 노출&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;NestJS DI는 모듈 단위로 인스턴스를 만든다.&lt;/b&gt; 한 클래스를 두 모듈에서 각각 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;providers&lt;/span&gt;에 넣으면, 싱글톤이라도 인스턴스가 두 개 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 막기 위해 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;LoggerModule&lt;/span&gt;로 묶고 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Global()&lt;/span&gt;을 붙였다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Global()
@Module({
  providers: [AppLogger, RequestContextService],
  exports: [AppLogger, RequestContextService],
})
export class LoggerModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;AppModule&lt;/span&gt;에서 한 번만 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;import: [LoggerModule]&lt;/span&gt;해두면 어디서든 같은 인스턴스를 주입받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;조심해야 할 ALS의 특징&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 일부 비동기 패턴은 컨텍스트가 끊긴다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;async/await&lt;/span&gt;, Promise 체인, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;setTimeout&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;setImmediate&lt;/span&gt; 같은 일반적인 비동기는 ALS를 잘 통과한다. 하지만 다음과 같은 상황에서는 끊길 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;EventEmitter 패턴&lt;/b&gt;: emit 시점이 아니라 &lt;b&gt;listener가 등록된 시점&lt;/b&gt;의 컨텍스트가 유지된다. 부트스트랩 시점에 등록된 리스너 안에서는 traceId가 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커넥션 풀 / Worker thread 경계&lt;/b&gt;: native binding을 거치는 일부 라이브러리는 컨텍스트를 잃을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우엔 ALS에서 traceId를 꺼내 명시적으로 같이 넘기는 식으로 우회한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 미들웨어 바깥의 로그는 traceId가 없다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부트스트랩 로그, cron 작업, 백그라운드 태스크 등 HTTP 요청 컨텍스트 밖에서 찍히는 로그는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;getStore()&lt;/span&gt;가 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;undefined&lt;/span&gt;라 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;traceId: 'unknown'&lt;/span&gt;으로 찍힌다. 정상 동작이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cron이라면 cron의 진입점에서 별도로 컨텍스트를 만들어주면 된다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@Cron('0 * * * *')
handleHourly() {
  this.requestContext.run({ traceId: `cron-${randomUUID()}` }, () =&amp;gt; {
    this.doWork();
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. ALS의 오버헤드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;async_hooks&lt;/span&gt;를 활성화하면 비동기 작업마다 약간의 오버헤드가 생긴다. Node.js 16 이후로는 V8 자체에 ALS 전용 최적화 경로가 있어서 영향이 매우 작다. 일반적인 웹 서버 워크로드에서는 측정 가능한 영향이 거의 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Before&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;After&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Logger scope&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;REQUEST&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;DEFAULT (싱글톤)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;traceId 전달 방법&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;req.traceId 직접 mutate&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;ALS로 자동 전파&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;미들웨어/인터셉터 로그&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;console.log 우회&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;일관된 logger 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;winston 인스턴스&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;요청마다 새로 생성&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;앱당 1번 생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Scope bubbling&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;발생&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;없음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Request Scope는 강력한 도구지만, &lt;b&gt;로거처럼 모든 곳에서 쓰이는 의존성&lt;/b&gt;에는 어울리지 않는다. 한 곳에서 request scope를 쓰면 거기에 닿는 모든 코드가 끌려간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AsyncLocalStorage에 한 번 익숙해지면 traceId 외에도&amp;nbsp; 같은 &quot;요청 단위로 따라다녀야 하는 것들&quot;을 깔끔하게 다룰 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://nodejs.org/api/async_context.html#class-asynclocalstorage&quot;&gt;Node.js Docs - AsyncLocalStorage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nestjs.com/fundamentals/injection-scopes&quot;&gt;NestJS Docs - Injection Scopes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nestjs.com/fundamentals/injection-scopes#scope-hierarchy&quot;&gt;NestJS Docs - Scope Hierarchy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>JavaScript</category>
      <author>MIRACLE LIFE</author>
      <guid isPermaLink="true">https://geonbbang.tistory.com/76</guid>
      <comments>https://geonbbang.tistory.com/76#entry76comment</comments>
      <pubDate>Sat, 23 May 2026 14:54:03 +0900</pubDate>
    </item>
    <item>
      <title>Docker를 이용하여 EC2에 Node 서버 배포하기 (2)</title>
      <link>https://geonbbang.tistory.com/75</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 아래 과정을 직접 수동으로 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Docker 이미지 빌드 -&amp;gt; ECR Push -&amp;gt; EC2 Pull -&amp;gt; docker run&lt;/span&gt;&lt;/i&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 매번 직접 명령어를 입력하는 것은 번거로운 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 GitHub Actions를 사용해서 Node 서버를 자동 배포하는 과정을 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://geonbbang.tistory.com/74&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://geonbbang.tistory.com/74&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1778483099390&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Docker를 이용하여 EC2에 Node 서버 배포하기 (1)&quot; data-og-description=&quot;Node 서버를 운영하다 보면 다음과 같은 문제들이 생길 수 있다.배포를 했더니 개발 환경과 운영 환경이 같지 않아서 문제가 발생한다.다른 개발자와의 개발 환경이 달라 충돌이 발생한다.가끔 &quot; data-og-host=&quot;geonbbang.tistory.com&quot; data-og-source-url=&quot;https://geonbbang.tistory.com/74&quot; data-og-url=&quot;https://geonbbang.tistory.com/74&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/WuMdT/dJMb84qdcMb/FGQ9WwyHNFkeQsiCcmI3W1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/caYELs/dJMb9jgBQdI/hAvFFme1E3Goi2OMQkGqAk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bX7P3b/dJMb9jgBQdH/AkIhxmZIHkxoSXqGSBfOa0/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://geonbbang.tistory.com/74&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://geonbbang.tistory.com/74&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/WuMdT/dJMb84qdcMb/FGQ9WwyHNFkeQsiCcmI3W1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/caYELs/dJMb9jgBQdI/hAvFFme1E3Goi2OMQkGqAk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bX7P3b/dJMb9jgBQdH/AkIhxmZIHkxoSXqGSBfOa0/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Docker를 이용하여 EC2에 Node 서버 배포하기 (1)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Node 서버를 운영하다 보면 다음과 같은 문제들이 생길 수 있다.배포를 했더니 개발 환경과 운영 환경이 같지 않아서 문제가 발생한다.다른 개발자와의 개발 환경이 달라 충돌이 발생한다.가끔&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;geonbbang.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름은 아래와 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;GitHub Actions 실행
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Docker 이미지 빌드&lt;/li&gt;
&lt;li&gt;AWS ECR Push&lt;/li&gt;
&lt;li&gt;EC2 스크립트 실행&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;EC2 스크립트 작성&lt;/li&gt;
&lt;li&gt;docker-compose.yml 작성&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GitHub Actions 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions 실행을 위해 아래 경로에 workflow 파일을 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;.github/workflows/deploy.yml&lt;/u&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778117230941&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Build + Deploy

on:
  push:
    branches: [dev]

jobs:
  build-analyze-deploy:
    runs-on: ubuntu-latest

    permissions:
      id-token: write
      contents: read
      pull-requests: write
      checks: write

    steps:
      # 코드 체크아웃
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # Node 세팅
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 24

      # 의존성 설치
      - name: Install deps
        run: npm install

      # AWS 인증 (OIDC)
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ap-northeast-2

      # ECR 로그인
      - name: Login to ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      # Docker build &amp;amp; push
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ steps.login-ecr.outputs.registry }}/geonbbang/sample-app:latest
            ${{ steps.login-ecr.outputs.registry }}/geonbbang/sample-app:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          retries: 2

      # EC2 배포
      - name: Deploy to EC2
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            bash ~/app/deploy.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 workflow가 실행되면 전체 흐름은 아래처럼 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub&amp;nbsp;Push&lt;br /&gt;&amp;rarr;&amp;nbsp;GitHub&amp;nbsp;Actions&amp;nbsp;실행&lt;br /&gt;&amp;rarr;&amp;nbsp;Docker&amp;nbsp;이미지&amp;nbsp;Build&lt;br /&gt;&amp;rarr;&amp;nbsp;ECR&amp;nbsp;Push&lt;br /&gt;&amp;rarr;&amp;nbsp;EC2&amp;nbsp;접속&lt;br /&gt;&amp;rarr;&amp;nbsp;deploy.sh&amp;nbsp;실행&lt;br /&gt;&amp;rarr;&amp;nbsp;Blue/Green&amp;nbsp;배포&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;deploy.sh 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions의 마지막 단계에서는 아래 명령이 실행된다.&lt;/p&gt;
&lt;pre id=&quot;code_1778132635622&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;script: |
  bash ~/app/deploy.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, EC2 내부에서 실제 배포를 수행하는 역할을 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;deploy.sh&lt;/p&gt;
&lt;pre id=&quot;code_1778132790875&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash

cd ~/app

STATE_FILE=~/app/deploy_state

CURRENT=$(cat $STATE_FILE)

if [ &quot;$CURRENT&quot; = &quot;blue&quot; ]; then
  NEW=green
  NEW_PORT=3001
  OLD=blue
  OLD_PORT=3000
else
  NEW=blue
  NEW_PORT=3000
  OLD=green
  OLD_PORT=3001
fi

echo &quot;Deploying $NEW...&quot;

echo &quot;Logging in to ECR...&quot;

aws ecr get-login-password \
--region ap-northeast-2 \
| docker login \
--username AWS \
--password-stdin {ECR_URL}

# 최신 이미지 pull
docker compose pull

# 새 컨테이너 실행
docker compose up -d app-$NEW

# =========================
# HEALTH CHECK
# =========================
echo &quot;Health checking...&quot;

for i in {1..10}; do
  if curl -f http://localhost:$NEW_PORT/health; then
    echo &quot;Healthy!&quot;
    break
  fi
  sleep 2
done

# 실패 시 롤백
if ! curl -f http://localhost:$NEW_PORT/health; then
  echo &quot;Deploy failed &amp;rarr; rollback&quot;
  docker compose stop app-$NEW
  exit 1
fi

# =========================
# NGINX SWITCH
# =========================
echo &quot;Switching nginx &amp;rarr; $NEW_PORT&quot;

sudo sed -i &quot;s/$OLD_PORT/$NEW_PORT/g&quot; /etc/nginx/sites-available/sample-app
sudo nginx -s reload

# 상태 저장
echo &quot;$NEW&quot; &amp;gt; $STATE_FILE

# 기존 컨테이너 종료
docker compose stop app-$OLD

echo &quot;Deploy complete&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 스크립트의 역할은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 서비스 중인 컨테이너는 그대로 둔 상태에서, 반대쪽 컨테이너에 새 버전 앱을 먼저 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 버전이 정상인지 헬스체크로 확인한 뒤, Nginx 설정을 바꿔 트래픽을 새 버전으로 전환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전환이 끝나면 이전 컨테이너를 종료하고, 문제가 있으면 기존 서비스는 유지한 채 배포만 실패 처리 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;deploy_state 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 현재 어떤 컨테이너가 운영 중인지 저장하는 파일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;deploy_state&lt;/p&gt;
&lt;pre id=&quot;code_1778132857437&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;blue&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일은 굉장히 단순하지만 Blue/Green 배포에서 핵심 역할을 한다.&lt;/p&gt;
&lt;p data-end=&quot;1763&quot; data-start=&quot;1744&quot; data-ke-size=&quot;size16&quot;&gt;배포 스크립트는 이 값을 기준으로:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1816&quot; data-start=&quot;1765&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1790&quot; data-start=&quot;1765&quot; data-section-id=&quot;1n4euy7&quot;&gt;현재 blue 운영 중 &amp;rarr; green 배포&lt;/li&gt;
&lt;li data-end=&quot;1816&quot; data-start=&quot;1791&quot; data-section-id=&quot;1d16van&quot;&gt;현재 green 운영 중 &amp;rarr; blue 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1825&quot; data-start=&quot;1818&quot; data-ke-size=&quot;size16&quot;&gt;를 결정한다.&lt;/p&gt;
&lt;p data-end=&quot;1825&quot; data-start=&quot;1818&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-end=&quot;1825&quot; data-start=&quot;1818&quot; data-ke-size=&quot;size23&quot;&gt;docker-compose.yml 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 실제 컨테이너를 정의하는 파일이다.&lt;/p&gt;
&lt;pre id=&quot;code_1778132961589&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  app-blue:
    image: {ECR_URL}/geonbbang/sample-app:latest
    container_name: app-blue
    command: node dist/main
    ports:
      - &quot;3000:3000&quot;
    env_file:
      - .env
    restart: always

  app-green:
    image: {ECR_URL}/geonbbang/sample-app:latest
    container_name: app-green
    command: node dist/main
    ports:
      - &quot;3001:3000&quot;
    env_file:
      - .env
    restart: always&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 컨테이너를 2개 사용하는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blue/Green 배포의 핵심은 &lt;b&gt;&lt;u&gt;현재 운영 중인 서버를 유지한 상태로 새 버전을 띄우는 것&lt;br /&gt;&lt;/u&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 현재 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;app-blue&lt;/span&gt; 가 운영 중이라면 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;app-green&lt;/span&gt; 에 새 버전을 먼저 배포한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 서버가 정상이라면 nginx 트래픽을 전환하고, 이후 기존 서버를 종료한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구성함으로써&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 수동 배포 제거&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 무중단 배포&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 자동 롤백 기반 확보&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 운영 안정성 향상&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의 효과를 챙길 수 있게 되었다.&lt;/p&gt;</description>
      <category>DevOps</category>
      <author>MIRACLE LIFE</author>
      <guid isPermaLink="true">https://geonbbang.tistory.com/75</guid>
      <comments>https://geonbbang.tistory.com/75#entry75comment</comments>
      <pubDate>Mon, 11 May 2026 19:00:41 +0900</pubDate>
    </item>
    <item>
      <title>Docker를 이용하여 EC2에 Node 서버 배포하기 (1)</title>
      <link>https://geonbbang.tistory.com/74</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Node 서버를 운영하다 보면 다음과 같은 문제들이 생길 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;282&quot; data-start=&quot;201&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;228&quot; data-start=&quot;201&quot; data-section-id=&quot;yzfb2q&quot;&gt;배포를 했더니 개발 환경과 운영 환경이 같지 않아서 문제가 발생한다.&lt;/li&gt;
&lt;li data-end=&quot;253&quot; data-start=&quot;229&quot; data-section-id=&quot;1h9a3jb&quot;&gt;다른 개발자와의 개발 환경이 달라 충돌이 발생한다.&lt;/li&gt;
&lt;li data-end=&quot;282&quot; data-start=&quot;254&quot; data-section-id=&quot;13a25tj&quot;&gt;가끔 패키지 관련 문제로 인해 서버 실행이 안 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;390&quot; data-start=&quot;284&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서는 이런 문제를 해결하기 위해&lt;br /&gt;&lt;b&gt;Docker &amp;rarr; ECR &amp;rarr; EC2&lt;/b&gt;&amp;nbsp;흐름으로&lt;br /&gt;Node 서버를 배포하는 과정을 진행한다.&lt;/p&gt;
&lt;p data-end=&quot;390&quot; data-start=&quot;284&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;390&quot; data-start=&quot;284&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;390&quot; data-start=&quot;284&quot; data-ke-size=&quot;size16&quot;&gt;전체 흐름은 아래와 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;390&quot; data-start=&quot;284&quot;&gt;Docker 이미지 빌드&lt;/li&gt;
&lt;li data-end=&quot;390&quot; data-start=&quot;284&quot;&gt;AWS ECR에 이미지 Push&lt;/li&gt;
&lt;li data-end=&quot;390&quot; data-start=&quot;284&quot;&gt;EC2에서 이미지 Pull&lt;/li&gt;
&lt;li data-end=&quot;390&quot; data-start=&quot;284&quot;&gt;Docker로 컨테이너 실행&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker 이미지 빌드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 이미지를 빌드하기 위해서는 Dockerfile이란 이름을 가진 파일이 필요하다. 아래와 같은 Dockerfile을 작성한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile&lt;/p&gt;
&lt;pre id=&quot;code_1778030482220&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM node:24-alpine

WORKDIR /app

# 의존성 먼저 복사 (캐시 활용)
# 소스 코드가 바뀌어도 package.json이 그대로면
# 해당 레이어는 캐시를 재사용할 수 있음
COPY package*.json ./

RUN npm install

# 컨테이너가 사용할 포트
EXPOSE 3000

# 컨테이너 시작 시 실행할 기본 명렁
CMD [&quot;node&quot;, &quot;dist/main&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성한 Dockerfile을 빌드한다.&lt;/p&gt;
&lt;pre id=&quot;code_1778031948190&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker build -t sample .&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드가 완료되면 이미지가 정상적으로 생성되었는지 확인한다.&lt;/p&gt;
&lt;pre id=&quot;code_1778032231640&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker images&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목록에 sample이 보이면 이미지가 정상적으로 생성된 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지를 만들었으면, 실제로 서버가 잘 뜨는지 확인해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 실행&lt;/p&gt;
&lt;pre id=&quot;code_1778033560923&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run -d -p 3000:3000 --name sample-app sample&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;-d: 백그라운드 실행&lt;/li&gt;
&lt;li&gt;-p 3000:3000: 로컬 3000 포트를 컨테이너 3000 포트와 연결&lt;/li&gt;
&lt;li&gt;--name sample-app: 컨테이너 이름 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 확인&lt;/p&gt;
&lt;pre id=&quot;code_1778034748942&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker ps&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AWS ECR에 이미지 Push&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이미지를 서버에 전달해야 한다. 그 역할을 하는게 ECR이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECR(Elastic Container Registry)이란?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Docker Hub 같은 Docker 이미지 저장소&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 AWS console의 ECR에서 리포지토리를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 리포지토리에 이미지를 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인&lt;/p&gt;
&lt;pre id=&quot;code_1778040968303&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;aws ecr get-login-password --region ap-northeast-2 \
| docker login \
--username AWS \
--password-stdin {ECR_URI}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 태깅&lt;/p&gt;
&lt;pre id=&quot;code_1778041009129&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker tag sample:latest {ECR_URI}:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 푸시&lt;/p&gt;
&lt;pre id=&quot;code_1778041060433&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker push {ECR_URI}:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECR 리포지토리의 이미지 목록에 나타난다면 잘 된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EC2에서 이미지 Pull&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 ECR에 있는 이미지를 가져올 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인&lt;/p&gt;
&lt;pre id=&quot;code_1778041445676&quot; class=&quot;dsconfig&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;aws ecr get-login-password --region ap-northeast-2 \
| docker login \
--username AWS \
--password-stdin {ECR_URI}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이미지 다운로드&lt;/p&gt;
&lt;pre id=&quot;code_1778041471209&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker pull {ECR_URI}:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지가 정상적으로 내려받아졌는지 확인해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1778041853361&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker images&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker로 컨테이너 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 실행&lt;/p&gt;
&lt;pre id=&quot;code_1778042222368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run -d -p 3000:3000 --name sample-app {ECR_URI}:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 확인&lt;/p&gt;
&lt;pre id=&quot;code_1778042248413&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker ps&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 목록에 sample-app이 보이면 정상이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안 그룹에서 3000 포트를 열었다면 브라우저에서 아래 주소로 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1778042302189&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http://EC2_PUBLIC_IP:3000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;&lt;b&gt;서버를 Docker 이미지로 만들고,&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; AWS ECR에 올리고,&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; EC2에서 이미지를 가져와 실행했다.&lt;/b&gt;&quot;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 GitHub Actions를 사용하여 자동 배포가 이루어질 수 있도록 할 것이다.&lt;/p&gt;</description>
      <category>DevOps</category>
      <category>docker</category>
      <category>nest</category>
      <category>node</category>
      <author>MIRACLE LIFE</author>
      <guid isPermaLink="true">https://geonbbang.tistory.com/74</guid>
      <comments>https://geonbbang.tistory.com/74#entry74comment</comments>
      <pubDate>Tue, 28 Apr 2026 17:01:24 +0900</pubDate>
    </item>
    <item>
      <title>NestJS Custom Decorator로 유저 인증 처리하기</title>
      <link>https://geonbbang.tistory.com/72</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Nest는 TypeScript의 데코레이터를 기반으로 한 Custom 데코레이터를 더 쉽게 사용할 수 있도록 기능을 제공한다.&lt;/p&gt;
&lt;p data-end=&quot;141&quot; data-start=&quot;72&quot; data-ke-size=&quot;size16&quot;&gt;이를 활용하면 비즈니스 로직과 관련 없는 부분을 숨기고, 공통 처리를 통해 중복 코드를 줄이며 재사용성을 높일 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;141&quot; data-start=&quot;72&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;208&quot; data-start=&quot;143&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@User()&lt;/span&gt; Custom 데코레이터를 생성하여, 필요한 컨트롤러에서 유저 정보를 간편하게 가져올 수 있도록 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, Custom decorator에 대한 이해를 돕기 위한 간단한 예제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제는 Nest 공식 문서에서 확인할 수 있다.&lt;br /&gt;&lt;a href=&quot;https://docs.nestjs.com/custom-decorators&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.nestjs.com/custom-decorators&lt;/a&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1740059061061&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Get()
getProfile(@Req() req) {
	const user = req.user;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 사용자 정보를 필요로 하는 각 컨트롤러에서 매번 요청 객체(request)에서 데이터를 수동으로 추출해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740059283462&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) =&amp;gt; {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740059506858&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Get()
getProfile(@User() user) {
	console.log(user);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;createParamDecorator&lt;/span&gt;를 이용해 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@User()&lt;/span&gt; Custom 데코레이터를 만들어서 간편하게 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 요청 객체(Request)에 user 데이터를 첨부하는 방법과 함께, 유저 인증 과정도 다룰 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 '&lt;b&gt;이미 검증된 온전한 User 데이터&lt;/b&gt;'가 필요한 곳에서 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@User()&lt;/span&gt; 데코레이터만 사용하면 되도록 만드는 것이 목표다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제에서는 인증을 위해서 JWT 토큰을 사용한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;방법1. 가드(Guard)와 함께 사용&lt;/p&gt;
&lt;pre id=&quot;code_1740061643032&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';
import { JwtService } from '@nestjs/jwt';

import { UserService } from '../user/user.service';

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

  async canActivate(context: ExecutionContext): Promise&amp;lt;boolean&amp;gt; {
    const request: Request = context.switchToHttp().getRequest();
    const authHeader = request.headers.authorization;
    if (!authHeader) return false;

    const token = authHeader.split(' ')[1];
    if (!token) return false;

    try {
      const decoded = this.jwtService.verify(token, { secret: process.env.JWT_SECRET });
      const user = await this.userService.findOne(decoded.userId);
      if (!user) return false;

      request.user = user; // 유저 정보를 요청 객체에 저장
      return true;
    } catch (error) {
      return false;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740061774434&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@UseGuards(JwtAuthGuard) // 가드 적용
@Get()
getProfile(@User() user) {
	console.log(user);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법에는 한 가지 문제점이 있다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;135&quot; data-start=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@User()&lt;/span&gt; 데코레이터를 기대한 대로 사용하려면 반드시 앞서 정의한 가드와 함께 적용해야 한다. 그러나 이 과정에서 가드 적용 코드를 빠트리는 실수(휴먼 에러) 가 발생할 가능성이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;방법2. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@USER()&lt;/span&gt; 데코레이터 단독 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 Custom 데코레이터를 생성하는 과정에서 유저 정보를 함께 저장하는 방식이다.&lt;/p&gt;
&lt;p data-end=&quot;154&quot; data-start=&quot;55&quot; data-ke-size=&quot;size16&quot;&gt;하지만 여기에는 한 가지 문제가 있다. 데코레이터 내부에서는 Provider에 직접 접근할 수 없으므로, 다음과 같은 일반적인 코드로는 유저 데이터를 가져올 수 없다.&lt;/p&gt;
&lt;pre id=&quot;code_1740226941263&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const user = await this.userService.findOne(userId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때, 사용할 수 있는 방법으로는 &lt;b&gt;Provider를 글로벌 변수로 저장&lt;/b&gt;하고 필요한 곳에서 사용하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 글로벌 변수 ProviderRegistry 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1740195514309&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ProviderRegistry {
  private static providers = new Map&amp;lt;string, any&amp;gt;();

  static set(name: string, provider: any) {
    this.providers.set(name, provider);
  }

  static get(name: string) {
    return this.providers.get(name);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;set()으로 Provider를 저장하고 get()으로 가져올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Provider를 글로벌 변수에 저장&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1740195840983&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Module, OnModuleInit } from '@nestjs/common';
import { UserService } from './user.service';

export class AppModule implements OnModuleInit {
  constructor(private readonly userService: UserService) {}

  onModuleInit() {
    ProviderRegistry.set('UserService', this.userService);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OnModuleInit() 에서 사용할 Provider를 ProviderRegistry에 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 데코리에터에서 Provider 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1740196224583&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  async (data: unknown, ctx: ExecutionContext) =&amp;gt; {
    ... 
    const userService: UserService = ProviderRegistry.get('UserService');
    const user = await userService.findOne(decoded.userId);
    return user;
  },
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740196335839&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Get()
getProfile(@User() user) {
	console.log(user);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 원하는 대로 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@User()&lt;/span&gt; 데코레이터 하나만 사용하여 유저 정보를 가져올 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 이 방식에는 Nest가 ProviderRegistry를 추적하지 못한다는 점이 있지만, 이 점이 큰 문제는 아닐 수 있다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;252&quot; data-start=&quot;146&quot; data-ke-size=&quot;size16&quot;&gt;물론, 프로바이더를 글로벌 변수로 등록하는 추가 작업이 필요하지만, 인증된 유저 데이터가 필요할 때 &lt;span style=&quot;background-color: #dddddd; color: #333333; text-align: start;&quot;&gt;@User()&lt;/span&gt; &lt;b&gt;데코레이터 하나만 사용하면 된다는 점에서 괜찮은 방법&lt;/b&gt;이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마무리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nest에서 제공하는 createParamDecorator 뿐만 아니라, Reflector.createDecorator를 사용하여 원하는 데코레이터를 만들 수 있다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;165&quot; data-start=&quot;100&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 &lt;b&gt;중복 코드를 줄이고, 공통 관심사를 처리&lt;/b&gt;하여 코드를 &lt;b&gt;모듈 단위로 효율적으로 관리&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740198216152&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Documentation | NestJS - A progressive Node.js framework&quot; data-og-description=&quot;Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea&quot; data-og-host=&quot;docs.nestjs.com&quot; data-og-source-url=&quot;https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata&quot; data-og-url=&quot;https://docs.nestjs.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/QIGU7/hyYfXENqBB/0ZkTDKpoCLLaNXk4xKaqi0/img.png?width=820&amp;amp;height=429&amp;amp;face=0_0_820_429&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/QIGU7/hyYfXENqBB/0ZkTDKpoCLLaNXk4xKaqi0/img.png?width=820&amp;amp;height=429&amp;amp;face=0_0_820_429');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Documentation | NestJS - A progressive Node.js framework&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.nestjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.typescriptlang.org/ko/docs/handbook/decorators.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.typescriptlang.org/ko/docs/handbook/decorators.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740198246071&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Documentation - Decorators&quot; data-og-description=&quot;TypeScript Decorators overview&quot; data-og-host=&quot;www.typescriptlang.org&quot; data-og-source-url=&quot;https://www.typescriptlang.org/ko/docs/handbook/decorators.html&quot; data-og-url=&quot;https://www.typescriptlang.org/ko/docs/handbook/decorators.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.typescriptlang.org/ko/docs/handbook/decorators.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.typescriptlang.org/ko/docs/handbook/decorators.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Documentation - Decorators&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;TypeScript Decorators overview&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.typescriptlang.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>JavaScript</category>
      <category>Custom</category>
      <category>Decorator</category>
      <category>js</category>
      <category>nestjs</category>
      <category>TS</category>
      <author>MIRACLE LIFE</author>
      <guid isPermaLink="true">https://geonbbang.tistory.com/72</guid>
      <comments>https://geonbbang.tistory.com/72#entry72comment</comments>
      <pubDate>Wed, 19 Feb 2025 22:26:17 +0900</pubDate>
    </item>
    <item>
      <title>CloudWatch Logs와 Slack 통합: 에러 메시지 실시간 전송 설정</title>
      <link>https://geonbbang.tistory.com/71</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;알람이 자주 울리면 처음에는 신경을 쓰다가도 점점 무뎌지기 마련이다. 중요하지 않은 알람이나 즉시 처리할 필요가 없는 알람이 계속 오면, 어느 순간 '또 알람이네' 하고 무심코 넘겨버리게 된다. 문제는 이 과정에서 정말 중요한 알람까지 놓칠 수 있다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;303&quot; data-start=&quot;170&quot; data-ke-size=&quot;size16&quot;&gt;이런 거짓 알람을 원천적으로 차단하는 것이 가장 좋은 해결책이지만, 현실적으로 모든 알람을 완벽하게 정리하기는 어렵다. 따라서, 알람이 왔을 때 내용을 바로 파악할 수 있도록 구성하는 것이 도움이 될 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;303&quot; data-start=&quot;170&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;451&quot; data-start=&quot;305&quot; data-ke-size=&quot;size16&quot;&gt;이를 위해 &lt;b&gt;CloudWatch 구독 필터&lt;/b&gt;를 설정하고 &lt;b&gt;Lambda&lt;/b&gt;를 활용해 &lt;b&gt;Slack으로 오류 메시지를 전송&lt;/b&gt;하는 방식을 적용할 수 있다. 이를 통해 오류를 신속하게 파악하고, 즉각 대응이 필요한 크리티컬한 문제인지 빠르게 판단할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;사전 준비 사항.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;CloudWatch Logs 활성화&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;1. Slack Webhook API 생성&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Slack API를 활용해 원하는 채널로 메시지를 보낼 수 있도록 설정할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;140&quot; data-start=&quot;50&quot; data-ke-size=&quot;size16&quot;&gt;먼저, 채널 설정에서 &lt;b&gt;Integrations&lt;/b&gt; 탭으로 이동한 후, &lt;b&gt;Add an App&lt;/b&gt;을 선택해 &lt;b&gt;Incoming WebHooks&lt;/b&gt;를 추가한다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;216&quot; data-start=&quot;142&quot; data-ke-size=&quot;size16&quot;&gt;이 과정에서 메시지를 전송할 수 있는 &lt;b&gt;URL&lt;/b&gt;이 제공되며, 이를 이용해 Slack 채널로 메시지를 보낼 수 있다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;참고. &lt;span data-prosemirror-node-inline=&quot;true&quot; data-prosemirror-node-name=&quot;inlineCard&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://jojoldu.tistory.com/552&quot;&gt;https://jojoldu.tistory.com/552&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;2. Lambda 생성&lt;/h2&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;2-1. 함수 생성&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;트리거로 CloudWatch Logs를 가진 &lt;b&gt;Lambda&amp;nbsp;함수를 생성&lt;/b&gt;한다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot;&gt;
&lt;div data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;c8e9b874-c08b-41f6-9595-0653388c1fdc.png&quot; data-origin-width=&quot;1653&quot; data-origin-height=&quot;285&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0Klvt/btsMeMB2TUb/7MCI1PGS7FlbKOc4kD92MK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0Klvt/btsMeMB2TUb/7MCI1PGS7FlbKOc4kD92MK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0Klvt/btsMeMB2TUb/7MCI1PGS7FlbKOc4kD92MK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0Klvt%2FbtsMeMB2TUb%2F7MCI1PGS7FlbKOc4kD92MK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1653&quot; height=&quot;285&quot; data-filename=&quot;c8e9b874-c08b-41f6-9595-0653388c1fdc.png&quot; data-origin-width=&quot;1653&quot; data-origin-height=&quot;285&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot; data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;Process CloudWatch log data&lt;/span&gt; 블루프린트 선택 시 아래 사진과 같이 트리거를 설정하는 화면이 보인다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot;&gt;
&lt;div data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;8ada26cf-b509-477f-81d3-17d614f5f2f9.png&quot; data-origin-width=&quot;1669&quot; data-origin-height=&quot;505&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxykkX/btsMgI5Gr0L/lmdlcBJa7i71Jd0SXyLpV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxykkX/btsMgI5Gr0L/lmdlcBJa7i71Jd0SXyLpV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxykkX/btsMgI5Gr0L/lmdlcBJa7i71Jd0SXyLpV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxykkX%2FbtsMgI5Gr0L%2FlmdlcBJa7i71Jd0SXyLpV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1669&quot; height=&quot;505&quot; data-filename=&quot;8ada26cf-b509-477f-81d3-17d614f5f2f9.png&quot; data-origin-width=&quot;1669&quot; data-origin-height=&quot;505&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;로그 그룹&lt;/b&gt;:&lt;br /&gt;구독 필터를 적용할 로그 그룹을 선택한다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;필터 이름&lt;/b&gt;:&lt;br /&gt;필터의 역할을 잘 파악할 수 있도록 적절한 이름을 입력한다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;필터 패턴&lt;/b&gt;:&lt;br /&gt;필터의 패턴을 지정한다. 패턴과 매칭되는 로그가 발생하는 경우, 해당 람다가 트리거 된다.&lt;br /&gt;ex. &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;{ $.statusCode = %5[0-9]{2}% }&lt;/span&gt; &amp;lt;- 500 에러를 감지한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;2-2. 함수 구현&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;로그 메세지를 포맷팅하여 Slack으로 전송한다.&lt;/p&gt;
&lt;pre id=&quot;code_1739928843018&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import * as zlib from &quot;node:zlib&quot;;
import * as https from &quot;node:https&quot;;

export const handler = async event =&amp;gt; {
  try {
    const message = extractLogMessage(event);
    const slackUrl = &quot;slack url&quot;;
    await sendToSlack(message, slackUrl);
    return `Successfully processed log event.`;
  } catch (error) {
    console.error(&quot;Error processing event:&quot;, error);
    throw error;
  }
};

function extractLogMessage(event) {
  const payload = Buffer.from(event.awslogs.data, &quot;base64&quot;);
  const parsed = JSON.parse(zlib.gunzipSync(payload).toString(&quot;utf8&quot;));
  return parsed.logEvents?.[0]?.message || &quot;No message found&quot;;
}

async function sendToSlack(message, slackUrl) {
  const { host, pathname } = new URL(slackUrl);
  const options = {
    hostname: host,
    path: pathname,
    method: &quot;POST&quot;,
    headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
  };

  return new Promise((resolve, reject) =&amp;gt; {
    const req = https.request(options, res =&amp;gt; {
      let responseBody = &quot;&quot;;
      res.on(&quot;data&quot;, chunk =&amp;gt; (responseBody += chunk));
      res.on(&quot;end&quot;, () =&amp;gt; resolve(responseBody));
    });

    req.on(&quot;error&quot;, reject);
    req.write(JSON.stringify({ text: message }));
    req.end();
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;여기까지 완료했으면 등록한 필터 패턴과 일치하는 로그가 발생하는 경우 지정한 Slack 채널으로 에러 내용을 포함한 메세지를 받아볼 수 있다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고.&lt;/b&gt; 등록한 Lambda 구독 필터는 CloudWatch 로그 그룹의 &lt;b&gt;구독 필터&lt;/b&gt; 탭에서 조회/관리 할 수 있다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;유용한 도구.&lt;br /&gt;&lt;a href=&quot;https://app.slack.com/block-kit-builder&quot;&gt;https://app.slack.com/block-kit-builder&lt;/a&gt;&lt;br /&gt;Slack 메시지를 커스터마이즈할 때 사용할 수 있다.&lt;/p&gt;</description>
      <category>DevOps</category>
      <category>cloudwatch</category>
      <category>Lambda</category>
      <category>Logs</category>
      <category>Slack</category>
      <category>모니터링</category>
      <category>알람</category>
      <author>MIRACLE LIFE</author>
      <guid isPermaLink="true">https://geonbbang.tistory.com/71</guid>
      <comments>https://geonbbang.tistory.com/71#entry71comment</comments>
      <pubDate>Wed, 25 Dec 2024 15:59:52 +0900</pubDate>
    </item>
    <item>
      <title>TypeORM의 save() 사용 시 데이터 변경 감지 원리 이해하기</title>
      <link>https://geonbbang.tistory.com/70</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;TypeORM의 Repository API를 사용하여 데이터를 변경하는 방법에는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;update()&lt;/span&gt;와 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;save()&lt;/span&gt;등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;update()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;update&amp;nbsp;-&amp;nbsp;Partially&amp;nbsp;updates&amp;nbsp;entity&amp;nbsp;by&amp;nbsp;a&amp;nbsp;given&amp;nbsp;update&amp;nbsp;options&amp;nbsp;or&amp;nbsp;entity&amp;nbsp;id.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; style=&quot;background-color: #282c34; color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;await repository.update({ age: 18 }, { category: &quot;ADULT&quot; })
// executes UPDATE user SET category = ADULT WHERE age = 18

await repository.update(1, { firstName: &quot;Rizzrak&quot; })
// executes UPDATE user SET firstName = Rizzrak WHERE id = 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;save()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;save - Saves a given entity or array of entities. If the entity already exist in the database, it is updated. If the entity does not exist in the database, it is inserted. It saves all given entities in a single transaction (in the case of entity, manager is not transactional). Also supports partial updating since all undefined properties are skipped. Returns the saved entity/entities.&lt;/p&gt;
&lt;pre class=&quot;css&quot; style=&quot;background-color: #282c34; color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;await repository.save(user)
await repository.save([category1, category2, category3])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://typeorm.io/repository-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://typeorm.io/repository-api&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;save()&lt;/span&gt;를 사용하여 연관 관계가 있는 외래 키 컬럼의 데이터를 변경할 때 주의해야 할 점&lt;/b&gt;이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제를 통해 더 자세히 알아보자.&lt;/p&gt;
&lt;pre id=&quot;code_1740463530192&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity()
export class User {
	@PrimaryGeneratedColumn()
	id: number

	@Column()
	name: string
    
	@Column({ name: &quot;team_id&quot; })
	teamId: string;

	@ManyToOne(() =&amp;gt; Team, (team) =&amp;gt; team.users)
	@JoinColumn({ name: &quot;team_id&quot; })
	team: Team; // 유저는 하나의 팀에만 속할 수 있다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저 데이터를 다룰 때, 팀의 전체 정보가 아닌 팀 ID만 필요할 때가 있다. 이러한 경우, 위 엔티티 코드와 같이 &lt;b&gt;team&lt;/b&gt;과는 별도로 &lt;b&gt;teamId&lt;/b&gt;를 추가로 정의하여 사용하곤 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황에서, 유저의 속성을 변경하기 위해 다음과 같은 코드를 작성할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1740464561889&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;user.name = req.newName; // 이름 변경
user.teamId = req.newTeamId; // 팀 변경

await repository.save(user);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 코드를 실행해보면 &lt;b&gt;name&lt;/b&gt;은 데이터베이스에 정상적으로 반영되지만, &lt;b&gt;teamId는 변경되지 않은 것을 확인&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드만 봐서는 &lt;b&gt;name&lt;/b&gt;이 변경된 것처럼&amp;nbsp;&lt;b&gt;teamId&lt;/b&gt;도 변경되어야 할 것 같은데 그렇지 않은 이유가 뭘까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내부 라이브러리 코드 파악&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;save()&lt;/span&gt;의 코드 내부 동작을 더 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1740465806170&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;save(entityOrEntities, options) {
	return this.manager.save(this.metadata.target, entityOrEntities, options);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;compute()&lt;/h4&gt;
&lt;pre id=&quot;code_1740466027330&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class SubjectChangedColumnsComputer {
	// -------------------------------------------------------------------------
	// Public Methods
	// -------------------------------------------------------------------------
	/**
	* Finds what columns are changed in the subject entities.
	*/
	compute(subjects) {
		subjects.forEach((subject) =&amp;gt; {
		this.computeDiffColumns(subject);
		this.computeDiffRelationalColumns(subjects, subject);
	});
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;save()&lt;/span&gt; 호출 스택을 계속 따라가다 보면, 위 코드에서 보이는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;compute&lt;/span&gt; 메서드가 실행되는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드는 설명에서 알 수 있듯이 &lt;b&gt;변경된 컬럼을 찾는 역할&lt;/b&gt;을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;compute()&lt;/span&gt;가 호출하는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;computeDiffColumns()&lt;/span&gt;와 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;computeDiffRelationalColumns()&lt;/span&gt;의 코드를 살펴 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1740466593299&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;subject.diffColumns.push(column); // computeDiffColumns() 내부

subject.diffRelations.push(relation); // computeDiffRelationalColumns() 내부&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 보면, 변경된 컬럼이 있는 경우 &lt;b&gt;diffColumns&lt;/b&gt;와 &lt;b&gt;diffRelations&lt;/b&gt;에 저장되고, 이후 업데이트가 처리된다는 것을 알 수 있다. 이제 &lt;b&gt;변경 여부가 어떻게 결정되는지에 대한 기준&lt;/b&gt;만 알게 되면 궁금증이 풀릴 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;subject 객체&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;computeDiffColumns()&lt;/span&gt;와 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;computeDiffRelationalColumns()&lt;/span&gt;에서 사용하는 &lt;b&gt;subject&lt;/b&gt;에 대해 알아보자.&lt;/p&gt;
&lt;p data-end=&quot;189&quot; data-start=&quot;89&quot; data-ke-size=&quot;size16&quot;&gt;두 메서드 모두 &lt;b&gt;subject&lt;/b&gt; 내부의 &lt;b&gt;databaseEntity&lt;/b&gt;와 &lt;b&gt;entity&lt;/b&gt;를 비교하여 다를 경우, &lt;b&gt;diffColumns&lt;/b&gt;와 &lt;b&gt;diffRelations&lt;/b&gt;에 추가한다.&lt;/p&gt;
&lt;p data-end=&quot;203&quot; data-start=&quot;191&quot; data-ke-size=&quot;size16&quot;&gt;각 값은 다음과 같다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;databaseEntity&lt;/b&gt;: 데이터베이스에서 현재 저장된 실제 엔티티의 상태&lt;/li&gt;
&lt;li&gt;&lt;b&gt;entity&lt;/b&gt;: 변경된 엔티티, 즉 사용자 코드에서 수정된 엔티티의 상태 (&lt;span style=&quot;background-color: #dddddd;&quot;&gt;await repository.save(user)&lt;/span&gt;의 &lt;b&gt;user&lt;/b&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 &lt;b&gt;entity&lt;/b&gt;의 상태는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1740467482153&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
	...
	name: &quot;변경한 이름&quot;;
	team_id: &quot;변경한 teamId&quot;;
	team: { id: &quot;기존 teamId&quot; };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;computeDiffColumns()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;computeDiffColumns()&lt;/span&gt;는 컬럼들을 순회하면서 &lt;b&gt;subject&lt;/b&gt;의 &lt;b&gt;databaseEntity&lt;/b&gt;와 &lt;b&gt;entity&lt;/b&gt; 간 값이 다른 컬럼을 찾는다. 이 과정에서, 반복문 내의 다음 코드를 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1740470645601&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (column.relationMetadata) {
	const value = column.relationMetadata.getEntityValue(subject.entity);
	if (value !== null &amp;amp;&amp;amp; value !== undefined)
		return;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 코드는 &lt;b&gt;column.relationMetadata&lt;/b&gt;에 값이 존재하면 코드 실행을 중단하고, 다음 컬럼의 비교로 넘어간다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;즉, &lt;b&gt;team_id&lt;/b&gt; 컬럼의 경우 &lt;b&gt;databaseEntity&lt;/b&gt;와 &lt;b&gt;entity&lt;/b&gt;의 값이 다르더라도 &lt;b&gt;relationMetadata&lt;/b&gt;가 존재하기 때문에 &lt;b&gt;diffColumns&lt;/b&gt;에 추가되&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;지 않는다. 반면, &lt;b&gt;name&lt;/b&gt;과 같은 일반 컬럼은 끝까지 코드가 실행되어 &lt;b&gt;diffColumns&lt;/b&gt;에 포함된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;(엔티티 코드에서 연관 관계가 설정된 경우, 메타데이터가 생성될 때 &lt;b&gt;relationMetadata&lt;/b&gt;에 값이 할당된다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;computeDiffRelationalColumns()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;computeDiffRelationalColumns()&lt;/span&gt;는 일반 필드가 아닌 &lt;b&gt;team&lt;/b&gt;과 같은 연관 관계 필드만 비교한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현재 &lt;b&gt;entity&lt;/b&gt;(User 객체)에서는 &lt;b&gt;team_id&lt;/b&gt;만 변경되었을 뿐, &lt;b&gt;team&lt;/b&gt; 객체는 변경되지 않았기 때문에 아무 작업도 수행하지 않으며, &lt;b&gt;diffRelations&lt;/b&gt;에도 추가되지 않는다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;247&quot; data-start=&quot;198&quot; data-ke-size=&quot;size16&quot;&gt;이러한 이유로 &lt;b&gt;team_id&lt;/b&gt;는 기대와 달리 데이터베이스에서 업데이트되지 않은 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 전체 코드를 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/typeorm/typeorm/blob/master/src/persistence/SubjectChangedColumnsComputer.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/typeorm/typeorm/blob/master/src/persistence/SubjectChangedColumnsComputer.ts&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;올바른 코드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 실제 DB에서 유저의 &lt;b&gt;team_id&lt;/b&gt;가 정상적으로 변경되도록 하려면 다음과 같이 코드를 작성해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1740470942253&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;user.team.id = req.newTeamId;
// 또는
user.team = newTeam;

await repository.save(user);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;마무리.&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TypeORM에서 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;save()&lt;/span&gt;를 사용할 때 주의해야 할 점과, 업데이트 시 변경 사항을 감지하는 원리에 대해 이해하게 되었다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>JavaScript</category>
      <category>Save</category>
      <category>typeorm</category>
      <category>Update</category>
      <author>MIRACLE LIFE</author>
      <guid isPermaLink="true">https://geonbbang.tistory.com/70</guid>
      <comments>https://geonbbang.tistory.com/70#entry70comment</comments>
      <pubDate>Sun, 15 Dec 2024 22:59:17 +0900</pubDate>
    </item>
    <item>
      <title>Nest 프로젝트에 Pinpoint 적용</title>
      <link>https://geonbbang.tistory.com/68</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;목록&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Java 설치&lt;/li&gt;
&lt;li&gt;HBase 설치&lt;/li&gt;
&lt;li&gt;Pinpoint Collector 설치&lt;/li&gt;
&lt;li&gt;Pinpoint Web 설치&lt;/li&gt;
&lt;li&gt;Pinpoint Node Agent 설치&lt;/li&gt;
&lt;li&gt;실행 및 Pinpoint Web 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;준비. EC2 설치&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1.Java 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 8 설치&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;sudo yum install java-1.8.0-openjdk -y&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 버전 확인&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;java -version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JAVA_HOME 경로 설정&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;echo 'export JAVA_HOME={Java 경로 입력}' &amp;gt;&amp;gt; ~/.bashrc
echo 'export PATH=$JAVA_HOME/bin:$PATH' &amp;gt;&amp;gt; ~/.bashrc 
source ~/.bashrc&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바 경로는 &lt;code&gt;sudo update-alternatives --config java&lt;/code&gt; 를 통해 확인할 수 있다. &lt;code&gt;bin/java&lt;/code&gt;를 제외한 나머지가 경로다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.Hbase 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hbase 압축 파일 다운&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;wget https://archive.apache.org/dist/hbase/1.2.7/hbase-1.2.7-bin.tar.gz&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;압축 해제&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;tar xzf hbase-1.2.7-bin.tar.gz&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디렉토리 링크 설정&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ln -s hbase-1.2.7 hbase&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hbase 실행&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;hbase/bin/start-hbase.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pinpoint 관련 테이블 생성 스크립트 다운&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;wget https:*//raw.githubusercontent.com/pinpoint-apm/pinpoint/master/hbase/scripts/hbase-create.hbase&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크립트 실행&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;hbase/bin/hbase shell ../hbase-create.hbase&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3.Pinpoint Collector 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jar파일 다운&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;wget https://github.com/pinpoint-apm/pinpoint/releases/download/v2.2.2/pinpoint-collector-boot-2.2.2.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 권한 부여&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;chmod +x pinpoint-collector-boot-2.2.2.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;nohup java -jar -Dpinpoint.zookeeper.address=localhost pinpoint-collector-boot-2.2.2.jar &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.Pinpoint Web 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jar파일 다운&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;wget https://github.com/pinpoint-apm/pinpoint/releases/download/v2.2.2/pinpoint-web-boot-2.2.2.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 권한 부여&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;chmod +x pinpoint-web-boot-2.2.2.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;nohup java -jar -Dpinpoint.zookeeper.address=localhost pinpoint-web-boot-2.2.2.jar &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5.pinpoint-node-agent 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;npm i pinpoint-node-agent&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경변수 설정&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;#PINPOINT_APM
PINPOINT_COLLECTOR_IP=&quot;ec2 ip 입력&quot; 
PINPOINT_SAMPLING_RATE=1
PINPOINT_APPLICATION_NAME=pinpoint-nest
PINPOINT_AGENT_ID=local&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 제일 처음에 import&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;import 'pinpoint-node-agent' &lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6.실행 및 Pinpoint Web 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 실행 후 브라우저에서 Pinpoint Web을 연다. 기본 포트는 8080이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qzzIp/btsMeuBFYcB/cWTb5uBkEmyivkE8yjrrWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qzzIp/btsMeuBFYcB/cWTb5uBkEmyivkE8yjrrWk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;370&quot; data-filename=&quot;Monosnap PINPOINT 2025-02-12 14-15-59.png&quot; style=&quot;width: 58.5742%; margin-right: 10px;&quot; data-widthpercent=&quot;59.26&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qzzIp/btsMeuBFYcB/cWTb5uBkEmyivkE8yjrrWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqzzIp%2FbtsMeuBFYcB%2FcWTb5uBkEmyivkE8yjrrWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;862&quot; height=&quot;370&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMX0dw/btsMgxJ1YNI/l0FRE73aRTwhhwyq3kah7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMX0dw/btsMgxJ1YNI/l0FRE73aRTwhhwyq3kah7K/img.png&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;281&quot; data-filename=&quot;스크린샷 2025-02-12 오후 2.12.30.png&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;40.74&quot; style=&quot;width: 40.2631%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMX0dw/btsMgxJ1YNI/l0FRE73aRTwhhwyq3kah7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMX0dw%2FbtsMgxJ1YNI%2Fl0FRE73aRTwhhwyq3kah7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;281&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실시간으로 API 요청을 쉽고 뚜렷하게 볼 수 있어 문제가 발생하는 경우 디버깅에 도움을 준다&lt;/li&gt;
&lt;li&gt;처리 속도가 느린 요청을 빠르게 파악할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 &lt;a href=&quot;https://jojoldu.tistory.com/573&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jojoldu.tistory.com/573&lt;/a&gt;&lt;/p&gt;</description>
      <category>DevOps</category>
      <category>apm</category>
      <category>nodejs</category>
      <category>PinPoint</category>
      <author>MIRACLE LIFE</author>
      <guid isPermaLink="true">https://geonbbang.tistory.com/68</guid>
      <comments>https://geonbbang.tistory.com/68#entry68comment</comments>
      <pubDate>Thu, 5 Dec 2024 22:57:21 +0900</pubDate>
    </item>
    <item>
      <title>비관적 락 vs 낙관적 락</title>
      <link>https://geonbbang.tistory.com/66</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;비행기 예약 서비스에서 두 사람이 동시에 같은 좌석을 예약한다거나, 온라인 쇼핑몰에서 수량이 1개 남은 상품을 두 사람이 동시에 결제하게 된다면 어떻게 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 우리가 사용하는 서비스에서 빈번하게 마주할 수 있는 문제를 '동시성 문제'라고 한다. 이러한 상황이 발생하지 않도록 사전에 처리가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시성 제어를 위한 두 가지 접근법으로 &lt;u&gt;비관적 락(Pessimistic Lock)&lt;/u&gt;과 &lt;u&gt;낙관적 락(Optimistic Lock)&lt;/u&gt;이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비관적 락(Pessimistic Lock)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락은 '충돌이 자주 발생할 것이다'라는 비관적인 가정에 기반한 제어 방식이다. 데이터에 잠금을 설정해서 다른 사용자가 같은 데이터에 접근할 수 없도록 차단하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 데이터를 읽거나 수정하기 전에 데이터에 락(Lock)을 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 락이 설정된 데이터는 다른 곳에서 접근할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 작업이 완료되면 락을 해제한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;낙관적 락(Optimistic Lock)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙관적 락은 '충돌이 자주 발생하지 않을 것이다'라는 낙관적인 가정에 기반한 제어 방식이다. 데이터에 잠금을 설정하지 않고, 데이터 변경 시점에 충돌 여부를 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 데이터를 읽을 때 현재 상태를 나타내는 version 값을 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 데이터를 수정할 때 저장 시점의 version과 데이터베이스의 version이 동일한지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 값이 일치하면 version을 증가시켜 업데이트 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 값이 다르면 작업을 중단하거나 재시도한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;결론&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙관적 락은 락을 설정하고 해제하는 과정이 없기 때문에 비관적 락 방식보다 자원의 소모가 적고 빠르게 실행된다. 하지만 충돌이 자주 발생하는 상황에서는 무의미한 실행과 재시도 로직 등으로 인하여 성능이 더 안좋을 수가 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;낙관적 락이 적합한 경우&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;읽기 작업이 많은 환경&lt;/b&gt;: 데이터 충돌이 드문 시스템&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분산 시스템&lt;/b&gt;: 데드락 방지가 중요한 경우&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자 데이터&lt;/b&gt;: 개인화된 데이터를 처리하며 충돌 가능성이 낮은 애플리케이션&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;비관적 락이 적합한 경우&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;충돌이 빈번한 환경&lt;/b&gt;: 동시에 데이터에 접근하는 트랜잭션이 많은 경우&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 무결성이 중요한 작업&lt;/b&gt;: 단일 작업 실패가 치명적인 영향을 미치는 경우&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실시간 처리&lt;/b&gt;: 즉시 결과가 필요한 작업으로 재시도 로직을 피해야 하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상황에 맞게 적절한 방법을 선택하는 것이 중요하다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>Database</category>
      <category>lock</category>
      <category>동시성문제</category>
      <category>재고관리</category>
      <author>MIRACLE LIFE</author>
      <guid isPermaLink="true">https://geonbbang.tistory.com/66</guid>
      <comments>https://geonbbang.tistory.com/66#entry66comment</comments>
      <pubDate>Tue, 19 Nov 2024 13:07:16 +0900</pubDate>
    </item>
    <item>
      <title>프로토타입</title>
      <link>https://geonbbang.tistory.com/65</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트는 프로토타입 기반 언어이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로토타입이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;자바스크립트에서는 모든 객체가 프로토타입을 가지고 있다. 프로토타입은 객체의 숨겨진 속성으로, 다른 객체로부터 상속받은 속성과 메서드를 포함하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;자바스크립트에서 생성자 함수를 new 연산자와 함께 호출하면 새로운 인스턴스가 생성된다. 이 때 인스턴스에는 __proto__라는 속성이 자동으로 부여되는데, 이 속성은 constructor의 prototype이라는 프로퍼티를 참조한다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717547792151&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function Person(name) {
    this.name = name;
}

Person.prototype.greet = function() {
    console.log(`Hello, my name is ${this.name}`);
};

const alice = new Person('Alice');
alice.greet(); // &quot;Hello, my name is Alice&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;위 코드에서 Person&lt;span style=&quot;text-align: start;&quot;&gt; 생성자 함수는 새로운 인스턴스를 생성한다. 생성된 &lt;b&gt;인스턴스 &lt;/b&gt;&lt;/span&gt;&lt;b&gt; alice&lt;span style=&quot;text-align: start;&quot;&gt;는 &lt;/span&gt;__proto__&lt;span style=&quot;text-align: start;&quot;&gt;라는 숨겨진 프로퍼티를 가지며, 이는 &lt;/span&gt;Person.prototype&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;b&gt;을 참조&lt;/b&gt;한다. &lt;/span&gt;Person.prototype&lt;span style=&quot;text-align: start;&quot;&gt;에는 &lt;/span&gt;greet&lt;span style=&quot;text-align: start;&quot;&gt; 메서드가 정의되어 있어, alice&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt; 인스턴스에서도 이 메서드를 사용할 수 있게 된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;프로토타입 체인&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 객체의 __proto__에는 Object.prototype이 연결된다. 이는 모든 객체가 기본적으로 Object 객체의 속성과 메서드를 상속받음을 의미한다. prototype 객체도 예외가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 데이터의 __proto__프로퍼티 내부에 다시 __proto__&amp;nbsp;프로퍼티가 연쇄적으로 이어진 것을 &lt;b&gt;프로토타입 체인&lt;/b&gt;이라고 하고, 이 체인을 따라가며 검색하는 것을 &lt;b&gt;프로토타입 체이닝&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1717549118069&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;console.log(alice.__proto__.__proto__ === Object.prototype); // true
console.log(alice.toString()); // &quot;[object Object]&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 &lt;span style=&quot;color: #333333;&quot;&gt; alice.__proto__.__proto__&lt;span style=&quot;text-align: start;&quot;&gt;는 &lt;/span&gt;Object.prototype&lt;span style=&quot;text-align: start;&quot;&gt;을 가리키고 있다. 따라서 alice&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;인스턴스는 &lt;/span&gt;Object&lt;span style=&quot;text-align: start;&quot;&gt; 객체의 메서드인 &lt;/span&gt;toString&lt;span style=&quot;text-align: start;&quot;&gt;을 사용할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 생성자 함수이든 prototype은 객체이기 때문에 Object.prototype이 언제나 프로토타입 체인의 최상단에 존재하게 된다. 때문에 &lt;b&gt;객체만을 대상으로 동작하는 객체 전용 메서드들은 부득이 스태틱 메서드로 부여할 수 밖에 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다중 프로토타입 체인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;다중 프로토타입 체인을 구현하려면 생성자 함수의 &lt;/span&gt;prototype&lt;span style=&quot;text-align: start;&quot;&gt;을 연결하고자 하는 생성자 함수의 인스턴스로 설정하면 된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717549554152&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function Developer(name, skill) {
    Person.call(this, name);
    this.skill = skill;
}

Developer.prototype = Object.create(Person.prototype);
Developer.prototype.constructor = Developer;

Developer.prototype.code = function() {
    console.log(`${this.name} is coding in ${this.skill}`);
};

const develpoer = new Developer('Alice', 'JavaScript');
develpoer.greet(); // &quot;Hello, my name is Alice&quot;
develpoer.code(); // &quot;Alice is coding in JavaScript&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;위 예제에서 &lt;/span&gt;Developer&lt;span style=&quot;text-align: start;&quot;&gt; 생성자 함수는 &lt;/span&gt;Person&lt;span style=&quot;text-align: start;&quot;&gt; 생성자 함수를 상속받아 다중 프로토타입 체인을 형성한다. &lt;/span&gt;Developer.prototype&lt;span style=&quot;text-align: start;&quot;&gt;을 &lt;/span&gt;Person.prototype&lt;span style=&quot;text-align: start;&quot;&gt;의 인스턴스로 설정함으로써 &lt;/span&gt;Developer&lt;span style=&quot;text-align: start;&quot;&gt; 인스턴스는 &lt;/span&gt;Person&lt;span style=&quot;text-align: start;&quot;&gt;의 메서드도 사용할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>JavaScript</category>
      <author>MIRACLE LIFE</author>
      <guid isPermaLink="true">https://geonbbang.tistory.com/65</guid>
      <comments>https://geonbbang.tistory.com/65#entry65comment</comments>
      <pubDate>Wed, 22 May 2024 21:02:38 +0900</pubDate>
    </item>
    <item>
      <title>this / 콜백 함수 / 클로저</title>
      <link>https://geonbbang.tistory.com/64</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;this&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에서 상황별로 this가 어떻게 달라지는지 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원칙 - &lt;b&gt;this는 함수를 호출할 때(실행 컨텍스트가 생성될 때) 결정된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 전역 공간에서의 this&lt;br /&gt;전역 공간에서 this는 전역 객체를 가리킨다. 브라우저 환경에서는 &lt;b&gt;window&lt;/b&gt;, Node 환경에서는 &lt;b&gt;global&lt;/b&gt;이 전역 객체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 메서드로서 호출할 때 그 메서드 내부에서의 this&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 함수를 호출할 때 그 &lt;b&gt;함수 이름 앞에 객체가 명시돼 있는 경우에는 메서드로 호출한 것&lt;/b&gt;이고, 그렇지 않은 모든 경우는 함수로 호출한 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출한 주체에 대한 정보가 this에 담긴다. 메서드로서 호출하는 경우 this는 함수명 앞의 객체이다.&lt;/p&gt;
&lt;pre id=&quot;code_1717378273751&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const obj = {
    name: 'Alice',
    greet: function() {
        console.log(this);
    }
};

obj.greet(); // obj&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 함수로서 호출할 때 그 함수 내부에서의 this&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수로서 호출할 경우에는 호출한 주체가 없다. 그래서 this는 전역 객체를 가리킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;this 바인딩에 관해서는 오직 해당 함수를 호출하는 구문 앞에 점이 있는지 없는지에 달려있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정이 빠지게 되어, 상위 스코프의 this를 그대로 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1717378405828&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function greet() {
    console.log(this);
}

greet(); // Window {...}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 콜백 함수 호출 시 그 함수 내부에서의 this&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백 함수에서의 this는 콜백 함수의 제어권을 가지는 코드가 콜백 함수에서의 this를 결정하며, 특별히 정의하지 않은 경우 전역객체를 가리킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 생성자 함수 내부에서의 this&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자 함수 내부에서의 this는 곧 새로 만들 인스턴스 자신이 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1717378479457&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function Person(name) {
    this.name = name;
}

const alice = new Person('Alice');
console.log(alice.name); // 'Alice'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콜백 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백 함수는 &lt;b&gt;다른 코드에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수&lt;/b&gt;이다. 콜백 함수를 위임받은 코드는 이 콜백 함수를 적절한 시점에 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백 함수로 어떤 객체의 메서드를 전달하더라도 그 메서드는 &lt;b&gt;메서드가 아닌 함수로써 호출&lt;/b&gt;된다. 이때 'this'는 전역 객체를 가리킨다.&lt;/p&gt;
&lt;pre id=&quot;code_1717116840989&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const obj = {
    name: 'gini',
    func: function() {
        console.log(this)
    }
};
setTimeout(obj.func, 1000); // Window {...}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bind 메서드를 사용해서 콜백 함수 내부 this에 다른 값을 바인딩할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1717116895511&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const obj = {
    name: 'gini',
    func: function() {
        console.log(this)
    }
};
setTimeout(obj.func.bind(obj), 1000); // {name: 'gini', func: &amp;fnof;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클로저&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.&lt;/p&gt;
&lt;pre id=&quot;code_1717117483047&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function outer() {
    let count = 0;
    return function inner() {
        count++;
        console.log(count);
    };
}

const counter = outer();
counter(); // 1
counter(); // 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;클로저의 메모리 관리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클로저는 본질이 메모리를 계속 차지하는 개념이므로 더는 사용하지 않게 된 클로저에 대해서는 메모리를 차지하지 않도록 관리해줄 필요가 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1717117778877&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const outer = (function () {
    let count = 0;
    const inner = function() {
        count++;
        console.log(count);
    };
    return inner;
})();

outer();
outer = null;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조하는 것을 제거하면 된다. 보통 null이나 undefined를 할당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;클로저 활용 사례&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 콜백 함수 내부에서 외부 데이터를 사용하고자 할 때&lt;/p&gt;
&lt;pre id=&quot;code_1717399168613&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function fetchData(url) {
    let data;
    fetch(url).then(response =&amp;gt; {
        data = response.json();
        console.log(data);
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 접근 권한 제어(정보 은닉)&lt;/p&gt;
&lt;pre id=&quot;code_1717399179673&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function createCounter() {
    let count = 0;
    return {
        increment: function() {
            count++;
            console.log(count);
        },
        decrement: function() {
            count--;
            console.log(count);
        }
    };
}

const counter = createCounter();
counter.increment(); // 1
counter.decrement(); // 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 부분 적용 함수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가, 나중에 나머지 인자를 넘겨서 함수의 실행 결과를 얻을 수 있는 함수&lt;/p&gt;
&lt;pre id=&quot;code_1717399192075&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function multiply() {
    let result = 1;
    for (let i = 0; i &amp;lt; arguments.length; i++) {
    	result *= arguments[i];
    }
    return result;
}

const multiplyPartial = multiply.bind(null, 1, 2, 3);
console.log(multiplyPartial(4, 5, 6)); // 720&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 커링 함수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것. 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않는다.&lt;/p&gt;
&lt;pre id=&quot;code_1717399203626&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function curry(func) {
    return function (a) {
    	return function (b) {
        	return function (c) {
            	     return func(a, b, c);
            }
        }
    }
}

const curriedMax = curry(Math.max);
console.log(curriedMax(1)(2)(3)); // 3&lt;/code&gt;&lt;/pre&gt;</description>
      <category>JavaScript</category>
      <author>MIRACLE LIFE</author>
      <guid isPermaLink="true">https://geonbbang.tistory.com/64</guid>
      <comments>https://geonbbang.tistory.com/64#entry64comment</comments>
      <pubDate>Mon, 20 May 2024 21:41:35 +0900</pubDate>
    </item>
  </channel>
</rss>