<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>daechan_jo.log</title>
        <link>https://velog.io/</link>
        <description>BackEnd Developer</description>
        <lastBuildDate>Thu, 18 Jan 2024 17:33:31 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>daechan_jo.log</title>
            <url>https://velog.velcdn.com/images/daechan_jo/profile/5c953266-c1e6-4514-80fa-962c19852d7e/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. daechan_jo.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/daechan_jo" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[NestJS - Interceptors]]></title>
            <link>https://velog.io/@daechan_jo/NestJS-Interceptors</link>
            <guid>https://velog.io/@daechan_jo/NestJS-Interceptors</guid>
            <pubDate>Thu, 18 Jan 2024 17:33:31 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/daechan_jo/post/11dc5a47-332f-486e-bb7c-efb2aa233cd6/image.png" alt=""></p>
<p>공식 문서에 따르면 인터셉터는 <code>@Injectable()</code> 데코가 달린 클래스라고 한다.
하지만 서비스 레이어의 클래스에서 해당 데코를 이미 사용하고 있는데, 같은 데코를 사용하고 있지만 서로 다른 목적으로 설계되었기에 개념을 명확히 해야한다.</p>
<p>NestJS의 서비스클래스도 다른 프레임워크 또는 <del>우리모두가사용하는그</del>패턴과 비슷하게 일반적으로 비즈니스 로직, 데이터 조작 및 기타 애플리케이션별 기능들을 처리하고 해당 비즈니스 로직을 캡슐화하며 코드 모듈성을 촉진한다.</p>
<p>반면에 인터셉터는 어떨까.</p>
<br />

<h3 id="같은-데코-다른-목적">같은 데코 다른 목적</h3>
<p>인터셉터는 전역적으로 또는 경로별로 요청과 응답 흐름을 가로채 처리하는 데 사용할 수 있는 미들웨어와 유사한 구성 요소이다. 즉, 흐름이 컨트롤러나 서비스에 도달하기 전이나 후에 로직을 실행시킬 수 있다.</p>
<br />

<h3 id="pipe-와의-차이점">pipe 와의 차이점</h3>
<p>NestJs에서 파이프와 인터셉터는 요청을 가로채고 처리하는 측면에서 많은 유사점을 가지고 있다. 하지만 파이프는 주로 데이터 변환 및 검증에 중점을 두고 입력 데이터가 라우트 핸들러에 도달하기 전에 변환하거나 출력 데이터가 응답으로 전송되기 전에 유효성 검사를 진행하고 변환하는데 사용한다.
보통 파이프는 경로 매개변수, 요청 본문, 쿼리 매개변수 등에 적용하는게 일반적이다.</p>
<p>반면에 인터셉터는 더 넓은 범위의 문제에 사용할 수 있다.
인터셉터는 전역적으로 또는 라우트별로 요청과 응답을 가로채고 처리하는 데 사용되고 로깅, 요청 응답 수정, 인증 등과 같은 작업에 적합하다.
또한 파이프와의 가장 큰 차이점으로 실행 컨텍스트에 액세스할 수 있으므로 요청 처리 수명 주기의 다양한 단계에서 작업을 수행할 수 있다.</p>
<p>요약하자면, 파이프와 인터셉터 사이에는 기능상 일부 중복이 있지만 서로 다른 목적으로 설계되었다. 파이프는 데이터 변환 및 검증에 특화되어 데이터의 정확성을 보장하는 반면, 인터셉터는 보다 범용적이며 요청 및 응답 처리와 관련된 더 광범위한 문제에 적용될 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS - pipe 사용법]]></title>
            <link>https://velog.io/@daechan_jo/NestJS-pipe-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@daechan_jo/NestJS-pipe-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Sun, 14 Jan 2024 11:56:20 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/daechan_jo/post/68678912-ee83-40d2-85c6-ff4571fd62cb/image.png" alt=""></p>
<h2 id="pipe">pipe</h2>
<p>파이프는 일반적으로 다음 두 가지 사용 사례가 있다.</p>
<ul>
<li>변환 : 데이터를 원하는 형식으로 변환</li>
<li>유효성 검사 : 데이터를 평가하고 유효하지 않다면 예외 발생</li>
</ul>
<p>express에선 하나하나 미들웨어로 만들어야했다면 nest.js에선 자체적으로 제공하는 빌트인 파이프를 데코로 붙여서 사용하기만 하면 된다 (편-안)</p>
<h3 id="validation-pipe">Validation Pipe</h3>
<p>예를 들어 게시글을 작성하는 api의 컨트롤러에서 입력받은 데이터를 검증하고자 한다면 다음과 같이 할 수 있다.</p>
<pre><code class="language-ts">  @Post()
  @UsePipes(new ValidationPipe())
  async createPost(
    @Body() postContent: PostContentDto,
  ): Promise&lt;PostDto&gt; {
    ...
  }</code></pre>
<p>@Body 데코레이터는 HTTP 요청 본문에 있는 값을 메서드의 인자로 바인딩하는 역할을 하고 이 때 바인딩된 DTO 객체는 @UsePipes(new ValidationPipe()) 데코레이터에 의해 유효성 검사를 실행하기 된다.</p>
<p>즉, PostContentDto 클래스에 정의된  @IsString(), @IsNotEmpty() 등의 데코레이터에 부합하는지 검사하게 된다.</p>
<pre><code class="language-ts">export class PostContentDto {
  @ApiProperty()
  @IsNotEmpty()
  @IsString()
  @Length(1, 50)
  title: string;

  @ApiProperty()
  @IsNotEmpty()
  @IsString()
  @Length(1, 1000)
  content: string;
}</code></pre>
<br />
<br />
<br />

