<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>개발 끄적임 🖥</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sun, 27 Oct 2024 04:32:09 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>개발 끄적임 🖥</title>
            <url>https://velog.velcdn.com/images/ryu_log/profile/c38a1e4d-ffa4-4a59-b24b-f976c2a1026a/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 개발 끄적임 🖥. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ryu_log" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[5년간 잘 사용한 코드 리팩토링하기]]></title>
            <link>https://velog.io/@ryu_log/5%EB%85%84%EA%B0%84-%EC%9E%98-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%BD%94%EB%93%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ryu_log/5%EB%85%84%EA%B0%84-%EC%9E%98-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%BD%94%EB%93%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 27 Oct 2024 04:32:09 GMT</pubDate>
            <description><![CDATA[<p>최근, 서비스의 주요 플로우가 기존과 크게 달라지면서 프로덕트적으로 큰 변화가 있었는데요. 비즈니스 요구사항을 반영하면서 이번 기회를 통해 프로젝트 전체적으로 리팩토링을 하게되었습니다. 그 경험을 공유해보려고 합니다.</p>
<p>이 글을 이해하는데 있어서 이해하면 좋은 것들</p>
<ul>
<li>공유 전기자전거 도메인에서 개발을 하고 있습니다.</li>
<li>Python,  Django 를 통해 백엔드를 개발 하고 있습니다.</li>
<li>자사 서비스를 B2C, B2B 등 다양한 형태로 제공하고 있습니다.</li>
</ul>
<p>이번 프로젝트 태스크를 간단하게 생각하면, 기존의 구조를 유지한채 요구 사항을 수행하면 됩니다. 기존 로직에 <code>if/else</code> 평가를 추가하여 신규 로직을 반영하고 일정 기간이 지나면 기존 로직을 제거하고.. 아마 이러한 방법은 아마 대부분의 조직에서 사용하고 있는 방법이라고 생각됩니다. </p>
<p>하지만 이러한 방식이 반복되고, 비즈니스가 복잡하게 될 수록 남아있는 코드는 유지보수 난이도가 기하 급수적으로 올라갈 뿐만 아니라 작은 변화 하나에도 사이드 이펙트를 예상하기 어려워집니다. 이미 코드 상에는 수많은 비즈니스 요구 사항을 수행하기 위해 중첩된 <code>if/else</code> 가 존재했고, 최종 로직을 확인 하기 위해서는 평가의 평가의 평가의 평가의 … 평가를 이해 해야지만 최종 로직을 이해할 수 있었습니다.</p>
<p>다행히(?) 중요한 프로젝트인만큼 이전 프로젝트들보다 상대적으로 여유로운 개발 기간을 확보받았고, 서비스 앱의 전체적인 플로우의 변화가 예정되어 있었기 때문에 이번 기회가 리팩토링의 적기라고 생각했습니다. 프로젝트 시작 전, 함께하는 동료들과 굳건히 “귀찮음과 타협하지 않으리..” 마음을 먹고 시작하게 되었습니다.
<br></p>
<h3 id="기존-로직의-문제-정의하기">기존 로직의 문제 정의하기</h3>
<p>리팩토링을 시작 하기 전, 먼저 기존 서비스 로직의 문제를 정의해보았습니다. 문제점에 따라 리팩토링의 방향성이 달라지기 때문입니다.</p>
<ul>
<li>중복 코드<ul>
<li>주요 기능에 대한 모듈화가 되어 있지 않기 때문에 우리 서비스를 제공하는 여러 endpoint 마다 거의 비슷한 코드가 중복 생성되고 있었습니다.</li>
</ul>
</li>
<li>로직 내부의 강한 결합<ul>
<li>주요 도메인 함수 내부에서 외부 의존성이 강한 타 도메인을 직접 호출하고 있어, 테스트 및 재사용이 어려웠고 변경이 발생할 경우 관련 메서드를 모두 찾아 대응하는 등 유지보수 난이도가 매우 높았습니다.</li>
</ul>
</li>
</ul>
<p>이외에도 행위를 알 수 없는 평가와 방대한 클래스/메서드, 암묵적인 호출 등등 여러 부분들이 있었지만, 우선 순위를 정리하고 보니 대부분의 팀원이 유지 보수성이 떨어지는 것에 대해서 문제 의식이 있었고, 향후 비즈니스의 방향성을 고려했을 때 이번 리팩토링의 방향을 <code>확장성</code> 과 <code>유지보수 용이성</code> 으로 설정했습니다.
<br></p>
<h3 id="사용할-모듈-정의하기">사용할 모듈 정의하기</h3>
<p>확장성과 유지보수의 용이성을 확보하기 위해서는 우선 도메인 모듈을 만들고 각 모듈을 주입하기로 결정했습니다. 기존 로직에서는 크게 “모듈” 의 개념을 찾아보기는 어려웠습니다. Active-records 패턴을 적용하고 있는 Django의 경우 많은 로직이 Model 내부에 존재하다보니 사실 상 Model이 서비스 모듈로써 주요 비즈니스 로직을 처리하고 있었습니다.</p>
<p>하지만 대부분의 주요 로직이 모델 내부에서 흐르다보니 모델의 역할이 점점 커지고 모델 내부의 방대한 로직을 파악하기 어려웠웠습니다. 또한 너무 방대한 모델(fat-model) 이 전체 로직에서 너무 많은 역할을 하다보니, 각 영역간의 역할이 모호해졌습니다. 그래서 비즈니스 로직을 Django Model이 아닌  외부의 비즈니스 클래스로 위임하기로 결정했고, 이번 글에서는 대표적인 두 모듈만 소개하려고 합니다. </p>
<ul>
<li>RidingHandler: 라이딩 관련 모듈</li>
<li>FleetHandler: 기기 제어 모듈<br>

</li>
</ul>
<h3 id="신중하게-개발-순서-정의하기">신중하게 개발 순서 정의하기</h3>
<p>그 다음으로 일감에 대한 개발 순서를 정해보았습니다. 자칫 일감의 순서를 잘못 설계한 경우 이전 작업물이 현재 작업물에 영향을 미치고, 만약 이전 작업물에서 미쳐 고려하지 못한 부분이 있다면 작게는 수정해야하고, 최악의 경우 다시 개발해야하는 생산성의 저하가 발생할 수 있습니다. 따라서 신중하게 개발 순서를 선정 해야 했습니다. </p>
<p>이전 작업물에 최대한 영향을 받지 않기 위해서는 도메인간 의존성이 없는 부분부터 개발해야합니다. 최대한 간단하게 유저 플로우를 통해 도메인간의 의존성을 정리해보았습니다.</p>
<ol>
<li>유저는 <strong>라이딩 시작</strong>을 한다. 이 때 유저의 상태를 라이딩으로 변경하고, <strong>기기의 잠금의 해제 한다.</strong></li>
<li>유저는 <strong>라이딩을 마무리</strong>한다. 이 때 유저의 상태를 결제 상태로 변경하고, <strong>기기를 잠근다.</strong></li>
</ol>
<p>이렇게 작성하고 보니, 기기 제어 도메인은  이전 도메인의 영향을 받지 않고, 수행의 결과만 전달해주는 도메인이었고, 그 결과를 라이딩 도메인이 영향을 받고 있었습니다. 따라서 개발 순서를 기기 제어 모듈 → 라이딩 모듈 과 같이 정해보았습니다.
<br></p>
<h3 id="ref-1-templatemethod-pattern-으로-중복-제거하기">Ref 1. TemplateMethod Pattern 으로 중복 제거하기</h3>
<p>가장 먼저 중복을 제거하기 위해 <a href="https://refactoring.guru/ko/design-patterns/template-method">template method pattern</a> 을 적용해보았습니다. template method pattern 은 부모 클래스에서 알고리즘의 골격을 정의하지만, 해당 알고리즘의 구조를 변경하지 않고 자식 클래스들이 알고리즘의 특정 단계들을 오버라이드(재정의)할 수 있도록 하는 행동 디자인 패턴입니다.</p>
<p>위의 글 배경과 같이, 현재 자사 서비스를 B2C, B2B(InApp), B2B(API) 등 다양한 형태로 제공하고 있지만 모듈화 되어 있지 않기 때문에 비슷한 코드가 사실상 중복으로 관리되고 있었습니다. 한 예로, 저희 프로덕트에서는 전기 자전거를 타는 행위를 라이딩이라고 하며 이 “라이딩 시작” 로직이 각 endpoint별로 비슷한 형태로 관리되고 있었습니다. 라이딩의 제공 형태마다 조금씩 로직은 다르지만 큰 틀에서는 동일하고, 라이딩을 수행하기 위한 주요 로직이 중복으로 관리되고 있었습니다. </p>
<p>이 부분을 충분히 공통 메서드로 추상화 할 수 있다고 판단했고, 이후 필요하다면 overriding하여 행동을 구분하는 것이 유지 보수에 효율적일 것으로 판단했습니다. 먼저 base 클래스를 생성했습니다. base 클래스의 역할은 abstract class와 template method 를 정의한 클래스입니다. </p>
<p>라이딩 시작을 담당하는 클래스입니다. 이를 상속 받는 클래스는 <code>start()</code> 를 구현해야 합니다. 이전에는 abstract class 와 공통메서드를 담는 Mixin 클래스를 구분했지만 불필요한 상속 계층으로 판단되어 이번에는 base 클래스에 공통메서드를 함께 정의했습니다.</p>
<pre><code class="language-python">class RidingStartHandler(ABC):
    def __init__(
        self,
        ch: Optional[Thru],
        user: User,
        fleet_controller: AbstractFleetController,
    ):
        self.ch = ch
        self.user = user
        self.fleet_controller = fleet_controller
        self.bike = self.fleet_controller.bike

    @abstractmethod
    def start(self, data: RidingStartData) -&gt; Riding:
        pass</code></pre>
<p>이후 이전에 각각 관리되던 중복 로직을 base 클래스에 정의했습니다. 만약 하위 클래스에서 필요하다면 overriding 하여 구현을 다르게 가져갈 수 있습니다.</p>
<pre><code class="language-python">class RidingStartHandler(ABC):
           .
           .
    def validate(self, validate_data: RidingStartData):
            &quot;&quot;&quot;라이딩 시작 전 유효성 검증&quot;&quot;&quot;

    def create_riding(self, start_point: Point, start_addr: str, started_at: datetime.datetime, ch: Optional[Thru], extra: RidingStartExtra) -&gt; Riding:
        &quot;&quot;&quot;라이딩 생성&quot;&quot;&quot;

    def create_riding_sequence(self, riding: Riding) -&gt; RidingSequence:
        &quot;&quot;&quot;라이딩 이력 생성&quot;&quot;&quot;

    def set_status(self, riding_id: int, start_point: Point):
        &quot;&quot;&quot;
        바이크 / 유저의 상태를 라이딩 상태로 변경
        Args:
            riding_id: 대상 라이딩 id
            start_point: 라이딩 시작 위치

        Returns:
            None
        &quot;&quot;&quot;

    def set_riding_point(self, riding: Riding, start_point: Point, origin: int, gps_accuracy: float):
        &quot;&quot;&quot;
        라이딩 시작 위치 기록, 라이딩 포인트 캐시 초기화
        Args:
            riding: 대상 라이딩
            start_point:
                - 자사 라이딩: 클라이언트로부터 요청 받은 위치
                - b2b 라이딩: 기기 위치
            origin: 위치 정보 출처(0: 클라이언트, 1: 기기)
            gps_accuracy: 기기 위치 정확도. 낮을 수록 신뢰할 수 있는 값

        Returns:
            None
        &quot;&quot;&quot;</code></pre>
<p>이 방식을 통해 중복 코드를 제거할 수 있었고, 공통 메서드를 벗어난 경우 하위 구현체에서 메서드를 추가하거나 overriding을 통해 다시 구현하면 되기 때문에 이전 로직을 수행하는데도 문제 없이 처리 할 수 있었습니다.
<br></p>
<h3 id="ref-2-factory-pattern으로-동적으로-사용할-모듈-결정하기">Ref 2. Factory Pattern으로 동적으로 사용할 모듈 결정하기</h3>
<p>다음은  <a href="https://refactoring.guru/ko/design-patterns/factory-method/python/example">팩토리 패턴</a>을 적용했습니다. 팩토리 패턴이란 객체 생성 로직을 별도의 “팩토리 클래스”나 “메서드”로 분리해 관리하는 디자인 패턴입니다. 객체 생성의 복잡성을 감추고 코드의 유연성과 재사용성을 높이는 패턴입니다.</p>
<p>자사 서비스에서 제공하는 라이딩 형태를 정의하고, 특정 slug 에 따라 라이딩 시작을 하는 행위를 담당하는 팩토리 클래스를 생성했습니다. (실제 코드는 아닙니다. 협력사 정보가 포함되어 일부 수정하여 작성했습니다). </p>
<pre><code class="language-python">class RidingStartFactory:
    @staticmethod
    def create(ch: Optional[Thru], user: User, fleet_controller: AbstractFleetController) -&gt; RidingStartHandler:
        match ch:
            case Thru.DIRECT | None:
                return DirectRidingStartHandler(ch, user, fleet_controller)
            case Thru.APPLESS:
                return AppLessRidingStartHandler(user, fleet_controller)
            case Thru.B2B_1:
                return B2B1RidingStartHandler(user, fleet_controller)
            case Thru.B2B_2:
                return B2B2RidingStartHandler(user, fleet_controller)
            case _:
                raise PreconditionRequired(&#39;지원하지 않는 라이딩입니다.&#39;)</code></pre>
<p>라이딩을 시작하고 싶은 호출부에서는 해당 조건을 충족할 slug와 파라미터만 주입 한다면 간편하게 객체를 생성할 수 있습니다.</p>
<p>기기 제어 모듈도 마찬가지입니다. 통신 방식에 따라 구분이 되어 호출하는 모듈이 다르기 때문에 이전에는 하나의 코드 상에서 <code>if/else</code> 로 로직이 전개되었다면, 팩토리 패턴을 통해 보다 깔끔하게 객체를 생성할 수 있습니다.</p>
<pre><code class="language-python">class FleetControllerFactory:
    @staticmethod
    def create(bike: Bike, logger: ControlLogger, mediator: Mediator, **kwargs) -&gt; AbstractFleetController:
        if bike.interactor.legacy_protocol:
            return LegacyFleetController(bike, logger, mediator, **kwargs)
        else:
            return FleetController(bike, logger, mediator, **kwargs)</code></pre>
<p>호출부의 코드는 아래와 같습니다. 각 endpoint별로 매우 절차지향적으로 객체 생성 부터, 비즈니스 로직까지 전개되던 부분을 다음 layer 인 비즈니스 모듈로 넘기면서 controller 에서는 전체적인 코드 전개만 파악할 수 있게 되면서 코드를 파악할 때 훨씬 이점이 생겼습니다.</p>
<pre><code class="language-python">def riding_start():
    # 기기 제어 모듈 생성
        fleet_controller = FleetControllerFactory.create(bike, Logger(), Mediator())

        # 라이딩 시작 모듈 생성
        riding_handler = RidingStartFactory.create(data.partner_slug, user, fleet_controller)

        # 라이딩 시작
        riding = riding_handler.start()</code></pre>
<br>

<h3 id="ref-3-strategy-pattern-으로-기능-주입-받기">Ref 3. Strategy Pattern 으로 기능 주입 받기</h3>
<p>마지막으로 <a href="https://refactoring.guru/ko/design-patterns/strategy">strategy pattern</a> 을 적용해보았습니다. strategy pattern을 찾아보면, 알고리즘들의 패밀리를 정의하고, 각 패밀리를 별도의 클래스에 넣은 후 그들의 객체들을 상호교환할 수 있도록 하는 행동 디자인 패턴.. 이라고 정의하고 있습니다. </p>
<p>매우 어려운 표현인데요. 제가 쉽게 이해한 바는 행위를 인터페이스로 구현한 후, 이를 상속 받은 하위 구현체들을 생성합니다. 그리고 이 모듈에 타 모듈에 주입하여 사용하는 패턴입니다. 전략 패턴의 가장 큰 장점은 수많은  <code>if/else</code> 코드 블럭을 제거하면서 행위 자체에 집중할 수 있다는 점입니다. 또한 신규 기능이 추가되더라도 기존 기능을 손대지 않고도 기능을 추가할 수 있습니다.</p>
<p>전략 패턴의 예시는 이미 위에 모두 표현이 되었는데요. 전략 패턴의 관점에서 다시 한 번 정리하면, 아래와 같습니다. 기기 제어 모듈의 인터페이스는 아래와 같습니다.</p>
<pre><code class="language-python">class AbstractFleetController(ABC):
    def __init__(self, bike: Bike, logger: ControlLogger, mediator: Mediator):
        self.bike = bike
        self.logger = logger
        self.mediator = mediator

    @abstractmethod
    def lock(self) -&gt; tuple[bool, str]:
        &quot;&quot;&quot;기기를 잠금한다.&quot;&quot;&quot;
        pass

    @abstractmethod
    def unlock(self) -&gt; tuple[bool, str]:
        &quot;&quot;&quot;기기를 잠금 해제한다.&quot;&quot;&quot;
        pass</code></pre>
<p>이를 상속받은 클래스는 FleetController, LegacyFleetController 가 있습니다.</p>
<pre><code class="language-python">class FleetController(AbstractFleetController):
   def lock(self) -&gt; tuple[bool, str]:
        if self.bike.status in (self.bike.NEED_REPAIRING, self.bike.IN_REPAIRING):
            command = self.get_fix_command()
        else:
            command = self.get_lock_command()

    return self.execute_command(command, ControlStatus.REQUESTED)

  def unlock(self) -&gt; tuple[bool, str]:
      command = self.get_unlock_command()
      return self.execute_command(command, ControlStatus.REQUESTED)


class LegacyFleetController(AbstractFleetController):
    def lock(self) -&gt; tuple[bool, str]:
        if self.bike.status in (Bike.NEED_REPAIRING, Bike.IN_REPAIRING):
            return self.in_repairing()
        return self.execute_command(action=&#39;lock_control&#39;, value=&#39;lock&#39;)

    def unlock(self) -&gt; tuple[bool, str]:
        return self.execute_command(action=&#39;lock_control&#39;, value=&#39;unlock&#39;)</code></pre>
<p>기기 제어 모듈은 팩토리 패턴으로 생성됩니다.</p>
<pre><code class="language-python">class FleetControllerFactory:
    @staticmethod
    def create(bike: Bike, logger: ControlLogger, mediator: Mediator, **kwargs) -&gt; AbstractFleetController:
        if bike.interactor.legacy_protocol:
            return LegacyFleetController(bike, logger, mediator, **kwargs)
        else:
            return FleetController(bike, logger, mediator, **kwargs)

# 기기 제어 모듈 생성
fleet_controller = FleetControllerFactory.create(bike, Logger(), Mediator())</code></pre>
<p>이렇게 생성된 기기 제어 모듈을 라이딩 모듈에 주입하고, 이후 비즈니스 로직이 수행됩니다.</p>
<pre><code class="language-python">**# 라이딩 시작 모듈에 기기 제어 모듈 주입**
riding_handler = RidingStartFactory.create(data.partner_slug, user, **fleet_controller**)

# 라이딩 시작
riding = riding_handler.start()</code></pre>
<p>이럴 경우, 라이딩 모듈은 기기 제어 모듈이 어떤 프로토콜로 정의된 모듈인지는 중요하지 않고, <code>unlock</code>, <code>lock</code> 과 같은 행위만 보고 실행하게 됩니다. 또한 새로운 프로토콜이 추가된 기기 제어 모듈이 추가되더라도 라이딩 모듈을 수정할 필요 없이, 기기 제어 모듈을 추가 구현한 후 팩토리 클래스에 이를 추가하기만 한다면 기존 로직을 유지한 채 새로운 로직을 추가할 수 있는 이점이 있습니다.
<br></p>
<h3 id="마무리">마무리</h3>
<p>이처럼 우연치 않게 좋은 기회를 맞아 서비스 전반의 로직을 리팩토링 해보았는데요. 이번 리팩토링을 진행하며서 사업/기획 팀에게 가장 많이 들은 질문은 “왜 리팩토링을 하시는거에요 ?”, “이번에 꼭 리팩토링이 필요했나요?” 였습니다. </p>
<p>당연히 리팩토링이 모든 태스크의 must-have 는 아닐뿐더러 기존 로직을 크게 변경하는만큼 프로덕트의 리스크를 감수해야할 뿐만 아니라 리팩토링만을 위한 추가 리소스가 필요하기 때문에 비즈니스 관점에서는 크게 환영 받지 못하는 개발 태스크라는 생각이 많이 들었던 것 같습니다. </p>
<p>이번 작업을 하면서 들었던 생각은 앞으로 리팩토링을 오로지 개발팀의 욕구를 충족하는 태스크가 아니라 프로덕트, 비즈니스 관점에서도 충분히 생각할 가치가 있는 태스크라고 생각이 들었습니다. 리팩토링을 통해 사이드 이펙트 없이 신규 기능 변경 및 추가, 빠르게 신규 기능을 추가할 수 있는 부분 등 장기적으로 보았을 때 분명 비즈니스의 전체적인 생산성 증가가 나타날 것이라고 생각이 들었습니다. 이후 다시 리팩토링을 하게 된다면 .. 요런 관점에서 어필을 통해 좀 더 개발 리소스 확보 및 리팩토링에 대한 설득을 할 수 있지 않을까 생각이 들었습니다. </p>
<p>그리고 … 리팩토링을 준비하는 분들이 있다면 반드시 먼저 읽어보시면 좋을 것 같습니다 <a href="https://news.hada.io/topic?id=16575">좋은 리팩토링 vs 나쁜 리팩토링</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[grpc 환경에서 golang sentry 연동하기]]></title>
            <link>https://velog.io/@ryu_log/grpc-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-golang-sentry-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ryu_log/grpc-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-golang-sentry-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 21 Apr 2024 09:32:38 GMT</pubDate>
            <description><![CDATA[<p>Sentry는 실시간 로그 취합 및 분석 도구이자 모니터링 플랫폼입니다. 로그에 대해 다양한 정보를 제공하고 이벤트별, 타임라인으로 얼마나 많은 이벤트가 발생하는지 알 수 있고 설정에 따라 알림을 받을 수 있습니다. (출처: <a href="https://tech.kakaopay.com/post/frontend-sentry-monitoring">https://tech.kakaopay.com/post/frontend-sentry-monitoring</a>)</p>
<p>grpc, unary protocol 환경에서 golang 애플리케이션과 sentry를 연동하려고 할 때는 grpc 서버 옵션을 추가해주면 됩니다.</p>
<h4 id="서버-띄우기">서버 띄우기</h4>
<p>일반적으로 golang + grpc는 main.go에서 아래와 같이 서버를 띄웁니다.</p>
<pre><code class="language-go">// main.go

grpcServer := server.NewGRPCServer(opts...)</code></pre>
<h4 id="옵션-추가하기">옵션 추가하기</h4>
<p>이 때 opts.. 는 <code>[]grpc.ServerOption</code> 타입이고 <code>grpc.ChainUnaryInterceptor()</code> 메서드를 통해 옵션을 추가할 수 있습니다. </p>
<pre><code class="language-go">
// main.go

var opts []grpc.ServerOption
opts = append(opts, grpc.ChainUnaryInterceptor())
grpcServer := server.NewGRPCServer(opts...)</code></pre>
<p><code>ChainUnaryInterceptor()</code>에는 이때 <code>UnaryServerInterceptor</code>의 타입이 올 수 있는데 이 때 함수 타입이며 아래와 같습니다</p>
<pre><code class="language-go">type UnaryServerInterceptor func(ctx context.Context, req any, info *UnaryServerInfo, handler UnaryHandler) (resp any, err error)</code></pre>
<h4 id="미들웨어-정의">미들웨어 정의</h4>
<p>이제 <code>UnaryServerInterceptor</code>와 같은 타입의 센트리 미들웨어를 추가해야합니다. 코드는 간단합니다. grpc handler의 결과의 err가 존재한다면 이를 sentry로 전송하는 미들웨어입니다.</p>
<pre><code class="language-go">// main.go

// SentryInterceptor is a gRPC interceptor that captures exceptions with Sentry.
func SentryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    resp, err := handler(ctx, req)
    if err != nil {
        resp = sentry.CaptureException(err)    
    }
    return resp, err
}</code></pre>
<h4 id="unknown만-핸들링하기">unknown만 핸들링하기</h4>
<p>하지만 위와 같이 진행하면, 문제가 생깁니다. 의도한 에러를 발생시키더라도 err가 존재하여 모두 센트리에 잡히게 됩니다.
센트리의 경우, 내가 예상하지 못한 에러일 경우에만 알림을 줘야하는데요. 만약 모든 에러가 센트리에 잡혀버린다면 효율적인 이슈 트래킹이 어렵습니다. 그렇기 때문에 아래와 같은 코드를 추가해줘야합니다.</p>
<pre><code class="language-go">// SentryInterceptor is a gRPC interceptor that captures exceptions with Sentry.
func SentryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    resp, err := handler(ctx, req)
    if err != nil {
        if status.Code(err) == codes.Unknown {
            resp = sentry.CaptureException(err)
        }
    }
    return resp, err
}</code></pre>
<p>err 의 코드가 grpc error 에 정의된 에러 코드 중 2, <code>Unknown</code>일 때만 센트리로 전송할 수 있게 추가했습니다. 만약 그렇다면 Not Found, Internal 등 의도적으로 필요에 의해 에러를 반환한 경우 센트리에 찍히지 않게됩니다.</p>
<p>(참고) grpc error interface</p>
<pre><code class="language-go">const (
    Canceled Code = 1

    Unknown Code = 2

    InvalidArgument Code = 3

    DeadlineExceeded Code = 4

    NotFound Code = 5

    AlreadyExists Code = 6

    PermissionDenied Code = 7

    ResourceExhausted Code = 8

    FailedPrecondition Code = 9

    Aborted Code = 10

    OutOfRange Code = 11

    Unimplemented Code = 12

    Internal Code = 13

    Unavailable Code = 14

    DataLoss Code = 15

    _maxCode = 17
)</code></pre>
<h4 id="실제-로직에서-적용하기">실제 로직에서 적용하기</h4>
<pre><code class="language-go">cursor, mongoErr := r.Mongo.FindOne(ctx, &quot;db&quot;, &quot;collection&quot;, bson.M{&quot;id&quot;: &quot;id&quot;})
if mongoErr != nil {
    if errors.Is(mongoErr, m.ErrNoDocuments) {
        return nil, errors.WithStack(status.Errorf(codes.NotFound, &quot;can not found&quot;))
    } else {
        return nil, errors.WithStack(mongoErr.Error())
    }
}</code></pre>
<p>위 코드는 mongo driver를 통해 데이터를 조회하는 로직입니다. 여기서 만약 error의 타입이 <code>ErrNoDocuments</code>라면, 의도했고 이를 제외한 모든 에러는 핸들링하지 않은 에러입니다. <code>ErrNoDocuments</code> 의 경우는 센트리에 찍히지 않을 것이고, 나머지 경우에는 센트리에 찍혀 이를 확인하고 대응 할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[테스트 코드를 짜다보면 보이는 것들.. (이론/실전편)]]></title>
            <link>https://velog.io/@ryu_log/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%A7%9C%EB%8B%A4%EB%B3%B4%EB%A9%B4-%EB%B3%B4%EC%9D%B4%EB%8A%94-%EA%B2%83%EB%93%A4..-%EC%9D%B4%EB%A1%A0%EC%8B%A4%EC%A0%84%ED%8E%B8</link>
            <guid>https://velog.io/@ryu_log/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%A7%9C%EB%8B%A4%EB%B3%B4%EB%A9%B4-%EB%B3%B4%EC%9D%B4%EB%8A%94-%EA%B2%83%EB%93%A4..-%EC%9D%B4%EB%A1%A0%EC%8B%A4%EC%A0%84%ED%8E%B8</guid>
            <pubDate>Sun, 21 Apr 2024 08:06:15 GMT</pubDate>
            <description><![CDATA[<h2 id="왜-테스트-코드를-짜라고-하는거야-이론편">왜 테스트 코드를 짜라고 하는거야.. (이론편)</h2>
<p>테스트 코드를 짜는 이유.. 를 검색해보면 너무나도 많은 블로그와 글에서 테스트코드의 중요성을 전달하고 있습니다. </p>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/4750ef44-57ee-432d-8ee6-677434ab030f/image.png" alt=""></p>
<p>그 내용을 요약하면 아래와 같고, 많은 분들이 이미 알고있는 내용이라서 상세하게 풀지는 않겠습니다.</p>
<h3 id="테스트-코드를-짜야하는-이유">테스트 코드를 짜야하는 이유</h3>
<ol>
<li>디버깅 비용 절감</li>
<li>코드 변경에 대한 불안감 해소<ul>
<li>개발 초기 단계에 문제를 발견하게 도와줌</li>
<li>리팩토링 / 라이브러리 업그레이드에서 기존 기능이 올바르게 작동하는지 알 수 있음</li>
</ul>
</li>
<li>단위 테스트는 시스템에 대한 실제 문서를 제공<ul>
<li>단위 테스트 자체가 문서로 사용 가능</li>
</ul>
</li>
<li>결합도와 의존성이 낮은 코드 지향</li>
</ol>
<h3 id="나도-알긴-아는데">나도 알긴 아는데..</h3>
<blockquote>
</blockquote>
<p>🤔 너도나도 테스트 코드를 작성해야한다고 하고.. 
🤔 막상 들어보면 틀린말도 아니며.. 
🤔 TDD, TDD.. 하는데 나도 한 번 짜볼까 하지만..</p>
<p>막상 일을 하다보면 테스트 코드는 늘 후순위가 되곤 했습니다. 그래도 주변 사람들이 좋다고 하니.. 
<code>해볼까</code> 하는 생각에, 몇 개의 프로젝트에서만 테스트 코드를 일부 적용해보기로 했습니다.</p>
<h2 id="왜-테스트-코드를-짜라고-하는거야-실전편">왜 테스트 코드를 짜라고 하는거야.. (실전편)</h2>
<p>막상 테스트 코드를 작성하고 프로젝트에 적용해보니, 저는 아래의 경험을 얻게 되었습니다.</p>
<h3 id="1-디버깅-비용-절감-어떻게-다들-기능-테스트-하시나요-">1. 디버깅 비용 절감 (어떻게 다들 기능 테스트 하시나요 ?)</h3>
<p>이전까지 저의 개발 플로우는 아래와 같았습니다.</p>
<blockquote>
<ol>
<li>개발 - 2. 서버 시동 - 3. 포스트맨 호출 - 4. 디버깅 - 5. 오류 수정 - 6. 서버 재시동 - 7. 포스트맨 호출 … 8. (반복) ...</li>
</ol>
</blockquote>
<p>여기에 특정 값에 의해 결과가 달라져야한다면, <code>DB의 값을 추가 / 변경</code> 하곤 했었습니다. 이미 저에게는 익숙한 플로우라 개발을 하는데 불편함은 없었지만 늘 고민은 <code>개발 - 테스트 - 완료</code> 한 사이클을 진행하는데 너무 많은 시간 비용을 투자하는 것이었습니다.</p>
<p>이런 저의 개발 플로우에 테스트코드를 적용해보니 아래와 같이 변경되었습니다.</p>
<blockquote>
<ol>
<li>개발 - 2. 테스트 코드 실행 - 3. 디버깅 - 4. 오류 수정 - 5. 테스트 코드 실행 … 6. (반복)</li>
</ol>
</blockquote>
<p>뭐가 크게 달라졌는가 싶지만, 위의 <code>개발 - 테스트 - 완료</code> 한 사이클을 진행하는데 시간이 절반 이하로 줄어들었습니다. </p>
<blockquote>
<ol>
<li>서버를 재시동 할 필요도 없고</li>
<li>포스트 맨을 통해 호출할 필요가 없었으며</li>
<li>무엇보다 테스트 대상의 mocking value만 변경하면 되니, db를 손대지 않아도 되니, 그 시간이 크게 줄었습니다</li>
</ol>
</blockquote>
<p>개발의 한 사이클의 시간을 줄이고 나니까, 그 시간에 여러 값을 mocking 해볼 수 있었고, 안보이던 버그나 오류들도 찾아낼 수 있었습니다.</p>
<h3 id="2-단위-테스트는-시스템에-대한-실제-문서를-제공-with-python">2. 단위 테스트는 시스템에 대한 실제 문서를 제공 (with. python)</h3>
<h4 id="어떻게-메서드의-역할과-요구사항을-잘-전달하려고-하시나요-">어떻게 메서드의 역할과 요구사항을 잘 전달하려고 하시나요 ?</h4>
<p>비즈니스 사이드의 요청 사항은 늘 가변적이며, 그 히스토리를 파악하기 어렵습니다. 그래서 일반적으로 리뷰어나 다른 사람에게 요구 사항을 잘 전달하기 위해서는 </p>
<blockquote>
<ol>
<li>docstring을 추가하거나<ol start="2">
<li>in-line 코멘트에 그 역할과 주의 사항을 적어놓거나</li>
<li>method signature를 잘 작성하거나</li>
</ol>
</li>
</ol>
</blockquote>
<p>등의 방법으로 요구사항과 히스토리를 전달합니다.</p>
<h4 id="어떻게-남이-짜놓은-메서드를-파악하시나요-">어떻게 남이 짜놓은 메서드를 파악하시나요 ?</h4>
<p>이 기능이 어떤 기능인지, 어떤 맥락을 가지고 있는지를 확인하기 위해서는 코드 라인 한줄 한줄 따라 가야 합니다. </p>
<blockquote>
<ol>
<li>한 개의 메서드가 100줄, 200줄이라면 ?</li>
<li>만약 이런 메서드가… 100개.. 200개.. 1000개 라면 ?</li>
</ol>
</blockquote>
<p>기능하나를 이해하는데 러닝 커브가 기하급수적으로 증가하게 됩니다.</p>
<p>물론 대부분의 PR Conversation에 작업의 히스토리가 담겨있거나, 좀 더 체계적인 조직에서는 관련 문서가 모두 정리되어 있습니다. 하지만 이 모든 것을 갖춘 환경은 찾기 어렵고, 체계적으로 히스토리가 관리되어 있지는 않습니다.</p>
<h4 id="아래-메서드에-담긴-히스토리는-무엇일까요-">아래 메서드에 담긴 히스토리는 무엇일까요 ?</h4>
<p>아래 코드는 프로모션 할인 금액을 계산하는 코드입니다. 최대한 비즈니스 요구사항과, 맥락, 기능을 전달하기 위해 in-line 코멘트를 작성했습니다. in-line 코멘트에도 적혀있다시피 구 프로모션과의 관계, 구 프로모션과 신규 프로모션이 겹칠 경우, 결제 취소가 되는 경우 등등 요구 사항이 매우 복잡합니다.</p>
<pre><code class="language-python">def get_promotion_discount(self) -&gt; int:

    PROMOTION_FREE_BENEFIT = 5

    # 3월 20일 까지는 구 프로모션만 적용한다.
    if settings.PROD_DB and datetime.date(2024, 3, 20) &gt;= datetime.date.today():
        return self.get_legacy_es_union_promotion_discount()

    # 구 프로모션 혜택을 받을 수 있는 유저의 경우, 구 프로모션을 혜택만 적용한다.
    if self.user.extra.get(&#39;launching_promo_2312&#39;) and (discount_amount := self.get_legacy_es_union_promotion_discount()):
        return discount_amount

    # 론칭 프로모션 혜택을 받았던 라이딩이라면, 이후 결제 취소가 되더라도 재결제 시 동일한 론칭 프로모션 혜택을 받는다.
    if self.riding.extra.get(&#39;launching_promo_2403&#39;):
        return self.calculate_discount(self.riding.pricing, PROMOTION_FREE_BENEFIT)

    # 유저 생애 최초 3회 라이딩의 경우, 신규 프로모션 혜택을 받는다.
    if self.user.ridings.exclude(id=self.riding.id).count() &gt;= 3:
        return 0

    # 프로모션 혜택 받은 라이딩이라면, 별도의 슬러그를 추가한다.
    self.riding.extra[&#39;launching_promo_2403&#39;] = True
    self.riding.save(update_fields=(&#39;extra&#39;, &#39;modified_at&#39;))

    return self.calculate_discount(self.riding.pricing, PROMOTION_FREE_BENEFIT)</code></pre>
<p>여러가지 장치로 메서드의 기능을 확인하기는 용이할지몰라도, 도메인 / 비즈니스 요구사항을 파악하기는 상대적으로 어렵습니다. 하지만 테스트 코드를 본다면 비즈니스를 이해하기 상대적으로 용이합니다.</p>
<h4 id="테스트-코드를-보면-코드의-히스토리를-알기-편하다-">테스트 코드를 보면 코드의 히스토리를 알기 편하다 !</h4>
<pre><code class="language-python">def test_구_프로모션_기간_첫_이용_후_30일_이내에_시작한_하루_최초_라이딩은_신규_프로모션_혜택을_받을_수_있다()
    ...

def test_구_프로모션_기간_첫_이용_후_30일_이후에_시작한_하루_최초_라이딩은_신규_프로모션_혜택을_받을_수_없다()
    ...

def test_구_프로모션_기간_중_하루_두번_째_라이딩은_신규_프로모션_혜택을_받을_수_없다()
    ...

def test_신규_프로모션_이후_생애_첫_라이딩을_시작했고_총_라이딩_횟수가_3회_이하라면_첫_세번_무료_혜택만_받는다()
    ...

def test_신규_프로모션_이후_생애_첫_라이딩을_시작했고_총_라이딩_횟수가_3회_초과라면_어떤_혜택도_받지_않는다()
    ...

def test_신규_프로모션_이후_생애_첫_라이딩을_시작했고_전체환불의_경우도_횟수에_총_라이딩_횟수에_포함되기_때문에_3회_이상_라이딩을_했다면_혜택을_받을_수_없다()
    ...

def test_구_프로모션과_신규_프로모션_중복_대상자의_하루_첫_라이딩에는_구_프로모션_혜택을_받는다()
    ...

def test_구_프로모션과_신규_프로모션_중복_대상자의_하루_첫_라이딩이_아니라면_신규_프로모션_혜택을_받는다()
    ...</code></pre>
<p>유닛 테스트 케이스를 보면, </p>
<blockquote>
<ol>
<li>이 코드가 어떤 역할을 해야 하는지</li>
<li>어떤 예외 사항을 처리 해야 하는지</li>
<li>어떤 맥락과 히스토리를 가지고 있는지</li>
</ol>
</blockquote>
<p>별도의 문서를 찾아보지 않더라도 코드의 역할을 쉽게 이해할 수 있습니다.</p>
<h3 id="3-결합도와-의존성이-낮은-코드-지향-더-나은-코드를-위해-with-golang">3. 결합도와 의존성이 낮은 코드 지향. 더 나은 코드를 위해 (with. golang)</h3>
<h4 id="어떻게-코드를-유지보수-하시나요--어떤-관점으로-리팩토링-하시나요-">어떻게 코드를 유지보수 하시나요 ? 어떤 관점으로 리팩토링 하시나요 ?</h4>
<p>아래 코드는 </p>
<blockquote>
<ul>
<li>정책 옵션을 조회하여</li>
</ul>
</blockquote>
<ul>
<li>외부 쿠폰 플랫폼에게 grpc 요청을 통해 특정 기간 동안 사용한 쿠폰의 개수를 조회한 후</li>
<li>정책 상 최대 사용 개수와 실제 사용 개수를 비교한 후</li>
<li>다음 쿠폰을 발급할지를</li>
</ul>
<p>결정하는 메서드입니다.</p>
<pre><code class="language-go">// checkMaxUsedCountIssuable 정책에 정의된 기간 내에 사용한 쿠폰의 최대 개수를 초과하지 않았는지 확인
func (svc CouponPolicyService) checkMaxUsedCountIssuable(memberId int64, policyId string, rule repository.Rule) (bool, error) {
    // 정책 옵션 조회
    policyOption, err := svc.getMaxUsedCountPolicyOption(rule)
    if err != nil {
        return false, err
    }

    // grpcClient 생성
    conf := config.NewConfig().GetConfig()
    grpcClient := client.NewGRPCClient(
        client.WithAddress(conf.CouponPlatformHost, &quot;443&quot;),
        client.WithInsecure(false),
        client.WithToken(conf.CouponPlatformGRPCToken),
    )

    location, _ := util.KSTLocation()
    interval := util.GetInterval(time.Now(), option.day, location)

    // 사용된 쿠폰 개수 조회
    res, requestErr := grpcClient.RequestListUsedCoupons(memberId, policyId, interval)
    if requestErr != nil {
        return false, requestErr
    }

    // 최대 사용 가능 개수와 실제 사용된 쿠폰 개수 비교
    issuable := option.count &gt;= int32(len(res.Coupons))

    return issuable, nil
}</code></pre>
<p>위 코드에 대한 통합 테스트(integration test)를 진행하려고 하니, 테스트가 불가했습니다. <code>memberId int64, policyId string, rule repository.Rule</code>과 같은 파라미터는 mocking 할 수 있지만 아래 코드는 mocking이 불가능 했기 때문입니다.</p>
<pre><code class="language-go">// grpcClient 생성
conf := config.NewConfig().GetConfig()
grpcClient := client.NewGRPCClient(
    client.WithAddress(conf.CouponPlatformHost, &quot;443&quot;),
    client.WithInsecure(false),
    client.WithToken(conf.CouponPlatformGRPCToken),
)</code></pre>
<p><code>checkMaxCountIssuable()</code> 매서드 내부에서 grpcClient를 생성하다보니, 강한 결합 때문에 실제 통신을 하지 않는다면 통합테스트는 불가했습니다. 하지만 테스트의 경우, 어떤 외부 요소의 영향을 받아서는 안되는 독립적으로 실행해야하기 때문에 원본 코드를 수정해야 했었습니다.</p>
<h4 id="외부에서-의존성-주입하기-">외부에서 의존성 주입하기 !</h4>
<p>어떻게 수정할까 고민하다가 생성자에서 grpcClient를 주입해주었습니다. 이 때의 타입은 client 패키지에 정의되어 있는 <code>IGPRCClient</code> 인터페이스 입니다.</p>
<pre><code class="language-go">// coupon policy service 생성자
func NewCouponPolicyService(
    repository repository.ICouponPolicyRepository, 
    client client.IGPRCClient
) ICouponPolicyService {
    return CouponPolicyService{
        Repo: repository, 
        Client: client
      }
}</code></pre>
<p>그리고 원본 코드를 수정하니 아래와 같이 변경되었습니다.</p>
<pre><code class="language-go">// checkMaxUsedCountIssuable 정책에 정의된 기간 내에 사용한 쿠폰의 최대 개수를 초과하지 않았는지 확인
func (svc CouponPolicyService) checkMaxUsedCountIssuable(memberId int64, policyId string, rule repository.Rule) (bool, error) {
    // 정책 옵션 조회
    option, err := svc.getMaxUsedCountPolicyOption(rule)
    if err != nil {
        return false, err
    }

    // grpcClient 호출
    client := svc.Client.GetGRPCClient()

    location, _ := util.KSTLocation()
    interval := util.GetInterval(time.Now(), option.day, location)

    // 사용된 쿠폰 개수 조회
    res, requestErr := client.RequestListUsedCoupons(memberId, policyId, interval)
    if requestErr != nil {
        return false, requestErr
    }

    issuable := option.count &gt;= int32(len(res.Coupons))

    return issuable, nil
}</code></pre>
<p>이제 </p>
<blockquote>
<ol>
<li>테스트 코드에서 grpcClient를 mocking하여 RequestListUsedCoupons() 의 값을 변경 하면서 테스트 할 수 있습니다.</li>
<li>또한 객체의 메서드를 직접 호출하는 것이 아니라 인터페이스에 정의된 메서드를 통해서만 통신하다보니 상대적으로 느슨한 결합을 할 수 있었고, </li>
<li>외부에 의존하지 않고 기능으로만 테스트가 가능해졌습니다.</li>
</ol>
</blockquote>
<h2 id="왜-테스트-코드를-작성해야-할까요-">왜 테스트 코드를 작성해야 할까요 ?</h2>
<ul>
<li>좋은 코드는 테스트하기 쉽다</li>
<li>외부의 영향을 받거나 내부적으로 의존성을 가진 코드는 변화에 유연하지 못하다</li>
<li>재사용하기 어렵다</li>
<li>“만약 내가 작성한 코드가 테스트하기 어렵다면 냄새나는 코드일 가능성이 높다”</li>
</ul>
<p>테스트코드를 작성해야하는 이유를 다룬 여러 아티클에서 얻은 인사이트입니다. 실제 테스트 코드를 작성하다보니 내 코드가 얼마나 테스트 하기 어렵고, 내부의 의존성이 존재하고, 재사용하기 어려운지 한 마디로 냄새나는 코드였는지 한 번 더 알게 되었습니다.</p>
<h4 id="안정성이-테스트-코드의-모든-이유는-아니다-">안정성이 테스트 코드의 모든 이유는 아니다 !</h4>
<p>이전에는 테스트코드의 목적이자 유일한 이유는 안정성이라고만 생각을 했었습니다. 하지만 실제 테스트 코드를 짜다보니 개발 주기, 문서화, 확장성 있는 유연한 코드 등 여러 이점이 있다는 사실을 이번 기회에 알게 되었습니다.</p>
<h4 id="tdd-tdd-test-driven-development">TDD,,, TDD,, Test Driven Development</h4>
<p>이번에는 <code>개발 - 테스트코드 - 리팩토링</code>의 사이클을 경험했었는데요, 만약 테스트코드를 먼저 짜보았더라면 개발 사이클을 <code>테스트코드-개발</code>로 줄이면서 보다 빠르고 안전하며 확장성 있게 개발 할 수 있었지 않을까 싶은 생각이 들었습니다. 그리고 이게 사람들이 말하는 TDD가 아닐까 싶었습니다</p>
<h2 id="억지인데요-내테내코--내돈내산-내테내코">억지인데요.. 내테내코 ! (내돈내산? 내테내코!)</h2>
<p>내돈내산이라는 말이 있습니다. <code>내 돈으로 내가 산 것</code>의 약자로 나의 실제 후기를 가장 잘 나타내는 말입니다. 
이 글은 <code>내가 테스트 짜보면서 내 코드를 개선 시킨</code> 내용입니다. 이전까지는 저 역시도 테스트 코드를 왜 짜야하는지를 잘 몰랐습니다. 하지만 한 두개씩 테스트 코드를 작성하다보니 많은 이점이 있는 것을 알게되었습니다.</p>
<p>내테내코.. 라는 억지와 함께 글을 마무리 해보겠습니다.. 🙇‍♂️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[github action으로 CICD 모듈화하기 !]]></title>
            <link>https://velog.io/@ryu_log/github-action-%EB%AA%A8%EB%93%88%ED%99%94%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ryu_log/github-action-%EB%AA%A8%EB%93%88%ED%99%94%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 31 Mar 2024 13:05:03 GMT</pubDate>
            <description><![CDATA[<h2 id="cicd를-옮기고-나니-알게된-문제점-🤔">CICD를 옮기고 나니 알게된 문제점 🤔</h2>
<p>많은 팀에서 cicd 도구로 github action을 사용합니다. 저희 팀에서는 최근까지 젠킨스를 통한 CICD를 운영하다가 github action으로 CICD 작업을 옮기고 있는데요. 대부분의 프로젝트의 CICD를 옮기고 나서 운영하다보니 아래와 같은 문제점을 발견했습니다.</p>
<ol>
<li>대부분의 cicd flow가 동일하고, 개발/상용 환경의 일부 입력값만 변경하여 운영되고 있었습니다. </li>
<li>cicd flow를 하나라도 변경하게 되면 개발/상용 각각 수정 사항을 반영 해야하고 이는 곧 휴먼에러의 가능성과 냄새나는 코드라고 판단했습니다.</li>
<li>현재 여러 프로젝트에서 각자의 CICD를 운영하고 있었기 때문에 동일한 build / deploy를 진행한다고 하더라도 서로 다른 진행 과정을 거치게 되고, 다수의 프로젝트를 운영하는 팀의 입장에서는 서비스 운영 난이도가 높아진다고 판단했습니다.</li>
</ol>
<p>우선 이전에 운영되던 github action script는 아래와 같습니다. jobs는 크게 <code>build</code> 와 <code>deploy</code>로 구분되며, build에서는 <code>checkout</code> - <code>install dependency</code> -  <code>run test code</code> - <code>build and push</code> 를 진행하고, deploy에서는 <code>ecs service update</code> - <code>notification</code>을 진행합니다.</p>
<pre><code class="language-json">Build and Deploy (DEV)

on:
  push:
    branches: [develop]

env:
  AWS_REGION: ap-northeast-2
  ECR_REPOSITORY: project
  ECS_CLUSTER: project-dev
  ECS_SERVICE: project-dev

jobs:
  build:
    runs-on: ubuntu-latest
    environment: DEV
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Go 1.21.4
        uses: actions/setup-go@v3
        with:
          go-version: &quot;1.21.4&quot;

      - name: Install dependencies
        run: go mod download

      - name: Display Go version
        run: go version

      - name: Run Test Code
        run: go test -v ./...

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_DEV }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_DEV }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Create env file
        run: |
          echo &quot;${{ secrets.ENV_FILE_DEV }}&quot; &gt; .env;
          chmod 644 .env

      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: develop
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo &quot;$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG&quot;

  deploy:
    runs-on: ubuntu-latest
    environment: DEV
    needs: [build]
    if: always()
    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_DEV }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_DEV }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Deploy to ECS
        run: |
          aws ecs update-service --cluster=${{ env.ECS_CLUSTER }} --service=${{ env.ECS_SERVICE }} --task-definition=${{ env.ECS_SERVICE }} --force-new-deployment;

      - name: Set Status
        id: set_status
        run: |
          if [ &quot;${{ needs.build.result }}&quot; == &#39;success&#39; ] &amp;&amp; [ &quot;${{ job.status }}&quot; == &#39;success&#39; ]; then
            echo &quot;color=good&quot; &gt;&gt; $GITHUB_OUTPUT
            echo &quot;status=success&quot; &gt;&gt; $GITHUB_OUTPUT
            echo &quot;emoji=:gopher-dance:&quot; &gt;&gt; $GITHUB_OUTPUT
          else
            echo &quot;color=danger&quot; &gt;&gt; $GITHUB_OUTPUT
            echo &quot;status=failure&quot; &gt;&gt; $GITHUB_OUTPUT
            echo &quot;emoji=:gopherlift:&quot; &gt;&gt; $GITHUB_OUTPUT
          fi

      - name: Slack Notification
        uses: rtCamp/action-slack-notify@v2
        env:
          SLACK_COLOR: &quot;${{ steps.set_status.outputs.color }}&quot;
          SLACK_ICON: https://s3.ap-northeast-2.amazonaws.com/slack/gopher.jpeg
          SLACK_TITLE: Message
          SLACK_MESSAGE: &quot;deploy ${{ steps.set_status.outputs.status }} `develop` ${{ steps.set_status.outputs.emoji }}&quot;
          SLACK_USERNAME: &quot;CICD Bot&quot;
          SLACK_WEBHOOK:  ${{ secrets.SLACK_WEBHOOK_URL_DEV }}</code></pre>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/7f8cdd93-4c68-44e8-947b-2dda54f516d5/image.png" alt="">
github에서 지원하는 action UI상에서는 매우 간단해보이지만, 실제 스크립트를 보게되면 언뜻 보더라도 전체적인 CICD의 전체적인 맥락을 파악하기 어렵습니다. 만약 기존 플로우에서 push 하는 이미지의 tag 정책이 변경되거나, git tagging 같은 새로운 job이 들어간다면 개발/상용환경 스크립트를 동시에 수정해야하는 위험과 번거로움이 수반됩니다.</p>
<h2 id="cicd-모듈화-하기--⚒️">CICD 모듈화 하기 ! ⚒️</h2>
<p>이러한 배경으로 각 작업을 모듈화하는 CICD 모듈화 작업을 진행하게되었습니다. 
github action의 job은 <code>workflow_call</code> trigger로 동작 시킬 수 있습니다. uses 통해 action을 custom action을 호출하게 되는데 이 때 <code>inputs</code>과 <code>secrets</code>를 통해 가변 인자를 넘길 수 있습니다. 각 작업을 모듈화 한 뒤, <code>workflow_call</code>를 통해 호출하기로 했습니다.</p>
<p>먼저 큰 단계를 구성했습니다. </p>
<ol>
<li>test</li>
<li>build-and-push</li>
<li>deploy</li>
<li>notification</li>
<li>git tagging / generate release</li>
</ol>
<h3 id="test">test</h3>
<p>test는 아래와 같습니다. checkout 후, 배포할 코드의 테스트 도커 컨테이너를 생성하고 테스트 코드를 실행시킵니다.</p>
<pre><code class="language-json">on:
  workflow_call:
    inputs:
      service:
        required: true
        type: string

    secrets:
      repository_access_token:
        required: true
        description: github private repository access token

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Build
        run: |
          docker build --no-cache --build-arg REPOSITORY_ACCESS_TOKEN=${{ secrets.repository_access_token }} --target test-stage --tag ${{ inputs.service }}:test .

      - name: Run Test Code
        run: docker run --rm ${{ inputs.service }}:test

      - name: Clean up
        run: docker rmi -f ${{ inputs.service }}:test</code></pre>
<h3 id="build-and-push">build-and-push</h3>
<p>테스트 코드를 통과하게 되면 이미지를 빌드하고 빌드된 이미지를 ecr로 push하게 됩니다. 이 때 push되는 image의 tag는 latest입니다(task definition에서 latest 이미지를 바라보고 있습니다)</p>
<pre><code class="language-json">on:
  workflow_call:
    inputs:
      service:
        required: true
        type: string
        description: 서비스 이름

      image_tag:
        required: true
        type: string
        description: 이미지 태그

    secrets:
      env_file:
        required: true
        description: env file

      repository_access_token:
        required: true
        description: github private repository access token

      role_arn:
        required: true
        description: AWS ecr role arn

permissions:
  id-token: write
  contents: read

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Create env file
        run: |
          echo &quot;${{ secrets.env_file }}&quot; &gt; .env;
          chmod 644 .env

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.role_arn }}
          aws-region: ap-northeast-2

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build &amp; Push image to Amazon ECR
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/arm64
          push: true
          tags: ${{ steps.login-ecr.outputs.registry }}/${{ inputs.service }}:${{ inputs.image_tag }}
          build-args:
            REPOSITORY_ACCESS_TOKEN=${{ secrets.repository_access_token }}</code></pre>
<h3 id="deploy">deploy</h3>
<p>정상적으로 image가 push된 후 해당 이미지를 통해 ecs 서비스 업데이트를 진행합니다. 기존에 사용 중이던 script를 실행시켜 진행했습니다.</p>
<pre><code class="language-json">on:
  workflow_call:
    inputs:
      env:
        required: true
        type: string

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: deploy
        run: |
          make deploy env=${{ inputs.env }}</code></pre>
<h3 id="notification">notification</h3>
<p>배포 결과를 슬랙 채널에 공유합니다.</p>
<pre><code class="language-json">on:
  workflow_call:
    inputs:
      result:
        required: true
        type: string
        description: 빌드 및 배포 결과. 성공, 실패(스킵-빌드 실패)

      service:
        required: true
        type: string
        description: 서비스 이름. slack 메시지에 서비스 이름 표시

      branch:
        required: true
        type: string
        description: 브랜치 이름. slack 메시지에 배포 브랜치 표시

    secrets:
      slack_webhook_url:
        required: true
        description: slack webhook url

jobs:
  nofitication:
    runs-on: ubuntu-latest
    steps:
      - name: Set Status
        id: set_status
        run: |
          if [ &quot;${{ inputs.result }}&quot; == &#39;success&#39; ]; then
            echo &quot;color=good&quot; &gt;&gt; $GITHUB_OUTPUT
            echo &quot;status=success&quot; &gt;&gt; $GITHUB_OUTPUT
            echo &quot;emoji=:gopher-dance:&quot; &gt;&gt; $GITHUB_OUTPUT
          else
            echo &quot;color=danger&quot; &gt;&gt; $GITHUB_OUTPUT
            echo &quot;status=failure&quot; &gt;&gt; $GITHUB_OUTPUT
            echo &quot;emoji=:gopherlift:&quot; &gt;&gt; $GITHUB_OUTPUT
          fi

      - name: Slack Notification
        uses: rtCamp/action-slack-notify@v2
        env:
          SLACK_WEBHOOK: ${{ secrets.slack_webhook_url }}
          SLACK_COLOR: ${{ steps.set_status.outputs.color }}
          SLACK_TITLE: Message
          SLACK_MESSAGE: ${{ inputs.service }} deploy ${{ steps.set_status.outputs.status }} `${{ inputs.branch }}` ${{ steps.set_status.outputs.emoji }}
          MSG_MINIMAL: ref, event</code></pre>
<h3 id="git-tagging--generate-release">git tagging / generate release</h3>
<p>배포까지 성공하게 되면 배포된 코드의 tagging과 변경 사항에 대한 release를 자동으로 생성하게 됩니다. 그리고 이 작업의 결과로 새로운 tag를 output으로 내보내게 됩니다.</p>
<pre><code class="language-json">on:
  workflow_call:
    secrets:
      repository_access_token:
        required: true
        description: github private repository access token

    outputs:
      new_tag:
        value: ${{ jobs.auto-tagging-and-release.outputs.new_tag }}

jobs:
  auto-tagging-and-release:
    runs-on: ubuntu-latest
    outputs:
      new_tag: ${{ steps.set_output.outputs.new_tag }}
    steps:
      - name: Bump version and push tag
        id: tag_version
        uses: mathieudutour/github-tag-action@v6.1
        with:
          github_token: ${{ secrets.repository_access_token }}
          custom_release_rules: hotfix:patch:preminor

      - name: Create a GitHub release
        uses: ncipollo/release-action@v1
        with:
          tag: ${{ steps.tag_version.outputs.new_tag }}
          name: Release ${{ steps.tag_version.outputs.new_tag }}
          body: ${{ steps.tag_version.outputs.changelog }}

      - name: Set output
        id: set_output
        run: echo &quot;new_tag=${{ steps.tag_version.outputs.new_tag }}&quot; &gt;&gt; $GITHUB_OUTPUT</code></pre>
<h2 id="모듈화한-작업들을-하나의-스크립트에서-호출하기-">모듈화한 작업들을 하나의 스크립트에서 호출하기 !</h2>
<p>이렇게 나눈 각 기능들을 아래와 같이 필요한 동작만 조합해서 사용할 수 있습니다. </p>
<pre><code class="language-json">name: CI/CD-PROD

on:
  push:
    branches: [ main ]

jobs:
  test:
    uses: ./.github/workflows/test.yml
    with:
      service: mock-project
    secrets:
      repository_access_token: ${{ secrets.REPOSITORY_ACCESS_TOKEN }}

  build-and-push:
    needs: [ test ]
    uses: ./.github/workflows/build-and-push.yml
    with:
      service: mock-project
      image_tag: latest
    secrets:
      env_file: ${{ secrets.ENV_FILE }}
      repository_access_token: ${{ secrets.REPOSITORY_ACCESS_TOKEN }}
      role_arn: ${{ secrets.AWS_ROLE_ARN }}

  deploy:
    needs: [ build-and-push ]
    uses: ./.github/workflows/deploy.yml
    with:
      env: prod

  tag-and-release:
    needs: [ deploy ]
    if: ${{ needs.deploy.result == &#39;success&#39; }}
    uses: ./.github/workflows/tag-and-release.yml
    secrets:
      repository_access_token: ${{ secrets.REPOSITORY_ACCESS_TOKEN }}

  notification:
    needs: [ deploy ]
    if: always()
    uses: ./.github/workflows/notification.yml
    with:
      result: ${{ needs.deploy.result }}
      service: mock-project
      branch: main
    secrets:
      slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}

  build-new-tag-image-and-push:
    needs: [ tag-and-release ]
    uses: ./.github/workflows/build-and-push.yml
    with:
      service: mock-project
      image_tag: ${{ needs.tag-and-release.outputs.new_tag }}
    secrets:
      env_file: ${{ secrets.ENV_FILE }}
      repository_access_token: ${{ secrets.REPOSITORY_ACCESS_TOKEN }}
      role_arn: ${{ secrets.AWS_ROLE_ARN }}</code></pre>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/87d9d5ff-0f48-40f2-981f-087342cf028e/image.png" alt=""></p>
<h2 id="모듈화를-통해-얻게-된-점-🚀">모듈화를 통해 얻게 된 점 🚀</h2>
<p>모듈화 하기 전 스크립트와 비교해보면 어떤가요 ? action UI상 단계는 늘어났지만, 코드 상으로 각 세부 단계나 전체적인 플로우를 파악하기 용이해진 것 같네요. 또한 만약 개발환경 CICD를 구성한다고 했을 때는 개발환경에서 필요없는 <code>tag-and-release</code> 나 <code>build-new-tag-image-and-push</code> 는 제외하고 입력 파라미터만 수정하고, 필요한 구성으로만 각 동작을 조합하여 새로운 CICD를 쉽게 구성 할 수 있습니다.</p>
<p>또한 새로운 스크립트의 마지막을 보면 <code>build-new-tag-image-and-push</code> 동작이 추가되었는데요. <code>tag-and-release</code> 의 결과로 산출된 새로운 태그를 붙여 ecr 이미지로 push하는 동작입니다. 이는 이미지 히스토리를 남기고, 롤백 상황 발생시 최대한 빠르게 전 코드를 반영하게 하기 위함입니다. (<code>build-and-push</code>는 latest tag로 ecr에 푸쉬합니다. task definition에서는 latest를 바라보고 있습니다)</p>
<pre><code class="language-json">  build-new-tag-image-and-push:
    needs: [ tag-and-release ]
    uses: ./.github/workflows/build-and-push.yml
    with:
      service: mock-project
      image_tag: ${{ needs.tag-and-release.outputs.new_tag }}
    secrets:
      env_file: ${{ secrets.ENV_FILE }}
      repository_access_token: ${{ secrets.REPOSITORY_ACCESS_TOKEN }}
      role_arn: ${{ secrets.AWS_ROLE_ARN }}</code></pre>
<p>만약 위의 동작을 모듈화하지 않았다면, 거의 동일한 코드가 중복되어 사용되어 유지보수의 난이도가 높아졌을 것입니다. 하지만 모듈화를 통해 image tag만 변경하여 해당 모듈을 호출하게 되어 보다 쉽게 새로운 동작을 추가했습니다. 무엇보다 CICD 스크립트 내에서 각 동작의 목적이나 의도도 확실하게 나타나고, 중복 코드도 줄일 수 있다는 것이 가장 큰 얻음이었습니다 !</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[결제서버 분리하기 - 3: msa 프로젝트 효율적으로 관리하기]]></title>
            <link>https://velog.io/@ryu_log/%EA%B2%B0%EC%A0%9C%EC%84%9C%EB%B2%84-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-3-msa-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9C%BC%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ryu_log/%EA%B2%B0%EC%A0%9C%EC%84%9C%EB%B2%84-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-3-msa-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9C%BC%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 02 Mar 2024 16:06:01 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ryu_log/post/72df625f-a6d6-4557-863b-3dd9470d0e4f/image.png" alt=""></p>
<p>지난 편들에서는 <a href="https://velog.io/@ryu_log/%EA%B2%B0%EC%A0%9C%EC%84%9C%EB%B2%84-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-1-Monolithic-to-MSA">결제서버를 분리하기로 한 배경</a>, <a href="https://velog.io/@ryu_log/%EA%B2%B0%EC%A0%9C%EC%84%9C%EB%B2%84-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-2-Golang-gRPC">그 과정에서의 고민</a>들을 적어보았는데요. 마지막으로 서비스가 하나 둘 분리되어가는 과정에서 어떻게하면 효율적으로 여러 프로젝트를 관리 할 수 있을지에 대한 고민을 적어보려고 합니다.</p>
<h2 id="msa-환경에서-공통-코드-안전하게-관리하기">msa 환경에서 공통 코드 안전하게 관리하기</h2>
<p>msa로 각 서비스가 분리되어가는 과정에서 가장 큰 고민은 “분리되는 서비스에 대한 유지보수” 일 것 입니다. 막상 분리를 시켰지만 msa의 장점을 경험하기도 전에 자칫 관리포인트만 늘어난 것으로 생각할 수 있습니다. 더군다나 스타트업의 경우에는 제한된 인원수로 여러 프로젝트들을 관리해야 하기 때문에 유지보수 비용은 msa도입을 고민하게 하는 포인트 중 하나입니다. </p>
<p>이미 팀에서는 두 개의 프로젝트에서 golang을 사용하고 있습니다. 이 때까지는 각 서비스에서 모든 로직이 포함되어 있었습니다. 하지만 팀 내에서 사용하는 모니터링 모듈이나 에러 모듈은 동일하다보니 거의 비슷한 코드가 중복으로 관리되고 있었습니다. 이번에 분리한 paygo 프로젝트도 마찬가지였습니다. 동일한 목적을 가진 코드가 분산되어 관리되다보니, 이후 코어한 정책이 변경된다면 모든 프로젝트에 변경 사항을 변경해야하는 번거로움과 의사결정마다 따르는 휴먼에러의 가능성을 배제할 수 없었습니다.</p>
<p>이런 유지보수의 비효율을 개선하기 위해, 여러 golang 프로젝트에서 사용할 수 있는 공통 모듈 프로젝트를 생성하기로 했습니다. 우선 모든 프로젝트에서 필수적으로 포함되어야하는 요소들을 정리해보니 아래와 같았습니다. </p>
<ul>
<li>DB: RDB. NoSQL</li>
<li>Monitoring: Datadog</li>
<li>Error(Sentry) &amp; Logging</li>
</ul>
<p>하나의 예시로, 서로 다른 프로젝트에서 nosql 데이터베이스인 mongodb를 생성하는 로직을 살펴보면 아래와 같습니다.</p>
<pre><code class="language-go">// A project 

type MongoClient struct {
    client *mongo.Client
}

func NewMongoClientOption(config *config.MongoConfig) *options.ClientOptions {
    clientOptions := options.Client()
    clientOptions.ApplyURI(config.Url).SetTimeout(5 * time.Second)

    return clientOptions
}

func NewMongoClient(opts ...*options.ClientOptions) (*MongoClient, error) {
    client, err := mongo.Connect(context.Background(), opts...)
    if err != nil {
        return nil, errors.WithStack(err)
    }

    err = client.Ping(context.Background(), nil)
    if err != nil {
        return nil, errors.WithStack(err)
    }

    return &amp;MongoClient{client: client}, nil
}</code></pre>
<pre><code class="language-go">// B project 

type mongoConfig struct {
    url string
    db  string
}

func MongoNewClient() *mongo.Client {
    uri := ENV.MongoConfig.getMongo()
    clientOptions := options.Client().ApplyURI(uri)

    client, err := mongo.Connect(context.Background(), clientOptions)
    if err != nil {
        log.Fatal(err)
    }

    // 클라이언트와의 연결 확인
    err = client.Ping(context.Background(), nil)
    if err != nil {
        log.Fatal(&quot;Could not connect to MongoDB:&quot;, err)
    }

    return client
}</code></pre>
<p>간단하지만 MongoDB 생성자 로직입니다. 로직상 차이는 있지만 동일하게, MongoClient를 생성하고, Ping을 통한 연결을 확인하고 있습니다. 만약 팀 내 몽고디비 생성 관련된 정책이 변경된다면 이 두 개의 프로젝트에서 변경 사항을 적용하여 배포해야합니다. 그 상황에서 새로운 정책을 반영하여 각각 다시 배포해고 만약 변경 사항이 프로젝트 전체와 디펜던시가 있을 경우 유지보수의 난이도가 급격히 올라가게 됩니다.</p>
<p>위와 같은 유지보수의 비효율을 개선하고자 공통 프로젝트에서 mongodb를 모듈화하여 아래와 같이 공통으로 사용하기로 했습니다. 또한 이전처럼 구조체가 아닌 인터페이스를 반환하게 하면서 다른 클라이언트 코드들과의 의존성을 낮출 수 있었고, 테스트를 위한 mocking의 이점을 챙길 수 있었습니다.</p>
<pre><code class="language-go">type IClient interface {
    ...
}

var _ IClient = (*Client)(nil)

type Client struct {
    client *mongo.Client
}

func NewClient(opts ...*options.ClientOptions) (IClient, error) {
    client, err := mongo.Connect(context.Background(), opts...)
    if err != nil {
        return nil, errors.WithStack(err)
    }

    err = client.Ping(context.Background(), nil)
    if err != nil {
        return nil, errors.WithStack(err)
    }

    return &amp;Client{client: client}, nil
}

func NewClientOption() *options.ClientOptions {
    return options.Client()
}</code></pre>
<p>이제 모든 팀 내 golang 프로젝트에서는 <a href="http://go.mongodb.org/mongo-driver">go.mongodb.org/mongo-driver</a> 모듈을 설치하는 것이 아닌 go mod에 <code>github.com/***/go-common/mongo</code>를 정의하고 설치해서 사용하게 됩니다. 실제 서비스를 컨테이너 기반으로 서빙할 때는 빌드 시점에  <code>go get</code> 명령어를 통해 해당 모듈 가져올 수 있습니다. </p>
<pre><code class="language-go">require (
                            .
                            .
    github.com/***/go-common v0.0.0-20240215041307-cb68b4e61e4b
                            .
                            .
)</code></pre>
<p>아래와 같이 설치하면 default 브랜치를 참조하게되고, @develop 와 같이 특정 브랜치를 지정하여 가져 올 수 있습니다.</p>
<pre><code class="language-bash">go get github.com/***/go-common</code></pre>
<h2 id="common-module-version-control">common module version control</h2>
<p>위와 같이 운영을 하게되면 변경사항에 대한 이점을 챙길 수 있습니다. 하지만 변경사항을 각 프로젝트에서 매번 바로바로 적용하기는 쉽지 않고, 만약 빌드 시점에 다른 변경 사항을 반영하려고 했지만 가장 최신의 공통 모듈을 가져온다고 한다면 또다른 사이드 이펙트가 발생할 수 있습니다. 이러한 문제를 방지하기 위해 공통 모듈의 버저닝을 진행하기로 했습니다.</p>
<p>일반적으로 프로젝트의 version control은 git tag와 sementic versioning을 조합하여 사용합니다. 새로운 작업물이 release branch에 반영될 때마다 수동으로 git tag를 지정할 수 있지만 이러한 번거로움을 줄이기 위해 github action을 사용하여 version control을 자동화 했습니다.</p>
<p>먼저 모든 브랜치의 기준이 되는 브랜치(main)에 PR을 올리고 작업물이 merge가 되면, 자동으로 release 브랜치에 반영이 되는 액션 스크립트를 정의했습니다.</p>
<pre><code class="language-yaml">on:
  pull_request:
    types:
      - closed
    branches:
      - main

jobs:
  auto_merge:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    steps:
      - name: Sync ${{ github.ref }} to release
        uses: devmasx/merge-branch@master
        with:
          GITHUB_TOKEN: ${{ secrets.REPOSITORY_ACCESS_TOKEN }}
          type: now
          from_branch: ${{ github.ref }}
          target_branch: release
</code></pre>
<p>이 액션이 동작하게되면, 다음 자동으로 tagging과 release를 작성해주는 스크립트를 생성했습니다. 이 때 version control의 기준은 merge 커밋 메시지의 prefix입니다. </p>
<ul>
<li>ex) <code>perf: breaking change</code> -&gt; <code>Major Release</code></li>
<li>ex) <code>feat: add new feature</code> -&gt; <code>Minor Release</code></li>
<li>ex) <code>fix: fix Minor</code> -&gt; <code>Patch Release</code></li>
</ul>
<pre><code class="language-yaml">on:
  push:
    branches:
      [release]

jobs:
  auto_merge:
    runs-on: ubuntu-latest
    steps:
      - name: Bump version and push tag
        id: tag_version
        uses: mathieudutour/github-tag-action@v6.1
        with:
          github_token: ${{ secrets.REPOSITORY_ACCESS_TOKEN }}
          custom_release_rules: hotfix:patch:preminor

      - name: Create a GitHub release
        uses: ncipollo/release-action@v1
        with:
          tag: ${{ steps.tag_version.outputs.new_tag }}
          name: Release ${{ steps.tag_version.outputs.new_tag }}
          body: ${{ steps.tag_version.outputs.changelog }}</code></pre>
<p>이렇게 버전을 관리하고, 실제 서비스에서 go.mod를 보게 되면 맨 처음 모듈을 설치했을 때와 같이 특정 커밋 포인트가 아닌 설치된 버전이 명시됩니다.</p>
<pre><code class="language-go">require (
                            .
                            .
    github.com/***/go-common v1.0.0
                            .
                            .
)</code></pre>
<p>공통 모듈로 많은 부분을 이관하고나니 패키지 구조가 아래와 같이 단순해졌습니다.</p>
<pre><code class="language-go">// 공통 모듈 분리 전 패키지 구조
.
├── config
├── injector
├── nginx
├── scripts
├── protocol
└── server
        ├── db
        ├── monitor
    ├── error
    ├── handler
    └── logger

// 공통 모듈 분리 후 패키지 구조
.
├── config
├── injector
├── nginx
├── scripts
└── server
    ├── handler
    └── logger
</code></pre>
<p>프로젝트에서 데이터베이스, 에러, 모니터링 등을 걷어내니 조금 더 비즈니스 로직을 파악하는데 용이해졌고, 프로젝트 자체에서 신경써야할 부분들이 많이 줄어들었습니다.</p>
<h2 id="multi-stage-build">Multi Stage Build</h2>
<p>도커로 프로젝트를 빌드했더니 약 800mb 이미지가 생성되었습니다. 파일이 크다보니 저장소(물론 ECR과 github action을 사용해서 용량 걱정이 없긴 했지만..)의 용량의 비용이 신경쓰였고, 만약 github action을 self-hosting한다면 그 저장 용량도 항상 신경써줘야합니다. 만약 800mb 정도의 이미지가 생성된다면 10번만 빌드하더라도 약 8gb의 용량을 차지하게 됩니다. 또한 이렇게 큰 이미지의 경우 이미지를 다운 받거나 빌드 &amp; 배포 할 때도 시간이 오래 걸리게 됩니다.</p>
<p>이렇게 큰 이미지가 생성되는 이유는 컨테이너 이미지에 생성에 필요한 여러 이미지와 파일이 함께 포함되기 때문입니다. 하지만 이것들은 실제 컨테이너를 실행시킬 때 필요하지 않은 것들입니다. 그러던 중 알게된 것이 multi stage build입니다. 요약하면 build stage를 두 단계 이상으로 나눠 실제 컨테이너 실행에 필요한 파일 및 디렉토리만 추출하여 사용하는 것을 의미합니다.</p>
<p>맨 처음 작성한 도커 파일은 아래와 같습니다. 일반적인 도커파일입니다.</p>
<pre><code class="language-docker">FROM golang:1.21.4-alpine

ENV GOARCH=arm64\
    GOOS=linux

RUN apk update

WORKDIR /root

COPY . .

RUN go mod download &amp;&amp; go mod tidy

EXPOSE 8000

RUN go build -o main .

ENTRYPOINT [&quot;./main&quot;]</code></pre>
<p>multi stage build를 적용한 도커 파일입니다. 아래에 보면 FROM 구문을 기준으로 stage를 구분합니다. 또한 사용한 scratch는 여러 이미지들 가운데에서도 가장 경량화가 잘된 이미지입니다. super minimal image에 최적화되어 있습니다. </p>
<pre><code class="language-docker">FROM golang:1.21.5-alpine as builder

ENV GOARCH=arm64\
    GOOS=linux\
    CGO_ENABLED=0\
    TZ=UTC

RUN apk update &amp;&amp; apk add git ca-certificates tzdata

WORKDIR /root

COPY . .

RUN go mod download &amp;&amp; go mod tidy

RUN go build -a -ldflags &#39;-s&#39; -o main .

FROM scratch

COPY --from=builder /root/main .
COPY --from=builder /root/.env .
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

EXPOSE 8000

ENTRYPOINT [&quot;./main&quot;]</code></pre>
<p>이렇게 두 컨테이너 이미지 크기를 비교해보면, 아래와 같이 764mb → 18.52mb로 약 42배 가량 줄어든 것을 확인할 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/6a951763-1c90-45f9-9727-54c58927f647/image.png" alt=""></p>
<h2 id="나는-무엇을-얻었는가-">나는 무엇을 얻었는가 !</h2>
<p>현재 결제 서버 분리 프로젝트는 상용환경에서 우리 서비스의 결제 승인의 모든 처리를 진행하고 있습니다. 다행히 리소스적으로도 안정적으로 동작중이고, 현재까지 장애나 이상상황 없이 결제를 처리하고 있습니다. </p>
<p>이 프로젝트를 통해서 무엇을 얻었는가 ? 는 질문을 참 많이 받았습니다. 사실 서비스적으로도 이미 결제 도메인은 안정적으로 동작하고 있었고 자사 페이 서비스가 없다면 결제 서버 분리 자체가 가지는 이점도 크지는 않습니다. 그럼에도 모놀리식으로 운영되던 서비스를 작게나마 분리하면서 각 도메인간의 역할과 책임 분리의 이점을 챙겼다고 생각했고, 막연하게만 생각했던 msa에 한 발짝 다가갔다고 생각합니다. 분리하면서 팀 내에 gRPC라는 기술스택을 도입했다는 것과 작지만 msa를 통해 전체 서비스의 스케일업이 아닌 각 서비스별 스케일업을 할 수 있다는 msa의 장점을 느껴보기도 했습니다. 만약 기존의 구조를 유지했다면 쉽게 경험하지 못할 부분이었다고 생각합니다.</p>
<p>무엇보다 개인적으로 얻은 부분이 참 많습니다. 프로젝트의 오너로서 기획부터 설계, 구현, 모니터링, 유지보수의 과정을 거치면서 “어떻게 패키지 구조를 가져갈지”, “공통 코드는 어떻게 처리할지”, “유지보수의 효율을 높이기 위해서는” 과 같은 고민을 하고 스스로 답을 찾았다는 점에서 배운 것이 참 많습니다. 그렇게 차근 차근 채워나가면서 처음에는 아무것도 없었지만 지금은 어느덧 상용환경에서 우리 서비스의 모든 결제의 승인 처리를 담당하고 있으니 뿌듯하기도 합니다.</p>
<p>아직 모든 결제 도메인 로직이 넘어오지 않았습니다. 작게 작게 기능을 추가하면서 v0.4.1까지 왔는데, 모든 기능을 추가하여 빨리 v1.0.0 을 출시하고 싶네요 ! 긴 글 읽어주셔서 감사합니다 !</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[결제서버 분리하기 - 2: Golang + gRPC]]></title>
            <link>https://velog.io/@ryu_log/%EA%B2%B0%EC%A0%9C%EC%84%9C%EB%B2%84-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-2-Golang-gRPC</link>
            <guid>https://velog.io/@ryu_log/%EA%B2%B0%EC%A0%9C%EC%84%9C%EB%B2%84-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-2-Golang-gRPC</guid>
            <pubDate>Sun, 18 Feb 2024 11:28:53 GMT</pubDate>
            <description><![CDATA[<p>지난 편(<strong><a href="https://velog.io/@ryu_log/%EA%B2%B0%EC%A0%9C%EC%84%9C%EB%B2%84-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-1-Monolithic-to-MSA">결제서버 분리하기 - 1: Monolithic to MSA</a></strong>)에는 결제서버를 분리하기로한 배경을 적어보았습니다. 이번에는 실제로 어떻게 결제서버를 분리했는지 실제 개발 과정에서 겪었던 과정과 고민을 적어보려고 합니다. 일반적인 REST API 서버보다는 상대적으로 gRPC 서버 관련된 레퍼런스가 적어, 이미 상용환경에서 gRPC 서버를 운영하고 있는 <a href="https://blog.banksalad.com/">뱅크샐러드 테크블로그</a>를 많이 참조하여 초기 방향을 잡아보았습니다. </p>
<h2 id="grpc과-idl">gRPC과 IDL</h2>
<p>gRPC는 서비스 정의를 protobuf 파일로 생성한 후 이를 스텁(stub)으로 컴파일하여 사용합니다. 애플리케이션 내부에 stub 파일을 정의하여 사용 할수도 있지만 stub파일이 한 애플리케이션에 종속되게 되면서 공통으로 사용 가능성이 있는 Interval과 같은 message가 애플리케이션마다 중복으로 발생합니다. 매번 정의해야하는 비효율도 있지만 모든 서비스에서 사용하는 타입을 한 번 수정하게 된다면 모든 애플리케이션에서 이를 재정의해야하기 때문에 그 비용이 크게 증가합니다. 또한 새로운 프로그래밍 언어로 MSA 프로젝트를 구성하게 된다면 동일한 protobuf 파일을 옮기는 과정에서 발생할 수 있는 휴먼에러의 가능성을 늘 염두에 두어야합니다.</p>
<p>이러한 문제들을 효율적으로 핸들링하고자 IDL(Interface Definition Langauge) 프로젝트를 생성했습니다. IDL프로젝트는 모든 gRPC 서비스에서 사용하는 protobuf파일과 이를 컴파일한 stub파일을 정의한 프로젝트입니다. stub은 각 프로젝트, 프로그래밍 언어별로 구분되어 관리됩니다. 이를통해 모든 gRPC 프로젝트는 IDL을 바라보면서 중복을 제거하고 관리의 효율성을 챙길 수 있었습니다. 이 idl 프로젝트를 어떻게 활용했는지는 다음편에 작성하도록 하겠습니다.</p>
<h2 id="pg사와-통신하기-pgclient-interface-정의하기">PG사와 통신하기: PGClient Interface 정의하기</h2>
<p>현재 사용하고 있는 PG사는 NicePay와 NaverPay입니다. 각 PG사의 디테일한 부분을 제외하면 PG사의 통신은 크게 아래 동작을 수행하고 있었습니다. </p>
<ol>
<li>Header를 정의한다.</li>
<li>RequestBody를 생성한다.</li>
<li>HTTP 요청을 보낸다.</li>
<li>HTTP Response Body를 파싱한다 .</li>
</ol>
<p>우선 이 4가지 동작을 추상화 하기로 결정했습니다. 현재 사용하고 있는 PG사 이외의 새로운 PG사를 추가하더라도 큰 틀에서는 우리가 정의한 4가지 동작을 수행할 것이고, 각 PG사의 디테일한 부분만 챙긴다면 안전하고 통일성 있는 로직을 추가할 수 있을 것이라 생각했습니다.</p>
<p>Golang의 interface는 추상화된 상호 작용으로 관계를 표현하는데 사용됩니다. 기본적으로 Golang에서 interface는 타입이며 변수로 선언할 수도 있습니다. 기본적으로 duck typing을 지원하면서 다형성을 구현할 수 있습니다. interface의 기본 선언은 아래와 같습니다.</p>
<pre><code class="language-go">type Interface interface {
    Do()   (Dto, error)
    Close() 
}</code></pre>
<p>맨 처음 정의한 PGClient interface 입니다.</p>
<pre><code class="language-go">type PGClient interface {
  // Header 생성
  CreateHeader(map[string]string) (http.Header, error)    

  // Request Body 생성 
  CreateBody(gRPCRequest interface{}) (io.Reader, error)

  // HTTP 요청
  Call(ctx context.Context, header http.Header, body io.Reader) (*http.Response, error)

  // HTTP Response를 파싱하여 DTO로 변환
  ParseBody(pgResponseBody *http.Response) (interface{}, error)
}</code></pre>
<p>인터페이스를 정의하고 막상 로직을 구현하다보니 문제가 발생했습니다. 각 PG사로 HTTP요청을 보내기 위한 값이 서로 달랐고, PG사로 응답을 받아 이를 다음 로직으로 넘겨줄 때 반환값이 각 PG사 별로 다르다는 것이었습니다. 더군다나 NaverPay의 경우 결제 예약(Reserve) 이후 결제 승인(Approval)을 하는 이중요청의 구조를 가지고 있기 때문에, NicePay / NaverPay 별로 interface를 선언한다고 하더라도 타입 이슈를 해결할 수 없었습니다. </p>
<p>이를 유연하게 대처하고자 interface 타입으로 정의했지만 실제 로직에서 interface 타입 데이터를 핸들링하는 불필요한 로직이 생성되면서 코드의 양이 증가하고 읽기 어려운 코드로 작성되기 시작했습니다. 이 문제를 해결하기 위해 도입한 것이 generic입니다.</p>
<h2 id="generic-사용하기">Generic 사용하기</h2>
<p>generic은 데이터나 함수를 추상적으로 다룰 수 있게 하는 기능입니다. 이전까지는 Golang에서 generic을 지원하지 않았지만 1.18 버전부터 generic을 지원하기 시작했습니다. generic을 사용하면 특정 타입에 크게 종속되지 않고 일반적인 형태로 코드를 작성할 수 있습니다. 이를 통해 특정 함수나 데이터를 재사용할 수 있게 되면서 코드의 유연성이나 재사용성을 높일 수 있습니다.</p>
<p>generic을 활용하여 새롭게 정의한 PGClient interface입니다.</p>
<pre><code class="language-go">type PGClient[T, V any] interface {
  // Header 생성
  CreateHeader(map[string]string) (http.Header, error)    

  // Request Body 생성 
  CreateBody(grpcRequest *T) (io.Reader, error)

  // HTTP 요청
  Call(ctx context.Context, header http.Header, body io.Reader) (*http.Response, error)

  // HTTP Response를 파싱하여 DTO로 변환
  ParseBody(pgResponseBody *http.Response) (V, error)
}</code></pre>
<p>이전에 interface 선언되던 CreateBody의 인자값과 ParseBody의 응답이 T, V와 같은 타입으로 정의되었습니다. </p>
<p>실제 이를 구현한 곳에서 이 interface 타입은 아래와 같이 정의할 수 있습니다. 아래의 protocol.NicePayRequest가 T, *protocol.NicePayResponse가 V 이며 generic 타입입니다. 이를 통해 추상적이며 유연하게 구조를 선언했고, 코드의 재사용성을 높였습니다.</p>
<pre><code class="language-go">type NicePayServiceServer struct {
  client PGClient[protocol.NicePayRequest, *protocol.NicePayResponse]
}</code></pre>
<h2 id="dependency-injection-golang-wire">Dependency Injection: Golang Wire</h2>
<p>DI를 사용하면 각 컴포넌트가 직접적으로 의존하는 객체를 생성하지 않고 외부에서 주입 받을 수 있습니다. 이를 통해 컴포넌트간의 결합도가 낮아지며 좀 더 유연한 구조를 만들 수 있습니다. 개발자입장에서도 매번 객체를 생성하는 번거로움과 발생할 수 있는 휴먼 에러를 줄이고, 일관성 있는 형태를 구현할 수 있습니다.</p>
<p>하나의 serviceServer를 구성하기 위해 우리가 정의한 컴포넌트는 4가지입니다.</p>
<ul>
<li>config: pg사 service key 및 각종 설정</li>
<li>pgClient: pg사 구조체</li>
<li>logger: 결제 승인 로깅</li>
<li>serviceServer: gRPC ServiceServer</li>
</ul>
<p>생성자 매서드등을 통해 의존 관계를 주입해 줄 수 있지만 매번 정의를 해줘야하는점, 재사용을 하기 위해서는 매번 새롭게 손수 정의 해줘야 한다는 점을 고려해보았을 때 DI 모듈을 사용하기로 결정했습니다. </p>
<p>Golang 의 여러 DI module이 존재하지만 github 기준 가장 star 도 많고 레퍼런스도 많은 <a href="https://github.com/google/wire">wire</a>를 도입하기로 했습니다. 아래와 같이 wire는 의존성을 정의한 파일을 선언하고 이를 컴파일하게 되면 의존성이 정의된 새로운 파일이 생성됩니다.</p>
<pre><code class="language-go">// ex) wire.go: 의존 관계 정의
func InitializeNicePayServiceServer(logger_ *logger.MongoLogger) handler.NicePayServiceServer {
    wire.Build(
        config.NewNicePayConfig,
        handler.NewNicePayClient,
        handler.NewNicePayServiceServer,
    )
    return handler.NicePayServiceServer{}
}

// wire_gen.go: wire.go 컴파일 후 생성된 파일. 의존 관계 정의 
func InitializeNicePayServiceServer(logger_ *logger.MongoLogger) handler.NicePayServiceServer {
    nicePayConfig := config.NewNicePayConfig()
    pgClient := handler.NewNicePayClient(nicePayConfig, logger_)
    nicePayServiceServer := handler.NewNicePayServiceServer(pgClient)
    return nicePayServiceServer
}</code></pre>
<p>이렇게 생성된 wire_gen.go 파일은 실제 로직에서 아래와 같이 사용 할 수 있습니다</p>
<pre><code class="language-go">nicePayServiceServer := injector.InitializeNicePayServiceServer(logger)</code></pre>
<p>wire를 사용하면서 보다 가독성 있는 코드를 작성 할 수 있었고, 일관성 있는 의존 관계를 정의할 수 있었습니다.</p>
<h2 id="마무리">마무리</h2>
<p>이번 글에서는 비즈니스 로직을 개발하면서 마주했던 고민과 이를 해결했던 방법에 대해서 적어보았습니다. 다음 마지막 글에서는 인프라 설계와 효율적인 운영을 하기 위해 마주했던 고민을 적어보겠습니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[결제서버 분리하기 - 1: Monolithic to  MSA]]></title>
            <link>https://velog.io/@ryu_log/%EA%B2%B0%EC%A0%9C%EC%84%9C%EB%B2%84-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-1-Monolithic-to-MSA</link>
            <guid>https://velog.io/@ryu_log/%EA%B2%B0%EC%A0%9C%EC%84%9C%EB%B2%84-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0-1-Monolithic-to-MSA</guid>
            <pubDate>Sun, 04 Feb 2024 14:21:37 GMT</pubDate>
            <description><![CDATA[<h2 id="monolithic-to-microservice의-결정">Monolithic to MicroService의 결정</h2>
<p>서비스가 커가면서 고민하게 되는 여러가지 옵션 중 하나가 바로 MSA입니다. 기존 모놀리식 아키텍쳐(이하 모놀리식)에서는 SPOF(Single Point Of Failure) 단일 장애점에 대한 위험을 늘 감수해야하고, scale-out을 하더라도 각각의 서비스 리소스와 상관 없이 모든 서비스가 scale-out 되어야 하기 때문에 시스템 리소스 비효율이 발생하기도 합니다. 그외에도 각 서비스가 강결합되어 있어, 전체 시스템 구조 파악이 어렵고, 수정 시 장애 영향 파악이 어려울 수 있다는 등의 문제가 있습니다. </p>
<p>현재 우리의 서비스는 모놀리식로 서비스가 운영되고 있습니다. 모놀리식의 장점도 명확합니다. 대부분의 개발자가 모놀리식을 경험해보았기 때문에 MSA에 비해 상대적인 러닝 커브도 낮고 프로젝트를 쉽게 시작할 수 있습니다. 또한 모든 코드가 단일 코드베이스에 존재하기 때문에 배포, 디버깅, 테스트 모니터링이 용이합니다. </p>
<p>물론 앞으로도 현재의 모놀리식 환경에서도 충분히 서비스를 서빙할 수 있고 지금의 방식대로 새로운 기능을 추가하고 서비스를 유지보수하며 운영 할 수 있습니다. 오히려 작은 팀의 규모에서 MSA는 관리 포인트만 추가되는 것이고, 운영 효율성을 낮출 수도 있기 때문에 한편으로는 오버엔지니어링이 될 수 있습니다. 비즈니스 사이드에서도 “왜 잘 되는 기능을 옮기는 것이죠 ?”라는 질문을 받을 수도 있습니다. 그럼에도 MSA를 결정하게 된 이유는 크게 두 가지 입니다.</p>
<blockquote>
<p>💡 앞으로 서비스가 더 성장하고, 팀이 커가는 과정에서 현재의 모놀리식 구조를 유지하는 것이 우리팀의 생산성이나 서비스의 운영의 위험성을 높이는 것이라고 판단했습니다.</p>
</blockquote>
<blockquote>
<p>💡 기술적인 이유 뿐만 아니라, 우리 팀이 성장하고, 발전하기 위해서는 지난 몇 년 간 유지했던 모놀리식 구조를 벗어나 작게나마 MSA 환경에서 서비스를 서빙하는 것이 우리에게도 큰 배움이 있을 것이라는 확신이 있었습니다. 인재 영입의 관점에서도 MSA 환경에서 서비스를 서빙하는 것은 지원자들에게 큰 매력이라고 생각했습니다.</p>
</blockquote>
<p>이러한 이유로 MSA의 첫삽을 뜨게 되었습니다.</p>
<h2 id="msa-고민-시작--">MSA.. 고민 시작 .. !</h2>
<h3 id="고민-1-무엇부터-옮겨야하지-">고민 1. 무엇부터 옮겨야하지 ?</h3>
<p>MSA를 결정하게 된 후 가장 큰 고민은 “무엇을 분리 해야하는가?” 에 대한 고민입니다. 모놀리식 구조로 이미 서비스를 런칭하고 있었기에 대부분에 서비스가 강결합되어 있었고, 하나의 데이터베이스를 기반으로 설계 되었기 때문에 쉽게 분리하기가 어려웠습니다. </p>
<p>그렇기 때문에 사실상 “무엇을 분리 해야하는가?”에 대한 고민보다는 <strong>“도대체 무엇을 분리 할 수 있을까?”</strong> 에 대한 고민을 해야하는 상황이었습니다. 특정 도메인을 분리하려고 하면 DB는 ? 연결된 비즈니스 로직은 ? 등 제약 사항이 많았고, 예상할 수 없는 사이드 이펙트에 대한 걱정도 컸습니다. 최종 결정은 “일단은 어떠한 서비스라도 분리 하자” 였습니다. 일단 분리를 하고 운영을 하다 보면 다음 서비스.. 그 다음 서비스..를 분리하는데는 이전보다 쉬울 것이라고 판단했고, 이 과정에서 진정으로 MSA가 우리팀의 핏에 맞는 환경인지도 판단 할 수 있을 것이라 생각했습니다.</p>
<p>그렇게 고민하다가 발견한 것이 바로 결제 서비스입니다. 결제 도메인의 가장 큰 특징은 바로 PG사와의 외부 통신이 일어난다는 것입니다. 그렇기에 여러 서비스 도메인들과의 결합도도 상대적으로 낮았고, DB 분리 측면에서도 결제 서버 앞단에서 메인 서버가 모든 결제의 유효성 검증을 한 후, 결제 서버로 PG사와의 거래 요청만 한고 결제 서버는 이 결과를 메인 서버로 전달해주는 역할만 한다면 기존 DB를 참조하지 않고도 결제 도메인을 분리를 할 수 있다고 생각했습니다.</p>
<h3 id="고민-2-어떻게-옮길-수-있을까-">고민 2. 어떻게 옮길 수 있을까 ?</h3>
<Python to Golang>

<p>팀에서는 Python-Django를 메인 기술스택으로 서비스를 운영하고 있습니다. Django는 다양한 내장 기능을 제공하지만 상대적으로 무겁다보니 마이크로 서비스용으로 운영하기에는 부적절하다고 생각했습니다. 또한 MSA 자체가 하나인 특정 언어, 프레임워크에 종속적이지 않은 장점을 경험하기 위해 기존 Python-Django을 벗어나보기로 결정했습니다. 최종 결정한 언어는 Golang입니다. Golang으로 결정한 이유는 크게 두 가지입니다.</p>
<ul>
<li>팀 내에서 Golang으로 별도의 서비스를 운영하고 있어 팀 내 언어 커버리지를 높이기 위해 결제 분리 프로젝트의 메인 언어를 Golang으로 결정했습니다.</li>
<li>결제 서비스에서 가장 중요한 것은 운영 안전성이라고 생각했습니다. 언어 자체의 타입 제약이 없는 파이썬보다는 Golang으로 운영 안정성을 높이고자 했습니다. 이와 함께 컴파일언어의 성능적인 부분에서도 Golang의 충분히 이점이 있을 것이라 생각했습니다.</li>
</ul>
<http to gRPC>

<p>MSA와 함께 따라오는 키워드는 EventDriven, Kafka, DistributedTransaction, RPC .. 모놀리식 환경에서는 쉽게 접하기 힘든 개념들이었습니다. 각 개념을 하나하나 고려해보았습니다.</p>
<p>처음에는 EventDriven 구조로 설계해보았습니다. </p>
<ol>
<li>메인 서버와 결제서버 사이 Kafka를 붙여 메인 서버가 결제 이벤트를 발행시키면 </li>
<li>이를 구독하는 결제 서버가 PG사와의 거래를 처리하고 </li>
<li>처리 결과를 다시 메인 서버로 전송하여 처리하는 하는 구조로 설계했었습니다. </li>
</ol>
<p>사실 구현은 가능하지만 결제라는 도메인의 특징상 비동기로 설계하는 것이 맞지 않다고 생각했고, 직접 적인 결제보다는 결제 이후 대사처리를 비동기로 하는 것이 더 적합하다고 생각하여 하면서 EventDriven 고려 대상에서 제외했습니다.</p>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/4585a233-86cc-495f-8f38-2cc7b4ffc2be/image.png" alt=""></p>
<p>두 번째는 gRPC 였습니다. gRPC는 구글에서 개발한 오픈소스로 HTTP/2 기반의 Remote Procedure Call 프레임워크입니다. 공통된 인터페이스 정의(Proto 파일)를 각 언어에 맞게 컴파일하여 생성되는 Stub을 각 클라이언트와 서버가 공통으로 사용합니다. </p>
<p>높은 header 압축률을 보장하고 중복을 제거하여 HTTP/1 에 비해 효율적입니다. 마이크로서비스를 운영하는데 있어서 통신 비용을 생각 하지 않을 수 없습니다. 몇 ms 라도 이득을 보는것이 중요한데, 이러한 측면에서 gRPC는 괜찮은 옵션이었습니다. </p>
<p>또한 자체 인증이나 암호화, channel이라는 쉬운 인터페이스 등 내장  기능이 풍부하여 이후의 추가적인 기능을 붙이는데도 크게 어렵지 않게 붙일 수 있을 것이라 생각했습니다.</p>
<p>이러한 장점들을 고려해보았을 때 최종적으로 기존 HTTP RESTful 보다는 gRPC를 선택하여 진행하는 것이 보다 많은 이점이 있을 것 같아 gRPC를 선택하게 되었습니다.</p>
<h2 id="payment--golang--grpc--paygo">Payment + Golang + gRPC = PayGo</h2>
<p>PayGo. 결제 프로젝트명이자 결제 대행 서버의 이름입니다. Pay라는 서비스 성격과 Golang을 적절히 섞은 프로젝네이밍입니다. 이 PayGo의 목표는 크게 두 가지 였습니다.</p>
<ul>
<li>장애없는 결제 처리</li>
<li>결제 레이턴시 발생시키지 않기</li>
</ul>
<p>본질적으로 안전한 결제 서비스를 서빙해야했고, 하나의 서버에서 처리하던 결제를 별도의 서버에서 나눠 진행하면서 각 서버간의 통신 및 처리 과정에서의 레이턴시를 최대한 발생시키지 않는 것이 중요하다고 생각했습니다. 여기까지가 PayGo가 나오기까지의 고민과 의사 결정의 배경이었습니다. 이후의 편에서는 실제 클라이언트와 서버에서 어떻게 구조를 가져갔고 이과정에서 발생한 고민들을 한 번 적어보도록 하겠습니다 !
  <img src="https://velog.velcdn.com/images/ryu_log/post/afbc3f56-a237-44e2-84c9-c00dcf46cb3f/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Golang Options Pattern]]></title>
            <link>https://velog.io/@ryu_log/Golang-Options-Pattern</link>
            <guid>https://velog.io/@ryu_log/Golang-Options-Pattern</guid>
            <pubDate>Sun, 07 Jan 2024 12:57:54 GMT</pubDate>
            <description><![CDATA[<h2 id="go-생성자">Go 생성자</h2>
<p>Go에서는 Python <code>__init__()</code> 과 같은 명시적인 생성자가 없습니다. 그렇기 때문에 일반적으로 객체를 생성할 때는 함수의 네이밍을 <code>New + &lt;StructName&gt;</code>로 구성하면서 이 함수가 생성자임을 나타냅니다. 아래는 하나의 객체를 생성하는 간단한 예시입니다. 필요한 매개변수를 함수에 직접 전달하는 방식으로 객체의 속성의 값을 설정하고 생성할 수 있습니다.</p>
<pre><code class="language-go">type Person struct {
    FirstName string
    LastName  string
}

func NewPerson(firstName, lastName string) *Person {
    return &amp;Person{
        FirstName: firstName,
        LastName:  lastName,
    }
}

func main() {
    person := NewPerson(&quot;John&quot;, &quot;Doe&quot;)
    fmt.Println(person)
}</code></pre>
<p>간단한 객체를 생성할 때는 위와 같이 직접 전달하여 생성하는 것이 직관적이고 간결합니다. 하지만 Struct에 속성이 늘어난다면 어떨까요 ? 간단한 예시입니다.</p>
<pre><code class="language-go">type Person struct {
    FirstName string
    LastName  string
    Age       int
    Gender    string
    Address   string
}

func NewPerson(firstName, lastName, gender, address string, age int) *Person {
    return &amp;Person{
        FirstName: firstName,
        LastName:  lastName,
        Age:       age,
        Gender:    gender,
        Address:   address,
    }
}

func main() {
    person := NewPerson(&quot;John&quot;, &quot;Doe&quot;, &quot;male&quot;, &quot;New York&quot;, 25)
    fmt.Println(person)
}</code></pre>
<p>물론 위와 같이 모든 매개변수를 전달하여 생성할 수도 있지만, 우리는 몇 가지 질문이 떠오릅니다.</p>
<ul>
<li>전달해야하는 매개변수가 더 증가한다면 ?</li>
<li>일부 속성으로만(선택적으로) 객체를 생성하고 싶을 때는 ?</li>
<li>구조체의 새로운 속성이 추가 될 때마다 모든 객체의 생성부를 수정 해야하지 않을까 ?</li>
<li>그 때마다 떨어지는 가독성은 ?</li>
</ul>
<p>등등 여러 고민에 맞닥뜨리게 됩니다. 여기서 이러한 문제를 보완하는 것이 Options Pattern 입니다.</p>
<h2 id="options-pattern">Options Pattern</h2>
<blockquote>
<p>Options Pattern: 객체 생성 시, 필요한 설정만 선택적으로 사용하여 객체를 생성하는 패턴 </p>
</blockquote>
<p>선택적으로 사용한다는 것은 Setter를 구현하고 이를 사용하는 것입니다. Go에서 구조체의 속성을 설정하기 위한 Setter 함수의 네이밍은 일반적으로 With~ 혹은 Set~ 의 네이밍을 주로 사용합니다. (Options Pattern에서는 좀 더 With~ 네이밍을 사용하는 것 같네요 .. ?)</p>
<pre><code class="language-go">func WithFirstName(firstName string) func(*Person) {
    return func(p *Person) {
        p.FirstName = firstName
    }
}

func WithLastName(lastName string) func(*Person) {
    return func(p *Person) {
        p.LastName = lastName
    }
}

func WithAge(age int) func(*Person) {
    return func(p *Person) {
        p.Age = age
    }
}</code></pre>
<p>위의 함수들의 시그니처를 보게되면, <code>With + &lt;속성&gt;</code>의 네이밍을 가지고 있고, 속성에 설정할 값을 매개변수로 받고 있습니다. 응답 타입은 포인터 구조체를 매개변수로 받는 함수이고, 그 함수를 보게되면, 구조체의 특정 속성을 매개변수로 설정하고 있습니다.</p>
<p>그리고 이를 호출하고 활용하는 방법은 아래와 같습니다. 생성자 함수를 보게 되면 이전에는 각 속성을 직접적인 매개변수로 받았지만, Options Pattern을 적용한 이후 Setter 함수들을 매개변수로 받으면서 내부에서 그 함수들을 순회하고, 최종 객체를 반환합니다.</p>
<pre><code class="language-go">func NewPerson(opts ...func(*Person)) *Person {
    p := &amp;Person{}
    for _, opt := range opts {
        opt(p)
    }
    return p
}

func main() {
    person := NewPerson(
        WithFirstName(&quot;Jin&quot;),
        WithLastName(&quot;Lee&quot;),
        WithAge(30),
    )
    fmt.Println(person)
}</code></pre>
<p>Options Pattern을 사용하면, </p>
<ul>
<li>사용자는 필요한 옵션만 선택사여 전달하면 되니 매서드의 호출이 보다 유연해집니다. 만약 사용자가 특정 옵션을 전달하지 않는다면 기본값이 사용됩니다.</li>
<li>새로운 옵션이 추가되더라도 기존 함수의 시그니처를 변경하지 않고도 새로운 옵션만 추가하여 사용 할 수 있습니다. 코드의 확장성과 유지보수성이 증가했습니다.</li>
<li>옵션들을 호출하는 매서드의 네이밍이 직관적이다보니 객체의 어떤 속성 값이 설정되는지 쉽게 이해가 되어 가독성을 향상되었습니다.</li>
</ul>
<p>이러한 장점들로 객체의 복잡한 설정이 필요한 경우 자주 사용되곤합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[멱등성이 보장되는 airflow 로직 만들기 !]]></title>
            <link>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-%EB%A9%B1%EB%93%B1%EC%84%B1%EC%9D%B4-%EB%B3%B4%EC%9E%A5%EB%90%98%EB%8A%94-airflow-%EB%A1%9C%EC%A7%81-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-%EB%A9%B1%EB%93%B1%EC%84%B1%EC%9D%B4-%EB%B3%B4%EC%9E%A5%EB%90%98%EB%8A%94-airflow-%EB%A1%9C%EC%A7%81-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sun, 17 Dec 2023 15:57:43 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>간단하게 저의 환경을 소개드리면,</p>
<ul>
<li>현재 회사에서 AWS EC2 + ECS 환경에서 Airflow를 운영하고 있습니다</li>
<li>데이터 웨어하우스로 구글 빅쿼리를 사용 중이며, airflow를 통해 메인 데이터베이스에서 데이터를 빅쿼리로 옮기는 ETL Load 작업을 진행하고 있습니다</li>
</ul>
<h3 id="간단-개념--etl과-airflow">간단 개념 ! <strong>ETL과 Airflow</strong></h3>
<blockquote>
<p>💡 ETL은 추출(Extract), 변환(Transform), 로드(Load)를 나타내며 조직에서 여러 시스템의 데이터를 단일 데이터베이스, 데이터 저장소, 데이터 웨어하우스 또는 데이터 레이크에 결합하는 방법입니다 <a href="https://cloud.google.com/learn/what-is-etl?hl=ko">출처</a></p>
</blockquote>
<blockquote>
</blockquote>
<p>💡 Airflow는 Python 기반의 프레임워크로, 워크플로우(workflow)를 작성하고 작업의 스케쥴링, 모니터링을 할 수 있습니다. 많은 기업에서 Airflow를 통해 ETL 자동화를 하고 있습니다.</p>
<h2 id="멱등성">멱등성</h2>
<blockquote>
<p>💡 멱등성은 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질</p>
</blockquote>
<p>즉, 다른 날짜/시간/조건에서 동일한 매개변수로 실행되는 경우, 그 결과가 동일하게 유지되어야 한다는 것입니다. 멱등성은 ETL을 구성할 때 중요한 고민 포인트 중 하나입니다. 반복적이고 지속적으로 진행되는 ETL 작업에서 만족스러운 데이터 퀄리티 유지하기 위해서는 작업 누락, 원본 데이터와의 정합성도 중요하지만 결국 작업에 대한 멱등성이 보장되어야 데이터 중복과 같은 이슈에서 안전 할 수 있습니다.</p>
<p>물론 이전에도 멱등성을 고민하지 않은 것은 아닙니다. 이전에는 일부 테이블에 대해서 빅쿼리 WRITE_TRUNCATE 옵션을 통해 데이터의 멱등성을 보장했습니다.</p>
<p>(참고) 빅쿼리 데이터 쓰기 작업에는 두 가지의 옵션을 줄 수 있습니다. [<a href="https://cloud.google.com/bigquery/docs/scheduling-queries?hl=ko">출처</a>]</p>
<ul>
<li>WRITE_TRUNCATE: 테이블이 존재하면 BigQuery가 테이블 데이터를 덮어씁니다</li>
<li>WRITE_APPEND: 테이블이 존재하면 BigQuery가 데이터를 테이블에 추가합니다</li>
</ul>
<p>방법은 간단합니다. 원본 테이블 전체를 dump 하여 빅쿼리에 적재하면 됩니다. 이 경우, 기존 빅쿼리 테이블을 덮어써버리기 때문에 원본 데이터 베이스의 변경 사항이 빅쿼리에도 즉각 반영이 되고, 몇 번을 반복하더라도 동일한 결과가 유지 됩니다. </p>
<p>하지만 이 경우는 테이블 크기가 작은 테이블에만 적용 할 수 있습니다. 테이블 크기가 일정 수준을 넘어가 버린다면, dump query 자체가 시스템 리소스의 큰 부하를 주게되어, 다른 작업에 영향이 갈 수도 있습니다. 그래서 이런 경우에는 어쩔 수 없이 WRITE_APPEND 옵션을 주고 airflow가 예상대로만 동작하기만을 기대할 수 밖에 없었습니다.</p>
<h3 id="고민">고민</h3>
<p>airflow는 자체 DB를 통해 작업 스케쥴링을 관리합니다. airflow는 meta db에 저장된 작업의 최종 상태에 따라 작업을 종료할 수도, 다시 수행할 수도 있습니다. </p>
<p>여기서 문제가 발생합니다. 평시에는 문제가 없지만 종종 airflow가 내려갔다가 올라가는 경우 이전에 누락된 작업들이 다시 한 번에 동작을 하게 되는데, 이 때 멱등성이 보장되지 않은 상태에서 작업이 완료되다보니 일부 데이터들에 대한 중복이 발생하는 것이었습니다.</p>
<p>그럴 때 마다 다시 빅쿼리에서 데이터를 제거하고, 다시 Airflow를 통해 데이터를 보정하곤 했습니다. 물론이 작업에 포함되는 주요 로직이 시간 기반 쿼리이다보니 발생한 문제이기도 하지만, 이것을 해결 한다고 하더라도 근본적으로 중복이 발생할 수도 있는 상황이었습니다. </p>
<h2 id="멱등성이-보장되는-airflow-로직-만들기-">멱등성이 보장되는 Airflow 로직 만들기 !</h2>
<p>위의 고민을 하다가 빅쿼리 MERGE를 사용하여 멱등성을 보장하는 Airflow 로직을 개발하기로 했습니다. 이를 한 장으로 정리하면 아래와 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/6dd3e4a4-c738-4225-8dea-0fde2670f7f8/image.png" alt=""></p>
<p>왼쪽이 기존 로직, 오른쪽이 새롭게 도입한 로직입니다.</p>
<p>왼쪽은 간단합니다. 메인 데이터베이스의 데이터를 airflow를 통해 빅쿼리 서비스용 DB로 옮기는 작업입니다. 반면 오른쪽은 메인 데이터베이스의 데이터를 빅쿼리 비지니스 및 분석용 DB 바로 옮기는 것이 아닌 임시 DB에 옮겨 놓은 후 임시 DB 테이블과 서비스 DB 테이블 사이의 MERGE를 통해 최종적으로 데이터를 Load하는 방식입니다.</p>
<h3 id="bigquery-merge">BigQuery MERGE</h3>
<p>빅쿼리 MERGE는 간단합니다. 여러 테이블의 데이터를 합치는 방식으로 병합 조건에 따라, 병합 조건에 만족하면 UPDATE or DELETE를 실행하고 조건에 만족하지 않는다면 INSERT 합니다. 한마디로 있으면 업데이트, 없으면 추가하는 쿼리문 입니다.</p>
<pre><code class="language-sql">MERGE INTO `target_table` USING `source_table`
ON `merge_condition`
WHEN MATCHED THEN
  UPDATE SET `column1` = `value1`, `column2` = `value2`, ...
WHEN NOT MATCHED THEN
  INSERT (`column1`, `column2`, ...) VALUES (`value1`, `value2`, ...)</code></pre>
<h3 id="bigquerymergeoperator">BigQueryMergeOperator</h3>
<p>Airflow에 BigQueryMerge만을 담당하는 Operator는 없습니다. 그래서 기존 BigQueryInsertJobOperator를 상속 받은 BigQueryMergeOperator를 커스텀 했습니다.</p>
<p>먼저 BigQueryInsertJobOperator 의 동작원리는 간단합니다. configuration, 쿼리를 포함한 동작 요청을 정의하면 execute 단계에서 빅쿼리에 해당 작업이 수행됩니다. 따라서 이 BigQueryInsertJobOperator configration을 잘 정의하면 CustomMergeOperator를 만들 수 있습니다.  </p>
<p>우선 특정 작업에서만 사용할 것이 아닌 전반적인 로직에서 사용할 것이기에, 하드코딩이 아닌 동적으로 값이 변화해야합니다. configration을 구성하는 작업은 크게 두 단계로 나눴습니다.</p>
<ol>
<li><p>target 빅쿼리 테이블 컬럼 조회</p>
<pre><code class="language-python"> def fields(self):
     &quot;&quot;&quot;빅쿼리 테이블의 필드 정보&quot;
     hook = BigQueryHook(location=&#39;asia-northeast3&#39;)
     try:
        schemas = hook.get_schema(
             dataset_id=self.target_dataset,
             table_id=self.table,
        )
        return [field[&#39;name&#39;] for field in schemas[&#39;fields&#39;]]
     except Exception:
        return []</code></pre>
</li>
<li><p>동적으로 merge query 생성</p>
</li>
</ol>
<pre><code class="language-python">    def merge_query(self):
        &quot;&quot;&quot;merge query 생성&quot;&quot;&quot;
        _fields = self.fields
        update = &#39;, &#39;.join([f&quot;{field}=source.{field}&quot; for field in _fields])
        insert_fields = &#39;, &#39;.join([f&quot;{field}&quot; for field in _fields])

        target_source_condition = f&quot;MERGE {self.target_dataset}.{self.table} target USING {self.source_dataset}.{self.table} source&quot;
        on_pk_condition = &quot;target.id = source.id&quot;
        update_condition = f&quot; WHEN MATCHED THEN UPDATE SET {update}&quot;
        insert_condition = f&quot; WHEN NOT MATCHED THEN INSERT ({insert_fields}) VALUES ({insert_fields})&quot;
        return target_source_condition + &quot; ON &quot; + on_pk_condition + update_condition + insert_condition</code></pre>
<h2 id="마무리">마무리</h2>
<p>데이터 퀄리티를 높이기 위해서는 단순 멱등성 뿐만 아니라 제 시간에 데이터가 업데이트 되었는지, 원본 데이터 베이스와의 정합성이 보장되는지, 누락된 데이터가 있는지 등 여러가지 요소를 모두 고려해야합니다. 그래도 이번 기회를 통해 적어도 멱등성 관련한 영역에서의 데이터 퀄리티를 높일 수 있어 되어 뿌듯하네요 🚀</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠폰 로직 리팩토링 하기 !]]></title>
            <link>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-%EC%BF%A0%ED%8F%B0-%EB%A1%9C%EC%A7%81-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-%EC%BF%A0%ED%8F%B0-%EB%A1%9C%EC%A7%81-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 07 Dec 2023 15:03:58 GMT</pubDate>
            <description><![CDATA[<h3 id="글을-읽기-전-배경">글을 읽기 전 배경</h3>
<ul>
<li>모빌리티 스타트업에서 백엔드 엔지니어로 근무하고 있어요 !</li>
<li>python, django를 주로 사용합니다 !</li>
</ul>
<h3 id="복잡한-결제와-쿠폰">복잡한 결제와 쿠폰</h3>
<p>결제는 언제나 복잡합니다. 주행 요금이 최초 산정되면 유저가 소유한 정기권, 쿠폰,  B2B 제휴사, 거치대 할인, 각 파트너사 등 모든 경우를 적용하여 최종 요금이 부과됩니다. 그 중 쿠폰은 정기권과 함께 유저가 가장 많이 사용하는 할인 수단 중 하나입니다. 비즈니스 요구사항은 점차 다양해지면서 다양한 유형의 쿠폰이 생겨났고, 현재는 크게 4가지 타입의 쿠폰이 존재합니다.</p>
<blockquote>
<p>N분 무료 쿠폰
잠금해제 무료 쿠폰
요율 할인 쿠폰
금액 할인 쿠폰</p>
</blockquote>
<h3 id="하나의-쿠폰이-사용되기-위해서는-아래와-같은-사항들이-고려되어야-합니다">하나의 쿠폰이 사용되기 위해서는 아래와 같은 사항들이 고려되어야 합니다</h3>
<blockquote>
<p>(적용 시점) 현재 적용 가능한 쿠폰인가 ?
(적용 시점) 적용 시점에 만료가 되었다면 만료 처리를 진행 할 수 있는가 ?
(적용 시점) 각 쿠폰 타입에 따라 요금 할인이 올바르게 적용 되었는가 ?
(사용 시점) 결제 이후 쿠폰에 대한 사용 처리 했는가 ?</p>
</blockquote>
<p>쿠폰의 적용 시점만 보더라도 <strong>“쿠폰의 타입의 구분”</strong>하고 <strong>“적용 가능 여부를 확인”</strong>하며, <strong>“쿠폰의 혜택에 맞는 요금 계산”</strong> 을 진행해야합니다. 만약 위의 과정을 하나의 플로우에서 처리한다면 어떨까요 ? 간단하게 생각하면 수많은 if / elif / else 체이닝을 통해 해결 할 수 있을 것 입니다.</p>
<p>하지만 이럴 경우, 테스트 코드 관점에서는 하나의 로직을 통으로 테스트 해야하기 때문에 단위 단위의 테스트 코드를 작성하기에 취약하고, 누군가 새로운 로직을 추가할 때 한 눈에 로직을 파악하기 어렵습니다. 사소한 변경에도 예상치 못한 사이드 이펙트가 발생할 수가 있고, 무엇보다 앞으로 새로운 유형의 쿠폰이 등장한다면 ? 유지보수의 난이도가 급격히 높아질 것입니다. 이시점에서라도 앞으로의 유지보수의 효율을 높이기 위해 쿠폰 적용 로직을 <strong>인터페이스 기반으로 리팩토링</strong>을 진행하기로 했습니다.</p>
<h3 id="1차-리팩토링">1차 리팩토링</h3>
<p>우선 쿠폰의 대표적인 적용 과정을 추상화하여 이를 추상 클래스로 정의했습니다. </p>
<pre><code class="language-python">class CouponHandlerBase(ABC):
    &quot;&quot;&quot;interface class&quot;&quot;&quot;

    @abstractmethod
    def can_consume(self) -&gt; bool:
        &quot;&quot;&quot;쿠폰 적용 가능 여부&quot;&quot;&quot;
        raise NotImplementedError

    @abstractmethod
    def consume(self, charge: int) -&gt; int:
        &quot;&quot;&quot;쿠폰 적용. 쿠폰 할인 금액 반환&quot;&quot;&quot;
        raise NotImplementedError

    @abstractmethod
    def finalize(self) -&gt; None:
        &quot;&quot;&quot;쿠폰 사용 후 처리&quot;&quot;&quot;
        raise NotImplementedError</code></pre>
<p>이후 주행 쿠폰외에 다른 쿠폰(정기권을 할인 해주는 쿠폰이라던지..)이 생성될 것을 고려하여 공통으로 사용할 메서드들을 담는 CouponHandlerMixin 클래스를 정의했습니다. 그리고 이것들을 상속받는 주행 전용 쿠폰 클래스인 RidingCouponHandler를 정의했습니다.</p>
<pre><code class="language-python">class CouponHandlerMixin:
    &quot;&quot;&quot;mixin class&quot;&quot;&quot;

    def _used(self) -&gt; None:
        &quot;&quot;&quot;쿠폰 사용 처리&quot;&quot;&quot;
        ...

    def _expired(self) -&gt; None:
        &quot;&quot;&quot;쿠폰 만료 처리&quot;&quot;&quot;
        ...</code></pre>
<pre><code class="language-python">class RidingCouponHandler(CouponHandlerMixin, CouponHandlerBase):
    def __init__(self):
        ...

    def consume(self, charge: int) -&gt; int:
        # 세부 구현체에서 overriding
        pass

    def can_consume(self) -&gt; bool:
        # 쿠폰 적용 가능 여부 로직의 경우 공통된 부분이기 때문에 상위 구현체에서 구현
        pass

    def finalize(self) -&gt; None:
        # 쿠폰 사용 후 처리 로직의 경우 공통된 부분이기 때문에 상위 구현체에서 구현
        pass</code></pre>
<p>그리고 RidingCouponHandler를 상속 받는 최종적으로 세부 구현체를 구현했습니다.</p>
<pre><code class="language-python">class UnlockFreeCoupon(RidingCouponHandler):
    &quot;&quot;&quot;잠금 해제 무료 쿠폰&quot;&quot;&quot;

    def consume(self, charge: int) -&gt; int:
        # 쿠폰 적용 로직 구현
        ...</code></pre>
<p>이 모든 것을 다이어그램으로 그리면 아래와 같습니다.
<img src="https://velog.velcdn.com/images/ryu_log/post/014018c7-9fc5-43ba-bacf-91f930d57562/image.png" alt=""></p>
<p>어떤가요 ? 하나의 로직에서 쿠폰의 유형을 구분하고, 적용 가능한지 확인 한 후, 실제 적용을 했을 때보다 정리가 되었고, 각 쿠폰 구현체 별 로직을 좀 더 파악 할 수 있지 않나요 ? 하지만 1차 리팩토링 이후 몇 가지 문제점을 발견했습니다.</p>
<h3 id="1차-리팩토링의-문제점-발견">1차 리팩토링의 문제점 발견</h3>
<blockquote>
<ol>
<li>추상 메서드와 일반 메서드가 함께 존재하는 안티패턴</li>
<li>불필요한 상속으로 인한 복잡성 증가 및 유연성 감소</li>
</ol>
</blockquote>
<h4 id="문제점-1-추상-메서드와-일반-메서드가-함께-존재하는-안티패턴">문제점 1) 추상 메서드와 일반 메서드가 함께 존재하는 안티패턴</h4>
<ul>
<li>RidingCouponHandler를 보면 추상 메서드와 일반 메서드가 공존합니다. 하나의 클래스 내에서 추상 메서드와 일반 메서드가 함께 존재한다면, 이 클래스를 상속받는 하위 클래스에서는 반드시 추상 메서드를 구현해야 하지만 일반 메서드는 선택적으로 오버라이드할 수 있습니다. 이는 상속 관계에서 일관성이 떨어질 수 있고, 예상치 못한 동작을 유발할 수 있기 때문에 안티 패턴입니다.</li>
<li>따라서 추상 클래스에서는 추상 메서드만을 정의하고, 구현된 메서드는 일반 클래스에서 처리하는 것이  코드의 일관성을 유지하며, 각 클래스 계층 구조가 명확해지게 됩니다. 여기서 공통된 로직이 필요한 경우, 이를 다루기 위한 별도의 클래스나 인터페이스를 고려하는 것이 좋습니다.</li>
</ul>
<h4 id="문제점-2-불필요한-상속으로-인한-복잡성-증가-및-유연성-감소">문제점 2) 불필요한 상속으로 인한 복잡성 증가 및 유연성 감소</h4>
<ul>
<li>실제 세부 구현체인 UnlockFreeCoupon을 이해하기 위해서는 기본 인터페이스부터, 공통 로직이 포함된 Mixin 클래스, 상위 구현체인 RidingCouponHandler 클래스를 이해해야합니다. 상속 단계에 대한 대한 명확한 룰은 없지만 상속 계층이 간단할 수록 이해하기 쉽고, 유지보수에 용이합니다.</li>
<li>많은 상속 계층이 있다면, 자칫 상위 클래스의 변경이 하위 클래스에 예상치 못한 영향을 끼칠 수 있습니다. 또한 많은 상속은 결국 클래스간 의존성을 높임과 동시에 다이아몬드 문제를 늘 염두에 두고 개발을 진행해야합니다.</li>
</ul>
<p>그렇게 1차 리팩토링 후 개섬점을 발견했고, 액션 아이템을 도출했습니다.</p>
<blockquote>
<p>추상 클래스와 구현체의 명확한 구분
불필요한 상속을 줄여 유지보수에 용이한 단순한 구조 만들기</p>
</blockquote>
<h3 id="2차-리팩토링">2차 리팩토링</h3>
<p>우선 추상 매서드와 일반 메서드와의 불편한 공존을 제거하고 공통된 로직은 Mixin 클래스에 구현하면서 RidingCouponHandler의 불필요한 상속을 정리했습니다.</p>
<pre><code class="language-python">class CouponHandlerBase(ABC):
    &quot;&quot;&quot;interface class&quot;&quot;&quot;

    @abstractmethod
    def consume(self, charge: int) -&gt; int:
        &quot;&quot;&quot;쿠폰 적용. 쿠폰 할인 금액 반환&quot;&quot;&quot;
        raise NotImplementedError</code></pre>
<pre><code class="language-python">class CouponHandlerMixin:
    &quot;&quot;&quot;mixin class&quot;&quot;&quot;

    def _used(self) -&gt; None:
        &quot;&quot;&quot;쿠폰 사용 처리&quot;&quot;&quot;
                ...

    def _expired(self) -&gt; None:
        &quot;&quot;&quot;쿠폰 만료 처리&quot;&quot;&quot;
                ...

    def can_consume(self) -&gt; bool:
                &quot;&quot;&quot;쿠폰 적용 여부&quot;&quot;&quot;
        ...

    def finalize(self) -&gt; None:
                &quot;&quot;&quot;쿠폰 사용 후 처리&quot;&quot;&quot;
        ...</code></pre>
<p>그리고 세부 구현체에서 Mixin 클래스와 추상 클래스와 상속받으면서, 중복을 제거하는 동시에 추상 클래스의 행동을 강제할 수 있었습니다. </p>
<pre><code class="language-python">class UnlockFreeCoupon(CouponHandlerMixin, CouponHandlerBase):
    &quot;&quot;&quot;잠금 해제 무료 쿠폰&quot;&quot;&quot;

    def __init__(self, coupon: Coupon, riding: Riding):
        ...

    def consume(self, charge: int):
        # 쿠폰 적용 로직 구현
        ...</code></pre>
<p>최종 다이어그램은 아래와 같습니다. 어떤가요 ? 1차 리팩토링 이후와 비교해보았을 때 확실히 단순해졌고, 이해하기 쉬워졌습니다.
<img src="https://velog.velcdn.com/images/ryu_log/post/ddfe6883-7ce6-451e-8d3f-35aca653d545/image.png" alt=""></p>
<h3 id="테스트코드">테스트코드</h3>
<p>이전에는 쿠폰 적용 로직을 테스트 하기 위해서는 모든 적용 로직을 한 번에 테스트를 했어야 하나, 이제는 각 단계별로 구분하여 단위 별 테스트를 할 수 있게 되었습니다.</p>
<pre><code class="language-python">@pytest.fixture
def coupon(user) -&gt; Coupon:
    expire_at = (timezone.now() + relativedelta(days=10)).strftime(&#39;%Y-%m-%d&#39;)
    return baker.make(Coupon, user=user, is_active=True, expire_at=expire_at)


@pytest.fixture
def time_free_coupon(coupon) -&gt; Coupon:
    coupon.type = Coupon.TypeChoices.TIME_ONCE
    coupon.value = 10
    coupon.save()
    return coupon

@pytest.fixture
def expired_coupon(coupon) -&gt; Coupon:
    coupon.expire_at = (timezone.now() - relativedelta(days=1)).strftime(&#39;%Y-%m-%d&#39;)
    coupon.save()
    return coupon
​
@pytest.mark.django_db
class TestCoupon:
    def test_10분_무료_쿠폰을_적용한다(self, time_free_coupon, endpoint, auth_client):
        response: Response = auth_client.post(endpoint, data={&#39;coupon_id&#39;: time_free_coupon.id})

        assert response.status_code == status.HTTP_200_OK
        assert 600 == response.json()[&#39;amount&#39;]


    def test_적용_시점에_만료된_쿠폰은_사용할_수_없다(self, expired_coupon, endpoint, auth_client):
        response: Response = auth_client.post(endpoint, data={&#39;coupon_id&#39;: expired_coupon.id})

        assert response.status_code == status.HTTP_412_PRECONDITION_FAILED</code></pre>
<h3 id="마무리">마무리</h3>
<p>절차적으로 적용되던 쿠폰 로직를 인터페이스를 적용한 로직으로 리팩토링하면서 얻을 수 있는 효과는 아래와 같습니다.</p>
<h4 id="이해하기-쉽습니다">이해하기 쉽습니다.</h4>
<ul>
<li>쿠폰 적용이 추상 클래스에 정의되어(InterfaceClass), 기능 클래스(MixinClass)와 실제 구현체(class)가 구분되어 각 클래스간 구조와 역할을 한 눈에 파악할 수 있습니다.<h4 id="더-이상-if-분기를-따라가며-로직을-파악하지-않아도-됩니다">더 이상 if 분기를 따라가며 로직을 파악하지 않아도 됩니다.</h4>
</li>
<li>변화에 빠르게 대응 할 수 있습니다</li>
<li>새로운 쿠폰 유형과 새로운 적용 로직이 등장하더라도, 더 이상 어디에 if / elif / else 체이닝을 추가할지 고려하지 않고 쉽게 추가 할 수 있습니다.<h4 id="안전합니다">안전합니다.</h4>
각 구현체의 크기가 작고, 명확하게 구분되어 있어 테스트 코드를 작성 할 때도, 특정 구현체의 로직을 변경하더라도 안전하게 테스트하고 적용 할 수 있습니다.</li>
</ul>
<p>물론 이번 리팩토링이 완벽하지는 않습니다. 리팩토링을 하면서도 이게 맞나 싶은 고민점들이 많았고, 공통의 기능을 Mixin으로 빼는 것이, 최종 구현체를 이렇게 구현하는 것이 맞는지 계속 의문이 들고 있습니다. 하지만 언제나 개선을 안한 것 보다는 한 것이 낫고, 저의 고민이 들어간 코드가 반영된 것만으로도 행복합니다. 무엇보다 이번 경험을 통해 이전에는 해보지 못한 고민을 해본 것이 가장 큰 의미라고 생각합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Postgres PK가 33씩 뛰는 auto increase 현상 트러블 슈팅기(해결은 아니고 .. 원인 찾기 !)]]></title>
            <link>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-PK%EA%B0%80-33%EC%94%A9-%EB%9B%B0%EB%8A%94-auto-increase-%ED%98%84%EC%83%81-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EA%B8%B0%ED%95%B4%EA%B2%B0%EC%9D%80-%EC%95%84%EB%8B%88%EA%B3%A0-..-%EC%9B%90%EC%9D%B8-%EC%B0%BE%EA%B8%B0</link>
            <guid>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-PK%EA%B0%80-33%EC%94%A9-%EB%9B%B0%EB%8A%94-auto-increase-%ED%98%84%EC%83%81-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EA%B8%B0%ED%95%B4%EA%B2%B0%EC%9D%80-%EC%95%84%EB%8B%88%EA%B3%A0-..-%EC%9B%90%EC%9D%B8-%EC%B0%BE%EA%B8%B0</guid>
            <pubDate>Sun, 12 Mar 2023 13:49:40 GMT</pubDate>
            <description><![CDATA[<h2 id="1-→-34-→-67-">1 → 34 → 67 ?</h2>
<p>어느 날 데이터베이스 일부 테이블에서 id 컬럼(pk)의 값이 순차적으로 증가하지 않고 일부 값을 건너뛰며 생성되어 있는 것을 발견했습니다. 처음에는 일부 컬럼이 제거 되었나 생각이 되었지만 건너 뛴 간격 만큼 어떠한 작업이 진행되는 테이블이 아니었습니다.</p>
<p>또 유심히 살펴보니 증가 폭이 일정함을 발견했습니다. 1 → 34 → 67 과 같이 33씩 건너 뛰고 있었습니다. 이를 통해 이것은 우연이 아닌 특정 패턴을 가지는 이슈일 것이라 예상을 하고 문제를 바라보기 시작하고 찾아보기 시작했습니다. 찾아보니 같은 이슈에 대한 stackoverflow 글을 발견 할 수 있었습니다. </p>
<p>Aurora for PostgreSQL + pk 점프 현상: <a href="https://stackoverflow.com/questions/70957330/why-is-id-as-serial-discontinuous-values-after-failover-in-rds-aurora-postgresql">Why is id as SERIAL discontinuous values after failover in RDS Aurora PostgreSQL?</a></p>
<h2 id="sequence">Sequence</h2>
<blockquote>
<p>💡 <strong>Sequence:</strong> 자동으로 순차적으로 증가하는 유일한 순번을 반환하는 데이터베이스 객체</p>
</blockquote>
<blockquote>
<p>💡 <strong>Regclass</strong>: 시퀀스의 ObjectID(OID)</p>
</blockquote>
<p>이번 트러블 슈팅을 관통하는 하나의 개념은 Sequence입니다. sequence는 주로 ID와 같이 순차적으로 증가하는 컬럼에 자주 사용됩니다. 테이블과는 독립적으로 저장되고 생성. 여러테이블에서 참조해서 공유되지 않도록 주의 해야합니다. 시퀀스는 <code>RollBack</code>이 되지 않습니다. 시퀀스를 재설정하지 않는 방식으로 트랜잭션이 진행되기 때문에 <code>INSERT</code> 진행 중 실패하게 된다면 생성된 시퀀스 값이 손실됩니다.</p>
<p>시퀀스를 롤백하지 않는 이유는 크게 두 가지 입니다. </p>
<ol>
<li>A트랜잭션에서 시퀀스를 1을 가져갔고 B트랜잭션이 이어 시퀀스 2를 가져갑니다. 그런데 A가 롤백됨으로 이미 증가한 시퀀스를 -1 하거나 0으로 초기화한다면 정상적으로 처리된 B트랜잭션에서 부여된 시퀀스 2와 추후에 생성될 시퀀스는 중복이 생길 수도 있습니다. </li>
<li>또한 무결성을 위해 이미 부여된 시퀀스를 수정 한다면 병목이 발생 하여 성능 문제를 야기할 수 있습니다. </li>
</ol>
<p>따라서 트랜잭션 중 실패한 시퀀스는 따로 재설정하지 않습니다. 이는 은행 번호표와 같은데, 은행에서 번호표를 뽑았는데 중간에 고객이 기다리다가 지쳐 나가면 이전 번호를 채우는 것이 아니라 그 다음 번호로 진행하는 것을 생각하면 이해하기 쉽습니다. 따라서 트랜잭션 내에서 할당되었지만 사용되지 않은 번호는 트랜잭션이 종료되면 손실되어 시퀀스에 구멍이 발생할 수 있습니다.</p>
<h2 id="트러블-슈팅이라고-적고-삽질이라고-읽는다">트러블 슈팅이라고 적고 삽질이라고 읽는다..</h2>
<h3 id="try1-중복되는-pk-sequence가-존재하지-않을까-">try1. 중복되는 PK sequence가 존재하지 않을까 ?</h3>
<p>맨 처음 했던 생각은 혹여나 다른 테이블에서 문제가 되는 테이블의 sequence 값을 참조하여 사용하지 않을까 하는 의심이었습니다. 현재 모든 테이블의 PK는 sequence로 부여되고 있었는데, 이 값을 다른 테이블에서 참조하여 사용한다면 충분히 그럴 수 있다는 판단이었습니다. 그래서 전체 테이블의 sequence를 조회해보았더니 중복되는 sequnce 값은 존재하지 않았습니다. (혹여나 하는 마음이었지만 .. 그 누가 테이블 생성 시 default로 부여되는 이 값을 굳이 굳이 조작할 일이 있을까 하는 .. 또한 그 증가폭이 33이라는 일정함은 어떻게 설명하지 ..)</p>
<pre><code class="language-sql">SELECT c.relname FROM pg_class c WHERE c.relkind = &#39;S&#39; ORDER BY relname;</code></pre>
<h3 id="try2-pk의-증가-값이-1이-아닌-다른-값인가-">try2. PK의 증가 값이 1이 아닌 다른 값인가 ?</h3>
<p>또 한 편으로는 PK 증가 값이 1이 아닌 다른 값인가에 대한 의심이 들었습니다. 하지만 이 역시 확인해보니 증가폭(increment)이 1인 것을 확인했습니다.</p>
<pre><code class="language-sql">SELECT sequence_name, increment FROM information_schema.sequences ORDER BY sequence_name;</code></pre>
<h3 id="try3-sequence-cache_value-">try3. sequence cache_value ?</h3>
<p>찾아보니 PostgreSQL 시퀀스를 캐싱해 메모리에 캐싱해놓으면 엑세스 효율이 올라간다는 사실을 발견했습니다. 이 시퀀스 캐싱은 세션 레벨에서 동작하기 때문에 서로 다른 세션에서 nextval 시퀀스 값을 조회하면 cache_size 만큼 차이가 날 수 있음을 알게 되었습니다. 혹여나 어떠한 시스템의 이유로 캐싱된 데이터가 사라졌다면 시퀀스 값이 일정한 간격 만큼 증가할 수 있다고 생각했습니다. 그래서 캐시를 활용해 시퀀스를 관리하고 있는지 확인해보았지만 아쉽게도 모든 sequence의 cache_size는 1이었습니다.</p>
<pre><code class="language-sql">SELECT sequencename, cache_size FROM pg_catalog.pg_sequences;</code></pre>
<h2 id="seq_log_vals-"><strong>SEQ_LOG_VALS !</strong></h2>
<pre><code class="language-sql">/*
 * We don&#39;t want to log each fetching of a value from a sequence,
 * so we pre-log a few fetches in advance. In the event of
 * crash we can lose (skip over) as many values as we pre-logged.
 */
#define SEQ_LOG_VALS    32</code></pre>
<p>그러다가 발견한 것이 <strong>SEQ_LOG_VALS</strong> 입니다. PostgreSQL은 시스템 상에서 가져온 sequence 값을 일일이 기록하지 않고 한 번에 여러개를 가져오고 이 값을 한 번에 기록합니다. 이 값을 SEQ_LOG_VALS 라고 하고, PostgreSQL은 32개의 숫자를 미리 가져오고 시퀀스 부여를 진행되면서 가져온 SEQ_LOG_VALS를 모두 소진하면 <strong><a href="https://www.postgresql.org/docs/13/wal-intro.html">WAL(Write Ahead Log)</a></strong>에 기록하여 팔로워 노드에 알리고 다시 32개를 가져와 다시 시작하는 구조를 가지고 있습니다.</p>
<p>다만 이 값은 충돌이 발생하면 이 상태 값을 잃을 수 있으며 이전에 사전 할당된 모든 번호가 모두 &quot;사용&quot;되었다는 가정으로 새롭게 시작합니다. 현재 이 값은 시스템상 기본 디폴트로  <code>#define SEQ_LOG_VALS 32</code> 로 설정 되어있음(PostgreSQL.ver 마다 다를수도) (<a href="https://github.com/postgres/postgres/blob/master/src/backend/commands/sequence.c#L57">PostgreSQL SEQ_LOG_VALS</a>)</p>
<h2 id="그래도-왜-33씩-뛰는지-알게-되었다-">그래도.. 왜 33씩 뛰는지 알게 되었다 !</h2>
<p>충돌이 나면 이전까지 시퀀스를 부여를 기록하던 SEQ_LOG_VALS가 손실되고 새롭게 SEQ_LOG_VALS(32) + cahce_value(1) = 33 값이 추가된 값이 새롭운 시퀀스로 부여되기 때문에 서두의 문제제기에서 발견한 1 → 34 → 67 이라는 일정한 패턴이 발견된 것입니다. </p>
<p>또한 알아야 할 것은 충돌 또는 대기 상태로의 장애 조치 후 시퀀스가 일부 값을 건너뛸 수 있다는 것입니다. 시스템상 어떠한 이유에 의해서 시퀀스 번호를 받고 에러, 비정상적인 트랜잭션 종료 등이 발생하면 PK 값이 일정하지 않고 중간 중간 구멍이 발생할 수 있습니다. 중요한 것은 이러한 시퀀스로 부여되는 PK이 순차적인지, 직렬인지가 아닌 고유 해야 한다는 점입니다. 인조식별되는 PK에 순번에 유의미하게 설계한다면 성능 이슈는 물론 엄청난 복잡합이 동반됩니다.</p>
<h2 id="그래서-앞으로-남은-미션">그래서 앞으로 남은 미션</h2>
<p>가장 의문이었던 “33”이라는 규칙적이지만 미스테리한 숫자의 원인은 찾게되었습니다. 그 자체로 의미를 둘 수 있고 PK 중간 중간 구멍이 날 수 있음을 인정하지만 우리 시스템 상 어떠한 이유에서 지속적으로 SEQ_LOG_VALS가 손실되는지 찾아야 할 것 같습니다. 대부분의 (아니 거의 모든) 문서에서 “충돌” 이라는 표현을 사용하고 있습니다. 이 충돌의 범위가 어디까지인지, 어떠한 이벤트로 인해 발생하는 것인지 이제부터 조금 더 깊게 생각을 해봐야할 것 같습니다.</p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://tawool.tistory.com/306">https://tawool.tistory.com/306</a></li>
<li><a href="https://stackoverflow.com/questions/38450394/postgresql-sequence-jump-30-or-33-number-with-cache-equals-1">https://stackoverflow.com/questions/38450394/postgresql-sequence-jump-30-or-33-number-with-cache-equals-1</a></li>
<li><a href="https://stackoverflow.com/questions/66456952/what-does-log-cnt-mean-in-the-postgres-sequence/66458412#66458412">https://stackoverflow.com/questions/66456952/what-does-log-cnt-mean-in-the-postgres-sequence/66458412#66458412</a></li>
<li><a href="https://github.com/postgres/postgres/blob/master/src/backend/commands/sequence.c">https://github.com/postgres/postgres/blob/master/src/backend/commands/sequence.c</a></li>
<li><a href="https://incident.io/blog/one-two-skip-a-few">https://incident.io/blog/one-two-skip-a-few</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MongoDB Day Seoul 2022 후기]]></title>
            <link>https://velog.io/@ryu_log/MongoDB-Day-Seoul-2022-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@ryu_log/MongoDB-Day-Seoul-2022-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Mon, 24 Oct 2022 15:38:52 GMT</pubDate>
            <description><![CDATA[<h3 id="mongodb-day-seoul-2022">MongoDB Day Seoul 2022</h3>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/23050860-a8fe-428e-8983-6bd286e1bd6c/image.png" alt=""></p>
<p> 10월 19일 수요일 양재역 엘타워에서 진행한 몽고디비 컨퍼런스에 참여했습니다. 이번 몽고디비 컨퍼런스는 3년 만에 오프라인으로 진행되었습니다.
  <img src="https://velog.velcdn.com/images/ryu_log/post/c2b8a59b-4c2b-43df-8ec9-76ff9539ee3f/image.png" alt=""></p>
<blockquote>
<p>💡 MongoDB ? 
몽고디비는 도큐먼트 지향형 데이터베이스입니다. 일반적으로 사용하는 관계형데이터베이스가 아닌 비관계형 데이터베이스로서 비정규화된 형태를 가지고 있는 것이 특징입니다</p>
</blockquote>
<p> 몽고디비는 최근 12개월의 다운로드 수가 지난 12년간의 다운로드 횟수보다 많은 만큼 최근 각광 받는 데이터 베이스입니다. 수많은 기업에서 몽고디비를 사용하고 있고, 몽고디비 Atlas 솔루션을 도입하고 있습니다. </p>
<h3 id="session">Session</h3>
<p> 이번 MongoDB Day Seoul 2022는 총 8개의 세션으로 진행되었습니다(오프닝과 키노트 발표는 제외). 크게는  인프런, 네이버와 같은 몽고디비 고객사 세션 / AWS, Confluent이 진행한 파트너 세션 / 몽고디비 자체 세션으로 나눠 진행되었습니다.
<img src="https://velog.velcdn.com/images/ryu_log/post/7d5bed61-c1c8-4cc4-9753-4bbd67f5c548/image.png" alt=""></p>
<p> 고객사 세션은 각 플랫폼에서 몽고디비 솔루션을 적용하고 도입한 경험을 공유했고,  파트너 세션의 경우 파트너사의 솔루션과 몽고디비가 함께 만들어 갈 수 있는 큰 차원의 방향을 제안했습니다. 마지막으로 몽고디비 자체 세션은 이번에 새롭게 release된 몽고디비 6.0을 중점으로, 개선되고 보완된 몽고디비 솔루션에 대한 소개를 진행했습니다. </p>
<p>각 세션에 대한 간략한 정리를 해보았습니다.
<strong>[고객사 세션 1] 시리즈 A 스타트업이 검색엔진으로 MongoDB Atlas Search를 선택한 이유</strong></p>
<ul>
<li><p>인프런이 가지고 있던 문제</p>
<ol>
<li>대부분의 기능을 Aurora for PostgreSQL에 성능에 의존 (DB엔진에 의존)<ul>
<li>검색 중 Full Text Search 가 발생하게 되면 데이터베이스 부하가 발생</li>
<li>부하 이후 시스템 장애로 이어질 수 있음</li>
</ul>
</li>
<li>인프런 프로젝트가 가지고 있는 레거시<ul>
<li>이것은 대부분의 스타트업에서 가지고 있는 레거시</li>
</ul>
</li>
<li>인프런의 검색 기능의 노후화<ul>
<li>DB 엔진에 의존한 검색의 한계</li>
</ul>
</li>
</ol>
</li>
<li><p>맨처음 도입하려고 했던 아키텍쳐</p>
<ul>
<li>기존 PG + Redis</li>
<li>검색엔진: AWS OpenSearch</li>
<li>비정형 데이터: AWS DynamoDB</li>
<li>하지만 이들을 모두 안정적으로 운영할 수 있을까에 대한 고민</li>
<li>최고의 아키텍쳐 보다는 지금 가장 효율적인 아키텍쳐로 비즈니스의 요구사항을 적시에 달성하는 것이 더 중요하다고 생각</li>
</ul>
</li>
<li><p>Atlas Search 도입 결과</p>
<ul>
<li>AWS Aurora 리소스 감소. 평균 50% → 20%</li>
<li>검색 성능이 증가됨에 따라, 검색 이후 실제 구매까지 이어지는 비율 증가(비즈니스적 성과)</li>
</ul>
</li>
<li><p>Atlas Search 단점</p>
<ul>
<li>부족한 커뮤니티와 레퍼런스</li>
<li>성능적인 부분에서 Elastic Search &gt; Atlas Search</li>
<li>로컬 테스트 환경 구성이 어려움</li>
<li>검색 텍스트가 어떻게 tokenizing 되는지 알 수가 없음</li>
</ul>
</li>
<li><p>Atlas Search 장점</p>
<ul>
<li>커뮤니티와 레퍼런스가 부족하지만 MongDB 서포트팀이 이러한 부분들을 매우 잘 지원해줌</li>
<li>인프런 백엔드 개발자들이 기본 node에 대한 이해가 있어서 상대적 익숙함</li>
<li>높은 SLA. 99.995% ← 1년에 서비스 다운타임이 26m 17s</li>
<li>형태소 분석기 성능</li>
</ul>
</li>
<li><p>인프런이 도입해보니..</p>
<ul>
<li>RDBMS만 사용하고 있고, RDMBS의 Full Text Search로 검색을 하면서 비정형 데이터 베이스 도입이 예정되어 있는 소규모 스타트업은 고려해볼만한 옵션</li>
<li>최소한의 도구로 다양한 문제들을 일정수준 이상으로 해결가능</li>
</ul>
</li>
</ul>
<p><strong>[고객사 세션 2] 네이버의 MongoDB 활용사례</strong></p>
<ul>
<li>몽고디비 서포터들이 네이버에서 잘 사용할 수 있게 도와줬다는 이야기</li>
</ul>
<p><strong>[파트너 세션 1]</strong> <strong>Confluent와 함께 분산된 데이터를 MongoDB로 연결하기</strong></p>
<ul>
<li>여러 레거시 시스템을 클라우드에 통합하는 것은 매우 복잡</li>
<li>전체 환경에서 레거시 데이터 시스템을 교체하거나 리팩토링 하는 것은 쉽지 않음. 그동안 데이터 가시성이 제한 될 수 있음</li>
<li>여러 데이터 사일로 및 데이터 형식 통합의 어려움</li>
<li>무튼 그래서 Confluent에서 제공하는 통합 솔루션을 사용하면 매우 편하게 합칠 수 있다..</li>
</ul>
<p><strong>[파트너 세션 2]</strong> <strong>MongoDB Atlas on AWS와 함께하는 MSA Journey</strong></p>
<ul>
<li>모놀리식 아키텍쳐의 한계<ul>
<li>성능 이슈</li>
<li>확장성의 한계</li>
<li>개발자들의 유연성 저해</li>
<li>개발 및 배포 지연</li>
</ul>
</li>
</ul>
<p><strong>[몽고디비 세션 1] MongoDB 6.0 새로운 기능</strong></p>
<ul>
<li><p>역대 몽고디비 주요 release 및 변화</p>
<ul>
<li>3.0 / 3.2: wired tiger 엔진 도입</li>
<li>3.4 / 3.6: MongoDB Atlas</li>
<li>4.0: 분산 환경에서의 ACID 도입</li>
<li>4.4: shard key 변경 가능</li>
</ul>
</li>
<li><p>몽고디비 6.0 주요 기능</p>
<ul>
<li>Time series collection<ul>
<li>시계열 컬렉션에 대한 성능과 정렬 개선</li>
<li>5.0 시계열 컬렉션 등장</li>
<li>5.2 버전부터 시계열 컬렌션에 열압축을 진행했었는데, 6.0에서 압축율 개선. i/o 성능 개선</li>
</ul>
</li>
<li>Relational Migratior<ul>
<li>rdb → mongo db 마이그레이션 솔루션</li>
<li>기존 db의 스키마를 분석하고, 이를 몽고디비에 적용하여 rdb에서 mongdo db로 데이터를 마이그레이션</li>
<li>지원 가능한 rdb: Oracle, MSSQL, MySQL, PostgresSQL</li>
</ul>
</li>
<li>Cluster to cluster sync<ul>
<li>기존 MongoDB 클러스터에서 다른 MongoDB 클러스터로 데이터를 복제 &amp; 동기화</li>
</ul>
</li>
<li>Atlas Search<ul>
<li>Atlas Search: Apache Lucence을 탑재한 search 솔루션</li>
<li>교차 검색, Facet등 성능 개선</li>
<li>전용 분석 노드 / 컬럼 인덱싱을 활용한 분석 워크로드에 대한 더 많은 유연성 제공</li>
</ul>
</li>
<li>Altas serverless</li>
<li>initial sync 성능 4배 개선. 더 빠른 속도로 더 나은 프로비저닝 가능<ul>
<li>initial sync: 몽고디비 primary node 데이터를 replica node로 옮기는 작업(초기 단계의 동기화)</li>
</ul>
</li>
<li>스토리지 엔진 개선</li>
<li>샤딩 환경에서 connection storm 개선</li>
</ul>
</li>
</ul>
<p><strong>[몽고디비 세션 2]</strong> <strong>MongoDB Atlas Search Deep-dive Session</strong></p>
<ul>
<li>DB엔진 vs 검색엔진<ul>
<li>검색 엔진<ul>
<li>검색에 최적화된 데이터 구조</li>
<li>식별자가 아닌 일반 언어를 통한 검색. 사용자가 무엇을 검색할지 알 수 없음</li>
</ul>
</li>
<li>DB 엔진<ul>
<li>BSON 형태의 데이터. MQL을 이용한 데이터 조회</li>
<li>미리 정의된 쿼리를 통한 운영 데이터 베이스 조회. 보통 식별자를 통한 조회 → 정확한 결과</li>
</ul>
</li>
</ul>
</li>
<li>MongoDB Atlas 내부에 DB엔진과 검색엔진이 함께 존재</li>
<li>데이터베이스(mongod)와 검색(mongot) 인덱스 간의 데이터 자동 동기화(데이터 아키텍쳐의 단순화)</li>
<li>데이터와 스키마가 변해도 이를 자동으로 변경</li>
<li>비즈니스 개발에 더욱더 집중 가능. 개발 생산성 증가</li>
<li>Atlas Search의 주요 개념<ul>
<li>Analyzer: 주어진 도큐먼트를 읽어 검색 가능한 term을 반환</li>
<li>Inverted Index: Analyzer가 생성한 Term들에 대해서 Index를 생성하고, 각각의 Term을 가지고 있는 도큐먼트들에 대한 목록을 유지</li>
<li>Score: 사용자가 검색한 질의가 얼만큼 연관이 있는지 표현. 사용자의 결과는 Score를 기반으로 정렬</li>
</ul>
</li>
</ul>
<h3 id="후기">후기</h3>
<p> 컨퍼런스 자체에서 큰 교훈이나 솔루션을 얻기는 쉽지 않습니다. 짧은 시간 동안 깊고 방대한 내용을 전달한다는 일차적인 한계가 가장 크고, 컨퍼런스의 내용이 현재의 문제와 어떤 관계가 있고, 도입 가능한 부분인가에서 오는 이차적인 한계가 있습니다. </p>
<p> 그럼에도 컨퍼런스에서 얻을 수 있는 것은 기술 변화의 큰 흐름을 따라 갈 수 있다는 것이 가장 크다고 생각합니다. 그리고 지금 당장은 고려되지는 않지만, 이후 만날 문제들에 대하여 컨퍼런스의 내용이 솔루션을 찾는 과정에서의 좋은 배경이자 은색총알이 될 수 있다고 생각합니다. </p>
<h3 id="기타-사진들-">기타 사진들 ..</h3>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/6c2d7f74-2403-4ed3-80a6-ba4132acf20f/image.png" alt="">
<img src="https://velog.velcdn.com/images/ryu_log/post/3981f3db-131d-4342-98b4-970acb684c88/image.png" alt="">
<img src="https://velog.velcdn.com/images/ryu_log/post/3ac3acfd-5a32-4af1-9785-e69ab2660044/image.png" alt="">
<img src="blob:https://velog.io/a03c0638-6a7c-49c6-91bd-916b51f48e93" alt="업로드중.."></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2022 파이콘 후기]]></title>
            <link>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-2022-%ED%8C%8C%EC%9D%B4%EC%BD%98-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-2022-%ED%8C%8C%EC%9D%B4%EC%BD%98-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sat, 15 Oct 2022 16:39:42 GMT</pubDate>
            <description><![CDATA[<h3 id="do-anything-with-python-">Do Anything with Python !</h3>
<p>Do Anything with Python 이라는 멋진 슬로건과 함께 열린 2022 파이콘에 다녀왔습니다. 10월 2일부터 3일까지 명동 커뮤니티 하우스 마실에서 진행되었고, 코로나 이후 3년만에 온/오프라인 동시에 진행되는 파이콘이니만큼 더욱 기대를 하고 다녀왔습니다. &quot;파이썬과 함께라면 모든 것을 할 수 있어!&quot; 라는 슬로건과 같이 타임테이블을 확인해보시면 서비스 웹 프레임워크, 블록체인, 게임 개발, 양자 컴퓨팅, 시각화 등 여러 주제가 있었고 그 외에도 파이썬 코드리뷰 꿀팁, 각종 라이브러리, ORM, MSA 등 파이썬과 관련한 여러 분야에서 다양한 세션이 열렸습니다.
<img src="https://velog.velcdn.com/images/ryu_log/post/6602f295-177f-48c5-9685-ac7cacc778d7/image.png" alt=""></p>
<h3 id="세션">세션</h3>
<p>개발 컨퍼런스가 처음이었기에 모두 설렜지만 가장 기대했던 것은 다양한 세션들이었습니다. 과거 (코로나 이전의) 파이콘 영상을 자주 보고는 했는데, django-orm이나 celery 등 실제 운영 환경에서 경험한 깊이 있는 내용을 자주 다뤘었기에 기대감이 컸습니다. 주니어 개발자로서 부족했던 내용에 대해 좀 더 탐구하고 싶었고, 다른 조직에서 먼저 경험한 내용을 간접 경험하고 싶은 마음이었습니다. 그리고 발표자와 청중 사이의 현장감도 저의 기대 포인트 중 하나였습니다.</p>
<p>아쉽게도 올 해 파이콘의 세션은 영상으로 대체 되었습니다. 그만큼 현장이 주는 몰입감이나 내용 전달의 아쉬움, 질의 응답이 없는 것에서 대한 많은 아쉬움이 남았습니다. 오히려 현장의 각종 커뮤니티 활동, 체험부스 등으로 인한 부산스러움 때문에 집중도가 많이 떨어졌습니다. 세션 중간중간에도 출입이 잦았고 세션 중간에 빔프로젝트 앞을 지나가시기도 하면서 어쩌면 동시 송출되고 있는 유튜브 영상을 보는 것이 내용 파악에 더 좋았을 수도 있겠다는 생각을 했습니다.
<img src="https://velog.velcdn.com/images/ryu_log/post/04df0638-0fe3-477c-a3c6-e83f02dd8480/image.png" alt="">
<img src="https://velog.velcdn.com/images/ryu_log/post/8d535a11-d7ca-4743-8366-dd6f14788a61/image.png" alt=""></p>
<h3 id="이벤트">이벤트</h3>
<p>세션은 나중에 유튜브에서 찾아보자! 마음 먹고 이후부터는 여러 이벤트에 참여했습니다. 이벤트는 다양했습니다. 파이콘 후원사에서 준비한 채용 설명회부터 도장 찍기, ox퀴즈, 토크 콘서트, 커뮤니티, 로또 추첨 등 혼자오더라도 부담없이 참여할 수 있는 이벤트가 많았습니다. 낯을 가려서인지 처음 참여한 개발 컨퍼런스였는지는 몰라도 제가 좀 더 적극적이었다면 얻어가는 것이 많았을텐데 하는 아쉬움이 남았습니다. (토크콘서트, 커뮤니티 등등)<br><img src="https://velog.velcdn.com/images/ryu_log/post/8b5539fb-2123-4ce4-827e-c206104d1497/image.png" alt="">
<img src="https://velog.velcdn.com/images/ryu_log/post/843a128e-7b04-434e-879a-52355ebfa6f3/image.png" alt="">
<img src="https://velog.velcdn.com/images/ryu_log/post/6061fb32-8ead-4fa3-856d-7f1e3dbbc40e/image.png" alt=""></p>
<h3 id="파이콘-속의-글또-✒️">파이콘 속의 글또! ✒️</h3>
<p>토크콘서트가 마무리되고 토크콘서트 호스트였던 호성님께서 글또에서 활동하시는 분들과 커피챗을 제안을 남겨주셔서 정말 큰 용기로 찾아가 이야기를 했었습니다. 간단한 자기소개와 함께 각자 살아가는 이야기를 했고, 인생 세컷으로 글또 모임 마무리 .. ㅎ
<img src="https://velog.velcdn.com/images/ryu_log/post/d63b2e77-3439-4287-84bd-a89b16389fe0/image.png" alt=""></p>
<h3 id="어쩌면-굿즈콘이었을-수도">어쩌면 굿즈콘이었을 수도..</h3>
<p>다양한 후원사에서 준비한 파이콘이니만큼 여러 굿즈들을 받았습니다. 유쾌한 스티커부터 텀블러, 무선충전기, 에코백, 휴대용 가습기 등등 이것저것 많이 챙길 수 있었습니다 😃
<img src="https://velog.velcdn.com/images/ryu_log/post/bc9c3871-89ed-41a1-84fb-6335b3c63a18/image.png" alt="">
<img src="https://velog.velcdn.com/images/ryu_log/post/c4db9256-9ed5-4723-9ef2-834b5583424b/image.png" alt="">
<img src="https://velog.velcdn.com/images/ryu_log/post/6ad0da79-1fa4-4bfb-a337-342323831468/image.png" alt=""></p>
<h3 id="마무리">마무리</h3>
<p>너무 기대한 탓(?)에 아쉬움은 남지만 즐거운 경험이었습니다. 전혀 몰랐던 부분들도 알게되었고, 따로 적용해보고 싶은 부분도 많았습니다. 처음 만나는 분들과 함께 이야기 할 수 있었고, 약간 리프레쉬 받은 느낌 ? 내년에도 오프라인으로 개최된다면 다시 한 번 참여해보고 싶네요 ! 그 때는 오프라인 세션이었으면 더 좋겠네요 😃  </p>
<h3 id="pycon의-이모저모">pycon의 이모저모!</h3>
<p>요기요 팀의 기술스택 홍보
<img src="https://velog.velcdn.com/images/ryu_log/post/05e36d96-3e19-4b15-b4d0-55885c5fb5b8/image.png" alt="">
파이콘 내 다양한 주제로 열린 커뮤티니 토크
<img src="https://velog.velcdn.com/images/ryu_log/post/aa7bf773-65bc-43af-a873-a5e47c7f95fa/image.png" alt="">
<img src="https://velog.velcdn.com/images/ryu_log/post/29b7497b-c433-44eb-a24f-c606b117dda1/image.png" alt="">
본부에서 챙겨주신 파이콘 요깃거리
<img src="https://velog.velcdn.com/images/ryu_log/post/afa28915-9295-4e2a-8650-58e7c821d6ad/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿼리가 인덱스를 타지 않았던 이유.. (feat. type /timezone casting)]]></title>
            <link>https://velog.io/@ryu_log/%EC%BF%BC%EB%A6%AC%EC%97%90-%EC%9D%B8%EB%8D%B1%EC%8A%A4%EB%A5%BC-%ED%83%9C%EC%9B%8C%EB%B3%B4%EC%9E%90-feat.-type-timezone-casting</link>
            <guid>https://velog.io/@ryu_log/%EC%BF%BC%EB%A6%AC%EC%97%90-%EC%9D%B8%EB%8D%B1%EC%8A%A4%EB%A5%BC-%ED%83%9C%EC%9B%8C%EB%B3%B4%EC%9E%90-feat.-type-timezone-casting</guid>
            <pubDate>Sun, 02 Oct 2022 13:26:56 GMT</pubDate>
            <description><![CDATA[<h3 id="개요">개요</h3>
<p>하루에 한 번, 전 날의 데이터를 AWS S3로 올리는 데이터 서빙 작업이 있습니다. 데이터의 수가 그렇게 많지 않음에도 아래와 같이 순간적인 부하가 발생했습니다. 아래의 그라파나 모니터링 지표와 slack alert를 통해서 확인할 수 있습니다.</p>
<ul>
<li>그라파나 모니터링 지표
<img src="https://velog.velcdn.com/images/ryu_log/post/a2c4f62e-5424-4b3d-926d-9a89ef65ea6c/image.png" alt=""></li>
<li>쿼리 로컬 테스트 부하 경보
<img src="https://velog.velcdn.com/images/ryu_log/post/62d3baa9-589a-4c82-8f67-d62b927eec7f/image.png" alt=""></li>
</ul>
<p>분석을 위한 데이터베이스이기 때문에 순간적인 부하가 전체 서비스에 대한 영향을 미치는 것은 아니지만, 쿼리 대비 예상보다 큰 부하가 발생한 점, 향후 데이터가 늘어날 것을 대비하여 이번 기회에 이를 개선하고 원인을 탐색해보았습니다.</p>
<h3 id="as-is-query">as-is query</h3>
<p>데이터를 서빙하게 위해 사용했던 쿼리는 매우 간단합니다. 데이터 서빙 작업 시간을 기준으로 어제 날짜의 데이터를 모두 조회합니다. 이 때 조건에 되는 timestamp 컬럼에 대한 timezone을 UTC에서 KST로 timezone casting을 진행했고, timestamp에서 date로 type casting을 진행했습니다.</p>
<pre><code class="language-sql">SELECT * FROM public.table WHERE (timestamp at time zone &#39;Asia/Seoul&#39;)::timestamp::date = &#39;2022-09-16&#39;</code></pre>
<h3 id="postgresql-explain">Postgresql EXPLAIN</h3>
<p>postgresql은 <code>EXPLAIN</code>이라는 쿼리 플랜 및 성능 측정 기능을 제공합니다. <code>EXPLAIN</code>을 사용하면 쿼리를 진행하는데 발생하는 예상 코스트와 탐색에 필요한 기본적인 플랜을 확인 할 수 있습니다. 
<img src="https://velog.velcdn.com/images/ryu_log/post/fe1b2165-8513-4f21-964c-1524c9fe94fc/image.png" alt=""></p>
<h3 id="인덱스를-타지-않는다-">인덱스를 타지 않는다.. ?</h3>
<p>제가 가장 의아한 부분은 QUERY PLAIN의 3번 째 행을 확인해보면 <code>Parallel Seq Scan</code> 입니다. 이는 테이블 풀 스캔을 하며 조회가 진행되고 있다는 것입니다. 하지만 조회하는 테이블은의 경우 timestamp 컬럼에 시계열 조회에 특화된 brin index가 설정되어 있습니다. 당연히 설정된 인덱스를 타면서 쿼리가 효율적으로 진행될 줄 알았는데 이상하게 테이블을 풀 스캔하며 조건에 해당 하는 데이터를 조회하고 있었습니다.</p>
<h3 id="인덱스를-타지-않는-이유">인덱스를 타지 않는 이유</h3>
<p>우선적으로 쉽게 발견한 이유는 <code>인덱스 컬럼의 내부적인 데이터 변환을 진행하면 설정한 인덱스를 타지 않는다</code> 는 것이었습니다. 아마 위의 쿼리에서 인덱스가 설정되어 있는 timestamp 컬럼을 date로 변경하면서 발생했다고 생각했습니다. (참고로 이외에도 여러 이유로 쿼리가 인덱스를 타지 않을 수 있습니다. 자세한 내용은 <a href="https://hckcksrl.medium.com/index%EB%A5%BC-%ED%83%80%EC%A7%80%EC%95%8A%EB%8A%94-%EC%BF%BC%EB%A6%AC-41f0417bfe03">Index를 타지않는 쿼리</a> 포스팅을 참고하시면 좋을 것 같습니다.)</p>
<h3 id="재도전">재도전</h3>
<p>희망찬 마음에 date로 형변환 되는 부분을 제거하고 BETWEEN을 통해 동일한 결과의 쿼리로 변경했습니다.</p>
<pre><code class="language-sql">SELECT * from public.table WHERE (timestamp at time zone &#39;Asia/Seoul&#39;)
BETWEEN &#39;2022-09-16 00:00:00&#39; and &#39;2022-09-16 23:59:59&#39;</code></pre>
<p>하지만 여전히 DB부하는 발생했고, postgresql EXPLAIN의 결과 또한 같았습니다. 여전히 Parallel Seq Scan를 진행되었으며 인덱스를 타지 않고 테이블 풀 스캔을 통해 데이터를 조회하고 있었습니다.
<img src="https://velog.velcdn.com/images/ryu_log/post/cfca519b-78ed-4fb0-b894-6e1ea6ef4046/image.png" alt=""></p>
<h3 id="그렇다면-남은-것은--">그렇다면 남은 것은 .. ?</h3>
<p>인덱스가 설정된 컬럼에 대한 형 변환도 제거했지만 지속적으로 비효율적인 쿼리가 발생했습니다. 정렬을 다시해보는 등 여러가지 시도를 해보았지만 결과적으로 인덱스를 타지 않았습니다. 그러면서 발견한 것이 <code>timezone</code> 변경이었습니다. </p>
<p>현재 저희 서비스 데이터베이스에는 별도의 <code>timezone</code>이 설정되지 않은 UTC의 시간대로 <code>timestamp</code>를 저장하고 있습니다. 따라서 쿼리 할 때는 <code>timezone</code> 부여하면서 보다 사용자 친화적인 쿼리를 하곤 했습니다. </p>
<p>혹시나 하는 마음에 <code>timezone</code> 변경을 제외하고 쿼리를 진행했더니 인덱스를 타는 EXPLAIN 결과를 얻을 수 있었습니다.</p>
<pre><code class="language-sql">select * from public.table where timestamp
between &#39;2022-09-16 00:00:00&#39; and &#39;2022-09-16 23:59:59&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/f4e51b36-98de-46c2-9009-4487928b7ad8/image.png" alt=""></p>
<h3 id="타임존-변경-쿼리가-인덱스를-타지-않았던-이유">타임존 변경 쿼리가 인덱스를 타지 않았던 이유</h3>
<p>pg 상에서는 시간을 저장하는 타입은 크게는 네 가지, 작게는 두 가지가 있습니다.</p>
<blockquote>
<p>time [without timezone]
time [with timezone]
timestamp [without timezone]
timestamp [with timezone]</p>
</blockquote>
<p>우선 이번 이슈에서는 별도의 time 타입을 사용하지 않으니 제외하겠습니다. 중요한 것은 <code>timezone</code>을 포함하냐, 하지 않느냐 입니다. <code>timezone</code> 포함여부에 따라 데이터 타입이 달라집니다. (이제 보니 <code>timezone</code>을 포함한 <code>timestamp</code>는 <code>timestampz</code> 라고 표기..)</p>
<p>현재 저희가 사용하는 <code>timestamp</code>는 별도의 <code>timezone</code>이 설정되지 않는 <code>timestamp[without timezone]</code> 였고, 일반적으로 쿼리할 때 <code>timestamp</code> 컬럼에 대해서 <code>at time zone</code>을 통해 타임존을 UTC -&gt; KST로 <code>timezone</code>을 부여하여 쿼리를 진행했습니다. 여기서 놓쳤던 부분이 timezone을 부여되게 되면 데이터 타입이 <code>timestamp[without timezone]</code>에서<code>timestamp[with timezone]</code>로 형 변환이 진행 된다는 것이었습니다.</p>
<p>결국 timestamp를 date으로 데이터 타입을 변경해 인덱스가 타지 않았던 것처럼, timestamp 컬럼에 timezone을 부여하면서 timestampz로 데이터 타입이 변경되면서 인덱스를 타지 않았던 것이었습니다.</p>
<h3 id="마무리">마무리</h3>
<p>결과적으로 쿼리 시간도 약 60초에서 6초로 1/10으로 줄어들었고, DB부하도 기존 60%정도에서 20%정도로 큰 성과를 얻을 수 있었습니다. 효율을 높이기위해 선택했던 인덱스를 이제서야 제대로 활용하다니.. !이런 발견을 할 수 있음에 뿌듯하면서도 아쉬웠던 배움이었습니다 </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python @property]]></title>
            <link>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-Python-property</link>
            <guid>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-Python-property</guid>
            <pubDate>Sun, 04 Sep 2022 11:53:06 GMT</pubDate>
            <description><![CDATA[<p>파이썬에는 @property라는 데코레이터가 있습니다. @property를 사용하면 보다 pythonic 하게 코드를 작성할 수 있다는 장점이 있습니다. 또 어떤 상황에서 파이썬 @property 데코레이터를 사용하면 좋을까요 ?</p>
<p>@property를 알아가기전 접근제한자와 접근제한자가 등장한 배경인 캡슐화에 대해 간단하게 짚고 넘어가겠습니다.</p>
<h3 id="캡슐화와-정보은닉">캡슐화와 정보은닉</h3>
<blockquote>
<p>💡 캡슐화(Encapsulation): 객체의 속성과 행위를 하나로 묶고 실제 구현 내용 일부를 외부에 감추어 은닉</p>
</blockquote>
<p>💊 캡슐화는 OOP의 특징 중 하나이며, 객체가 독립접으로 역할을 수행하기 위해 필요한 데이터와 기능을 한 곳에 패키징 한 것을 의미합니다. 패키징 된 다양한 기능은 캡슐과 같이 감싸져 있어 외부에서는 객체의 내부 구현을 알기 어렵습니다</p>
<p>💊 따라서 객체가 외부 공개를 허용한 일부의 인터페이스를 통해서만 객체에 접근 할 수 있습니다. 이를 통해 외부에 의한 중요 정보 탈취 및 무차별적인 객체 손상을 방지함으로써 정보은닉이 실현될 수 있습니다</p>
<p>💊 캡슐화를 통해 정보은닉이라는 하나의 목적을 달성하기 위해 <code>접근제한자</code> 를 이용하게 됩니다</p>
<h3 id="접근제한자access-modifier">접근제한자(Access Modifier)</h3>
<p>🔒 접근제한자는 객체 정보은닉을 달성하기위해 객체에 존재하는 속성(필드)와 기능(메서드)에 대한 접근을 제한하는 역할을 수행합니다</p>
<p>🔒 접근제한자는 크게 <code>private</code>, <code>default</code>, <code>protected</code>, <code>public</code> 4가지가 있습니다. 일반적으로는 완전공개(public)와 완전비공개(private)를 주로 사용합니다. 자바의 경우 객체의 속성이나 매서드 앞에 접근제한자를 붙힘으로써 접근 제한 여부를 나타냅니다.</p>
<table>
<thead>
<tr>
<th align="center">구분</th>
<th align="center">공개부분</th>
<th align="center">접근불가능</th>
</tr>
</thead>
<tbody><tr>
<td align="center">public</td>
<td align="center">같은 클래스 내에서 접근 가능</td>
<td align="center">없음</td>
</tr>
<tr>
<td align="center">default</td>
<td align="center">패키지 공개</td>
<td align="center">자식 패키지 + 다른 패키지</td>
</tr>
<tr>
<td align="center">protected</td>
<td align="center">패키지 공개 + 자식 패키지</td>
<td align="center">다른 패키지</td>
</tr>
<tr>
<td align="center">private</td>
<td align="center">같은 클래스 공개</td>
<td align="center">다른 패키지, 다른 클래서</td>
</tr>
<tr>
<td align="center">🔒  private로 보호된 필드는 외부에서 접근이 불가능합니다. 따라서 getter/setter method를 공개하여 외부에서 메소드를 통해 접근하도록 유도하여, 데이터를 우회하여 가져오거나 변경하며 객체의 무결성을 유지할 수 있습니다</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">🔒 특히 setter의 경우, 내부 속성의 값을 다시 설정할 때 특정 조건을 두어 재설정에 대한 안정성을 높힐 수 있습니다</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">### python의 접근제한자</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">🔐 파이썬에는 별도의 접근제한자가 없지만(?) 있습니다(?)</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">🔐 public, private와 같은 명시적인 접근제한자는 없지만 네이밍 컨벤션을 통해 접근 여부를 표기합니다.</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">```python</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">def <strong>init</strong>(self):</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">self.public_property = 1</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">self._protected_property = 1</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">self.__privated_property = 1</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">```</td>
<td align="center"></td>
<td align="center"></td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th align="center">구분</th>
<th align="center">공개부분</th>
<th align="center">접근불가능</th>
</tr>
</thead>
<tbody><tr>
<td align="center">public_property</td>
<td align="center">외부에서 접근 가능</td>
<td align="center">없음</td>
</tr>
<tr>
<td align="center">_protected_property</td>
<td align="center">자기 자신 클래스와 상속된 클래스에서 사용 가능</td>
<td align="center">없음</td>
</tr>
<tr>
<td align="center">__privated_property</td>
<td align="center">자기 자신 클래스</td>
<td align="center">외부에서 접근 불가능. 접근시 에러 발생</td>
</tr>
</tbody></table>
<p>🔐 파이썬도 마찬가지로 접근 제한자가 지정되어 있는 변수들에 대한 getter/setter method를 생성하여 객체 내부의 속성을 읽거나 수정할 수 있습니다.</p>
<pre><code class="language-python"># getter / setter 선언
class GenG:
    def __init__(self):            # 객체 초기화
        self.__jungle = &#39;peanut&#39;

    def get_jungle(self):
        return self.__jungle

    def set_jungle(self, value):
        self.__jungle = value

# getter / setter 호출
gen_g = GenG()
print(gen_g.get_jungle()) # peanut

gen_g.set_jungle(&#39;youngjae&#39;)
print(gen_g.get_jungle()) # youngjae
</code></pre>
<h3 id="python의-property">Python의 @property</h3>
<p>📍 파이썬의 @property 데코레이터는 getter/setter를 좀 더 우아하고 간결하게 표현할 수 있습니다</p>
<pre><code class="language-python">class GenG:
    def __init__(self):            # 객체 초기화
        self.__jungle = &#39;peanut&#39;

    @property
    def jungle(self):
        return self.__jungle

    @jungle.setter
    def jungle(self, value):
        if not getattr(value, str):
            raise &#39;타입이 맞지 않습니다&#39;
        self.__jungle = value

# getter / setter @property 호출
gen_g = GenG()
print(gen_g.jungle) # peanut

gen_g.jungle = &#39;youngjae&#39;
print(gen_g.jungle) # youngjae</code></pre>
<p>📍 기존 getter / setter과의 차이가 보이시나요 ? </p>
<ul>
<li>기존 getter / setter를 사용했을 때는 메소드()와 같이 getter / setter를 호출했다면 ex) 객체.get_jungle()</li>
<li>@property 데코레이터가 추가된 getter / setter의 경우 객체의 프로퍼티에 접근하듯이 내부 값을 호출합니다 ex) 객체.jungle</li>
</ul>
<p>📍 여기서 주의할 점은 @property 가 @getter.setter보다 위에 있어야합니다</p>
<h3 id="그렇다면-property를-어떻게-활용하면-좋을까요-">그렇다면 @property를 어떻게 활용하면 좋을까요 ?</h3>
<p>🖋 기본적으로 @property의 목적은 객체의 프로퍼티에 직접 접근을 제한하기 위해 사용합니다</p>
<p>🖋 @property를 잘 활용하면 코드의 재활용성과 가독성이 높아지고, 유지보수가 용이합니다</p>
<p>🖋 코드를 작성하다보면 자연스럽게 조건 분기를 타는 경우가 많습니다. 하지만 조건 분기가 많아질수록 코드의 가독성이 떨어질 수 있는데 이때 @property를 잘 활용할 수 있습니다</p>
<p>🖋 2개 이상의 조건이나 단순 3항연산자로 표현하기에는 길어지거나, 그 의미를 파악하기 어려울 경우에 @property를 활용하면 보다 깔금한 코드를 작성할 수 있습니다</p>
<p>🖋 복잡한 연산이지만 별도의 매개변수 없이 객체 내부의 속성의 값으로만 연산하여 값을 도출 할 수 있을 때 @property를 활용하면 보다 직관적으로 나타낼 수 있습니다</p>
<h3 id="참고자료">참고자료</h3>
<ul>
<li><a href="http://selab.gnu.ac.kr/oop/basics/review/Review03_%EC%BA%A1%EC%8A%90%ED%99%94%EC%99%80%EC%A0%95%EB%B3%B4%EC%9D%80%EB%8B%89.pdf">http://selab.gnu.ac.kr/oop/basics/review/Review03_%EC%BA%A1%EC%8A%90%ED%99%94%EC%99%80%EC%A0%95%EB%B3%B4%EC%9D%80%EB%8B%89.pdf</a></li>
<li><a href="https://jminie.tistory.com/m/46">https://jminie.tistory.com/m/46</a></li>
<li><a href="https://velog.io/@kksh1205/python-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EA%B3%BC-property-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0">https://velog.io/@kksh1205/python-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EA%B3%BC-property-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/lib/python3.8/site-packages/tzdata/zoneinfo/Asia/Hanoi' 에러]]></title>
            <link>https://velog.io/@ryu_log/FileNotFoundError-Errno-2-No-such-file-or-directory-usrlocallibpython3.8site-packagestzdatazoneinfoAsiaHanoi-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@ryu_log/FileNotFoundError-Errno-2-No-such-file-or-directory-usrlocallibpython3.8site-packagestzdatazoneinfoAsiaHanoi-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Fri, 12 Aug 2022 18:35:27 GMT</pubDate>
            <description><![CDATA[<p>Django상에서 배포를 하려고하니 아래와 같은 에러가 발생했습니다.</p>
<blockquote>
<p>FileNotFoundError: [Errno 2] No such file or directory: &#39;/usr/local/lib/python3.8/site-packages/tzdata/zoneinfo/Asia/Hanoi&#39;</p>
</blockquote>
<blockquote>
<p>ZoneInfoNotFoundError(f&quot;No time zone found with key {key}&quot;</p>
</blockquote>
<p>pytz 버전이 2022.1에서 2022.02로 업데이트 되어 있는데, 새롭게 배포된 버전에서 해당 에러가 발생하고 있습니다.
Django 프로젝트의 경우, pytz 라이브러리에 의존성을 가지고 있어 별도의 버전 설정을 안하는 경우가 많은데 이럴 경우 이번과 같은 에러를 경험할 수 밖에 없습니다.<br>이럴 때는 강제적으로 requirements에 pytz==2022.1과 같이 이전 버전을 추가하고 다시 배포를 진행하면 문제없이 정상 배포가 진행되는 것을 확인했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MongoDB Create Index]]></title>
            <link>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-MongoDB-Create-Index</link>
            <guid>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-MongoDB-Create-Index</guid>
            <pubDate>Sat, 06 Aug 2022 16:57:50 GMT</pubDate>
            <description><![CDATA[<h2 id="0-개요">0. 개요</h2>
<p>지난 수요일 15분 간 전체 서비스가 중단되는 장애가 발생했습니다. 원인은 몽고디비의 컬렉션에 인덱스를 추가하는 과정에서 primary 노드 컬렉션에 인덱스가 추가될 때 primary 노드에 대한 모든 요청에 대한 Write 작업이 중단되었고 서비스앱과 여러 서비스들이 몽고디비의 요청에 응답받지 못해 발생한 장애였습니다.(자세한 내용은 밑에 정리를 해두었습니다)</p>
<p>이번 계기를 통해 몽고디비에서의 인덱스와 foreground / background / hybrid 인덱스 빌드, 롤링 인덱스 빌드 등에 대한 개념을 정리해보려고 합니다.</p>
<h2 id="1-mongodb">1. MongoDB</h2>
<p>몽고 DB는 관계형 데이터베이스(relational database)가 아닌 도큐먼트 지향 데이터베이스(document-oriented database) 입니다. 도큐먼트 지향 데이터베이스에서는 RDB의 행(row) 개념 대신 도큐먼트(document)를 사용하는데 이때 도큐먼트의 키와 값의 타입을 미리 정의하지 않습니다. 한마디로 고정된 스키마가 없고, 그렇기에 필요에 따라 쉽게 키를 추가하거나 제거할 수 있습니다.</p>
<p>몽고 DB의 장점은 도큐먼트 지향 데이터 모델을 통한 높은 확장성과 다양한 인덱스와 고성능, ReplicaSet을 통한 높은 가용성 등이 있습니다.</p>
<p>몽고 DB의 데이터베이스의 주요 개념 및 계층은 아래와 같습니다(RDB와 비교)
<img src="https://velog.velcdn.com/images/ryu_log/post/2d66abda-19ee-4942-a3a7-ff91d5c281b9/image.png" alt=""></p>
<h2 id="2-mongodb-index와-성능">2. MongoDB Index와 성능</h2>
<p>인덱스는 몽고 DB의 핵심 기능입니다. 고성능을 지향하는 몽고 DB에서 각 컬렉션에 적합한 인덱스를 선택하는 것은 그 목적을 달성하는데 큰 영향을 미치며 데이터가 많아질수록 그 진가가 크게 발휘됩니다. 기본적으로 모든 컬렉션은 생성될 때 기본적으로 <code>_id</code> 필드에 대한 인덱스가 부여됩니다(<code>_id</code>는 몽고 DB의 모든 도큐먼트가 생성될 때 부여되는 값으로 같은 <code>_id</code>를 가진 문서를 중복적으로 추가하는 것을 방지). 하나의 컬렉션에 최대 64개의 인덱스까지 생성가능합니다.</p>
<p>쿼리 성능적인 부분에서 인덱스는 중요한 개념이지만 인덱스를 사용하는데는 유지비용이 발생합니다. 데이터가 변경될 때마다 몽고 DB는 모든 인덱스를 갱신합니다. 따라서 쓰기 작업이 빈번한 컬렉션에서의 복잡한 인덱스 설계는 오히려 성능을 저하시킬 수도 있습니다. 또한 몽고디비의 인덱스는 메모리에 저장되어 있습니다. 하나의 인덱스는 16kb의 크기를 가지기 때문에 인덱스를 무수히 많이 생성한다면 쿼리 성능 저하를 유발할 수 있습니다.</p>
<p>따라서 데이터베이스와 실행할 쿼리 종류등을 고려하여 인덱스를 잘 설계해야합니다. </p>
<h2 id="3-mongodb-index-추가">3. MongoDB Index 추가</h2>
<p>몽고 DB에서 인덱스를 생성하는 방법은 아래와 같습니다. 인덱스 생성을 희망하는 데이터베이스.컬렉션에 원하는 도큐먼트 키값과 정렬 방향을 value값에 넣으면 컬렉션에 인덱스가 생성됩니다. 새로운 인덱스 구축은 많은 시간과 많은 리소스를 요구합니다. 그렇기에 몽고 DB 4.2 버전 이전에는 인덱스를 최대한 빨리 구축하기 위해 컬렉션의 모든 읽기와 쓰기작업을 중단(Exclusive)시켰습니다.</p>
<h3 id="31-foreground-index-build">3.1 foreground index build</h3>
<p> 따라서 아래의 쿼리처럼 별도의 옵션을 주지 않은 채 인덱스를 생성한다면 진행 컬렉션에 대한 global lock이 잡히며 모든 요청이 중단될 것입니다. </p>
<pre><code class="language-json">&lt;database&gt;.&lt;collection&gt;.createIndex({&quot;&lt;indexKey&gt;&quot;: 1})</code></pre>
<h3 id="32-background-index-build">3.2 background index build</h3>
<p>background 옵션을 추가해주며 global lock을 피할 수가 있습니다. background 옵션을 추가해준다면 작업 중인 collection으로 요청이 있다면 인덱스 생성 잠시 중단되었다가 요청 완료 후 다시 인덱스를 생성합니다. </p>
<pre><code class="language-json">&lt;database&gt;.&lt;collection&gt;.createIndex({&quot;&lt;indexKey&gt;&quot;: 1}, {&quot;background&quot;: true})</code></pre>
<p>따라서 동시 사용성이 증가하지만 foreground 방식에 비해 느리다는 단점이 있습니다. 무엇보다도 몽고 DB의 경우, 메모리 성능이 전체적인 퍼포먼스에 영향을 주게 되는데, 인덱스 생성 작업은 서버에 메모리를 추가적으로 사용하는 작업입니다. 따라서 background 옵션을 통해 인덱스 빌드를 진행할 때는 메모리 모니터링을 함께 진행하는 것이 안정성 측면에서 이점이 있습니다. 만약 인덱스를 추가하는 작업으로 인한 메모리 부족현상이 나타난다면 장시간 성능저하와 지연 현상이 장시간 발생할 수도 있습니다. </p>
<h3 id="33-hybrid-index-build">3.3 hybrid index build</h3>
<p>몽고 DB의 createIndex 명령어는 4.2 버전과 4.4 버전에서 많은 부분 변화했는데, 4.2 버전에서는 인덱스 추가 프로세스 과정에서 시작과 끝에서만 컬렉션에 대한 Exclusive(W) 잠금을 유지하는 최적화 된 빌드 프로세스를 채택했습니다. 이러한 hybrid 인덱스 생성 방식은 이전의 background 인덱스 생성의 성능을 보장하면서 lockless합니다. 4.4 버전에서는 모든 ReplicaSet에서 인덱스가 동시에 빌드됩니다. </p>
<h2 id="4-rolling-index-build">4. Rolling index build</h2>
<p>이번 인덱스 추가 작업은 CLI 명령어가 아닌, mongoDB atlas 웹 상에서 제공하는 인덱스 추가 기능을 사용했습니다. 현재 mongoDB atlas cloud 상에서 4.2.xx 버전를 사용하고 있는데, 인덱스 추가 작업 진행 시 rolling index build 옵션이 있어 선택을 한 후 진행을 했습니다. 현재 저희의 mongo cluster는 P-S-S로 구성되어 있고, 클러스터에 인덱스를 추가할 때는 rolling index build를 추천한다고 하여 진행을 하게 되었습니다.</p>
<p>롤링 인덱스 빌드는 replica node(secondary) 부터 시작하여 각 노드별로 인덱스 빌드를 독립적으로 실행합니다. 각 노드별로 인덱스를 빌드할 때는 해당 컬렉션에 대한 모든 쓰기를 중지됩니다. 아마 이번 장애가 primary node 인덱스 빌드 진행될 때 전체의 쓰기 작업이 중지 되면서 장애가 발생했을거라는 추측을 하게되었습니다. </p>
<p>롤링 인덱스 빌드 과정은 아래와 같습니다. </p>
<ol>
<li>replica node(secondary) 중 하나를 중단하고 stand-alone으로 독립 실행하여 재 실행</li>
<li>새로운 인덱스 추가</li>
<li>replica set에 stand-alone node를 새로운 구성원으로 추가하고 다시 시작</li>
<li>남은 replica node 에 대한 1~3번 과정 반복</li>
<li>primary node 인덱스 빌드</li>
</ol>
<h2 id="5-mongodb-index-삭제">5. MongoDB Index 삭제</h2>
<pre><code class="language-json">&lt;database&gt;.&lt;collection&gt;.dropIndex({&quot;&lt;indexKey&gt;&quot;: 1})
&lt;database&gt;.&lt;collection&gt;.dropIndexes()</code></pre>
<p>dropIndex()를 통해 인덱스 명을 지정하여 해당 인덱스를 삭제할 수 있습니다. dropIndexes()는 모든 인덱스를 삭제 할 수 있습니다. 단, <code>_id</code> 인덱스는 삭제 되지 않습니다. 4.4 버전 부터는 dropIndexes() 에서는 생성중인 인덱스도 중지 시킬 수 있습니다. </p>
<p>이전 버전에서는 database.currentOp() 를 통해 현재 실행되고 있는 리스트를 출력한다음, 종료하고자하는 op의 id값을 가져와서 database.killOp(op_id)를 진행하면 됩니다.</p>
<h2 id="6-마무리">6. 마무리</h2>
<p>정말 길었던 15분이었습니다. 다행히 빠르게(?) 복구되어 더 큰 문제는 없었지만 인덱스를 생성할 때 좀 더 신중하게 결정해야겠다는 큰 배움이 있었습니다.  </p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://kkyunstory.tistory.com/67">https://kkyunstory.tistory.com/67</a></li>
<li><a href="https://rastalion.me/mongodb-lock-%EC%9E%A0%EA%B8%88/">https://rastalion.me/mongodb-lock-잠금/</a></li>
<li><a href="https://www.mongodb.com/docs/manual/core/index-creation/#comparison-to-foreground-and-background-builds">https://www.mongodb.com/docs/manual/core/index-creation/#comparison-to-foreground-and-background-builds</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django ORM]]></title>
            <link>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-Django-ORM</link>
            <guid>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-Django-ORM</guid>
            <pubDate>Sun, 24 Jul 2022 11:54:15 GMT</pubDate>
            <description><![CDATA[<h2 id="django">Django</h2>
<p>Django는 파이썬 기반 웹프레임워크이며, 파이썬 백엔드 웹 프레임워크 생태계 중에서 중 가장 높은 점유율을 차지하고 있습니다. 기본적으로 admin 패널, auth, django template 등을 제공하며 Django와 연계된 다양한 서드파티 패키지들이 매우 많아 높은 확장성으로 빠르고 안정적으로 추가 기능을 개발할 수 있다는 장점이 있습니다. 또한 손에 꼽을 정도로 큰 큐모의 커뮤니티가 존재하며 상세한 공식 문서를 통해 필요한 정보를 빠르게 취득할 수 있습니다. 이외에도 여러가지 장점이 있지만 저는 Django의 가장 큰 장점은 ORM이라고 생각합니다. </p>
<h2 id="orm">ORM</h2>
<p>ORM이란 Object Relational Mapping의 약자로 객체와 관계형 데이터 베이스의 데이터를 자동으로 연결해주는 것을 말합니다. ORM을 통해 개발자는 코드 상에서 객체를 통해 실제 데이터 베이스를 조작할 수 있습니다. 대부분의 프로그래밍 아케텍쳐는 낮은 결합도와 높은 응집도를 지향합니다. 모듈과 모듈간의 의존정도에 따라 유지보수와 운영의 관점에서 투입해야하는 리소스가 다르기 때문에 느슨하게 연결되어야 효율적이라고 볼 수 있습니다.</p>
<p>ORM을 통해 개발자는 서버 애플리케이션과 데이터베이스 사이의 낮은 결합도(느슨한 연결)을 지향할 수 있습니다. ORM은 서버 애플리케이션과 데이터베이스 사이에 들어간 추상화 계층이며 결합도를 낮추는 가장 직접적인 방법이며 입니다. 만약 ORM 없이 데이터베이스와 강하게 결합된 상황이라면 간단한 스키마 수정에도 광역적인 변경이 발생할 수 있습니다. 꼭 ORM이 아니더라도 추상화를 구현할 수 있지만 이미 많은 개발자들이 사용하며 검증된 ORM을 사용하지 않을 이유는 없습니다.</p>
<h2 id="queryset">QuerySet</h2>
<p>QuerySet 이란 한마디로 전달받은 객체의 목록이며, ORM을 통해 가져온 데이터베이스의 row를 말합니다. 이반적으로 리스트와 구조는 같지만 파이썬의 기본 자료구조가 아니기 때문에 파이썬에서 이를 이용하기 위해서는 자료형 변환을 해주어야 합니다.</p>
<h2 id="lazy-loading지연-로딩">Lazy Loading(지연 로딩)</h2>
<p>지연 로딩이란 한 마디로 필요한 시점에만 쿼리를 날리는 개념입니다. 데이터가 필요하지 않다면 가져오지 않음을 말하며 이를 Django QuerySet의 개념을 통해 설명하면, QuerySet이 <strong>평가</strong>될 때 실제 SQL은 동작합니다. 예를 들어 아래와 같은 상황에서도 실질적인 SQL은 1번 밖에 발생하지 않습니다.</p>
<pre><code class="language-python">users = User.objects.all()
orders = Order.objects.all()
companies = Company.objects.all()
list(users)</code></pre>
<p><a href="https://docs.djangoproject.com/en/4.0/ref/models/querysets/#when-querysets-are-evaluated">Django 공식문서</a>에도 QuerySet 평가와 관련된 내용이 설명되어 있는데, QuerySet 평가는 아래 이미지와 같이 7개의 상황에서만 평가가 진행됩니다. (반복 (Iteration), 슬라이싱 (Slicing), 피클링 / 캐싱 (Pickling/Caching), repr(), len(), list(), bool())</p>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/ac76a265-6eef-4664-8163-2a49fe32b417/image.png" alt=""></p>
<p>Lazy Loading은 크게 두 가지의 문제를 가지고 있는데 중복쿼리 가능성과 N+1 문제입니다.</p>
<h3 id="lazy-loading-단점-1-비효율성">Lazy Loading 단점 1. 비효율성</h3>
<p>예를 들어 아래와 같은 Django ORM 코드가 있다면, 위에서 말한 두 가지 QuerySet 평가가 동작하도 2개의 SQL이 동작하게 됩니다.</p>
<pre><code class="language-python">users = User.objects.all()

first_user = users[0] # &lt;- LIMIT 1 인 SQL 동작 (slicing)
user_list = list(users) # &lt;- 전체 User 데이터를 가져오는 SQL 동작 (list)</code></pre>
<p>위처럼 1번 째 유저 목록을 가져오기 위해 슬라이싱을 통한 쿼리셋 평가가 진행되면 LIMIT 1이 포함된 SQL이 호출되고, 전체 유저의 목록을 가져오기 위해 리스트를 통한 쿼리셋 평가가 진행되면 앞의 쿼리를 재사용하지 않고 불필요한 SQL이 호출되게 됩니다.</p>
<p>이러한 문제는 QuerySet 캐싱을 사용하며 보완할 수 있습니다. QuerySet을 캐싱해두고 필요한 정보를 가져올 때 앞에 선언한 변수를 확용하면 추가 쿼리를 호출하지 않습니다. 간단하게 순서만 변경하는 것으로도 Lazy Loading의 단점을 보완할 수 있습니다. </p>
<pre><code class="language-python">users = User.objects.all()

user_list = list(users) # &lt;- user_list 변수에 유절 목록 캐싱
first_user = user_list[0] # &lt;- 추가 쿼리 발생하지 않음</code></pre>
<h3 id="lazy-loading-단점-2-n1-문제">Lazy Loading 단점 2. N+1 문제</h3>
<p>Lazy Loading은 쿼리를 날릴 때, 참조모델(fk, many-to-many)의 데이터는 가지고 오지 않습니다. 단순히 해당 모델이 가지고 있는 필드만을 가지고옵니다. 그렇기 때문에 현재의 모델에서 외래키 관계의 모델을 호출할 때마다 추가 쿼리가 발생하게 됩니다. </p>
<pre><code class="language-python">posts = Post.objects.all()

for post in posts:
    post.user.name # &lt;-  추가 쿼리 발생 지점</code></pre>
<p>따라서 이러한 문제를 개선하기위해 select_related, prefetch_related를 통한 Eager Loading 전략이 있습니다.</p>
<h2 id="orm-eager-loading즉시로딩">ORM Eager Loading(즉시로딩)</h2>
<p>queryset은 기본 Lazy Loading을 하지만 한 번에 여러 개의 데이터를 가져오고 싶을 때는 select_related, prefetch_related를 통한 Eager Loading 전략을 통해 가져옵니다. Eager Loading를 통해 성능향상을 기대할 수 있습니다.</p>
<h3 id="select_related">select_related</h3>
<p>select_related란 원래 쿼리에 join을 통해 데이터를 즉시 로딩하는 방식입니다. one-to-one, foreign key인 경우에만 select_related 옵션을 줄 수 있습니다. 이는 Django의 제약사항입니다. SQL 단계에서 join이 발생합니다.</p>
<h3 id="prefetch_related">prefetch_related</h3>
<p>prefetch_related는 추가 쿼리를 수행하며 데이터를 즉시 로딩하는 방식입니다. many-to-many, one-to-many의 정참조이거나 역참조 Foreign Key일 때 사용합니다. 각 관계별로 DB 쿼리를 수행하고 파이썬 레벨에서 join이 발생합니다</p>
<h2 id="정리">정리</h2>
<p>django를 사용한지 3개월 정도 사용하며 느낀점은 django orm을 통해 개발자는 쿼리에 대한 고민을 줄이고 비즈니스 로직에만 집중 할 수 있다는 점이 매우 크게 다가왔습니다. django orm은 매우 수준높게 구현되어 있으며 특히 다양한 모델별 orm이 존재하여 그 위상에 더 큰 감탄을 했습니다.(ex 공간 관련 모델의 orm 등등). 물론 위와 같이 가장 기본적인 내용 없이는 orm을 사용하는 이유를 찾기 힘들며, 실제 사용하더라도 더 큰 비효율을 발생시킬 수 있으니 사용할 때는 “왜 사용하며”, “어떻게 사용해야하는지” 한 번더 배우는 기회였습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Messaging Platform RabbitMQ and Kafka]]></title>
            <link>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-Messaging-Platform-RabbitMQ-and-Kafka</link>
            <guid>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-Messaging-Platform-RabbitMQ-and-Kafka</guid>
            <pubDate>Sun, 26 Jun 2022 13:48:48 GMT</pubDate>
            <description><![CDATA[<h1 id="🧐-고민">🧐 고민</h1>
<ul>
<li><p>현재 회사에서 비동기 작업 처리를 위한 메시징 플랫폼으로 RabbitMQ를 사용하고 있습니다. 하지만 향후 증가하게 될 부하 및 처리량에 대한 이슈를 대비하기 위해 팀 차원에서의 새로운 고민 하게 되었습니다. 가까운 미래를 안전하고 효율적으로 대비하기 위해 새로운 메시징 플랫폼 리서치를 시작하게 되었습니다. </p>
</li>
<li><p>저희가 도입 고민 중인 메시징 플랫폼은 Kafka입니다. 최근 메시징 플랫폼에서 빠지지 않고 언급되며 많은 기업에서도 적용하여 사용중인 Kafka 도입을 고려하고 있지만, 신규 기술도입은 많은 리스크를 안고 있기에 보다 신중하게 접근해야합니다. </p>
</li>
<li><p>가장 큰 고민은 현재도 안정적으로 운영 중인 RabbitMQ를 떠나 새로운 메시징 플랫폼인 Kafka를 도입할 가치가 있는가에 대해 고민이 입니다. 그러던 중 [NHN FORWARD 2020] RabbitMQ and Cloud Messaging Platform이라는 영상을 발견하게 되었습니다. RabbitMQ와 Kafka의 주요 컨셉부터 역할까지 잘 비교하는 영상이었고 이를 정리해보았습니다.</p>
</li>
</ul>
<h1 id="📭-메시징-플랫폼">📭 메시징 플랫폼</h1>
<h2 id="1-메시지-브로커">1. 메시지 브로커</h2>
<blockquote>
<p>애플리케이션, 시스템 및 서비스가 서로 간에 통신하고 정보를 교환할 수 있도록 해주는 소프트웨어
    정규 메시징 프로토콜 간에 메시지를 변환함으로써 이를 수행</p>
</blockquote>
<ul>
<li><p>많은 기업들에서 메시지 기반 미들웨어 아키텍쳐에서 자주 사용</p>
</li>
<li><p>미들웨어: 서비스하는 애플리케이션들을 보다 효율적으로 아키텍쳐들을 연결하는 소프트웨어(메시징 플랫폼, 인증 플랫폼, 데이터베이스 등등)</p>
</li>
<li><p>메시지 처리 후 즉시 또는 짧은 시간 내에 삭제</p>
</li>
<li><p>메시지 브로커는 메시지를 생산 - 처리 - 삭제</p>
</li>
<li><p>RedisQueue, RabbitMQ</p>
<h2 id="2-이벤트-브로커">2. 이벤트 브로커</h2>
<blockquote>
<p>메시지 브로커의 큐 기능을 가지고 있고 추가로 이벤트 별로 관리가 가능</p>
</blockquote>
</li>
<li><p>메시지 브로커의 역할 가능</p>
</li>
<li><p>이벤트 또는 메시지라고도 불리는 레코드를 하나만 보관하고 인덱스를 통해 개별 엑세스를 관리</p>
</li>
<li><p>업무상 필요한 시간동안 메시지를 관리 가능</p>
</li>
<li><p>이벤트 브로커는 데이터를 삭제하지 않음</p>
</li>
<li><p>이벤트 브로커는 서비스에서 나오는 이벤트를 마치 데이터베이스에 저장하듯이 이벤트 브로커의 큐에 저장 </p>
<ul>
<li>저장을 통해 얻는 이점</li>
<li>딱 한 번 일어난 이벤트 데이터를 브로커에 저장하므로서 단일 진실 공급원으로 사용 가능</li>
<li>장애 발생시 장애가 일어난 시점부터 재 처리가 가능</li>
<li>많은 양의 실시간 스트림 데이터를 효과적으로 처리 가능</li>
<li>다양한 이벤트 기반 MSA에서 중요한 역할 가능 </li>
</ul>
</li>
<li><p>Kafka, AWS Kinesis</p>
</li>
</ul>
<h1 id="🤲-rabbitmq-vs-kafka">🤲 RabbitMQ vs Kafka</h1>
<h2 id="1-main-concept">1. Main Concept</h2>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/16d43130-6d1c-4fbd-9886-dac7254d323e/image.png" alt=""></p>
<h3 id="rabbitmq">RabbitMQ</h3>
<ul>
<li>Queue</li>
<li>Queue의 메시지는 소비되면 짧은 기간 보관 또는 삭제. 일시적인 메시지 보관에 최적화</li>
<li>Queue의 생성자(producer)와 소비자(consumer)가 독집적/비동기적으로 동작할 수 있게 사이에서 큐잉버퍼의 역할</li>
<li>메시지가 Queue에서 머무르는 시간이 짧을 수록 성능이 좋게 구현</li>
</ul>
<h3 id="kafka">Kafka</h3>
<ul>
<li>Log</li>
<li>일반적으로 파일을 쓰는 것으로 이해하면 쉬움. 메시지를 긴 시간 혹은 영구적으로 보관가능</li>
<li>큐와는 다르게 같은 데이터를 반복적으로 읽을 수 있음</li>
<li>이벤트 소싱을 구현 할 때 좋은 미들웨어</li>
<li>추가되는 메시지를 지속적으로 덧붙히는 방식. 따라서 큐와는 달리 구현 방식이 매우 간단함</li>
</ul>
<h2 id="2-broker">2. Broker</h2>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/7d43f260-c066-47e1-ae43-9babf385cb67/image.png" alt=""></p>
<h3 id="rabbitmq-1">RabbitMQ</h3>
<ul>
<li>Smart Broker &amp; Dumb Consumer</li>
<li>모든 큐와 메시지를 브로커가 직접 관리</li>
<li>대부분의 기능을 브로커가 처리하고 컨슈머는 독립적인 구조</li>
<li>따라서 컨슈머 애플리케이션을 구현하거나 설계할 때 엔지니어링 리소스가 적음</li>
<li>스케일 아웃과정에서도 단순하게 컨슈머 인스턴스의 수를 늘리면 됨</li>
<li>하지만 컨슈머가 늘어남에 따라 브로커의 역할이 커짐. 여러 컨슈머가 소비하는 메시지의 상태를 모두 관리해야함<h3 id="kafka-1">Kafka</h3>
</li>
<li>Bumb Broker &amp; Smart Consumer</li>
<li>메시지를 단순하게 덧붙힘. 브로커는 컨슈머가 어떤 메시지를 어디까지 읽었는지 관리하지 않음. 메시지 관리는 컨슈머에서 관리</li>
<li>따라서 컨슈머 애플리케이션 엔지니어링 리소스가 RabbitMQ보다 큼. 개발자의 실수로 인한 장애가 발생할 가능성이 커짐</li>
<li>스케일 아웃 과정에서도 단순하게 컨슈머만 늘리는 것이 아니라 파티션의 수도 함께 늘려줘야함</li>
<li>이런점 때문에 컨슈머의 스케일 아웃이 브로커에 매우 의존적임</li>
</ul>
<h2 id="2-queality-of-service메시지-전달-보장-수준">2. Queality of Service(메시지 전달 보장 수준)</h2>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/97d7e437-db3b-4fd6-b8a9-f6eddbb08488/image.png" alt=""></p>
<h3 id="at-most-once">At most once</h3>
<ul>
<li>메시지 전달에서 손실이 발생할 수 있는 부분</li>
<li>네트워크 문제나 기타 문제로 컨슈머가 받는 메시지 손실이 발생할 수도 있음<h3 id="at-least-once">At least once</h3>
</li>
<li>브로커가 메시지 손실 가능성을 사라졌지만 중복 가능성이 있는 메시지가 전달될 가능성이 있음</li>
<li>브로커는 컨슈머에서 메시지 전송 응답이 올 때까지 큐에 메시지를 저장</li>
<li>만약 오류가 발생해 컨슈머에서 브로커에게 메시지 응답 중 연결이 끊어진다면, 큐에 남아 있는 메시지는 다른 컨슈머에게 전송되어 중복 문제가 발생할 수 있음<h3 id="exactly-once">Exactly once</h3>
</li>
<li>메시지를 손실 중복 없이 전달하는 메시지 전달 보장 수준</li>
<li>가장 안전하지만 일반적으로 프로듀서 - 브로커 - 컨슈머 사이에 트랜잭션을 사용하기 때문에 성능이 나쁨</li>
</ul>
<h2 id="4-protocol">4. Protocol</h2>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/d42be2e9-a4b9-4731-a2f3-65a9b872b65d/image.png" alt=""></p>
<h3 id="rabbitmq-2">RabbitMQ</h3>
<ul>
<li>amqp, mqtt, stomp</li>
<li>다양한 표준 프로토콜 제공<h3 id="kafka-2">Kafka</h3>
</li>
<li>자체 전이된 tcp 기반 바이터리 프로토콜 제공</li>
<li>400개가 넘는 커넥터가 존재하기 때문에 다른 서비스와 미들웨어에 적용하는 것이 어렵지 않음<h2 id="5-usablilty-and-use-cases">5. Usablilty and Use cases</h2>
<img src="https://velog.velcdn.com/images/ryu_log/post/9d4c9157-ec3c-4cbe-90f9-cb70d2782b47/image.png" alt=""><img src="https://velog.velcdn.com/images/ryu_log/post/fe6ca624-c8ae-4991-ae2b-05331e8ed98f/image.png" alt=""></li>
</ul>
<h3 id="rabbitmq-3">RabbitMQ</h3>
<ul>
<li>높은 사용성</li>
<li>설치 및 운영이 상대적으로 쉬움. 빌트인 콘솔 있음</li>
<li>메시징 기능에 초점을 둔 전통적인 메시지 브로커<h3 id="kafka-3">Kafka</h3>
</li>
<li>관리를 위한 별도의 도구 필요</li>
<li>실시간 스트림 프로세싱</li>
<li>메트릭, 이벤트 소싱 등 다양한 곳에서 사용 가능</li>
</ul>
<h2 id="참고자료">참고자료</h2>
<ul>
<li>[NHN FORWARD 2020] RabbitMQ and Cloud Messaging Platform (<a href="https://www.youtube.com/watch?v=SmE_k8lqfRQ">https://www.youtube.com/watch?v=SmE_k8lqfRQ</a>)</li>
<li>카프카, 레빗엠큐, 레디스 큐의 큰 차이점! 이벤트 브로커와 메시지 브로커에 대해 알아봅시다(<a href="https://www.youtube.com/watch?v=H_DaPyUOeTo">https://www.youtube.com/watch?v=H_DaPyUOeTo</a>)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[No space left on device? Docker prune]]></title>
            <link>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-No-space-left-on-device-Docker-prune</link>
            <guid>https://velog.io/@ryu_log/%EA%B8%80%EB%98%90-No-space-left-on-device-Docker-prune</guid>
            <pubDate>Sun, 12 Jun 2022 14:13:37 GMT</pubDate>
            <description><![CDATA[<h3 id="no-space-left-on-device-장치에-남은-공간이-없음">No space left on device: 장치에 남은 공간이 없음</h3>
<p>변경 사항을 ECS에 배포 후 발생하는 에러를 확인해보니 아래와 같은 메시지가 확인 되었습니다.</p>
<pre><code class="language-bash">OSError: [Errno 28] No space left on device: &#39;/usr/local/...-airflow/logs&#39;</code></pre>
<p>우선은 급하게 EBS 볼륨 10GB를 추가하고 본격적으로 원인을 찾아보았습니다.</p>
<p>실제 사용되는 디스크 용량을 확인해보니 루트 EBS 볼륨 블록 디바이스(/dev/nvme0n1)의 용량이 상당히 많은 양을 차지하는 것을 알 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/67665617-3721-4140-9a0e-52a15c0f5859/image.png" alt=""></p>
<p>처음에는 단순히 logfile이 많이 쌓여 발생했겠구나 싶어서 확인해보니 해당 프로젝트의 로깅 레벨은 <code>ERROR</code> 였고, 실제 저장되어있는 로그 파일의 용량이 매우 적은 것을 확인했습니다.</p>
<h3 id="docker-system-df--v">docker system df -v</h3>
<p>문득 도커 컨테이너 용량이 궁금해져 <code>docker system df -v</code> 명령어를 통해 확인해보니 아래와 같이 사용되지 않는 컨테이너가 정리되지 않고 있었습니다. (아래 사진은 당시의 결과가 아닙니다 .. 당시 캡쳐를 하지 못해 다른 사진으로 대체합니다)</p>
<p><em>💡 <code>docker system df</code> : 현재 사용중인 이미지, 컨테이너 및 볼륨이 얼마나 많은 공간을 사용하고 있는지 확인할 수 있는 명령어. 뒤에 <code>-v</code> (vervose)옵션을 추가하면 사용하지 않는 이미지와 컨테이너도 확인 가능</em></p>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/51cd69b6-783a-4738-a80d-6bb46890aebe/image.png" alt=""></p>
<p>실제 컨테이너의 SIZE의 크기가 수십 KB에서 많게는 800MB 까지 정리되지 않은 컨테이너들이 많았고, 이에 볼륨(디스크)이 찼던 것이었습니다. </p>
<h3 id="docker-prune">docker prune</h3>
<p>다행히도 빠르게 원인을 발견 할 수 있었고, 구글링을 통해 <code>docker prune</code>이라는 해결방법을 찾을 수 있었습니다. </p>
<p><em>💡 <code>docker prune</code> : 사용하지 않는 컨테이너 이미지를 제거하는 명령어. prune의 가장 큰 특징은 어떠한 옵션을 주는가에 따라 그 역할이 달라진다는 것</em></p>
<ul>
<li>option<ul>
<li><code>docker volume prune</code> : 미사용 볼륨 제거</li>
<li><code>docker container prune</code> : 미사용 컨테이너 제거</li>
<li><code>docker image prune</code> : 미사용 이미지 제거</li>
<li><code>docker system prune</code> : 미사용 중인 이미지, 컨테이너, 볼륨 모두 제거</li>
</ul>
</li>
</ul>
<p>우선적으로 <code>docker systme prune</code> 명령어를 통해 일부 용량을 확보 할 수 있었습니다. </p>
<p>추가적으로 재발 방지를 위해, 아래와 같이 일주일이 지난 컨테이너 및 이미지 등에 대하여 주기적으로 삭제하는 크론탭을 daily.cron에 등록하였습니다. prune 명령어에는 <code>—-filter</code> 옵션이 있는데 이를 잘 활용하면 운영에 큰 이점이 있습니다.</p>
<pre><code class="language-bash">docker system prune -af --filter &quot;until=$((7*24))h&quot;</code></pre>
<h3 id="varlibdockeroverlay2difftmp">/var/lib/docker/overlay2/diff/tmp</h3>
<p><code>/var/lib/docker</code> 내부에는 컨테이너, 볼륨, 빌드 등 다양한 정보가 저장됩니다. 여기서부터는 관리자 권한이니 <code>sudo su</code>나 root 계정으로 로그인하여 진행하면 편합니다.</p>
<p>특히 docker/ 이하 overlay2는 기본 스토리지 드라이버이며 docker의 image가 저장되는 곳이기도 합니다. docker image는 layer 방식을 통해 관리됩니다.</p>
<p><em>💡 <strong>레이어</strong>: 기존 이미지에 추가적인 파일이 필요할 때 다시 다운로드 받는 방식이 아니라 해당 파일을 추가하기 위한 개념. 이미지는 여러 개의 읽기 전용 레이어로 구성되고, 파일이 추가되면 새로운 레이어가 생성</em></p>
<p><img src="https://velog.velcdn.com/images/ryu_log/post/1da2433c-fe94-4b7a-8572-b17586a1a653/image.png" alt=""></p>
<p>즉 overlay2/diff/tmp 디렉토리의 역할은 이미지의 변경 사항을 저장하고, 백업의 역할을 수행하기 위해 생긴 곳입니다. 따라서 잦은 빌드를 통해 새로운 컨테이너가 생성된다면 해당 디렉토리 내부의 용량이 커질 수도 있습니다. 따라서 이곳을 정리한다면 용량을 확보 할 수도 있습니다.</p>
<h3 id="docker-알면-유용한-명령어들">docker 알면 유용한 명령어들</h3>
<p><code>docker info</code></p>
<ul>
<li>현재 환경에서 사용하는 docker에 대한 정보 확인</li>
</ul>
<p><code>docker exec -it [container id] /bin/bash</code></p>
<ul>
<li>실행 중인 컨테이너 내부로 접속</li>
<li><code>docker run</code>과 비슷함. <code>docker run</code>의 경우 새로운 컨테이너 환경을 만드는 반면 <code>docker exec</code>는 이미 실행된 특정 컨테이너의 환경을 디버깅 하는 용도로 사용</li>
<li><code>-it</code> 옵션<ul>
<li><code>-i</code> : 표준 입출력을 사용하겠다는 의미</li>
<li><code>-t</code> : 가상 tty를 통해 접속하겠다는 의미</li>
</ul>
</li>
<li>컨테이너 접속 후 빠져나올 때는 <code>exit</code> 를 통해 간단하게 빠져나올 수 있음</li>
</ul>
<p><code>docker ps</code></p>
<ul>
<li>현재 사용 중인 이미지와 컨테이너의 목록을 알 수 있음</li>
<li>-a 옵션을 추가하면 현재 사용 중이지 않은 컨테이너까지 조회 가능</li>
</ul>
<p><code>docker stats</code></p>
<ul>
<li>컨테이너의 리소스 사용량 통계를 실시간 반복 출력으로 보여줌</li>
<li><code>—-no-stream</code> 옵션을 추가하면 반복출력하지 않고 현 시점의 결과만 1회 출력</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>