<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hkleeeee11.log</title>
        <link>https://velog.io/</link>
        <description>야생의 개발자!</description>
        <lastBuildDate>Mon, 18 Dec 2023 08:00:57 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hkleeeee11.log</title>
            <url>https://velog.velcdn.com/images/hee_kyoung11/profile/ea03ad2c-6733-42a2-ad3d-f57693613a67/image.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hkleeeee11.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hee_kyoung11" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[부스트캠프 웹・모바일 그룹프로젝트 6주차 회고]]></title>
            <link>https://velog.io/@hee_kyoung11/%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%E3%83%BB%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B7%B8%EB%A3%B9%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-6%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hee_kyoung11/%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%E3%83%BB%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B7%B8%EB%A3%B9%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-6%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 18 Dec 2023 08:00:57 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/14f492e7-6b1c-44f8-8a93-60bd754ff973/image.png" alt="">
끝났다.......
부스트캠프를 끝내며 하는 회고는 따로하기로하고
그룹프로젝트에 대한 회고만 해보자</p>
<h1 id="keep">Keep</h1>
<p>프로젝트를 진행하면서 모든 고민에 대해서 문서화를 하지는 못했지만,
그대로 중요하게 고민했던 것들에 대해서는 문서화를 잘 해둔 것 같다.
문서화를 잘 하는 팀원들을 만나서 자극받아 열심히 했던 것 같다! 
무한감사 🙏
쨋든 그 덕에 발표자료를 준비할 것이 많이 없었다.
기술적도전으로 적어둔 내용들을 거의 그대로 발표할 수 있었다.
앞으로도 모든 <code>&#39;왜&#39;</code>에 대한 나의 모든 고민들을 더 많이 문서화해야겠다.</p>
<p>수료식 커피챗에서 들었던 말</p>
<blockquote>
<p>천천히 가더라도 아무도 뭐라고 하지 않아요.
그런데 급하게 가면 어떻게 되겠어요? 체할겁니다.</p>
</blockquote>
<h1 id="problem">Problem</h1>
<p>발표자료를 만들때는 적절한 문자와 이미지가 배치되어야하는 것 같다
발표할 때 글씨가 너무 많으면 보는 사람이 지루해지고,
또 글씨가 너무 없으면 눈에 내용이 잘 들어오지도 않고 나중에 PPT만 보게된다면 이해할 수 없을 것이다.
다음에 발표를 하게 된다면, (과연 앞으로 발표를 하게될 일이 많을까??)
이미지와 텍스트를 적절히 섞은 발표자료를 만들어야겠다.
과하지 않은 시각적효과도 좋은 것 같다.</p>
<p>전체 발표에 선정된 팀들의 발표자료를 보면서 많이 배웠다.</p>
<h1 id="try">Try</h1>
<p>이제 그룹프로젝트가 끝났다.
앞으로의 개발자 인생에서 어떤 것들을 더 해볼 수 있을까?</p>
<ol>
<li><p>내가 이것을 왜 해야하는지 인식하기
커피챗에서 해주셨던 이야기인데 개발자에게 있어 가장 중요한 것은 <code>why</code>이고 그 다음이 <code>how</code>라고 생각한다 하셨다.
그저 열심히 배운다기보다 무엇을 배울 때 이것을 공부해야하는 이유를 알고 해야한다고. 그것을 발굴할 줄 알아야한다고 한다.</p>
</li>
<li><p>TDD
테스트를 많이 하기
혹자는 이 정도 규모의 프로젝트에서, 짧은 6주간의 개발에서 테스트 코드는 사치라고 할 수도 있다.
하지만 이번에 프로젝트를 하면서 테스트 코드의 필요성을 절실히 느꼈다.
동시에 엣지케이스를 잘 찾아내는 것의 중요성도 배울 수 있었다.
캠퍼들과 했던 데모에서도 느꼈지만 네트워킹데이에 참석한 현업자분께 시연하던 중 바로 버그가 발굴되어버려 더더욱 필요함을 느꼈다.</p>
</li>
<li><p>많이 알기보단 정확히 알기
그동안 조급함 때문에 얕고 넓게 알려고 했던 것 같다.
많이 아는 척은 할 수 있지만 진짜로 알지는 못하는 사람이 되고 있었던 것이다.
이번 부스트캠프를 하면서 그건 진짜로 아는 것이라고 할 수 없다 생각하게 되었다.
위에 적었던 것처럼 천천히 가더라도 그 과정에서 왜를 잘 생각하고 기록할 수 있는 개발자가 되어야겠다.</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS에 winston으로 로그 남기기]]></title>
            <link>https://velog.io/@hee_kyoung11/NestJS%EC%97%90-winston%EC%9C%BC%EB%A1%9C-%EB%A1%9C%EA%B7%B8-%EB%82%A8%EA%B8%B0%EA%B8%B0</link>
            <guid>https://velog.io/@hee_kyoung11/NestJS%EC%97%90-winston%EC%9C%BC%EB%A1%9C-%EB%A1%9C%EA%B7%B8-%EB%82%A8%EA%B8%B0%EA%B8%B0</guid>
            <pubDate>Thu, 14 Dec 2023 08:35:09 GMT</pubDate>
            <description><![CDATA[<h1 id="winston-logger">Winston Logger</h1>
<p>Winston의 Logging 레벨은 RFC5424 표준을 따르고 있다고 한다.</p>
<p><a href="https://datatracker.ietf.org/doc/html/rfc5424">RFC 5424</a>: The Syslog Protocol</p>
<pre><code class="language-jsx">Numerical         Severity
 Code

  0       Emergency: system is unusable
  1       Alert: action must be taken immediately
  2       Critical: critical conditions
  3       Error: error conditions
  4       Warning: warning conditions
  5       Notice: normal but significant condition
  6       Informational: informational messages
  7       Debug: debug-level messages</code></pre>
<ul>
<li>winston logger level</li>
</ul>
<pre><code class="language-jsx">const levels = {
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  verbose: 4,
  debug: 5,
  silly: 6
};</code></pre>
<p>winston logger를 생성할 때</p>
<p><code>level</code> 옵션을 주게 되면 주어진 옵션보다 작거나 같은 레벨만 출력된다.</p>
<pre><code class="language-jsx">const logger = winston.createLogger({
  level: &#39;info&#39;,
  transports: [
    new winston.transports.Console(),
  ]
});</code></pre>
<p>예를 들어 위와같이 info 레벨을 주게되면, error, warn, info만 출력되게 된다.</p>
<h1 id="로그-파일로-남기기">로그 파일로 남기기</h1>
<p>winston으로 로그를 남길 때 어디에 로그를 출력할 지 설정할 수 있다.</p>
<p>transports 옵션으로 출력을 설정할 수 있다.</p>
<p>위의 예시 처럼 남기게되면 콘솔에만 찍히고 어디에도 남아있게 되지 않는다.</p>
<pre><code class="language-jsx">const logger = winston.createLogger({
  transports: [
    new winston.transports.File({
      filename: &#39;combined.log&#39;,
      level: &#39;info&#39;
    }),
  ]
});</code></pre>
<p>이렇게 설정하게 되면 파일로 저장할 수 있다.</p>
<p>하지만 위와 같이 설정하게 되면 모든 로그가 한 파일에 담길 수 있다.</p>
<p>로그를 날짜, 시간별로 관리하기 위해서 <code>winston-daily-rotate-file</code>을 도입했다.</p>
<p>로그를 순환할 수 있고, 개수, 경과일 수를 기준으로 오래된 로그를 삭제할 수있게 도와주는 패키지이다.</p>
<pre><code class="language-jsx">const transport: winston.transport[] = [
      new winstonDaily({
        filename: &#39;%DATE%.log&#39;,
        datePattern: &#39;YYYY-MM-DD-HH&#39;,
        dirname: logDir,
        zippedArchive: true,
        maxSize: &#39;20m&#39;,
        maxFiles: &#39;7d&#39;,
      }),
    ];</code></pre>
<p>각종 옵션들로 로그파일의 최대 사이즈, 몇 일간의 로그를 저장할지, 어떤 패턴으로 저장할 지 등을 지정할 수 있다.</p>
<h1 id="nestjs에-도입하기">NestJS에 도입하기</h1>
<p>NestJs에서는 기본적으로 Logger를 제공하고 있고, 싱글톤 패턴으로 관리한다.</p>
<p>Winston을 NestJS 생명주기에 적합하게 도입하기 위해서 Winston으로 LoggerService의 구현체를 만들어 주입해서 이용해야한다. </p>
<p>LoggerService 인터페이스는 다음과 같이 구현되어있었다.</p>
<pre><code class="language-jsx">export interface LoggerService {
    /**
     * Write a &#39;log&#39; level log.
     */
    log(message: any, ...optionalParams: any[]): any;
    /**
     * Write an &#39;error&#39; level log.
     */
    error(message: any, ...optionalParams: any[]): any;
    /**
     * Write a &#39;warn&#39; level log.
     */
    warn(message: any, ...optionalParams: any[]): any;
    /**
     * Write a &#39;debug&#39; level log.
     */
    debug?(message: any, ...optionalParams: any[]): any;
    /**
     * Write a &#39;verbose&#39; level log.
     */
    verbose?(message: any, ...optionalParams: any[]): any;
    /**
     * Write a &#39;fatal&#39; level log.
     */
    fatal?(message: any, ...optionalParams: any[]): any;
    /**
     * Set log levels.
     * @param levels log levels
     */
    setLogLevels?(levels: LogLevel[]): any;
}</code></pre>
<p>로깅 레벨이 Winston과 살짝 다른 것을 발견할 수 있었다.</p>
<p>NestJS는 Logging Level을 지정하는 것이 아니라 출력하고 싶은 레벨을 배열로 설정하는 형태인 것을 알 수 있었다.</p>
<pre><code class="language-jsx">const app = await NestFactory.create(AppModule, {
  logger: [&#39;error&#39;, &#39;warn&#39;],
});
await app.listen(3000);</code></pre>
<p>Winston과 NestJS은 서로 다른 로그 시스템을 가지고 있지만,</p>
<p>NestJS에서 Winston을 이용하기위해 인터페이스에서 제공하는 것과 Winston에서 일치하는 로그레벨만 사용했다.</p>
<p>Winston에서 제공하는 로그레벨의 순서를 알아냈어야 했다.</p>
<pre><code class="language-jsx">// winstonLogger.service.ts
// LoggerService 구현체

log(message: string, ...optionalParams: any[]) {
  this.logger.log(message, ...optionalParams);
}

error(message: string, ...optionalParams: any[]) {
  this.logger.error(message, ...optionalParams);
}

warn(message: string, ...optionalParams: any[]) {
  this.logger.warn(message, ...optionalParams);
}
debug(message: string, ...optionalParams: any[]) {
  return this.logger.debug(message, ...optionalParams);
}
verbose(message: string, ...optionalParams: any[]) {
  this.logger.verbose(message, ...optionalParams);
}</code></pre>
<pre><code class="language-jsx">@Controller()
export class AppController {
  private readonly logger = new Logger();

  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    this.logger.log(&#39;log level&#39;);
    this.logger.error(&#39;error level&#39;);
    this.logger.warn(&#39;warn level&#39;);
    this.logger.debug(&#39;debug level&#39;);
    this.logger.verbose(&#39;verbose level&#39;);

    return this.appService.getHello();
  }
}</code></pre>
<p>위와 같이 테스트용 컨트롤러를 구성하고, winson 출력로그 레벨을 바꾸면서 테스트했다.</p>
<h3 id="레벨별-출력-결과">레벨별 출력 결과</h3>
<p>debug</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/9f2a944c-7d7a-44ec-8a9f-eb5653874a33/image.png" alt=""></p>
<p>verbose</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/e9ce0592-7448-49e5-b081-1e22c1a0b525/image.png" alt=""></p>
<p>info</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/2a0cee36-c3a2-46c2-b843-099613d3ba91/image.png" alt=""></p>
<p>warn</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/24b4c445-ad27-419e-a3c9-d898d0ac964e/image.png" alt=""></p>
<p>error</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/9ce7bb73-08ae-4e98-9060-4ba4318da3db/image.png" alt=""></p>
<p>우선순위는 </p>
<p><code>error</code> → <code>warn</code> → <code>info</code> → <code>verbose</code> → <code>debug</code> 순서임을 알 수 있었고</p>
<p>NestJS의 log가 Winston의 info에 해당한다는 것도 알 수 있었다.</p>
<h1 id="운영하는-서버에서-로그-확인하기">운영하는 서버에서 로그 확인하기</h1>
<p>winston, winston-daily-rotate-file, NestJS LoggerService를 이용해서 서버 로그를 남길 수 있게되었고</p>
<p>서버에서 로그파일을 확인할 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/c95f3717-992a-4f43-a362-65dd486591ee/image.png" alt=""></p>
<p>그런데 한 가지 문제가 있었다.</p>
<p>도커로 서버를 띄워서 운영하고 있었기 때문에, 재배포가 일어나면 도커컨테이너가 변경되어 쌓아온 로그파일이 날라가 버렸다.</p>
<p>개발 중 서버가 한 번 죽은 적이 있었는데, 이전 로그가 다 날아가버려서 이유를 찾을 수 없었다.</p>
<h3 id="도커-볼륨-마운트">도커 볼륨 마운트</h3>
<p>이를 해결하기 위해서 서버의 볼륨과 도커 컨테이너의 볼륨을 마운트해서 로그파일이 계속 유지되도록 했다.</p>
<pre><code class="language-jsx">version: &#39;3.8&#39;

services:
  blue:
    container_name: blue
    image: hkleeeee/api
    ports:
      - 4001:4000
    env_file:
      - .env
    volumes:
      - ./logs:/var/app/logs
    restart:
      always
  green:
    container_name: green
    image: hkleeeee/api
    ports:
      - 4002:4000
    env_file:
      - .env
    volumes:
      - ./logs:/var/app/logs
    restart:
      always</code></pre>
<p>도커 컴포즈 파일에 volumes을 연결해주었다.</p>
<p>이 옵션을 통해 도커 재시작에도 로그 파일을 유지할 수 있게 되었고,</p>
<p>로그를 확인하는 것도 더 편해졌다.</p>
<p>전에는 서버 ssh 접속 → 도커 인터렉티브 모드로 접속 → 로그파일 위치로 이동 과정을 통해 로그파일을 확인할 수 있었는데,</p>
<p>볼륨 마운트를 통해서 도커에 접속하지 않고도 로그 파일을 확인할 수 있었다.</p>
<h3 id="에러-레벨-별도로-모으기">에러 레벨 별도로 모으기</h3>
<p>모든 로그를 한 파일에 작성하면서 서버를 운영하고 있었다.</p>
<p>그리고 부하테스트를 진행했는데 이 부분에 불편함이 생겼다.</p>
<p>무수히 많은 요청 속에서 에러가 발생하는 곳의 로그를 보고 싶었는데, 모든 레벨의 로그를 한 파일에 담고 있으니 에러 로그를 찾기가 너무 어려웠다.</p>
<p>그래서 에러레벨의 로그만 별도로 저장하는 설정을 추가해주었다.</p>
<pre><code class="language-jsx">const transport: winston.transport[] = [
      new winstonDaily({
        filename: &#39;%DATE%.log&#39;,
        datePattern: &#39;YYYY-MM-DD-HH&#39;,
        dirname: logDir,
        zippedArchive: true,
        maxSize: &#39;20m&#39;,
        maxFiles: &#39;7d&#39;,
      }),
      new winstonDaily({
        filename: &#39;%DATE%.error.log&#39;,
        datePattern: &#39;YYYY-MM-DD-HH&#39;,
        dirname: logDir + &#39;/error&#39;,
        zippedArchive: true,
        maxSize: &#39;20m&#39;,
        maxFiles: &#39;7d&#39;,
        level: &#39;error&#39;,
      }),
    ];</code></pre>
<p>첫번째 transport는 모든 로그를 순서대로 보여주어 로그의 흐름을 알 수 있다.</p>
<p>두번째 transport는 출력되는 로그중 에러레벨만 따로 저장하고 있어 에러가 발생한 경우만 따로 찾아볼 수 있게되었다.</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/58d7ddf5-ce7d-4366-ad4c-4582d16ba498/image.png" alt=""></p>
<hr>
<p>완성한 코드</p>
<pre><code class="language-jsx">//winstonLogger.service.ts
import { utilities, WinstonModule } from &#39;nest-winston&#39;;
import * as winstonDaily from &#39;winston-daily-rotate-file&#39;;
import * as winston from &#39;winston&#39;;
import * as path from &#39;path&#39;;

const { combine, timestamp, printf, colorize } = winston.format;
import { Injectable, LoggerService } from &#39;@nestjs/common&#39;;
import { ConfigService } from &#39;@nestjs/config&#39;;

@Injectable()
export class WinstonLogger implements LoggerService {
  private logger: LoggerService;
  private readonly logDir = (() =&gt; {
    const __dirname = path.resolve();
    return path.join(__dirname, &#39;logs&#39;);
  })();

  constructor(private configService: ConfigService) {
    const transport = this.getTransport();
    this.logger = this.createLogger(transport);
  }

  getTransport(): winston.transport[] {
    const transport: winston.transport[] = [
      new winstonDaily({
        filename: &#39;%DATE%.log&#39;,
        datePattern: &#39;YYYY-MM-DD-HH&#39;,
        dirname: this.logDir,
        zippedArchive: true,
        maxSize: &#39;20m&#39;,
        maxFiles: &#39;7d&#39;,
      }),
      new winstonDaily({
        filename: &#39;%DATE%.error.log&#39;,
        datePattern: &#39;YYYY-MM-DD-HH&#39;,
        dirname: this.logDir + &#39;/error&#39;,
        zippedArchive: true,
        maxSize: &#39;20m&#39;,
        maxFiles: &#39;7d&#39;,
        level: &#39;error&#39;,
      }),
    ];

    if (this.configService.get&lt;string&gt;(&#39;NODE_ENV&#39;) !== &#39;production&#39;) {
      const devConsole = new winston.transports.Console({
        format: combine(
          colorize(),
          utilities.format.nestLike(&#39;API Server&#39;, {
            prettyPrint: true,
          }),
        ),
      });

      transport.push(devConsole);
    }
    return transport;
  }

  createLogger(transport: winston.transport[]) {
    const logFormat = printf(
      ({ level, message, timestamp }) =&gt; `${timestamp} ${level}: ${message}`,
    );
    return WinstonModule.createLogger({
      format: combine(
        timestamp({ format: &#39;YYYY-MM-DD HH:mm:ss.SSS&#39; }),
        logFormat,
      ),
      level:
        this.configService.get&lt;string&gt;(&#39;NODE_ENV&#39;) !== &#39;production&#39;
          ? &#39;debug&#39;
          : &#39;info&#39;,
      transports: transport,
    });
  }

  log(message: string, ...optionalParams: any[]) {
    this.logger.log(message, ...optionalParams);
  }

  error(message: string, ...optionalParams: any[]) {
    this.logger.error(message, ...optionalParams);
  }

  warn(message: string, ...optionalParams: any[]) {
    this.logger.warn(message, ...optionalParams);
  }
  debug(message: string, ...optionalParams: any[]) {
    return this.logger.debug(message, ...optionalParams);
  }
  verbose(message: string, ...optionalParams: any[]) {
    this.logger.verbose(message, ...optionalParams);
  }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[refresh token 도입기]]></title>
            <link>https://velog.io/@hee_kyoung11/refresh-token-%EB%8F%84%EC%9E%85%EA%B8%B0</link>
            <guid>https://velog.io/@hee_kyoung11/refresh-token-%EB%8F%84%EC%9E%85%EA%B8%B0</guid>
            <pubDate>Tue, 12 Dec 2023 16:40:49 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/b4042aac-7ab7-4ec8-9965-b1d7e03ebbe9/image.png" alt=""></p>
<p>로그인시 JWT 토큰을 발행하기로 했다. </p>
<p>JWT는 세션방식으로 로그인을 유지할 때보다 서버가 저장하고 있어야할 데이터가 줄어드는 장점이 있지만 </p>
<p>탈취되었을 경우 더 위험하다. </p>
<p>서비스의 성격을 고려했을 때 개인정보를 거의 저장하고 있지 않았다. 개인정보를 많이 저장하고 있는 서비스 보다 보안의 위협이 덜하였기 때문에 JWT가 더 적절하다고 생각했다. </p>
<p>그럼에도 보안은 항상 중요하기 때문에 JWT Access Token를 보완하기위한 Refresh Token을 도입하기로 했다.</p>
<h1 id="jwt-토큰을-어디에-저장할-것인가">JWT 토큰을 어디에 저장할 것인가?</h1>
<p>발급한 토큰들을 어떻게 저장해야할지 정해야했다.</p>
<p>아래 두 이유로 토큰을 쿠키에 저장하기로 했다.</p>
<ol>
<li><p>쿠키는 Http Only 옵션이 있어서 XSS 공격을 예방할 수 있다.</p>
</li>
<li><p>클라이언트가 저장 중이다가 필요할 때 헤더에 넣어서 요청을 보내는 방식은 번거로웠다.</p>
<p> 프론트에서 신경써야할 것도 많고, access token이 만료되었을때 다시 refresh token을 요청하고 다시 헤더로 받는 과정들이 추가되었다. </p>
<p> 앱 프로젝트라면 선택의 여지가 없지만 웹 프로젝트였기 때문에 할 수 있는 선택이었다.</p>
</li>
</ol>
<h1 id="어떻게-사용자를-인가할-것인가">어떻게 사용자를 인가할 것인가?</h1>
<p>NestJS에서는 JWTModule로 JWT를 발급, 검증하고 Guard를 이용해서 요청하는 사용자의 권한을 확인한다.</p>
<p>이 두 가지를 이용해서 사용자 인가를 처리하기로 했다.</p>
<p>이 과정에서 Passport는 사용하지 않았다. </p>
<p>Passport를 이용한 PassPortStratgy를 Guard에 적용해서 사용하는 방법이 있었는데, 리프레시 토큰을 도입하자니 문제가 있었다.</p>
<ol>
<li><p>Passport 정책을 사용한다고해도 결국 Guard를 사용해야한다.</p>
<p> Passport 정책을 통과하지 못하는 경우에서도 Guard에서 모두 true를 리턴해버리니 권한이 있는 사용자로 처리되었다.</p>
</li>
<li><p>Passport를 활용하려면 access strategy, refresh strategy 가 각각 필요했고 내가 원하는 방식으로 커스텀하는 것이 어려웠다.</p>
</li>
</ol>
<p>이런 이유로 Passport를 걷어 내고 NestJS에서 제공하는 기능만으로 구현했고 충분했다!</p>
<p>생각한 정책은 다음과 같다.</p>
<blockquote>
<p>🔒 JWT 정책</p>
</blockquote>
<ol>
<li>access token와 refresh token이 유효한지 확인한다.</li>
</ol>
<ul>
<li>두 토큰이 모두 없는 경우 권한이 없다고 판단한다.</li>
</ul>
<ol start="2">
<li>access token이 유효한 경우</li>
</ol>
<ul>
<li>refresh token이 만료되었다면 재발급한다.
(refresh token 재발급은 쿠키에 저장, Redis에 저장 두 가지 과정을 거친다.)
request에 access token에서 읽어온 사용자 정보를 넣어 요청 핸들러로 보낸다.  </li>
</ul>
<ol start="3">
<li>access token이 유효하지 않지만 refresh token이 유효한 경우
<em>(여기서 access token이 유효하지 않은 경우는 값이 없거나 유효기간이 끝난 경우이다. 이외의 경우(변형됨… )에는 바로 401(Unauthorized) 처리를 했다.</em>
 Redis에서 해당 유저의 refresh token을 꺼내와 비교한다.
 비교 후 값이 같으면 access token을 재발급하고,
 request에 refresh token에서 읽어온 사용자 정보를 넣어 요청 핸들러로 보낸다.
데이터베이스에 저장된 값과 서버로 받은 값이 다르면 리프레쉬 토큰이 변형되었다고 판단하고 403(Forbidden) 처리 한다.</li>
</ol>
<p>이렇게 세운 정책은 문제가 있었다.</p>
<p>항상 사용자가 가지고 있는 두 종류의 토큰을 검증해야한다.</p>
<p>단지 액세스 토큰이 유효한데 리프레시 토큰이 만료되었을 그 찰나를 위해서 매번 사용자의 토큰 검증이 2배가 되어야한다는 것은 비효율적이라고 판단했다.</p>
<p>고민해보고 멘토님께 조언을 구한 결과 이런 답을 얻을 수 있었다.</p>
<p>‘리프레시 토큰 재발급은 재로그인을 통해서만 이루어지는 게 보안상 더 안전하다.’</p>
<p>이런 답을 가지고 변경한 정책은 다음과 같다.</p>
<blockquote>
<p>🔒 변경한 JWT 정책</p>
</blockquote>
<ol>
<li>access token와 refresh token 두 토큰이 모두 없는 경우 권한이 없다고 판단한다.</li>
<li>access token을 검증한다</li>
<li>access token이 유효한 경우
 request에 access token에서 읽어온 사용자 정보를 넣어 요청 핸들러로 보낸다.</li>
<li>access token이 유효하지 않지만 refresh token이 유효한 경우
 Redis에서 해당 유저의 refresh token을 꺼내와 비교한다.
비교 후 값이 같으면 access token을 재발급하고,
request에 refresh token에서 읽어온 사용자 정보를 넣어 요청 핸들러로 보낸다.
데이터베이스에 저장된 값과 서버로 받은 값이 다르면 리프레쉬 토큰이 변형되었다고 판단하고 403(Forbidden) 처리 한다.</li>
</ol>
<p>훨씬 명확한 정책으로 변경할 수 있었다.</p>
<p>구현한 결과는 다음과 같다.</p>
<pre><code class="language-jsx">async canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    const response = context.switchToHttp().getResponse();
    const accessToken = request.cookies.access_token;
    const refreshToken = request.cookies.refresh_token;
    if (!accessToken &amp;&amp; !refreshToken) {
      throw new JsonWebTokenError(jwtError.NO_TOKEN);
    }

    const accessResult = await this.verifyAccessToken(accessToken);
    if (accessResult) {
      request.user = this.serializeUser(accessResult);
      return true;
    }

    const refreshResult = await this.verifyRefreshToken(refreshToken);
    const refreshTokenHave = await this.redisService.getRefreshToken(
      refreshResult.sub,
    );
    if (refreshTokenHave !== refreshToken) {
      this.logger.warn(
        `${refreshResult} 사용자가 변형된 리프레시 토큰을 보유함`,
      );
      return false;
    }
    const user = this.serializeUser(refreshResult);

    const newAccessToken = await this.authService.getAccessToken(user);
    this.authService.setAccessToken(response, newAccessToken);
    request.user = user;
    return true;
  }</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[OAuth 2.0은 무엇이고 어떻게 적용할 수 있을까?]]></title>
            <link>https://velog.io/@hee_kyoung11/OAuth-2.0%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%81%EC%9A%A9%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@hee_kyoung11/OAuth-2.0%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%81%EC%9A%A9%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Tue, 12 Dec 2023 16:34:27 GMT</pubDate>
            <description><![CDATA[<h1 id="oauth-20">OAuth 2.0</h1>
<p>Open Standard for Authorization</p>
<p><em>2006년 Twitter와 Google이 정의한 개방형 Authorization 표준</em></p>
<p>요즘 웹, 모바일 서비스를 이용하다보면 ~~로 로그인하기로 불리는 소셜로그인을 자주 찾아볼 수 있다.</p>
<p>이게 OAuth이다.</p>
<p>작은 서비스에서는 사용자의 정보를 안전하게 저장하고 관리하는 것은 큰 부담이다. 이 부담을 <strong>다른 큰 플랫폼(구글, 페이스북, 네이버 등)에 위임</strong>하여 이용할 수 있다. OAuth는 이 위임 과정을 위한 <strong>규약</strong>이다.</p>
<p>2.0 버전은 1.0 버전에서 발생된 보안 문제를 개선한 버전으로,</p>
<p>1.0 버전에서는 프로토콜로 2.0으로 업그레이드 되면서 프레임워크로 정의되고 있다.</p>
<p>표준 문서 (RFC6749) :  <a href="https://datatracker.ietf.org/doc/html/rfc6749">https://datatracker.ietf.org/doc/html/rfc6749</a>   </p>
<pre><code class="language-jsx">  +--------+                               +---------------+
    |        |--(A)- Authorization Request -&gt;|   Resource    |
  |        |                               |     Owner     |
  |        |&lt;-(B)-- Authorization Grant ---|               |
  |        |                               +---------------+
  |        |
  |        |                               +---------------+
  |        |--(C)-- Authorization Grant --&gt;| Authorization |
  | Client |                               |     Server    |
  |        |&lt;-(D)----- Access Token -------|               |
  |        |                               +---------------+
  |        |
  |        |                               +---------------+
  |        |--(E)----- Access Token ------&gt;|    Resource   |
  |        |                               |     Server    |
  |        |&lt;-(F)--- Protected Resource ---|               |
  +--------+                               +---------------+</code></pre>
<h1 id="용어">용어</h1>
<ol>
<li><p>Resource Owner, 자원 소유자</p>
<p> 우리 서비스의 사용자를 말한다.</p>
<p> 구글 로그인을 통해서 구글의 유저 정보를 가져오는 상황이라면 그 구글 유저 자원에 대한 소유자로 이해할 수 있다.</p>
</li>
<li><p>Client, 클라이언트</p>
<p> OAuth 2.0을 통해서 타사 서비스의 자원을 이용하고자하는 주체를 말한다.</p>
<p> 내가 개발하고 있는 서비스가 클라이언트가 된다. (내가 개발하고 있는 서버가 클라이언트가 된다..!!!)</p>
<p> 구글입장에서의 클라이언트라고 생각하면 편할 것 같다.</p>
</li>
<li><p>Authorization Server, 인가 서버</p>
<p> 인가 처리를 담당하는 서버이다.</p>
<p> 클라이언트에 ID, Secret을 부여하고, Code를 발급하거나 Token을 발급한다.</p>
</li>
<li><p>Resource Server, 자원 서버</p>
<p> 클라이언트에서 사용하고자 하는 자원(여기서는 유저 정보)를 가지고있는 서버이다.</p>
<p> 클라이언트는 인가서버에서 발급받은 토큰으로 자원 서버에 접근해서 원하는 자원을 이용할 수 있다.</p>
</li>
</ol>
<h1 id="적용한-매커니즘">적용한 매커니즘</h1>
<p>(서버와 클라이언트가 헷갈릴 수 있어 프론트엔드, 백엔드 용어로 작성했습니다.)</p>
<p>OAuth를 공부하며 찾아본 글들 중 인증, 인가처리를 프론트엔드에서 하고 있는 경우를 종종 찾아볼 수 있었다.</p>
<p>하지만 프론트엔드에서 이것들을 처리하게 되면 여러 문제가 있다.</p>
<ol>
<li><p>Client ID를 공개해야한다.</p>
<p> 이것은 문제라기 보다는 개인적으로 이렇게 하고 싶지않았다.</p>
<p> OAuth 인가서버에 요청을 보낼 때 Client ID를 함께 보낸다.</p>
<p> 프론트엔드에서 인증, 인가를 처리하려면 Client ID를 공개해야 한다. </p>
<p> 리다이렉션을 하게 되면 url을 통해 공개가 되지만, 코드에 남게하고 싶지 않았다.</p>
</li>
<li><p>발급받은 액세스 토큰이 노출될 수 있다.</p>
<p> 인증코드와 함께 응답을 받으면 인증코드와 Client Secret을 이용해 액세스 토큰을 발급받는다.</p>
<p> 그리고 발급받은 액세스토큰을 가지고 자원 서버에 필요한 자원을 요청받아 이용한다.</p>
<p> 이 과정을 백엔드에서 처리하게 되면 프론트엔드에서 알 수 있는 것은 자원서버에서 얻어온 정보뿐이지만, 프론트엔드에서 진행하게 되면 액세스토큰까지 알게 되어 OAuth 계정 정보 탈취의 위협이 생긴다.</p>
</li>
</ol>
<p>이런 이유들로 백엔드에서 모든 OAuth 과정을 진행하는 것이 옳다고 판단했고, 다음과 같은 매커니즘으로 구현하였다.</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/3af8d3dd-6258-4c20-9ec8-b8cdcd3f5694/image.jpg" alt=""></p>
<h3 id="github-login-적용-모습">Github Login 적용 모습</h3>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/8643f5d9-a419-41e4-8b6e-fe17a87b3677/image.png" alt=""></p>
<hr>
<p>참고</p>
<p><a href="https://hudi.blog/oauth-2.0/">https://hudi.blog/oauth-2.0/</a></p>
<p>이어지는 글
<a href="https://velog.io/@hee_kyoung11/refresh-token-%EB%8F%84%EC%9E%85%EA%B8%B0">👉 refresh token 도입기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AsyncLocalStorage를 이용해 Transaction 관심사 분리하기  (lines 40% 감소)]]></title>
            <link>https://velog.io/@hee_kyoung11/AsyncLocalStorage%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-Transaction-%EA%B4%80%EC%8B%AC%EC%82%AC-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-lines-40-%EA%B0%90%EC%86%8C</link>
            <guid>https://velog.io/@hee_kyoung11/AsyncLocalStorage%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-Transaction-%EA%B4%80%EC%8B%AC%EC%82%AC-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-lines-40-%EA%B0%90%EC%86%8C</guid>
            <pubDate>Mon, 11 Dec 2023 14:07:37 GMT</pubDate>
            <description><![CDATA[<p>진행하는 프로젝트 특성상 DB 작업이 복잡하게 일어나지 않습니다. ORM에서 제공하는 트랜잭션, 쿼리문으로 충분한 수준입니다.</p>
<p>하지만 만약 DB 작업이 복잡해진다면 어떻게 해야할까요? 쿼리나 트랜잭션을 직접 관리해주는 것이 좋을 것이라 생각했습니다.</p>
<p>그래서 이번 프로젝트에서도 트랜잭션을 적용해보기로 했습니다.
<a href="https://github.com/boostcampwm2023/web05-AlgoITNi">프로젝트 Github</a></p>
<h1 id="문제점">문제점</h1>
<p>가장 기본적으로 트랜잭션은 이렇게 구현할 수 있습니다.</p>
<pre><code class="language-jsx">  const qr = this.getQueryRunner();
  try {
    await qr.connect();
    await qr.startTransaction();
    await qr.manager.save&lt;UserEntity&gt;(user); // 실제 쓰기가 일어나는 곳
    await qr.commitTransaction();
  } catch (e) {
    console.log(e);
    await qr.rollbackTransaction();
    throw new TransactionRollback();
  } finally {
    await qr.release();
  }</code></pre>
<p>실제 쓰기가 일어나는 곳 하나를 작성하기위해 무수히 많은 코드가 필요합니다.</p>
<p>프로젝트에는 쿼리가 하나만 있는 것이 아니기 때문에 매번 데이터베이스 쓰기작업을 하는 함수마다 이 모든 걸 작성해주는 것은 귀찮고, 비효율적이고, 휴먼에러 발생확률이 매우 높습니다.</p>
<p>그래서 관심사 분리를 통해 공통 로직들을 분리합니다.</p>
<h1 id="aop">AOP</h1>
<p>Aspect Oriented Programming, 관점 지향 프로그래밍</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/75d2b663-3dca-446d-a81d-3aa283446d51/image.png" alt="">
출처 <a href="https://tecoble.techcourse.co.kr/post/2021-06-25-aop-transaction/">https://tecoble.techcourse.co.kr/post/2021-06-25-aop-transaction/</a></p>
<p>위의 그림으로 가장 잘 AOP가 설명된다고 생각합니다.</p>
<p>Class A가 회원가입을 담당하고 있으면 A에서는 회원가입만하고 로깅, 보안, 트랜잭션은 다른 곳에서 하게 하겠다는 것이죠.</p>
<p>핵심 비즈니스 로직과 공통적으로 필요한 코드를 분리해서 단일책임원칙을 지킬 수 있게 됩니다.</p>
<h1 id="intercepter">Intercepter?</h1>
<p>NestJs에는 인터셉터라는 개념이 존재합니다.</p>
<p>라우트핸들러가 있을때 인터셉터를 적용하면 라우트핸들러 실행 전, 후로 실행되어 생명주기를 관리할 수 있습니다.</p>
<p>트랜잭션 인터셉터는 다음과 같이 구현할 수 있습니다.</p>
<p><code>return</code> 문 전까지는 핸들러 실행 전에 실행이 되고, <code>next.handle()</code>로 핸들러가 실행되고 <code>pipe</code>를 통해 그 이후의 로직들을 관리할 수 있습니다.</p>
<pre><code class="language-jsx">@Injectable()
export class TransactionInterceptor implements NestInterceptor {
  constructor(private readonly dataSource: DataSource) {}

  async intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Promise&lt;Observable&lt;any&gt;&gt; {
    const request = context.switchToHttp().getRequest();

    const qr = this.dataSource.createQueryRunner();

    await qr.connect();

    await qr.startTransaction();

    request.queryRunner = qr;

    return next.handle().pipe(
      catchError(async (e) =&gt; {
        await qr.rollbackTransaction();
        await qr.release();
        throw new TransactionRollback();
      }),
      tap(async () =&gt; {
        await qr.commitTransaction();
        await qr.release();
      }),
    );
  }
}</code></pre>
<p>꽤나 괜찮아보이는지만 한 가지 문제점이 있습니다.</p>
<p>인터셉터는 컨트롤러 레벨에 적용된다는 것입니다.</p>
<p>실제로 트랜잭션이 적용되어야할 컨트롤러 함수에는 트랜잭션 하나로 관리되어야할 코드만 들어간다는 보장이 없습니다.</p>
<p>한 라우트핸들러에 복수 개의 트랜잭션이 필요할 수도 있고, 읽기 쓰기 작업이 혼용되기도 합니다. </p>
<p>저는 쓰기 작업에만 트랜잭션을 적용시키고 싶고요.</p>
<pre><code class="language-jsx">@Get(&#39;github-callback&#39;)
  async githubCallback(
    @Req() req: Request,
    @Res() res: Response,
    @Query(&#39;code&#39;) code: string,
  ) {
    const accessToken = await this.githubService.getGithubAccessToken(code);
    const user: UserDto = await this.githubService.getUserInfo(accessToken);
    let findUser = await this.userService.findUser(user);
    if (findUser === null) {
      await this.userService.addUser(user, &#39;github&#39;);
      findUser = await this.userService.findUser(user);
    }
    const returnTo: string = await this.authService.login(findUser, res, req);

    return res.redirect(returnTo);
  }</code></pre>
<p>그리고 인터셉터로 구현하게 되면 함수의 인자로 쿼리러너를 전달해주고, 서비스 레벨에서 쿼리러너를 이용하려면 서비스레벨에도 쿼리러너를 전달해주어야하는 불편함이 생깁니다.</p>
<h1 id="decorator--asynclocalstorage">Decorator &amp; AsyncLocalStorage</h1>
<p>위의 불편함들 때문에 서비스 레벨에 적용시킬 수 있는 데코레이터를 만들어 트랜잭션을 관리하고 싶어졌습니다.</p>
<p>이것을 구현하기까지 두 가지 어려움이 있었습니다.</p>
<ol>
<li>NestJs 라이프 사이클에 맞는 데코레이터 만들기</li>
<li>데코레이터에서 생성한 쿼리러너를 서비스함수에서도 이용하기</li>
</ol>
<h3 id="nestjs-라이프-사이클에-맞는-데코레이터-만들기">NestJs 라이프 사이클에 맞는 데코레이터 만들기</h3>
<p>NestJS는 싱글톤으로 인스턴스들을 관리해 앱이 초기화될 때 모든 클래스들이 초기화됩니다.</p>
<p>express에서 MethodDecorator를 구현할 때와 약간 동작에 차이점이 있었습니다.</p>
<p>데코레이터로 메타데이터 정보를 입력하고, </p>
<p>NestJS 라이프 사이클에 따라 클래스들이 초기화 되고 그 후 <code>DiscoverService</code>로 등록된 인스턴스들을 탐색하고 필터링하여 트랜잭션 적용이 필요한 곳을 찾아 원래함수를 wrapping해주어 구현하였습니다.</p>
<pre><code class="language-jsx">// transaction.module.ts
import { Module } from &#39;@nestjs/common&#39;;
import { DiscoveryModule } from &#39;@nestjs/core&#39;;
import { TransactionService } from &#39;./transaction.service&#39;;

@Module({
  imports: [DiscoveryModule],
  providers: [TransactionService],
})
export class TransactionModule {}</code></pre>
<pre><code class="language-jsx">// transaction.decorator.ts
export const TRANSACTIONAL_KEY = Symbol(&#39;TRANSACTION&#39;);
export type ORM = &#39;typeorm&#39; | &#39;mongoose&#39;;
export function Transactional(orm: ORM): MethodDecorator {
  return applyDecorators(SetMetadata(TRANSACTIONAL_KEY, orm));
}</code></pre>
<p>프로젝트에서 typeorm과 mongoose를 사용해서 데코레이터 옵션을 통해 orm 타입을 받아주었습니다.</p>
<pre><code class="language-jsx">// transaction.service.ts
@Injectable()
export class TransactionService implements OnModuleInit {
  constructor(
    private readonly discoveryService: DiscoveryService,
    private readonly metadataScanner: MetadataScanner,
    private readonly reflector: Reflector,
    private readonly dataSource: DataSource,
    @InjectConnection() private readonly connection: Connection,
  ) {}

  onModuleInit(): any {
    const providers = this.discoveryService.getProviders();

    const instances = providers
      .filter((v) =&gt; v.isDependencyTreeStatic())
      .filter(({ metatype, instance }) =&gt; {
        return !(!metatype || !instance);
      })
      .map(({ instance }) =&gt; instance);

    instances.map((instance) =&gt; {
      const names = this.metadataScanner.getAllMethodNames(
        Object.getPrototypeOf(instance),
      );
      for (const name of names) {
        const originalMethod = instance[name];
        const metadata = this.reflector.get&lt;ORM&gt;(
          TRANSACTIONAL_KEY,
          originalMethod,
        );
        switch (metadata) {
          case &#39;typeorm&#39;:
            instance[name] = this.typeormTransaction(originalMethod, instance);
            return;
          case &#39;mongoose&#39;:
            instance[name] = this.mongooseTransaction(originalMethod, instance);
        }
      }
    });
  }
... (생략)
}</code></pre>
<h3 id="데코레이터에서-생성한-쿼리러너를-서비스함수에서도-이용하기">데코레이터에서 생성한 쿼리러너를 서비스함수에서도 이용하기</h3>
<p>데코레이터로 함수를 조작해줄 때 인자값을 더해주지 않고 동일한 쿼리러너를 서비스함수에서도 이용할 수 있어야합니다.</p>
<p>자바에서는 이를 ThreadLocal로 구현할 수 있습니다. 스레드 영역에 변수를 할당해 특정 스레드에서 실행되는 모든 코드에서 그 변수에 접근이 가능합니다.</p>
<p>하지만 자바스크립트는 싱글스레드인데요?</p>
<p>필요한 모든 건 항상 만들어져있습니다…</p>
<p><strong>AsyncLocalStorage</strong>라는 것을 통해 비동기작업이 일어나는 동안에 접근가능한 storage를 만들 수 있습니다.</p>
<p>데코레이터에서 여기에 쿼리러너를 저장하고, 서비스 함수에서 이 storage영역에서 쿼리러너를 읽어와 사용할 수 있습니다.</p>
<pre><code class="language-jsx">// transaction.service.ts
typeormTransaction(originalMethod, instance) {
    const dataSource = this.dataSource;
    return async function (...args: any[]) {
      const qr = await dataSource.createQueryRunner();

      await queryRunnerLocalStorage.run({ qr }, async function () {
        try {
          await qr.startTransaction();
          const result = await originalMethod.apply(instance, args);
          await qr.commitTransaction();
          return result;
        } catch (e) {
          await qr.rollbackTransaction();
          this.logger.error(e);
          throw new TransactionRollback();
        } finally {
          await qr.release();
        }
      });
    };
  }

  mongooseTransaction(originalMethod, instance) {
    const connection = this.connection;
    return async function (...args: any[]) {
      const session: ClientSession = await connection.startSession();

      const result = await sessionLocalStorage.run(
        { session },
        async function () {
          try {
            await session.startTransaction();
            const result = await originalMethod.apply(instance, args);
            await session.commitTransaction();
            return result;
          } catch (e) {
            await session.abortTransaction();
            throw new TransactionRollback();
          } finally {
            await session.endSession();
          }
        },
      );
      return result;
    };
  }</code></pre>
<pre><code class="language-jsx">// transaction.decorator.ts
export function getLocalStorageRepository&lt;T extends ObjectLiteral&gt;(
  target,
): Repository&lt;T&gt; {
  const queryRunner = queryRunnerLocalStorage.getStore();
  return queryRunner?.qr?.manager.getRepository(target);
}
export function getSession(): ClientSession {
  const session = sessionLocalStorage.getStore();
  return session?.session;
}</code></pre>
<h3 id="service에-적용">Service에 적용</h3>
<p>이제 서비스 클래스에서 트랜잭션을 적용하고자 하는 함수에 위의 데코레이터만 작성해주면 트랜잭션이 적용됩니다.</p>
<pre><code class="language-jsx">@Transactional(&#39;typeorm&#39;)
@Transactional(&#39;mongoose&#39;)</code></pre>
<p>쓰기 작업에만 트랜잭션이 적용되는 모습</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/2a80dd65-4ab4-4478-88ea-c8c6aa2bea54/image.png" alt=""></p>
<h1 id="코드-길이-비교">코드 길이 비교</h1>
<p>트랜잭션 관심사분리를 통해서 코드를 많이 줄일 수 있었습니다.</p>
<h2 id="typeorm">TypeORM</h2>
<h3 id="before">Before</h3>
<pre><code class="language-jsx">async addUser(userDTO: UserDto, oauth: OAUTH) {
  const user = new UserEntity();
  user.name = userDTO.name;
  user.authServiceID = userDTO.authServiceID;
  user.oauth = oauth;

  const qr = this.getQueryRunner();
  try {
    await qr.connect();
    await qr.startTransaction();
    await qr.manager.save&lt;UserEntity&gt;(user);
    await qr.commitTransaction();
  } catch (e) {
    console.log(e);
    await qr.rollbackTransaction();
    throw new TransactionRollback();
  } finally {
    await qr.release();
  }
}</code></pre>
<h3 id="after">After</h3>
<pre><code class="language-jsx">@Transactional(&#39;typeorm&#39;)
  async addUser(userDTO: UserDto, oauth: OAUTH) {
    const user = new UserEntity();
    user.name = userDTO.name;
    user.authServiceID = userDTO.authServiceID;
    user.oauth = oauth;
    const repository = getLocalStorageRepository(UserEntity);
    await repository.save&lt;UserEntity&gt;(user);
  }</code></pre>
<h2 id="mongoose">Mongoose</h2>
<p>특히 쓰기 작업이 필요한 곳이 더 많을수록 더 빛을 발했습니다. 
지금보다 DB 작업이 많은 프로젝트라면 더 큰 효과를 보일 것입니다.</p>
<h3 id="before-1">Before</h3>
<pre><code class="language-jsx">async save(saveCodeDto: SaveCodeDto): Promise&lt;Code&gt; {
    const session = await this.connection.startSession();
    session.startTransaction();
    try {
      const code = await this.codeModel.create(saveCodeDto);
      await session.commitTransaction();
      return code;
    } catch (e) {
      await session.abortTransaction();
      this.logger.error(e);
      throw new TransactionRollback();
    } finally {
      await session.endSession();
    }
  }

  async update(userID: number, objectID: string, saveCodeDto: SaveCodeDto) {
    const query = { userID: userID, _id: objectID };
    const session = await this.connection.startSession();
    session.startTransaction();
    try {
      const result = await this.codeModel.updateOne(query, saveCodeDto);
      await session.commitTransaction();
      return result;
    } catch (e) {
      await session.abortTransaction();
      this.logger.error(e);
      throw new TransactionRollback();
    } finally {
      await session.endSession();
    }
    return;
  }

  async delete(userID: number, objectID: string) {
    const query = { userID: userID, _id: objectID };
    const session = await this.connection.startSession();
    session.startTransaction();
    try {
      await this.codeModel.deleteOne(query);
      await session.commitTransaction();
    } catch (e) {
      await session.abortTransaction();
      this.logger.error(e);
      throw new TransactionRollback();
    } finally {
      await session.endSession();
    }
  }</code></pre>
<h3 id="after-1">After</h3>
<pre><code class="language-jsx">@Transactional(&#39;mongoose&#39;)
  async save(saveCodeDto: SaveCodeDto) {
    const session = getSession();
    const code = await this.codeModel.create([saveCodeDto], {
      session: session,
    });
    return code[0];
  }

  @Transactional(&#39;mongoose&#39;)
  async update(userID: number, objectID: string, saveCodeDto: SaveCodeDto) {
    const query = { userID: userID, _id: objectID };
    const session: ClientSession = getSession();
    const result = await this.codeModel
      .updateOne(query, saveCodeDto)
      .session(session);
    return result;
  }

  @Transactional(&#39;mongoose&#39;)
  async delete(userID: number, objectID: string): Promise&lt;DeleteResult&gt; {
    const query = { userID: userID, _id: objectID };
    const session = getSession();
    return this.codeModel.deleteOne(query).session(session);
  }</code></pre>
<p><code>codes.service.ts</code>에서 import문을 제외했을때 67 lines이던 코드가 40 lines로 줄어들었습니다. (40% 감소)</p>
<hr>
<p>참고</p>
<p><a href="https://tecoble.techcourse.co.kr/post/2021-06-25-aop-transaction/">https://tecoble.techcourse.co.kr/post/2021-06-25-aop-transaction/</a></p>
<p><a href="https://toss.tech/article/nestjs-custom-decorator">https://toss.tech/article/nestjs-custom-decorator</a></p>
<p><a href="https://www.freecodecamp.org/news/async-local-storage-nodejs/">https://www.freecodecamp.org/news/async-local-storage-nodejs/</a></p>
<p><a href="https://nodejs.org/api/async_context.html#class-asynclocalstorage">https://nodejs.org/api/async_context.html#class-asynclocalstorage</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[부스트캠프 웹・모바일 그룹프로젝트 4,5주차 회고]]></title>
            <link>https://velog.io/@hee_kyoung11/%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%E3%83%BB%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B7%B8%EB%A3%B9%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-45%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hee_kyoung11/%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%E3%83%BB%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B7%B8%EB%A3%B9%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-45%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 11 Dec 2023 05:12:40 GMT</pubDate>
            <description><![CDATA[<p>벌써 6주차가 시작되어버렸다..!
뭔가 초반에 빠른 기능구현을 목적으로 달렸더니,
기능 구현을 어느정도 마무리하고 나니 지쳐버렸다.
2주나 회고를 건너뛸 뻔 했지만, 비록 지금 6주차 월요일이지만 2주간의 회고를 해보려한다</p>
<h1 id="keep">Keep</h1>
<p>기능 구현을 빨리 끝내다보니까 여러 예외를 처리해보기도 하고 추가 기능 구현, 성능 향상 등 팀원들 각자 하고 싶었던 것들을 해보고 있어 전체적으로 프로젝트가 풍성해지고 퀄리티가 높아지는 것 같다.
(코드 실행 기능 부하테스트를 하고 결과를 정리하고 있는데 나 논문쓰는줄 알았다. 물론 그 수준은 아니지만!!)</p>
<p>지원하는 언어도 6개로 늘릴 수 있었고, 사실 큰 기능 구현을 한 건 아니지만 눈으로 보이는 결과가 있어서 제일 뿌듯했다.
생각보다 각 언어 환경을 구축하는데 애를 먹기도 했고
<img src="https://velog.velcdn.com/images/hee_kyoung11/post/73b8d30f-4e1d-4093-9325-1113353d30fd/image.png" alt="">
그래서 신나서 슬랙에 자랑했다~~</p>
<p>그리고 데이터구조가 복잡하지 않는 프로젝트 특성상 트랜잭션 구현이 그렇게 우선순위가 높지 않았다.
ORM에서 제공하는 트랜잭션으로도 충분했고, 조인도 없어서 쿼리에도 문제가 없었다.
하지만 시간이 생기다보니 AsyncLocalStorage를 이용해서 Service레벨에 적용할 수 있는 데코레이터를 만들어보고 싶었고, 다른 캠퍼들의 조언을 듣고 구현도 해보고 싶어졌다. (6주차에 꼭 할거다.)
이렇게 여러가지 시도해 볼 수 있는게 많아서 좋다.</p>
<p>5주차에는 다른 캠퍼들과 서로의 서비스를 직접 이용해보고 피드백을 남겨주는 시간을 가졌다.
많지는 않지만 실사용자를 가져보는 것이 왜 중요한지 알 수 있었다.
개발하면서 아무리 테스트해도 알 수 없었던 문제들, 미쳐 생각하지 못했던 문제들, 사용자의 요구사항 등을 알 수 있었고 다시 활력을 찾아 열심히 문제해결, 개발을 했다.</p>
<h1 id="problem">Problem</h1>
<p>앞에서도 말했듯이 다 좋은데 빨리 지쳐버린게 문제였다.
해야할 과제가 눈앞에서 사라져버리다보니 내가 찾아서, 굳이굳이 만들어서 해야하는데 이게 쉽지 않았다.
그래서 &#39;오랜 호흡으로 가져가는 프로젝트에 참여하게 된다면 어떤 자세로 임해야할까?&#39;, &#39;꾸준한 태도를 가지는 개발자가 되려면 어떻게 해야할까?&#39;를 고민하게 된 것 같다.</p>
<p>그리고 사실 데모에서도 문제가 있었다.
데모하는 주차에 추가로 끼워넣은 기능들에 충분한 예외처리를 하지 않아서 데모하는데 예외가 많이 발생했다.
원래 되던건데 안되니까 좀 당황스럽기도 했고, 새 기능을 추가하면서 저런거 체크도 안했구나 하는 사실에 좀 반성하기도 했다.</p>
<h1 id="try">Try</h1>
<p>프로젝트가 끝나가니까 앞으로의 교훈으로 남겨지는 것들이 많은  것 같다.</p>
<ul>
<li>아무리 간단한 기능이라도 변경이 있을 땐 모든 예외를 고려하기</li>
<li>테스트 코드 잘 짜기</li>
<li>한 번에 모든 힘을 쏟아 붓지 않기, 주차별 목표를 계획해서 긴 호흡으로 꾸준한 모습 보이기.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[WebSocket을 이용한 코드 실행 요청을 처리해보자]]></title>
            <link>https://velog.io/@hee_kyoung11/WebSocket%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%BD%94%EB%93%9C-%EC%8B%A4%ED%96%89-%EC%9A%94%EC%B2%AD%EC%9D%84-%EC%B2%98%EB%A6%AC%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@hee_kyoung11/WebSocket%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%BD%94%EB%93%9C-%EC%8B%A4%ED%96%89-%EC%9A%94%EC%B2%AD%EC%9D%84-%EC%B2%98%EB%A6%AC%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 30 Nov 2023 06:08:08 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/f651bbfd-68ab-4f70-86f7-68a13b09671d/image.png" alt=""></p>
<p><a href="https://velog.io/@hee_kyoung11/%EC%9E%91%EC%97%85%EC%9D%B4-%EC%98%A4%EB%9E%98%EA%B1%B8%EB%A6%AC%EB%8A%94-%EC%9A%94%EC%B2%AD%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9D%91%EB%8B%B5%ED%95%A0%EA%B9%8C">작업이 오래걸리는 요청을 어떻게 응답할까?</a></p>
<p>위 글에 이어서 팀원들의 의견과 멘토님들의 의견을 모아 소켓을 이용한 방식으로 코드 실행 요청을 처리해보기로 했습니다.</p>
<p>코드 실행 요청 플로우는 다음과 같습니다.</p>
<blockquote>
</blockquote>
<ol>
<li>코드 실행 버튼을 클릭합니다.</li>
<li>api 서버에 소켓이 연결됩니다.</li>
<li>소켓 연결이 완료되면 소켓 아이디 값을 반환 받아 http POST 요청을 보냅니다.<br> 이때 서버는 해당 서버에 연결된 소켓 정보를 Map 형태로 저장합니다.<br> <code>Map&lt;SocketID, Socket&gt;</code></li>
<li>http 요청을 받으면 코드 유효성 검사를 실시합니다.
 유효성 검사에 통과하지 못하면 403에러를 통과하면 202 코드를 반환합니다.</li>
<li>유효성 검사를 마치고 해당 코드 실행 요청 정보를 <strong>Redis MQ</strong>에 넣습니다.</li>
<li>running 서버에서는 Redis MQ에 job이 들어왔다는 이벤트가 발생하면 consume하여 해당 job 정보를 이용해서 코드를 실행합니다.</li>
<li>코드 실행이 완료되면 그 결과를 <strong>Redis에 Pub</strong> 합니다.</li>
<li>api 서버는 <strong>Redis를 Subcribe</strong>하고 있으며, 메세지를 받으면 메세지 정보에 포함된 소켓아이디가 아까 저장해두었던 Map 구조에 있는지 확인하고, 있다면 소켓 모듈에 해당 소켓 아이디의 코드 실행 요청이 완료되었다는 이벤트를 발생시킵니다.</li>
<li>소켓 모듈에서 코드 실행 요청 완료 이벤트를 수신하면 Map 구조에서 소켓을 찾아 해당 소켓으로 완료 결과를 보냅니다.</li>
<li>클라이언트는 연결해 둔 소켓으로 코드 실행 결과를 받은 후 소켓 연결을 해제합니다.</li>
</ol>
<p>위의 로직으로 코드 실행 기능을 구현했고, 로컬에서 잘 작동해서 배포를 했습니다.</p>
<p><a href="https://github.com/boostcampwm2023/web05-AlgoITNi/pull/115">[BE] feature: 코드 실행 기능 v3 &amp; fix: 로그인 리다이렉션 by HKLeeeee · Pull Request #115 · boostcampwm2023/web05-AlgoITNi</a></p>
<h1 id="이슈다-이슈">이슈다 이슈</h1>
<p>그런데 배포환경에서 이슈가 생겼습니다.</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/329227ea-54be-4c58-bb46-593a5bc854da/image.png" alt=""></p>
<p>api 서버는 2코어이고 pm2로 모든 프로세스를 코어 수 만큼 실행시키고 있습니다.</p>
<p>위에 명시한 코드 실행 기능 프로세스는 소켓 연결과 코드실행 요청을 한 서버에, pm2 환경에서는 같은 프로세스에 되어야 적절한 응답 메세지를 줄 수 있습니다.</p>
<p>하지만 pm2 cluster는 기본적으로 라운드로빈 방식으로 동작하여 항상 같은 프로세스로 연결되게 보장할 수 없습니다.</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/2f6fb1fc-3bce-4bc8-9b01-d32e7ff1e600/image.png" alt=""></p>
<p>실제로 pm2 클러스터를 2개 운영해서 코드 실행 테스트를 해 본 결과 소켓 연결은 0번 프로세스에 연결되었지만 http 요청은 1번 서버로 들어가는 것을 확인 할 수 있었습니다.</p>
<p>소켓을 이용한 코드 실행요청에 개선이 필요했고 두 가지 선택지가 있었습니다.</p>
<blockquote>
<ol>
<li>항상 같은 프로세스로 연결을 유지할 수 있는 방법을 찾기</li>
<li>http 요청을 소켓 통신으로 변경하기</li>
</ol>
</blockquote>
<ol>
<li><p><strong>항상 같은 프로세스로 http 요청을 유지하기</strong></p>
<p> 0번 클러스터에 소켓이 연결되었다면 http 요청도 0번 클러스터로 연결되게 만들어주는 겁니다.</p>
<p> 서버단위가 아닌 프로세스 단위인 것이 접근하기 어려웠습니다. </p>
<p> 또한 이 방법을 택하기 위해서는 <a href="https://www.notion.so/a0d4cdbbb8154079b73954b234b8c758?pvs=21">시그널링 서버를 다중 소켓 환경으로 구성했던 것</a>처럼 연결 전 어떤 서버로 연결될 지 지정하는 별도의 트래픽 관리서버가 필요할 것 입니다.</p>
</li>
<li><p><strong>http 요청을 소켓 통신으로 변경하기</strong></p>
<p> 코드 실행 요청에 소켓을 도입했음에도 http 요청으로 코드 실행을 했던 이유는 코드 실행 요청이 accept 되었음을 코드 실행 결과가 나오기 이전에도 전달하기 위함이었습니다.</p>
<blockquote>
<p><strong><code>202 Accepted</code></strong> 는 요청이 처리를 위해 수락되었으나, 아직 해당 요청에 대해 처리 중이거나 처리 시작되지 않았을 수 있다는 것을 의미합니다.
 <a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Status/202">https://developer.mozilla.org/ko/docs/Web/HTTP/Status/202</a></p>
</blockquote>
<p> 단순히 요청이 수락되었음을 위해 Http 요청을 보내기로 결정했다면, 소켓 통신으로 방식을 변경했을때 요청이 수락됨을 표시할 수 없을까요? </p>
<p> 그렇지 않다고 생각합니다. 코드 실행 요청이 실행되고 MQ에 넣었다는 것을 별도의 소켓 이벤트로 만들어 클라이언트에 알려줄 수 있어 충분히 대체제로 선택할 수 있습니다.</p>
</li>
</ol>
<p>한 가지를 결정하기 전에 2가지 선택지를 모두 시도해보았습니다.</p>
<p>pm2로 소켓 연결을 할 때 <code>@socket.io/pm2</code> 를 설치해 사용할 수 있다는 공식문서를 발견해서 시도해보았습니다.</p>
<p><a href="https://socket.io/docs/v4/pm2/">Usage with PM2 | Socket.IO</a></p>
<p>공식문서에 설명이 짧아 정확히 이 패키지가 어떤 역할을 할 수 있는지 파악하기 어려웠습니다.</p>
<p>websocket을 지원하지 않는 브라우저에서 polling으로 연결할 때, 그러니까 소켓 연결 시에만 sitkcy session을 지원해주는지, 
or 소켓이 연결된 이후에도 sticky session을 지원해주는지 파악하기 어려웠습니다. </p>
<p>전자의 경우라면 위의 패키지를 사용할 필요가 없습니다.  최신브라우저는 websocket을 지원하지 않는 경우를 찾아보기가 더 힘드니까요. </p>
<p>WebSocket MDN : <a href="https://developer.mozilla.org/ko/docs/Web/API/WebSocket">https://developer.mozilla.org/ko/docs/Web/API/WebSocket</a></p>
<p>그런데 <a href="https://github.com/socketio/socket.io-sticky">@socket.io/sticky github</a>에서<br><code>this package is not needed if you only use WebSockets (which might be a sensible choice as of 2021)</code><br>이런 메세지를 찾아볼 수 있었습니다.</p>
<p>이 메세지를 통해 이 패키지는 전자의 경우의 소켓 연결을 돕기위해 필요한 패키지가 아닐까?라고 유추해 볼 수 있었습니다.</p>
<p><strong>정확히 <code>@socket.io/pm2</code> 이 패키지의 역할을 아시는 분이 있다면 알려주시면 감사하겠습니다!</strong></p>
<p><del><em>지원 범위를 실제로 확인하는 것은 쉽지않았습니다. NestJS에 적용하는 예제를 찾아보기 힘들어 정상 구현이 어려웠습니다ㅜㅠ</em></del></p>
<p>오랜 탐색 끝에 1번 방법을 구현하기 어렵다 판단해 2번 방법을 채택하기로했습니다.</p>
<p>2번 방법도 현재 배포환경에서는 크게 문제가 없습니다. (어떤 예상하지 못한 문제들이 발생할 수 있을까요?)</p>
<p>변경한 코드 실행로직은 다음과 같습니다.</p>
<blockquote>
</blockquote>
<ol>
<li>코드 실행 버튼을 클릭합니다.</li>
<li>api 서버에 소켓이 연결됩니다.
 이때 서버는 해당 서버에 연결된 소켓 정보를 Map 형태로 저장합니다.
 <code>Map&lt;SocketID, Socket&gt;</code></li>
<li>소켓 연결이 완료되면 코드 실행 요청 이벤트를 보냅니다.</li>
<li>서버에서 이벤트를 받으면 코드 보안 검사를 실행하고 실패시 바로 소켓 응답을 주고 클라이언트에서 소켓을 종료합니다.
 유효성 검사를 통과하면 그 이후 로직은 동일합니다.</li>
</ol>
<p><a href="https://github.com/boostcampwm2023/web05-AlgoITNi/pull/165">[BE] feature : 코드 실행 요청 v3 - 소켓만 이용 by HKLeeeee · Pull Request #165 · boostcampwm2023/web05-AlgoITNi</a></p>
<h1 id="결과-확인">결과 확인</h1>
<p>코드 실행 기능 테스트를 위해서 로컬 환경을 구축해두고 이용하고 있는데요,</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/9d99eb3f-942a-466b-860d-c54ad0248124/image.png" alt=""></p>
<p>api 서버를 2개 클러스터로 켜두고 테스트를 해보면</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/a011c247-06c3-4869-833f-88f2c54a7b22/image.png" alt=""></p>
<p>잘 작동하는 것을 확인할 수 있었습니다 🥳
(gif가 안올라가네요..)</p>
<p>이제 성능테스트만이 남았습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[부스트캠프 웹・모바일 그룹프로젝트 3주차 회고]]></title>
            <link>https://velog.io/@hee_kyoung11/%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%E3%83%BB%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B7%B8%EB%A3%B9%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hee_kyoung11/%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%E3%83%BB%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B7%B8%EB%A3%B9%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 26 Nov 2023 16:18:48 GMT</pubDate>
            <description><![CDATA[<p>하하.. 벌써 12시가 지나 새로운 주의 시작인 월요일이 되어버려 조금 늦었지만 3주차에 대한 회고를 짧게 적어보려 한다!
이번 주는 몸상태가 별로 안좋아서 조금 힘들었다 😢
집에만 있는데 감기몸살은 왜 걸린 건지... 너무 무리했나?
<img src="https://velog.velcdn.com/images/hee_kyoung11/post/9d7c446e-7208-4553-8f5c-71ef0bf6f33e/image.png" alt="눈물줄줄"></p>
<h1 id="이번-주에-한-일">이번 주에 한 일</h1>
<ul>
<li>github oauth</li>
<li>회원 기능</li>
<li>mongo db 연결<ul>
<li>코드 저장관련 CRUD</li>
</ul>
</li>
<li>로그인 완료 후 원래 있던 위치로 리다이렉션</li>
<li>코드 실행 기능 v3 (소켓, pub/sub)</li>
</ul>
<p>이번 주는 주로 회원관련 기능을 개발했다.
예전부터 해야하는데~ 생각만 했던 몽고디비도 간단하게나마 다루어볼 수 있어서 좋았다. DB라면 RDB만 다루어보아 딱딱하게만 느껴졌는데 NoSQL을 처음 접하고 난 후 조금은 &quot;힙&quot;한 DB 같이 느껴졌고 아주 맘에 들었다!</p>
<h1 id="keep">Keep</h1>
<p>어떤 기능을 개발할 때 실제 어떻게 구현해야할까? 고민이 들면 실제 운영하고 있는 서비스에 찾아들어가 네트워크 탭을 살피고 코드도 살피고 있다.
이 부분이 요즘 가장 잘 하고 있는 부분이 아닐까한다.
코드 실행 기능을 어떻게 구현할 지는 구름과 프로그래머스를 참고했고 로그인 완료 후 원래 위치로 리다이렉션은 벨로그를 참고했다.
벨로그가 여러모로 좋은게 일단 오픈소스이고 백엔드도 javascript로 되어있어 쉽게 이해할 수 있다.</p>
<h1 id="problem">Problem</h1>
<p>생활하는 측면에서 크게 두 가지 &quot;real&quot; problem이 있다.</p>
<ul>
<li>자는데 코딩하는 꿈을 꾼다...</li>
<li>개발 중에 해결 못한 문제가 있으면 잠이 안온다...</li>
</ul>
<p>이번 주에 몸도 안좋고 김장하러 시골에 가게 되서 잘 쉬자하고 일찍부터 자는데도 꿈에서 코딩하니까 쉬는 느낌이 안든다.
예전부터 나는 취직하면 워커홀릭이 될거같아~라는 생각이 종종들었는데 벌써?</p>
<p>프로젝트할 때와 안할 때를 잘 구분시켜서 생활해야할 것 같다.</p>
<p>우리 팀 모토는 빨리 기능 개발을 쳐내고 나중에 고도화시키자였는데,
대부분의 기능 개발이 완료되고 있는 지금 문제점이 하나, 둘 드러나는 것 같다.
수 많은 엣지케이스들을 처리하지 못하고 배포 상태에서 에러가 난다.
또 기능이 될 때도 있고 안될 때도 있다.</p>
<h1 id="try">Try</h1>
<p>어떻게 개선해 볼 수 있을까?</p>
<ol>
<li><p>나만의 코어 수면 시간 정하기
암묵적으로 늦어도 3~4시 쯤에는 잠드는 것 같은데 4시는 그 다음 날에 무리가 되는 것 같다.
남은 일이 있다면 2시 반까지만 하고 뒤도 돌아보지 않고 3시에는 잠드는게 어떻까?</p>
</li>
<li><p>엣지케이스들을 팀원들과 많이 생각해보고 문서화해두고 하나하나 체크해가면서 개발하기?
✚ 테스트 코드 작성하기
이번 주부터는 기능의 디테일에 살리는 것을 목표로 하고 이런 엣지케이스들에 대해서 자세히 논의해보기로 했다. 논의한 내용을 토대로 체크리스트를 만들어서 체크해가면서 개발하는 건 어떨까?
또 테스트 코드 도입에 필요성을 절실히 느끼고 있는데, 일단 당장 작성가능한 테스트 코드에 대해서 하나씩 작성해보면서 경험을 키워가는게 필요할 것 같다.
팀원 분이랑 스터디하는 건 어떠냐고 물어봐야지! </p>
</li>
</ol>
<hr>
<p>추가로 이제 한 주의 시작이니까..!</p>
<h1 id="to-do">TO DO</h1>
<h2 id="chore">chore</h2>
<ul>
<li>실제 현업에서도 배포환경과 똑같은 테스트환경(서버 같은 경우)을 만들어두고 개발하는지 질문</li>
<li>구글 로그인 완성</li>
<li>OAuth 문서화</li>
<li>러닝서버 v3 문서화</li>
<li>NestJS test 강의 </li>
</ul>
<h2 id="main-task">main task</h2>
<ul>
<li>소켓 부하 테스트 </li>
<li>예외 처리</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[서버와 클라이언트 간 쿠키 주고받기]]></title>
            <link>https://velog.io/@hee_kyoung11/%EC%84%9C%EB%B2%84%EC%99%80-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EA%B0%84-%EC%BF%A0%ED%82%A4-%EC%A3%BC%EA%B3%A0%EB%B0%9B%EA%B8%B0</link>
            <guid>https://velog.io/@hee_kyoung11/%EC%84%9C%EB%B2%84%EC%99%80-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EA%B0%84-%EC%BF%A0%ED%82%A4-%EC%A3%BC%EA%B3%A0%EB%B0%9B%EA%B8%B0</guid>
            <pubDate>Thu, 23 Nov 2023 11:54:01 GMT</pubDate>
            <description><![CDATA[<p>로그인 기능을 개발하던 중 포트가 다른 두 서버를 띄워 테스트를 해보려했다. 그때 서로 쿠키가 전송되지 않는 문제를 발견했다!</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/3f2c1d36-54de-4058-8083-b51cde4df391/image.png" alt=""></p>
<p>3000번 포트에 접속한 위치에는 쿠키가 있는데</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/8408b136-4129-4d1b-b14e-c622c1ce2012/image.png" alt=""></p>
<p>4000번 포트로 돌고있는 서버에는 쿠키가 전송되지 않았다.</p>
<p>console.log로 쿠키를 출력해보았을 때 object null prototype이 출력된 것을 통해 확인해보았다.</p>
<p>이 때 쿠키 설정은 httpOnly이외에 모두 default였다.</p>
<h1 id="왜-보내지지-않았을까">왜 보내지지 않았을까?</h1>
<ol>
<li><p>도메인이 달라서?</p>
<p> sameOrigin 정책과 쿠키에 사용되는 sameSite 정책은 다르다.</p>
<blockquote>
<p>GPT한테 두 정책의 차이점을 물어봤다</p>
<h3 id="samesite"><strong>SameSite:</strong></h3>
<p><strong><code>SameSite</code></strong>는 쿠키의 속성 중 하나로, 웹 브라우저가 어떤 상황에서 쿠키를 전송할 것인지를 제어하는 역할을 합니다.</p>
<ul>
<li><strong>SameSite=None:</strong> 이 옵션은 모든 상황에서 쿠키가 전송되도록 허용합니다. 이때는 <strong><code>Secure</code></strong> 속성도 함께 설정되어야 하며, HTTPS 연결을 사용하는 경우에만 적용 가능합니다.</li>
<li><strong>SameSite=Lax:</strong> 기본값이며, 타 사이트로의 요청에서 쿠키를 전송하지 않습니다. 하지만, <strong><code>&lt;a&gt;</code></strong> 태그를 클릭하여 타 사이트로 이동하는 경우에는 쿠키가 전송됩니다.</li>
<li><strong>SameSite=Strict:</strong> 모든 상황에서 쿠키를 전송하지 않습니다. 타 사이트로의 요청에 대해서도 쿠키가 전송되지 않습니다.</li>
</ul>
<h3 id="sameorigin"><strong>SameOrigin:</strong></h3>
<p><strong><code>SameOrigin</code></strong>은 웹 보안 정책 중 하나로, 동일한 출처에서만 리소스를 요청할 수 있도록 하는 규칙입니다.</p>
<ul>
<li><strong>Same-Origin Policy:</strong> 이 정책은 기본적으로 다른 출처(도메인, 포트, 프로토콜)에서의 리소스 요청을 차단합니다. 이는 웹 애플리케이션이 특정 출처의 스크립트나 데이터를 불러오는 것을 막아서 보안을 강화합니다.</li>
<li><strong>Cross-Origin Resource Sharing (CORS):</strong> Same-Origin Policy를 우회하기 위해 사용되는 메커니즘이며, 서버에서 특정 출처로의 요청을 허용하도록 설정하는 것입니다.</li>
</ul>
<p>요약하면, <strong><code>SameSite</code></strong>는 쿠키의 전송을 제어하고, <strong><code>SameOrigin</code></strong>은 웹 리소스의 출처에 대한 보안을 강화하는 정책입니다. 두 가지 개념은 서로 다른 측면에서 웹 보안을 다루고 있습니다.</p>
</blockquote>
<p> Samesite는  하위 도메인까지도 Samesite로 간주한다.</p>
<p> 우리의 도메인으로 예를 들면 algoitni.site와 api.algoitni.site는 SameSite로 쿠키 전송이 가능하다.</p>
<p> 로컬에서 테스트하는 경우는 도메인이 같으므로 이 문제는 아니다.</p>
</li>
<li><p>Cookie 설정할 때 samesite 옵션을 지정해주지 않으면 default를 Lax로 동작한다.</p>
<p> setCookie의 samesite 옵션에는 none, Lax, strict가 있다. 위에 GPT가 잘 설명해줬다.</p>
<p> 현재 상태는 Lax이다. a 태그로 변경해주니 서버에서 쿠키 정보를 받을 수 있었다.</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/a18b06ee-a301-46ba-9c16-c1095f6658c8/image.png" alt="">
button은 작동하지 않지만</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/0460a317-df93-4f49-93a1-b57fff5c0e6d/image.png" alt="">
a 태그로 보냈을 때 쿠키 정보가 서버에 잘 전송되어 출력되었다.</p>
<p>그래도 이상하다.
localhost는 samesite인데 Lax에 막히다니?
button 태그에 fetch로 요청을 보내고 있어서 fetch가 의심스러웠다.</p>
<ol start="3">
<li><p>fetch?!?!</p>
<p> 아래와 같이 요청을 보내고 있었다.</p>
<pre><code class="language-jsx"> &lt;button type=&quot;button&quot; id=&quot;info&quot;&gt;유저 정보 조회&lt;/button&gt;
 &lt;script&gt;
     document.getElementById(&quot;info&quot;).addEventListener(&quot;click&quot;, ()=&gt;{
         fetch(&quot;http://localhost:4000/auth/profile&quot;)
         .then(res =&gt;  res.json())
         .then(console.log)
     })
 &lt;/script&gt;</code></pre>
<p> <a href="https://developer.mozilla.org/ko/docs/Web/API/fetch">fetch() 전역 함수 - Web API | MDN</a></p>
<p> fetch 요청시 브라우저가 쿠키, 인증서등을 보낼지 credential 옵션으로 설정해줄 수 있는데,</p>
<p> 알고보니 fetch 요청 credential 옵션의 기본값이 “same-origin”이었다.</p>
<p> 로컬 테스트 시 쿠키의 same site 정책에는 위배되지 않지만 fetch 요청의 same-origin 정책에 위배되어 값을 받을 수 없었던 것이다.</p>
</li>
</ol>
<p>fetch 요청의 옵션을 변경해주고 나니 로컬에서 테스트에 성공했다.</p>
<pre><code class="language-jsx">document.getElementById(&quot;info&quot;).addEventListener(&quot;click&quot;, ()=&gt;{
    fetch(&quot;http://localhost:4000/auth/profile&quot;, {credentials: &#39;include&#39;})
   .then(res =&gt;  res.json())
   .then(console.log)
 })</code></pre>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/c49b7f92-c49e-4c53-a2e8-aa00bb0cdef4/image.png" alt=""></p>
<hr>
<p>전 부터 쿠키옵션을 하나씩 적용해보고 있었는데 SameSite로 엄청 삽질한 기념으로 쿠키옵션을 정리해봤다.</p>
<h1 id="쿠키-옵션">쿠키 옵션</h1>
<h3 id="http-only">Http Only</h3>
<p>https only 옵션을 true로 주면 자바스크립트로 조회할 수 없다.</p>
<p>이 옵션을 주면 XSS 공격을 예방할 수 있다.</p>
<blockquote>
<p>XSS(Cross-Site Scripting)
악의적인 용도로 웹에 스크립트를 삽입하는 공격.</p>
</blockquote>
<h3 id="http-secure">Http Secure</h3>
<p>Http Secure 옵션을 true로 주면 Https 통신환경에서만 쿠키가 전송된다.</p>
<p>로컬호스트에서는 예외라고 한다.</p>
<h3 id="samesite-1">SameSite</h3>
<p>같은 Site에서만 쿠키를 이용할지 설정해주며 옵션은 세 가지가 있다.</p>
<p>SameSite 옵션을 통해서 다른 사이트에 우리서버의 쿠키값을 전달하는 것을 방지해 CSRF 공격을 예방할 수 있다.</p>
<blockquote>
<p>CSRF (Cross Site Request Forgery)
사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 하는 공격</p>
</blockquote>
<p>SameSites는 도메인과는 약간 다른 개념인데, 하위 도메인이 같은 경우에도 같은 site로 인정한다.</p>
<p>예를 들면 algoitni.site와 api.algoitni.site는 SameSite 이다.</p>
<ul>
<li><p>None</p>
<p>  Site를 구분하지 않고 항상 쿠키를 같이 보낸다.</p>
<p>  이 옵션을 사용하려면 Http Secure가 ture가 되어있어야한다.</p>
</li>
<li><p>Lax</p>
<p>  별다른 옵션이 없으면 Lax 옵션으로 작동한다.</p>
<p>  안전하다고 판단되는 예외에는 쿠키를 보내고, 그 이외의 경우에는 같은 Site에만 쿠키를 보낸다.</p>
<p>  예외에는 a태크의 href속성으로 보내는 것, 같은 site 간의 GET 메서드 등이 있다.</p>
</li>
<li><p>Strict</p>
<p>  같은 Site에만 쿠키를 보낸다.</p>
</li>
</ul>
<h3 id="max-age-expires">Max-Age, Expires</h3>
<p>쿠키의 유효기간을 설정한다.</p>
<p>express에서는 별도로 설정하지 않으면 session으로 설정된다.</p>
<p>유효기간이 세션인 쿠키는 브라우저가 닫히면 삭제된다.</p>
<p>Expires 옵션은 유효기간을 타임스탬프로 지정해주어야하며</p>
<p>Max-Age 옵션은 초로 유효기간을 지정해준다.</p>
<h3 id="path">Path</h3>
<p>도메인에서 쿠키가 적용될 경로를 지정한다.</p>
<p>만약 쿠키 Path가 /user 인 경우 /post 경로에서는 쿠키를 이용할 수 없다.</p>
<p>express의 기본값은 root(’/’)로 root Path가 적용되면 해당 사이트 모든 경로에서 그 쿠키를 사용할 수 있다.</p>
<p>express의 <code>response.cookie</code> 를 통해서 생성하지 않고 Set-Cookie 헤더로 직접 쿠키를 적용할 때 Path 옵션이 주어져 있지 않으면 그 요청이 들어온 위치로 지정되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로그인 후 요청했던 페이지로 돌아가기]]></title>
            <link>https://velog.io/@hee_kyoung11/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%9B%84-%EC%9A%94%EC%B2%AD%ED%96%88%EB%8D%98-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%A1%9C-%EB%8F%8C%EC%95%84%EA%B0%80%EA%B8%B0</link>
            <guid>https://velog.io/@hee_kyoung11/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%9B%84-%EC%9A%94%EC%B2%AD%ED%96%88%EB%8D%98-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%A1%9C-%EB%8F%8C%EC%95%84%EA%B0%80%EA%B8%B0</guid>
            <pubDate>Tue, 21 Nov 2023 15:07:28 GMT</pubDate>
            <description><![CDATA[<p>보통 웹서비스를 이용하다보면 다음과 같은 상황을 자주 마주한다.</p>
<blockquote>
<p>로그인하지 않고 이용하다가 특정 기능을 이용하면 로그인이 필요해서 로그인창으로 이동되고 로그인을 마치면 원래 다시 작업하던 페이지로 돌아간다.</p>
</blockquote>
<p>어떻게 이 기능을 구현할 수 있을까?</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/09e0beef-7ff4-4d80-9f26-4ea1b0e7043b/image.png" alt="">
<del>나 다시 돌아갈래!!!</del></p>
<p>두 가지를 활용해서 생각보다 쉽게 구현할 수 있었다.</p>
<ul>
<li>request Header에 referer</li>
<li>request session</li>
</ul>
<h1 id="referer-header">Referer Header</h1>
<p>리소스가 요청된 주소 정보를 담고 있는 헤더</p>
<p>이 헤더 정보를 사용하면 서버에서는 이 위치로의 요청이 어디서 발생했는지 알 수 있어서 데이터 분석, 로깅, 캐싱에 활용할 수 있다고 한다.</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer">https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer</a></p>
<h1 id="session">Session</h1>
<p>클라이언트와 서버의 연결 정보를 저장하는 방법</p>
<p>HTTP 프로토콜을 비연결을 지향하는 무상태 프로토콜로 기본적으로 한 번의 요청 사이클이 끝나면 서버에서 클라이언트의 정보를 알 수 없다.</p>
<p>그렇지만 클라이언트의 상태를 서버에서 저장할 필요가 있는데 이때 세션을 활용할 수 있다.</p>
<h1 id="구현하기">구현하기</h1>
<p>개발 중인 서비스는 모든 로그인은 소셜로그인으로 구현하고 있다.</p>
<p>OAuth로 로그인 로직을 구현하는 경우는 웹 서비스가 별도의 로그인 페이지를 가지는 것과 다르게 인증서버, 리소스서버로 리다이렉션이 여러 번 이루어진다.</p>
<p>로그인 페이지에서 모든 로그인 로직이 끝나는 경우라면 Referer 헤더만 가지고도 구현하고자하는 기능을 만들 수 있지만,</p>
<p>여러 번 리다이렉션 요청이 있다면 모든 로그인 로직이 끝나고도 처음 요청이 들어왔던 위치를 기억하고 있어야하기 때문에 세션을 이용하였다.</p>
<p>처음 github 로그인 요청이 들어오면 아래 컨트롤러에서 처리하게 된다.</p>
<pre><code class="language-jsx">@Get(&#39;github&#39;)
  async githubProxy(@Res() res: Response, @Req() req: Request) {
    req.session[&#39;returnTo&#39;] = req.headers.referer || &#39;/&#39;;
    const redirectUrl = await this.githubService.authProxy();
    return res.redirect(redirectUrl);
  }</code></pre>
<p>Request 헤더에서 referer 정보를 꺼내서 세션에 저장해준다.</p>
<p>그리고 브라우저에서 github 로그인이 끝나면 아래 컨트롤러로 리다이렉트 된다.</p>
<pre><code class="language-jsx">@Get(&#39;github-callback&#39;)
  async githubCallback(
    @Req() req: Request,
    @Res() res: Response,
    @Query(&#39;code&#39;) code: string,
  ) {

    /*
        *
        ********  로그인 로직 ******** 
        *
        */ 

    const returnTo = req.session[&#39;returnTo&#39;];
    delete req.session[&#39;returnTo&#39;];

    res.redirect(HttpStatus.NOT_MODIFIED, returnTo);
  }</code></pre>
<p>이때 세션에 저장해두었던 referer 정보를 꺼내서 그 위치로 리다이렉트 응답을 보내면 완성된다.</p>
<p>한 번 사용한 referer 정보는 나중에 재사용될 수 없게 삭제처리했다.</p>
<hr>
<p>참고</p>
<p><a href="https://docs.nestjs.com/techniques/session">Documentation | NestJS - A progressive Node.js framework</a></p>
<hr>
<h1 id="배포-후-문제점">배포 후 문제점</h1>
<p>위와 같이 기능을 개발하고 배포를 했다.</p>
<p>배포 환경은 pm2로 작동시키고 있었다.</p>
<p>원래 서버가 1코어 cpu였으므로 문제가 발생하지 않았는데, 2코어 서버로 업그레이드 하고 난 후 문제가 발생했다 😱</p>
<ol>
<li><p>서로 다른 프로세스간 메모리가 공유되지 않는다.</p>
<p> 현재 옵션으로 세션은 서버의 메모리에 저장되는데 2개의 프로세스에서 앱을 작동시키면 처음 referer 정보를 저장한 서버와 리다이렉션 된 서버가 다르면 referer 정보가 없어 undefind가 발생하고... undefind로 리다이렉트가 되니 당연히! 404 에러가 났다.</p>
<p> 전에 세션 방식을 사용한 로그인을 구현할 때는 이런 상황때문에 세션정보를 저장하는 레디스를 두었다.</p>
<p> 다른 프로세스간 IPC로 정보를 교환할 수 있지만 잘 사용되지 않는 방식이기도 하고 또 다중 프로세스가 아니라 다중 서버 환경이라면 적용할 수 없다.</p>
<p> session store에 redis를 넣어주어 세션정보를 redis에 저장했다.</p>
<pre><code class="language-jsx"> const redisStore = new RedisStore({
     client: new Redis({
       port: configService.get&lt;number&gt;(&#39;REDIS_PORT&#39;),
       host: configService.get&lt;string&gt;(&#39;REDIS_HOST&#39;),
       password: configService.get&lt;string&gt;(&#39;REDIS_PASSWORD&#39;),
     }),
     prefix: &#39;sess:&#39;,
   });

   app.use(cookieParser());
   app.use(
     session({
       store: redisStore,
       secret: configService.get&lt;string&gt;(&#39;SESSION_SECRET&#39;),
       resave: false,
       saveUninitialized: false,
     }),
   );</code></pre>
</li>
<li><p>http referer 정보 없음…</p>
<p> 어디로 돌아갈지 http referer 헤더에서 찾아 세션에 저장해주고 있는데,</p>
<p> http referer 헤더가 들어오지 않는 경우가 있다.  그러면 돌아갈 수 없다.</p>
<p> 에러가 나지 않게 default 값 처리를 해두었지만 원하는 동작이 아니다.</p>
<pre><code class="language-jsx"> @Get(&#39;github&#39;)
   async githubProxy(@Res() res: Response, @Req() req: Request) {
     req.session[&#39;returnTo&#39;] = req.headers.referer || &#39;/&#39;;
     req.session.save((err) =&gt; {
       if (err) this.logger.log(err);
     });
     const redirectUrl = this.githubService.getAuthUrl();
     return res.redirect(redirectUrl);
   }</code></pre>
<p> 생각난 해결 방법</p>
<ol>
<li><p>클라이언트에서 요청시 referer 정보를 항상 넣어서 보내준다.</p>
<pre><code class="language-jsx"> document.getElementById(&quot;local&quot;).addEventListener(&quot;click&quot;, () =&gt; {
            fetch(&quot;http://localhost:4000/auth/github&quot;, {
              method: &#39;GET&#39;,
              headers: {&#39;Referer&#39;: &#39;&quot;http://localhost:3000/&#39;},
              })
             .then(response =&gt; response.json())
             .then(data =&gt; {
                     console.log(data)
             })
              .catch(error =&gt; console.error(&#39;Error:&#39;, error)); 
        })</code></pre>
<p> 하지만 이렇게 요청을 보내면 CORS 에러가 발생한다.</p>
</li>
</ol>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/78d6e17a-035b-4584-a105-7a2a15bb4399/image.png" alt=""></p>
<p>이것을 해결하는 방법은 서버 프록시를 거처 클라이언트에게 로그인페이지를 보내줄 수 있고
서버코드의 변경이 필요하고, 클라이언트도 매번 보낼때마다 fetch 요청을 보내야한다는 번거로움이 있다</p>
<ol start="2">
<li>쿼리파라미터로 referer 정보를 보내준다.</li>
</ol>
<p>괜찮은 방법인 것 같다!</p>
<p> velog는 어떻게 하고 있을까? 찾아보니 쿼리파라미터로 현재 보고있는 페이지의 경로를 적어주고 있었다!</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/fd9a3613-6b0e-4b7c-9f10-987223af80e0/image.png" alt=""></p>
<h1 id="선택한-방법">선택한 방법</h1>
<p>velog의 방식대로 쿼리파라미터로 돌아갈 위치를 입력받기로 했다.</p>
<p>next로 위치를 받고, 그 정보를 세션에 저장해 주었다.</p>
<p>dev는 로컬에서 개발할 때 편의성을 위해 추가한 파라미터이다. next 파라미터 값으로 demo가 들어온다면, false일때<code>algoitni.site/next</code> 로 리다이렉트하고 true 값을 주게되면 <code>localhost:3000/next</code>로 리다이렉트하게 된다. default는 false이다.</p>
<pre><code class="language-jsx">@Get(&#39;github&#39;)
  async githubProxy(
    @Res() res: Response,
    @Req() req: Request,
    @Query(&#39;next&#39;) next: string = &#39;/&#39;,
    @Query(&#39;dev&#39;) dev: boolean = false,
  ) {
    req.session[&#39;returnTo&#39;] = this.getRedirectionPath(dev, next);
    req.session.save((err) =&gt; {
      if (err) this.logger.log(err);
    });
    const redirectUrl = this.githubService.getAuthUrl();
    return res.redirect(redirectUrl);
  }

getRedirectionPath(dev: boolean, next: string) {
    return dev
      ? `http://${path.join(&#39;localhost:3000&#39;, next)}`
      : `https://${path.join(&#39;algoitni.site&#39;, next)}`;
  }</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[부스트캠프 웹・모바일 그룹프로젝트 2주차 회고]]></title>
            <link>https://velog.io/@hee_kyoung11/%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%E3%83%BB%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B7%B8%EB%A3%B9%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hee_kyoung11/%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%E3%83%BB%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B7%B8%EB%A3%B9%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 17 Nov 2023 17:54:43 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/c9882b6a-4a5f-4c46-9a38-3afa73b8983d/image.png" alt=""></p>
<p>벌써 그룹프로젝트 2주차가 끝났고 프로젝트 3분의 1일 끝났다 ㅇㅁㅇ!
이번 주부터 본격적인 개발을 하기 시작한 주였는데, 힘들기도 했지만 재밌었다.
지난 주 회고를 좋았던 점, 아쉬웠던 점을 정리했는데 이게 KPT 회고 방식이랑 유사해서 이번 주에는 KPT 회고를 시도해보겠다!</p>
<p><a href="https://brunch.co.kr/@jinha0802/35">KPT 회고란 무엇인가?</a></p>
<h1 id="이번-주에-한-일">이번 주에 한 일</h1>
<p>KPT 전 간단하게 이번 주에 무엇을 했는지 적어보자</p>
<ul>
<li>서버 https 적용</li>
<li>코드 보안 검사 기능</li>
<li>코드 실행 요청 기능 🌟<ul>
<li>http 요청 방식</li>
<li>메시지 큐 방식</li>
</ul>
</li>
<li>K6 테스트 툴 간단 테스트</li>
</ul>
<p>한 줄 씩 적어보니 되게 적어보이는 건 기분 탓인가...
코드 실행 요청 기능이 성능적으로 개선해보고 실험해 볼 수 있는 포인트가 될 것 같아 계속 고민하며 만들고 있다. 아직은 조금 더 개선이 필요하다!</p>
<h1 id="keep">Keep</h1>
<p>우리 팀은 주로 개발은 각자 집에서 작업하지만 그 때 그 때 의사소통이 잘 되는 것 같다.
게더타운에서 작업하다가 필요하면 이야기 하고 싶은 사람한테 찾아가서 이야기하고,
또 전체 호출을 하기도 한다. 또 다들 카톡도 잘본다 👍</p>
<p>각자 맡은 일이 착착 진행되고 있어서 빠르게 진행할 수 있는 것 같다.
말은 안했지만 데일리 스크럼 때 놀랄 때도 있다.
<del>엥 어제 하루동안 이거 다 했다고요??</del></p>
<p>맡아서 진행한 내용 중에 성능 테스트를 한 것이 있는데
여러 시나리오 별로 임계점 테스트를 하고 실험하는 과정이 재밌었다.
이 과정을 조금 더 체계적으로 이어가면 좋을 것 같다.</p>
<p>마지막으로 멘토님의 조언을 정답처럼 받아들이지 않고 우리 팀만의 의사결정을 한 점이 좋았다.
기능이 너무 많다는 조언을 계속 해주셨는데, 빼고 싶은 기능이 없었다.
그리고 다 할 수 있을 것 같기도 했다.
나는 회의하면서 눈물을 머금고... 뺀다면 채팅기능을 빼도 괜찮지 않을까요? 라는 타협점을 제시했는데 받아들여지지 않았다. 
그런데 오히려 더 좋았다. 
사실 나조차도 욕심내고 있었던 기능이었기 때문이다.
그렇게 우리의 힘을 보여주자며 모든 기능을 가지고 가기로 결정했다.
줏대있게 살아!!!!</p>
<h1 id="problem">Problem</h1>
<p>개발 인원이 혼자가 아님 + 서버가 여러 개로 나눠져있음
이 사실을 간과했던 것 같다.
서로 DTO 형식이나 함수 네이밍 어떻게 할 지 사전에 정한 것 없이 api 서버와 running 서버 개발을 했다. (네이밍 컨벤션에 대한 이야기만 나눴었다)
그리고 나서 api서버와 running 서버 통신 기능을 맡아서 진행하다 보니 헷갈리는 점이 한 두 개가 아니다. (진행형..🥲)
DTO도 다르고 형식도 다르고 네이밍도 헷갈리고...</p>
<p>이런 부분은 코드 리뷰를 통해 맞춰갈 수 있었을까? 
이번 주는 코드 리뷰 후 머지 과정이 조금 생략됐었다. 
아직 우리 팀이 리뷰 문화가 정착되지 않기도 했고 작업 속도를 빠르게 가져가려다 보니 <code>빨리 머지하고 이어서 고칠게요!</code>를 해버렸다.</p>
<p>코드 리뷰 단계에서 자세히 보고 미리 맞췄어야 했나? 라는 생각이 조금 들기도 했지만,
다시 생각해보면 직접 경험해보기 전까지는 이런 포인트에서 문제가 있을 거라는 것을 알지 못했을 것이다.</p>
<h1 id="try">Try</h1>
<p>이번 주 팀회고 시간을 통해서 코드 리뷰나 PR 관련한 이야기도 나왔었다.</p>
<p>사실 팀 회고는 형식적인게 아닌가? 생각이 없지않았는데 
이번 주 팀회고는 굉장히 생산적이었고 좋았다.
그리고 팀원들이 비슷한 결의 고민을 가지고 있었던 것도 신기했다.</p>
<p>논의해 본 결과 작업 순서를 구체화하고 모두가 의식적으로 지키기위해 노력하기로 했다.</p>
<blockquote>
<p><strong><code>제품 백로그</code></strong> →  <strong><code>스프린트 백로그 필터링</code></strong> → <strong><code>이슈</code></strong> → <strong><code>개인별 기능구현</code></strong> → <strong><code>PR작성</code>→ <code>문서화</code></strong> → <strong><code>카톡 알림</code></strong> → <strong><code>리뷰</code> → <code>리뷰내용 반영</code></strong>→ <strong><code>머지</code></strong> → <strong><code>백로그 업데이트</code>  → <code>배포</code></strong> <br>
이 로직이 습관화 될 때까지 서로가 서로를 케어해줍시다!!!</p>
</blockquote>
<p>다음 주는 이 로직을 최대한 의식적으로 지키려고 노력해보면서 코드단에 대한 이야기를 더 많이 나눠보면 좋을 것 같다.</p>
<p>마지막으로 내 생활 부분에서는 운동을 조금 더 가고싶다.
복싱 두 번, 헬스 한 번...</p>
<hr>
<p>이번 주에 신기한 일이 하나 있었다.
길벗에서 책 리뷰 신청을 했는데 당첨되서 이번 주에 책을 받았다.
받고나서 책을 본 순간 추천사에 어디서 많이 본 이름이???????
우리 멘토님이랑 이름 똑같은데????????
?????????????????????????????????
근데 여쭤봤더니 진짜였다.
살다보니 이런 일도 생기고 신기하다.</p>
<p><del>책 읽어보고 리뷰해야되는데 이거 언제하지</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[작업이 오래걸리는 요청을 어떻게 응답할까?]]></title>
            <link>https://velog.io/@hee_kyoung11/%EC%9E%91%EC%97%85%EC%9D%B4-%EC%98%A4%EB%9E%98%EA%B1%B8%EB%A6%AC%EB%8A%94-%EC%9A%94%EC%B2%AD%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9D%91%EB%8B%B5%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@hee_kyoung11/%EC%9E%91%EC%97%85%EC%9D%B4-%EC%98%A4%EB%9E%98%EA%B1%B8%EB%A6%AC%EB%8A%94-%EC%9A%94%EC%B2%AD%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9D%91%EB%8B%B5%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Thu, 16 Nov 2023 17:01:17 GMT</pubDate>
            <description><![CDATA[<p>프로젝트에서 코드 실행 요청을 보내는 기능을 구현하고 있다.</p>
<p>클라이언트가 코드 실행 요청을 보내면 서버에서 그 코드를 실행해 결과를 알려주는 형태이다.</p>
<p>실행을 요청한 코드가 실행에 오래걸리는 코드이거나, </p>
<p>사용자가 많아서 내가 보낸 요청이 처리되는데 오래걸릴 수도 있다.</p>
<h1 id="생각해보기">생각해보기</h1>
<p>이럴 땐 어떻게 클라이언트에 응답을 줄 수 있을까? 생각해 본 방식은 두 가지가 있다.</p>
<ol>
<li><p>하염없이 기다리기</p>
<p> long polling에 가깝다. </p>
<p> 일단 polling은 주기적으로 요청하여 서버의 상태와 클라이언트 상태를 동기화 하기위해 사용하기 때문에 엄밀히 말하면 polling은 아니라고 생각한다.</p>
<p> 하지만 요청을 보내면 응답할 내용이 생길 때까지 서버는 대기를 하다가 지정한 시간 내에 응답이 발생하면 클라이언트에게 응답을 보내고 지정한 시간내 응답할 내용이 생기지 않으면 timeout 처리를 하는 것이다.</p>
<p> status Code는 408을 활용할 수 있을 것이다.</p>
</li>
<li><p>주기적으로 요청보내기</p>
<p> 서버가 코드 실행 요청을 보내면 클라이언트에 202 응답을 내려준다.</p>
<p> 202는 해당 요청이 accept 되었음을 알려준다.</p>
<p> 클라이언트가 202코드를 응답받으면 다시 결과를 가져오는 요청을 보낸다.</p>
<p> 이 요청에 200 응답을 받을 때 까지 주기적으로 요청을 보낼 수 있을 것이다.</p>
<p> 이 방식은 short polling과 유사하다고 느꼈다.</p>
</li>
</ol>
<h2 id="프로그래머스는-어떻게-하고-있을까">프로그래머스는 어떻게 하고 있을까?</h2>
<p>프로그래머스에서 코드 실행 버튼을 누르면 events 요청이 발생하고 202 응답 받는 것을 확인할 수 있었다.</p>
<p>202 응답을 받은 이후에 네트워크 요청이나 응답이 없는데 코드 실행 결과는 계속 실행되고 결과까지 나왔다.</p>
<p>대체 어떻게~ (요리용디톤으로)</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/4800c410-411d-4683-8e74-39e0b6354953/image.png" alt="">
<del>for문 백만번 돌려서 죄송합니다…</del>
<br></p>
<p>한참을 코드를 들여다보는데 도저히 결과를 다시 요청하거나 받는 코드를 찾을 수 없었다. </p>
<p>그러다 문득 떠오른 생각. 혹시 문제 화면 들어올 때 웹소켓??</p>
<p>네트워크 도구를 켠 채로 나갔다 다시 들어오니 웹 소켓 연결을 찾을 수 있었다.</p>
<p>문제를 선택해 처음 문제풀이 화면에 들어올 때 웹소켓에 연결된다. 
<img src="https://velog.velcdn.com/images/hee_kyoung11/post/576dd0f4-b72c-4fa4-9df1-35799b0449f4/image.png" alt=""></p>
<h2 id="구름은-어떻게-할까">구름은 어떻게 할까?</h2>
<p>실행을 눌렀을 때 run 이벤트가 발생하고 socket.io로 서버와 웹소켓을 연결을 통해 데이터를 주고 받는다.
<img src="https://velog.velcdn.com/images/hee_kyoung11/post/a18b8f2a-027b-4b80-bbc8-b271f0b1fd60/image.png" alt=""></p>
<p>여기도 역시 웹소켓을 사용한다.</p>
<p>내가 생각했었던 방식은 결국 polling 계열인데, 얘네들은 socket 개념이 등장하기 전에 나온 개념이다.</p>
<p>그렇다보니 두 사이트 모두 소켓을 사용하여 서버에서 실행완료 시 (클라이언트의 요청이 없이도) 응답을 보내는 것 같다.</p>
<h1 id="그럼-이제-어떻게-하면-좋을까">그럼 이제 어떻게 하면 좋을까?</h1>
<p>우리 프로젝트의 코드 실행 기능의 흐름은 다음과 같다.</p>
<ul>
<li>클라이언트가 코드 실행 요청을 보내면 api 서버로 요청이 보내진다.</li>
<li>api 서버에서 코드 보안 검사에 통과하면 redis 메시지 큐에 job으로 등록한다.</li>
<li>job으로 등록되면 running 서버에서 consume 하여 job을 꺼내수행한다.</li>
<li>결과를 다시 redis에 저장한다.</li>
</ul>
<p>우리 프로젝트는 서버가 나눠져있다. </p>
<p>이 구조에서 발생하는 문제는 api 서버가 요청한 코드 실행이 완료되었는지 알 수가 없다는 것이다.</p>
<p>api 서버에 실행 완료 여부를 탐색하는 로직이 추가로 필요하다.</p>
<ol>
<li>pub/sub 구조를 도입해 running 서버에서 코드 실행이 완료되면 publish 하고 그걸 다시 api 서버가 받아 응답하거나</li>
<li>주기적으로 redis를 탐색해 코드실행 결과가 있는지 탐색해야한다.  </li>
</ol>
<br>

<p><strong>1번 시나리오로 간다면 다음과 같이 구현해야한다.</strong></p>
<ul>
<li>웹소켓이 연결이 된다.</li>
<li>코드 실행 요청이 생겼을 때, 보안 검사에 통과하면 202 실패하면 forbidden 403을 응답하여 일단 http 통신을 완료한다.</li>
<li>그리고 api 서버는 메시지 큐에, running 서버는 그걸 받아 수행하고 publish 한다.</li>
<li>api 서버에서는 subscribe로 데이터를 받고 본인이 요청한 job id 목록에 있다면 웹소켓으로 응답을 준다</li>
<li>웹소켓을 닫는다.</li>
</ul>
<p>(우리 서비스에서 웹소켓 연결/해제 시점은 코드 실행 요청이 있을 때가 더 좋아보인다. 방에 참여한 사람들 중 한 명이 실행 요청을 보내면 그 사람만 웹 소켓연결을 하면 모든 사람이 코드 실행 결과를 공유할 수 있다.)</p>
<br>

<p><strong>2번 시나리오로 간다면 주기적으로 redis를 찾는 과정을 잘 설계하는 것이 필요하다.</strong></p>
<p>이때 레디스의 부하는 무시하기로 했다. 
레디스 벤치마크 테스트를 해 본 결과 백만 건의 요청도 거뜬했다.</p>
<br> 

<p>2-1. 하염없이 기다리기</p>
<p>코드 실행 요청이 생겼을 때 보안 검사에 실패하면 403 응답을 주고 통과하면 메시지큐, running 서버를 거쳐 redis에 실행 결과가 쌓인다.</p>
<p>api 서버는 메시지 큐에 job을 push하고 주기적으로 계속 redis를 탐색해서 실행 결과를 찾고, 찾으면 그때 200 상태코드와 함께 실행 결과 응답을 보낸다.</p>
<p>서버는 계속 탐색하고 있어 서버 부하가 크다.</p>
<br>

<p>2-2. 먼저 202 응답을 주고, 클라이언트 측에서 주기적으로 요청을 보내기</p>
<p>계속 탐색하는 역할을 클라이언트 측으로 위임한 것이다.</p>
<p>2-1의 경우는 탐색 주기를 적절히 설정하는 것이 어렵고 탐색 주기와 탐색 시도 횟수에 따라 크게 성능이 달라진다.</p>
<p>탐색 역할을 클라이언트 측에 맡김으로써 서버 측에 부하를 줄이고 http 통신을 통한 약간에 딜레이로 시간을 벌 수 있어 두 파라미터에 대한 의존성을 약간 줄일 수 있다는 장점이 있다.</p>
<br>

<p>제일 처음 생각했던 것에 이제 웹소켓 옵션이 하나 더해졌다.</p>
<p>2-3. 웹소켓 이용</p>
<p>먼저 202 응답을 준다. 그리고 1번 방식과 거의 동일하다.</p>
<p>하지만 http 요청 핸들러의 수명주기가 끝난 상태라</p>
<p>job id와 웹소켓 id 정보를 어디엔가 저장하고 </p>
<p>서버에서 주기적으로 레디스를 탐색하다가 데이터를 찾으면 웹소켓 아이디 정보를 이용해서 실행결과를 보내준다.</p>
<hr>
<p>서비스가 나눠지니까 이렇게 복잡하다</p>
<p>이쯤되니 마이크로서비스의 단점이 더 와닿는다</p>
<p>하지만 또 마음잡고 생각해보면 api 서버에서 코드를 실행하다가 죽어버리면 이제 큰일이 나는 것이다. </p>
<p>로그인 조차도 못하고 그냥 서비스가 중지된다. </p>
<p>하지만 api 서버와 running 서버가 분리되어 있어 running 서버에서 코드를 실행하다 문제가 생겨 서버가 다운되더라도 api 서버는 멀쩡하기 때문에 서비스에서 코드 실행 기능이외에 다른 기능들을 이용할 수 있다.</p>
<p>지금 적은 많은 생각들을 공유하고 의논해본 후 적용해봐야겠다.
모두 해보고 임계점 테스트를 해 보는 것도 재밌을 것 같다.</p>
<p>다음글 : <a href="https://velog.io/@hee_kyoung11/WebSocket%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%BD%94%EB%93%9C-%EC%8B%A4%ED%96%89-%EC%9A%94%EC%B2%AD%EC%9D%84-%EC%B2%98%EB%A6%AC%ED%95%B4%EB%B3%B4%EC%9E%90">WebSocket으로 코드 실행 요청 처리하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[certbot으로 HTTPS 설정하기]]></title>
            <link>https://velog.io/@hee_kyoung11/certbot%EC%9C%BC%EB%A1%9C-HTTPS-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hee_kyoung11/certbot%EC%9C%BC%EB%A1%9C-HTTPS-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 14 Nov 2023 17:19:27 GMT</pubDate>
            <description><![CDATA[<p>Ncloud로 Ubuntu 서버를 생성했고 Let&#39;s Encrpty와 certbot을 이용해서 https 설정을 해보려한다.
Let’s Encrpty 는 기관이고, 이 기관을 통해서 무료로 TLS 인증서를 발급받을 수 있다.
Certbot은 Let’s Encrpty에서 인증서를 발급받고 https를 활성화하는 오픈소스 소프트웨어이다.</p>
<h1 id="https란">HTTPS란?</h1>
<p>암호화 프로토콜을 거친 암호화된 HTTP 통신</p>
<p>이전에는 SSL(Secure Sockets Layer)으로 불렸지만 SSL 2.0에서 발견된 취약점을 보완하기 위해 재설계된 SSL3.0을 배포하며 TLS로 이름이 변경되었다.</p>
<p>HTTP는 80 포트를 사용하고 HTTPS는 443  포트가 사용된다.</p>
<p><a href="!https://www.cloudflare.com/ko-kr/learning/ssl/what-is-https/">what is https - cloudeflare</a> </p>
<p><a href="!https://brunch.co.kr/@swimjiy/47">그림으로 쉽게 보는 HTTPS, SSL, TLS - brunch</a></p>
<h3 id="그러면-또-tls는-뭔데">그러면 또 TLS는 뭔데?</h3>
<p>Transport Layer Security, 전송 계층 보안</p>
<p>인터넷 상의 커뮤니케이션을 위한 개인 정보와 데이터 보안을 용이하기 위해 설계되어 널리 채택된 보안 프로토콜</p>
<p>HTTP 프로토콜 위의 TLS 암호화를 구현한 것이 HTTPS이다</p>
<p>TLS는 </p>
<ol>
<li>전송되는 데이터를 암호화하고</li>
<li>통신하는 대상을 인증하고</li>
<li>데이터의 무결성을 체크한다.</li>
</ol>
<p><a href="!https://www.cloudflare.com/ko-kr/learning/ssl/transport-layer-security-tls/">transport-layer-security-tls - cloudflare</a></p>
<p>이해하기 쉬운 예시로 사용자 로그인을 예시로 들면 Http 프로토콜로 사용자의 아이디와 비밀번호를 서버에 보내면 평문으로 보내지게 된다. 그러면 통신과정에서 정보를 탈취당하면 그대로 정보가 노출된다. 하지만 Https를 이용하게 되면 데이터가 암호화되어 데이터가 탈취되더라도 쉽게 알아내기 어렵다.</p>
<p>이제 HTTPS에 대해서 간단히 알아보았으니 ubuntu에 설정을 해보자!</p>
<p><strong>그전에!</strong></p>
<ul>
<li>서버의 TCP 443, 80 포트를 오픈해야한다. AWS라면 인바운드 규칙이 되겠고 ncloud라면 포트포워딩이 될 것이다</li>
<li>도메인 주소가 필요하다.</li>
</ul>
<p>프로젝트에서 쓸 도메인을 구매해두어서 그걸 사용했다.
없다면 <code>내도메인.한국</code>을 이용해봐...????</p>
<h1 id="snap-설치">Snap 설치</h1>
<p>certbot을 설치하기 전에 snap 패키지 관리 도구를 설치한다.
Certbot Documentation에서 Snap을 이용한 설치를 권장하고 있기 때문!
snap은 패키지 관리 도구로 apt와 달리 모든 종속성을 포함한다는 특징을 가진다.</p>
<p><a href="!https://ubuntu.com/core/services/guide/snaps-intro">snaps-intro</a></p>
<pre><code class="language-bash">apt-get install snapd -y</code></pre>
<pre><code class="language-bash">snap --version</code></pre>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/84ad6fb7-a4f6-4cb6-bc69-1d6805699724/image.png" alt=""></p>
<h1 id="certbot-설치">Certbot 설치</h1>
<pre><code>sudo snap install --classic certbot</code></pre><pre><code>sudo ln -s /snap/bin/certbot /usr/bin/certbot</code></pre><pre><code>certbot --version</code></pre><p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/e3252d65-58b0-4b1a-8a92-fa70b5c34115/image.png" alt=""></p>
<h1 id="certbot-실행">Certbot 실행</h1>
<pre><code># 인증서 발급과 설치 모두 진행
sudo certbot</code></pre><p>입력하고 차례로 <code>이메일, y, y 도메인</code>을 입력한다.</p>
<p>그 후 https로 접속해보면 바로 실행이 된다!</p>
<ul>
<li>Certbot에 대해서 더 알아보기 : <a href="!https://eff-certbot.readthedocs.io/en/latest/install.html">Certbot Documentation</a></li>
</ul>
<hr>
<p>참고</p>
<ul>
<li><a href="!https://howtosanta.com/korea/ubuntu-20-04%EC%97%90%EC%84%9C-snap-package-manager%EB%A5%BC-%EC%84%A4%EC%B9%98%ED%95%98%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95/">Ubuntu 20.04에서 Snap Package Manager를 설치하고 사용하는 방법</a></li>
<li><a href="!https://freewebserver.tistory.com/entry/%EB%AC%B4%EB%A3%8C-SSL-%EC%9D%B8%EC%A6%9D%EC%84%9C-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0with-certbot#google_vignette">5. 무료 SSL 인증서 설치하기(with. certbot)</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[소켓 서버가 여러 개 일 때 데이터 순서는 어떻게 보장할 수 있을까?]]></title>
            <link>https://velog.io/@hee_kyoung11/%EC%86%8C%EC%BC%93-%EC%84%9C%EB%B2%84%EA%B0%80-%EC%97%AC%EB%9F%AC-%EA%B0%9C-%EC%9D%BC-%EB%95%8C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%88%9C%EC%84%9C%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%B3%B4%EC%9E%A5%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@hee_kyoung11/%EC%86%8C%EC%BC%93-%EC%84%9C%EB%B2%84%EA%B0%80-%EC%97%AC%EB%9F%AC-%EA%B0%9C-%EC%9D%BC-%EB%95%8C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%88%9C%EC%84%9C%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%B3%B4%EC%9E%A5%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Sun, 12 Nov 2023 14:49:51 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/633fadd6-0c3b-4c57-8639-0bc8a07afe33/image.png" alt="DALLE-2가 그려준 그림"></p>
<p>소켓을 이용한 채팅 서비스를 구현하려는 계획을 세우고 있다.
소켓 서버가 하나라면, 또 실제 다수의 사용자가 없다면 매우 간단하다.
하지만 서비스가 커져 소켓 서버가 여러 개가 필요하고 사용자가 많아져 동시에 처리되어야할 메시지가 많아진다면 들어온 메시지들의 순서를 어떻게 보장할 수 있을까?</p>
<p>이번 프로젝트의 기술적 도전을 해당 주제로 잡고 나서 구현할 수 있는 방법을 탐색 중이다. 
받은 키워드는 <code>#메시지채번</code> <code>#snowflake</code> <code>#uuid</code> <code>#autoincrement</code> 이다.</p>
<h1 id="채번">채번</h1>
<p>채번은 메시지에만 국한된 용어는 아닌 듯 하다.
채번(採番)은 새로운 번호를 딴다는 의미라고 한다.
메시지 채번은 클라이언트가 전송한 메시지에 번호를 붙인다는 의미쯤으로 이해했다.
<a href="https://dataonair.or.kr/db-tech-reference/d-lounge/expert-column/?mod=document&amp;uid=51981">https://dataonair.or.kr/db-tech-reference/d-lounge/expert-column/?mod=document&amp;uid=51981</a></p>
<p>메시지에 번호를 붙여 관리하려면 모든 메시지를 저장하는 구조가 추가로 필요하겠다고 생각했고, 메시지 큐(RabbitMQ, Redis)가 먼저 떠올랐다. 또 UUID나 Auto Increment말고 타임스탬프를 사용할 수도 있지 않을까? 하는 생각이 들었다.</p>
<p>채번이라는 키워드로 검색하는데 개발관련 자료가 많이 안나와서 GPT에게 영어로 어떤 용어일지 알려달라했더니 <strong>&quot;Message Sequencing&quot;, &quot;Message Numbering&quot;</strong>를 알려줬다.</p>
<p>Message Seqeuncing에 대한 IBM문서를 하나 찾을 수 있었다.
<a href="https://www.ibm.com/docs/en/app-connect/11.0.0?topic=sequences-message-sequencing">https://www.ibm.com/docs/en/app-connect/11.0.0?topic=sequences-message-sequencing</a></p>
<h1 id="snowflake">snowflake</h1>
<p>공식 문서에 들어가 Key Concepts &amp; Architecture를 읽어보았다.
트위터에서 만든 데이터 저장, 처리, 분석 솔루션을 빠르고 쉽게 제공하는 서비스라고 한다.
찾아보니 분산 시스템에서 UUID를 생성하는데 활용할 수 있다고 한다.</p>
<h1 id="uuid-auto-increment">UUID, Auto Increment</h1>
<h3 id="uuid">UUID</h3>
<p>네트워크 상에서 고유성을 보장하는 ID를 만들기위한 표준 규약</p>
<h3 id="auto-increment">Auto Increment</h3>
<p>데이터베이스에 Insert 할 때마다 PK의 숫자를 하나씩 증가시켜 자동으로 PK를 지정한다.</p>
<p>두 가지는 데이터베이스의 PK를 UUID로 할 것인가 Auto Increment로 할 것인가에 대한 이야기가 많이 나왔었어서 비교적 익숙했다.</p>
<p>둘 중 어느 방식으로든지 메시지에 key를 부여하면 메시지를 구분할 수는 있다. 하지만 순서를 보장하려면 메시지 큐가 필요할 것 같다. </p>
<h1 id="타임스탬프">타임스탬프</h1>
<p>메시지가 발생한 시점에 타임스탬프를 번호로 가진다면 어떨까? 생각해봤다.
그러면 순서를 보장할 수 있지않을까?
하지만 조금 더 생각해보니 정말도 동시에 메시지 이벤트가 발생해 똑같은 타임스탬프를 가진다면 이 경우는 처리할 수 없으니 그리 좋은 방법은 아닌 것 같다.</p>
<hr>
<p>해결 방법에 대해서 찾아보며 의문점들이 생겼다.</p>
<ol>
<li><p>클라이언트에서 메시지를 전송할 때 바로 메시지 큐에 요청을 보내진 않을 것이다.
그러면 메시지 큐에 데이터를 넣는 것은 어디에서 처리할 것인가?
소켓서버? 아니면 별도의 서버가 필요할까?</p>
</li>
<li><p>별도의 서버가 필요하다면 그 서버도 소켓 서버만큼 다수가 필요할까?
메시지 큐에 넣는 작업을 하는 서버가 하나라면 소켓서버를 하나로 처리하는 이유가 없지않나?</p>
</li>
<li><p>연결된 소켓정보는 어디에 저장되나? 메모리에 저장될 것 같은데, 다중 서버환경에서 소켓 정보들을 어떻게 공유하고 관리할 수 있지?</p>
</li>
</ol>
<p>계속해서 해결방법을 찾아보면서 과정을 기록해보자!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[부스트캠프 웹・모바일 그룹프로젝트 1주차 회고]]></title>
            <link>https://velog.io/@hee_kyoung11/%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%E3%83%BB%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B7%B8%EB%A3%B9%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hee_kyoung11/%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%E3%83%BB%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B7%B8%EB%A3%B9%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 10 Nov 2023 08:33:40 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/5741c907-5e41-492a-81c3-7b552435dd92/image.png" alt=""></p>
<p>이번주부터 그룹프로젝트가 시작되었다.
1주일 간 프로젝트를 진행하면서 이번주는 뭘 했는지, 좋았던 점, 아쉬웠던 점을 기록해보려한다.</p>
<p>우리 팀은 저번 주 금요일에 오프라인으로 만나 주제를 정했다.
주제는 <code>동료들과 함께 소통하며(화상, 음성, 채팅) 알고리즘 학습을 할 수 있는 플랫폼</code> 이다
WebRTC나 CRDT 등 실시간 기술들을 사용해보고 싶어 이런 저런 아이디어를 내다가 결정하게 되었다.</p>
<p>그리고 이번 주에는 기획 구체화, 팀 협업 룰 설정, 팀 페이지 작성, 개발환경 설정, 서버 생성 등의 일을 함께했다.</p>
<h1 id="이번-주에-한-일">이번 주에 한 일</h1>
<p>주로 이번주에 내가 한 일은 다음과 같다.</p>
<ul>
<li>socket.io와 WebRTC를 학습하고</li>
<li>WebRTC에 필요한 STUN/TURN 서버를 coturn을 이용해 구축</li>
<li>Ncloud Server를 열기 및 Nginx 설정</li>
<li>github action으로 이벤트 발생시 CI &amp; 도커 이미지 빌드 후 업로드.
까지 이다.</li>
</ul>
<h1 id="좋았던-점">좋았던 점</h1>
<p>좋았던 점은 혼자 진행할 때보다 학습을 더 집중있게 할 수 있었던 것이다.
혼자할 때는 모르면 하기싫은 마음이 크게 들었는데, 팀원들과 소통하기 위해서는 더 자세히 알아야하기 때문에 더 열심히 밀도있게 공부할 수 있었다.</p>
<p>Blue Green 배포를 무중단 배포 전략으로 선택했는데 CI/CD를 경험해보지 않아서 중간중간 어려움을 겪었다. 그 때 이미 그 배포를 경험해 본 팀원 분께 질문하고 같이 해결해나가며 성장할 수 있었던 점이 좋았다.</p>
<p>개발환경 설정이나 github 템플릿도 해보진 않았지만 이미 해 본 팀원들이 있어서 정말 빠르게 진행되었다.
생각보다 간단하게 설정할 수 있어서 신기했다. 
언젠가는 내가 팀원들에게 도움을 줄 수 있는 부분도 생기겠지? 그럴 수 있도록 노력해야겠다!</p>
<p>또 신기하게도 온라인으로 모집된 팀이지만 팀원들의 결이 잘 맞는 것 같다.</p>
<h1 id="아쉬웠던-점">아쉬웠던 점</h1>
<p>아쉬운 점도 있었다.
내가 경험한 부분들과 팀원이 경험한 부분과 깊이에 차이가 있다보니까 따라가기엔 조금 벅찼던 것 같다.
심지어 저번 주와 이번 주 초까지는 개인적인 일이 많아서 미리 공부를 못했더니 죄송한 마음이 들었다.</p>
<p>그리고 우리가 기획한 내용들이 생각보다 
<strong>백엔드가 코드로써 가져갈 수 있는 기술적인 부분</strong>이 많지 않은 것 같다.
DB 테이블을 설계할 내용도 많지 않다.</p>
<p>실시간 소통 기술이 대부분 클라이언트에 집중된 기술이고 서버는 중계만 해주는 역할이 대부분이라 그런 것 같다.
정말 간단한 내용을 구현하는데 협업을 하기도 애매해서 <strong>&#39;너무 따로 진행되는건 아닌가?&#39;</strong> 하는 기분도 들었다. 
앞으로 극복해 나가야 할 포인트가 아닐까?
앞으로 멘토링도 받고 프로젝트의 틀을 잡아가며 기술적 도전을 할 수 있는 부분들을 잘 의논해봐야겠다.</p>
<h4 id="피어세션에서-이런-이야기를-나누어봤는데-협업과-분업의-차이는-서로가-하는-일을-잘-알고있는가이라는-조언을-들은-이야기를-전해주셔서-이-부분을-주의하면서-프로젝트를-진행해야겠다">피어세션에서 이런 이야기를 나누어봤는데 협업과 분업의 차이는 &#39;서로가 하는 일을 잘 알고있는가?&#39;이라는 조언을 들은 이야기를 전해주셔서 이 부분을 주의하면서 프로젝트를 진행해야겠다.</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[블로그를 velog로 옮기며]]></title>
            <link>https://velog.io/@hee_kyoung11/%EB%B8%94%EB%A1%9C%EA%B7%B8%EB%A5%BC-velog%EB%A1%9C-%EC%98%AE%EA%B8%B0%EB%A9%B0</link>
            <guid>https://velog.io/@hee_kyoung11/%EB%B8%94%EB%A1%9C%EA%B7%B8%EB%A5%BC-velog%EB%A1%9C-%EC%98%AE%EA%B8%B0%EB%A9%B0</guid>
            <pubDate>Fri, 10 Nov 2023 05:38:00 GMT</pubDate>
            <description><![CDATA[<p>그동안 기술블로그를 티스토리에 작성해왔다.</p>
<p><a href="https://stop-thinking-start-now.tistory.com/">https://stop-thinking-start-now.tistory.com/</a></p>
<p>티스토리를 계속 이용할까 다른 블로그를 이용해볼까하며 고민이 많았었다.
아무래도 티스토리에는 인공지능을 공부하며 적었던 내용들이 많고,
작성 과정이 마크다운은 완벽하게 지원하지 않아서 불편한 점들이 많아 잘 찾지 않게 되는 것 같았다.</p>
<p>부스트캠프를 진행하며, 또 백엔드로 전향하며 이전과는 다른 느낌의 블로그를 써보고 싶었고
편안히 드나들며 글을 적기에는 벨로그가 사용성이 좋을 것 같아서 벨로그를 이용해보려고한다</p>
<p><img src="https://velog.velcdn.com/images/hee_kyoung11/post/6b103b27-efcc-42d9-8739-df42f965ac2b/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>