<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dev_hoon2.log</title>
        <link>https://velog.io/</link>
        <description>백엔드 개발자가 되자!</description>
        <lastBuildDate>Wed, 11 Oct 2023 18:04:50 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dev_hoon2.log</title>
            <url>https://velog.velcdn.com/images/dev_hoon2/profile/e5980d63-2233-4d79-8f3e-5e1f0ecaed57/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dev_hoon2.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_hoon2" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Postgresql 한글 정렬 관련 문제]]></title>
            <link>https://velog.io/@dev_hoon2/Postgresql-%ED%95%9C%EA%B8%80-%EC%A0%95%EB%A0%AC-%EA%B4%80%EB%A0%A8-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@dev_hoon2/Postgresql-%ED%95%9C%EA%B8%80-%EC%A0%95%EB%A0%AC-%EA%B4%80%EB%A0%A8-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Wed, 11 Oct 2023 18:04:50 GMT</pubDate>
            <description><![CDATA[<p>nestjs + postgresql + typeorm으로 개인 프로젝트를 진행하던 중이였는데 카테고리를 전체 조회하는 로직에서 문제가 생겼다.</p>
<pre><code>async findAll() {
    const categories = await this.categoryRepository.find({
      where: {
        parent: IsNull(),
      },
      relations: [&#39;children&#39;],
      order: {
        name: &#39;asc&#39;,
        children: {
          name: &#39;asc&#39;,
        },
      },
    });

    return categories;
  }</code></pre><pre><code>  [
      {
          &quot;id&quot;: &quot;0c0645da-ed48-4723-b0a1-a444145f0982&quot;,
          &quot;name&quot;: &quot;개발/프로그래밍&quot;,
          &quot;created_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
          &quot;updated_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
          &quot;fk_parent_category_id&quot;: null,
          &quot;children&quot;: [
              {
                  &quot;id&quot;: &quot;a1e8875e-bf5c-4f06-beaa-9187e4537480&quot;,
                  &quot;name&quot;: &quot;백엔드&quot;,
                  &quot;created_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
                  &quot;updated_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
                  &quot;fk_parent_category_id&quot;: &quot;0c0645da-ed48-4723-b0a1-a444145f0982&quot;
              },
              {
                  &quot;id&quot;: &quot;8776688a-2744-4687-b664-006ef8b9861b&quot;,
                  &quot;name&quot;: &quot;풀스택&quot;,
                  &quot;created_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
                  &quot;updated_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
                  &quot;fk_parent_category_id&quot;: &quot;0c0645da-ed48-4723-b0a1-a444145f0982&quot;
              },
              {
                  &quot;id&quot;: &quot;d8b22a7e-37ba-4193-b228-f63b2491b72a&quot;,
                  &quot;name&quot;: &quot;프론트엔드&quot;,
                  &quot;created_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
                  &quot;updated_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
                  &quot;fk_parent_category_id&quot;: &quot;0c0645da-ed48-4723-b0a1-a444145f0982&quot;
              },
              {
                  &quot;id&quot;: &quot;c25b6b8e-9166-4389-8b16-28804af221a2&quot;,
                  &quot;name&quot;: &quot;알고리즘/자료구조&quot;,
                  &quot;created_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
                  &quot;updated_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
                  &quot;fk_parent_category_id&quot;: &quot;0c0645da-ed48-4723-b0a1-a444145f0982&quot;
              },
              {
                  &quot;id&quot;: &quot;1afc46e1-65ec-44fb-a256-ec5943254477&quot;,
                  &quot;name&quot;: &quot;모바일 앱 개발&quot;,
                  &quot;created_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
                  &quot;updated_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
                  &quot;fk_parent_category_id&quot;: &quot;0c0645da-ed48-4723-b0a1-a444145f0982&quot;
              }
          ]
      },
      {
          &quot;id&quot;: &quot;8ef0f3c5-c640-4cbb-9fba-eab6b249f859&quot;,
          &quot;name&quot;: &quot;게임 개발&quot;,
          &quot;created_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
          &quot;updated_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
          &quot;fk_parent_category_id&quot;: null,
          &quot;children&quot;: [
              {
                  &quot;id&quot;: &quot;48685f11-7f96-4098-98c2-1545c77e17bd&quot;,
                  &quot;name&quot;: &quot;게임 프로그래밍&quot;,
                  &quot;created_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
                  &quot;updated_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
                  &quot;fk_parent_category_id&quot;: &quot;8ef0f3c5-c640-4cbb-9fba-eab6b249f859&quot;
              }
          ]
      },
      {
          &quot;id&quot;: &quot;36b5d240-c0e5-4208-82ae-be5e61517bf9&quot;,
          &quot;name&quot;: &quot;데이터 사이언스&quot;,
          &quot;created_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
          &quot;updated_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
          &quot;fk_parent_category_id&quot;: null,
          &quot;children&quot;: [
              {
                  &quot;id&quot;: &quot;08650064-250b-48ee-8103-fd763185e7cb&quot;,
                  &quot;name&quot;: &quot;데이터 분석&quot;,
                  &quot;created_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
                  &quot;updated_at&quot;: &quot;2023-08-21T07:31:09.297Z&quot;,
                  &quot;fk_parent_category_id&quot;: &quot;36b5d240-c0e5-4208-82ae-be5e61517bf9&quot;
              }
          ]
      }
  ]</code></pre><p>위의 코드와 정렬 방식을 보면 나는 메인 카테고리와 서브 카테고리의 정렬을 &#39;ASC&#39;로 하였지만 전혀 정렬이 되지 않은걸 볼 수 있다.
그래서 구글링을 통해 알아본 결과 Postgresql의 collate옵션 설정이 문제였다.
collate는 데이터베이스의 정렬 순서를 결정한다고 한다.</p>
<p>그래서 나의 문제를 해결하기 위해서는 내 데이터베이스의 collate 설정이 ko_KR.UTF-8 또는 en_US.utf8로 되어있는 것을 C로 변경하면 된다.</p>
<p>하지만 데이터베이스의 collate 설정 변경은 초기 데이터베이스 생성시에만 가능하기 때문에</p>
<pre><code>psql 접속 후

$ DROP DATABASE 데이터베이스이름; 
$ CREATE DATABASE 데이터베이스이름 LC_COLLATE &#39;C&#39;;</code></pre><p>이렇게 데이터베이스를 삭제 후 collate 설정을 추가해 만들어주었다.</p>
<p>하지만..... 쉽게 넘어가지 않았다......</p>
<p><img src="https://velog.velcdn.com/images/dev_hoon2/post/15f930d3-4801-4973-b1df-30cbb5a81097/image.png" alt=""></p>
<p>이런 오류가 나오는데 데이터베이스를 기본적으로 생성할 때 collate 설정이 이미 초기화 되어있는 template1을 복제해 생성한다고 한다. 그래서 우리는 collate 설정이 초기화 되어있지않은 template0을 사용해야한다.</p>
<pre><code>$ CREATE DATABASE 데이터베이스이름 TEMPLATE template0 LC_COLLATE &#39;C&#39;;  </code></pre><p>이렇게 명령어를 쳐주니 collate가 C로 된 데이터베이스가 생성되었고 정렬문제가 해결되었다.</p>
<p>참고한  사이트 : <a href="https://jupiny.com/2016/12/12/sort-korean-in-postgresql/">https://jupiny.com/2016/12/12/sort-korean-in-postgresql/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS - @Exclude()에 대하여??]]></title>
            <link>https://velog.io/@dev_hoon2/NestJS-Exclude%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@dev_hoon2/NestJS-Exclude%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Thu, 06 Apr 2023 07:22:15 GMT</pubDate>
            <description><![CDATA[<p>NestJS로 개인프로젝트를 만드는 중인데 User를 생성하고 반환값으로 생성한 User의 정보를 내보낸다.
<img src="https://velog.velcdn.com/images/dev_hoon2/post/e4f29fd2-adbc-49bd-b6e7-d844fb61704b/image.png" alt="">
하지만 위의 이미지에서 보면 반환값에 비밀번호가 그대로 노출되는 것을 볼 수 있다.
NestJS와 GraphQL로 작업할 당시에는 단순히 플레이그라운드에 안보여주는 형식으로 하였는데, 이번에는 REST-API로 작업하고 있어 어떤식으로 해야하는지 궁금했다.</p>
<p><strong>@Exclude() 데코레이터를 이용하자!!</strong></p>
<p>우선 Entity파일에 아래 이미지와 같이 데코레이터를 입력한다.
참고로 @Exclude() 데코레이터를 이용하기 위해서는 class-transformer라는 라이브러리를 설치해야한다.
<img src="https://velog.velcdn.com/images/dev_hoon2/post/1d03d377-0103-4118-8570-b4d6e98aca9c/image.png" alt=""></p>
<p>하지만 단순히 저렇게만 한다고 끝이 아니다. 바로 Serialization이라고 데이터 직렬화를 해야한다.</p>
<p><strong>여기서 잠깐!
데이터 직렬화란??????</strong></p>
<blockquote>
<p>데이터 직렬화는 객체나 데이터를 저장하거나 전송하기 위해, 해당 데이터를 일련의 바이트(bytes)로 변환하는 과정입니다. 직렬화된 데이터는 파일에 저장하거나, 인터넷을 통해 전송할 수 있습니다. 이때, 데이터를 처리할 수 있는 DTO(Data Transfer Object) 클래스를 정의하고, 이를 이용하여 데이터를 직렬화합니다.</p>
</blockquote>
<p>ChatGPT에게 물어본 결과 위와 같이 대답해주었다.
쉽게 말해서 우리는 NestJS를 이용하므로 JSON형식으로 받게 만들어주는 것을 직렬화라고 할 수 있다.
그리고 직렬화를 도와주는 것이 바로 위에말한 class-transformer 라이브러이다.</p>
<p>그런다음 모든 HTTP 요청에서 직렬화하기 위해 글로벌 인터셉터를 등록할 것이다.
<img src="https://velog.velcdn.com/images/dev_hoon2/post/8c26cd38-33e6-4742-91d7-c44655e20de8/image.png" alt="">
위의 코드를 main.ts에 등록해주면 된다.</p>
<p>app.useGlobalInterceptors()는 NestJS 에서 전역적으로 사용되는 인터셉터를 등록하는 메서드이며, ClassSerializerInterceptor는 NestJS에서 기본적으로 제공하는 직렬화 인터셉터로, 데이터를 요청에 맞게 변환하는 기능을 제공한다.
그래서 @Exclude()를 사용하면 직렬화 대상에서 제외하며, @Expose()를 사용하면 반대로 직렬화 대상으로 지정할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hoon2/post/754a60c1-dce6-4739-8fa6-f11932f1125f/image.png" alt="">
그러면 위의 이미지처럼 비밀번호가 제외된 응답을 보여준다.!!!</p>
<p>참고한 사이트 : <a href="https://jojoldu.tistory.com/610">https://jojoldu.tistory.com/610</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[WebSocket에 대하여]]></title>
            <link>https://velog.io/@dev_hoon2/WebSocket%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@dev_hoon2/WebSocket%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Sun, 19 Mar 2023 16:25:04 GMT</pubDate>
            <description><![CDATA[<p>팀프로젝트에서 실시간채팅창 구현을 맡게 되면서 Websocket라는 것에 대하여 공부해야겠다라는 생각이 들었다.</p>
<p>HTTP와 WebSocket 둘 다 프로토콜이다.</p>
<p>http는 유저가 요청을 보내면 서버가 응답을 보내고 요청과 응답의 반복이다.
http의 중요한점은 stateless기 때문에 백엔드(서버)는 유저를 기억하지 못한다.
즉, 요청과 응답 과정 뒤에 백엔드는 유저를 잊어버린다.
websocket은 브라우저가 서버에 요청을 보내면 서버는 거절하거나 수락을 한다. 수락을 했다면 서로 악수한거처럼 연결이 된다. 그래서 서버는 유저를 알 수 있다. 그래서 서버는 요청이 없어도 응답을 보낼 수 있다. 즉, 브라우저는 서버에게 어떤 때나 메세지를 보낼 수 있고 서버도 마찬가지다. 즉, 나와 Wi-Fi를 생각하면 된다. 연결 끊기 전까지 계속 연결되어있다.</p>
<p>웹 소켓의 특징</p>
<ol>
<li>양방향 통신</li>
</ol>
<ul>
<li>데이터 송수신을 동시에 처리할 수 있는 통신 방법</li>
<li>클라이언트와 서버가 서로에게 원할 때 데이터를 주고 받을 수 있다.</li>
<li>통상적인 http 통신은 클라이언트가 요청을 보내는 경우에만 서버가 응답하는 단방향 통신</li>
</ul>
<ol start="2">
<li>실시간 네트워킹</li>
</ol>
<ul>
<li>웹 환경에서 연속된 데이터를 빠르게 노출</li>
<li>예를들면 채팅,주식,비디오 데이터</li>
<li>여러 단말기에 빠르게 데이터를 교환</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[사이드 프로젝트 하면서 배운점 2]]></title>
            <link>https://velog.io/@dev_hoon2/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%98%EB%A9%B4%EC%84%9C-%EB%B0%B0%EC%9A%B4%EC%A0%90-2</link>
            <guid>https://velog.io/@dev_hoon2/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%98%EB%A9%B4%EC%84%9C-%EB%B0%B0%EC%9A%B4%EC%A0%90-2</guid>
            <pubDate>Wed, 25 Jan 2023 11:08:11 GMT</pubDate>
            <description><![CDATA[<p>오늘도 Promise.all과 map의 중첩이 된 코드를 리팩토링 하였다.</p>
<pre><code>await Promise.all(
    memberQuery.map(async (member) =&gt; {
      const qqq = Array.from({ length: monthStartToEnd.length }, () =&gt; []);
      await Promise.all(
        organizationId.map(async (organizationId) =&gt; {
          await Promise.all(
            monthStartToEnd.map(async (date, i) =&gt; {
              const start = new Date(date);
              const copyDate = new Date(date);
              const end = new Date(copyDate.setDate(copyDate.getDate() + 1));

              const aaa = await this.workCheckRepository
                .createQueryBuilder(&#39;WorkCheck&#39;)
                .leftJoinAndSelect(&#39;WorkCheck.member&#39;, &#39;member&#39;)
                .leftJoinAndSelect(&#39;WorkCheck.company&#39;, &#39;company&#39;)
                .leftJoinAndSelect(&#39;WorkCheck.organization&#39;, &#39;organization&#39;)
                .leftJoinAndSelect(&#39;WorkCheck.schedule&#39;, &#39;schedule&#39;)
                .leftJoinAndSelect(&#39;WorkCheck.roleCategory&#39;, &#39;roleCategory&#39;)
                .where(&#39;WorkCheck.member = :memberId&#39;, {
                  memberId: member.id,
                })
                .andWhere(&#39;WorkCheck.organization = :organizationId&#39;, {
                  organizationId,
                })
                .andWhere(
                  `WorkCheck.workDay BETWEEN &#39;${start.toISOString()}&#39; AND &#39;${end.toISOString()}&#39;`,
                )
                .orderBy(&#39;WorkCheck.workDay&#39;, &#39;ASC&#39;)
                .addOrderBy(&#39;member.name&#39;, &#39;ASC&#39;)
                .getMany();

              aaa.length !== 0 ? (qqq[i] = aaa) : false;
            }),
          );
        }),
      );
      qqq.flat(2).length !== 0 ? result2.push(qqq) : false;
    }),
  );</code></pre><p>위 의 코드는 오늘 날짜를 기준으로 해당 월의 조회 결과를 반환하는 로직이다.
이 로직도 Promise.all과 map이 중첩되어 있으며 qqq라는 변수에 Array.from을 통해 해당 월의 날짜만큼 길이의 배열을 생성해주며, 조건에 맞는 결과들을 qqq라는 변수에 넣어주다 보니 데이터가 적은데도 조회하는데 시간이 오래걸리는 문제점이 있었다.</p>
<pre><code>await Promise.all(
        memberInOrg.map(async (member) =&gt; {
          const workChecks = await this.workCheckRepository
            .createQueryBuilder(&#39;WorkCheck&#39;)
            .leftJoinAndSelect(&#39;WorkCheck.company&#39;, &#39;company&#39;)
            .leftJoinAndSelect(&#39;WorkCheck.member&#39;, &#39;member&#39;)
            .leftJoinAndSelect(&#39;WorkCheck.organization&#39;, &#39;organization&#39;)
            .leftJoinAndSelect(&#39;WorkCheck.schedule&#39;, &#39;schuedule&#39;)
            .leftJoinAndSelect(&#39;WorkCheck.roleCategory&#39;, &#39;roleCategory&#39;)
            .where(&#39;WorkCheck.member = :memberId&#39;, { memberId: member.id })
            .andWhere(
              &#39;DATE(WorkCheck.workDay) BETWEEN DATE(:start) AND DATE(:end)&#39;,
              {
                start: monthStartToEnd[0],
                end: monthStartToEnd[monthStartToEnd.length - 1],
              },
            )
            .orderBy(&#39;WorkCheck.workday&#39;, &#39;ASC&#39;)
            .addOrderBy(&#39;member.name&#39;, &#39;ASC&#39;)
            .getMany();

          const memberWorkCheck = [];

          monthStartToEnd.forEach((workDay, i) =&gt; {
            const workChecksForDay = workChecks.filter(
              (workCheck) =&gt; workCheck.workDay.getDate() === workDay.getDate(),
            );

            memberWorkCheck[i] = [...workChecksForDay];
          });

          const temp = {
            member,
            data: memberWorkCheck,
          };
          result.push(temp);
        }),
      );</code></pre><p>위의 코드로 리팩토링을 진행하였는데 위의 코드에서는 생략되었지만 먼저 member의 저장소에서 조건에 맞는 member들을 미리 조회하는 로직을 만들었고 map을 돌면서 조건에 맞는 결과들을 새로운 배열에 담아주었다.</p>
<p>중첩됐던 코드가 하나로 줄었으며, 조회 속도 또한 이전 코드 보다 훨씬 빨라 졌다.
아직 부족한 점이 많은 리팩토링 코드지만 나의 원래 코드보다 빨라졌다는 점에서 칭찬을 주고싶다.</p>
<p>새로 배운점 : 쿼리빌더의 andWhere 조건에서 DATE, BETWEEN으로 검색하게 되면 시간이 아닌 날짜로 비교를 하기 때문에 MySQL에서의 저장되는 시간과 서버의 시간이 맞지 않아 조회안되던 결과도 조회된다는 점을 알게되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[사이드 프로젝트 하면서 배운점 1]]></title>
            <link>https://velog.io/@dev_hoon2/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%98%EB%A9%B4%EC%84%9C-%EB%B0%B0%EC%9A%B4%EC%A0%90-1</link>
            <guid>https://velog.io/@dev_hoon2/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%98%EB%A9%B4%EC%84%9C-%EB%B0%B0%EC%9A%B4%EC%A0%90-1</guid>
            <pubDate>Wed, 25 Jan 2023 11:07:34 GMT</pubDate>
            <description><![CDATA[<p>사이드 프로젝트에서 코드를 작성하며 배운점을 블로그에 정리하려고 한다.
나는 현재 부트캠프 동기들과 사이드 프로젝트를 진행하고 있으며 주제는 통합 인력관리 솔루션인 시프티(<a href="https://shiftee.io/ko?gclid=Cj0KCQiA8aOeBhCWARIsANRFrQHoxdcH3FGp_ZubumDul0OieWByp4XVzYIXWFmSlqmsVk6hdjRC9WQaApZpEALw_wcB">링크</a>)를 클론 코딩하는 중이다.</p>
<p>나는 현재 출퇴근기록API, 근무일정API, 공지사항게시판API, 근무일정템플릿API, 근무일정유형API, 멤버API를 맡고있다.</p>
<pre><code>  async weekFind({ today, organizationId, roleCategoryId }) {
    const week = currentWeek(today);
    const startWeek = new Date(week[0]);
    const end = new Date(week[1]);
    const endWeek = new Date(end.setDate(end.getDate() + 1));

    const result = [];

    await Promise.all(
      organizationId.map(async (organizationId) =&gt; {
        await Promise.all(
          roleCategoryId.map(async (roleCategoryId) =&gt; {
            const schedule = await this.scheduleRepository
              .createQueryBuilder(&#39;Schedule&#39;)
              .leftJoinAndSelect(&#39;Schedule.member&#39;, &#39;member&#39;)
              .leftJoinAndSelect(&#39;Schedule.organization&#39;, &#39;organization&#39;)
              .leftJoinAndSelect(&#39;Schedule.roleCategory&#39;, &#39;roleCategory&#39;)
              .leftJoinAndSelect(
                &#39;Schedule.scheduleTemplate&#39;,
                &#39;scheduleTemplate&#39;,
              )
              .leftJoinAndSelect(&#39;Schedule.company&#39;, &#39;company&#39;)
              .leftJoinAndSelect(
                &#39;Schedule.scheduleCategory&#39;,
                &#39;scheduleCategory&#39;,
              )
              .where(&#39;Schedule.organization = :organizationId&#39;, {
                organizationId,
              })
              .andWhere(&#39;Schedule.roleCategory = :roleCategoryId&#39;, {
                roleCategoryId,
              })
              .andWhere(
                `Schedule.date BETWEEN &#39;${startWeek.toISOString()}&#39; AND &#39;${endWeek.toISOString()}&#39;`,
              )
              .orderBy(&#39;member.name&#39;, &#39;ASC&#39;)
              .addOrderBy(&#39;Schedule.date&#39;, &#39;ASC&#39;)
              .getMany();

            result.push(schedule);
          }),
        );
      }),
    );

    return result.flat();
  }

  // 
  const currentWeek = (today) =&gt; {
    today = new Date(today);
    const sunday = today.getTime() - 86400000 * today.getDay();

    today.setTime(sunday);

    const week = [today.toISOString().slice(0, 10)];

    for (let i = 1; i &lt; 7; i++) {
        today.setTime(today.getTime() + 86400000);
        week.push(today.toISOString().slice(0, 10));
    }

    return [week[0], week[week.length - 1]];
};</code></pre><p>위 의 코드는 근무일정API에서 오늘 날짜를 받은 뒤 currentWeek라는 함수에서 오늘 날짜를 기준으로 일주일을 계산한 뒤 일주일의 첫 시작날과 마지막날을 반환한 뒤 쿼리빌더의 BETWEEN을 사용하여 일주일 사이의 organazationId(지점Id), roleCategoryId(직무Id)와 일치하는 결과를 조회하는 로직이다.</p>
<p>위 코드의 문제점은 organizationId와 roleCategoryId가 배열로 들어오는데 내가 코드를 짜면서 생각했던 점은 처음 organizationId 배열에서 map을 돌리고 organization(지점)안에 roleCategory(직무)가 있으니 map 안에서 또 map을 돌려 조건에 맞는 결과들을 result라는 빈 배열안에 push를 해주고 결과값으로 반환 해주었다.</p>
<p>위 코드의 문제점은 Promise.all안에 map이 그 안에 또 Promise.all과 map이 있어서 조회 속도가 느리다는 단점이 있엇다.</p>
<p>그래서 리팩토링을 해야한다는 생각이 있었고 아래의 코드와 같이 바꾸게 되었다.</p>
<pre><code>async weekFind({ today, organizationId, roleCategoryId }) {
    const week = currentWeek(today);
    const startWeek = new Date(week[0]);
    const end = new Date(week[1]);
    const endWeek = new Date(end.setDate(end.getDate() + 1));

    const schedules = await this.scheduleRepository
      .createQueryBuilder(&#39;Schedule&#39;)
      .leftJoinAndSelect(&#39;Schedule.member&#39;, &#39;member&#39;)
      .leftJoinAndSelect(&#39;Schedule.organization&#39;, &#39;organization&#39;)
      .leftJoinAndSelect(&#39;Schedule.roleCategory&#39;, &#39;roleCategory&#39;)
      .leftJoinAndSelect(
        &#39;Schedule.scheduleTemplate&#39;,
        &#39;scheduleTemplate&#39;,
      )
      .leftJoinAndSelect(&#39;Schedule.company&#39;, &#39;company&#39;)
      .leftJoinAndSelect(
        &#39;Schedule.scheduleCategory&#39;,
        &#39;scheduleCategory&#39;,
      )
      .where(&#39;Schedule.organization IN (:...organizationId)&#39;, { organizationId })
      .andWhere(&#39;Schedule.roleCategory IN (:...roleCategoryId)&#39;, { roleCategoryId })
      .andWhere(
        `Schedule.date BETWEEN &#39;${startWeek.toISOString()}&#39; AND &#39;${endWeek.toISOString()}&#39;`
      )
      .orderBy(&#39;member.name&#39;, &#39;ASC&#39;)
      .addOrderBy(&#39;Schedule.date&#39;, &#39;ASC&#39;)
      .getMany();

    return schedules;
}</code></pre><p>중첩되었던 Promise.all 과 map을 제거하고 쿼리빌더의 IN을 이용하여 where조건으로 넣는것으로 바꿧다. 그 결과 조회속도가 처음 코드보다 한결 나아진걸 느낄 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[팀 프로젝트 등산로 API 구현기 - 2]]></title>
            <link>https://velog.io/@dev_hoon2/%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%ED%98%84%EA%B8%B02</link>
            <guid>https://velog.io/@dev_hoon2/%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%ED%98%84%EA%B8%B02</guid>
            <pubDate>Fri, 30 Dec 2022 09:16:23 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@dev_hoon2/%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%93%B1%EC%82%B0%EB%A1%9C">등산로 API 구현기 1편</a></p>
<hr>
<p>등산로 API 구현기 1편에서 내가 오픈 API를 이용해 만들었던 코드는 많은 단점들이 있어 포기하였다. 여기서 포기할까? 라는 생각이 많이 들었었다. 그래서 이 때 제일 힘들었고 멘탈이 많이 날아갔었다. 하지만 우리 프로젝트의 제일 메인 기능이었고, 나 스스로도 포기하기가 매우 싫었다.</p>
<p>그래서 내가 두번째로 생각하게된 방법은 산림빅데이터거래소라는 홈페이지를 이용하는 방법이였다.(<a href="https://www.bigdata-forest.kr/product/FRT000801">참고링크</a>) 하지만 한가지 문제가 있었는데, .shx라는 확장자 파일을 이용하여야 했다.</p>
<blockquote>
<p> <strong>참고</strong> : 쉐이프파일이란 ?</p>
<p>쉐이프파일(shapefile) : 지리현상에 대한 기하학적 위치와 속성 정보를 저장, 제공해주는 데이터 포맷을 말하며, 주로 GIS상에서 사용하지만 최근에는 ESRI ArcView 뿐만아니라 기타 GIS등의 프로그램 및 3D 프로그램, Autocad map 등에서도 사용되고 있다. </p>
</blockquote>
<p>SHP파일의 유형 및 기능 : 쉐이프파일의 유형은 5개의 파일로 이루어져 있다.</p>
<ol>
<li>.shp : 지리 사상의 기하학 정보를 저장</li>
<li>.shx : 지리 사상의 기하학 정보의 인덱스를 저장(shp파일의 인덱스)</li>
<li>.dbf : 지리 사상의 속성 정보를 제공하는 dBASE 파일(table)</li>
<li>.sbn : 지리 사상 공간 인덱스를 저장하는 파일</li>
<li>.sbx : spatial join등의 기능을 수행하거나, shape 필드에 대한 인덱스를 생성할 때 필요한 파일<blockquote>
</blockquote>
참고한 사이트 : <a href="https://likedalhyang.tistory.com/43">https://likedalhyang.tistory.com/43</a></li>
</ol>
<p>내가 받은 압축파일에는 shp, shx, dbf, prj가 들어있었으며, 저 파일들을 이용하기위해 열심히 구글링을 하였고 QGIS라는 프로그램이 필요하다고 하여 설치하였다.</p>
<p>참고 : QGIS란?</p>
<p>QGIS는 FOSS4G 프로젝트의 일환으로 오프손스 데스크탑 GIS 분야에서 널리 사용되는 GIS 소프트웨어 이다. 오픈소스 프로젝트로 누구나 자유롭게 이용 및 개선할 수 있으며, QGIS는 공간데이터 조회, 편집, 분석 기능을 제공하는 대표적인 오픈소스 데스크탑 지리정보시스템(GIS) 소프트웨어이다.</p>
<p>그리고 열심히 찾아본 결과 QGIS 프로그램을 이용하여 압축파일을 실행한 뒤 GeoJSON으로 바꿔 사용하자고 생각을 하게 되었다.</p>
<p>참고 : GeoJSON이란?</p>
<p>GeoJSON은 다양한 지리 데이터 구조를 JSON 형식으로 인코딩하기 위한 포맷이며, 애플리케이션에서 지리적 데이터 구조를 표현하고 통신 교환할 수 있도록 도와준다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hoon2/post/fb107255-affc-489b-be59-7f84068cb0e9/image.png" alt="">
위의 사진은 내가 산림빅데이터거래소에서 받은 압축파일을 QGIS로 실행한 모습이다. 오른쪽 화면에 등산로들의 경로가 나오는게 보인다. 그런 다음 내가 한 작업은 각각의 등산로의 좌표들과 산의 이름만을 뽑은 다음 GeoJSON형태로 가공하였다. 그리고 JSON파일의 데이터들을 MongoDB에 저장하였다. 근데 한가지 문제는 좌표의 뒷자리 ex) 127.1241242423123 그러니까 127 뒷 부분이 15자리인데 15자리 그대로 MongoDB에 저장시키려 하니까 node가 에러를 뿜으며 터져버렸다...... 그래서 조절을 하였고 13자리에서 성공하였다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hoon2/post/5cacff3e-e348-4484-8e2d-75555acfde58/image.png" alt="">
위의 사진은 내가 만든 GeoJSON 파일이다. 이걸 만들기 위해 1주일 ~ 2주일이 걸렸는데 정말 감격스러웠다. 참고로 스크롤을 오른쪽으로 옮기면 무수히 많은 좌표들이 나온다.</p>
<pre><code>  async saveTrekking() {
    const file = fs.readFileSync(&#39;./src/apis/trekking/13qq.geojson&#39;);

    const result = file.toString();
    const result1 = JSON.parse(JSON.stringify(result));
    const result2= JSON.parse(result1);
    const result3 = result2.features;


    for (let i = 0; i &lt; 1; i++) {
      const arr = result3[i].geometry.coordinates.flat();
     await this.trekkingInfoModel.create({
        mountainName: result3[i].properties[&#39;MNTN_NM&#39;],
        trekkingName: result3[i].poperties[&#39;PMNTN_NM&#39;],
        difficulty: result3[i].properties[&#39;PMNTN_DFFL&#39;],
        coordinate: arr.map((el) =&gt; el.reverse()),
      });
    }

    return &#39;성공&#39;;
  }</code></pre><p>위의 코드는 내가 만든 geojson파일을 MongoDB에 저장시키기 위한 코드이다. 그리고 산 이름을 검색하면 해당 산의 등산로 좌표들이 나오게 조회 로직을 만들었다.</p>
<p>그리고 이 좌표들을 프론트엔드 쪽에 넘겨주었고 카카오맵을 이용하여 경로를 그렸다.
<img src="https://velog.velcdn.com/images/dev_hoon2/post/d54500e6-cef8-4a5c-a61f-946c2757fdd3/image.png" alt="">
마지막으로 위의 사진은 우리 팀 프로젝트의 등산로이다.</p>
<p>아쉬운점</p>
<ul>
<li><p>일단, 제일 아쉬웠던 점은 좌표들이 등산로에 맞게 잘나오는 것도 많았지만 빗나가거나 안나오는 좌표들도 많았다는 점이다.</p>
</li>
<li><p>나는 geojson파일의 데이터를 MongoDB에 임의로 한번만 저장시켜 사용하였는데 뭔가 좀 더 좋은 방법이 있지 않았을까라는 생각이 든다.</p>
</li>
</ul>
<p>참고한 사이트 :
<a href="https://wallacearchivemain.gatsbyjs.io/devlog/geojson/">https://wallacearchivemain.gatsbyjs.io/devlog/geojson/</a>
<a href="https://velog.io/@jaehye0ng2/GIS-QGIS">https://velog.io/@jaehye0ng2/GIS-QGIS</a>
<a href="https://iron-jin.tistory.com/entry/QGIS%EB%A1%9C-shp%ED%8C%8C%EC%9D%BC%EC%9D%84-geoJson%EC%9C%BC%EB%A1%9C-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0">https://iron-jin.tistory.com/entry/QGIS%EB%A1%9C-shp%ED%8C%8C%EC%9D%BC%EC%9D%84-geoJson%EC%9C%BC%EB%A1%9C-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[팀 프로젝트 등산로 API 구현기 -1]]></title>
            <link>https://velog.io/@dev_hoon2/%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%93%B1%EC%82%B0%EB%A1%9C</link>
            <guid>https://velog.io/@dev_hoon2/%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%93%B1%EC%82%B0%EB%A1%9C</guid>
            <pubDate>Fri, 23 Dec 2022 04:51:23 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@dev_hoon2/%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0">팀 프로젝트 회고록 이동</a> </p>
<hr>
<p>내가 부트캠프에서 했던 팀 프로젝트는 등산 할 크루들을 모으는 일회성 모집 사이트이다.
등산 크루들을 모으는 사이트이기 때문에 어느 산의 등산로를 등반할지와 등산로에 대한 경로를 보여주는게 중요했다.</p>
<p>그래서 백엔드에서는 내가 등산로에 관한 API를 담당하게 되었다. 내가 맡은 등산로 API는 각각 산들에 속한 등산로의 좌표들을 뽑아와 프론트엔드에게 줘야했다.</p>
<p>그래서 팀 프로젝트 당시 나는 오픈 API를 통해 등산로들의 정보와 좌표를 가져오려고 했다.
그래서 공공데이터포털 V월드에서 산림청에서 제공하는 전국 등산로 정보라는 오픈 API가 있어서(<a href="https://www.vworld.kr/dev/v4dv_2ddataguide2_s002.do?svcIde=frstclimb">V월드 등산로 API 링크</a>) 옳다구나! 하고 이걸 쓰면 되겠구나! 하고 생각했다. 하지만 이것이 나의 멘탈이 붕괴되고 지옥의 시작이 될 줄은 이때만 해도 전혀 상상하지 못했다.</p>
<p>해당 오픈 API의 사용법이나 설명서가 보이지 않아서 API 실데이터 검색이라는 사용 예가 있어서 보기로 하였다.
<img src="https://velog.velcdn.com/images/dev_hoon2/post/4cc69162-e0a5-4f1c-bbdd-04f6b1816c9e/image.png" alt="">
위의 이미지에서 요청파라미터로 geomFilter와 attrFilter, crs가 입력되었고 검색결과로 산명칭과 공간정보객체 등등 여러 정보가 나왔다. 근데 입력값이 이해가 안가 파라미터들에 대한 설명을 보았다.
<img src="https://velog.velcdn.com/images/dev_hoon2/post/8e10b21c-3596-42f5-b2f6-1d020d51709b/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev_hoon2/post/3e69e817-e1da-471c-83a3-c9579cd3ec38/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev_hoon2/post/7bc309e0-0a04-4205-bf79-d890d6fe8e86/image.png" alt=""></p>
<p>위의 이미지는 필수 파라미터들에 대한 정보와 설명이 있었다. 일단 필수로는 request와 data, geomFilter, attrFilter, crs가 있엇다. attrFilter와 crs는 필수는 아니지만 attrFilter는 범위를 좁히기 위해서는 필수라고 생각하여 넣었으며, crs는 반환하는 좌표들을 카카오맵이 쓰는 EPSG:5181 또는 EPSG:4326으로 변환하기 위해 필요하다고 생각하여 넣었다.</p>
<p>여기서 나에게 가장 큰 시련을 준 건 geomFilter였다. 지오메트리 필터라고 부르는데 포맷으로 POINT, LINESTRING, POLYGON, MULTIPOLYGON가 있었다. 검색을 해보니 지도 서비스를 구성하는 백터 데이터라고 하는데 간략하게 설명 해보자면,
Point : 좌표 공간에서 한 지점의 위치를 표시
LINESTRING : 다수의 Point를 연결해주는 선분
POLYGON : 다수의 선분들이 연결되어 닫혀 있는 상태인 다각형
MULTIPOLYGON : 다수 개의 Polygon 집합</p>
<p>그래서 등산로의 경로가 있으면 데이터에 하나 하나의 Point들이 있고 그걸 선으로 이으면 Linestring이 되며 등산로의 경로가 된다고 생각하였다. 근데 문제는 필수 파라미터로 geomFilter가 있어서 대체 어떻게 저 좌표를 파라미터로 넣을 수 있을까 고민하였다. 그래서 실 사용예제에 있는 LINESTRING(13133057.313802 4496529.073264,14133023.872602 4496514.7413212)에 대해서 많은 고민을 하였는데 저 좌표를 좌표 변화 사이트에서 다양한 crs로 변경하여 구글맵, 카카오맵에 검색하는 등 소위말해 별짓을 다 해봣다.</p>
<p>근데 나에게 멘붕을 준건 지도에 안뜨거나 빗나가거나 하는 문제였다. 그래서 코딩공부가 아니라 좌표공부를 열심히 하기도 했고, 팀 프로젝트 점검을 받을 때 개발자 분들께 질문도 드렸는데, 개발자 분들도 안되면 포기하거나 다른 식으로 하자고 하셨다.
그래도 오기가 생겨 많은 고민을 하였는데, 우연히 geomFilter가 필수지만 빼도 상관이 없다는 걸 우연히 알게되어 attrFilter를 이용하여 여러 조건을 줘서 검색하자라는 생각을 하게되었다.
attrFilter는 속성조회를 위한 조건검색을 정의하며 다양한 조건을 넣을 수 있었다.</p>
<p>그래서 아래와 같은 코드를 만들었다.</p>
<pre><code>// 읍면동 코드 오픈 API
  async getEmdCdInfo({ address }) {
    const emdCdInfo = await axios({
      url: &#39;http://api.vworld.kr/req/data&#39;,
      method: &#39;GET&#39;,
      params: {
        key: &#39;21C35C47-E16F-3811-A71E-F3E339A7D1AE&#39;,
        attrFilter: `full_nm:=:${address}`,
        data: &#39;LT_C_ADEMD_INFO&#39;,
        request: &#39;GetFeature&#39;,
        domain: &#39;https://develop.wetrekking.kr&#39;,
      },
      headers: {
        &#39;Content-type&#39;: &#39;application/json&#39;,
      },
    }).catch((err) =&gt; {
      throw err;
    });

    const emdCd =
      emdCdInfo.data.response.result.featureCollection.features[0].properties
        .emd_cd;
    // console.log(emdCd);
    return emdCd;
  }

  // 등산로 오픈 API
  async getTrekkingInfo({ addressEmdCd, mountainName }) {
    const arr = [];
    const getTrekkingInfo = await axios({
      url: &#39;http://api.vworld.kr/req/data&#39;,
      method: &#39;GET&#39;,
      params: {
        key: &#39;21C35C47-E16F-3811-A71E-F3E339A7D1AE&#39;,
        attrFilter: `mntn_nm:=:${mountainName}|emdCd:=:${addressEmdCd}`,
        crs: &#39;EPSG:4326&#39;,
        data: &#39;LT_L_FRSTCLIMB&#39;,
        domain: &#39;https://develop.wetrekking.kr&#39;,
        request: &#39;GetFeature&#39;,
      },
      headers: {
        &#39;Content-Type&#39;: &#39;application/json&#39;,
      },
    }).catch((err) =&gt; {
      throw err;
    });

    const wetrekking =
      getTrekkingInfo.data.response.result.featureCollection.features;

    for (let i = 0; i &lt; wetrekking.length; i++) {
      const data = wetrekking[i].geometry.coordinates[0];

      for (let j = 0; j &lt; data.length; j++) {
        const temp = data[j][1];
        data[j][1] = data[j][0];
        data[j][0] = temp;
      }

      arr.push(data);
    }
    const result = {
      mountainName,
      coordinate: arr.flat(),
    };

    return result;
  }</code></pre><p>나는 파리미터로 attrFilter에 산의 이름과 해당 산의 읍면동을 입력해서 검색 결과에 맞는 등산로 들이 나오게 만들었다. 읍면동 코드 또한 오픈 API를 이용하여 구현하였다.
<img src="https://velog.velcdn.com/images/dev_hoon2/post/ff258ffe-3fef-426c-a322-1b40af974c45/image.png" alt="">
위의 결과는 Playground로 위의 코드로 조회를 해본 결과인데 저렇게 주소와 산의 이름을 치면 좌표와 산의 이름이 나왔다.</p>
<p>하지만 저 API의 큰 단점은 산의 주소를 정확하게 알아야 하며, 지리산 같이 여러 도시에 걸쳐져 있는 경우 어느 주소를 입력해야 하는지 잘 모르는 단점이 있었다. 그리고 제일 큰 문제는 오른쪽의 좌표들 중 몇개를 구글지도와 카카오지도에 넣어봤는데 좌표가 안나오는 경우가 있거나 엉뚱한 지점이 찍히는 경우가 많았다.</p>
<p>이렇게 나의 1차 시도는 실패로 끝났다...........</p>
<p>참고한 사이트 : <a href="https://sparkdia.tistory.com/24">https://sparkdia.tistory.com/24</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[사이드 프로젝트 2일차]]></title>
            <link>https://velog.io/@dev_hoon2/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@dev_hoon2/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 07 Dec 2022 10:38:37 GMT</pubDate>
            <description><![CDATA[<p>2일차가 시작되었다.</p>
<p>시작은 역시나 회의로 시작되었고 프론트와 백엔드가 각각 나뉘어져 회의를 시작하였다.</p>
<p>내가 속한 백엔드에서는 어떠한 기능들이 있는지 조사하는 것부터 시작하였고 기능들은 전부 나열한 뒤 우리가 구현이 가능한 기능들을 추렸다.</p>
<p>프론트와 백엔드가 서로의 회의를 마친 후 다시 모였고, 서로가 생각한 기능들과 페이지등을 가지고 2차 회의를 가졌다.</p>
<p>2차 회의를 마친 후 우리 백엔드는 다시 모여 바로 ERD 작업에 들어갔으며, ERD 작업을 완료하고 2일차를 마쳤다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hoon2/post/5269b08d-49d1-4135-93e4-3a39d7aaa8cd/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[사이드 프로젝트 1일차]]></title>
            <link>https://velog.io/@dev_hoon2/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@dev_hoon2/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Thu, 01 Dec 2022 12:09:45 GMT</pubDate>
            <description><![CDATA[<p>부트캠프를 수료 후 프론트와 백엔드에서 사이드 프로젝트를 진행할 인원이 모였다. 프론트 5명 백엔드 4명이 모였고 11월 28일에 첫 회의를 시작하였다. 첫 회의의 시작은 역시나 프로젝트 기획회의 였다.</p>
<p>다들 아이디어가 넘쳐났고 많은 기획들이 나왔다.</p>
<p>많은 기획들 중 우리가 선택한건 통합 인력관리 솔루션 시프티를 참고해 우리들 만의 웹 서비스를 만들기로 했다.</p>
<p>그 후에는 코어타임, 협업툴, 팀장 선정을 하였고 1일차는 이렇게 종료 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[팀 프로젝트 회고]]></title>
            <link>https://velog.io/@dev_hoon2/%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@dev_hoon2/%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Wed, 30 Nov 2022 11:44:42 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_hoon2/post/9db86bbb-f6c0-4b99-9a70-dee04b217ff1/image.png" alt=""></p>
<blockquote>
<p>팀 프로젝트 기간 : 10월 28일 ~ 11월 22일
팀 프로젝트 이름 : WeTrekking
팀 프로젝트 설명 : 등산 크루원 모집</p>
</blockquote>
<hr>
<p>사실 회고를 1일차 부터 쓰고 싶었지만 1일차 부터 정신이 없고 바빠서 제대로 쓰지 못했다. 그래서 수료 후에 어느정도 여유가 생겼을 때 마지막 정리 회고라도 써보자 생각하여 적게되었다.</p>
<hr>
<blockquote>
<p><strong>첫 만남 ~ 회의 ~ 첫 시작?(10월 28일~10월 30일)</strong></p>
</blockquote>
<ul>
<li><p>이전 기수들의 팀 프로젝트 회고록이나 여러사항을 참고 하였는데 기획을 빨리 결정해야 시간에 안쫓긴다는 얘기가 많아서 팀 프로젝트OT가 끝나자마자 우리 팀원들은 바로 회의에 들어갔다.</p>
</li>
<li><p>일단 자기소개를 서로 하였고 서로 맡을 역할을 정하게 되었다. 나는 Git관리자가 되었고 바로 서로의 프로젝트 아이디어를 꺼내놓기 시작하였다. 우리 팀은 아이디어뱅크 였는지 많은 아이디어들이 나왔고 투표를 통하여 등산 동호회 모집이 선택되었다.</p>
</li>
<li><p>어떤 프로젝트를 할지 결정한 후 대충이나마 어떤 기능들이 들어갈지 정한 후 페이지 구성을 어떤 식으로 할지 회의를 하였으며 와이어프레임을 대충이나마 정하였다.
<img src="https://velog.velcdn.com/images/dev_hoon2/post/a1490090-a545-432d-9a07-f67097ab9286/image.jpeg" alt=""></p>
</li>
</ul>
<hr>
<blockquote>
<p><strong>1주차 (10월 31일 ~ 11월 6일)</strong></p>
</blockquote>
<ul>
<li><p>본격적으로 팀 프로젝트가 시작되는 날이였는데, 첫 회의 때 정했던 기획을 바탕으로 구체적으로 만들어가기 시작했다.</p>
</li>
<li><p>첫 주차인 만큼 팀의 규칙을 정하였다. 출근 시간은 언제로 할지, 코어타임은 몇시 부터 몇시 까지로 할지, 주말에는 어떻게 할 것인지? 그리고 Git 전략은 어떻게 할 것인지? 같은 여러가지 규칙들을 정하였다. 그리고 노션을 통하여 규칙들과 회의록을 작성하였다.
<img src="https://velog.velcdn.com/images/dev_hoon2/post/95244428-cdaa-4d5c-8976-9085dd1df25d/image.png" alt=""></p>
</li>
</ul>
<ul>
<li><p>기획이 구체적으로 만들어져 가면서 프론트엔드 분들은 디자이너님과 연락하면서 디자인 방향에 대해 정하였고, 내가 속한 백엔드에서는 어떤 API들이 필요할지 의견을 나누며 지금 까지 나온 API들을 바탕으로 API Docs와 ERD 작업에 바로 들어갔다.</p>
</li>
<li><p>나는 API Docs 제작과 Git관리자를 맡았기 때문에 현재까지 나온 API들을 바탕으로 Docs제작을 하였고, Git 브랜치 전략을 정하였다. 내가 정한 브랜치 전략은 위험성이 높지만 쉬운 방법으로 정하였다.
<img src="https://velog.velcdn.com/images/dev_hoon2/post/180551d8-9c17-4b38-aab0-ebddbc06aabc/image.png" alt=""></p>
</li>
<li><p>ERD와 API Docs가 얼추 완성된 후 기본적인 CURD 작업에 들어갔다. 그리고 주 마다 한번 ~ 두번 정도 개발자분들께 점검을 받는데 우리 팀은 뼈아픈 점검들을 많이 받았다. 그리고 중요한점!!!!은 질문이 많아야 한다는 것이다.</p>
</li>
<li><p>나의 경우에는 유저쪽 CURD 작업을 하던 도중 점검 때 메인기능이 우선시 되어야 한다는 얘기를 듣게 되어서 등산로 API작업과 채팅 API를 맡게 되었는데......... 멘붕의 연속이였다.</p>
</li>
</ul>
<hr>
<blockquote>
<p><strong>2주차 (11월 7일 ~ 11월 13일)</strong></p>
</blockquote>
<ul>
<li><p>2주차가 되고나서 부터는 기본적인 CURD들은 완성이 되었고 추가적인 API 작업들이 이루어졌다. 그리고 배포시작을 위한 세팅도 시작하였다. 목요일쯤부터는 프론트엔드 분들은 퍼블리싱 작업이 마무리되어 본격적으로 백엔드 API와의 기능연결이 시작되었다.</p>
</li>
<li><p>나의 경우 1주차 부터 등산로 API를 하는데 정말 멘탈붕괴가 많이 되었다. 공공데이터포털에 들어가서 오픈 API를 사용하려고 하였지만 사용방법이 정말 불친절 한데다가 좌표도 지오메트리 좌표를 사용해야 해서 코딩 공부보다 좌표 공부를 더 했던 기억이난다.....
이때는 정말 포기하고 싶을 정도로 힘이 들었다. 더군다나 1주차 점검 때 개발자분께서 3일동안만 붙자아보고 안되면 과감히 포기하라고 하셨는데 자존심 때문인지 포기할 수가 없없다.</p>
</li>
<li><p>2주차 점검 때 들었던 피드백으로는 2주차 까지는 프론트와 백엔드간의 기능이 연결이 완료되어야 한다는 것이었다.</p>
</li>
<li><p>2주차 쯤에는 각자의 API에서 오류도 많이 나왔지만 제일 문제는 우리를 언제나 괴롭히는 CORS 에러였다. 구글링도 해보고 이 방법, 저 방법 써봐도 해결이 안되어 정말 힘들었던 기억이 떠오른다.</p>
</li>
</ul>
<hr>
<blockquote>
<p><strong>3주차 (11월 14일 ~ 11월 20일)</strong></p>
</blockquote>
<ul>
<li><p>발표 준비 하기 전 마지막 주인데 정말 바빴던 기억이 난다. 기능들을 연결하였는데 오류들도 많았고, 갑자기 추가 구현해야할 API가 생겨 새벽까지 작업해야하는 경우가 많았다.</p>
</li>
<li><p>나의 경우에는 채팅 API는 어찌저찌 돌아가게는 구현을 하였지만 역시나 제일 큰 문제는 등산로 API 였다. 우리 프로젝트의 제일 중요한 메인기능 이였지만 완성이 안되었다. 이때는 정말 포기해야하나 생각도 들었는데 어떻게 완성이 되어(이 부분은 따로 작성예정) 3주차 마무리를 할 수 있게 되었다.</p>
</li>
</ul>
<hr>
<blockquote>
<p><strong>마무리 (11월 21일 ~ 11월 22일)</strong></p>
</blockquote>
<ul>
<li>11월 22일이 대망의 발표날이라서 그 전에 발표 자료 준비 및 마지막 준비를 하였다.
발표 당일날에 갑자기 백엔드 배포 서버가 계속해서 꺼져버리는 현상이 생겼었는데 우리 조가 첫번째 발표였는데 다들 심장이 철컹했던 경험이 있었다.</li>
</ul>
<hr>
<blockquote>
<p><strong>결론</strong></p>
</blockquote>
<p>우리 팀이 어찌저찌해서 1등이라는 결과를 얻게 되었는데 모두가 열심히 노력한 결과인거 같다. 팀 프로젝트를 하면서 내가 얻은 교훈은 팀 프로젝트의 기획 즉, 얼마나 참신한가 이런 쪽도 중요하다지만 나의 생각은 다르다. 나는 개인적으로 백엔드니까 기능 쪽이 중요하다고 생각하는데 API를 만들면서 단순히 수업 때 배웠던 코드들을 전부 쓰는게 아니라 새로운 기능들 이나 다른식으로 코드를 짜보는게 중요하다고 생각한다. 그러면서 배우는점들이 많다고 생각한다.
그리고 팀 프로젝트를 하면서 배운점이 있는데 프론트엔드와 백엔드 사이에 협업의 중요성이였다. 팀 프로젝트를 하면서 마지막으로 갈수록 우리 쪽에서는 되는데 왜 저쪽에서는 안되지? 하는 생각도 많이 들었고 어떤식으로 결과값을 반환해줘야 하는지도 고민이 많이 되었다.
어쨋든 뒷 기수분들이 이 글을 보실지는 모르겠지만 화이팅입니다....!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Gitflow , workflow]]></title>
            <link>https://velog.io/@dev_hoon2/Gitflow-workflow</link>
            <guid>https://velog.io/@dev_hoon2/Gitflow-workflow</guid>
            <pubDate>Thu, 27 Oct 2022 03:13:05 GMT</pubDate>
            <description><![CDATA[<h1 id="git-flow">Git-flow</h1>
<blockquote>
<p>깃플로우(Git-flow)는 2010년에 Vincent Driessen이 만든 Git을 이용한 개발 작업 절차이다. 즉, 프로그램 같은 것이 아니라 약속,규칙의 개념이다. 그리고 중요한 점은 깃플로우는 완벽한 방법이 아니기 때문에 각자 개발 환경에 따라서 수정하고 변형해서 사용하는 것을 추천한다.</p>
</blockquote>
<h3 id="git-flow의-브랜치">Git-flow의 브랜치</h3>
<p>깃 플로우는 5가지의 브랜치를 사용한다.</p>
<ul>
<li>master : 제품으로 출시(배포)할 수 있는 브랜치</li>
<li>develop : 다음 버전을 개발하는 브랜치</li>
<li>feature : 단위별로 기능을 개발하는 브랜치이며, 완료되면 develop 브랜치와 병합한다.</li>
<li>release : 배포하기 전에 QA를 통해 버그를 찾아내기 위한 브랜치이다.</li>
<li>hotfixes : master 브랜치에서 발생한 버그를 긴급하게 수정하는 브랜치이다.</li>
</ul>
<h3 id="git-flow-예시">Git-flow 예시</h3>
<p><img src="https://velog.velcdn.com/images/dev_hoon2/post/ef46167c-e070-4d67-a7d8-013a00b2cb6c/image.png" alt=""><a href="https://ux.stories.pe.kr/183">이미지 출처</a>
위의 이미지는 Git-flow 예시를 설명할 때 가장 많이 사용되는 이미지이다.</p>
<ol>
<li>처음은 master 브랜치에서 시작한다.</li>
<li>동일한 브랜치를 develop에도 생성을 하며, 개발자들은 develop 브랜치에서 개발을 진행한다.</li>
<li>개발을 진행하다가 기능 구현이 필요하다면 A개발자는 develop브랜치에서 feature 브랜치를 하나 생성해서 기능 구현을 하고, B개발자도 기능 구현이 필요하다면 마찬가지로 feature 브랜치를 하나 생성해서 구현한다.</li>
<li>완료된 feature 브랜치는 검토를 거쳐 다시 develop 브랜치에 합쳐진다.</li>
<li>모든 기능이 완료되면 develop 브랜치를 release 브랜치로 만든다. 그리고 QA를 진행하며 보완점을 보완하고 버그를 고친다.</li>
<li>모든것이 끝나면 release 브랜치를 master브랜치와 develop 브랜치로 보낸다. master 브랜치에서 버전추가를 위해 태그를 하나 생성하고 배포한다.</li>
<li>배포를 했는데 버그가 있을 경우 hotfixes 브랜치를 만들어 수정 후 태그를 생성하고 수정 배포한다.</li>
</ol>
<h1 id="workflow">Workflow</h1>
<blockquote>
<p>팀에서 브랜치를 어떻게 사용할 지에 대한 규칙을 Workflow라고 한다.</p>
</blockquote>
<p>참고한 사이트 :
<a href="https://velog.io/@pond1029/Git-Workflow">https://velog.io/@pond1029/Git-Workflow</a>
<a href="https://ux.stories.pe.kr/183">https://ux.stories.pe.kr/183</a>
<a href="https://chanyeong.com/blog/post/15">https://chanyeong.com/blog/post/15</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CI / CD]]></title>
            <link>https://velog.io/@dev_hoon2/CI-CD</link>
            <guid>https://velog.io/@dev_hoon2/CI-CD</guid>
            <pubDate>Thu, 27 Oct 2022 03:12:30 GMT</pubDate>
            <description><![CDATA[<h1 id="cicontinuous-integration">CI(Continuous Integration)</h1>
<blockquote>
<p>CI는 빌드,테스트 자동화 과정이라고 할 수 있으며, 개발자를 위한 자동화 프로세스인 지속적인 통합을 의미한다. 애플리케이션의 버그 수정이나 새로운 코드 변경이 주기적으로 빌드 및 테스트되면서 공유되는 Repository에 머지(merge)된다.</p>
</blockquote>
<h3 id="ci-장점">CI 장점</h3>
<ul>
<li>코드 검증에 들어가는 시간이 줄어든다.</li>
<li>개발 편의성이 증가한다.</li>
<li>테스트를 통과한 코드만 Repository에 올라가기 때문에, 코드 퀄리티가 좋다.</li>
</ul>
<h1 id="cdcontinuous-deployment--continuous-delivery">CD(Continuous Deployment / Continuous Delivery)</h1>
<blockquote>
<p>CD는 배포 자동화 과정이며, 지속적인 서비스 제공(Continuous Delivery) / 지속적인 배포(Continuous Deployment)를 의미하며 두 가지 용어는 상호 교환적으로 사용된다.</p>
</blockquote>
<ul>
<li><p>지속적인 제공
개발자들이 테스트를 거쳐 적용한 변경 사항이 자동으로 Repository에 업로드 된다.</p>
</li>
<li><p>지속적인 배포
개발자들의 변경 사항을 Repository에서 고객이 사용하는 프로덕션 환경까지 자동으로 릴리즈한다.</p>
</li>
</ul>
<h3 id="cd-장점">CD 장점</h3>
<ul>
<li>배포보다 개발에 더 신경을 쓸 수 있다.</li>
<li>원클릭으로 수작업 없이 빌드, 테스트, 배포까지 자동화 할 수 있다.</li>
</ul>
<h4 id="ci--cd-종류">CI / CD 종류</h4>
<ul>
<li>Jenkins</li>
<li>CircleCI</li>
<li>TravisCI</li>
<li>Github Actions</li>
<li>등등....</li>
</ul>
<p>참고한 사이트 :
<a href="https://codingpractices.tistory.com/entry/cicd-CICD%EB%9E%80-%EA%B0%9C%EB%85%90-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0">https://codingpractices.tistory.com/entry/cicd-CICD%EB%9E%80-%EA%B0%9C%EB%85%90-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</a>
<a href="https://seosh817.tistory.com/104">https://seosh817.tistory.com/104</a>
<a href="https://jud00.tistory.com/entry/CICD%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C">https://jud00.tistory.com/entry/CICD%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[서버리스]]></title>
            <link>https://velog.io/@dev_hoon2/%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4</link>
            <guid>https://velog.io/@dev_hoon2/%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4</guid>
            <pubDate>Thu, 27 Oct 2022 03:11:47 GMT</pubDate>
            <description><![CDATA[<h1 id="서버리스serverless">서버리스(Serverless)</h1>
<blockquote>
<p>서버리스는 그대로 해석하면 서버가 없다라고 해석될 수 있지만 서버리스의 진짜 정의는 서버를 직접 관리할 필요가 없는 아키텍쳐를 뜻하며 서버리스 아키텍처라고도 부른다. 즉, 서버를 관리하거나 신경 쓸 필요 없도록 해준다.</p>
</blockquote>
<h3 id="서버리스-아키텍처의-구현-방식">서버리스 아키텍처의 구현 방식</h3>
<h4 id="faasfunction-as-a-service">FaaS(Function as a Service)</h4>
<p>함수를 서비스로 제공하며, 사용자가 백엔드에서 작성한 코드를 서버리스 제공자의 서버에 업로드하면 해당 서버는 업로드한 코드를 함수 단위로 쪼개서 대기상태로 둔 다음 요청이 들어오면 대기상태의 함수를 실행시켜 처리한다.</p>
<p>FaaS 특징</p>
<ul>
<li>Stateless
FaaS는 함수가 실행되는 동안에만 자원을 할당하며, 함수가 항상 같은 머신에서 실행된다는 보장이 없다.</li>
<li>Ephemeral
함수는 특정 이벤트가 발생했을 때만 컨테이너로서 배포되며, 실행이 끝난 후엔 자원이 회수되므로 일시적으로만 배포된다.</li>
</ul>
<h4 id="baasbackend-as-a-service">BaaS(Backend as a Service)</h4>
<p>SNS연동이나 DB와 같이 백엔드에 필요한 기능들을 사용자가 직접 구현할 필요 없이 제공하는 API로 해당 기능을 구현할 수 있게 해준다.</p>
<h3 id="서버리스의-장점">서버리스의 장점</h3>
<ul>
<li>이벤트 기반이기 때문에 비용이 저렴하다.</li>
<li>인프라 구성, 운영, 보안 등에 신경쓰지 않고 비즈니스 로직에 집중할 수 있다.</li>
<li>자동 스케일 업 및 스케일 다운이 가능하다.</li>
<li>간단한 패키징 및 배포가 가능하다.</li>
<li>릴리즈 주기 감소</li>
<li>높은 생산성</li>
</ul>
<h3 id="서버리스의-단점">서버리스의 단점</h3>
<ul>
<li>실시간 서비스에는 적합하지 않다.</li>
<li>클라우드 서비스 업체에 종속적이다.</li>
<li>마이그레이션의 어려움이 있다.</li>
<li>실행 시간에 한계가 있다.</li>
<li>로컬 데이터를 사용할 수 없다.</li>
<li>디버깅이나 테스팅에 불편하다.</li>
</ul>
<p>참고한 사이트 :
<a href="https://jaehoney.tistory.com/77">https://jaehoney.tistory.com/77</a>
<a href="https://dev-coco.tistory.com/171">https://dev-coco.tistory.com/171</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠버네티스]]></title>
            <link>https://velog.io/@dev_hoon2/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4</link>
            <guid>https://velog.io/@dev_hoon2/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4</guid>
            <pubDate>Thu, 27 Oct 2022 03:10:24 GMT</pubDate>
            <description><![CDATA[<h1 id="쿠버네티스">쿠버네티스</h1>
<blockquote>
<p>쿠버네티스는 컨테이너화된 애플리케이션을 자동 배포, 자동 스케일링 등을 제공하는 오픈 소스 기반 관리 시스템이다.</p>
</blockquote>
<h3 id="쿠버네티스-용어">쿠버네티스 용어</h3>
<ul>
<li><p>클러스터(Cluster)
쿠버네티스에서 관리하는 가장 큰 단위를 클러스터라고 부르며, 여러 서버를 논리적으로 하나로 묶었다고 보면 된다.
클러스터 내부에는 실제로 서비스를 담당하는 워커 노드(Worker Node)와 워커 노드를 관리하는 마스터 노드(Master Node)가 존재한다.</p>
</li>
<li><p>노드(Node)
물리 서버, 가상 서버를 의미하며 한 개의 노드는 한 대의 머신(가상 or 물리)을 뜻한다. 쉽게말해서 노드를 컴퓨터 한대라고 이해 할 수 있다.</p>
</li>
<li><p>파드(Pod)
노드 내부에 애플리케이션을 설치할 수 있는데 도커로 해당 애플리케이션의 이미지를 만든 후 도커 허브에 업로드하면 쿠버네티스에서 애플리케이션 배포에 필요한 준비는 끝난다. 애플리케이션 이미지가 있는 도커 허브 링크를 설정해준 후 이 이미지를 기반으로 컨테이너를 가지고 있는 파드를 생성할 수 있다. 파드는 하나 이상의 컨테이너를 묶은거라고 할 수 있고 쿠버네티스는 파드를 언제든지 버리고 새로만들 수 있지만 조심해야할 사항들이 있다.</p>
<ol>
<li><p>파드는 자신만의 가상 IP를 받는다. 하지만 파드는 새로 생성되면 IP가 바뀌는 유동적인 특성이 있기때문에 파드를 독자적으로 배포하게 된다면 IP를 설정하기 까다롭다.</p>
</li>
<li><p>가상 IP를 가지고 있기 때문에 외부에서 파드로 접근할 수 없다. 접근하려면 경로 설정이 필요하다.</p>
</li>
<li><p>파드는 장애가 발생하면 죽을 수 있기 때문에 애플리케이션이 중단될 가능성을 가지고 있다.</p>
</li>
<li><p>사용하는 이미지를 새로운 버젼으로 업데이트 하려면 모든 파드를 새로운 애플리케이션으로 업데이트 해야한다. 즉, 지금 존재하는 파드를 제거하고 새로운 버전의 이미지를 가진 파드를 생성해야 한다.</p>
</li>
</ol>
</li>
<li><p>서비스(Service)
파드의 IP가 언제든 바뀔 수 있기 때문에 파드를 참조할 수 있는 IP를 미리 정해두고 파드의 IP가 변한다면 알아서 연동해주는 기술이다. 서비스를 참조하면 서비스는 자신이 관리하는 파드에 연결해준다. 서비스는 고정적인 가상 IP를 받으므로 서비스를 제거하지 않는 한 변하지 않으므로 애플리케이션과 연동하기에 적합하다.
그리고 서비스는 자신이 관리해야할 파드를 파악하기 위해 라벨을 붙여놓는다.</p>
</li>
<li><p>레플리카세트(ReplicaSet)
파드는 언제든지 죽을 수 있기 때문에 ReplicaSet가 파드들을 직접 관리하며 파드에 문제가 생기면 새로운 파드를 생성하는 역할을 한다. 그리고 ReplicaSet를 생성할 때 어떤 파드를 만들어야 하는지 파드 정보를 설정하는 템플릿을 가진다.</p>
</li>
<li><p>디플로이먼트(Deployment)
애플리케이션의 새로운 버전을 배포하려면 모든 파드들을 죽인 후 새로 배포해야한다. 디플로이먼트는 이러한 문제를 해결해 준다.
ReplicaSet가 파드를 템플릿으로 가지고 있다면 디플로이먼트는 ReplicaSet를 템플릿으로 가지고 있다. 그래서 새로운 버전을 선언하는 명령어를 입력하면 디플로이먼트는 템플릿을 이용해 새로운 ReplicaSet를 생성하고 파드를 순차적으로 죽이고 새로운 파드를 생성한다. 한번에 파드를 죽이는게 아니기 때문에 애플리케이션이 정지되지 않는다.</p>
</li>
<li><p>인그레스(Ingress)
쿠버네티스 클러스터로 들어오는 요청들을 URL별로 분산시켜주는 L7 로드밸런서라고 보면된다. 같은 IP로 접근하여도 어떤 URL을 가지고 접근하는지, 어떤 패스를 가지고 접근하지에 따라 다른 서비스로 분산시켜 준다.</p>
</li>
</ul>
<h3 id="쿠버네티스-장점">쿠버네티스 장점</h3>
<ul>
<li><p>애플리케이션 생산성 향상
애플리케이션을 개발하기 위한 환경을 통일하여 환경 세팅을 하는 등 추가적인 작업이 필요가 없어 시간이 절약된다. 버그가 발견 되면 수정하기 위해 롤백과 롤아웃 기능도 제공한다.</p>
</li>
<li><p>신뢰성 있는 서비스 제공
쿠버네티스는 배포 및 통합을 위해 서버가 정지 없이 계속해서 운영이 되며 유저들에게 신뢰성 있는 서비스를 제공한다.</p>
</li>
<li><p>유연한 리소스 활용(오토스케일링)
쿠버네티스를 사용하면 사용자가 많다면 빠르게 서버를 확장하거나 사용자가 적다면 축소하여 리소스의 낭비를 줄이고 비용을 절약한다.</p>
</li>
</ul>
<p>참고한 사이트 :
<a href="https://deveric.tistory.com/103">https://deveric.tistory.com/103</a>
<a href="https://notepad96.tistory.com/125">https://notepad96.tistory.com/125</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSL, HTTPS]]></title>
            <link>https://velog.io/@dev_hoon2/SSL-HTTPS</link>
            <guid>https://velog.io/@dev_hoon2/SSL-HTTPS</guid>
            <pubDate>Thu, 27 Oct 2022 03:09:38 GMT</pubDate>
            <description><![CDATA[<h1 id="ssl이란">SSL이란???</h1>
<blockquote>
<p>SSL은 Secure Socket Layer의 약자로 암호화 기반의 인터넷 보안 프로토콜이다. 인터넷 통신의 개인정보 보호, 인증, 데이터 무결성을 보장하기 위해 Netscape가 1995년에 개발하였다.
SSL을 사용하는 웹사이트의 URL은 HTTP 대신 HTTPS를 사용한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev_hoon2/post/1140e000-7bbc-4cf7-b715-cf5601a30ceb/image.svg" alt=""><a href="https://www.cloudflare.com/ko-kr/learning/ssl/what-is-ssl/">이미지 출처</a></p>
<h3 id="ssl의-특징">SSL의 특징</h3>
<ul>
<li>URL 프로토콜은 HTTPS이고, 기본 포트는 443이다.</li>
<li>통신 데이터가 암호화되어 있기 때문에 패킷이 탈취당해도 데이터를 지킬 수 있다.</li>
<li>SSL 인증서를 통해 도메인의 신뢰성을 검증할 수 있다.</li>
<li>데이터 송/수신 과정에서 암/복호화가 발생하므로 속도가 느리다.</li>
</ul>
<h3 id="ssl의-원리">SSL의 원리</h3>
<p>아래 이미지 과정을 HandShake라고 부른다.
<img src="https://velog.velcdn.com/images/dev_hoon2/post/8064e367-50ae-4ea9-9156-251cc2c10519/image.png" alt=""><a href="https://blog.itcode.dev/posts/2021/08/18/about-ssl">이미지 출처</a></p>
<ol>
<li><p>ClientHello 요청
클라이언트가 특정 주소에 접근하면 난수 데이터, 암호화 프로토콜 정보, 클라이언트가 사용 가능한 암호화 기법, 세션 아이디, 기타 확장 정보를 해당하는 서버로 요청을 보낸다.</p>
</li>
<li><p>ServerHello 응답
서버가 ClientHello 요청을 받으면 난수 데이터(ClientHello의 데이터와 다르다.), 서버가 사용할 암호화 기법, 인증서를 담아 클라이언트에게 보내준다.</p>
</li>
<li><p>인증서 검토
서버가 전달한 인증서가 실제 해당 서버의 인증서인지, 신뢰할 수 있는 CA에서 발급한 것인지, 실제 해당 CA에서 발급받은 것인지를 검토한다.</p>
</li>
<li><p>Premaster Secret 송수신
ClientHello, ServerHello의 난수 데이터를 조합하여 Premaster Secret을 생성한다. 그리고 ServerHello에서 전달받았던 공개키로 암호화한다. 이 암호화한 키는 서버가 가진 개인키로만 복호화가 가능하다.</p>
</li>
<li><p>통신 키 생성
Premaster Secret을 토대로 Master Secret, Session Key를 생성한다. 이를 통해 클라이언트와 서버가 동일한 키를 보유하게 되므로 자신들끼리의 암호화 통신이 가능하다.</p>
</li>
<li><p>데이터 송수신
필요한 데이터는 저장된 Session Key를 통해 대칭키 암호화 방식으로 암호화,복호화하여 통신한다.</p>
</li>
<li><p>세션 종료
클라이언트와 연결이 끊기면 Session Key는 폐기한다.</p>
</li>
</ol>
<h1 id="https">HTTPS</h1>
<blockquote>
<p>HTTPS는 HyperText Transfer Protocol over Secure Sockey Layer의 약자로써 SSL 인증서를 이용한 HTTP 프로토콜의 보안 버전이다.</p>
</blockquote>
<h3 id="https의-필요성">HTTPS의 필요성</h3>
<p>HTTP는 통신을 할 때 데이터를 암호화하지 않고 그냥 전송하기 때문에 해킹당할 위험성이 높다. 그래서 SSL을 이용하여 주고 받는 정보를 암호화할 필요가 있다.</p>
<h3 id="https의-암호화-통신-과정">HTTPS의 암호화 통신 과정</h3>
<ul>
<li><p>사전 작업</p>
<ol>
<li>웹 서버에서 암호화된 HTTP 메시지를 해독할 키를 두 개 만든다.</li>
<li>키 중 하나는 서버에 보관하고, 다른 하나는 인증기관에게 준다.</li>
<li>인증기관은 이 키를 보안작업해서 인증서를 만든다.</li>
</ol>
</li>
<li><p>Request(클라이언트 요청)</p>
</li>
</ul>
<ol>
<li>브라우저가 서버에 보낼 HTTP 메시지 요청을 만든다.</li>
<li>브라우저가 인증기관에게 해당 서버의 인증서를 요청한다.</li>
<li>인증서가 있으면 브라우저에게 응답해준다.</li>
<li>브라우저가 인증서 내부의 키로 HTTP 메시지를 암호화한다.</li>
<li>암호화된 메시지를 서버에게 보낸다.</li>
</ol>
<ul>
<li>Response(서버 응답 과정)</li>
</ul>
<ol>
<li>서버가 암호화된 메시지를 받는다.</li>
<li>보관하고 있던 키로 메시지를 해독하고 확인한다.</li>
<li>사용자의 요청에 맞는 응답 HTTP 메시지를 만들고 키로 암호화한다.</li>
<li>암호화된 메시지를 브라우저에게 보낸다.</li>
</ol>
<ul>
<li>응답 확인</li>
</ul>
<ol>
<li>브라우저는 응답 HTTP 메시지를 인증서 내부의 키로 열고 확인한다.</li>
</ol>
<p>참고한 사이트 :
<a href="https://defineall.tistory.com/815">https://defineall.tistory.com/815</a>
<a href="https://www.cloudflare.com/ko-kr/learning/ssl/what-is-ssl/">https://www.cloudflare.com/ko-kr/learning/ssl/what-is-ssl/</a>
<a href="https://blog.itcode.dev/posts/2021/08/18/about-ssl">https://blog.itcode.dev/posts/2021/08/18/about-ssl</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Firewall / DMZ / VPC]]></title>
            <link>https://velog.io/@dev_hoon2/Firewall-DMZ-VPC</link>
            <guid>https://velog.io/@dev_hoon2/Firewall-DMZ-VPC</guid>
            <pubDate>Thu, 27 Oct 2022 03:08:11 GMT</pubDate>
            <description><![CDATA[<h1 id="firewall">Firewall</h1>
<blockquote>
<p>Firewall은 방화벽이라고도 하며, 미리 정의된 보안 규칙에 기반한 들어오고 나가는 네트워크 트래픽을 모니터링하고 제어하는 네트워크 보안 시스템이라고 할 수 있다.
방화벽은 일반적으로 신뢰할 수 있는 내부 네트워크와 신뢰할 수 없는 외부 네트워크 사이에 장벽을 구성한다.</p>
</blockquote>
<h3 id="firewall방화벽의-역활">Firewall(방화벽)의 역활</h3>
<p>방화벽은 불순한, 신뢰성이 낮은 데이터들의 유입을 막는 역활을 한다.
그리고 해킹 공격 방법과 패턴의 수준이 높아지면 방화벽 또한 복잡한 조건을 걸어 데이터를 막거나 접근을 제한 시키는 고급 기능도 구현 가능하다.</p>
<h3 id="방화벽의-기능">방화벽의 기능</h3>
<ul>
<li><p>패킷 필터
네트워크 계층에서 동작하는 방화벽이며 수립되어 있는 정책에 위반될 시에 패킷을 통과시키지 않는다.
패킷 필터는 패킷 자체만을 검사하는 무상태 방화벽과 패킷이 속하는 세션을 관리하여 이 세션에 속하는 패킷들을 모두 똑같이 처리하는 상태 방화벽이 있다.</p>
</li>
<li><p>프록시
세션에 포함된 유해성을 검사하기 위해 기존 세션을 종료하고 새로운 세션을 형성하는 기능이다.
검사를 많이 해서 부하가 걸리지만 좀 더 안전하고, 프로토콜 변경 등 추가적인 기능도 수행 가능하다.</p>
</li>
<li><p>NAT
내부 네트워크에서 사용하는 IP주소와 외부에 드러나는 주소를 다르게 유지해주는 기능이다.</p>
</li>
</ul>
<h3 id="방화벽의-형태">방화벽의 형태</h3>
<ul>
<li><p>스크린 라우터를 이용한 구조
패킷 필터 기능이 있는 스크린 라우터를 이용해서 방화벽 환경을 구성한다.
가장 심플한 구조이다.</p>
</li>
<li><p>듀얼 홈 호스트 구조
호스트가 두 개의 네트워크 인터페이스를 사용하는 구조다. 하나는 외부 네트워크에 연결하고 다른하나는 내부 네트워크에 연결한다.</p>
</li>
<li><p>스크린 호스트 구조
내부망에 베스천 호스트를 두고 스크린 라우터가 외부와 내부 하나씩 연결되어 있는 방식이다.
모든 통신은 베스천 호스트를 통해서만 가능하다.
그래서 베스천 호스트가 망가지면 큰일난다.</p>
</li>
<li><p>스크린 서브넷 구조
외부 네트워크와 내부 네트워크 사이에 하나 이상의 경계 네트워크를 두는 구조이다.
두개의 스크린 라우터를 사용한다.
하나는 외부와 DMZ, 다른 하나는 DMZ와 내부를 연결한다.</p>
</li>
</ul>
<h1 id="dmz">DMZ</h1>
<blockquote>
<p>DMZ는 Demilitarized Zone의 약자로 비무장지대라고도 부른다. 조직의 내부 네트워크와 외부 네트워크 사이에 위치한 서브넷이다.
내부 네트워크와 외부 네트워크가 DMZ로 연결될 수 있도록 허용하고, DMZ 내의 컴퓨터는 오직 외부 네트워크에서만 연결할 수 있도록 한다.
즉, DMZ 안에 있는 호스트들은 내부 네트워크로 연결할 수 없으며 DMZ에 있는 호스트들이 외부 네트워크로 서비스를 제공하면서 DMZ 안의 호스팅의 침입으로부터 내부 네트워크를 보호한다.
<img src="https://velog.velcdn.com/images/dev_hoon2/post/ac07c42c-e001-431c-bdf5-f212a57e128c/image.png" alt=""><a href="https://itprogramming119.tistory.com/entry/IT-%EC%83%81%EC%8B%9D-DMZ-%EC%84%9C%EB%B2%84%EB%9E%80">이미지 출처</a></p>
</blockquote>
<h1 id="vpc">VPC</h1>
<blockquote>
<p>VPC는 Virtual Private Cloud의 약자로서 GCP 리소를 위한 관리형 네트워킹 기능을 제공한다.
실제 네트워크와 동일한 방식으로 작동하며, 데이터 센터의 지역 가상 서브넷으로 구성된다.
글로벌 광역 네트워크로 연결된 글로벌 리소스이다.</p>
</blockquote>
<h3 id="vpc-네트워크의-특징">VPC 네트워크의 특징</h3>
<ul>
<li>연결된 라우터와 방화벽 규칙을 포함한 전역 리소스이다.</li>
<li>방화벽 규칙은 인스턴스에서 송수신 되는 트래픽을 제어할 수 있다.</li>
<li>서브넷은 지역 리소스이며, 각 서프벳은 CIDR을 이용해 IP주소 범위로 정의한다.</li>
<li>내부 IP 주소가 있는 인스턴스는 Google API 및 서비스와 통신이 가능하다.</li>
<li>네트워크 관리는 IAM을 사용해 관리 가능하다.</li>
<li>VPC 공유를 이용하면 VPC 네트워크를 공용 Host Project에 유지할 수 있다.</li>
<li>VPC 피어링으로 VPC 네트워크를 다른 Project 또는 organization의 다른 VPC 네트워크에 연결 할 수 있다.</li>
<li>Cloud VPN이나 Interconnect를 이용하면 온프레미스 환경이나 타 벤더의 클라우드 서비스를 연결할 수 있는 하이브리드 환경을 지원한다.</li>
<li>VPC 네트워크는 IPv4 유니 캐스트 트래픽만 지원한다.</li>
<li>각 프로젝트는 사전 정의된 default 네트워크로 시작하고, 커스텀을 통한 네트워크도 가능하다.</li>
</ul>
<h3 id="네트워크와-서브넷">네트워크와 서브넷</h3>
<ul>
<li>각 VPC 네트워크는 반드시 하나 이상의 서브넷을 가진다. 그래서 VPC 네트워크의 IP 주소 범위는 서브넷에 의해 결정된다.</li>
<li>서브넷은 두가지 모드로 생성이 된다.</li>
</ul>
<ol>
<li>자동모드 : 네트워크가 생성될 때 서브넷이 자동 생성되므로 IP범위가 겹치지 않는다.</li>
<li>커스텀모드 : 사용자가 직접 서브넷과 IP범위를 설정한다. 그래서 IP범위가 겹칠 수 있다.</li>
</ol>
<p>참고한 사이트 :
<a href="https://plummmm.tistory.com/423">https://plummmm.tistory.com/423</a>
<a href="https://itprogramming119.tistory.com/entry/IT-%EC%83%81%EC%8B%9D-DMZ-%EC%84%9C%EB%B2%84%EB%9E%80">https://itprogramming119.tistory.com/entry/IT-%EC%83%81%EC%8B%9D-DMZ-%EC%84%9C%EB%B2%84%EB%9E%80</a>
<a href="https://techblog-history-younghunjo1.tistory.com/21">https://techblog-history-younghunjo1.tistory.com/21</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jest]]></title>
            <link>https://velog.io/@dev_hoon2/Jest</link>
            <guid>https://velog.io/@dev_hoon2/Jest</guid>
            <pubDate>Wed, 26 Oct 2022 06:00:40 GMT</pubDate>
            <description><![CDATA[<h1 id="jest">Jest</h1>
<blockquote>
<p>Jest(제스트)는 페이스북에서 만든 자바스크립트 테스팅 라이브러리이다.
Jest 이전에는 자바스크립트 코드를 테스트하기 위해 여러가지 테스팅 라이브러리를 조합해서 사용하였지만 Jest가 등장하였고 Jest는 라이브러리 하나만 설치하면 Test Runner, Test Mathcher, Test Mock 프레임워크까지 제공해준다.</p>
</blockquote>
<h3 id="jest-기본문법">Jest 기본문법</h3>
<p>먼저 test 파일을 만든다. test 파일은 테스트할함수파일명.test.js로 해준다.</p>
<ul>
<li><p>describe()
여러개의 test() 코드를 하나의 테스트 작업 단위로 묶어주는 API다.
하나의 테스트 케이스를 test()라고 하면 describe()는 여러 개의 테스트 케이스를 하나의 그룹으로 묶어준다.</p>
</li>
<li><p>test()
테스트 코드를 돌리기 위한 API이다. 하나의 테스트 케이스를 의미한다.</p>
</li>
<li><p>expect()
테스트할 대상을 넣는 API다. 주로 테스트 입력 값 또는 기대 값을 넣는다.</p>
</li>
<li><p>beforeEach()
테스트 파일의 각 테스트 코드가 돌기 전에 수행할 로직을 넣는 API다. 테스트 케이스마다 반복되는 로직을 넣기에 좋다.</p>
</li>
</ul>
<p>참고한 사이트 :
<a href="https://inpa.tistory.com/entry/JEST-%F0%9F%93%9A-jest-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC">https://inpa.tistory.com/entry/JEST-%F0%9F%93%9A-jest-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC</a>
<a href="https://resilient-923.tistory.com/330">https://resilient-923.tistory.com/330</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Typescript Generic]]></title>
            <link>https://velog.io/@dev_hoon2/Typescript-Generic</link>
            <guid>https://velog.io/@dev_hoon2/Typescript-Generic</guid>
            <pubDate>Wed, 26 Oct 2022 06:00:16 GMT</pubDate>
            <description><![CDATA[<h1 id="typescript-generic">Typescript Generic</h1>
<ul>
<li>제네릭(Generic)은 타입을 함수의 파라미터처럼 사용하는 것들 의미한다.</li>
<li>정적 type 언어는 클래스나 함수를 정의할 때 type을 선언해야 한다.</li>
<li>제네릭은 코드가 수행될 때 타입을 명시한다.</li>
<li>코드를 작성할 때 식별자를 써서 아직 정해지지 않은 타입을 표시한다.</li>
<li>일반적으로 식별자는 T,U,V ...를 사용한다.</li>
</ul>
<h3 id="typescript-generic-예시">Typescript Generic 예시</h3>
<p>일반 함수의 경우 아래 코드처럼 aaa라는 파라미터에 어떤 값이 들어가더라도 그대로 반환한다.</p>
<pre><code>function qqq(aaa) {
    return aaa;
}

qqq(&quot;멍멍&quot;); // &quot;멍멍&quot;
qqq(7); // 7
qqq(true); // true</code></pre><hr>
아래 코드는 제네릭 기본 문법이 적용된 형태이다. 그리고 함수를 호출할 때 아래와 같이 함수 안에서 사용할 타입을 넘겨줄 수 있다. 그리고 `qqq<string>("멍멍")`을 예로 들어보자면 string 타입이 전부 제네릭 값으로 넘어간다. 즉 T들이 전부 string이 된다.

<pre><code>function qqq&lt;T&gt;(aaa: T): T {
    return aaa;
}

qqq&lt;string&gt;(&quot;멍멍&quot;); // &quot;멍멍&quot;
qqq&lt;number&gt;(7); // 7
qqq&lt;boolean&gt;(true); // true</code></pre><h3 id="제네릭을-사용하는-이유">제네릭을 사용하는 이유</h3>
<ul>
<li>한 가지 타입보다 여러 가지 타입에서 동작하는 컴포넌트를 생성하는데 사용된다.</li>
<li>재사용성이 높은 함수와 클래스를 생성할 수 있다.</li>
<li>오류를 쉽게 포착할 수 있다.(any타입은 컴파일 시 타입을 체크하지 않는다.)</li>
<li>제네릭도 any처럼 타입을 지정하지 않지만, 타입을 체크해 컴파일러가 오류를 찾을 수 있다.</li>
</ul>
<p>참고한 사이트 :
<a href="https://joshua1988.github.io/ts/guide/generics.html#%EC%A0%9C%EB%84%A4%EB%A6%AD-generics-%EC%9D%98-%EC%82%AC%EC%A0%84%EC%A0%81-%EC%A0%95%EC%9D%98">https://joshua1988.github.io/ts/guide/generics.html#%EC%A0%9C%EB%84%A4%EB%A6%AD-generics-%EC%9D%98-%EC%82%AC%EC%A0%84%EC%A0%81-%EC%A0%95%EC%9D%98</a>
<a href="https://lakelouise.tistory.com/188">https://lakelouise.tistory.com/188</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Crontab]]></title>
            <link>https://velog.io/@dev_hoon2/Crontab</link>
            <guid>https://velog.io/@dev_hoon2/Crontab</guid>
            <pubDate>Wed, 26 Oct 2022 05:59:16 GMT</pubDate>
            <description><![CDATA[<h1 id="crontab">Crontab</h1>
<blockquote>
<p>Crontab은 원하는 시간에 특정 작업을 실행하게 해주는 텍스트 파일이다.
이 텍스트 파일을 찾아서 Cron 이라는 시스템의 데몬이 작성된 일정대로 작업을 실행한다.</p>
</blockquote>
<h3 id="crontab-명령어">Crontab 명령어</h3>
<pre><code>$ crontab -e</code></pre><p>위의 명령어를 입력하면 텍스트를 입력할 수 있는 창이 뜨고 여기서 crontab을 설정할 수 있다. 다 작성하였다면 esc를 누르고 :q(저장안하고 종료), :q!(저장안하고 강제종료), :wq(저장하고 종료)를 통해서 편집기를 빠져나올 수 있다.</p>
<pre><code>$ crontab -l</code></pre><p>위의 명령어를 입력하면 등록된 스케줄을 확인할 수 있다.</p>
<pre><code>$ crontab -r</code></pre><p>위의 명령어를 입력하면 내용을 삭제한다.</p>
<p>Crontab * * * * * 의 의미</p>
<pre><code>*         *           *          *          *
분(0~59)  시간(0~23)   일(1~31)    월(1~12)   요일(0~6) / 0=일요일</code></pre><ul>
<li>매 분 test.sh 실행
```</li>
<li><ul>
<li><ul>
<li><ul>
<li><ul>
<li>test.sh
```</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>한시간 마다 test.sh 실행<pre><code>0 * * * * test.sh</code></pre></li>
<li>매주 일요일 오전 7시 30분에 test.sh 실행<pre><code>30 7 * * 0 test.sh</code></pre></li>
</ul>
<h4 id="crontab-로깅">Crontab 로깅</h4>
<pre><code>* * * * * test.sh &gt; /home/test.sh.log</code></pre><h4 id="crontab-강제종료">Crontab 강제종료</h4>
<pre><code>pkill -f &#39;wget -q -O - https://happist.com/wp-cron.php?doing_wp_cron &gt;/dev/null 2&gt;&amp;1&#39;</code></pre><h4 id="crontab-백업명령">Crontab 백업명령</h4>
<pre><code>23시50분에 매일 /home/backup/ 안에 crontab_bup.txt 이름으로 백업
50 23 * * * crontab -l &gt; /home/backup/crontab_bup.txt</code></pre><p>참고한 사이트 :
<a href="https://blog-han.tistory.com/91">https://blog-han.tistory.com/91</a>
<a href="https://hbase.tistory.com/304">https://hbase.tistory.com/304</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ACID]]></title>
            <link>https://velog.io/@dev_hoon2/ACID</link>
            <guid>https://velog.io/@dev_hoon2/ACID</guid>
            <pubDate>Wed, 26 Oct 2022 05:58:02 GMT</pubDate>
            <description><![CDATA[<h1 id="acid">ACID</h1>
<blockquote>
<p>ACID는 트랜젝션의 특징들의 앞글자를 딴 단어이다.</p>
</blockquote>
<h3 id="원자성atomicity">원자성(Atomicity)</h3>
<ul>
<li>트랜잭션과 관련된 작업들이 부분적으로 실행되다가 중단되지 않는 것을 보장한다.</li>
<li>모든 작업이 반영되거나 모두 롤백되는 특성이다.</li>
</ul>
<h3 id="일관성consistency">일관성(Consistency)</h3>
<ul>
<li>트랜잭션이 실행을 성공적으로 완료하면 언제나 일관성 있는 데이터베이스 상태로 유지된다.</li>
</ul>
<h3 id="고립성isolation">고립성(Isolation)</h3>
<ul>
<li>트랜잭션을 수행 시 다른 트랜잭션의 연산 작업이 끼어들지 못하게 보장한다.</li>
<li>트랜잭션 밖에 있는 어떤 연산도 중간 단계의 데이터를 볼 수 없다.</li>
<li>고립성은 트랜잭션 실행내역은 연속적이어야 함을 의미한다.</li>
</ul>
<h3 id="지속성durability">지속성(Durability)</h3>
<ul>
<li>성공적으로 수행된 트랜잭션은 영원히 반영되어야 함을 의미한다.</li>
<li>시스템 문제, DB 일관성 체크 등을 하더라도 유지되어야 한다.</li>
<li>모든 트랜잭션은 로그로 남고 시스템 장애 발생 전 상태로 되돌릴 수 있다.</li>
<li>트랜잭션은 로그에 모든 것이 저장된 후에만 커밋상태로 간주될 수 있다.</li>
</ul>
<p>참고한 사이트 :
<a href="https://goodgid.github.io/ACID/">https://goodgid.github.io/ACID/</a>
<a href="https://chrisjune-13837.medium.com/db-transaction-%EA%B3%BC-acid%EB%9E%80-45a785403f9e">https://chrisjune-13837.medium.com/db-transaction-%EA%B3%BC-acid%EB%9E%80-45a785403f9e</a></p>
]]></description>
        </item>
    </channel>
</rss>