<h3 id="변환">변환</h3>
<p>express라면 입력받은 데이터를 가공하거나 형변환을 하려면 함수 내부에서 코드를 작성해야했더라면 nest.js에서는 마찬가지로 간단한 파이프 데코로 처리할 수 있어 보다 간결한 컨트롤러를 구현할 수 있다. 
하지만 개인적인 생각으로 형변환에 관련된 빌트인 파이프들은 알맞게 바로 사용할 수 없는 경우가 많아서 직접 만들어서 쓴 경우가 많았던것 같다.</p>
<p>사용법은 다음과 같다.</p>
<pre><code class="language-ts">  @Post()
  @UsePipes(new ValidationPipe())
  @UseGuards(AuthGuard(&#39;jwt&#39;))
  async createComment(
    @Request() req: RequestWithUser,
    @Body() commentContent: CommentContentDto,
    @Query(&#39;postId&#39;, ParseIntPipe) postId: number,
    @Optional() @Query(&#39;parentId&#39;, OptionalIntPipe) parentId?: number,
  ): Promise&lt;CommentDto&gt; {
    ...
  }</code></pre>
<p>ParseIntPipe 는 전달받은 문자열인 postId를 10진수로 변환해주는 빌트인 파이프이다.
하지만 쿼리중 옵션으로 받게되는 parentId에도 ParseIntPipe를 적용하면 해당 값이 없는 경우 변환할 수 없으므로 예외를 발생시키게 된다. 이런 경우 파이프를 커스텀해서 사용해야한다.</p>
<pre><code class="language-ts">import { ArgumentMetadata, Injectable, PipeTransform, BadRequestException } from &#39;@nestjs/common&#39;;

@Injectable()
export class OptionalIntPipe implements PipeTransform {
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  transform(value: string, metadata: ArgumentMetadata) {
    if (!value) return null;
    const val: number = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException(&#39;Validation failed (numeric string is expected)&#39;);
    }
    return val;
  }
}</code></pre>
<p>만일 value가 null 또는 undefined라면 null을 반환하도록 하는데 이는 선택적인 값을 처리하기 위함이다. 
value가 존재하면 해당 값을 10진수 숫자로 변환하고 반환한다. 만약 NaN이라면, 즉 숫자로 변환할 수 없는 문자열이라면 예외를 발생시키도록 만든 파이프이다.</p>
<br />

<h3 id="파이프의-동작-방식">파이프의 동작 방식</h3>
<p>파이프는 예외 구역 내부에서 실행된다. 이는 파이프가 예외를 발생시킬 때 예외 계층(전역 예외 필터 및 현재 컨텍스트에 적용되는 모든 예외 필터 ) 에 의해 처리된다는 것을 의미한다.</p>
<p>&quot;예외 구역&quot;은 파이프가 실행되는 코드 영역을 지칭한다. 파이프는 요청 처리 파이프라인의 일부로서, 데이터를 변환하거나 유효성을 검사하는 역할을 하게되고 이 과정에서 파이프 내부에서 예외가 발생하면 예외는 NestJS의 &quot;예외 계층&quot;에 의해 처리된다.</p>
<p>&quot;예외 계층&quot;이란 전역 예외 필터와 현재 컨텍스트(context)에 적용되는 모든 예외 필터를 포함하는 계층을 의미한다. 이 계층은 애플리케이션의 예외 처리 메커니즘을 담당하며, 파이프에서 발생하는 예외를 적절히 처리하여 사용자에게 알려준다.</p>
<p>즉, 파이프는 컨트롤러의 메서드가 실행되기 전 유효성 검사 및 형변환을 수행하고 이 과정에서 예외가 발생하게되면 컨트롤러 메서드는 실행되지 않는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TDD (feat.nestjs + jest)]]></title>
            <link>https://velog.io/@daechan_jo/TDD-feat.nestjs-jest</link>
            <guid>https://velog.io/@daechan_jo/TDD-feat.nestjs-jest</guid>
            <pubDate>Sat, 06 Jan 2024 09:57:39 GMT</pubDate>
            <description><![CDATA[<h3 id="외해야되">외해야되?</h3>
<p>사실 지금도 테스트 주도 개발이란 말이 뼈에 와닿지는 않는다.
그동안 했던 프로젝트들은 엉망진창이었던 기획으로 시도 때도 없이 날아오는 스펙들과 촉박한 마감에서 사실상 TDD를 적용한다는 것 자체가 굉장히 비효율적일뿐더러 산출 기한 내에 프로젝트를 완성하지도 못했을 것 같다.</p>
<p>사실 TDD를 제외하고도 간단한 기능 하나 구현하는데도 계층을 쪼개고 DTO를 적용하고 수많은 유효성 검사를 거치는 등 가끔 너무 과하지 않나 생각이 들 때도 있다. <del>매우 간단한 기능을 만들었는데 100줄의 코드가 넘어가는 걸 보면 이게 맞나 싶을 때도 있다</del>
그런데도 추상화와 규칙이 있어야 하는 이유는 결국 유지보수를 위한 게 아닐까 생각한다.
간혹 내가 작성했던 코드도 기능이 고도화될수록 추후 수정사항이 생길 때 어떻게 손을 대야 할지 엄두도 안 날 때가 많았다.</p>
<br />
<br />
<br />

<h3 id="단점은생략한다">단점은생략한다</h3>
<p><img src="https://velog.velcdn.com/images/daechan_jo/post/57ddc226-25c0-491f-9181-b9a4a2e83df7/image.png" alt=""></p>
<p>만들고 싶은 기능에 대한 테스트 케이스를 작성하고 이후 단위 테스트를 통과할 수 있을 정도 최소한의 코드를 작성한 뒤 최적의 성능을 위해 리팩토링을 진행한 후 프로덕션 레벨에 적용하는 순서를 밟기 때문에 <del>강제적으로</del> 구현해야 할 기능에 대해 더 깊이 이해하게 되고 결과적으로 더 높은 품질의 코드를 작성하는 데 도움이 된다.</p>
<p>또 기능 변경이나 추가가 필요할 때, 또는 구조를 변경하거나 개선하는 리팩토링을 할 때 프로덕션 코드가 아닌 미리 작성되어있는 테스트 케이스로 테스트하면 안전하게 수정할 수 있다. </p>
<p>사용하는 데 주의하면 좋을 점으론, TDD도 결국 객체지향을 위한 방법이므로 각각의 테스트는 독립적이면서 재사용이 보장되어야 하고 의존해서는 안 되며 어느 환경에서도 반복할 수 있어야 한다.</p>
<br />
<br />
<br />


<h2 id="적용해보기">적용해보기</h2>
<pre><code>├─ src
│  ├─ app.module.ts
│  ├─ entities
│  ├─ main.ts
│  ├─ modules
│  │  ├─ auth
│  │  │  ├─ auth.controller.spec.ts
│  │  │  ├─ auth.controller.ts
│  │  │  ├─ auth.module.ts
│  │  │  ├─ auth.service.spec.ts
│  │  │  ├─ auth.service.ts
│  │  │  └─ dto</code></pre><p>다음과 같이 회원가입, 로그인 인증 컨트롤러 코드가 있다고 가정한다</p>
<pre><code class="language-ts">  @Post()
  @ApiOperation({
    summary: &#39;회원가입&#39;,
  })
  @ApiBody({ type: JoinDataDto })
  @ApiResponse({ status: 201, type: CreatedUserDto })
  @UsePipes(new ValidationPipe())
  async createUser(@Body() joinData: JoinDataDto): Promise&lt;CreatedUserDto&gt; {
    if (!this.authService.isValidPassword(joinData)) {
      throw new HttpException(&#39;Passwords do not match&#39;, 400);
    }
    const { passwordConfirm: passwordConfirm, ...createUserDto } = joinData;
    return await this.authService.createUser(createUserDto);
  }

  @Post(&#39;login&#39;)
  @ApiOperation({
    summary: &#39;로그인&#39;,
    description: &#39;성공시 JWT 발급&#39;,
  })
  @ApiBody({ type: LoginDataDto })
  @ApiResponse({ status: 201, type: LoginUserDto })
  @UsePipes(new ValidationPipe())
  @UseGuards(AuthGuard(&#39;local&#39;))
  async login(@Request() req: RequestWithUser): Promise&lt;LoginUserDto&gt; {
    return await this.authService.login(req.user);
  }</code></pre>
<p><code>TDD순서가 뒤바꼈지만 지금은 Jest를 어떻게 사용하는가에 초점을 둔다</code>
회원가입은 특별한 인증 없이 이메일 중복여부와 사용자가 입력한 비밀번호와 컴펌 비밀번호가 일치한지만 검사하고 로그인은 passport의 local 전략을 이용한 기본적인 인증방식의 기능들의 컨트롤러 코드다.</p>
<br />
차근차근 테스트 케이스를 작성해보자

<pre><code class="language-ts">describe(&#39;AuthController&#39;, () =&gt; {</code></pre>
<p>describe는 Jest에서 테스트를 그룹화하는데 사용하는 함수다. 첫 번째 인자는 테스트 그룹의 이름이고 두 번째 인자는 그룹에 속한 테스트들을 정의하는 함수다.</p>
<br />

<pre><code class="language-ts">  let controller: AuthController;
  let authService: AuthService;
</code></pre>
<p>AuthController와 AuthService 인스턴스를 저장할 변수를 선언해준다.
<code>후에 테스트 모듈 내부에서 재할당을 해야하니 상수가아닌 let으로 해준다</code></p>
<br />

<pre><code class="language-ts">beforeEach(async () =&gt; {</code></pre>
<p>  beforeEach 는 각 테스트 케이스가 실행되기 전에 매번 호출되는 함수다. 이 함수 내에서 테스트 환경을 초기화하는 작업을 수행한다.</p>
<br />

<pre><code class="language-ts">    const module: TestingModule = await Test.createTestingModule({</code></pre>
<p>Test.createTestingModule는 NestJS의 테스트 모듈을 생성하는 메서드다. 해당 메서드를 호출하면 테스트에 필요한 의존성 주입 환경을 아래와 같이 설정할 수 있다. (NestJS의 일반적인 모듈 설정과 동일하다.)</p>
<pre><code class="language-ts">      imports: [
        JwtModule.register({
          secret: process.env.JWT_SECRET,
          signOptions: { expiresIn: &#39;24h&#39; },
        }),
        TypeOrmModule.forRoot(typeOrmConfig),
        TypeOrmModule.forFeature([User, Post]),
      ],
      controllers: [AuthController],
      providers: [AuthService, LocalStrategy],
     }).compile();</code></pre>
<p>테스트에 필요한 컨트롤러와 서비스를 등록해주고 compile 메서드를 호출해 테스트 모듈을 컴파일한다.</p>
<br />

<pre><code class="language-ts">    controller = module.get&lt;AuthController&gt;(AuthController);
    authService = module.get&lt;AuthService&gt;(AuthService);
  });</code></pre>
<p>module.get 메서드를 이용해 AuthController와 AuthService 인스턴스를 재할당한다. 해당 인스턴스들은 각 테스트 케이스에서 사용된다.</p>
<br />

<pre><code class="language-ts">  describe(&#39;createUser&#39;, () =&gt; {
    it(&#39;should create a user and return the created user&#39;, async () =&gt; {</code></pre>
<p>createUser 메서드에 대한 새로운 테스트 그룹을 추가해준다. it 함수의 첫 번째 인자는 테스트 케이스의 설명이고, 두 번째 인자는 테스트를 수행하는 함수다.
<br /></p>
<pre><code class="language-ts">      const joinData: JoinDataDto = {
        username: &#39;test&#39;,
        password: &#39;1234&#39;,
        passwordConfirm: &#39;1234&#39;,
        email: &#39;test@test.com&#39;,
      };
      const createdUser: CreatedUserDto = {
        username: &#39;test&#39;,
        password: &#39;1234&#39;,
        email: &#39;test@test.com&#39;,
      };</code></pre>
<p>테스트에 필요한 mock 데이터를 생성해준다</p>
<br />

<pre><code class="language-ts">      jest.spyOn(authService, &#39;isValidPassword&#39;).mockReturnValue(true);
      jest.spyOn(authService, &#39;createUser&#39;).mockResolvedValue(createdUser);</code></pre>
<p>jest.spyOn 함수를 이용해 authService 메서드를 mock 한다. mockReturnValue와 mockResolvedValue 메서드를 이용해 해당 메서드가 호출될 대 반환할 값을 지정해준다.</p>
<br />

<pre><code class="language-ts">      const result: CreatedUserDto = await controller.createUser(joinData);</code></pre>
<p>createUser 메서드를 호출하고 그 결과를 저장해준다.</p>
<br />

<pre><code class="language-ts">      expect(result).toEqual(createdUser);
      expect(authService.isValidPassword).toHaveBeenCalledWith(joinData);
      const { passwordConfirm, ...expectedCreateUserDto } = joinData;
      expect(authService.createUser).toHaveBeenCalledWith(expectedCreateUserDto);
    });</code></pre>
<p>expect 함수의 toEqual, toHaveBeenCalledWith 을 이용해 기대한 인자로 호출되었는지 확인한다.
이런식으로 유닛들의 테스트 케이스를 작성해 나가면 된다.</p>
<br />

<h3 id="테스트-케이스-전체-코드">테스트 케이스 전체 코드</h3>
<pre><code class="language-ts">import { Test, TestingModule } from &#39;@nestjs/testing&#39;;
import { AuthController } from &#39;./auth.controller&#39;;
import { AuthService } from &#39;./auth.service&#39;;
import { JoinDataDto } from &#39;./dto/joinData.dto&#39;;
import { CreatedUserDto } from &#39;./dto/createdUser.dto&#39;;
import { HttpException } from &#39;@nestjs/common&#39;;
import { JwtModule } from &#39;@nestjs/jwt&#39;;
import { TypeOrmModule } from &#39;@nestjs/typeorm&#39;;
import { User } from &#39;../../entities/User&#39;;
import { LocalStrategy } from &#39;../../passport/local.strategy&#39;;
import { Post } from &#39;../../entities/Post&#39;;
import { typeOrmConfig } from &#39;../../../typeorm.config&#39;;

describe(&#39;AuthController&#39;, () =&gt; {
  let controller: AuthController;
  let authService: AuthService;

  beforeEach(async () =&gt; {
    const module: TestingModule = await Test.createTestingModule({
      imports: [
        JwtModule.register({
          secret: process.env.JWT_SECRET,
          signOptions: { expiresIn: &#39;24h&#39; },
        }),
        TypeOrmModule.forRoot(typeOrmConfig),
        TypeOrmModule.forFeature([User, Post]),
      ],
      controllers: [AuthController],
      providers: [AuthService, LocalStrategy],
    }).compile();

    controller = module.get&lt;AuthController&gt;(AuthController);
    authService = module.get&lt;AuthService&gt;(AuthService);
  });

  describe(&#39;createUser&#39;, () =&gt; {
    // 회원가입
    it(&#39;should create a user and return the created user&#39;, async () =&gt; {
      const joinData: JoinDataDto = {
        username: &#39;test&#39;,
        password: &#39;1234&#39;,
        passwordConfirm: &#39;1234&#39;,
        email: &#39;test@test.com&#39;,
      };
      const createdUser: CreatedUserDto = {
        username: &#39;test&#39;,
        password: &#39;1234&#39;,
        email: &#39;test@test.com&#39;,
      };

      jest.spyOn(authService, &#39;isValidPassword&#39;).mockReturnValue(true);
      jest.spyOn(authService, &#39;createUser&#39;).mockResolvedValue(createdUser);

      const result: CreatedUserDto = await controller.createUser(joinData);

      expect(result).toEqual(createdUser);
      expect(authService.isValidPassword).toHaveBeenCalledWith(joinData);
      const { passwordConfirm, ...expectedCreateUserDto } = joinData;
      expect(authService.createUser).toHaveBeenCalledWith(expectedCreateUserDto);
    });

    // 로그인
    it(&#39;should throw HttpException when passwords do not match&#39;, async () =&gt; {
      const joinData: JoinDataDto = {
        username: &#39;test&#39;,
        password: &#39;1234&#39;,
        passwordConfirm: &#39;1234&#39;,
        email: &#39;test@gmail.com&#39;,
      };

      jest.spyOn(authService, &#39;isValidPassword&#39;).mockReturnValue(false);

      await expect(controller.createUser(joinData)).rejects.toThrow(
        new HttpException(&#39;Passwords do not match&#39;, 400),
      );
      expect(authService.isValidPassword).toHaveBeenCalledWith(joinData);
    });
  });
});</code></pre>
<br />

<h3 id="짚고넘어가기">짚고넘어가기</h3>
<p>잠깐 헷갈렸던 부분으로 result 변수에 할당된 값이 실제 데이터베이스에 사용자 정보를 저장하고 반환된 값인 줄 알았다.
애초에 테스트 모듈에 연결한 데이터베이스를 별도로 만들지 않고 <del>귀찮아서...어차피 혼자하는데...</del> 기존에 사용하던 DB를 연결해놨었고 jest.spyOn을 이용한 moking은 authService 단에서만 실행된 거로 이해했었다. 그럼, 테스트가 종료되고 나면 DB에 해당 데이터가 남아있어야 하는 게 없어서뭔가 했다.</p>
<p>테스트 코드에서 AuthService의 createUser 함수를 mocking 하게 되면, AuthController의 createUser 함수가 호출될 때 실제 AuthService의 createUser 함수가 호출되는 대신 mock 된 함수가 호출되게 된다.
즉 테스트환경에선 jest.spyOn을 사용해 특정 함수를 mocking 하게 되면 해당 함수를 호출하는 모든 경로를 자동으로 mock 된 함수로 변경하게 된다.</p>
<p>또 한 가지 재밌는 건 spyOn의 메서드인 mockReturnValue 와 mockResolvedValue 다.
예를 들어, authService.isValidPassword() 를 mocking 하여 항상 true를 반환하도록 설정했다. 이는 사용자가 올바른 비밀번호를 입력했을 때의 시나리오를 테스트하고자 하는 의도로 작성했는데, 실제로 이 메서드가 어떻게 구현되어 있고 어떤 값이 입력되었는지에 상관없이 항상 true를 반환하므로 이 함수를 호출하는 코드가 올바르게 동작하는지를 테스트할 수 있게 된다. 즉, AuthController를 테스트하기 위해서 AuthService에 의존할 필요가 사라진다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Amazon VPC]]></title>
            <link>https://velog.io/@daechan_jo/Amazon-VPC</link>
            <guid>https://velog.io/@daechan_jo/Amazon-VPC</guid>
            <pubDate>Sun, 31 Dec 2023 12:16:13 GMT</pubDate>
            <description><![CDATA[<p>얼마전 AWS를 이용한 네트워크를 구축하면서 VPC에 대한 지식이 없어 애먹은적이 있었다.</p>
<p>일반적으로 클라우드를 이용하지 않는 네트워크에서는 서버나 네트워크 장비를 연결하는 케이블 등을 모두 물리적으로 구축했어야 했다면 AWS와 같은 클라우드를 이용할 때는 물리적인 기기를 이용하지 않고 가상의 네트워크를 구축할 수 있다. 즉 네트워크 장비에 해당하는 리소스를 VPC안에 추가해 독립적인 네트워크를 구축할 수 있게된다.</p>
<p>VPC를 사용하기 전 짚고넘어가면 좋은것들을 알아봅니다.</p>
<h2 id="ip와-cidr">IP와 CIDR</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>이름 태크</td>
<td>sample-vpc</td>
<td>VPC를 식별하는 이름</td>
</tr>
<tr>
<td>IPv4 CIDR 블록</td>
<td>10.0.0./16</td>
<td>VPC에서 이용하는 프라이빗 네트워크의 IPv4 주소 범위</td>
</tr>
<tr>
<td>IPv6 CIDR 블록</td>
<td>IPv6 CIDR 블록 없음</td>
<td>VPC에서 이용하는 프라이빗 네트워크의 IPv6 주소 범위</td>
</tr>
<tr>
<td>tenancy</td>
<td>기본값</td>
<td>VPC 리소스의 전용 하드웨어에서의 실행 여부</td>
</tr>
<tr>
<td>VPC를 생성할 때는 사전에 네트워크 정보를 결정해야한다.</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<h4 id="ip-주소는-뭔지-알겠는데-cidr">IP 주소는 뭔지 알겠는데 CIDR?</h4>
<p>classless inter-domain routing은 IP주소를 관리하는 범위를 결정하는 방법 중 하나
유/무선 전화의 전화번호를 시외 국번, 시내 국번, 가입자 번호로 구분해서 관리하는것과 유사하다</p>
<h4 id="이름태크">이름태크</h4>
<p>VPC를 쉽게 식별하고자 알기 쉬운 이름을 붙여주면 된다. 이후에도 자유롭게 변경할 수 있으니깐 크게 고민하지 않아도 된다.</p>
<h4 id="ip-cidr-블록">IP CIDR 블록</h4>
<p>VPC에서 사용하는 프라이빗 네트워크용 IP 주소의 범위를 지정한다. 프라이빗 네트워크에서 이용할 수 있는 IP주소 범위는 다음 세 가지로 제공된다.</p>
<ul>
<li>24비트 블록 : 10.0.0.0 ~ 10.255.255.255</li>
<li>20비트 블록 : 172.16.0.0 ~ 172.31.255.255</li>
<li>16비트 블록 : 192.168.0.0 ~ 192.168.255.255</li>
</ul>
<p>일반적으로 IP 주소의 범위가 넓은 편이 같은 네트워크 안에 많은 IP주소를 제공할 수 있다. 하지만 VPC로 지정할 수 있는 서브넷 마스크는 최대 16비트까지이므로 어떤 범위를 이용해도 달라지는 점은 없다.</p>
<h4 id="tenancy">tenancy</h4>
<p>VPC상의 리소스를 전용 하드웨어에서 실행할지 지정한다. 기본으로 설정하면 다른 AWS 계정과 하드웨어 리소스를 공유하도록 선택하는 것과 같다. 
실뢰성이 매우 중요한 시스템의 경우에는 전용으로 설정하는것을 검토해도 좋지만 별도의 비용이 추가된다.</p>
<br />
<br />
<br />

<h2 id="서브넷과-가용-영역">서브넷과 가용 영역</h2>
<p>VPC 안에는 하나 이상의 서브넷을 만들어야한다. 서브넷은 VPC의 IP 주소 범위를 나누는 단위다. IP 주소 범위를 나누는 대표적인 이유는 다음 두 가지다.</p>
<ul>
<li>역할 분리 : 외부에 공개하는 리소스 여부를 구별</li>
<li>기기 분리 : AWS 안에서의 물리적인 이중화(다중화)를 수행</li>
</ul>
<br />

<h4 id="역할-분리">역할 분리</h4>
<p>시스템을 구축할 때는 다양한 리소스를 조합하게 된다. 예를 들어 리소스의 하나인 로드 밸런서는 외부 공개가 목적이므로 외부에서 접근할 수 있어야 한다. 반대로 DB 서버와 같이 외부에 노출시키지 않고 VPC내부 서버에서의 사용을 전체로 하므로 이런 규칙을 리소스마다 개별적으로 할당하지 않고, 리소스가 포함된 그룹 전체에 대해 할당하면 설정 누락 등을 피할 수 있다.</p>
<h4 id="기기-분리">기기 분리</h4>
<p>내결함성을 높이기 위해 기기를 분리한다. 내결함성이란 하드웨어 고장 등 예측할 수 없는 사태가 발생했을 때 시스템 자체를 사용하지 못하게 되는 것을 방지하는 능력이다. 클라우드라 하더라도 최종적으로는 어딘가 위치한 물리적인 기기상에서 작동한다. 예를 들어 서브넷이 여럿 존재하더라도 그 서브넷이 같은 기기에 대한 것이라면 기기에 고장이 발생했을 때 동시에 서브넷 안의 리소스를 이용할 수 없게 된다.
VPC에는 가용 영역(각 리전 안의 여러 독립된 위치)이라는 개념이 존재한다. 가용 영역이 다르면 독립되었음을 보장할 수 있으므로 가용 영역별로 서브넷을 제공하면 여러 서브넷을 동시에 이용하지 못하는 가능성을 낮출 수 있게 된다.</p>
<p>신뢰성 높은 AWS라도 시설 내 냉각 장치 고장 등의 이슈로 대규모 장애가 발생하는 경우가 있기에 가용 영역을 분리하는게 바람직하다고 볼 수 있다.</p>
<br />

<h2 id="ipv4-cidr-설계-방법">IPv4 CIDR 설계 방법</h2>
<p>서브넷을 한번 만들면 해당 서브넷이 이용하는 CIDR 블록은 변경할 수 없다. 따라서 처음부터 확실하게 CIDR을 설계해야 한다. 설계할 때는 다음 두 가지 항목을 고려해야한다.</p>
<ul>
<li>생성할 서브넷의 수</li>
<li>서브넷 안에 생성할 리소스 수</li>
</ul>
<p>이 두항목은 트레이드오프 관계다. 생성하는 서브넷 수가 늘어나면 서브넷 안의 리소스 수가 줄어든다.
<img src="https://velog.velcdn.com/images/daechan_jo/post/8beda3bc-4486-4e99-88ed-b19ed8bc13e7/image.jpeg" alt=""></p>
<p>서브넷 수가 많아지면 보안 또는 네트워크 관리에서 좀 더 세밀한 관리가 가능하다는 장점이 있다. 반대로 서브넷 수가 작아지면 단순한 네트워크 설계에 적합해지고 라우팅 테이블 크기 측면에서 리소스 효율성이 높일 수 있다. 그러므로 특정 요구 사항과 제약 조건을 기반으로 서브넷 수와 서브넷당 리소스 수 간의 올바른 균형을 찾아야한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스트레스 테스트 (feat.connection pool)]]></title>
            <link>https://velog.io/@daechan_jo/%EC%84%B1%EB%8A%A5%ED%85%8C%EC%8A%A4%ED%8A%B8-feat.connection-pool</link>
            <guid>https://velog.io/@daechan_jo/%EC%84%B1%EB%8A%A5%ED%85%8C%EC%8A%A4%ED%8A%B8-feat.connection-pool</guid>
            <pubDate>Fri, 15 Dec 2023 16:38:30 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/daechan_jo/post/f85cc74f-111b-431b-a36c-5ce2f7e3f6c7/image.png" alt="">
<code>nestjs, postgresql, typeorm, artillery</code></p>
<h2 id="어쩌다가">어쩌다가</h2>
<p>내가 만든 서버가 어느정도의 성능인지 정확하게 판단할 수 있는 성능지표같은게 있으면 좋겠다.
하지만 예외상황과 각각의 환경들과 목표, 분석 방법 등등 다양한 방법으로 인해 명확한 성능 수준을 측정하기 어려운거같다.
뭐 응애인 내가 판단하기엔 너무 이른거 같고 일단 타임아웃이 발생하지 않는 한계선까지 서버를 몰아붙인 상태에서 레이턴시라도 줄여보자 했다.</p>
<p><del>간지나는그래프를쓰고싶어서</del> Jmeter나 Ngrinder를 사용해볼까 하고 이것저것 설정하다 <del>또</del> 하루를 날려먹었고
딱히 node.js에 특화된 테스트툴도 아니라서 그냥 원래 사용하던 artillery을 사용하기로 했다.
부캠 프로젝트 때 겉 핥기 수준으로만 해봤는데 이번엔 테스트 후 성능 개선까지 진행해보고자 했다.</p>
<br />

<h2 id="테스트-환경-설정">테스트 환경 설정</h2>
<p>테스트하기 앞서 artillery 실행에 필요한 yaml을 작성해줘야한다. 파일명은 원하는대로 작성하면 된다.</p>
<pre><code class="language-yaml"># loadtest.yaml

config:
  plugins:
    publish-metrics:
      - reportFile: &#39;./report.html&#39;
  target: {서버주소}
  phases:
    - duration: 300 # 몇초간
      arrivalRate: 50 # 초당 생성할 가상 유저
      name: Warm up
scenarios: # 각 가상 유저가 실행할 시나리오(각 요청의 밀도)
  - name: User logs in and makes authenticated request
    flow:
      - post:
          url: &#39;/api/auth/login&#39;
          json:
            email: &quot;test@test.com&quot;
            password: &quot;1234&quot;
          capture: # 로그인 후 발급받은 토큰 캡쳐
            json: &#39;$.token&#39;
            as: &#39;authToken&#39;
      - get:
          url: &#39;/api/user/list-all&#39;
          headers:
            Authorization: &#39;Bearer {{ authToken }}&#39; # 요청 헤더에 캡쳐한 토큰 추가
      - get:
          url: &#39;/api/user/list?search=random&#39;
          headers:
            Authorization: &#39;Bearer {{ authToken }}&#39;
      - get:
          url : &#39;/api/user/list?search=sponsor&#39;
          headers:
            Authorization: &#39;Bearer {{ authToken }}&#39;
      - get:
          url : &#39;/api/user/list?search=sponsored&#39;
          headers:
            Authorization: &#39;Bearer {{ authToken }}&#39;
      - get:
          url : &#39;/api/user/list?search=allSponsored&#39;
          headers:
            Authorization: &#39;Bearer {{ authToken }}&#39;
</code></pre>
<br />

<p>그리고 터미널에서 해당 루트로 이동한 다음다 artillery를 실행해주면 설정 값에 따라 테스트가 시작된다</p>
<p><code>artillery run --output test-run-report.json loadtest.yaml</code>
<img src="https://velog.velcdn.com/images/daechan_jo/post/03fd8a80-0942-4d82-bcae-384d10639e79/image.png" alt=""></p>
<br />
<br />

<h2 id="load-test-1">Load test 1</h2>
<p>일단 타임아웃이 발생하지 않는 한계치를 알아내기위해 duration 은 60sec로 고정하고 arrivalRate 를 점차 늘려나갔다.</p>
<p>여담으로 원래 EC2에 배포된 서버에서 테스트 예정이였는데, 프로젝트 종료시점에서 갑자기 RDS 커넥션 이슈가 생겨서 로컬에서 진행하게 되었다..
<del>aws를 공부하지않고 맨땅에 헤딩한 결과랄까 ㅎㅎ.. 이 번 프로젝트 문서화가 끝나면 aws부터 다시 공부해야한다..</del></p>
<br />

<h4 id="arrivalrate--70-으로-테스트한-결과">arrivalRate : 70 으로 테스트한 결과</h4>
<p>초당 70의 가상 유저(4200명)를 생성하고, 각 유저는 시나리오(총 25200번 요청)를 실행한다. 이 때 시나리오에서 DB에 전달되는 쿼리는 11개이므로 데이터베이스엔 분당 46200 번의 쿼리가 실행된다.</p>
<pre><code>errors.ETIMEDOUT: .............................................................. 277
http.codes.200: ................................................................ 20169
http.codes.201: ................................................................ 4200
http.downloaded_bytes: ......................................................... 19726167
http.request_rate: ............................................................. 343/sec
http.requests: ................................................................. 24646
http.response_time:
  min: ......................................................................... 5
  max: ......................................................................... 9999
  mean: ........................................................................ 4437.9
  median: ...................................................................... 4147.4
  p95: ......................................................................... 8692.8
  p99: ......................................................................... 9047.6
http.responses: ................................................................ 24369
vusers.completed: .............................................................. 3923
vusers.created: ................................................................ 4200
vusers.created_by_name.User logs in and makes authenticated request: ........... 4200
vusers.failed: ................................................................. 277
vusers.session_length:
  min: ......................................................................... 156.4
  max: ......................................................................... 41889.9
  mean: ........................................................................ 26083.8
  median: ...................................................................... 27181.5
  p95: ......................................................................... 39747.5
  p99: ......................................................................... 41369.7
</code></pre><p><img src="https://velog.velcdn.com/images/daechan_jo/post/b0af5c30-0ef1-4c70-bce7-37f9b87af948/image.png" alt="">
테스트 결과를 보면 엉망진창인데, 데이터베이스엔 딱히 무리가 가지 않았으니 서버가 과부화 상태가 되어서 타임아웃과 지연이 발생하는걸 확인할 수 있다. </p>
<h3 id="arrivalrate-를-60으로-낮춰보자">arrivalRate 를 60으로 낮춰보자</h3>
<pre><code>http.codes.200: ................................................................ 18000
http.codes.201: ................................................................ 3600
http.downloaded_bytes: ......................................................... 17452303
http.request_rate: ............................................................. 346/sec
http.requests: ................................................................. 21600
http.response_time:
  min: ......................................................................... 4
  max: ......................................................................... 1901
  mean: ........................................................................ 817.8
  median: ...................................................................... 871.5
  p95: ......................................................................... 1380.5
  p99: ......................................................................... 1436.8
http.responses: ................................................................ 21600
vusers.completed: .............................................................. 3600
vusers.created: ................................................................ 3600
vusers.created_by_name.User logs in and makes authenticated request: ........... 3600
vusers.failed: ................................................................. 0
vusers.session_length:
  min: ......................................................................... 121
  max: ......................................................................... 8431.3
  mean: ........................................................................ 4915.3
  median: ...................................................................... 5272.4
  p95: ......................................................................... 7865.6
  p99: ......................................................................... 8186.6</code></pre><p>타임아웃이 발생하지 않고 모든 요청에 응답을 성공했다.
그럼 대략적인 서버의 최대 성능은 분당 3600명을 수용하고 초당 345의 요청을 처리하며 평균 817.5의 속도로 응답한다고 볼 수 있다.</p>
<br />
<br />

<h2 id="커넥션-풀링">커넥션 풀링</h2>
<p>문제는 지금부터였다. 더 이상 쿼리를 최적화 할 수 없는 상태였고 인덱스 설정도 잘 되어있다면 로드밸런싱을 제외한 성능 개선의 다른 방법이 없을까하고 고민했다.</p>
<pre><code class="language-ts">시나리오에 사용된 요청들은 단일 쿼리를 사용중이였다. 그나마 페이징이 필요한 요청에는 findAndCount 메서드를 사용해서 두 번의 쿼리를 쓰는정도.

  async getUsers(page: number, limit: number): Promise&lt;ResponseUserListDto&gt; {
    const [users, totalCount] = await this.userRepository.findAndCount({
      skip: (page - 1) * limit,
      take: limit,
      order: {
        createdAt: &quot;DESC&quot;,
      },
    });
    const totalPage: number = Math.ceil(totalCount / limit);

    return { users: plainToInstance(GetUserListDto, users), totalPage, currentPage: page };
  }</code></pre>
<p>그러다 커넥션풀링을 사용하면 성능향상에 도움되지 않을까 생각했다.
부하테스트에서 분당 3600명을 수용했기에 커넥션 옵션을 넉넉하게 4500으로 잡아주었다.
typeorm은 내부적으로 커넥션 풀링을 지원한다.</p>
<pre><code class="language-ts">export const typeOrmConfig: TypeOrmModuleOptions = {
  type: &quot;postgres&quot;,
  host: process.env.DB_HOST,
  port: Number(process.env.DB_PORT),
  username: process.env.DB_USER_NAME,
  password: String(process.env.DB_PASSWORD),
  database: process.env.DB_NAME,
  entities: [User, Post, Comment, Like, Payments, AccountHistory],
  synchronize: true,
  migrations: [&quot;dist/migration/*.js&quot;],
  migrationsTableName: &quot;migrations&quot;,
  extra: {
    max: 4500, // 풀에 유지할 최대 커넥션 수
    connectionTimeoutMillis: 5000,
  },
  ...
}</code></pre>
<br />
<br />

<h2 id="load-test-2">Load test 2</h2>
<p>커넥션 풀링을 설정하고 동일한 조건에서 다시 테스트를 진행해보자.
좀 더 정확한 집계를 위해 duration 을 300으로 증가시키고 커넥션 풀링을 적용하기 전과 후를 비교했다</p>
<pre><code>// 적용 전

http.codes.200: ................................................................ 90000
http.codes.201: ................................................................ 18000
http.downloaded_bytes: ......................................................... 87240246
http.request_rate: ............................................................. 360/sec
http.requests: ................................................................. 108000
http.response_time:
  min: ......................................................................... 3
  max: ......................................................................... 861
  mean: ........................................................................ 115
  median: ...................................................................... 39.3
  p95: ......................................................................... 528.6
  p99: ......................................................................... 645.6
http.responses: ................................................................ 108000
vusers.completed: .............................................................. 18000
vusers.created: ................................................................ 18000
vusers.created_by_name.User logs in and makes authenticated request: ........... 18000
vusers.failed: ................................................................. 0
vusers.session_length:
  min: ......................................................................... 77.7
  max: ......................................................................... 3897.9
  mean: ........................................................................ 698.5
  median: ...................................................................... 223.7
  p95: ......................................................................... 3134.5
  p99: ......................................................................... 3752.7</code></pre><pre><code>// 적용 후
errors.Failed capture or match: ................................................ 12
http.codes.200: ................................................................ 89744
http.codes.201: ................................................................ 17988
http.codes.500: ................................................................ 208    // 서버에러발생
http.downloaded_bytes: ......................................................... 87022853
http.request_rate: ............................................................. 360/sec
http.requests: ................................................................. 107940
http.response_time:
  min: ......................................................................... 2
  max: ......................................................................... 5774    // 과부화로 인한 응답 지연
  mean: ........................................................................ 224.9
  median: ...................................................................... 70.1
  p95: ......................................................................... 804.5
  p99: ......................................................................... 907
http.responses: ................................................................ 107940
vusers.completed: .............................................................. 17988
vusers.created: ................................................................ 18000
vusers.created_by_name.User logs in and makes authenticated request: ........... 18000
vusers.failed: ................................................................. 12
vusers.session_length:
  min: ......................................................................... 78.3
  max: ......................................................................... 13290.7
  mean: ........................................................................ 1354.5
  median: ...................................................................... 314.2
  p95: ......................................................................... 4867
  p99: ......................................................................... 6569.8
</code></pre><p>커넥션풀링을 적용 후 오히려 성능저하와 에러 가 발생되었다.</p>
<p>커넥션 사이즈를 설정한 기준은, 최대 세션길이와 초당 처리해야할 쿼리로 어림잡아 계산했었다.
그럼 항상 알맞은 크기의 풀이 대기중이여서 처리속도가 빨라졌어야하지만 오히려 느려졌기에, 풀이 너무 많아서 서버 성능을 저하시켰나 생각했지만 원인은 다른 곳에 있었다.</p>
<br />
<br />

<h2 id="less-is-more">Less is more</h2>
<p>출처: <a href="https://bugoverdose.github.io/docs/database-connection-pool-sizing/">[번역] 커넥션 풀 사이즈에 대하여</a> Copyright © Jinwoo&#39;s Blog</p>
<p><code>PostgreSQL - 데이터베이스 커넥션 개수 최적의 처리량을 위해서는 활성화된 커넥션의 개수가 ((core_count 2) + effective_spindle_count) 정도여야 한다는 것이 수 년에 걸쳐 입증된 공식이다. 하이퍼쓰레딩이 활성화되었더라도 코어 개수에 HT 쓰레드를 포함시키면 안 된다. 활성화된 데이터가 전부 캐쉬되어있다면 효율적인 스핀들 개수는 0입니다. 그리고 캐쉬 적중률이 감소할수록 점차 실제 스핀들의 개수에 가까워지게 됩니다. ... 다만, 해당 공식이 SSD에 얼마나 적절하게 적용될 수 있는지에 대한 분석은 아직 없습니다.</code></p>
<p>자세한 설명은 해당 링크에서 아주 자세히 잘 정리되어 있다.</p>
<p>PostgreSQL은 디스크와 네트워크에 대한 접근 시간을 고려하여, CPU 코어 개수의 두 배에 해당하는 크기로 설정하는 것을 권장한다고 한다. 이는 CPU 코어가 병렬로 작업을 처리할 수 있도록 하여 성능을 최적화하는 데 도움이 되고 데이터베이스에 동시에 접근할 수 있는 수가 증가하면서 블록된 커넥션의 수가 증가할 가능성이 있으므로, 이를 고려하여 effective_spindle_count를 추가로 더해준다.</p>
<p>결론은 &#39;풀이 얼마나 커야할까&#39;로만 생각하고 CPU와 쓰레드의 동작 방식을 이해하지 못한 채 무작정 커넥션 풀 사이즈의 크기만 키운게 화근이였다. </p>
<br />
<br />

<h2 id="final-load-test">Final load test</h2>
<p>이제 해당 공식에 맞게 커넥션 풀 사이즈를 변경하고 테스트를 진행한다.
<code>Connection Pool Size = (core_count * 2) + effective_spindle_count</code>
postgres 컨테이너는 4코어로 실행중인 상태였는데, 해당 공식에 맞춘다면 현재 가장 최적의 커넥션 풀 사이즈는 9가 된다. (모든 상황에 적용되는 공식이 아님을 주의)</p>
<pre><code>// 변경 전

errors.Failed capture or match: ................................................ 12
http.codes.200: ................................................................ 89744
http.codes.201: ................................................................ 17988
http.codes.500: ................................................................ 208    // 서버에러발생
http.downloaded_bytes: ......................................................... 87022853
http.request_rate: ............................................................. 360/sec
http.requests: ................................................................. 107940
http.response_time:
  min: ......................................................................... 2
  max: ......................................................................... 5774    // 과부화로 인한 응답 지연
  mean: ........................................................................ 224.9
  median: ...................................................................... 70.1
  p95: ......................................................................... 804.5
  p99: ......................................................................... 907
http.responses: ................................................................ 107940
vusers.completed: .............................................................. 17988
vusers.created: ................................................................ 18000
vusers.created_by_name.User logs in and makes authenticated request: ........... 18000
vusers.failed: ................................................................. 12
vusers.session_length:
  min: ......................................................................... 78.3
  max: ......................................................................... 13290.7
  mean: ........................................................................ 1354.5
  median: ...................................................................... 314.2
  p95: ......................................................................... 4867
  p99: ......................................................................... 6569.8</code></pre><pre><code>// 변경 후

http.codes.200: ................................................................ 90000
http.codes.201: ................................................................ 18000
http.downloaded_bytes: ......................................................... 87228881
http.request_rate: ............................................................. 353/sec
http.requests: ................................................................. 108000
http.response_time:
  min: ......................................................................... 3
  max: ......................................................................... 250
  mean: ........................................................................ 37.2
  median: ...................................................................... 25.8
  p95: ......................................................................... 90.9
  p99: ......................................................................... 147
http.responses: ................................................................ 108000
vusers.completed: .............................................................. 18000
vusers.created: ................................................................ 18000
vusers.created_by_name.User logs in and makes authenticated request: ........... 18000
vusers.failed: ................................................................. 0
vusers.session_length:
  min: ......................................................................... 78.1
  max: ......................................................................... 1068.5
  mean: ........................................................................ 230.8
  median: ...................................................................... 198.4
  p95: ......................................................................... 459.5
  p99: ......................................................................... 837.3</code></pre><p>공식에 맞게 커넥션 풀 사이즈를 조절하고 난 후 눈에 띄게 성능이 좋아진걸 확인할 수 있다.</p>
<table>
<thead>
<tr>
<th></th>
<th>공식 적용 전</th>
<th>공식 적용 후</th>
</tr>
</thead>
<tbody><tr>
<td>초당 응답 개수</td>
<td>360</td>
<td>353</td>
</tr>
<tr>
<td>평균 레이턴시</td>
<td>70.1</td>
<td>25.8</td>
</tr>
<tr>
<td>최대 레이턴시</td>
<td>5774</td>
<td>250</td>
</tr>
<tr>
<td>평균 세션길이</td>
<td>314.2</td>
<td>198.4</td>
</tr>
<tr>
<td>최대 세션길이</td>
<td>13290.7</td>
<td>1068.5</td>
</tr>
</tbody></table>
<br />
<br />
<br />
<br />
<br />



<p> <img src="https://velog.velcdn.com/images/daechan_jo/post/bfd375f8-9ff8-4ddc-8bdc-a9e93a4de6ca/image.png" alt=""></p>
<h3 id="짤막후기">짤막후기</h3>
<p>아무리 짱구를 굴려도 더 이상 최적화할 곳을 찾지 못했을 때 커넥션 사이즈가 생각났었다 <del>포폴에 쓸게없어 무조건 찾아야해</del>
기본으로 설정되어 있던 커넥션 사이즈가 100 이였기에 단순히 서버에 무리가 가지 않을 정도로 사이즈만 키우면 성능이 개선되겠구나 생각했다가 예상치 못한 테스트 결과에 뇌정지가 왔었다.</p>
<p>붙잡고 물어볼 사람이 없었다곤 하나 조금만 더 자세히 서치 했더라면 금방 해결할 수 있던 문제를 며칠이나 질질 끌게되었다.
개발을 하면 할 수록 CS, C언어 또는 데이터베이스의 근본적인 지식이 필요한 이슈를 자주만나게 되고, 그마저도 시원하게 해결되진 않고 또 다른 의문을 남긴다.
그렇다고 저 거대한 녀석들을 전부 짚고 넘어가기엔 시간도, 머리도 허락해줄리가 없다ㅋㅋㅋ.. 물고 늘어져야할 꼬리가 점점 늘어가는 기분이라, 앞으로 알아가야 할 방대한 지식에  압도당하는 기분마져 든다. 하다보면 언젠가는 되겠지😗
다른거 없이 스오플과 공식문서로만 개발했던 찐 개발자분들이 존경스러워지는 순간입니다..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OOP 원칙에 따른 리펙터링 (feat. S3)]]></title>
            <link>https://velog.io/@daechan_jo/OOP-%EC%9B%90%EC%B9%99%EC%97%90-%EB%94%B0%EB%A5%B8-%EB%A6%AC%ED%8E%99%ED%84%B0%EB%A7%81-feat.-S3</link>
            <guid>https://velog.io/@daechan_jo/OOP-%EC%9B%90%EC%B9%99%EC%97%90-%EB%94%B0%EB%A5%B8-%EB%A6%AC%ED%8E%99%ED%84%B0%EB%A7%81-feat.-S3</guid>
            <pubDate>Fri, 08 Dec 2023 05:02:18 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/daechan_jo/post/aad29eef-6cbf-48af-99e1-b5645da2e0bb/image.png" alt=""></p>
<p><em><code>macOS:14.2 / Nestjs / EC2</code></em></p>
<h3 id="왜-나의-코드는-뒤돌아보면-항상-더러운가">왜 나의 코드는 뒤돌아보면 항상 더러운가..</h3>
<p>기존 EC2 내부 스토리지를 사용하다 S3를 연결했는데 한가지 신경쓰이는게 있었다.
이번 토이프로젝트의 목표중 하나가 무과금이였는데 S3프리티어는 5GB 스토리지와 2만 get요청, 그리고 2천 put요청을 제공해준다.
5기가라는 다소 조금 귀여운 용량은 그렇다치고 제공된 요청건수를 넘어가면 과금이 시작되는 방식인데 둘이서 진행하는 프로젝트라 이정도면 충분하지 않을까 생각하다 혹시몰라 바로바로 전환할 수 있게 코드를 작성해보자 했다.</p>
<p>그렇게 코드를 수정하는데, 전에 작성한 코드들이 아무리 봐도 클래스 탈을 뒤집어쓴 함수형 코드다.
그럴만도한게 nestjs를 처음 접해봤고, 프로젝트 시작시 동료분의 괴물같은 템포를 따라잡고자 예전에 사용하던 express의 코드를 복붙해와서
당장 사용할 수 있을정도로 수정만 해두고 덕지덕지 기워놓은 상태였다.
OOP는 아아아아아주 예전에 JAVA를 찍먹해볼때나(나사실코딩천재인건가하던시절) 개념정도만 알고 있던 터라, 함수형 프로그래밍이 손가락에 익어버려서 생각없이 휘갈기다보면 어느샌가 이렇게 클래스껍데기만 쓰고 있는 함수지향형 코드가 완성되어있다.
그나마 nest의 독재정치?로 어느정도 객체지향을 달성할 수 있지만 앞으로 의식하고 코드를 작성해야되지 않을까 생각한다.</p>
<br />
<br />

<h3 id="리펙터링-전">리펙터링 전</h3>
<pre><code class="language-ts">@Injectable()
export class UploadService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository&lt;User&gt;,

    @InjectRepository(Post)
    private postRepository: Repository&lt;Post&gt;,
  ) {}

  async uploadProfileImage(userId: number, imageUrl: string): Promise&lt;string&gt; {
    const user = await this.userRepository.findOne({
      where: { id: userId },
    });
    if (process.env.NODE_ENV === &quot;production&quot;) {
      if (user.profileImg) {
          const params = {
            Bucket: process.env.AWS_BUCKET_NAME,
            Key: user.profileImg.split(`${process.env.AWS_BUCKET_NAME}/`)[1],
          };
          const deleteObjectCommand = new DeleteObjectCommand(params);
          await s3.send(deleteObjectCommand);
      }
      await this.userRepository.update({ id: userId }, { profileImg: imageUrl });

      return imageUrl;
    } else {
      if (user.profileImg) {
        const serverUrl: string = process.env.SERVER_URL;
        const relativeImagePath: string = user.profileImg.replace(serverUrl, &quot;&quot;).replace(/^\//, &quot;&quot;);
        const absoluteImagePath: string = path.join(__dirname, &quot;..&quot;, &quot;public&quot;, relativeImagePath);
        if (fs.existsSync(absoluteImagePath)) {
          fs.unlinkSync(absoluteImagePath);
        }
      }

      const finalUrl: string = await createUploadUrl(imageUrl);
      await this.userRepository.update({ id: userId }, { profileImg: finalUrl });

      return finalUrl;
    }
  }

...다른 메서드들

}</code></pre>
<p><em><code>아직 객체지향을 완벽하게 이해하지 못한 코린이의 주관적인 생각입니다</code></em>
대충 눈에 보이는 문제점은 다음과 같았다</p>
<ul>
<li>중복 코드 파티
유저와 게시글 엔터티는 각각 하나의 이미지만 가질 수 있도록 계획했기에 새롭게 이미지를 업데이트하면 기존 이미지는 삭제되어야 한다. 그렇게에 각각의 메서드에 삭제와 업데이트 로직이 중복되고 있었다.</li>
<li>SRP 위반
각 함수들이 여러가지 책임(삭제, 업데이트)을 가지게 되었고 이로인해 중복과 복잡도가 증가</li>
<li>if문 중첨
아직도 벗어나지 못한 if문 중첩..</li>
<li>확장성 부족
추후 다른 이미지 필드가 추가된다면 또 같은 로직을 반복해서 작성해야되므로 악순환의 반복이 예상</li>
<li>강한 의존성
환경변수로 프로덕션환경 여부와 버켓의 이름을 직접적으로 사용해 강한 의존성으로 유연성 저하</li>
</ul>
<p>다시보니 굳이 객체지향을 적용하지 않아도 문제가 많아보인다 <del>ㅋㅋ..</del></p>
<br />

<p>리펙터링 과정에서 특히 &#39;의존성&#39; 에 대해 이해하기가 쉽지 않았는데 조금 쉽게 풀어보자</p>
<br />
<br />

<h3 id="의존성종속성">의존성(종속성)</h3>
<p><code>의존성을 이해하는 데 도움이 될 수 있는 비유 중 하나는 팀 프로젝트를 생각해보면 된다.
예를 들어 한 사람은 디자인을 담당하고, 다른 사람은 코드를 작성하고, 또 다른 사람은 프로젝트 관리를 담당한다. 이렇게 각 팀원은 자신의 역할에 집중하면서 다른 팀원이 자신의 역할을 잘 수행할 것이라는 의존성을 가지게 된다.  
이와 마찬가지로, 소프트웨어에서의 의존성은 한 부분이 다른 부분의 기능이나 동작에 의존하는 관계를 의미한다. 예를 들어, 서비스 클래스는 데이터베이스와의 통신 기능을 필요로 하는데, 이 기능을 직접 구현하는 대신 데이터베이스와의 통신 기능을 제공하는 다른 클래스에 의존할 수 있다. 이렇게 하면 서비스 클래스는 자신의 주요 역할에 집중할 수 있으며 데이터베이스와의 통신은 의존하고 있는 클래스가 담당하게 된다. 이러한 의존성 관리는 코드의 유연성을 높이고, 각 부분의 역할을 명확하게 하며, 코드의 재사용성을 높인다.</code></p>
<p>비유가 장황하지만 더 쉽게 말하면 모든 기능을 모듈화(분리)하고, 필요에 따라 의존성으로 주입받아 사용한다고 생각하면 이해하기 수월하다. (지극히 개인적인 코린이의 주장)</p>
<p>의존성은 캡슐화와도 관계가 있는데, 의존성 주입을 사용하면 클래스는 주입받은 객체의 인터페이스에만 의존하게 된다. 이는 클래스가 주입받은 객체의 구체적인 구현에 의존하지 않고, 대신 그 객체가 제공하는 메서드와 속성에 의존한다는 것을 의미한다.</p>
<pre><code class="language-ts">class DatabaseService {
  getData() {
    // 데이터베이스에서 데이터를 가져오는 코드
  }

  saveData(data) {
    // 데이터베이스에 데이터를 저장하는 코드
  }
}</code></pre>
<pre><code class="language-ts">class UserService {
  constructor(private databaseService: DatabaseService) {} // UserService는 DatabaseService 의존성 주입

  getUser() {
    return this.databaseService.getData();
  }

  saveUser(user) {
    this.databaseService.saveData(user);
  }
}</code></pre>
<p>위 코드에서 UserService는 DatabaseService의 구체적인 구현에 의존하지 않고, 대신 DatabaseService가 제공하는 인터페이스인 getData와 saveData 메서드에 의존하게 된다.</p>
<p>더 쉽게 풀자면 UserService는 DatabaseService의 구체적인 내용을 알지 못하고 제공되는 메서드(인터페이스)인 getData와 saveData의 존재유무만 알 수 있다. 이로써 서로의 결합도가 낮춰지고 코드의 유연성이 높아짐에 따라 유지보수가 쉬워진다.</p>
<br />
<br />

<h3 id="리펙터링-후">리펙터링 후</h3>
<pre><code class="language-ts">
@Injectable()
export class UploadService {
  private readonly isProduction: boolean;
  private readonly bucketName: string;
  constructor(
    @InjectRepository(User)
    private userRepository: Repository&lt;User&gt;,

    @InjectRepository(Post)
    private postRepository: Repository&lt;Post&gt;,

    private configService: ConfigService,
  ) {
      // 환경변수를 의존성 주입으로 결합을 낮춘다
    this.isProduction = configService.get(&quot;NODE_ENV&quot;) === &quot;production&quot;; 
    this.bucketName = configService.get(&quot;AWS_BUCKET_NAME&quot;);
  }

  // 각 메서드 내부에서 반복되던 로직 분리 및 삼항연산자를 활용한 확장성 증가
  async deleteImage(
    target: &quot;user&quot; | &quot;post&quot;,
    targetId: number,
    field: &quot;profileImg&quot; | &quot;backgroundImg&quot; | &quot;postImg&quot;,
    isProduction: boolean,
  ) {
    const repository = target === &quot;user&quot; ? this.userRepository : this.postRepository;
    const entity = await repository.findOne({ where: { id: targetId } });

    const image = entity[field];

    if (image) {
      if (isProduction) {
        const params = {
          Bucket: this.bucketName,
          Key: image.split(`${this.bucketName}/`)[1],
        };
        const deleteObjectCommand = new DeleteObjectCommand(params);
        await s3.send(deleteObjectCommand);
      } else {
        await deleteRelativeImage(image);
      }
    }
    return;
  }

  // 각 메서드 내부에서 반복되던 로직 분리 및 삼항연산자를 활용한 확장성 증가
  async updateImage(
    target: &quot;user&quot; | &quot;post&quot;,
    targetId: number,
    imageUrl: string,
    field: &quot;profileImg&quot; | &quot;backgroundImg&quot; | &quot;postImg&quot;,
    isProduction: boolean,
  ): Promise&lt;string&gt; {
    const repository = target === &quot;user&quot; ? this.userRepository : this.postRepository;

    let finalUrl: string;

    if (isProduction) {
      finalUrl = imageUrl;
    } else {
      finalUrl = await createUploadUrl(imageUrl);
    }

    await repository.update({ id: targetId }, { [field]: finalUrl });

    return finalUrl;
  }

  // 업데이트, 삭제 메서드를 분리해 SRP원칙을 지키고 가독성 향상 및 중복코드 제거
  async uploadProfileImage(userId: number, imageUrl: string): Promise&lt;string&gt; {
    const user = await this.userRepository.findOne({
      where: { id: userId },
    });

    if (user.profileImg) await this.deleteImage(&quot;user&quot;, userId, &quot;profileImg&quot;, this.isProduction);
    return this.updateImage(&quot;user&quot;, userId, imageUrl, &quot;profileImg&quot;, this.isProduction);
  }

  async uploadBackgroundImage(userId: number, imageUrl: string): Promise&lt;string&gt; {
    const user = await this.userRepository.findOne({
      where: { id: userId },
    });

    if (user.backgroundImg)
      await this.deleteImage(&quot;user&quot;, userId, &quot;backgroundImg&quot;, this.isProduction);
    return this.updateImage(&quot;user&quot;, userId, imageUrl, &quot;backgroundImg&quot;, this.isProduction);
  }

  async uploadPostImage(postId: number, imageUrl: string): Promise&lt;string&gt; {
    const post = await this.postRepository.findOne({
      where: {
        id: postId,
      },
    });

    if (post.postImg) await this.deleteImage(&quot;post&quot;, postId, &quot;postImg&quot;, this.isProduction);
    return this.updateImage(&quot;post&quot;, postId, imageUrl, &quot;postImg&quot;, this.isProduction);
  }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[RDS 삽질기 (feat. AWS VPC 터널링)]]></title>
            <link>https://velog.io/@daechan_jo/RDS-%EC%82%BD%EC%A7%88%EA%B8%B0-feat.-AWS-VPC-%ED%84%B0%EB%84%90%EB%A7%81</link>
            <guid>https://velog.io/@daechan_jo/RDS-%EC%82%BD%EC%A7%88%EA%B8%B0-feat.-AWS-VPC-%ED%84%B0%EB%84%90%EB%A7%81</guid>
            <pubDate>Fri, 01 Dec 2023 10:00:29 GMT</pubDate>
            <description><![CDATA[<p><code>개발환경 : macOS(m1) / nestjs / postgres / typeORM</code></p>
<h3 id="무서운-aws-과금">무서운 AWS 과금</h3>
<p>최종적으로 원하는 시스템 아키텍쳐를 하나하나 적용시켜보기로 하고, 오락가락하는 EC2 t2.micro 인스턴스의 메모리를 조금이라도 확보하고자 데이터베이스 컨테이너를 내리고 RDS부터 붙여주기로 마음먹었었다.</p>
<p>처음엔 <del>감히건방지게</del> 별다른 설명을 보지 않고 무작정 RDS 인스턴스 생성을 시도했었다가 AWS의 청구 예상 비용 협박으로 잠시 주춤했다. 
<img src="https://velog.velcdn.com/images/daechan_jo/post/3ae9100c-de93-410e-84ba-8df30311ab09/image.png" alt=""></p>
<p>분명 프리티어 템플릿으로 생성을 진행했고 설명 중 과금요소가 있는 옵션들은 전부 제외했는데도 예상 청구액이 잡혀 이게 뭔가 했다.
다른 사람들의 사용 후기를 찾아보니 RDS프리티어 사용중 요금이 청구되었다는 사례가 종종 보이는걸 보아 내가 설정을 잘못했나 싶어서 이것저것 건드려봐도 예상 청구 비용은 사라지지 않았다.</p>
<p>어차피 부담스러울 정도의 금액도아니고 나오면 납부하자는 생각으로 생성했는데 3일차에 접어드는 지금까지 잡히는 비용이 없는걸 보니 구라 청구 예상 비용이라는게 결론.</p>
<br />
<br />
<br />
<br />


<h3 id="기술블로그-꼬ㅊ미남-형들-말만-믿다가-하루를-날렸다">기술블로그 꼬ㅊ미남 형들 말만 믿다가 하루를 날렸다</h3>
<p><img src="https://velog.velcdn.com/images/daechan_jo/post/56b76536-2647-49ad-ab1e-e178d516c0ec/image.png" alt=""></p>
<p>그렇게 과금 이슈를 뒤로한 채, 생성한 RDS를 로컬에서 접근해보기로 했다.
근데 왠걸, 연결이 안된닿...
요청이 가는걸 보니 경로는 맞는데 연결을 거부당했다.</p>
<p>편의를 위해 인바운드 규칙을 anywhere로 설정해놨었고 퍼블릭 엑세스도 허용해놓은 상태였는데 연결이 되지 않는 이유를 도무지 찾을 수 없었다. 그렇게 죄 없는 인스턴스를 지우고 반복하기 시작했다.</p>
<p>수 많은 기술 블로그에선 다들 너무나도 간단하게 연결에 성공하고 있었는데 왜 나는 안될까..
로그라도 상세했으면 좋겠지만 &#39;The connection attempt failed&#39; 가 끝이였고 결국 돌고 돌아 <del>파이널리</del> 공식문서 튜토리얼 첫 페이지에서 이슈의 원인을 찾아버리고 말았다.</p>
<br />
<br />

<h3 id="공식문서는-생각보다-친절하다-하지만-죽어도-안보죠">공식문서는 생각보다 친절하다 (하지만 죽어도 안보죠?)</h3>
<p><img src="https://velog.velcdn.com/images/daechan_jo/post/7d5a69ce-9fce-462b-8545-b0ebefd0f370/image.png" alt=""></p>
<p><code>but resources outside of the VPC can&#39;t access it</code></p>
<p><del>ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ</del></p>
<p>그렇다. 애초에 RDS는 EC2와 연계해서 사용하기 위한 인스턴스라 EC2의 VPC 내부에서만 접근할 수 있도록 설계되어있다.
<img src="https://velog.velcdn.com/images/daechan_jo/post/712f79f3-597b-49bc-8905-188775184eec/image.png" alt=""></p>
<p>하지만 퍼블릭 엑세스를 허용해놨다면 해당 VPC 외부에서도 접속할 수 있는게 맞다고는 하는데 왜 나는 안되는지 의문...</p>
<p>그리고 참고했던 기술블로거들의 글들을 보면 ssh 터널링으로 EC2를 거쳐 RDS에 접속해놓고 그 부분에 대한 설명이 하나도 없었다.
<del>뭐 결국 남들이 차려놓은 밥상에 숟가락 얹었다가 배탈난 꼴이라 할 말이 없긴 함..</del></p>
<p><img src="https://velog.velcdn.com/images/daechan_jo/post/f64395f7-d151-46ba-990b-dec933c7ac06/image.png" alt=""></p>
<p>그렇게 일단은 데이터베이스 툴에 ssh 터널을 추가해서 연결에는 성공했지만, app.module에 데이터베이스 연결설정에도 ssh 터널링을 추가해주고 배포시엔 다시 삭제해야하는 번거로움이 있기에 로컬 포트포워딩을 사용하기로 했다.</p>
<br />
<br />
<br />

<h3 id="포트포워딩으로-간편하게-rds에-접속하기">포트포워딩으로 간편하게 RDS에 접속하기</h3>
<p>[출처] <a href="https://neverfadeaway.tistory.com/42">https://neverfadeaway.tistory.com/42</a>
<code>자세한 설명은 이미 좋은 자료가 너무너무 많기 때문에 생략합니다</code></p>
<p>간단하게만 설명하자면 localhost+사용할포트 경로를 EC2 VPC 내부의 RDS주소로 요청하게 변환해주는거다. 
예를 들면 다음과 같은 방식으로 VPC를 뚫고 RDS에 접근할 수 있게 된다
<code>localhost:15432 -&gt; EC2 -&gt; RDS</code></p>
<p>이렇게 포트포워딩을 사용하면 매번 ssh터널링 코드를 설정할필요가 사라지고 데이터베이스 툴에 연결할때도 간단하게 로컬호스트와 지정한 포트를 사용하면되니 편리하다.</p>
<p>터미널에 다음을 실행하면 백그라운드에서 포트포워딩이 유지된다.</p>
<pre><code>ssh -f -N -i &lt;pem경로&gt; -L &lt;사용할로컬포트&gt;:&lt;RDS앤드포인트&gt;:&lt;RDS개방포트&gt; &lt;EC2유저&gt;@&lt;EC2퍼블릭IP&gt; -v</code></pre><ul>
<li>-f : 백그라운드 실행</li>
<li>-N : 셸을 실행하지 않고 포트 포워딩만 설정</li>
<li>-i : 개인 키 파일을 지정</li>
<li>-L : 로컬 포트포워딩</li>
<li>-v : 상세 디버깅 로그</li>
</ul>
<br />

<p><em>포트포워딩 설정 후 app.module.ts 코드예시</em></p>
<pre><code class="language-ts">@Module({
  imports: [
    ...modulle,
    TypeOrmModule.forRoot({
      type: &quot;postgres&quot;,
      host: process.env.DB_HOST, // localhost
      port: Number(process.env.DB_PORT), // 지정한 포트
      username: process.env.DB_USER_NAME, // rds 마스터 사용자
      password: process.env.DB_PASSWORD, // rds 마스터 암호
      database: process.env.DB_NAME, // DB 이름
      entities: [
       ...entity
      ],
      synchronize: true, // 엔터티 동기화
      ssl: {
        rejectUnauthorized: false, // 포트포워딩시 인증하므로 false로 설정
      },
    }),
  providers: [Logger],
})
export class AppModule {}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS Pree Tier 사용기]]></title>
            <link>https://velog.io/@daechan_jo/AWS-Pree-Tier-%EC%82%AC%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@daechan_jo/AWS-Pree-Tier-%EC%82%AC%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Fri, 24 Nov 2023 08:11:29 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/daechan_jo/post/4d0622fa-e2a8-4c89-a375-d72c972e3624/image.png" alt=""></p>
<h3 id="ec2-그게-뭐야">EC2 그게 뭐야..?</h3>
<p>새롭게 시작한 토이 프로젝트를 t2.micro 규격으로 배포했다.</p>
<p>부트캠프시절 초반에 간단하게나마 사용법을 배웠었지만 어지러운 aws콘솔창에 지레 겁부터먹고 외면했었다.
지금와서 다시 보니 그냥 사용하기 편한 <del>아는게없으면용감하다^^</del> 가상머신이였구나 했다.
심지어 일년간 공짜라고...? 가족들 명의까지 돌려서 사용할 생각에 당뇨초기증상까지 찾아왔지만 성능을 알고 나서 빠르게 완치되었다.</p>
<br />
<br />

<p><img src="https://velog.velcdn.com/images/daechan_jo/post/a907ea16-b2fb-484d-b559-ff3f4e830e3c/image.png" alt=""></p>
<h3 id="이정도일줄은-몰랐지">이정도일줄은 몰랐지</h3>
<p>CS지식이 얕기도 하고 인스턴스를 생성할 때 보안그룹에만 신경쓰느라 t2.micro의 규격을 주의깊게 보지 않았다. 애당초 이름부터가 micro인데 왜 이걸 몰랐을까 ㅋㅋ..</p>
<p>처음 ec2에 접속했을 때 속도가 좀 느린편이구나 라고만 생각했었는데, 컨테이너를 띄울때나 실행되고 있는 와중에도 죽어나가기 일수였다.
그제서야 t2.micro의 규격을 다시 살펴봤는데 램 1기가..</p>
<p>원래는 ec2에 젠킨스까지 설치할 생각이였는데 젠킨스는 고사하고 서버라도 제대로 굴러가면 다행인 수준이였다. 
그마저도 벅차서 수정사항이 생겨 재배포를 할때마다 물떠놓고 기도해야할 판국이였는데, 이대로는 도저히 사용할 수 없겠다 싶었다.</p>
<p>그러다 과거에 실수로 구매한 UTM이 있어서 이걸로 그냥 내가 가상머신 만들면 되잖아? 라고 했지만 포워딩 이슈로 실패하고, Vm ware 에선 가능하다는 정보를 입수하고 시도했으나 사용중인 인터넷이 아파트 공용 인터넷이라 네트워크 설정에 접근하지 못해 마찬가지로 실패했다.</p>
<p>다른 방법이 없을까 하다 메모리 스왑이라는 한 줄기의 빛을 발견했다.</p>
<br />
<br />


<p><img src="https://velog.velcdn.com/images/daechan_jo/post/8a9d80f3-6934-4861-ab37-63620e282320/image.png" alt=""></p>
<h3 id="반-송장">반 송장</h3>
<p>사실 빛이라고 하기엔 흑마술에 가깝지 않나 싶었다.
간단하게 설명하자면 하드 디스크의 일부를 메모리 확장 영역으로 사용하는 기술이다.
즉, 메모리가 부족해지면 당장 사용하지 않는 데이터를 하드 디스크의 스왑 공간으로 이동시키고 필요할때 다시 불러오는 방식이다.</p>
<p>메모리 스왑을 설정하고 난 후, ec2가 죽는일은 없어졌지만 그와 반대로 엄청난 성능저하도 찾아왔다.
서버를 시작하는데만 대략 10분정도 소요되는거 같았다 ㅋㅋㅋ... 해결책이라기보단 임시방편에 가깝고, 프로덕트 레벨에선 상비약같은 존재일려나..?</p>
<p>뭐 그래도 공짜에 이정도면 충분하지 않나 생각하다가도 자잘한 에러나 오타수정으로 재배포하고 확인하는 과정이 길어지다 보니 피로감이 상당하다.</p>
<p>그래도 한정된 자원 안에서 해결하려다 보니 좀 더 아키텍쳐에 대해 고민하게 되는 좋은 계기였던 것 같다.
현재 EC2 안에서 서버와 DB 컨테이너를 띄워놓은 상태인데 RDS를 사용하면 좀 더 괜찮아지지 않을까 라는 생각도 들고..<del>인간적으로 서비스 너무많아</del>
일단 S3와 RDS를 사용해보는것부터 시작해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[부트캠프가 끝난 후 나는 어떻게 살고 있는가]]></title>
            <link>https://velog.io/@daechan_jo/%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84%EA%B0%80-%EB%81%9D%EB%82%9C-%ED%9B%84-%EB%82%98%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%B4%EA%B3%A0-%EC%9E%88%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@daechan_jo/%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84%EA%B0%80-%EB%81%9D%EB%82%9C-%ED%9B%84-%EB%82%98%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%B4%EA%B3%A0-%EC%9E%88%EB%8A%94%EA%B0%80</guid>
            <pubDate>Thu, 16 Nov 2023 10:38:52 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/daechan_jo/post/8dd38494-d656-4904-a34f-39774c936a84/image.png" alt=""></p>
<h3 id="몸이-두개였으면-좋겠다">몸이 두개였으면 좋겠다</h3>
<p>부트캠프가 끝나고나면 살짝은 여유를 부려볼 심산이으나 심적 여유는 2일 만에 집을 나갔다.</p>
<p>부트캠프에서 계획한 로드맵을 어느 정도 이정표 삼아 할 때와는 다르게, 지금부터는 진짜 혼자서 맨땅에 머리를 갈아야 한다는 생각에 불안감이 커졌다. <del>막 군대 전역하고 난 후 기분이랄까</del></p>
<p>또 부트캠프를 하는동안 직장인으로 살아갈 때 가졌던 씀씀이와 개인적인 사정으로 안녕하지 못한 재정사항의 콜라보는 가히 환상적이었고, 그렇게 인생에 두 번 다시 없을 줄 알았던 알바를 시작하게 되었다 <del>뭐 물론 지인매장이라 월급루팡이지만</del></p>
<p>스터디도 하나 더 가입하고 사이드프로젝트도 새로 진행하게 되었고, 알바가 없는 날에는 엄마 딸의 자식도 육아하며, 틈틈이 지인들과의 사회생활도 놓치고 싶지 않았다.</p>
<p>죽겠다.</p>
<p>우습게 봤던 1일 1백준 스터디는 세삼 나의 큐트한 뇌의 한계를 명확히 일깨워주었다.
그래도 그동안 진행한 프로젝트가 있는데 <del>3개밖에안되잖아</del> 중간 정도 레벨이면 금방 풀지 않을까 했지만 어림도 없지.
생각해보면 그동안 진행했던 프로젝트 중에 특별히 복잡한 알고리즘을 요구하는 사항은 없었고, 나 또한 알고리즘보단 당장에 내가 쓰는 라이브러리들이 어떻게 사용되고 왜 사용해야하는지, 웹 통신의 플로우가 어떻게되는 지 등 전체적인 흐름에 초점을 맞췄었고 따라가기 급급했었다.</p>
<p>공부, 사이드 프로젝트, 가족, 지인
모두 포기하기 싫었고 완벽히 해내고 싶었다.
이러다간 언젠간 부러지겠구나 싶지만, 그전까지 비루한 내 몸뚱이를 요긴하게 써먹어 볼 생각이다.</p>
<br />
<br />

<h3 id="어차피-맞아야할-매라면-먼저-맞다가-죽을수도-있다">어차피 맞아야할 매라면 먼저 맞다가 죽을수도 있다</h3>
<p><img src="https://velog.velcdn.com/images/daechan_jo/post/90996527-aee4-4fd5-a059-54f982f37647/image.png" alt=""></p>
<p>진행중인 사이드프로젝트를 기획할 때, 이전에 해보지 못한 것들을 해보고 싶었다. 
그렇게 결제 시스템을 구현해보기로 하고, 개인적인 욕심으로 nest.js 와 쿠버네티스, elasticsearch까지 사용해보고 싶었다. 하지만 둘이서 진행하는 사이드프로젝트인 만큼, 개발에만 몰두할 수 없는 상황에서 새로운 스택으로 도배했다가 흐름을 따라가지 못할까봐 걱정되는 부분이 있었다.(상대분의 개발속도가 무진장 빠르다..)
그렇게 스스로 타협해서 새로운 프레임워크로 nest.js를 사용하고, ec2와 jenkins 를 이용해서 배포해보기로하고 진행하게 되었는데 나..... 무슨 자신감에 저 모든 걸 해보고 싶었던 거였지</p>
<br />
<br />

<h3 id="nestjs-는-생각보다-폭력적이였따">nestjs 는 생각보다 폭력적이였따</h3>
<p><img src="https://velog.velcdn.com/images/daechan_jo/post/7ee95ae3-ffbf-4ec5-94a6-15c99e09d38c/image.png" alt=""></p>
<p>분명 부트캠프를 시작했을 쯤 기업들의 구인 글들에서 nestjs는 보이지 않았었다 <del>저게 뭔지 몰라서 안보였던건지</del>
보통 typescript로 하드코딩 경험 유무 정도가 보였던 거 같은데 이게 웬걸, nestjs 천국이다.
그래서 이번 사이드프로젝트에 이것만큼은 꼭 써보고 싶었다. 
typescript를 처음 접할때 별다른 공부 없이 금방 적응했었고, 또 나름 남들보단 습득 속도가 빠르다는 자만에 방심했다가 고전을 면치 못했다. 
express 위에서 돌아가는 프레임워크라길래 뭐가 크게 다를까 했지만 오랜만에 접한 OOP와 다양하고 끊임없이 번식하는듯한 데코들에 한동안 눈 둘곳을 찾지 못했었다. 그나마 다행이였던 건 전 프로젝트에서 Dto패턴을 적용해보길 권장받아서 이거 하나만큼은 그나마 적응된 상태였다는 것 정도.
express 에선 서버 아키텍쳐를 개발자가 정하지만 nestjs는 구조가 이미 정해져 있는 상태였는데, 그 동안 했던 프로젝트의 서버개발에서 항상 서버아키텍쳐를 담당하면서 내가 직접 만든 보일러플레이트를 사용하고, 그렇게 내가 만든 구조에 쩔어있던 상태라 nestjs의 공산주의가 처음엔 불편했었다 <del>이래서 자기 코드에 애착을 주지 말라는건가</del> </p>
<p>어쨌든 한가하게 책이나 읽으면서 공부하기엔 심장이 허락하지 않았기에 nestjs와 누가누가 더 돌머리인가 박치기를 시작했고 그렇게 어느 정도 적응해 나가고 있다.</p>
<br />
<br />

<h3 id="외않되">외않되?</h3>
<p><img src="https://velog.velcdn.com/images/daechan_jo/post/b851c455-0286-48c2-bda4-fcc26d26f318/image.png" alt=""></p>
<p>그렇게 어느 정도 뼈대가 잡히고 결제시스템을 구현하기 전, https 부터 적용해야 할 것 같아서 간단하게 배포와 프록시 설정부터 하고 ssl 인증을 받으려고 했었다.
여기서부터 그동안 얼마나 편하게 개발에만 집중할 수 있었는지 새삼 깨달았는데, 부트캠프에서 제공해줬던 VM과 도메인이 없자 시작부터 속도가 더뎌지기 시작했다.
AWS 프리티어를 이용하기로하고 사용하는데 여간 똥컴이 아니었다.. <del>이래서 공짜구나</del></p>
<p>그렇게 덜덜거리는 ec2에 도커로 배포를 끝내고 ssl인증은 할 만큼 해봤다고 생각해서 <del>또</del> 자만하고 진행하다가 알 수 없는 이유로 nginx가 지정한 경로에 있는 pem파일을 찾지 못하는 이슈로 <del>화를참지못하고</del> 꼬박 밤을 새워버렸다. 진짜 왜 못 찾았는지 아직도 모름</p>
<p>결국 <del>분에못이겨</del> ec2 인스턴스를 통째로 날려버리고 처음부터 한땀한땀 설정을 맞춰나가면서 해결했다. 그동안 모르는 게 있으면 코치님 바짓가랑이를 잡고 늘어졌지만, 이제는 어엿한 독립체(?)로써 혼자 헤쳐 나가야만 했는데 나름 짜릿하고 재밌다. 해결했을 당시 새벽에 카페에서 작업중이였는데 육성으로 소리를 지르는 바람에 아주 잠깐 주인공이 되었었다.</p>
<br />
<br />


<h3 id="t2micro--어-더-하면-나-죽을게">t2.micro : 어? 더 하면 나 죽을게</h3>
<p><img src="https://velog.velcdn.com/images/daechan_jo/post/adc51fa7-bf60-4fc4-b17b-5124d25a743e/image.png" alt=""></p>
<p>내친김에 jenkins로 자동배포까지 적용시키고 넘어가는게 좋을 거 같다고 판단해 ec2에서 작업하려고 했지만 우리의 가녀린 t2.micro에 젠킨스까지 실행시켜버리면 슈퍼개복치가 되지 않을까 하는 걱정스런 마음으로 고민하던 중, 그냥 내 맥북에서 돌리기로 결정했다. 그렇게 나의 맥북짱은 <del>like NewYork</del> 잠들지 못할 운명이 되었는데 문제는 jenkins uri 설정에 있었다.</p>
<p>git과 jenkins를 연결하던 중 jenkins uri를 변경해줘야할 일이 생겼는데 이때 내 ip주소로 외부에서 접근하는걸 macOS가 상당히 불편해했다. 해결법은 있었지만 맥북이 재부팅이라도 되면 처음부터 다시 설정해줘야 하는 번거로움이 있었는데 이걸 또 어떻게할까 고민하다가 예전에 실수로 구매한 UTM이 있어서 그냥 우분투 가상머신을 만들자 까지가 현재 상황이다.</p>
<p>재밌다. 안해본것들 투성이라 오랜만에 피곤함이 사라졌는데, 프로젝트 파트너분께서 오매불망 결제시스템 구현만 기다리고 계시는중이라 나만 너무 물고 뜯고 맛보고 즐기고있는게 아닌가 하는 생각도든다. <del>스터디라도 하나 줄여야하나...^모^?</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI기반 영단어 학습 웹 서비스 프로젝트 회고]]></title>
            <link>https://velog.io/@daechan_jo/AI%EA%B8%B0%EB%B0%98-%EC%98%81%EB%8B%A8%EC%96%B4-%ED%95%99%EC%8A%B5-%EC%9B%B9-%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@daechan_jo/AI%EA%B8%B0%EB%B0%98-%EC%98%81%EB%8B%A8%EC%96%B4-%ED%95%99%EC%8A%B5-%EC%9B%B9-%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 07 Nov 2023 07:23:32 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/daechan_jo/post/ce07e580-56a7-451f-b17c-3f1877244c95/image.png" alt=""></p>
<p>Git : <a href="https://github.com/daechan-jo/Wordy">Wordy</a></p>
<p>당연하게도(?) 시작은 순조롭지 않았다.
기획단계에서 소극적인 반응들, 프론트 직무 희망자가 없고 초기 이탈자가 생기는 등의 많은 이슈가 있었다.
전에라면 급한 성격에 나서서 이렇게하고 저렇게하자 그랬겠지만, 전 프로젝트 때 너무 의욕이 앞서 자기주장만 내세우다 마찰이 생겼던거 같아 이번엔 한 발짝 뒤로 물러나있기로 마음먹은 터라, 어느정도 마음을 내려놓고 프로젝트를 진행하게 되었던것 같다.</p>
<p>또 이번 프로젝트엔 필수로 AI기능을 접목시켜야 했었는데 나는 자신이 없었다.
관심분야가 아닌데다 다소 높은 난이도, 살짝 꺽여버린 마음(?ㅋ)까지 더해져 AI 모델이 어떻게 구성되고 돌아가는지 정도만 이해하고 있었을 뿐, 실제로 모델을 만들고 학습시키기엔 역량이 부족한 상태였다.
다행이도 해당 기술에 관심을 가져주신 팀원분들이 계서서 AI관련 직무에서 벗어날 수 있었지만 프론트 직무 희망자가 아예 없는게 가장 큰 문제였다.</p>
<p>결국 프론트 개발경험이 있으신 한 분이 총대를 메고 혼자서 프론트를 담당하시기로 했지만, 개인적인 사정으로 중도하차까지 고려를 하고 계셨던 상태였기에 사실 이 때 프로젝트가 완성되지 않을수도 있겠구나 생각했었다.</p>
<p>프로젝트는 혼자 할 수 없고, 각자 희망하는 바도 다르기에 어쩔수없이 이런 상황이 생기게 된다.
하지만 나도 못하는걸 다른 사람이 해주길 바라는건 이기적이라 생각하니 좀 더 마음을 내려놓고 편안하게 진행을 했었던 것 같다.</p>
<p>그렇게 프로젝트는 진행되었고 프론트 개발 진행 속도가 썩 좋지 않았다. 우리가 메인으로 사용해야할 데이터셋 가공에 차질도 생겨 초기에 기획했던 기능들을 하나 둘 씩 없애기 시작하니 내가 할 일이 점점 사라지기 시작했었다.
막바지에 팀원 모두가 프론트개발에 두 손 두 발 다 거들어서 완성시켰는데, 다들 프론트 직무를 희망하지 않음에도 불구하고 너무나 잘해주셔서 나만 편하게 버스에 탑승한 기분이다.</p>
<p>팀원들의 노고와 희생 덕분에 상당한 여유?를 부릴 수 있었는데, 그 전 프로젝트에선 밀려오는 업무량에 데드라인까지 기능개발에 초첨을두고 했었다면 이번엔 내가 작성한 코드를 되돌아보고 생각하고, 나아가 최적화까지 해볼 수 있는 기회가 생겼었던 것 같다.</p>
<hr>
<h2 id="최대한-아주아주-쉽게-설명하자-feat-문서화">최대한 <del>아주아주</del> 쉽게 설명하자 (feat. 문서화)</h2>
<p>간혹 프론트 직무자와 대화를 하다보면 서로 무슨말을 하는지 모를때가 자주 생긴다.
<code>프론트 : 이건 이렇게해서 이러이러하게 전역관리를 할거에요</code>
내 입장에선 대충 외계어처럼 들린다.</p>
<p>이런 일도 있었다.</p>
<pre><code>Front : 이건 DB 어디에 들어가있는거에요?
me : 아 그건 OOO 테이블에 저장되어있어요.
Front : 그럼 DB는 어디에 있는거에요?
me : ㅖ?</code></pre><p>내가 프론트의 상태관리에 관심이 없듯이, 상대도 내 직무 관련 지식에는 관심이 없을 수 있다.
어디까지 내 입장에서나 시스템 아키텍쳐라든지, 데이터베이스라든지 끊임없이 생각하기에 당연한 개념들이지만
상대방 입장에선 용어조차 어려운 벅찬 개념일 수 있다.</p>
<p>당연히 이정도는 알지 않을까? 하고 뱉은 말들이, 상대는 되물어보기 민망하기에 이해하지 못한 채 넘어가서 결국 더 큰 문제를 만든 경우도 많지 않았나 생각이 든다. </p>
<p>API명세서를 별도로 작성하지 않고 swagger를 사용하기로 했었는데 swagger를 처음 접해본 프론트 입장에선 이 조차도 어려웠던 터라, 프로젝트가 어느정도 마무리되어가서야 어떻게 사용하는건지 물어보셨는데 좀 신선한 충격이였다. 이해를 위해 swagger와 같은 기능들을 어떻게 사용해야하는지 설명을 적어두고, 서비스 로직이 어떻게 흘러가는지 시퀀스 다이어그램을 그렸었는데 충분하지 않았던건가 생각했다.</p>
<p>협업에 있어서 상대를 얼마나 정확하게 이해시킬 수 있는지는 정말 중요한 것 같다.
상대는 내가 만든 문서가 어디에 있는지조차 상상 이상으로 관심이 없다. 포기하지 말고 끊임없이 이해시키고 알려주어야 한다.
또 모르는게 있다면 주저하지 않고 물어볼 수 있는 자세 또한 중요한 것 같다(나도 전 프로젝트에서 패스파람과 쿼리스트링의 차이를 몰랐지만 마음속 깊은 곳 자존심으로 인해 입꾹닫 하고 사고를 쳤었다..)
<br /></p>
<h2 id="이-에러가-당신것이-아니라는-보장은-없다">이 에러가 당신것이 아니라는 보장은 없다</h2>
<p> <img src="https://velog.velcdn.com/images/daechan_jo/post/7b6f9fdb-4b21-45be-afb3-3c92ae9f722f/image.png" alt="">
보통 API를 개발하면 포스트맨이나 Jest같은 라이브러리를 사용해 해당 기능들을 테스트한 후 프론트에 전달하는 방식으로 개발을 진행했었다. 내 쪽에서 테스트에 문제가 없는 상태라면 &#39;서버는 정상적으로 응답하고 있는데요?&#39; 시전을 자주 <del>나는모르쇠</del> 하게 된다. </p>
<p>그렇게 프론트직무자의 울부짖는 아우성에 눈을 감고 귀를 막았지만, 사실 알고 봤더니 해당 에러는 배포과정에서 경로를 잘못설정해 요청이 길을 못찾고 있던 상황.
문제의 시발점은 처음으로 도메인을 사용해봤고, nginx 를 리버스프록시 서버로 사용하게 되면서 나 조차도 아직 익숙하지 않은 기술을 사용해놓고 문제없이 돌아가고 있다 스스로 확신해버린 것이다 (미안해 도은짱..<del>그리고사랑해</del>)</p>
<p>완벽한 코드는 없다 <del>특히 내꺼</del>
방심하지 말자.</p>
<br />

<h2 id="시스템-아키텍쳐">시스템 아키텍쳐</h2>
<p> <img src="https://velog.velcdn.com/images/daechan_jo/post/6d27d00d-3c3b-4bb2-82f8-25500f975254/image.png" alt="">
그 전 프로젝트에선 그저 단순히 프론트를 nginx로 배포하고, 서버는 pm2로 배포하는 아주 단순한 방법을 사용했다면, 이번엔 어느정도 여유가 생겨서 CI/CD까지 적용한 배포를 진행해보고자 했었다. 
CI/CD는 캠프에서 제공하는 VM의 ssh키가 없어서 실제로 정상작동하는지 확인은 못했지만, 도커를 이용한 배포는 상당히 재밌었다. nginx 프록시 설정은 거의 헤딩한 수준으로 혼자서 처음 해보는거라 어려움이 있었지만 이론으로만 알고 있던것들을 직접 해보는 좋은 경험있다. 다음 사이드프로젝트에선 jenkins와 쿠버네티스를 이용해볼 예정이다.</p>
<h2 id="쿼리-최적화">쿼리 최적화</h2>
<p><a href="https://velog.io/@daechan_jo/%EC%BF%BC%EB%A6%AC-%EC%B5%9C%EC%A0%81%ED%99%94">해당 프로젝트 쿼리 최적화 관련 글</a></p>
<p><code>한 번의 요청에 하나의 쿼리가 가장 이상적이다</code>
안다. 귀에서 피날 지경이다.
하지만 막상 api를 개발하다보면 정말 지키기 어려운 이상향이다.
데이터베이스를 어떻게 설계하는지도 중요한데 이미 어느정도 완성이 되어버린 상황에서 데이터베이스 구조를 고치기란 쉽지 않다. 그렇기에 <del>또</del> 초기 기획이 중요하다.</p>
<p>그동안 진행했던 프로젝트에선 성능이고 뭐고 테스트해볼 시간도 없이 기능개발만 하기 바빴지만, 이번엔 상당한 여유가 있었기에 내가 작성한 쿼리를 되돌아 볼 기회가 생겼었다.</p>
<p>이번 프로젝트에선 단순히 기능개발이 아닌, 시스템을 어떻게 설계하고 성능을 어떻게 개선할것인지에 대한 많은 고민을 해볼 수 있는 기회가 있어서 좋았다. 사실 이제 기능구현보단 이런 부분에 대해 더 깊은 고민을 해봐야하지 않나 생각이 든다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[부트캠프] 엘리스 AI 트랙 회고]]></title>
            <link>https://velog.io/@daechan_jo/%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%97%98%EB%A6%AC%EC%8A%A4-AI-%ED%8A%B8%EB%9E%99-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@daechan_jo/%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%97%98%EB%A6%AC%EC%8A%A4-AI-%ED%8A%B8%EB%9E%99-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 07 Nov 2023 03:51:59 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/daechan_jo/post/b86a41fb-57ed-4fca-9ae0-4427024de471/image.png" alt=""></p>
<p>마지막 프로젝트를 끝내고 공식적으로 부트캠프 일정이 끝이났다.
길다면 길고 짧다면 짧은 6개월이였는데, &#39;와~ 끝이다!!&#39; 하는 생각은 들지 않는다 (오히려 할게 더 많아진...)
그래도 처음 시작할 때 나와 지금의 나를 비교하면, 그래도 뭔가 열심히 하긴 했구나 한다.</p>
<p>늦은 나이에 비전공자로서 개발자전향을 마음먹고 부트캠프를 알아볼 때 여기가 좋다, 여기가 안좋다 등 정보과다로 선택에 어려움이 있었다. 나도 사실 처음엔 백엔드를 지향하고 있었기에 spring 기반 커리큘럼을 가지고 있는 부트캠프를 알아보던 중 부득이한 사정으로 node.js 기반 엘리스 트랙에 참가하게되었는데 
비전공자 기준으로 어떤 경우에 엘리스 트랙을 진행하는게 좋은지, 좋았던점과 아쉬웠던 부분을 담백하게 적어보자 한다.</p>
<hr>
<h3 id="어디든-밥을-떠먹여주진-않는다">어디든 밥을 떠먹여주진 않는다.</h3>
<p>부트캠프를 알아보고있다면 이러한 후기를 가장 많이 볼 수 있다.
&#39;부트캠프에서 해주는게 없다&#39;, &#39;무책임하게 방목한다&#39; 등등...</p>
<p>오프라인은 해보지않아서 잘 모르겠지만, 만약 온라인 과정을 알아보고있다면 어디를 가든 이건 비슷할것 같다.
온라인으로 진행할 경우 약간의 제약 빼고는 (출결) 높은 자유도를 보장하는데, 그말은 즉 나를 붙잡아줄 족쇄가 없다는 뜻이다. 캠프측에서 제공하는 학습은 제한적이고, 그러기에 스스로에게 질문을 던지며 꼬리에 꼬리를 물고 공부를 해나가야한다.</p>
<h3 id="선행은-필수다">선행은 필수다.</h3>
<p>자신이 퓨어한 비전공자이고, 관련지식이 전무한 상태에서 부트캠프에 합격해서 트랙이 시작되길 기다리고있다면 <del>정신차리고</del>
이 꽉 깨물고 선행학습을 해야한다. 물론 커리큘럼이 비전공자 눈높이에 맞춰 진행되긴 하지만 6개월이라는 짧은 기간동안 우겨넣기엔 상당히 무리고, 그 과정에서 중도하차하는 사람들이 상당히 많다.</p>
<p>난 전 직장을 퇴사하기 전 취미로 파이썬과 자바, sql을 핥아본정도는 되었는데 갑작스럽게 엘리스 트랙에 참가하게 되면서 눈물을 흘리며 JS를 공부했었다.</p>
<h3 id="직무-풀스텍">직무..? 풀스텍...?</h3>
<p>처음 엘리스 트랙을 진행하기로 마음먹은 이유 중 하나는 풀스텍이였다.
자바스크립트 하나로 프론트와 백 둘 다를 할 수 있다고...? <del>개꿀</del></p>
<p>응 어림도없다. 자신이 전공자이거나 유니콘이라고 생각하지 않는다면 구름에서 내려오자.</p>
<p>AI 같은 경우 트랙을 시작하기 전 부터 openAPI 를 이용해서 서비스에 접목하는 정도가 아닐까? 라고 생각하고 들어와서 별다른 생각은 없었는데, AI부분에 큰 기대를 가지고 오시는분들이 몇몇 있었던 것 같다. 하지만, 개인적인 생각으로 AI 관련 직무의 모집공고의 지원조건을 보면 관련학과 최소 학서, 석사 이상만 받고 있는걸 보면 해당 트랙으로 AI직무로 취업까진 좀 힘들지 않을까..? 그래서 해당 트랙에 AI 하나에만 기대를 가지고 참여하기엔 좀 잘못된 선택이 아닐까 생각한다. 물론 실제로 모델을 만들고 학습시켜 사용하긴 하지만 절대 주류로 다루지 않기때문에 AI 관련 직무를 희망한다면 다른 곳을 알아보는게 좋다 생각한다.</p>
<h3 id="그럼-무조건-프론트해야겠네">그럼 무조건 프론트해야겠네</h3>
<p><img src="https://velog.velcdn.com/images/daechan_jo/post/092eb594-7729-4a83-98af-a9d04b592d95/image.png" alt="">
나도 그랬었다. 주위 현업자들에게 물어봐도 백엔드로 node.js는 수요가 적기때문에 모두 프론트를 추천해줬었다. 하지만 인생은 내 마음대로 굴러가지 않는다.</p>
<p>어느정도 기본적인 학습이 끝나면 첫 프로젝트를 시작하게 되는데, 이 때 랜덤하게 팀원이 정해지고, 팀 내에서 직무를 조율해야하기때문에 이슈가생긴다. 당연하게도? 프론트직무 희망으로 과반수가 몰리게되었고 다소 소심한 성격의 나는 울면서 백엔드직무를 하게 되었던 기억이... (물론 해당 계기로 백엔드의 매력에 빠지게 되었지만)</p>
<p>그 뿐 만이 아니라 다음 프로젝트에는 학습진행률 등을 고려해 팀이 결정되는데 여기서도 직무결정은 뜨거운 감자였다.
우리 기수가 유독 심했다곤 하는데, 첫 프로젝트 때 프론트의 매운맛을 보고 전부 백엔드직무를 희망하게되면서 프론트희망자가 부족해지는 상황이 발생했다. 하지만 이 때 나는 백엔드로 쭉 밀고나가기로 마음먹었을 때라 이번에도 눈치보면서 이력서에 첨부하게될 포트폴리오가 될지도 모르는 프로젝트에서 백엔드직무를 포기하기 싫었고 다행이도? 어떻게든 무사히 프로젝트가 마무리 되었었다.</p>
<p>총 3번의 프로젝트에서 동일한 이슈는 계속 발생하고 자신이 하고싶은것과 팀 전체를 저울질해야하는 상황이 온다.</p>
<p>애초에 해당 트랙이 풀스텍을 목표로 진행된다곤 하지만 비전공자 입장에서 3번의 프로젝트에서 다양한 직무를 소화하기엔 트랙이 종료된 후 너무 얕은 지식만 남는게 아닐까 하는 생각이 든다.</p>
<p>물론 프론트와 백엔드 모두 훌륭하게 소화하시는 분들도 많다. 자신이 정확히 무엇을 하고싶은지 미리 생각해두는게 좋다.</p>
<h3 id="투잡은-지양해야한다">투잡은 지양해야한다</h3>
<p>진짜 어쩔 수 없는 경우가 아니라면 투잡은 체력적으로도, 정신적으로도 힘들다.
부트캠프 기간동안 (취업하기 전 까지면 더 좋고..) 학업에 전념할 수 없는 상황이라면 추전해주기 어렵다.
학습기간엔 많은양의 새로운 기술들을 공부해야되고, 그 기술을 어떻게 사용해야하는지는 어디까지나 본인이 스스로 공부해야하기에 어지간한 체력과 정신력이 아니면 힘들다고 생각한다. </p>
<p>마지막 프로젝트 때, 불가피하게 저녁시간 때에 다른 일을 하게 되었는데 일하면서 다른 팀원들 요청에 응답도 해줘야하고, 일하는 시간만큼 생긴 구멍을 새벽에 잠못자고 메꾸느라 죽을맛이였다.</p>
<h3 id="실시간-온라인-강의">실시간 온라인 강의</h3>
<p>프로젝트 기간을 제외하고 매주 2번의 실시간 강의를 진행하는데, 조금 냉정하게 말하자면  강의자가 누군가에따라 편차가 크다.</p>
<p>물론 코치님들의 실력에 문제가 있는건 절대아니다.
나 같은 경우는 코치님의 강의스타일, 목소리, 말투 등 (?뭐햐냐 진짜)  조금이라도 루즈하거나 나와 맞지 않으면 강의를 듣는것 자체가 고통이였다. 
실시간 강의라곤 하지만 강의자료를 제공해주기도하고 동일한 내용이 커리큘럼에 포함되어있기 때문에 집중이 안될때면 혼자 구글링하거나 책을 보고 공부했었던거같다. 
집중도 못하고 강의만 틀어놓은 채 좀비처럼 앉아있을 바엔 과감하게 실강을 버리고? 자기주도적 학습을 하는게 차라리 좋다고 생각한다</p>
<h3 id="남는건-사람이다">남는건 사람이다</h3>
<p>트랙을 달리다보면 정말 감사한 분들을 많이 만나게 된다.</p>
<p>항상 진심을 다해 응원하고 감시?해주는 매니저님
같이 밤을 새워가며 동고동락한 팀원들
현업에 종사하면서 새벽에도 질문에 답해주시는 코치님
<del>나랑 머리뜯고 싸우신 분</del></p>
<p>사실 내가 개발자로 전향하기 마음먹은 이유 중하나가 사람을 피하기 위한 이유도 있었지만<del>이럴줄몰랐지</del>
애석하게도 개발바닥은 그렇게 호락호락하지 않았다.</p>
<p>개발자로서 살아가기 위해선 커뮤니케이션이 필수이고, 피할 수 없는 과정이다.
부트캠프를 시작할 때, 개인적인 일로 정서적으로 많이 불안했었고 그렇기에 사람을 밀어내는 경향이 있었다.
프로젝트를 진행하며 함께 고민하고, 해결하며 성장하는 과정에서 서로의 강점을 이해하고 약점을 보완해 나가면서 조금은 날서있던 나 자신이 말랑말랑해진것 같다.</p>
<p>부트캠프를 통해 얻는 지식과 기술도 중요하지만, 그 이상으로 사람과의 인연은 더욱 값진 것이라고 생각한다. 이러한 인연을 통해 서로가 서로에게 도움이 되고, 함께 성장할 수 있는 기회를 얻을 수 있다고 생각한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿼리 최적화]]></title>
            <link>https://velog.io/@daechan_jo/%EC%BF%BC%EB%A6%AC-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@daechan_jo/%EC%BF%BC%EB%A6%AC-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Sat, 28 Oct 2023 05:02:12 GMT</pubDate>
            <description><![CDATA[<p>쿼리 최적화의 중요성을 몸소 체험하게 되었다.
그 동안 딱히 복잡한 쿼리가 없었기에 실감하지 못했는데 이번 프로젝트에서 딱 봐도 서비스 내에서 가장 무거워보이는 쿼리를 장난삼아 부하테스트를 해보다가 <del>내가만든거지같은쿼리</del> 심각성을 깨달았다.</p>
<hr>
<p>영단어 학습 웹서비스를 개발중이던 우리는 사용자가 회원가입 하기 전, 어떻게 학습이 진행되는지 체험시켜주기 위해 10개의 랜덤한 단어를 순차적으로 보여주고, 각 단어에 사지선다를 제공해 재미삼아 문제를 풀어볼 수 있도록 해주기로 했다. 문제는 로그인한 사용자가 아니기에 모든 데이터를 한번에 보내주고 클라이언트에서 관리해야기에 쿼리가 상당히 뚱뚱해졌었다.</p>
<pre><code>요청 
-&gt; Word테이블에서 랜덤한 단어 10개 조회 
-&gt; 각 단어마다 랜덤한 4지선다를 제공하기 위해 Word테이블에서 랜덤으로 3개의 단어를 조회
-&gt; 셔플 후 반환</code></pre><p>한 번도 쿼리성능에 대해 깊게 생각해본적 없던 나는 쿼리를 루프안에서 사용하기 시작했고 <del>멈춰</del> 그렇게 부하테스트에서 충격적인 결과를 보고 쿼리를 확인해봤을 때, 한 번의 요청에 무려 31번의 쿼리가 실행되고있었다.</p>
<p><em>30초동안 초당 10번의 요청도 버티지 못하는 처참한 결과..</em></p>
<pre><code>errors.ETIMEDOUT: .............................................................. 188
http.codes.200: ................................................................ 18
http.codes.500: ................................................................ 94
http.downloaded_bytes: ......................................................... 64766
http.request_rate: ............................................................. 10/sec
http.requests: ................................................................. 300
http.response_time:
  min: ......................................................................... 116
  max: ......................................................................... 9566
  median: ...................................................................... 788.5
  p95: ......................................................................... 7117
  p99: ......................................................................... 9416.8
http.responses: ................................................................ 112
vusers.completed: .............................................................. 112
vusers.created: ................................................................ 300
vusers.created_by_name.0: ...................................................... 300
vusers.failed: ................................................................. 188
vusers.session_length:
  min: ......................................................................... 119.3
  max: ......................................................................... 9575.1
  median: ...................................................................... 788.5
  p95: ......................................................................... 7117
  p99: ......................................................................... 9416.8</code></pre><br />

<blockquote>
<h3 id="최적화-진행">최적화 진행</h3>
</blockquote>
<p>일단 현재 가장 많은 쿼리를 발생시키고 있는, 사지선다를 만들기 위한 쿼리부터 어떻게할까 고민했었다. 매번 요청이 들어올때마다 새로 만드는 것 보다 미리 3개의 뜻을 그룹화해서 테이블로 만들고 랜덤하게 조회된 단어의 뜻과 셔플을 해주면 되겠다 싶었다. 그러면 반복문을 사용하지 않아도 되기에 쿼리가 단 두 번으로 줄어들었다.</p>
<p><em>최적화 진행 후 동일한 조건으로 테스트해봤을 때 많이 안정적인 모습...</em></p>
<pre><code>http.codes.200: ................................................................ 300
http.downloaded_bytes: ......................................................... 0
http.request_rate: ............................................................. 10/sec
http.requests: ................................................................. 300
http.response_time:
  min: ......................................................................... 20
  max: ......................................................................... 123
  median: ...................................................................... 24.8
  p95: ......................................................................... 30.9
  p99: ......................................................................... 41.7
http.responses: ................................................................ 300
vusers.completed: .............................................................. 300
vusers.created: ................................................................ 300
vusers.created_by_name.0: ...................................................... 300
vusers.failed: ................................................................. 0
vusers.session_length:
  min: ......................................................................... 22.7
  max: ......................................................................... 142.5
  median: ...................................................................... 29.1
  p95: ......................................................................... 37
  p99: ......................................................................... 64.7
</code></pre><hr>
<p>조금 더 올려서 30초간 초당 30번, 총 900번의 요청을 보내보았다</p>
<pre><code>http.codes.200: ................................................................ 600
http.codes.429: ................................................................ 300
http.downloaded_bytes: ......................................................... 12600
http.request_rate: ............................................................. 30/sec
http.requests: ................................................................. 900
http.response_time:
  min: ......................................................................... 0
  max: ......................................................................... 554
  median: ...................................................................... 80.6
  p95: ......................................................................... 98.5
  p99: ......................................................................... 223.7
http.responses: ................................................................ 900
vusers.completed: .............................................................. 900
vusers.created: ................................................................ 900
vusers.created_by_name.0: ...................................................... 900
vusers.failed: ................................................................. 0
vusers.session_length:
  min: ......................................................................... 1.1
  max: ......................................................................... 564.2
  median: ...................................................................... 83.9
  p95: ......................................................................... 100.5
  p99: ......................................................................... 228.2</code></pre><p>서버를 보호하기 위해 사용자 당 분당 요청을 600번으로 제한했기에 300번의 요청은 429를 반환하고, 나머지 요청들은 모두 성공한걸 확인할 수 있었다.</p>
<hr>
<p>그럼 반대로, 짧은 시간동안 더 많은 요청을 보내보기로 했다.</p>
<pre><code>errors.ETIMEDOUT: .............................................................. 74
http.codes.200: ................................................................ 226
http.downloaded_bytes: ......................................................... 0
http.request_rate: ............................................................. 23/sec
http.requests: ................................................................. 300
http.response_time:
  min: ......................................................................... 6801
  max: ......................................................................... 9987
  median: ...................................................................... 9230.4
  p95: ......................................................................... 9801.2
  p99: ......................................................................... 9999.2
http.responses: ................................................................ 226
vusers.completed: .............................................................. 226
vusers.created: ................................................................ 300
vusers.created_by_name.0: ...................................................... 300
vusers.failed: ................................................................. 74
vusers.session_length:
  min: ......................................................................... 6804.8
  max: ......................................................................... 9991.2
  median: ...................................................................... 9230.4
  p95: ......................................................................... 9801.2
  p99: ......................................................................... 9999.2</code></pre><p>10초간 초당 30번의 요청을 보냈을 때 세션 길이도 많이 길어지고 74번의 타임아웃이 발생했다.</p>
<p>최적화 하기 전 30초간 초당 10번의 요청에 188번의 타임아웃을 발생시키던 때 보단 확실히 좋아지긴했지만 이정도면 쓸만한 상태인건지 확신이 들지 않는다.</p>
<p>해당 api가 그렇게 많이 사용되지 않는다면 괜찮지 않을까?
그렇다고 여기서 더 쿼리를 간소화할 방법이 생각나지 않는다
그러면 해당 서비스의 메커니즘 자체를 바꿔야하나..?</p>
<p>재밌다.
사실 여러번의 프로젝트를 거치면서 반복적인 작업에 조금은 지루함을 느끼고 있던 차에 새로운 문제를 발견하니 오히려 설레는 마음이 컸던것같다 <del>변태아니냐고</del></p>
<p>시간이 지날수록 코드를 작성하는 시간보다 노려보는 시간이 길어진다고 했었는데, 그 말이 조금씩 이해가 되어가고 있는 코린이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[docker-compose 사용기
]]></title>
            <link>https://velog.io/@daechan_jo/docker-compose-%EC%82%AC%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@daechan_jo/docker-compose-%EC%82%AC%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Sat, 21 Oct 2023 10:21:45 GMT</pubDate>
            <description><![CDATA[<p>언젠가는 부딪혀야할 docker 와 AWS.. <del>죽여줘</del>
뭐든지 처음이 어렵다고 도커를 이용한 배포는 처음이라 죽을맛이였따</p>
<blockquote>
<p>환경</p>
</blockquote>
<ul>
<li>개발 환경 : Apple M1 Pro (macOS Sonoma 14.1)</li>
<li>Server : express.ts + MySql (prisma)</li>
<li>VM : Ubuntu 20.04.6 LTS</li>
</ul>
<p>프록시 서버로 nginx를 이용하면 대략 다음과 같은 구조가 나온다.
<img src="https://velog.velcdn.com/images/daechan_jo/post/3fcaf8fe-874b-4f03-a723-d3cfde6f655b/image.png" alt=""></p>
<blockquote>
<ol>
<li>Dockerfile 작성</li>
</ol>
</blockquote>
<p>이 방법에선 프론트는 빌드한 코드를 직접 nginx 컨테이너에 마운트 해줄거기 때문에 도커이미지를 생성할 필요가 없다.</p>
<p>일단 도커이미지를 생성하기 위해 서버 루트경로에 Dockerfile을 생성한다.</p>
<pre><code>FROM node:16-buster

LABEL authors=&quot;daechanjo&quot;

WORKDIR /app

COPY package*.json ./
COPY . .

RUN npm install

EXPOSE 5001

CMD [&quot;yarn&quot;, &quot;start&quot;]</code></pre><ul>
<li>FROM : docker 이미지를 생성할 때 기본 이미지(바탕)을 지정한다. 여기에선 debian linux 배포의 buster 버전을 기반으로 한 16버전을 사용했다.</li>
<li>LABEL : 메타데이터</li>
<li>WORKDIR : 컨테이너 내에서 작업 디렉토리를 &#39;/app&#39; 으로 설정한다. 이후의 모든 명령은 해당 디렉토리에서 실행된다</li>
<li>COPY : 호스트에서 해당 파일을 WORKDIR 로 지정해준 경로에 복사한다. 이 때 docker의 레이어 캐싱 메커니즘이 활용되는데, 패키지 파일이 복사될 때 해당 단계를 캐시하고 패키지 파일이 변경되지 않는다면 이미지를 빌드할 때마다 Node.js 종속성을 다시 설치할 필요가 없다.</li>
<li>RUN : 이 단계에서 컨테이너 내에서 <code>npm install</code> 을 실행하게 된다.</li>
<li>EXPOSE : 컨테이너 내에서 어떤 포트를 노출시킬건지 정하는 명령어이다. 실제로 포트를 호스트 시스템에 공개하진 않는다.</li>
<li>CMD : 해당 이미지를 기반으로 컨테이너가 시작될 때 실행할 명령어를 지정한다. 후에 docker-compose.yml 에서 추가적으로 설정할거라 생략해도 되지만 충돌이 일어난다거나 그런건 아니라서 나중을 위해(개별로 테스트한다던지..) 미리 작성해두자.</li>
</ul>
<p>요약하면 node.js 환경을 설정하고 응용 프로그램의 종속성을 설치하며 해당 컨에티너 내에서 포트5001을 노출하고 컨테이너가 실행될 때 응용 프로그램을 시작하는 명령어라고 볼 수 있다. </p>
<p>이제 해당 파일을 이미지로 빌드하기만 하면 되는데 여기에서 첫 번째 문제가 발생했다.</p>
<blockquote>
<ol start="2">
<li>docker build (M1 너란 녀석...)</li>
</ol>
</blockquote>
<p>빌드를 마치고 해당 이미지를 도커허브에 올린 뒤, VM에서 이미지를 불러와 실행시켰는데, 컨테이너가 정상적으로 실행되었다가 바로 종료되어버렸는데 이유는 m1(arm64) 에서 생성한 도커이미지가 vm(amd64)에 호환이 안되는 이슈였다.</p>
<p>해결법을 찾아보니 docker 이미지를 빌드할 때 멀티플랫폼을 지원해주기에 해당 기능을 이용하기로 했다. (해당 빌드과정에서 멀티플랫폼 또한 지원해주지 않는 이상한 에러에 막혔는데 어차피 amd64로만 빌드되면 장땡?이기에 멀티플랫폼을 빠르게 손절했다..)
명령어 마지막에 . 을 놓치지 말자</p>
<p><code>docker buildx build --platform=linux/amd64 --load --tag 계정/이미지이름:태그 .</code></p>
<pre><code class="language-[+]"> =&gt; [internal] load .dockerignore                                                                                                                                                                                    0.0s
 =&gt; =&gt; transferring context: 148B                                                                                                                                                                                    0.0s
 =&gt; [internal] load build definition from Dockerfile                                                                                                                                                                 0.0s
 =&gt; =&gt; transferring dockerfile: 184B                                                                                                                                                                                 0.0s
 =&gt; [internal] load metadata for docker.io/library/node:16-buster                                                                                                                                                    1.8s
 =&gt; [auth] library/node:pull token for registry-1.docker.io                                                                                                                                                          0.0s
 =&gt; [1/5] FROM docker.io/library/node:16-buster@sha256:f77a1aef2da8d83e45ec990f45df50f1a286c5fe8bbfb8c6e4246c6389705c0b                                                                                              0.0s
 =&gt; [internal] load build context                                                                                                                                                                                    0.0s
 =&gt; =&gt; transferring context: 19.20kB                                                                                                                                                                                 0.0s
 =&gt; CACHED [2/5] WORKDIR /app                                                                                                                                                                                        0.0s
 =&gt; CACHED [3/5] COPY package*.json ./                                                                                                                                                                               0.0s
 =&gt; [4/5] COPY . .                                                                                                                                                                                                   0.2s
 =&gt; [5/5] RUN npm install                                                                                                                                                                                           36.5s
 =&gt; exporting to image                                                                                                                                                                                               2.7s
 =&gt; =&gt; exporting layers                                                                                                                                                                                              2.7s
 =&gt; =&gt; writing image sha256:39414574afead4a824ab5bc7dd963d4561a4a3005d0a11f86a11802564e4945c                                                                                                                         0.0s 
 =&gt; =&gt; naming to docker.io/daechanjo/wordy:0.1.0 </code></pre>
<p> 그러면 Dockerfile에 작성한 명령어 시퀀스를 관람할 수 있다.
 앞서 말한 레이어 캐싱 메커니즘도 확인해볼 수 있다.</p>
<p> 이제 생성한 이미지를 docker 허브에 올려주고, vm에서 다시 받아오면 이미지 준비과정은 끝이난다.</p>
<ul>
<li>local : <code>docker push 계정/이미지이름:태그</code></li>
<li>vm : <code>docker pull 계정/이미지이름:태그</code></li>
</ul>
<blockquote>
<p>환경변수 및 nginx.conf</p>
</blockquote>
<p>docker-compose 를 설정하기 전 서버에서 사용할 환경변수와 nginx.conf 를 설정해주자. 서버와 nginx는 컨테이너로 띄울거기 때문에 외부 작업 디렉토리 경로에서 작성후 후에 마운트해주면 된다</p>
<h3 id="nginxconf">nginx.conf</h3>
<pre><code>worker_processes auto;

events {
    worker_connections 1024;
}

http {
    server {
        listen 80;
        server_name localhost;

        location / {
            root /usr/share/nginx/html;
            try_files $uri $uri/ /index.html;
        }

        location /api/ {
            proxy_pass http://서버컨테이너이름:포트;    &lt;- 알맞게수정
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection &#39;upgrade&#39;;
            proxy_set_header Host $host;
            proxy_cache_bypass $http_upgrade;
         }
    }
}</code></pre><p>이 설정은 nginx를 리버스 프록시 서버로 설정하여 HTTP 요청을 처리하고 요청 경로에 따라 정적 html파일로 갈지, 서버로 요청을 보낼지 결정해준다. <del>하나하나 뜯고 맛보고 즐겨보자</del></p>
<ul>
<li><p>worker_processes auto
이 라인은 nginx가 들어오는 요청을 처리하는 데 사용할 워커 프로세스의 수를 지정한다. &quot;auto&quot; 옵션은 nginx에게 CPU 코어 수를 기반으로 워커 프로세스의 수를 자동으로 결정하도록 지시한다.</p>
</li>
<li><p>events
이벤트 처리와 관련된 설정을 정의한다. 각 워커 프로세스가 동시에 처리할 수 있는 최대 연결 수를 1024로 설정</p>
</li>
<li><p>http
서버 구성</p>
</li>
<li><p>server
이 블록은 nginx가 포트 80(HTTP)에서 대기하는 가상 서버 구성을 정의한다. &quot;localhost&quot;라는 서버 이름과 함께 HTTP 요청을 처리하는 기본 서버 블록이 된다.</p>
</li>
<li><p>location
nginx가 루트 경로 &quot;/&quot;와 &quot;/api&quot; 에 일치하는 요청을 처리하는 방법을 정의한다. 즉, 각 요청의 경로에 따라 프론트로 보낼지, 백엔드로 보낼지 결정한다고 볼 수 있다.</p>
</li>
</ul>
<hr>
<p>그리고 서버에서 사용할 환경변수를 .env을 생성해 작성해주면 된다. 이 때 주의할 점은 리눅스환경에서 따옴표와 쌍따옴표를 잘못사용하면 하나의 값으로  처리되기때문에 생략해주어야 한다. 간혹 이퀄이 포함된 특수문자는 불가피하게 따옴표를 써줘야하는데 그럴 땐 `` 로 감싸주면 된다.</p>
<pre><code>ex)
SERVER_URL=http://00.00.000.000:8080</code></pre><p>이제 거의 다왔다...
<br />
<br /></p>
<blockquote>
<ol start="3">
<li>docker-compose.yml</li>
</ol>
</blockquote>
<pre><code>version: &#39;3&#39;
services:
  db:
    image: mysql
    container_name: db-container
    env_file: .env
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
    ports:
      - &quot;5000:3306&quot;
  nginx:
    image: nginx
    container_name: nginx-container
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./front/build:/usr/share/nginx/html
    ports:
      - &quot;80:80&quot;
    depends_on:
      - server
  server:
    image: daechanjo/wordy:0.1.0
    container_name: server-container
    ports:
      - &quot;5001:5001&quot;
    env_file: .env
    environment:
      DATABASE_URL: ${DATABASE_URL}
      SERVER_URL: ${SERVER_URL}
      SERVER_PORT : ${SERVER_PORT}
      JWT_SECRET_KEY: ${JWT_SECRET_KEY}
      JWT_TOKEN_EXPIRES: ${JWT_TOKEN_EXPIRES}
      SESSION_SECRET_KEY: ${SESSION_SECRET_KEY}

      GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
      GOOGLE_SECRET_KEY: ${GOOGLE_SECRET_KEY}
    command : [&quot;sh&quot;, &quot;-c&quot;, &quot;npx prisma migrate deploy &amp;&amp; yarn start&quot;]

     depends_on :
       db :
         condition : service_healthy</code></pre><p>최상단의 버전도 꼭 기입해줘야 한다. <del>없으면 에러</del>
중복항목은 생략하면서 설명하면 다음과 같다.</p>
<ul>
<li>db 서비스
image: pull 받아온 mysql 이미지
container_name: 해당 서비스를 띄울 컨테이너 이름
env_file: .env 파일을 사용하여 환경 변수를 로드하며, 해당 환경 변수로 MySQL 루트 비밀번호 및 데이터베이스 이름을 설정
ports: 호스트의 포트 5000을 컨테이너 내부의 MySQL의 포트 3306과 연결</li>
</ul>
<ul>
<li><p>nginx 서비스
volumes: 호스트의 nginx.conf 파일을 컨테이너 내부의 /etc/nginx/nginx.conf로 볼륨 마운트 한다.
호스트의 front/build 디렉터리를 컨테이너 내부의 /usr/share/nginx/html 디렉터리로 볼륨 마운트하여 정적 웹 페이지를 제공한다.
depends_on : nginx 서비스는 server 서비스에 의존하며, server 서비스가 실행되고 난 후 nginx 서비스를 실행시키도록 한다.</p>
</li>
<li><p>server 서비스:
command: npx prisma migrate deploy &amp;&amp; yarn start 명령을 실행하여 Prisma 마이그레이션을 배포하고 애플리케이션을 시작한다.
server 서비스는 db 서비스에 의존하며, db 서비스가 정상 상태일 때 서버 컨테이너를 실행시킨다.</p>
</li>
</ul>
<hr>
<p>이렇게 길고 험난했던 설정이 끝이나면 <del>하...</del>
명령어 한줄로 지정한 서비스들의 컨테이너를 한 번에 띄우고 관리할 수 있게 된다. 여기서 -d 옵션은 백그라운드 옵션!</p>
<pre><code>docker-compose up -d
Creating network &quot;elice_default&quot; with the default driver
Creating db-container ... done
Creating server-container ... done
Creating nginx-container  ... done</code></pre><p>컨테이너가 정상적으로 생성되었다고 에러없이 실행중일거란 보장은 없다. docker ps로 실행중인 컨테이너의 상태를 확인하자..</p>
<br />

<p>컨테이너를 한번에 띄웠으니, 한번에 내리는것도 가능하다 <del>짱편해</del></p>
<pre><code>docker-compose down
Stopping nginx-container  ... done
Stopping server-container ... done
Stopping db-container     ... done
Removing nginx-container  ... done
Removing server-container ... done
Removing db-container     ... done
Removing network ..._default
</code></pre><hr>
<p>다시 배포를 하고 브라우저로 접속되면 성공이다.
단 nginx를 프록시서버로 사용하고 있기 때문에 모든 통신은 nginx를 거친다는 점을 기억하자.</p>
<br />
<br />

<hr>
<h3 id="추가">추가</h3>
<p>빌드한 프론트 파일을 어떻게 vm으로 옮길까</p>
<ol>
<li><p>vm 작업 디렉터로에 빌드파일을 저장할 경로를 생성해준다
권환설정을 꼭 해줘야한다. 안그러면 외부에서 파일을 전송해줄 때 거절당한다
<code>sudo mkdir -p /front/build &amp;&amp; sudo chown $USER:$USER /front/build</code></p>
</li>
<li><p>이제 로컬에서 빌드한 프론트파일을 해당 경로로 전송해주면 된다
<code>scp -r ./build/* 계정@IP:/front/build</code></p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[node-cron 스케쥴러 사용기]]></title>
            <link>https://velog.io/@daechan_jo/node-cron-%EC%8A%A4%EC%BC%80%EC%A5%B4%EB%9F%AC-%EC%82%AC%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@daechan_jo/node-cron-%EC%8A%A4%EC%BC%80%EC%A5%B4%EB%9F%AC-%EC%82%AC%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Fri, 13 Oct 2023 16:02:21 GMT</pubDate>
            <description><![CDATA[<p>현재 프로젝트를 진행하면서 단순하고 반복되는 CRUD보단, 좀 더 생소하고 재밌는 기능들이 없을까 하다 생각해낸 몇가지 중 하나가 특정 시간에 데이터베이스를 조회하고, 조건에 맞는 유저에게 메일을 전송해보면 어떨가 생각했다.</p>
<p>하지만 내가 알고있는 서버는, 클라이언트의 요청이 없으면 움직이지 않는 아주 수동적인 자세를  취하는데 어떻게 하면 클라이언트의 간섭 없이 서버 내에서 이를 처리할 수 있을까 찾아보다 node-cron이라는 아주 간단하고 재밌는 패키지를 찾았다</p>
<blockquote>
<h4 id="그래서-그게-뭔데">그래서 그게 뭔데?</h4>
</blockquote>
<p>node-cron은 이름에서 알 수 있듯이 Node.js환경에서 cron(유닉스 계열 컴퓨터 운영 체제의 시간 기반 잡 스케줄러) 작업을 구현하기 위한 패키지이다.특정 시간에 주기적으로 실행되어야 하는 작업들을 관리한다. 즉 Node.js 애플리케이션 내에서 타이머 또는 주기적인 작업을 스케줄링하는 기능을 제공한다.</p>
<p>주요기능은 다음과 같다.</p>
<ul>
<li>시간 기반 스케줄링
표준 cron 구문을 사용하여 분, 시, 일, 월, 요일 등에 따라 주기적인 작업 실행 시간을 정의할 수 있다.</li>
<li>시작 및 중지
생성된 각 cron 작업은 start와 stop 메서드를 이용해서 언제든 시작하거나 중지할 수 있다</li>
<li>시간대 지원
v1.7.0 부터는 특정 시간대를 지정하여 작업이 실행될 시각을 제어할 수 있다</li>
<li>Promise와 async/await 지원
비동기 함수를 스케줄링하는 것도 가능하다</li>
</ul>
<br />

<blockquote>
<h4 id="사용법">사용법</h4>
</blockquote>
<p>사용법이 따로 있다고 말하기 민망할정도로 엄청 간단하다.
일단 npm을(또는 yarn) 사용해 node-cron을 설치해준다.
<code>npm istall --save node-cron</code>
<br /></p>
<p>그리고 작성해준다.</p>
<pre><code class="language-ts">import cron from &quot;node-cron&quot;;

cron.schedule(&#39;* * * * *&#39;, () =&gt; {
  console.log(&#39;running a task every minute&#39;);
}, {
    scheduled: false,
    timezone: &quot;Asia/Seoul&quot;
});</code></pre>
<p>놀랍게도 끝이다.</p>
<p>schedule 함수의 첫 번째 인자로 크론 식(crontab syntax)을, 두 번째 인자로 크론작업이 시잘될 때 호출될 콜백 함수를 넘어주면 된다. 세 번째 인자로는 옵션 객체를 넣어줄 수 있는데 scheduled로 작업을 즉시 시작할지, 아니면 호출될 때. 시작되도록 할지 설정할 수 있고 timezone 옵션으로 작업이 실행될 시간대(도시)를 지정할 수 있다.</p>
<p>첫 번째 인자로 받는 크론식은 다음을 참고하면 된다.</p>
<pre><code> # ┌────────────── second (optional)
 # │ ┌──────────── minute
 # │ │ ┌────────── hour
 # │ │ │ ┌──────── day of month
 # │ │ │ │ ┌────── month
 # │ │ │ │ │ ┌──── day of week
 # │ │ │ │ │ │
 # │ │ │ │ │ │
 # * * * * * *</code></pre><p>각각의 값에는 콤마 등 여러방법을 지원하기 때문에 공식문서를 참조하면 좋다
<a href="https://www.npmjs.com/package/node-cron">node-cron README</a></p>
<pre><code class="language-ts">cron.schedule(&#39;1,2,4,5 * * * *&#39;, () =&gt; { ...
cron.schedule(&#39;1-5 * * * *&#39;, () =&gt; { ...
cron.schedule(&#39;*/2 * * * *&#39;, () =&gt; { ...
cron.schedule(&#39;* * * January,September Sunday&#39;, () =&gt; { ...</code></pre>
<br />

<blockquote>
<h4 id="적용하기">적용하기</h4>
</blockquote>
<p>진행중인 프로젝트는 영단어 학습과 관련된 웹 서비스 프로젝트였고, 사용자가 특정 시간동안 학습을 하지 않으면 리마인드할 수 있게 메일링하는 기능을 만들고자 했다.</p>
<p>사용 방법 자체는 간단하지만 <del>물론 잘못된 정보를 보고 약간의 삽질을 하긴 했지만</del> 두 번째 인자로 넣어줄 콜백함수는 상당히 지저분하기 때문에 따로 모듈화를 해주었다. 어디에 작성하는게 좋을까 고민하다가 (nest.js 공부 해야돼..?) 데이터베이스를 조작하기에 services 경로에 작성했다. 
<em><del>참고로 아주많이 참고한 모 앱의 기능중 하나</del></em></p>
<pre><code class="language-ts">import cron from &quot;node-cron&quot;;
import nodemailer from &quot;nodemailer&quot;;
import { PrismaClient } from &quot;@prisma/client&quot;;

const prisma = new PrismaClient();

const logo: string | undefined = process.env.LOGO;

// 노드메일러 설정
let transporter = nodemailer.createTransport({
  service: &quot;gmail&quot;,
  auth: {
    user: process.env.NODE_MAILER_USER,
    pass: process.env.NODE_MAILER_PASS,
  },
});

// cron 정의
export const startScheduler = () =&gt;
// 테스트를 위해 매 초마다 스케줄링이 실행되도록 설정
  cron.schedule(&quot;* * * * * *&quot;, async (): Promise&lt;void&gt; =&gt; {
    console.log(&quot;⏰ :: 스케줄링 작업 실행...&quot;);

    const today: Date = new Date();

    const daysInKorean: string[] = [&quot;일&quot;, &quot;월&quot;, &quot;화&quot;, &quot;수&quot;, &quot;목&quot;, &quot;금&quot;, &quot;토&quot;];

    let studyDays: any[] = [];

// 과거 일주일 동안 각 날짜가 미학습 상태인지 정장할 배열을 초기화
    for (let i = 6; i &gt;= 0; i--) {
      let d: Date = new Date();
      d.setDate(today.getDate() - i);
      studyDays.push({
        day: daysInKorean[d.getDay()],
        studied: false,
      });
    }

// 당일 학습 여부를 확인할 변수 초기화
    let hasStudiedToday: boolean = false;

// 데이터베이스에서 유저들의 학습 진행 상황 조회
    const users = await prisma.user.findMany({
      select: {
        name: true,
        email: true,
        id: true,
        wordProgress: {
          select: {
            studiedAt: true,
          },
          orderBy: {
            studiedAt: &quot;desc&quot;,
          },
        },
      },
    });

// 각 유저에 대해 학습 진행 상황을 확인하고, 그에 따라 제목과 본문내용을 선택하여 이메일 전송 
//(여기서부턴 지극히 개인적인 복잡한 코드라 안보셔도 됩니다.)
    for (let user of users) {
      if (user.email &amp;&amp; user.wordProgress.length &gt; 0) {
        const lastStudiedAt: Date = new Date(user.wordProgress[0].studiedAt);
        const daysSinceLastStudy: number = Math.ceil(
          (today.getTime() - lastStudiedAt.getTime()) / (1000 * 60 * 60 * 24),
        );

        for (let progress of user.wordProgress) {
          let progressDayIndex: number =
            (today.getDate() - new Date(progress.studiedAt).getDate() + 7) % 7;
          if (progressDayIndex &gt;= 0 &amp;&amp; progressDayIndex &lt; 7) {
            studyDays[progressDayIndex].studied = true;
            if (new Date(progress.studiedAt).getDate() == today.getDate()) {
              hasStudiedToday = true;
            }
          }
        }
        let subject;
        if (daysSinceLastStudy === 1) {
          subject = &quot;[Wordy] 오늘이 끝나기 전에 보러 와주실거죠..?🥺&quot;;
        } else {
          subject = `[Wordy] ${daysSinceLastStudy}일 동안 못봤네요🥺`;
        }
        if (!hasStudiedToday) {
          let mailOptions = {
            from: process.env.EMAIL_USERNAME,
            to: user.email,
            subject: subject,

            // 본문은 HTML 형식의 문자열로 작성하며, 유저의 이름과 과거 일주일 동안의 학습 진행 상황, 오늘 학습한 여부 등을 포함
            html: `&lt;div style=&quot;text-align:center;&quot;&gt;
&lt;img src=${logo} alt=&quot;Wordy Logo&quot; /&gt;
            &lt;h1&gt;안녕하세요, ${user.name}님!&lt;/h1&gt;
            &lt;hr /&gt;
            &lt;h3&gt;학습 진행 상황을 알려드립니다&lt;/h3&gt;&lt;br /&gt;
            &lt;p&gt;${studyDays.map((day) =&gt; `${day.day}: ${day.studied ? &quot;😎&quot; : &quot;🫥&quot;}`).join(&quot; | &quot;)}&lt;/p&gt;
            ${
              hasStudiedToday
                ? &quot;&lt;p&gt;오늘도 이미 학습을 완료하셨군요! 멋져요 👍&lt;/p&gt;&lt;br /&gt;&quot;
                : &quot;&lt;p&gt;아직 오늘의 학습을 하지 않으셨다면, 지금 바로 시작해보세요!&lt;/p&gt;&lt;br /&gt;&quot;
            }
            &lt;p&gt;🙌🏻노력은 배신하지 않습니다🙌🏻&lt;/p&gt;
            &lt;br /&gt;
             &lt;a href=&quot;${process.env.SERVER_URL}&quot; style=&quot;
                display: inline-block;
                margin-top: 20px;
                padding: 10px 20px;
                background-color: #007BFF;
                color: white;
                text-decoration: none;
                border-radius: 5px;&quot;&gt;학습하러 가기&lt;/a&gt;
        &lt;/div&gt;`,
          };

          transporter.sendMail(mailOptions, function (error: Error | null): void {
            if (error) {
              console.log(error);
            } else {
              console.log(`메일 전송 : ${user.email}`);
            }
          });
        }
      }
    }
  });
</code></pre>
<p>이렇게 작성하고 앞서 말한 사소한 삽질이 있었는데, 이 상태로 서버를 킨다고 해당 스케쥴러가 자동으로 작동하진 않는다.<del>누가 된다 했다고</del> 생각해보면 아주 당연하고 간단한건데 app.ts에서 모듈화시킨 cron함수를 호출해줘야 한다. 위 코드를 예로 들자면 코드상 서버 포트가 열리고 listen 하기 전 <code>startScheduler()</code> 를 작성해주면 된다.</p>
<p>그런 다음 서버를 실행하면 스케쥴러가 작동하는 로그를 확인할 수 있다.
<img src="https://velog.velcdn.com/images/daechan_jo/post/ddafa844-c405-47b0-a8ed-be7ac3b91d24/image.png" alt=""></p>
<p>메일을 확인해보면 다음과 같이 <em><del>귀염뽀짝</del></em> 정상적으로 스케쥴러가 작동한걸 확인할 수 있다
<img src="https://velog.velcdn.com/images/daechan_jo/post/c41504e2-f917-4198-b0f0-e325dc58d310/image.png" alt=""></p>
<hr>
<p>클라이언트 요청 없이 서버사이드 내에서 무언가를 처리하니깐 엄청 재밌었다.
이 기능을 이용해서 서버사이드로 클라이언트의 간섭 없이 무언가를 주기적으로 확인하거나 다른 재밌는 기능을 만들 수 있을것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠키와 세션]]></title>
            <link>https://velog.io/@daechan_jo/%EC%BF%A0%ED%82%A4%EC%99%80-%EC%84%B8%EC%85%98</link>
            <guid>https://velog.io/@daechan_jo/%EC%BF%A0%ED%82%A4%EC%99%80-%EC%84%B8%EC%85%98</guid>
            <pubDate>Tue, 03 Oct 2023 15:47:14 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h4 id="토큰은-보안이슈가-많으니-세션을-사용해야-하나요">토큰은 보안이슈가 많으니 세션을 사용해야 하나요</h4>
</blockquote>
<p>어느 날 모의면접에 참관할 기회가 생겨 구경하고있었는데, 당시 면접을 진행해주시던 현직자분께서 이런말을 했었다.
&quot;보통 부트캠프에서는 편의성을 위해 토큰을 이용한 인증, 인가방식을 배우고 사용하는데, 세션으로 구현하는게 더 옳고 더 안전하다.&quot;</p>
<p>궁금해졌다. 지금까지 토큰을 이용한 인증인가만을 계속 사용하고있었고 세션은 서버에 무리가 갈 수 있기 때문에 JWT를 이용한 인증 인가방식을 요즘은 많이 사용한다 <del>카더라</del> 라고 얼핏 들었기에 이에 대해 깊게 생각하고 있지 않았었다.</p>
<p>그럼 여기에서 생긴 의문점은 토큰과 쿠키의 단점 (유실, 변조, 도난) 에도 불구하고 왜 쿠키를 사용해야 하는가</p>
<blockquote>
<h4 id="http-프토콜과-cookie">HTTP 프토콜과 Cookie</h4>
</blockquote>
<p>쿠키를 이해하기 앞서 HTTP 통신 특징에 대해 알아야 한다.</p>
<p>HTTP 프로토콜은 연결을 유지시키지 않고 상태가 없는 특성을 가진다.
즉, 서버가 클라이언트 요청에 응답을 하는 순간 HTTP 연결이 끓어진다. 이러한 HTTP 프로토콜의 특징은 웹에서 애플리케이션을 구현하는데 큰 걸림돌이 되었었다. 수많은 요청들 중 서버는 동일한 사용자의 요청을 구별해내기가 쉽지 않았고, 이를 해결하기 위해 쿠키라는 기술이 등장했다고 한다.</p>
<p>쿠키를 한 마디로 정의하면 브라우저와 서버가 HTTP 프로토콜을 이용해서 서버가 어떤 데이터를 브라우저 측에 저장한 후 다시 그 데이터를 받아오는 기술 또는 그 데이터 자체를 뜻한다. </p>
<h4 id="짤막-쿠키-메커니즘">짤막 쿠키 메커니즘</h4>
<ul>
<li>서버가 어떤 쿠키를 브라우저에 저장하고 싶다.</li>
<li>하지만 서버는 클라이언트 요청이 없다면 데이터를 내보낼 수 없다.</li>
<li>하나의 요청에 하나의 쿠키만 응답 가능하다    </li>
</ul>
<blockquote>
<h4 id="쿠키와-세션의-상호보안">쿠키와 세션의 상호보안</h4>
</blockquote>
<p>쿠키와 세션을 별개의 기술로 생각하기 쉽지만, 사실 서로 상호보안하는 기술이고 알게모르게 혼용하고 있다.</p>
<p>예를 들어 JWT를 이용한 인증, 인가방식은 사용자가 인증을 통과하면 서버에서는 설정한 시크릿키와 페이로드를 이용해 해싱된 토큰을 발급하고, 사용자는 이를 쿠키에 저장하고 다른 요청 헤더에 첨부해 인가를 진행하게 된다. 
세션도 이와 비슷하게 인증을 통과하면 세션을 생성하고, 세션을 식별할 수 있는 고유한 ID를 쿠키에 저장하고 마찬가지로 인가가 필요한 요청에 사용하게 된다.</p>
<blockquote>
<h4 id="아니-세션이-더-안전하다며">아니 세션이 더 안전하다며?</h4>
</blockquote>
<p>또 다시 의문점이 생겼다.</p>
<p>결국 토큰도, 세션 식별자도 쿠키에 보관하기에 탈취위험은 동일해진다.
어차피 동일한 리스크를 감수해야한다면 서버의 자원을 소모하는 세션이 더 안좋은거 아닌가..?
보안적인 관점으로 보면 서버에서 상태관리를 하지 않기에 유효기간이 더욱 한정적인 토큰 인증방식이 더 안전해 보이기까지 한다. <del>또이또이</del></p>
<p>굳이 사용하자면 토큰(JWT) 과 세션을 혼용해서 서버에서도 클라이언트의 로그인 상태를 제어할 수 있게 하는정도이지 않을까..?</p>
<p>토큰이 세션보다 보안에 더 취약하다고 일괄적으로 말하는 것은 정확하지 않을 수 있으며, 각 방식의 특징과 요구사항을 이해한 후 그에 맞게 선택 및 구현하는 것이 중요한 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[네트워크 통신과 멱등성]]></title>
            <link>https://velog.io/@daechan_jo/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%ED%86%B5%EC%8B%A0%EA%B3%BC-%EB%A9%B1%EB%93%B1%EC%84%B1</link>
            <guid>https://velog.io/@daechan_jo/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%ED%86%B5%EC%8B%A0%EA%B3%BC-%EB%A9%B1%EB%93%B1%EC%84%B1</guid>
            <pubDate>Mon, 25 Sep 2023 03:44:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h3 id="멱등성의-의미">멱등성의 의미</h3>
</blockquote>
<p>멱등(idempotent) 이라는 단어는 수학에서 유래되었다고 한다. 어떤 연산을 여러 번 적용하더라도 결과가 변하지 않는 성질을 가리킨다. 이 성질은 분산 시스템 또는 네트워크 통신 환경에서도 유용하게 사용할 수 있다</p>
<blockquote>
<h3 id="http-메서드와-멱등성">HTTP 메서드와 멱등성</h3>
</blockquote>
<p>HTTP 프로토콜의 설계 원칙 중 하나가 바로 멱등성이다.
좀 더 쉽게 설명하자면 HTTP 통신에서의 멱등성이란, 동일한 요청이 여러번 수행되어도 동일한 응답값을 줄 수 있다면 멱등성이 지켜지고 있다고 볼 수 있다. 보통 멱등성을 가져야하는 메서드는 다음과 같다.</p>
<ul>
<li><p>GET : 리소스를 조회하는 용도로 사용되며 서버의 상태를 변경시키지 않는다. 따라서 같은 GET 요청에 대해서 항상 같은 응답이 반환되어야 한다.</p>
</li>
<li><p>PUT : 리소스를 지정한 상태로 만든다. 이미 지정된 상태와 동일한 상태로 요청하더라도 서버의 상태가 바뀌지 않는다.</p>
</li>
<li><p>DELETE : 리소스를 삭제하는데 사용된다. 이미 삭제된 리소스에 대해 다시 DELETE 요청을 보내더라도 그 결과에는 변화가 없다</p>
</li>
</ul>
<p>반면 POST 메서드는 원칙적으로 비멱등 메서드이다. POST 요청은 서버의 상태를 변경시키므로 같은 요청을 여러 번 보내면 서버의 상태가 여러번 바뀔 수 있다.</p>
<p>그렇다면 POST 메서드도 무조건 멱등하게 설계를 해야하는가?
꼭 그렇지만은 않지만 분산 시스템에서 네트워크 장애 등으로 동일한 요청이 중복해서 발생할 수 있으므로, 가능한 한 많은 연산들이 멱등하도록 설계되는게 좋다고 볼 수 있다. 그러면 클라이언트가 안전하게 같은 요청을 다시 보낼 수 있고, 그 결과로 시스템 전체의 안정성과 일관성을 유지할 수 있다.</p>
<hr>
<p>예시로 가장 기본적인 회원가입 API를 살펴보자</p>
<pre><code class="language-typescript">export const createUser = async (
    req: Request,
    res: Response,
    next: NextFunction,
) =&gt; {
    try {
        const { email, password, name, nickname } = req.body;
        const { emailExists, nicknameExists } =
            await authService.signUpDuplicateCheck(email, nickname);
        if (emailExists)
            return res
                .status(409)
                .json({ message: &quot;이미 존재하는 이메일입니다.&quot; });
        if (nicknameExists)
            return res
                .status(409)
                .json({ message: &quot;이미 존재하는 닉네임입니다.&quot; });
        const hashedPassword = await bcrypt.hash(password, 10);
        const newUser = await authService.createUser({
            email,
            name,
            nickname,
            password: hashedPassword,
        });
        return res.status(201).json({
            message: `회원가입에 성공했습니다 :: ${newUser.nickname}`,
        });
    } catch (error) {
        console.error(error);
        next(error);
    }
};</code></pre>
<p>사용자가 입력한 이메일과 닉네임이 중복검사를 통과하면 회원가입에 성공하고 201을 반환하지만, 만약 동일한 정보로 다시 요청을 보내면 409를 반환하게 설계되어있다. 즉 같은 요청을 반복할 때 동일한 응답을 하지 않기에 멱등성이 지켜지지 않다고 볼 수 있다.</p>
<p>이를 멱등하게 만들 가장 간단한 방법은 이미 존재하는 유저가 있을 경우 아무런 작업도 수행하지 않고 성공 메시지만 반환해버리면 된다.</p>
<p>하지만 그렇게되면 사용자에게 정확한 정보를 제공하지 못하게 된다.
멱등성과 사용자 경험을 동시에 만족시키기 위해서 먼저 유효성 검사를 수행하는 방법을 고려해볼 수 있다. 예를 들어 다음과 같이 이메일, 닉네임 유효성검사 API를 새롭게 추가한다</p>
<pre><code class="language-typescript">export const checkEmailOrNickname = async (
    req: Request,
    res: Response,
    next: NextFunction,
) =&gt; {
    /**
     * #swagger.tags = [&#39;Auth&#39;]
     * #swagger.summary = &#39;회원가입 이메일 및 닉네임 중복 체크&#39;
     */
    try {
        const email = req.query.email as string;
        const nickname = req.query.nickname as string;

        if (email) {
            const existingUserEmail = await authService.getUserByEmail(email);
            if (existingUserEmail)
                return res
                    .status(409)
                    .json({ message: &quot;이미 사용중인 이메일 입니다.&quot; });
            else
                return res
                    .status(200)
                    .json({ message: &quot;사용 가능한 이메일 입니다.&quot; });
        }

        if (nickname) {
            const existingUserNickname =
                await authService.getUserByNickname(nickname);
            if (existingUserNickname)
                return res
                    .status(409)
                    .json({ message: &quot;이미 사용중인 닉네임 입니다.&quot; });
            else
                return res
                    .status(200)
                    .json({ message: &quot;사용 가능한 닉네임 입니다.&quot; });
        }
    } catch (error) {
        console.error(error);
        next(error);
    }
};</code></pre>
<p>checkEmail 과 checkNickname 는 동일한 요청에 같은 응답을 반환하므로 멱등한 상태이다. </p>
<p>해당 함수를 이용해 회원가입 요청을 보내기 전에 폼에서 이메일 필드가 변경될 때마다 서버에 &#39;이메일 중복 확인&#39; 요청을 보내서 이미 등록된 이메일인지 미리 확인할 수 있다. 그러면 사용자가 실제로 회원가입 요청을 보내기 전에 이미 사용중인 이메일인지 알 수 있으므로, 불필요한 요청을 줄일 수 있고 사용자 경험도 향상시킬 수 있다. </p>
<p>단, 클라이언트 측에서의 유효성 검사만으론 충분하지 않기에 서버사이드에서도 동일한 유효성 검사를 수행하는게 안전성 측면에서 좋다.</p>
<blockquote>
<h3 id="get-put-delete-는-항상-멱등한가">GET, PUT, DELETE 는 항상 멱등한가</h3>
</blockquote>
<p>앞서 얘기할 때 POST를 제외하곤 원칙적으로 &#39;멱등하다&#39; 라고 할 수 있다 했지만 항상 그렇지는 않다. 다음은 게시글을 조회하고 조회수를 증가시키는 함수다</p>
<pre><code class="language-typescript">export const getPosts = async (
    req: Request,
    res: Response,
    next: NextFunction,
) =&gt; {
    try {
        const postId = Number(req.query.postId);
        const userId = Number(req.query.userId);
        const page = Number(req.query.page);
        const limit = Number(req.query.limit);
        if (postId) {
            const post = await postService.getPostByPostId(postId);
            if (!post)
                return res
                    .status(404)
                    .json({ message: &quot;존재하지 않는 게시글입니다.&quot; });
            const updatedViewCountPost =
                await postService.updatePostViewCount(postId);

            return res.status(200).json(updatedViewCountPost);
        } else if (userId) {
            const posts = await postService.getPostsByUserId(
                userId,
                page,
                limit,
            );
            return res.status(200).json(posts);
        } else {
            const posts = await postService.getAllPosts(page, limit);
            return res.status(200).json(posts);
        }
    } catch (error) {
        console.error(error);
        next(error);
    }
};</code></pre>
<p>쿼리로 postId를 받을 시, 게시글을 조회함과 동시에 조회수 카운터를 증가시킨다. 이는 해당 요청이 반복될 때 마다 다른 값을(증가된 조회수) 를 반환하기에 멱등하지 않다. 
이를 멱등하게 변경하려면 마찬가지로 조회수증가 API를 추가하면 되지만 무조건적으로 옳은 방법이라곤 할 수 없다.</p>
<p>각각의 API로 분리할 경우 멱등성을 유지할 수 있지만, 조회수 증가 요청보다 게시글조회 요청 응답이 먼저 도착하는 상황 등 순서 보장문제를 완전히 해결할 수 없다. 반대로 하나의 트랜잭션으로 처리하면 원자성과 순서 보장이 가능하지만 복잡성과 성능 문제 등이 발생할 수 있다.</p>
<p>따라서 가장 이상적인 방법은 상황과 요구 사항에 따라 다르므로 각 방법의 장단점을 고려하여 결정하는게 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[express] jest + supertest]]></title>
            <link>https://velog.io/@daechan_jo/express-jest-supertest</link>
            <guid>https://velog.io/@daechan_jo/express-jest-supertest</guid>
            <pubDate>Thu, 21 Sep 2023 11:53:43 GMT</pubDate>
            <description><![CDATA[<p>그동안 api를 개발하고 테스트는 포스트맨 하나에 의존하고있었고 딱히 문제도 없었었다.
불편했던 점이라면 하나의 api에 수정사항이 생겼을 때, 연동되는 api들을 전부 일일이 테스트했어야했다.
좋은 방법이 없나 생각해보다 예전에 첫 프로젝트를 할때 받았던 스켈레톤 코드 스크립트에서 봤던 jest가 생각이 났다.
그 당시에는 jest가 뭐지 <del>먹는건가</del> 하고 그냥 넘어갔었는데 좀 더 일찍 알았더라면 수월한 디버깅을 할 수 있었을텐데..</p>
<blockquote>
<p>Jset 와 Supertest</p>
</blockquote>
<p>Jest와 Supertest는 모두 JavaScript 및 Node.js 환경에서 테스트를 작성하고 실행하는 데 사용되는 도구들이다. 각각의 역할은 다음과 같다</p>
<ul>
<li><p>Jest: Jest는 Facebook이 개발한 자바스크립트 테스트 프레임워크로, 유닛 테스트와 통합 테스트를 모두 지원한다. Jest는 목(mock) 함수, 타이머 제어, 비동기 지원 등 다양한 기능을 제공하여 코드의 정확성을 검증하는 데 사용된다.</p>
</li>
<li><p>Supertest: Supertest는 HTTP 요청과 응답을 쉽게 테스트할 수 있게 해주는 라이브러리이다. 주로 Express.js 같은 Node.js 웹 서버에 대한 엔드포인트를 테스트하는 데 사용됩니다.</p>
</li>
</ul>
<p>따라서 Jest와 Supertest의 관계를 간략하게 설명하면, Jest가 전체적인 테스팅 환경(프레임워크)을 제공하고, 그 안에서 HTTP 요청/응답에 대한 구체적인 검증 작업을 Supertest가 담당한다고 볼 수 있다. Jset와 Supertest를 혼용하면 다음과 같이 사용할 수 있다.</p>
<pre><code class="language-javascript">ex) HTTP GET 요청을 보내고 그 결과를 검증

import request from &#39;supertest&#39;;
import app from &#39;./app&#39;;

describe(&#39;GET /&#39;, () =&gt; {
  it(&#39;responds with 200&#39;, async () =&gt; {
    const response = await request(app).get(&#39;/&#39;);
    expect(response.statusCode).toBe(200);
  });
});</code></pre>
<p>위 코드에서 describe 와 it 함수는 jest가 제공하고,
request(app).get(&#39;/) 부분은 Supertest가 제공하는 기능이다.</p>
<blockquote>
<p>통합테스트</p>
</blockquote>
<p><span style='color: #808080'>TypeScript | Service-Oriented MVC 패턴을 기준으로 작성되었습니다</span></p>
<p>api를 개별적으로 테스트코드를 작성하거나 컨트롤러와 서비스로직을 분리하는 등 단위 테스트가 가능하지만 개인적으로 비효율적으로 느껴졌다. <del>귀챠ㄴㅎ</del>
하나의 라우터를 통째로 테스트해보자.</p>
<hr>
<p>먼저 필요한 모듈을 설치한다.</p>
<p><code>npm i -D supertest</code>
<code>npm install -D ts-jest @types/jest</code>
<code>npm install -D @types/jest</code></p>
<p>테스트코드를 typescript로 작성할거기 때문에 jest.config도 설정해줘야 한다.
루트경로에 <code>jest.config.js</code> 를 생성 후 다음과 같이 작성해 주자</p>
<pre><code class="language-javascript">module.exports = {
    preset: &quot;ts-jest&quot;,
    testEnvironment: &quot;node&quot;,
};</code></pre>
<p>스크립트도 추가해주자</p>
<pre><code>&quot;scripts&quot;: {
        ...
        &quot;test&quot;: &quot;jest&quot;
    },</code></pre><br>
이제 라우터 경로에 코드를 작성해주면 된다. 
회원가입, 로그인 등 인증관련 API 테스트 코드를 routers 경로에 작성해줬다.


<p><span style='color: #808080'>jest에서 제공하는 fn 이나 mock 메서드를 사용하면 모킹할수 있지만 현재 코드에선 실제로 가동중인 서버가 아니고 생성된 레코드는 테스트가 끝남과 동시에 삭제될거라 사용중인 데이터베이스를 그대로 사용하고 mock메서드를 사용하지 않았다.</span></p>
<pre><code class="language-typescript">import request from &quot;supertest&quot;;
import express from &quot;express&quot;;
import authRouter from &quot;./authRouter&quot;;
import passport from &quot;passport&quot;;
import { local } from &quot;../passport&quot;;
import { jwt } from &quot;../passport&quot;;

//app.ts 에 작성된 서버설정 및 필요한 미들웨어도 전부 추가해줘야 한다
const app = express();
app.use(passport.initialize());
passport.use(&quot;local&quot;, local);
passport.use(&quot;jwt&quot;, jwt);
app.use(express.json());
app.use(&quot;/auth&quot;, authRouter);

//test suite 정의
//여러 개의 테스트 케이스를 그룹화하는 역할
//첫 번째 인자로 설명 문자열, 두 번째 인자로 테스트 케이스 정의 함수들을 넣어주면 된다
describe(&quot;Authentication API&quot;, () =&gt; {
    let userToken: any; // 로그인 이후 발급되는 토큰을 저장할 변수

    // 회원가입 테스트
    // it : 개별적인 테스트 케이스 정의
    // describe 과 마찬가지로 첫번째 인자로 설명, 두 번째 인자로 해당 테스트가 어떻게 동작할지 정의해준다
    it(&quot;POST /auth/signup - 새 유저를 생성하고 201을 반환&quot;, async () =&gt; {
        const res = await request(app).post(&quot;/auth/signup&quot;).send({
            email: &quot;test@example.com&quot;,
            password: &quot;password&quot;,
            name: &quot;Test User&quot;,
            nickname: &quot;testuser&quot;,
        });

          // 예상값 설정
        expect(res.statusCode).toEqual(201);
        expect(res.body.message).toContain(&quot;회원가입에 성공했습니다&quot;);
    });

    // 로그인 테스트
    it(&quot;POST /auth - 로그인 성공하고 200을 반환&quot;, async () =&gt; {
        const res = await request(app).post(&quot;/auth&quot;).send({
            email: &quot;test@example.com&quot;,
            password: &quot;password&quot;,
        });
        expect(res.statusCode).toEqual(200);
        expect(res.body.user).toEqual(&quot;Test User&quot;);
        expect(res.body.nickname).toEqual(&quot;testuser&quot;);
        userToken = res.body.token;
    });

    // 유저 상세보기 테스트
    it(&quot;GET /auth - 유저 정보 가져오고 200 반환&quot;, async () =&gt; {
        const res = await request(app)
            .get(&quot;/auth&quot;)
            .set(&quot;Authorization&quot;, `Bearer ${userToken}`);

        expect(res.statusCode).toEqual(200);
        expect(res.body.name).toEqual(&quot;Test User&quot;);
        expect(res.body.nickname).toEqual(&quot;testuser&quot;);
    });

    // 유저 정보 수정 테스트
    it(&quot;PUT /auth - 유저 정보 수정하고 201 반환&quot;, async () =&gt; {
        const res = await request(app)
            .put(&quot;/auth&quot;)
            .set(&quot;Authorization&quot;, `Bearer ${userToken}`)
            .send({
                // 업데이트할 필드를 자유롭게 추가
                name: &quot;Updated User&quot;,
            });
        console.log(res.statusCode);
        expect(res.statusCode).toEqual(201);

        // 업데이트된 필드만 추가
        expect(res.body.name).toEqual(&quot;Updated User&quot;);
    });

    // 회원 탈퇴 테스트
    it(&quot;DELETE /auth - 유저 삭제하고 204 반환&quot;, async () =&gt; {
        const res = await request(app)
            .delete(&quot;/auth&quot;)
            .set(&quot;Authorization&quot;, `Bearer ${userToken}`);

        expect(res.statusCode).toEqual(204);
    });
});
</code></pre>
<p>app.ts 에서 내보내는 app객체와 해당 api들을 사용하기 위해 꼭 필요한 모듈과 미들웨어들을 반드시 import 해줘야한다.
만약 테스트 결과에 서버에러 (500) 를 발생시키는 api가 있다면 높은 확률로 이 문제 때문이다.</p>
<hr>
<p>이제 <code>npm test</code> 혹은 <code>yarn test</code> 를 실행시키면 다음과 같은 결과를 확인할 수 있다.</p>
<pre><code>yarn test 
yarn run v1.22.19
$ jest
  console.log
    201

      at src/routers/auth.test.ts:63:17

 PASS  src/routers/auth.test.ts
  Authentication API
    ✓ POST /auth/signup - 새 유저를 생성하고 201을 반환 (99 ms)
    ✓ POST /auth - 로그인 성공하고 200을 반환 (69 ms)
    ✓ GET /auth - 유저 정보 가져오고 200 반환 (7 ms)
    ✓ PUT /auth - 유저 정보 수정하고 201 반환 (21 ms)
    ✓ DELETE /auth - 유저 삭제하고 204 반환 (4 ms)

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        1.251 s, estimated 2 s
Ran all test suites.
✨  Done in 1.96s.
</code></pre><p>결과에는 테스트정보와 api의 성공 여부 등이 출력된다.
이제 수정사항이 있을때 모든 api를 postman으로 하나하나 테스트해보는 번거로움은 겪지 않아도 된다
<br></p>
<blockquote>
<p>추가</p>
</blockquote>
<p>테스트에 목 객체를 사용하지 않고 실제 사용중인 데이터베이스로 테스트를 진행할 경우 보다 완전한 테스트를 진행해볼 수 있지만, 현재 각 라우터 단위로 통합테스트를 진행할 때 회원가입과 로그인, 그리고 테스트로 생성된 데이터들이 삭제되어야 되기 때문에 반복적인 코드가 발생된다. </p>
<p>그러므로 회원가입, 로그인, 회원탈퇴 테스트 코드를 모듈화시키고 jest 에서 제공하는 라이프사이클인  beforeAll, afterAll 을 이용해보자.</p>
<p>특징은 아래와 같다</p>
<ul>
<li>beforeAll() : 모든 테스트 케이스가 <strong>실행되기 전</strong>에 한 번만 실행시킨다. 일반적으로 테스트 데이터를 설정하거나 데이터베이스 연결을 설정하는 등의 작업을 수행한다<br></li>
<li>afterAll() : 모든 테스트 케이스가 <strong>완료된 후</strong>에 한 번만 호출된다. 일반적으로 사용한 자원을 정리하는데 쓰인다.</li>
</ul>
<p>두 함수 모두 선택적으로 비동기 동작을 지원하고 promise를 반환할 수 있다. 따라서 비동기 작업도 처리할 수 있다</p>
<br>
먼저 회원가입, 로그인, 회원탈퇴 함수를 모듈화시킨다

<pre><code class="language-typescript">...

export async function signUpUser() {
    const res = await request(app).post(&quot;/auth/signup&quot;).send({
        email: &quot;tests@example.com&quot;,
        password: &quot;password&quot;,
        name: &quot;Test User&quot;,
        nickname: &quot;testuser&quot;,
    });

    expect(res.statusCode).toEqual(201);
    expect(res.body.message).toContain(&quot;회원가입에 성공했습니다&quot;);
}

export async function loginUser() {
    const res = await request(app).post(&quot;/auth&quot;).send({
        email: &quot;tests@example.com&quot;,
        password: &quot;password&quot;,
    });

    expect(res.statusCode).toEqual(200);
    expect(res.body.user).toEqual(&quot;Test User&quot;);
    expect(res.body.nickname).toEqual(&quot;testuser&quot;);

    return res.body.token;
}

export async function deleteUser(userToken: string) {
    const res = await request(app)
        .delete(&quot;/auth&quot;)
        .set(&quot;Authorization&quot;, `Bearer ${userToken}`);

    expect(res.statusCode).toEqual(204);
}</code></pre>
<br>
모듈화된 함수를 auth.test.ts 에 라이프사이클 함수를 사용하여 적용시킨다

<pre><code class="language-typescript">...


describe(&quot;Authentication API&quot;, () =&gt; {
    let userToken: any;

    beforeAll(async () =&gt; {
        await signUpUser();
        userToken = await loginUser();
    });

    // 유저 상세보기 테스트
    it(&quot;GET /auth - 유저 정보 가져오고 200 반환&quot;, async () =&gt; {
        const res = await request(app)
            .get(&quot;/auth&quot;)
            .set(&quot;Authorization&quot;, `Bearer ${userToken}`);

        expect(res.statusCode).toEqual(200);
        expect(res.body.name).toEqual(&quot;Test User&quot;);
        expect(res.body.nickname).toEqual(&quot;testuser&quot;);
    });

    // 유저 정보 수정 테스트
    it(&quot;PUT /auth - 유저 정보 수정하고 201 반환&quot;, async () =&gt; {
        const res = await request(app)
            .put(&quot;/auth&quot;)
            .set(&quot;Authorization&quot;, `Bearer ${userToken}`)
            .send({
                // 업데이트 테스트 필드 추가 가능
                name: &quot;Updated User&quot;,
            });
        console.log(res.statusCode);
        expect(res.statusCode).toEqual(201);

                // 업데이트 필드 추가하면 여기도 추가
        expect(res.body.name).toEqual(&quot;Updated User&quot;);
    });

    afterAll(async () =&gt; {
        await deleteUser(userToken);
    });
});
</code></pre>
<p>이제 다른 라우터 통합테스트에서도 beforeAll, afterAll을 사용해 무의미한 중복코드를 줄이고 해당 라우터에서 테스트하고자 하는 api만 테스트하게 된다.</p>
<p>단, afterAll 의 deleteUser 로직은 해당 레코드가 삭제될 때 관련된 모든 데이터베이스의 데이터와 업로드파일도 삭제되도록 구현한 상태이기에 현재 자신의 코드에 맞게 구성하도록 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[prisma] Cascade]]></title>
            <link>https://velog.io/@daechan_jo/prisma-Cascade</link>
            <guid>https://velog.io/@daechan_jo/prisma-Cascade</guid>
            <pubDate>Thu, 14 Sep 2023 16:25:04 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 리펙토링 하던중 충격적인 사실을 알아버렸다..</p>
<p>그동안 삭제 서비스로직 코드를 작성할 때 코드가 상당히 괴랄해졌었는데
Prisma 에서 제공하는 Cascade 전략을 이제야 알았다..<del><em>눙물이앞을가림</em></del>
프로젝트를 진행하던 와중에 급작스럽게 ORM을 변경하게되면서 프리즈마를 처음 써보면서 겪은 해프닝이랄까</p>
<hr>
<p>눈물나는 나의 코드를 보자..</p>
<ol>
<li>그룹을 삭제하고싶어</li>
<li>그럼 그룹과 관련된 게시글도 삭제되어야 겠네</li>
<li>그럼 그룹 이미지와 그룹 게시글에 작성된 이미지도</li>
<li>그럼 그 게시글에 달린 댓글도 지워져야지!</li>
<li>그리고 그룹에 가입한 유저들도 탈퇴처리해야돼</li>
</ol>
<p>이런 스토리를 코드로 작성하게되면 대충 이렇게 되었었다
<img src="https://velog.velcdn.com/images/daechan_jo/post/f91827ab-6030-4ae5-90dc-b323ab4a922f/image.png" alt="">
<del><strong><em>끝도안보임....</em></strong></del></p>
<blockquote>
<h3 id="cascade">Cascade</h3>
</blockquote>
<p>프리즈마에서는 관계를 가진 레코드를 삭제할 여러가지 onDelete 전략을 사용할 수 있다. 여기서는 Cascade만 알아보자...</p>
<p>cascade 전략은 부모 필드가 삭제될 때 관려된 자식 필드들도 모두 삭제하는 방법이다. 만약 아래와같이 Group필드가 삭제될 때 GroupUser라는 관계필드를 가지고 있다 가정하면, 해당 관계에 onDelete: Cascade 를 사용하면 된다</p>
<pre><code class="language-javascript">model Group {
    id           Int          @id @default(autoincrement())
    manager      User         @relation(fields: [managerId], references: [id], onDelete: Cascade)
    managerId    Int
    posts        Post[]
    certposts    CertPost[]   @db.VarChar(1000)
    memberLimit  Int          @default(50)
    groupUser    GroupUser[]
    groupImage   GroupImage[]
      ...
}

model GroupUser {
    user       User    @relation(fields: [userId], references: [id], onDelete: Cascade)
    userId     Int
    group      Group   @relation(fields: [groupId], references: [id], onDelete: Cascade)
    groupId    Int
    isAdmin    Boolean @default(false)
    isAccepted Boolean @default(false)

    @@id([userId, groupId])
}

...</code></pre>
<p>이런식으로 관계설정에 onDelete를 추가해주면 상위 테이블의 필드가 삭제될 때, 하위테이블의 필드를 어떻게 할지 정할 수 있고, 장황한 삭제쿼리가 엄청나게 단순화 된다... 위에서 봤던 토나오는 삭제로직이 단 한줄로 변하는 매직...</p>
<pre><code class="language-javascript">await prisma.group.deleteMany({ where: { id: groupId } });</code></pre>
<p>현타와서 오늘은 못하겠고 내일부터 다시 리펙토링을 해야겠따..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[express] 예외의 'throw'이(가) 로컬에서 캡처되었습니다.]]></title>
            <link>https://velog.io/@daechan_jo/express-%EC%98%88%EC%99%B8%EC%9D%98-throw%EC%9D%B4%EA%B0%80-%EB%A1%9C%EC%BB%AC%EC%97%90%EC%84%9C-%EC%BA%A1%EC%B2%98%EB%90%98%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@daechan_jo/express-%EC%98%88%EC%99%B8%EC%9D%98-throw%EC%9D%B4%EA%B0%80-%EB%A1%9C%EC%BB%AC%EC%97%90%EC%84%9C-%EC%BA%A1%EC%B2%98%EB%90%98%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Wed, 13 Sep 2023 17:40:54 GMT</pubDate>
            <description><![CDATA[<p>개발공부를 시작하면서 <del><em>낡고병든머리로는힘들어서</em></del>  PyCham을 사용하고있었다
사실 뭐 이것말고도 다른 ide에서도 동일한 에러가 있을거같긴 하지만..
그 동안 코드가 정상적으로 작동하지 않던것도 아니고 약한 경고로 &#39;고치면 좋겠다~&#39; 정도의 느낌이라 방치하고 있었다. 그러다 서비스로직에서 무럭무럭 자라나고있는 저 throw 경고문이 눈에 거슬리기 시작해서 해결하기로 했다.</p>
<blockquote>
<h3 id="원인">원인</h3>
</blockquote>
<p>처음엔 &#39;로컬에서 캡처되었다&#39; 가 무슨 말인지 도통 이해가 안되었었다.
좀 더 쉽게 풀어보자면
<code>throw 를 try~catch가 캡쳐하고 있다</code>
즉, 예외 처리가 중복으로 되고 있다는 소리다.</p>
<p>조건문을 통해 throw가 실행되면 해당 로직은 중단되고 에러는 catch절로 이동하고, 해당 에러를 호출자인 컨트롤러로 보낸다.
컨트롤러는 해당 에러를 next객체를 통해 에러미들웨어로 보내게 된다.</p>
<pre><code class="language-javascript">//예시
const editPost = async (postId, userId, postData) =&gt; {
  try {
    const post = await prisma.post.findUnique({ where: { id: postId } });

    if (!post) throw new Error(&#39;존재하지 않는 게시글&#39;);
    if (post.writerId !== userId) throw new Error(&#39;권한이 없음&#39;);

    const filteredData = Object.entries(postData).reduce(
      (acc, [key, value]) =&gt; {
        if (value !== null) {
          acc[key] = value;
        }
        return acc;
      },
      {},
    );

    return await prisma.post.update({
      where: {
        id: postId,
      },
      data: filteredData,
    });
  } catch (error) {
    console.error(error);
    throw error;
  }
};</code></pre>
<blockquote>
<h3 id="아니-그래서-쓰면-외않되">아니 그래서 쓰면 외않되?</h3>
</blockquote>
<p>try<del>catch 문 안에서 throw를 사용한다고 해서 당장의 큰 문제는 없다.</del><em>아마도</em>~~ 그래서 ide에서도 약한 경고상태로 놔두는게 아닐까..</p>
<p>다만 에러를 재전파하고 있기 때문에 디버깅에 부정적인 영향을 줄 수 있고 코드의 복잡성이 증가할 수 있기때문에, 에러를 별도로 핸들링하기위해 throw를 사용하고싶다면 try~catch문 밖에서 사용해주는게 좋을것 같다.</p>
<pre><code class="language-javascript">//예시
const editPost = async (postId, userId, postData) =&gt; {
    const post = await prisma.post.findUnique({ where: { id: postId } });

    if (!post) throw new Error(&#39;존재하지 않는 게시글&#39;);
    if (post.writerId !== userId) throw new Error(&#39;권한이 없음&#39;);

    try {
    const filteredData = Object.entries(postData).reduce(
      (acc, [key, value]) =&gt; {
        if (value !== null) {
          acc[key] = value;
        }
        return acc;
      },
      {},
    );

    return await prisma.post.update({
      where: {
        id: postId,
      },
      data: filteredData,
    });
  } catch (error) {
    console.error(error);
    throw error;
  }
};</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[express.ts | scripts 설정 (feat.source-map-support)]]></title>
            <link>https://velog.io/@daechan_jo/express.ts-scripts-%EC%84%A4%EC%A0%95-feat.source-map-support</link>
            <guid>https://velog.io/@daechan_jo/express.ts-scripts-%EC%84%A4%EC%A0%95-feat.source-map-support</guid>
            <pubDate>Sat, 09 Sep 2023 11:54:17 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>&quot;start&quot;: &quot;npx prisma generate --schema=./prisma/schema.prisma &amp;&amp; nodemon --exec ts-node index.ts&quot;,</p>
</blockquote>
<p>원래 해당 스크립트가 먹히질않아서 계속 다른방법으로 시도하다가
아래와 같이 source-map-support 를 이용해서 js파일을 실행시키는 방법을 시도했었는데
해당 방법도 문제가 있었는데 왜인지 로그가 하나도 찍히질 않아서 결국 처음부터 다시 해보자 하고 ts-node를 사용하니깐 또 갑자기 되버리는.... 아마도 tsconfig 설정이나 다른데서 알게모르게 실수가 있었던거같은데 여러분들은 한방에 되시길...</p>
<p><del><em>하루종일 삽질하다가 허무하게 해결 되어버릴때 너무 짜릿해</em></del></p>
<br>


<p><em><strong>아래의 글은 아주매우 높은확률로 똥글이니 저런 옵션도 있구나 하고 뒤로가기를 눌러주세요</strong></em></p>
<hr>
<p>틈틈히 시간날때마다 타입스크립트로 보일러플레이트를 만들어보고 있었다.
처음에 타입을 넣어주는게 낮설고 번거로웠지만 금방 적응되었는데 예상치 못한데서 애를 먹었다.</p>
<p>초기 세팅에서 개발환경을 설정하고 서버가 정상적으로 켜지는걸 확인만 했었는데
어느정도 코드를 작성하고 통신테스트를하는데 클라이언트가 경로를 못찾는 이슈가 발생했다</p>
<br>

<p><del><em>스크립트였었던것</em></del></p>
<pre><code>&quot;scripts&quot;: {
        &quot;start&quot;: &quot;npx prisma generate --schema=./prisma/schema.prisma &amp;&amp; nodemon --exec ts-node ./index.ts&quot;,
        &quot;start:prod&quot;: &quot;node ./dist/index.js&quot;,
        &quot;build&quot;: &quot;npx prisma generate --schema=./prisma/schema.prisma &amp;&amp; tsc&quot;,
        &quot;swagger-autogen&quot;: &quot;node ./src/config/swagger.ts&quot;
    }</code></pre><p>개발환경과 배포환경을 분리하고 개발환경에선 별도의 빌드 없이 ts코드를 바로 읽을 수 있게 <del>거라생각했지만</del> 했는데 어째서인지 서버는 정상적으로 켜지고 앤드포인트나 다른 코드들도 정상인데 요청이 길을 못찾고있었다.
혹시나 하고 빌드된 js로 시도하니깐 정상적으로 요청과 응답을 주고받고있었다.</p>
<hr>
<blockquote>
<p>source-map-support</p>
</blockquote>
<p>그렇게 기약없는 삽질을 계속되었고 스크립트도 변경해보고 ts설정도 이래저래 변경해봤지만 소용이 없었다. 외않되....
그렇다고 nodemon 없이 개발하자니 너무 불편하고 js로 빌드된 환경에서 디버깅을하자니 ts로 확인을 할 수  없는것도 문제였는데 <del><em>gpt를 묶어놓고 고문하니깐</em></del> ts의 source-map이라는 옵션을 사용하면 js로 빌드된 코드를 디버깅할때도 ts로 확인할 수 있는 현재 내 상황에 갓벽한 모듈이 있었다</p>
<ul>
<li>가장 먼저 tsconfig.json 에서 다음을 활성화한다.
&quot;sourceMap: true&quot;</li>
<li>npm install --save-dev source-map-support</li>
</ul>
<pre><code>스크립트 수정

&quot;scripts&quot;: {
            &quot;start&quot;: &quot;npx prisma generate --schema=./prisma/schema.prisma &amp;&amp; tsc &amp;&amp; nodemon -r source-map-support/register ./dist/index.js&quot;,
            &quot;build&quot;: &quot;npx prisma generate --schema=./prisma/schema.prisma &amp;&amp; tsc &amp;&amp; babel src --out-dir dist --extensions &#39;.ts,.tsx&#39;&quot;,
            &quot;start:prod&quot;: &quot;node ./dist/index.js&quot;,
            &quot;swagger-autogen&quot;: &quot;node ./src/config/swagger.ts&quot;
        }</code></pre><p>이렇게되면 서버를 실행시킬 때 자동으로 js로 빌드되고, 빌드된 서버코드를 nodemon으로 실행시키고 에러를 ts로 확인할 수 있게 된다</p>
<p><del><em>근데 이러면 프로덕션환경이랑 분리한게 의미가 없..</em></del></p>
]]></description>
        </item>
    </channel>
</rss>