<?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>Tue, 25 Jun 2024 11:47:39 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/geon_km/profile/3b0045b7-c47b-42e6-9174-1ee1107fdbed/image.gif</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 김무건. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/geon_km" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Grafana, Loki, Prometheus  모니터링 구축]]></title>
            <link>https://velog.io/@geon_km/Grafana-Loki-Promtail-%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%B6%95</link>
            <guid>https://velog.io/@geon_km/Grafana-Loki-Promtail-%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%B6%95</guid>
            <pubDate>Tue, 25 Jun 2024 11:47:39 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<hr>
<ul>
<li>시스템을 운영을 하기에 모니터링은 매우 중요합니다. 이것을 해결하기 위한 다양한 모니터링 툴이 있습니다. 저는 개인적으로 취업을 하면 회사에 sentry, datadog등 다양한 모니터링이 있다고 생각했지만 현재 저희 서비스는 모니터링이 없어서 무료로 구축할 수 있는 Grafana, Loki, Prometheus를 이용해서 Was  모니터링, 로그 모니터링을 구축한 내용을 정리를 하였습니다.</li>
</ul>
<h1 id="본론">본론</h1>
<hr>
<h2 id="1-grafana-prometheus-was-모니터링">1. Grafana, Prometheus WAS 모니터링</h2>
<p><img src="https://velog.velcdn.com/images/geon_km/post/7891afcd-e974-41cc-9bd8-89d1bed28e2b/image.png" alt=""></p>
<ul>
<li>프로매테우스와 그라파나를 이용해서 스프링 서버의 모니터링을 구축하려고 한다.</li>
</ul>
<p>간단하게 설명하면 프로메테우스는 대상 시스템으로부터 모니터링 지표를 수집하는 역활을 수행하고 그라파나는 프로메테우스가 모집한 데이터를 시각화하는 모니터링 툴이다.</p>
<h3 id="1-1-docker-compose">1-1. docker-compose</h3>
<pre><code class="language-yml">version: &#39;3&#39;
services:
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: always
    ports:
      - &quot;3000:3000&quot;
    volumes:
      - grafana-data:/var/lib/grafana
      - ./grafana/provisioning/:/etc/grafana/provisioning/
    environment:
      - GF_SERVER_ROOT_URL=http://localhost:3000
      - GF_SECURITY_ADMIN_PASSWORD=admin
    depends_on:
      - loki
    networks:
      - monitoring

  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: always
    ports:
      - &quot;9090:9090&quot;
    volumes:
      - ./prometheus/config:/etc/prometheus/
      - prometheus-data:/prometheus
    command:
      - &#39;--config.file=/etc/prometheus/prometheus.yml&#39;
      - &#39;--storage.tsdb.path=/prometheus&#39;
    networks:
      - monitoring

  loki:
    image: grafana/loki:latest
    container_name: loki
    restart: always
    ports:
      - &quot;3100:3100&quot;
    command: -config.file=/etc/loki/local-config.yaml
    networks:
      - monitoring

  promtail:
    image: grafana/promtail:latest
    container_name: promtail
    volumes:
      - ./logs:/logs
      - ./promtail-config.yml:/etc/promtail/config.yml
    networks:
      - monitoring

volumes:
  grafana-data:
  prometheus-data:
  loki-data:

networks:
  monitoring:
    driver: bridge
</code></pre>
<ul>
<li>일단 docker-compose를 통해서 grafana, promethus, loki를 올리고 설명을 하겠다. </li>
</ul>
<h3 id="스프링에서-프로메테우스로-수집-데이터-전달">스프링에서 프로메테우스로 수집 데이터 전달</h3>
<h4 id="spring-actuator">Spring Actuator</h4>
<ul>
<li><p>개발자가 애플레케이션을 개발할 때 기능 요구사항만 개발하는 것은 아니다. 실제 운영 단계에 올리게 되면 개발자들이 해야하는 또 다른 중요한 업무가 있다.</p>
</li>
<li><p>운영 환경에서 서비스할 때 필요한 이른 기능들을 프로덕션 준비 기능이라고 한다.</p>
</li>
<li><p>지표(metric), 추적(trace), 감사(auditung), 모니터링</p>
</li>
<li><p>애플리케이션이 현재 살아있는지, 로그 정보는 정상적으로 설정이 되었는지, 커녁션 풀은 얼마나 사용되고 있는지</p>
</li>
</ul>
<p>의존성 추가</p>
<pre><code>    implementation &#39;org.springframework.boot:spring-boot-starter-actuator&#39;</code></pre><p>실행 결과</p>
<pre><code class="language-json">
{
&quot;_links&quot;: {
&quot;self&quot;: {
&quot;href&quot;: &quot;http://localhost:8080/actuator&quot;,
&quot;templated&quot;: false
},
&quot;health&quot;: {
&quot;href&quot;: &quot;http://localhost:8080/actuator/health&quot;,
&quot;templated&quot;: false
},
&quot;health-path&quot;: {
&quot;href&quot;: &quot;http://localhost:8080/actuator/health/{*path}&quot;,
&quot;templated&quot;: true
}
}
}</code></pre>
<ul>
<li><p>health</p>
<ul>
<li><p>만약에 up으로 나오면 health는 정상적으로 동작하고 있다고 의미한다.</p>
<p>이후 모든것을 노출하기 위해서는 yaml에 다음과 같이 설정을 해야된다.</p>
<pre><code class="language-yaml">management:
endpoints:
  web:
    exposure:
      include: &quot;*&quot;</code></pre>
</li>
<li><p>앤드포인트를 사용하려면 다음과 같은 2가지 과정이 필요하다.</p>
</li>
</ul>
<ol>
<li>엔드포인트 활성화</li>
<li>엔드포인트 노출</li>
</ol>
<ul>
<li>환성화란 해당 기능을 on, off 설정하는거</li>
<li>엔드 포인트를 노출은 http노출, jmx를 통해 노출할 지 선택을 해야된다.</li>
</ul>
</li>
</ul>
<p>엔드포인트는 기본적으로 활성화가 되어져 있다. ( 그런데 shutdown만 비활성화 - 실행하면 서버가 내려가기 때문에 )</p>
<p>따라서 어떤 엔드포인트를 노출할지 선택하면 된다. 보통 HTTP로 노출한다.</p>
<ul>
<li>만약에 shutdown을 처리하고 싶으면 다음과 같이 설정하면 된다.</li>
</ul>
<pre><code class="language-yaml">management:
  endpoints:
        shutdotn:
            enable: true
    web:
      exposure:
        include: &quot;*&quot;</code></pre>
<h3 id="엔드포인트-노출">엔드포인트 노출</h3>
<ul>
<li>스프링 공식 메뉴얼이 제공하는 예제를 통해 엔드포인트 노출 설정</li>
</ul>
<pre><code class="language-yaml">management:
  endpoints:
    web:
      exposure:
        include: &quot;*&quot;
                exclude: &quot;info&quot;</code></pre>
<h2 id="다양한-엔드포인트">다양한 엔드포인트</h2>
<p>각각의 엔드포인트를 통해서 개발자는 애플리케이션 내부의 수 많은 기능을 모니터링 할 수 있다.</p>
<h3 id="엔드포인트-목록">엔드포인트 목록</h3>
<ul>
<li>bean : 스프링 컨테이너에 등록된 빈을 보여준다.</li>
<li>conditions : condition을 통해서 빈을 등록할 때 평가 조건과 일치하거나 일치하지 않는 이유 표시</li>
<li>configprops : configurationProperties를 보여준다.</li>
<li>env : evniroment정보를 보여준다.</li>
<li>health : 애플리케이션 헬스 정보 보여준다.</li>
<li>httpexchanges : http 호출 응답 정보를 보여준다. httpexchangerepository를 구현한 빈을 별도로 등록해야 한다.</li>
<li>info : 애플리케이션 정보를 보여준다.</li>
<li>loggers : 애플리케이션 로거 설정을 보여주고 변경도 할 수 있다.</li>
<li>metrics : 애플리케이션의 메트릭 정보를 보여준다.</li>
<li>mappings : @requestmappging의 정보를 보여준다.</li>
<li>threaddump : 쓰레드 덤프를 실행해서 보여준다.</li>
<li>shutdown : 애플리케이션을 종료한다. 이 기능은 기본적으로 비활성화가 되어져 있다.</li>
</ul>
<h3 id="health-정보">health 정보</h3>
<ul>
<li>애플리케이션이 문제가 발생하면 빠르게 알 수 있다.</li>
<li>애플리케이션이 사용하는 데이터베이스가 응답하는지 디스크 사용량에는 문제가 없는지 판단하여 정보를 보여준다.</li>
</ul>
<pre><code class="language-yaml">management:
  endpoint:
    health:
      show-details: always
  endpoints:
    web:
      exposure:
        include: &quot;*&quot;</code></pre>
<ul>
<li>다음과 같이 설정하면 다음과 같이 나오게 된다.</li>
</ul>
<pre><code class="language-yaml">{
&quot;status&quot;: &quot;UP&quot;,
&quot;components&quot;: {
&quot;db&quot;: {
&quot;status&quot;: &quot;UP&quot;,
&quot;details&quot;: {
&quot;database&quot;: &quot;H2&quot;,
&quot;validationQuery&quot;: &quot;isValid()&quot;
}
},
&quot;diskSpace&quot;: {
&quot;status&quot;: &quot;UP&quot;,
&quot;details&quot;: {
&quot;total&quot;: 494384795648,
&quot;free&quot;: 423133499392,
&quot;threshold&quot;: 10485760,
&quot;path&quot;: &quot;/Users/mugeon/IdeaProjects/mybatis/.&quot;,
&quot;exists&quot;: true
}
},
&quot;ping&quot;: {
&quot;status&quot;: &quot;UP&quot;
}
}
}</code></pre>
<ul>
<li>만약에 이렇게 정보가 부담스러우면 다음과 같이 바꾸면 된다.</li>
</ul>
<pre><code class="language-json">show-components: always</code></pre>
<ul>
<li>이렇게 바뀌면 up, down만 출력이 된다.</li>
</ul>
<h3 id="애플리케이션-정보">애플리케이션 정보</h3>
<ul>
<li><p>info 엔드포인트는 애플리케이션의 기본 정보를 노출한다.</p>
<ul>
<li><p>java : 자바 런타임 정보</p>
</li>
<li><p>os</p>
</li>
<li><p>env</p>
</li>
<li><p>build :빌드 정보, meta-inf/build-info.properties파일이 필요하다.</p>
</li>
<li><p>git :git정보, <a href="http://git.properties">git.properties</a> 파일이 필요하다.</p>
</li>
</ul>
</li>
</ul>
<pre><code class="language-json">id &#39;com.gorylenko.gradle-git-properties&#39; version &#39;2.4.1&#39;  </code></pre>
<p><img src="https://velog.velcdn.com/images/geon_km/post/a778efa2-fc2d-4836-a83c-e2dd53b1608d/image.png" alt=""></p>
<p>management: info: git: mod : “full”로 처리하면 더 자세한 정보 공유</p>
<ul>
<li>기본적으로 java, env, os는 비활성화 되어여 짔다.</li>
</ul>
<h3 id="로거">로거</h3>
<ul>
<li>loggers 엔드포인트를 사용하면 로깅과 관련된 정보를 확인하고, 실시간으로 변경할 수 있다.</li>
</ul>
<h3 id="http-요청-응답-기록">HTTP 요청 응답 기록</h3>
<ul>
<li>HTTP 요청과 응답 과거 기록을 확인하고 싶으면 httpexchanges 엔드포이트로 확인이 가능하다.</li>
<li>httpexchangerepository 인터페이스를 빈으로 등록하면 사용할 수 있다. 기본적으로 스프링 부트는 InMemoryHttpExchangeRepository를 제공한다.</li>
</ul>
<h3 id="액츄에이터와-보안">액츄에이터와 보안</h3>
<p><strong>보안 주의</strong></p>
<ul>
<li><p>액츄에이터가 제공하는 기능은 너무 많아서 외부 인터넷 망이 공개된 곳에 노출은 좋은 방식이 아니다. 외부 접근을 막고 내부망에서 사용하는게 안전하다.</p>
</li>
<li><p>외부 인터넷 망을 통해서 8080포트에만 접근할 수 있다. 다른 포트는 내부망에서만 접근할 수 있으면 다른 포트에서 설정하면 된다.</p>
</li>
<li><p>액츄에이터의 기능을 애플리케이션 서버와는 다른 포트에서 실행하려면 다음과 같이 설정하면 된다.</p>
</li>
</ul>
<pre><code class="language-json">managerment.server.port = 9292</code></pre>
<ul>
<li>스프링에서 actuatorm promethus를 추가한다.</li>
</ul>
<pre><code>    implementation &#39;org.springframework.boot:spring-boot-starter-actuator&#39;
    implementation &#39;io.micrometer:micrometer-registry-prometheus&#39;</code></pre><blockquote>
<p>actuator는 실제 운영에서는 보안적인 부분을 신경을 써야합니다. 하지만 이번에는 간단한 프로젝트에서 기록하기 위해서 가장 간단한 버전으로 작성을 하겠습니다.</p>
</blockquote>
<ul>
<li><p>spring actuator를 추가를 하여 빌드를 하였으면 yml에 설정을 해야합니다.</p>
<pre><code class="language-yaml">management:
info:
  java:
    enabled: true
  os:
    enabled: true
  env:
    enabled: true
endpoint:
  health:
    show-details: always
endpoints:
  web:
    exposure:
      include: &quot;*&quot;</code></pre>
</li>
<li><p>이후 spring서버를 실행시키고 <code>localhost:8080/actuator</code>를 살펴보면 다양한 정보가 보이는데 여기서 <code>http://localhost:8080/actuator/prometheus</code>을 살펴보면 관련 데이터가 나오는데 이것을 프로메테우스에게 전달하는 데이터이다.
<img src="https://velog.velcdn.com/images/geon_km/post/19a8f8d4-7bcf-41e7-b444-9e3cde60baf0/image.png" alt=""></p>
</li>
<li><p>프로메테우스에 접속하기 위해서는 <code>9090</code>포트에 접근하면 프로메테우스에 접근할 수 있습니다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/da7913c1-441e-45ea-aa7f-be2fcbede876/image.png" alt=""></p>
<ul>
<li>현재 endpoint를 살펴보면 정상적으로 연결이 됩니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL HA(High Availability) Replication 구축]]></title>
            <link>https://velog.io/@geon_km/MySQL-HAHigh-Availability-Replication-%EA%B5%AC%EC%B6%95-1</link>
            <guid>https://velog.io/@geon_km/MySQL-HAHigh-Availability-Replication-%EA%B5%AC%EC%B6%95-1</guid>
            <pubDate>Tue, 25 Jun 2024 07:16:34 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<hr>
<ul>
<li><p>이번에는 Docker로 MySQL Replication 구축 과정에 대해서 기록합니다. Replication에 관심을 가지게 된 계기는 디비를 사용하고 운영할 때 확장, 가용성이 중요하다. 일반적으로 가장 많이 사용되는 기술이 Replication이다.</p>
</li>
<li><p>최종적으로 MySQL에서 Replication에서 다음과 같은 학습을 하겠습니다.</p>
</li>
</ul>
<ol>
<li>MySQL Bridge Network Replication</li>
<li>Orchestrator를 이용한 High Availability(HA) 구성</li>
<li>모니터링</li>
<li>Swarm 모드 확장 및 Backup</li>
</ol>
<ul>
<li>이번에는 1번 Bridge Network를 통한 Replication을 설정하겠습니다.</li>
</ul>
<h1 id="본론">본론</h1>
<hr>
<ul>
<li><p>이번에는 mysql PeronaServer을 기반으로 실행을 합니다. 물론 이 부분은 사용자에 따라서 다르게 설정이 가능합니다.
<a href="https://docs.percona.com/percona-server/8.0/quickstart-docker.html">https://docs.percona.com/percona-server/8.0/quickstart-docker.html</a></p>
</li>
<li><p>이번에는 간단하게 설명하는 다음과 같은 구조를 가지고 있습니다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/6c69ce5d-2593-4814-90de-ffdacc9d6c42/image.png" alt=""></p>
<h2 id="replication">Replication</h2>
<p>Replication은 데이터베이스에서 데이터를 한 서버(Master)에서 다른 서버(Slave)로 복제하는 기술입니다. 이를 통해 데이터의 확장성, 가용성을 높이고, 장애 복구 시간을 줄이며, 읽기 성능을 향상시킬 수 있습니다. 특히 <strong>분산 시스템에서 자주 사용</strong>됩니다. 이를 통해 주요 <strong>서버의 부하를 줄이고 여러 서버에 걸쳐 데이터를 분산시킬 수 있습니다.</strong></p>
<h3 id="replication의-동작-원리">Replication의 동작 원리</h3>
<p>MySQL의 Replication은 일반적으로 Master-Slave 구조로 구성됩니다. Master는 데이터를 변경하는 트랜잭션의 로그를 기록하고, Slave는 이를 읽어 자신의 데이터베이스에 반영합니다.</p>
<p>Replication의 기본 동작 흐름은 다음과 같습니다:</p>
<ol>
<li><p>Binlog 작성 (Master)
Master는 데이터베이스의 변경 사항(INSERT, UPDATE, DELETE 등)을 <strong>Binary Log(binlog)</strong>에 기록합니다.
이 로그는 트랜잭션의 순서와 상태를 포함하며 Slave에게 전달됩니다.</p>
</li>
<li><p>Relay Log 수신 및 저장 (Slave)
Slave는 Master의 Binlog를 복제하여 자신의 Relay Log에 저장합니다.
이 과정에서 Slave는 Master의 위치를 추적하여 복제를 지속적으로 유지합니다.</p>
</li>
<li><p>SQL 스레드 실행 (Slave)
Slave는 Relay Log를 기반으로 SQL 명령을 실행하여 Master와 동일한 데이터베이스 상태를 유지합니다.</p>
</li>
<li><p>비동기적 복제
MySQL Replication은 기본적으로 비동기 방식으로 동작합니다. Slave는 Master의 상태를 따라가지만 즉각적으로 동일한 상태가 되지는 않습니다. 필요에 따라 반동기적(Semi-synchronous) 또는 완전 동기적(Synchronous) 복제를 설정할 수도 있습니다.</p>
</li>
</ol>
<h2 id="replication의-주요-특징">Replication의 주요 특징</h2>
<ol>
<li><p>확장성(Scalability)
Master는 데이터 변경 작업을 처리하고 Slave는 읽기 작업을 분담함으로써 읽기 성능을 향상시킬 수 있습니다.</p>
</li>
<li><p>가용성(High Availability)
Master 서버에 문제가 생기더라도 Slave 서버로 빠르게 전환(Failover)하여 시스템을 계속 운영할 수 있습니다.</p>
</li>
<li><p>백업 및 분석 용이성
Slave 서버를 이용해 데이터를 백업하거나 분석 작업을 수행할 수 있습니다. 이 과정에서 Master의 부하를 최소화할 수 있습니다.</p>
</li>
<li><p>유연성
다양한 복제 설정(예: Single-Master Multi-Slave, Multi-Master 등)을 지원하여 요구 사항에 맞게 구성을 조정할 수 있습니다.</p>
</li>
</ol>
<h3 id="replication의-한계와-고려-사항">Replication의 한계와 고려 사항</h3>
<ol>
<li><p>비동기적 특성
기본적으로 비동기적으로 동작하므로, Master와 Slave 간 데이터 지연(Lag)이 발생할 수 있습니다.
반동기적 또는 동기적 복제를 설정하면 데이터 일관성이 향상되지만, 쓰기 성능에 영향을 미칠 수 있습니다.</p>
</li>
<li><p>Failover 복잡성
Master 장애 시 Slave를 Master로 승격(Promotion)하는 작업이 필요합니다. 이를 자동화하려면 추가적인 도구(예: Orchestrator)를 도입해야 합니다.</p>
</li>
<li><p>데이터 손실 가능성
Master 장애 시 Binlog가 Slave에 완전히 전달되지 않았다면 데이터 손실이 발생할 수 있습니다.</p>
</li>
</ol>
<h2 id="master-slave-replication-구성하기">Master-Slave Replication 구성하기</h2>
<ul>
<li><p>시작에 앞서 다음과 같은 구조를 가져가겠습니다.</p>
<pre><code>db001
|-- conf
|-- data
|   |-- #innodb_redo
|   |-- #innodb_temp
|   |-- mysql
|   |-- performance_schema
|   `-- sys
`-- log
db002
|-- conf
|-- data
|   |-- #innodb_redo
|   |-- #innodb_temp
|   |-- mysql
|   |-- performance_schema
|   `-- sys
`-- log
db003
|-- conf
|-- data
|   |-- #innodb_redo
|   |-- #innodb_temp
|   |-- mysql
|   |-- performance_schema
|   `-- sys
`-- log</code></pre></li>
<li><p>해당 경로를 살펴보면 크게 conf, data, log로 설정이 되어져 있습니다. 해당 부분은 docker는 stateless이기 때문에 결국에는 운영단계에서 사용하기 위해서는 volumn을 통해서 관련 정보를 저장합니다. </p>
</li>
<li><p>db 3개를 사용하기 때문에 3개의 폴더에 conf, data, log를 추가하겠습니다. 또한 권한을 추가가 필요합니다.</p>
</li>
</ul>
<blockquote>
<p>적절한 권한을 설정
테스트를 위해서 chmod 777를 주었을 때 mysql에서 권한의 범위가 매우 넓기 때문에 무시한 경우가 있습니다. 적절한 권한을 주기 위해서는 파일에 644, 폴더에는 755를 통해서 권한을 설정합니다.</p>
</blockquote>
<pre><code>chmod 644 *
chmod 755 *</code></pre><ul>
<li>각 conf에 간단한 my.cnf파일을 설정을 합니다. 해당 파일을 db001, db002, db003에 따라서 각각 처리를 합니다. 이때 가장 중요한 부분은 <code>server-id, report_host</code>를 적절하게 각 mysql에 맞게 설정을 합니다.</li>
</ul>
<pre><code>[mysqld]
log_bin                     = mysql-bin
binlog_format               = ROW
gtid_mode                   = ON
enforce-gtid-consistency    = true
server-id                   = 100
log_slave_updates
datadir                     = /var/lib/mysql
socket                      = /var/lib/mysql/mysql.sock

# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links              = 0

log-error                   = /var/log/mysql/mysqld.log
pid-file                    = /var/run/mysqld/mysqld.pid

report_host                 = db001

[mysqld_safe]
pid-file                    = /var/run/mysqld/mysqld.pid
socket                      = /var/lib/mysql/mysql.sock
nice                        = 0</code></pre><h3 id="docker-컨테이너-실행">Docker 컨테이너 실행</h3>
<ul>
<li>가장 간단한 방법으로 docker에 mysql을 3개 올리겠습니다. 관련 정보를 살펴보면 volume에서 log, conf, data를 설정하였습니다. 이후 가장 간단하게 docker ps를 통해서 docker의 유무를 체크하고 바로 다음 단계로 넘어가겠습니다.
```shell
docker run -d -p 3306:3306 --name db001 \</li>
<li>e MYSQL_ROOT_PASSWORD=1234 \</li>
<li>v /Users/mugeon/docker/db/db001/log:/var/log/mysql \</li>
<li>v /Users/mugeon/docker/db/db001/data:/var/lib/mysql \</li>
<li>v /Users/mugeon/docker/db/db001/conf:/etc/my.cnf.d <br>percona/percona-server:8.0.39-aarch64</li>
</ul>
<p>docker run -d -p 3307:3306 --name db002 <br>-e MYSQL_ROOT_PASSWORD=1234 <br>-v /Users/mugeon/docker/db/db002/log:/var/log/mysql <br>-v /Users/mugeon/docker/db/db002/data:/var/lib/mysql <br>-v /Users/mugeon/docker/db/db002/conf:/etc/my.cnf.d <br>percona/percona-server:8.0.39-aarch64</p>
<p>docker run -d -p 3308:3306 --name db003 <br>-e MYSQL_ROOT_PASSWORD=1234 <br>-v /Users/mugeon/docker/db/db003/log:/var/log/mysql <br>-v /Users/mugeon/docker/db/db003/data:/var/lib/mysql <br>-v /Users/mugeon/docker/db/db003/conf:/etc/my.cnf.d <br>percona/percona-server:8.0.39-aarch64</p>
<pre><code>
- 처음에 db001을 접근하여 master계정을 추가하겠습니다.
```shell
docker exec -it -uroot db001 mysql -u root -p 
# &gt; 비밀번호 입력

docker exec -it -uroot db001 mysql -u root -p 
CREATE USER &#39;repl&#39;@&#39;%&#39; IDENTIFIED BY &#39;repl&#39;;
GRANT REPLICATION SLAVE ON *.* TO &#39;repl&#39;@&#39;%&#39;;
# 권한 즉시 적용
FLUSH PRIVILEGES;</code></pre><ul>
<li>해당 과정을 통해서 master  db에서 유저를 만들 고 해당 db의 ip를 확인한다. ifconfig 명령어를 통해서 확인이 가능하다.</li>
<li>해당하는 ip를 slave db002, db003에 기본적인 ip로 설정을 합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/dc3ee518-5ff8-4c17-ae1f-85b5e1d7b943/image.png" alt=""></p>
<ul>
<li>이후 db002, db003에서 접속하여 다음과 같은 작업을 수행합니다.<pre><code class="language-shell">reset master;
</code></pre>
</li>
</ul>
<p>CHANGE MASTER TO MASTER_HOST=&#39;${master_IP}&#39;,     MASTER_USER=&#39;repl&#39;,
 MASTER_PASSWORD=&#39;repl&#39;,
 MASTER_AUTO_POSITION=1,
 MASTER_SSL=1;
START REPLICA;</p>
<p>show slave status\G</p>
<pre><code>![](https://velog.velcdn.com/images/geon_km/post/277a5932-6602-4067-a3a0-fc2e8c418130/image.png)

- 위 과정을 통해서 가장 간단하게 mysql replication을 수행할 수 있었습니다.

&gt; 하지만 조금의 문제점이 있습니다. 해당 docker를 통해서 꺼졌다가 켜지게 되면 IP가 변경이 됩니다. 또한 docker run을 통해서 관리하기 너무 힘들다는 문제가 있기 때문에 이 2가지 문제를 network, compose를 통해서 처리하겠습니다.



## Bridge Network Replication
- 컨테이너는 언제든지 재 시작될 수 있고 Container가 재 시작하게 되면 IP가 변경될 수 있다. 기존의 Ifconfig를 통해서 Master DB의 IP를 기반으로 설정을 하여 HA 구성을 처리했다.

- 하지만 IP가 바뀐다면 Replication이 깨질 수 있기 때문에 Bridge Network를 구성하고 alias를 통해서 변경이 되어서 문제를 해결할 수 있게 하겠다.

### 1. 네트워크 생성
```shell
docker network create --driver bridge mybridge</code></pre><h3 id="2-docker-composeyml">2. Docker-compose.yml</h3>
<ul>
<li><p>해당 과정을 통해서 이제는 docker run을 여러번 할 필요가 없어지고 관련설정을 따로 변수로 관리할 수 있습니다.</p>
</li>
<li><p>이제 생성된 Bridge Network를 이용하여 각 MySQL 서버의 컨테이너를 생성합니다. 각 컨테이너는 &#39;mybridge&#39;라는 이름의 네트워크에 연결되며, &#39;db001&#39;, &#39;db002&#39;, &#39;db003&#39;라는 net alias를 각각 부여받습니다. 이를 통해 각 컨테이너는 서로 통신할 수 있는 환경을 구성하게 됩니다.</p>
<pre><code class="language-shell">version: &#39;3.8&#39;
</code></pre>
</li>
</ul>
<p>networks:
  mybridge:
    driver: bridge</p>
<p>services:
  db001:
    image: percona/percona-server:8.0.39-aarch64
    container_name: db001
    networks:
      mybridge:
        aliases:
          - db001
    ports:
      - &quot;3306:3306&quot;
    environment:
      MYSQL_ROOT_PASSWORD: &quot;1234&quot;
    volumes:
      - /Users/mugeon/docker/db/db001/log:/var/log/mysql
      - /Users/mugeon/docker/db/db001/data:/var/lib/mysql
      - /Users/mugeon/docker/db/db001/conf/my.cnf:/etc/my.cnf </p>
<p>  db002:
    image: percona/percona-server:8.0.39-aarch64
    container_name: db002
    networks:
      mybridge:
        aliases:
          - db002
    ports:
      - &quot;3307:3306&quot;
    environment:
      MYSQL_ROOT_PASSWORD: &quot;1234&quot;
    volumes:
      - /Users/mugeon/docker/db/db002/log:/var/log/mysql
      - /Users/mugeon/docker/db/db002/data:/var/lib/mysql
      - /Users/mugeon/docker/db/db002/conf/my.cnf:/etc/my.cnf  </p>
<p>  db003:
    image: percona/percona-server:8.0.39-aarch64
    container_name: db003
    networks:
      mybridge:
        aliases:
          - db003
    ports:
      - &quot;3308:3306&quot;
    environment:
      MYSQL_ROOT_PASSWORD: &quot;1234&quot;
    volumes:
      - /Users/mugeon/docker/db/db003/log:/var/log/mysql
      - /Users/mugeon/docker/db/db003/data:/var/lib/mysql
      - /Users/mugeon/docker/db/db003/conf/my.cnf:/etc/my.cnf </p>
<pre><code>
### Replication 설정
- 이전과 동일하게 Master DB에 계정과 권한을 설정합니다.
```shell
docker exec -it -uroot db001 mysql -u root -p 
# &gt; 비밀번호 입력

docker exec -it -uroot db001 mysql -u root -p 
CREATE USER &#39;repl&#39;@&#39;%&#39; IDENTIFIED BY &#39;repl&#39;;
GRANT REPLICATION SLAVE ON *.* TO &#39;repl&#39;@&#39;%&#39;;
# 권한 즉시 적용
FLUSH PRIVILEGES;</code></pre><ul>
<li>이후 Slave 설정을 할때 기존의 Ifconfig를 통해 얻은 IP가 아닌 alias를 통해 연결합니다.<pre><code>docker exec -it db002 mysql -u root -p
# 비밀번호 입력
</code></pre></li>
</ul>
<p>CHANGE MASTER TO
    MASTER_HOST=&#39;db001&#39;,
    MASTER_USER=&#39;repl&#39;,
    MASTER_PASSWORD=&#39;repl&#39;,
    MASTER_AUTO_POSITION=1;
START REPLICA;</p>
<p>SHOW SLAVE STATUS\G;</p>
<p>docker exec -it db003 mysql -u root -p</p>
<h1 id="비밀번호-입력">비밀번호 입력</h1>
<p>CHANGE MASTER TO
    MASTER_HOST=&#39;db001&#39;,
    MASTER_USER=&#39;repl&#39;,
    MASTER_PASSWORD=&#39;repl&#39;,
    MASTER_AUTO_POSITION=1;
START REPLICA;</p>
<p>SHOW SLAVE STATUS\G;</p>
<p>```</p>
<h1 id="결론">결론</h1>
<hr>
<p>이번 글에서는 Docker와 MySQL을 활용하여 Master-Slave Replication 환경을 구축하는 방법을 다뤘습니다. Replication을 통해 장애 복구 및 읽기 성능을 향상시킬 수 있음을 확인했습니다.</p>
<p>다음 단계에서는 Orchestrator를 활용하여 High Availability를 구현하고, 모니터링 도구를 통해 상태를 관리하는 과정을 다룰 예정입니다.</p>
<h1 id="참고">참고</h1>
<hr>
<p><a href="https://www.inflearn.com/course/mysql-docker/dashboard">https://www.inflearn.com/course/mysql-docker/dashboard</a>
<a href="https://kimdubi.github.io/cloud/docker_mysql_repl/">https://kimdubi.github.io/cloud/docker_mysql_repl/</a>
<a href="https://jane096.github.io/project/mysql-master-slave-replication/">https://jane096.github.io/project/mysql-master-slave-replication/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[테스트 커버리지와 Jacoco]]></title>
            <link>https://velog.io/@geon_km/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BB%A4%EB%B2%84%EB%A6%AC%EC%A7%80%EC%99%80-Jacoco-7z9iawxx</link>
            <guid>https://velog.io/@geon_km/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BB%A4%EB%B2%84%EB%A6%AC%EC%A7%80%EC%99%80-Jacoco-7z9iawxx</guid>
            <pubDate>Tue, 25 Jun 2024 07:05:45 GMT</pubDate>
            <description><![CDATA[<h1 id="테스트-커버리지">테스트 커버리지</h1>
<ul>
<li>테스트 커버리지란 전체 코드에서 얼마나 테스트 코드를 작성이 실행이 되었는지 알 수 있는 지표를 의미를 한다.</li>
<li>일반적으로 테스트 커버리지를 통해 전반적으로 테스트가 부족한지 아니면 적정한지 알 수 있다고 많은 글에서 작성이 되어져 있지만 꼭 <strong>테스트 커버리지가 100%가 되는 것이 적절하지 않고 테스트 전략이 중요하다고 개인적으로 생각한다.</strong></li>
</ul>
<h2 id="테스트-커버리지-100">테스트 커버리지 100%?</h2>
<ul>
<li>테스트 커버리지는 단위 테스트 글에 의하면 100%를 해야지 올바른 방식이라고 확정하지 않는다.</li>
<li>테스트 커버리지는 일정 기중 이하 ( 일반적으로 60%라고 말한다. )이면 문제가 될 수 있다고 판단한다.<ul>
<li>이 부분도 물론 프로젝트에서 테스트의 전략에 따라서 중요하다고 생각한다.</li>
</ul>
</li>
<li>결국에는 글에서 말하는 내용은 <code>** 테스트 커버리지가 높다고 회귀 내성이 잘 지켜지고 있다고 알 수 없기 때문에 테스트 커버리지를 100% 목표로 테스트 코드를 작성해서는 안된다.**</code> </li>
<li>하지만 너무 낮은 테스트 커버리지는 회귀 내성과 일반적인 테스트가 수행이 안되기 때문에 개인적으로 60% 이상을 추천한다.</li>
</ul>
<h2 id="jacoco">Jacoco</h2>
<ul>
<li>jacoco는 jvm계열 언어에서 가장 많이 활용하는 커버리지의 도구이다. </li>
</ul>
<p><a href="https://docs.gradle.org/current/userguide/jacoco_plugin.html#sec:configuring_the_jacoco_plugin">https://docs.gradle.org/current/userguide/jacoco_plugin.html#sec:configuring_the_jacoco_plugin</a></p>
<p><a href="https://techblog.woowahan.com/2661/">https://techblog.woowahan.com/2661/</a></p>
<h3 id="gradle">gradle</h3>
<pre><code class="language-java">plugins {
    -- 추가
    id &#39;jacoco&#39;
}

-- 추가
jacoco {
    toolVersion = &quot;0.8.8&quot;
    reportsDirectory = layout.buildDirectory.dir(&#39;customJacocoReportDir&#39;)
}

-- 추가
test {
    finalizedBy jacocoTestReport
}

-- 추가
jacocoTestReport {
    reports {
        xml.required = false
        csv.required = false
        html.outputLocation = layout.buildDirectory.dir(&#39;jacocoHTML&#39;)
    }
    dependsOn test // jacocoTestReport 에 test라는 종속성을 추가
    finalizedBy &#39;jacocoTestCoverageVerification&#39;

    afterEvaluate {
        classDirectories.setFrom(
                files(classDirectories.files.collect {
                    fileTree(dir: it, excludes: [
                            &quot;**/*Config*&quot;,
                            &quot;**/*exception*&quot;,
                            &quot;**/*model*&quot;,
                            &quot;**/*Util*&quot;,
                            &quot;**/com/xx/xx/config/**&quot;,
                            &quot;**/com/xx/xx.class&quot;,
                            &quot;**/com/xx/xx.class&quot;,
                            &quot;**/com/xx/xx.class&quot;
                    ])
                })
        )
    }
}



jacocoTestCoverageVerification {
    violationRules {

        rule {
            enabled = true
            //코드 버커리지 체크 기준
            element = &#39;CLASS&#39;

            limit {
                counter = &#39;METHOD&#39;
                value = &#39;COVEREDRATIO&#39;
//                minimum = 0.5
            }
        }

    }
}


java {
    sourceCompatibility = &#39;17&#39;
}

ext[&#39;tomcat.version&#39;] = &#39;10.1.18&#39;

repositories {
    mavenCentral()
}

dependencies {
    // 생략
}

tasks.named(&#39;test&#39;) {
    useJUnitPlatform()
}

springBoot {
    buildInfo()
}
</code></pre>
<h3 id="jacoco-테스트-리포트-jacocotestreport">JaCoCo 테스트 리포트 (jacocoTestReport)</h3>
<p>JaCoCo 테스트 리포트는 테스트 결과를 리포트 형식으로 생성합니다. 사용자는 리포트 형식과 디렉토리를 지정할 수 있습니다.</p>
<h3 id="jacoco-테스트-커버리지-검증-jacocotestcoverageverification">JaCoCo 테스트 커버리지 검증 (jacocoTestCoverageVerification)</h3>
<p>JaCoCo 테스트 리포트 결과를 바탕으로 커버리지가 만족되는지 검증합니다. 이 과정에서 다음과 같은 옵션을 설정할 수 있습니다:</p>
<h4 id="검증-요소-element">검증 요소 (element)</h4>
<p>커버리지 체크 기준이 되는 요소는 다음 중 하나를 선택할 수 있습니다.</p>
<pre><code>BUNDLE (기본값): 패키지 번들
PACKAGE: 패키지
CLASS: 클래스 (클래스 단위로 브랜치와 라인 커버리지 체크)
SOURCEFILE: 소스파일
METHOD: 메소드
카운터 (counter)</code></pre><p>설정 가능한 카운터는 다음과 같습니다</p>
<pre><code>LINE: 빈 줄을 제외한 실제 코드의 라인 수
BRANCH: 조건문 등의 분기 수
CLASS: 클래스 수
METHOD: 메소드 수
INSTRUCTION (기본값): Java 바이트코드 명령 수
COMPLEXITY: 복잡도 (자세한 복잡도 계산은 JaCoCo 문서 참고)
값 (value)

TOTALCOUNT: 전체 개수
MISSEDCOUNT: 커버되지 않은 개수
COVEREDCOUNT: 커버된 개수
MISSEDRATIO: 커버되지 않은 비율 (0부터 1 사이의 숫자로, 1이 100%)
COVEREDRATIO (기본값): 커버된 비율 (0부터 1 사이의 숫자로, 1이 100%)
</code></pre><ul>
<li>verification에서 jacocoTestReport를 통해서 build하고 build &gt; report에서 index.html을 확인할 수 있습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/2d605f8c-bf59-4005-a9c0-6ddbee56c8ba/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안전한 AWS 네트워크 구성하기(VPC, Subnet, Route Table, Internet Gateway)]]></title>
            <link>https://velog.io/@geon_km/%EC%95%88%EC%A0%84%ED%95%9C-AWS-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0VPC-Subnet-Route-Table-Internet-Gateway-cnrk0hwq</link>
            <guid>https://velog.io/@geon_km/%EC%95%88%EC%A0%84%ED%95%9C-AWS-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0VPC-Subnet-Route-Table-Internet-Gateway-cnrk0hwq</guid>
            <pubDate>Thu, 23 May 2024 11:16:33 GMT</pubDate>
            <description><![CDATA[<h1 id="1-작성-배경">1. 작성 배경</h1>
<ul>
<li>취준생때 AWS를 학습하면서 모든 프로젝트를 인바운드, 아웃바운드를 모두 열고 사용하였다. 모두 열었을 때 악성 봇, 디도스등 악의적인 요청에 위협이 될수 있습니다. 업무를 하면서 온프로미스에서 AWS에서 넘어가면서 제일 처음 보안에 대해 생각하게 되어 학습한 내용을 정리하였습니다.</li>
</ul>
<h3 id="구현하려는-가장-간단한-aws-아키텍처">구현하려는 가장 간단한 AWS 아키텍처<img src="https://velog.velcdn.com/images/geon_km/post/ac0fe5fc-8b81-4ef5-8dff-fe091dd5e40d/image.png" alt=""></h3>
<ul>
<li>이번 글에서는 크게 VPC, Subnet(private, public), Route Table, Internet Gateway을 다루겠습니다. </li>
</ul>
<h1 id="2-aws-vpc-virtual-private-cloud">2. AWS VPC (Virtual Private Cloud)</h1>
<h2 id="2-1-vpc란">2-1. VPC란</h2>
<ul>
<li><p>vpc란 <code>큰 네트워크에서 용도 별로 네트워크를 쪼개서 사용하기 위한 서브 네트워크이다.</code> 간단히 설명하면 격리된 가상 데이터 센터라고 볼 수 있다. 보통 vpc를 ip로 구분해 나눈 후, 다중 AZ ( 가용영역 )에 걸쳐 서브넷을 구성합니다. 이것을 통하여 aws의 확장 가능한 인프라를 사용한다는 이점과 보안적인 이점, 네트워크를 제어할 수 있다는 장점이 있습니다.</p>
</li>
<li><p>VPC는 Amazon 콘솔에서 생성됩니다. 또한 하나의 VPC는 하나의 Region내에서만 생성이 가능하지만 두개 이상의 리전에 걸치는 것은 불가능합니다. 그렇지만  하나의 VPC는 여러개의 Amazon Availability Zone (이하 AZ) 에 걸쳐서 생성될 수 있습니다. 또한 가질 수 있는 IP 주소의 Range는 2^16 = 65535로 제한됩니다.</p>
</li>
</ul>
<h2 id="2-2-cidr">2-2. CIDR</h2>
<p>사이더는 ip 주소를 관리하는 체계이다.</p>
<ul>
<li><p>만약에 10.0.0.0/16 이라고 사용하면 16은 prefix를 의미한다.</p>
<ul>
<li>prefix란 ip를 2진수로 나타내면 앞에 2진수 16자리를 고정한다.</li>
</ul>
</li>
<li><p>즉 10.0.0.0 을 2진수로 표현하면 00001010.00000000/.0000000.000000인데 10.0은 16자리니깐 10.0만 고정하고 뒤에 0.0은 변경할 수 있다는 것을 의미한다.</p>
</li>
<li><p>하나의 수에는 256까지 사용가능한데 이때 0.0 총 2개니깐 256 * 256개의 수만큼 private ip를 사용할 수 있다는 것을 의미한다.</p>
</li>
</ul>
<h2 id="2-3-subnet">2-3. Subnet</h2>
<ul>
<li><p>서브넷이란 VPC를 나누는데 사용된다. VPC는 큰 네트워크를 쪼개서 사용하는 서브 네트워크라고 했는데 이때 VPC는 IP로 구분해 나눈 후, 다중 AZ(가용영역)에 걸쳐서 서브넷을 구성한다.</p>
</li>
<li><p>이때 서브넷은 public, private으로 분류한다.</p>
</li>
</ul>
<h2 id="2-4-가용영역">2-4. 가용영역</h2>
<ul>
<li>가용영역 : aws환경에서 물리적으로 분리되어 있는 데이터 센터를 논리적으로 묶은 인프라
예를 들면 리전에 대해서 들어봤을 것이다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/a4dd0cc0-f9e0-43f0-a1f2-959a67271226/image.png" alt=""></p>
<ul>
<li>여기서 리전은 도시, 가용영역은 주차장이라고 할 수 있다. 만약에 주차장에 문제가 생겨 주차를 못하여도, 도시의 다른 주차장을 이용할 수 있다. 이처럼 가용영역은 데이터센터에 장애가 발생해도 다른 데이터센터로 서비스를 제공하여 클라우드 HA을 보장한다.</li>
</ul>
<h2 id="2-5-internet-gateway">2-5. Internet Gateway</h2>
<p>vpc 한개에 internet gateway를 하나만 만들 수 있고 외부에서 public subnet에 접근할 수 있고 내부에서 외부롤 통신할 수 있는 통로이다.</p>
<ul>
<li><p>인터넷을 사용하기 위해서는 public IP, Route Table이 설정이 되어있어야지 가능하다.</p>
</li>
<li><p>route table은 트래픽을 어디에 보내줄지 직접 설정하는 것이다. 들어오는 트래픽은 vpc가 알아서 인스턴스에 전달을 해주지만 처리가 되고 나서 나가야 되는 트래픽들은 인터넷으로 내보내야 하는지 내부에서 사용해야 되는지 구분할 . 수없어서 route table로 internet gateway로 가라고 말해주어야 한다.</p>
</li>
<li><p>2개의 public subnet을 하나의 public route table로 관리를 한다.</p>
</li>
</ul>
<h2 id="2-6-nat-gateway">2-6. Nat Gateway</h2>
<p>private subnet은 외부에서 접근할  수 없다. 이때 외부에서 인터넷을 사용하기 위해서 들어오는 트래픽은 차단하고 나가는 트래픽만 허용해 주기 때문에 private subnet은 외부에서 접근할 . 수없다.</p>
<h1 id="3-vpc-생성하기">3. VPC 생성하기</h1>
<h2 id="3-1-vpc">3-1. vpc</h2>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/geon_km/post/7bfab657-9f8c-4995-9f15-ddea653d0186/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/geon_km/post/064d3b8e-3feb-4381-a888-515d356c8eb9/image.png" alt=""></th>
</tr>
</thead>
</table>
<ul>
<li>vpc에 접근하여 vpc 생성을 통해 vpc등을 선택한다. ( 이때 vpc만 생성을 통해서 모든 부분을 생성할 수 있지만 subnet, rb, igw를 자동으로 생성하는 방식을 소개하겠다. )</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/80701b89-e1b9-4a22-9eb4-3de7f53acd0a/image.png" alt=""></p>
<ul>
<li>public, private subnet을 각각 2개를 생성을 하고 각각의 CIDR를 설정을 합니다.</li>
<li>이후 NAT IG는 1개의 AZfh 설정하고 VPC 엔드포인트는 옵션입니다.</li>
</ul>
<h3 id="라우팅-테이블">라우팅 테이블</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/c2465625-4fa6-4a73-876f-3f0c266caa76/image.png" alt="">
라우팅 테이블에 들어가면 현재는 public이 생성이 되어져 있습니다. private subnet을 담당하는 table을 하나 만들어서 2개의 private subnet을 연결하고 라우팅에 nat, public subnet을 연결을 합니다.
<img src="https://velog.velcdn.com/images/geon_km/post/abbcf1b8-e6a5-4c30-8519-27b8bfb4e6d9/image.png" alt=""></p>
<h1 id="4-alb-설정">4. ALB 설정</h1>
<ul>
<li>alb는 load balancer로 외부의 요청을 앞단에 받아서 ec2에 부하를 분산을 시킬 수 있다. 여기서 HTTPS 인증을 수행하고 SSL Termination을 수행하여 WAS부터 내부 통신과 리스너 역활을 수행하기 위해서 사용을 하겠다.</li>
</ul>
<h4 id="네트워크-매핑">네트워크 매핑</h4>
<p><img src="https://velog.velcdn.com/images/geon_km/post/40f81e73-6ed4-405f-ae27-bdacfa504fcb/image.png" alt=""></p>
<ul>
<li>vpc를 선택하고 서브넷은 둘 다 public1, public2가 선택을 합니다. 이때 vpc는 방금 이전에 만든 vpc를 선택합니다. (기존에 default &gt; vpc )</li>
</ul>
<h4 id="보안-그룹">보안 그룹</h4>
<ul>
<li><p>alb에 접근하는 방법은 80, 22 포트를 허용합니다. 이때  22의 접근 아이피는 개인적인 ip를 허용하고 80포트는 모든 포트를 허용하게 만듭니다.</p>
</li>
<li><p>alb에 80과 22는 http, ssh를 적용을 합니다.</p>
</li>
</ul>
<h4 id="리스너">리스너</h4>
<ul>
<li>리스너에서는 대상 그룹을 선택을 합니다. 이때 <code>target group</code>을 하나 생성을 합니다. </li>
</ul>
<p>private subnet에 연결한 하나의 ec2를 생성을 합니다. 그렇게 되면 public ip가 없기 때문에 private subnet으로 접근이 가능합니다. 이것을 target group에 하나를 생성을 합니다.</p>
<p>방금 생성한 private subnet을 생성한 ec2에 80포트를 선택하고 등록합니다.
<img src="https://velog.velcdn.com/images/geon_km/post/fab96c56-ac5b-4c8c-8a60-526b64a5f0de/image.png" alt=""></p>
<h2 id="bastion-instance">Bastion Instance</h2>
<ul>
<li><p>바스티온 인스턴스는 가장 쉬운 private subnet 인스턴스에 접속할 수 있는 방식이다. 더욱 높은 보안성을 만들기 위해서는 <code>session manage</code>가 있습니다.</p>
</li>
<li><p><code>bastion instance</code>는 private subnet 인스턴스에 ssh를 통해 해당 인스턴스에 접속한 뒤 해당 인스턴스 내부에서 ssh를 통해 Private Subnet 내부의 인스턴스에 접속</p>
</li>
</ul>
<p>본인은 ftp를 통해 Private Instance에 접속할 수 있는 키를 Bastion Server에 올려두었다.
이후 <code>ssh -i idiot.pem ubuntu@[Private IP]</code> 명령어를 통해 Bastion Server에서 Private Instance로 접속이 가능하다.</p>
<p>HTTP 프로토콜을 통해 ALB의 DNS에 접속한다.
80포트가 열린 ALB는 리스너를 통해 80포트로 접속했음을 알 수 있다.
8080포트가 열린 타겟그룹으로 해당 트래픽이 8080포트로 전달된다.
8080포트가 열린 Private Instance는 해당 트래픽을 전달받을 수 있다.
이를 통해 아파치 톰캣 위에 올려진 스프링부트 웹서버의 URL로 접속이 가능하다.</p>
<h1 id="참고-문헌">참고 문헌</h1>
<hr>
<p><a href="https://velog.io/@sophia5460/AWS-ALB%EB%A5%BC-%ED%86%B5%ED%95%B4-Private-Instance-%EC%86%8D-Spring-Boot-%EC%9B%B9-%EC%84%9C%EB%B2%84-%EC%A0%91%EC%86%8D%ED%95%98%EA%B8%B0">https://velog.io/@sophia5460/AWS-ALB%EB%A5%BC-%ED%86%B5%ED%95%B4-Private-Instance-%EC%86%8D-Spring-Boot-%EC%9B%B9-%EC%84%9C%EB%B2%84-%EC%A0%91%EC%86%8D%ED%95%98%EA%B8%B0</a></p>
<p><a href="https://minjii-ya.tistory.com/32?category=946161">https://minjii-ya.tistory.com/32?category=946161</a></p>
<p><a href="https://tech.cloud.nongshim.co.kr/2018/10/16/4-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0vpc-subnet-route-table-internet-gateway/">https://tech.cloud.nongshim.co.kr/2018/10/16/4-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0vpc-subnet-route-table-internet-gateway/</a></p>
<p><a href="https://www.44bits.io/ko/post/understanding_aws_vpc">https://www.44bits.io/ko/post/understanding_aws_vpc</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS Aurora 오프라인 세미나]]></title>
            <link>https://velog.io/@geon_km/AWS-DB-%EB%94%94%EB%B9%84%EB%94%A5Aurora-%EC%98%A4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%84%B8%EB%AF%B8%EB%82%98-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@geon_km/AWS-DB-%EB%94%94%EB%B9%84%EB%94%A5Aurora-%EC%98%A4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%84%B8%EB%AF%B8%EB%82%98-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Tue, 21 May 2024 11:09:26 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/geon_km/post/9248151b-ad69-4663-80dc-75659b8d0633/image.jpeg" alt=""></p>
<h1 id="서론">서론</h1>
<hr>
<ul>
<li><p>회사에서 이번에 온프로미스에서 클라우드로 전환을 생각하면서 AWS 오프라인 세미나 참석 기회를 얻어서 AWS Korea에 방문하여 간단한 세미나를 참석을 했습니다.</p>
</li>
<li><p>각 날짜마다 다른 컨셉의 강의가 진행이 되었는데 저는 AWS Aurora를 선택을 했습니다. ( 다른 내용은 너무 어려워서 선택하지 못했습니다. )</p>
</li>
<li><p>세미나는 10~ 18시까지 진행을 하였고, 2~3시간 실습이 있어서 재미있게 세미나를 즐기고 왔습니다. 일단 이번에 포스팅 내용은 같이 못같던 팀원에게 공유, 세미나에서 이해하지 못한 내용을 간단하게 정리하기 위해서 작성을 하였습니다.</p>
</li>
</ul>
<h1 id="본론">본론</h1>
<hr>
<h2 id="1-aws-aurora-overview">1. AWS Aurora Overview</h2>
<h3 id="11-amazon-aurora">1.1 Amazon Aurora</h3>
<ul>
<li>관리형 서비스로 제공되는 오픈 소스 가격의 엔터프라이즈 데이터베이스</li>
</ul>
<p><strong>특징</strong></p>
<ol>
<li>상용 데이터베이스의 속도 및 가용성</li>
<li>오픈 소스 데이터베이스의 단순성과 비용 효율성</li>
<li>MySQL, PostSQL과의 호환성</li>
<li>사용한 만큼 지불하는 종량제 가격</li>
</ol>
<h3 id="12-기존의-온프로미스-데이터베이스에서-클라우드로-변경을-하면-이점">1.2 기존의 온프로미스 데이터베이스에서 클라우드로 변경을 하면 이점</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/117fd4c2-3323-45c8-841b-2376a7e8968b/image.png" alt=""></p>
<ul>
<li><p>기존에 온프로미스에서는 db의 운영하면서 많은 관리가 필요합니다. </p>
<ul>
<li>예를 들어서 fail over 또는 보안, 백업, 많은 부화를 스케일업</li>
</ul>
</li>
<li><p>온프로미스에서 클라우드로 변경을 하였을 때 운영적인 측면을 클라우드가 대체하고 스키마, 쿼리만 신경을 쓰면된다.</p>
<ul>
<li>AWS, Lambda, S3, IAM, Cloudwatch등 클라우드 에코시스템을 활용이 가능하다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>이러한 이점보다 제일 중요한 것은 Auora는 빠르다라고 말한다. 세미나에서 말하기로 다른 유사한 기능을 하는 DB는 일반적으로 1.5~2배정도 MySQL에 비해서 빠르다고 하지만 Auora는 최대 5배 빠르다고 말하였다.</p>
</blockquote>
<h3 id="13-aurora-architecture-decoupled-computing--storage">1.3 Aurora Architecture (Decoupled Computing &amp; Storage)</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/0d5fea03-6935-4475-b5aa-cc228aec2b77/image.png" alt=""></p>
<ul>
<li><p>연산을 위한 <code>computing 영역과 스토리지 영역은 서로  Life Cycle이 다르기 때문에</code> 서로 영향을 주면 빠르게 변화에 대응하기 힘들다.</p>
<ul>
<li>데이터가 많아져 스토리지 영역을 확장할 때 Computing 영역의 down time으로 이것을 방해하면 안된다. 두 개가 서로 분리되어져 있기 때문에 각 기능에만 포커싱하여 가용성과 확장성을 보장한다.</li>
</ul>
</li>
<li><p>Aurora의 Computing zone은 AZ을 기반으로 Master-Relica구조를 통해 장애 및 확장성에 대응한다. 최대 15개 Replica을 통해 확장성을 확보한다.</p>
</li>
</ul>
<h3 id="132-스토리지-영역">1.3.2 스토리지 영역</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/a0f27b8c-5230-448e-bc0b-9808961bbc83/image.png" alt=""></p>
<ul>
<li>Aurora의 스토리지는 <code>공유 분산 스토리지 볼륨</code>으로 구성되어져 있다. 이는 여러 개의 스토리지 노드가 하나의 스토리지 볼륨이 되어서 각 컴퓨팅의 노드가 된다.<ul>
<li>각 노드가 분리되어져 있기 때문에 스토리지에서 발생하는 I/O 작업이 분산되어 병렬처리가 된다.</li>
</ul>
</li>
</ul>
<h3 id="133-6-way-copy">1.3.3 6-way copy</h3>
<ul>
<li><p>각각의 스토리지 노드에 각각의 데이터가 위치 되어져 있기 때문에 데이터의 가용성 확보 ( 각 스토리지에 보면 빨간색 볼륨이 2개씩 * 3가 있다. )</p>
<ul>
<li>이러한 volume의 집합을 <code>protection Group</code>이라고 말한다.</li>
</ul>
</li>
<li><p>물리적으로 분리된 3가지 노드에 총 6개의 복제본을 통해 데이터의 가용성 확보는 스토리지 내에서 수행한다. </p>
<ul>
<li>데이터의 I/O는 projectio Group에 있는 6개의 복제본을 이용 Quorum 방식을 통하여 안전성을 확보한다.<ul>
<li>읽기의 경우에는 3개, 쓰기의 경우에는 4개의 블럭이 필요하다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>예를 들어서 하나의 AZ에 문제가 생겼다고 가정하겠다.
<img src="https://velog.velcdn.com/images/geon_km/post/86f2b342-d9be-4b3a-96b8-f17653ecc0ee/image.png" alt=""></p>
<ul>
<li><p>이렇게 되면 2개의 블록을 사용할 수 없다. 이 경우에는 읽기, 쓰기를 사용하기에 문제가 없다. ( 읽기의 경우에는 3개, 쓰기의 경우에는 4개가 필요하기 때문 )</p>
</li>
<li><p>하지만 총 2개의  A/Z에 문제가 생겼다고 가정하겠다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/a730fa0c-ac15-4c75-890c-421445b7e5d3/image.png" alt=""></p>
<ul>
<li>이렇게 된다면 읽기는 가능하지만 쓰기의 경우에는 불가능하게 된다.</li>
<li>이런 방식을 통하여 데이터의 유실을 방지하고 안전성을 확보할 수 있다.</li>
</ul>
<blockquote>
<p>Aurora 분산스토리지 제공</p>
</blockquote>
<ul>
<li>redo log 처리, 내결함성, 자가 복구 스토리지, 빠른 데이터베이스 복제 , db backtrack, 스냅샷, 확장성등 스토리지 처리와 관련된 행동은 Decoupled이 되어져 있기 때문에 트랜잭션, SQL 쿼리에 영향 없이 처리할 수 있다.</li>
</ul>
<h3 id="14-bluegreen-배포">1.4 Blue/Green 배포</h3>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/geon_km/post/1b970201-46b3-42d2-b55d-55a905bbd5ee/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/geon_km/post/b827fbf9-1978-448c-9b50-0ef30d4ed837/image.png" alt=""></th>
</tr>
</thead>
</table>
<ul>
<li><p>배포를 해보면  blue, green에 대해서 한번 쯤 보았을 것이다. 세미나에서 듣기로는 B/G 배포를 더욱 Develop한다고 들었던 것 같다.</p>
</li>
<li><p>Blue/Green에 대해서 간단하게 설명하면 2개의 node를 만든다. 이때 blue노드는 기존의 디비를 의미하며 green노드를 만든다. green 노드는 미러링된 복사본 즉. 미래의 프로덕션을 의미한다.</p>
</li>
<li><p>만약에 데이터 구조를 마이그레이션을 하게 된다면 blue에서 복제된 Green 노드를 만들어 데이터를 마이그레이션을 하고 테스트를 수행을 한다. 이때 기존의 blue노드 (프로덕션 디비)는 정상적으로 운영되기 때문에 운영에는 상관없고 green에서 QA를 검증하고 green으로 변경한다면 안전성 높은 배포를 수행할 수 있다.</p>
</li>
</ul>
<h3 id="142-bluegreen-과정">1.4.2 Blue/Green 과정</h3>
<ol>
<li><p>현재 운영 중인 DB 클러스터(예: mycluster-old1)가 있다고 가정합니다. 이 클러스터는 Aurora MySQL 2.10.2 (5.7) 버전을 사용하고 있습니다.</p>
</li>
<li><p>create-blue-green-deployment 명령을 사용하여 새로운 Target DB 클러스터(예: mycluster-green-x1234)를 생성합니다. 이 클러스터는 소스 클러스터와 동일한 버전 및 구성을 가집니다.</p>
<pre><code class="language-mysql">aws rds create-blue-green-deployment \
 --source-db-cluster-identifier mycluster-old1 \
 --target-db-cluster-identifier mycluster-green-x1234</code></pre>
</li>
<li><p>Target 클러스터가 생성되면 소스 클러스터의 데이터가 자동으로 복제됩니다. 이 과정에서 Target 클러스터는 읽기 전용(RO) 모드로 유지됩니다.</p>
</li>
<li><p>애플리케이션 트래픽을 새 Target 클러스터로 전환하기 위해 switchover-blue-green-deployment 명령을 사용합니다. 이 명령은 DB 클러스터 엔드포인트를 새 클러스터로 업데이트하고, 새 클러스터를 읽기/쓰기(RW) 모드로 전환합니다.</p>
<pre><code class="language-shell">aws rds switchover-blue-green-deployment \
 --blue-green-deployment-identifier mycluster-green-x1234</code></pre>
</li>
<li><p>Switchover가 완료되면 애플리케이션 트래픽이 새 클러스터로 라우팅됩니다. 이제 mycluster-green-x1234가 프로덕션 트래픽을 처리하게 됩니다.</p>
</li>
<li><p>필요에 따라 이전 클러스터(mycluster-old1)를 삭제할 수 있습니다. 이는 delete-blue-green-deployment 명령을 사용하여 수행할 수 있습니다.</p>
<pre><code class="language-shell">aws rds delete-blue-green-deployment \
 --blue-green-deployment-identifier mycluster-old1</code></pre>
</li>
</ol>
<h2 id="2-백업--운영">2. 백업 &amp; 운영</h2>
<h3 id="21-automated-backups--point-in-time-">2.1 Automated backups ( Point in time )</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/5edb92e2-5787-4774-9126-151cfafb4f34/image.png" alt=""></p>
<ul>
<li>전체 인스턴스의 예약된 일일 볼륨을 백업한다.
아카이브 데이터베이스 변경 로그, 최대 보존 기간은 35일이다.
데이터베이스 성능에 미치는 영향을 최소화하며 다중 AZ실행 시 standby에서 수행한다.</li>
</ul>
<h3 id="22-snapshots">2.2 snapshots</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/27e23db1-8c96-4e80-b6b2-c8507b8d14cb/image.png" alt=""></p>
<ul>
<li>스냅샷을 만드는 방식은 증분 백업과 비슷하다. aws ebs -&gt; 볼륨을 스냅샷을 만든다. 처음에는 전체를 백업하지만 이후에는 증분 백업을 수행하기 때문에 더 비용이 효율적이고, 더 빠르다.</li>
</ul>
<h3 id="23-log-backup">2.3 Log Backup</h3>
<ul>
<li>소산 백업 및 각종 이유로 여러가지 log를 따로 보관해야 하는 경우 존재 다운로드 가능한 log 목록<ul>
<li>audit log(감사): MySQL에서 실행되는 쿼리와 사용자 활동을 추적하고 기록하는 로그입니다.</li>
<li>slow log(느린 쿼리 로그):long_query_time 변수로 지정된 시간 이상 실행되는 쿼리를 기록합니다.</li>
<li>error log(오류): MySQL 서버에서 발생하는 오류, 경고 및 중요한 이벤트를 기록하는 로그입니다.</li>
<li>binlog(바이너리): 데이터 변경 이벤트(INSERT, UPDATE, DELETE 등)를 순서대로 기록하는 로그입니다.</li>
</ul>
</li>
</ul>
<h3 id="231-console-download">2.3.1 console download</h3>
<ol>
<li><a href="https://console.aws.amazon.com/rds/%EC%97%90%EC%84%9CAmazonRDS%EC%BD%98%EC%86%94%EC%9D%84%EC%97%BD%EB%8B%88%EB%8B%A4">https://console.aws.amazon.com/rds/에서AmazonRDS콘솔을엽니다</a>.</li>
<li>탐색창에서데이터베이스를선택합니다.</li>
<li>보고자하는로그파일을보유한DB인스턴스의이름을선택합니다.</li>
<li>로그및이벤트탭을선택합니다.</li>
<li>아래로스크롤하여[Logs]섹션을찾습니다.</li>
<li>로그섹션에서다운로드할로그옆에있는버튼을선택한다음다운로드를선택합니다.</li>
<li>제공된링크에대한컨텍스트(마우스오른쪽클릭)메뉴를열고나서[SaveLinkAs]를선택합니다.
로그 파일을 저장할 위치를 입력한 다음 저장을 선택합니다</li>
</ol>
<h3 id="232-log-backup--binlog-download">2.3.2 log backup – binlog download</h3>
<p>mysqlbinlog 유틸리티를 사용하여 RDS for MySQL DB 인스턴스에서 이진 로그를 다운로드하거나 스트리밍 가능 이진 로그를 로컬 컴퓨터로 다운로드하면 mysql 유틸리티를 사용하여 로그 재생과 같은 작업을 수행 가능</p>
<pre><code>Amazon RDS 인스턴스에 대해 mysqlbinlog 유틸리티를 실행하려면 다음의 옵션 사용

--read-from-remote-server

- 필수
--host – 인스턴스의 엔드포인트에서 DNS 이름
--port – 인스턴스에서 사용되는 포트
--user - REPLICATION SLAVE 권한이 부여된 MySQL 사용자
--password – MySQL 사용자의 암호. 또는 유틸리티에서 암호 입력을 요구하는 메시지가 표시되도록 암호 값을 생략 --raw - 파일을 이진 형식으로 다운로드
--result-file - 원시 출력을 수신할 로컬 파일
--stop-never - 이진 로그 파일 스트리밍
--verbose - ROW binlog 형식을 사용할 경우 행 이벤트를 유사 SQL 문으로 조회 가능
--verbose 옵션에 대한 자세한 내용은 MySQL 설명서의 mysqlbinlog 행 이벤트 표시 참조</code></pre><p>RDS는 보통 최대한 빨리 이진 로그를 제거하지만, mysqlbinlog가 액세스할 수 있도록 인스턴스에서 이진 파일을 여전히 사용할 수 있어야 합니다. RDS가 이진 파일을 보존할 시간을 지정하려면 mysql.rds_set_configuration 저장 프로시저를 사용하고 로그를 다운로드하기에 충분한 시간으로 기간을 지정합니다.</p>
<p>보존 기간을 설정한 후, DB 인스턴스의 스토리지 사용량을 모니터링하여 보존된 이진 로그가 너무 많은 스토리지를 차지하지 않도록 합니다. 다음 예제에서는 보존 기간을 1일로 설정합니다.
<code>call mysql.rds_set_configuration(&#39;binlog retention hours&#39;, 24);</code></p>
<h2 id="운영">운영</h2>
<h3 id="241-write-intensive-performance_insight">2.4.1 Write-Intensive Performance_insight</h3>
<blockquote>
<p>parameter변경은 신중하게 해야된다. 사이드이펙트의 변화를 확인해야된다.
무수히 많은 파라미터가 다 true일 수 있는데 반드시 하나씩 테스트를 거쳐 의미있는 변화가 있을 때 고민을 해야된다.</p>
</blockquote>
<p>Performance_schema를 On/Off 할 때에는 재부팅 필요 Performance_insight를 On/Off 할 때에는 재부팅 불필요하다.</p>
<blockquote>
<p>두 옵션 모두 on을 하는 것을 권고한다.</p>
</blockquote>
<ul>
<li>Performance_schema의 사용은 추가 Memory와 성능에 영향</li>
<li>Performance_schema는 여러가지 옵션이 있으며 옵션 추가 마다 Memory 사용량과 워크로드 변화</li>
<li>장애 상황과 각종 이슈에 대하여 기본적인 대응을위해서는 기본 옵션으로 사용 가능</li>
<li>Deep한 이슈 분석 혹은 장애 분석을 위해서는 추가적인 옵션을 켜고 사용 가능. 예) event, memory</li>
</ul>
<pre><code>• innodb_max_dirty_pages_pct : default 75 / 버퍼 풀에서 더티 페이지의 최대 백분율
• innodb_page_cleaners : default 4 / 버퍼 풀 인스턴스에서 더티 페이지를 플러시하는 페이지 클리너 스레드 수입니다.
• innodb_purge_threads : default 3 / InnoDB purge 작업에 할당된 백그라운드 스레드 수
• innodb_lru_scan_depth : default 1024 / InnoDB 버퍼 풀의 플러시 작업에 대한 알고리즘 및 휴리스틱에 영향을 미치는 매개변수입니다.
• Innodb_flush_log_at_trx_commit : default 1 / Innodb 트랜잭션 내구성 결정
• innodb_sync_spin_loops : default 30 / Thread가 일지 중지 되기 전 innoDB mutex가 해제 되기까지 기다리는 횟수</code></pre><h3 id="242-dns-ttl">2.4.2 DNS TTL</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/0748e6d9-e39f-426f-af9c-db8ac3b65ffc/image.png" alt=""></p>
<ul>
<li><p>애플리케이션에서는 데이터베이스 접속을 위해 dns를 통해 도메인을 ip주소로 반환한다. 이때 얻은 ip주소를 캐시에 저장하여 일정 시간 동안 재사용하여 dns 조회의 비용을 줄인다.</p>
</li>
<li><p>하지만 ttl값이 너무 크게되면 데이터베이스가 fail over가 되었을 때 커넥션 문제가 발생할 수 있다.</p>
</li>
</ul>
<blockquote>
<p>자바에서 dns caching ttl 설정 변경
java에서는 java vm을 완전히 down을 시키고 was를 재구동을 해야된다. 이때 ttl을 사용하도록 securitymanager의 policy를 수정해야된다.</p>
</blockquote>
<pre><code class="language-java">networkaddress.cache.ttl=60

private static void disableAddressCache() {
        Security.setProperty(&quot;networkaddress.cache.ttl&quot;, &quot;0&quot;);
        Security.setProperty(&quot;networkaddress.cache.negative.ttl&quot;, &quot;0&quot;);
    }</code></pre>
<p><a href="https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/jvm-ttl-dns.html">https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/jvm-ttl-dns.html</a></p>
<h3 id="243-rdsproxy">2.4.3 RDSProxy</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/6685dc86-cf10-4bd7-a74a-7fc178db4750/image.png" alt=""></p>
<ul>
<li><p>Amazon RDS 프록시 는 말 그대로 RDS의 연결을 관리하는 프록시 서비스이다. 즉. 프록시이기 때문에 특정 db가 장애가 발생해도 예비 db로 서비스 장애 시간을 최소화할 수 있게 만들어준다.</p>
</li>
<li><p>이것은 멀티플렉싱으로 트랜잭션 간 데이터베이스 연결 공유를 할 수 있게 만드며, 수십만 개의 연결을 지원하도록 확정성을 보장한다.</p>
</li>
</ul>
<blockquote>
<p>만약에 failover가 발생을 하더라도 빠르게 복구할 수 있다.</p>
</blockquote>
<ul>
<li>failover가 발생하면 유저가 요청한 것이 사라지는 것이 아니라 트랜잭션이 대기열에 추가된다.</li>
<li>nds 캐시 및 다운스트림 ttl을 우회하여 장애를 감지하고 대기에 더 빠르게 연결할 수 있다.</li>
</ul>
<h3 id="244-rdsproxy-한계">2.4.4 RDSProxy 한계</h3>
<ol>
<li><p>proxy를 사용하면 장점이 있지만 트레이드 오프로 단점도 존재한다. 예를 들어서 spring의 경우에는 spring boot 2.x 버전부터 기본적으로 hikari cp를 사용하여 connection pool을 사용한다. 
RDSProxy는 애플리케이션에 connection pool이 있는 경우에는 connectio issue가 발생한다.</p>
</li>
<li><p>Failover로 connection이 옮겨가는 도중에 Query 수행이 아닌 대기상태로 된다.</p>
</li>
</ol>
<h3 id="245-aurora에서-connection-분산과-확장-팁">2.4.5 Aurora에서 Connection 분산과 확장 팁</h3>
<ol>
<li>DNS TTL을 가능한 작게</li>
<li>Connection pool을 사용할 경우 Connection pool lifetime을 적정수치 이하로 축소</li>
<li>Database에 접근하는 API(Client)는 작은 타입을 여러 대 사용하는 것이 유리. Min/Max pool 조절</li>
<li>API level이 B/G로 분산하는 경우 switch over 전에 Green 환경에서 미리 connection 구성</li>
</ol>
<h1 id="출처">출처</h1>
<hr>
<p><a href="https://kr-resources.awscloud.com/data-kr-aws-innovate/amazon-aurora-database-intensive-analytics-kr-level-300">https://kr-resources.awscloud.com/data-kr-aws-innovate/amazon-aurora-database-intensive-analytics-kr-level-300</a></p>
<p><a href="https://www.youtube.com/watch?v=7_VXMqYixS4&amp;t=693s">https://www.youtube.com/watch?v=7_VXMqYixS4&amp;t=693s</a></p>
<p><a href="https://docs.aws.amazon.com/ko_kr/AmazonRDS/latest/AuroraUserGuide/CHAP_AuroraOverview.html">https://docs.aws.amazon.com/ko_kr/AmazonRDS/latest/AuroraUserGuide/CHAP_AuroraOverview.html</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 Dynamic Datasource Routing]]></title>
            <link>https://velog.io/@geon_km/%EC%8A%A4%ED%94%84%EB%A7%81-Multi-DataSource</link>
            <guid>https://velog.io/@geon_km/%EC%8A%A4%ED%94%84%EB%A7%81-Multi-DataSource</guid>
            <pubDate>Tue, 16 Apr 2024 13:43:01 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<hr>
<ul>
<li><p>안녕하세요. 이번에는 스프링 다중 데이터소스 라우팅에 대해서 정리를 하겠습니다. 프로젝트를 하다보면 하나의 DB를 바라보는 경우는 거의 드물기 때문에 설정에 대해서 한번 보시면 도움이 될 수 있다고 생각하여 정리를 했습니다.</p>
</li>
<li><p>다중 데이터 소스를 통하여 데이터베이스를 관리할 수 있지만 어쩔 수 없이 <code>Dynamic-Datasource-routing</code>을 사용해야 되는 경우가 있습니다. ( 저도 없을 줄 알았는데 결국 생기네요... )</p>
</li>
<li><p>간단하게 설명하자면 mybatis를 사용하면서 sqlSessionTemplate을 사용하는데 db가 변경이 되면 sqlSessionTemplate도 바뀌어야 하는 문제입니다.</p>
</li>
</ul>
<br/>


<h1 id="본론">본론</h1>
<hr>
<h2 id="1-다중-데이터소스">1. 다중 데이터소스</h2>
<ul>
<li><p>데이터베이스를 만약에 2개 이상 사용하게 된다면 각각의 <code>DataSource</code>를 관리 및 트랜잭션을 따로 관리를 해야합니다. 그렇게 하기 위해서는 각각의 <code>DataSource</code>를 설정을 해줘야한다.</p>
</li>
<li><p>일단은 application.yml을 시작으로 설정하는 방법에 대해서 자세하게 설명을 하겠습니다.</p>
</li>
</ul>
<pre><code class="language-yaml">spring:
  kmg:
    datasource:
      hikari:
        jdbc-url: jdbc:mysql://localhost:3306/KMG?serverTimezone=Asia/Seoul
        username: root
        password: 1234
        driver-class-name: com.mysql.jdbc.Driver

  foo:
    datasource:
      hikari:
        jdbc-url: jdbc:mysql://localhost:3306/foo?serverTimezone=Asia/Seoul
        username: root
        password: 1234
        driver-class-name: com.mysql.jdbc.Driver
</code></pre>
<ul>
<li>각각의 dataSource를 다른 이름으로 yml 파일에 작성을 합니다. 이후 config 폴더에 DataSourceConfig에 각각의 설정을 작성을 합니다.</li>
</ul>
<h3 id="datasourceconfig">DataSourceConfig</h3>
<pre><code class="language-java">@Configuration
public class DataSourceConfig {

    @Primary //autowired 우선 적용
    @Bean(name = &quot;dataSource&quot;)//수동 빈 등록
    @ConfigurationProperties(prefix = &quot;spring.kmg.datasource&quot;) //properties 파일의 key 값을 묶어서 Bean 등록
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    } //데이터 소스 반환

    @Bean(name = &quot;transactionManager&quot;) // 수동 빈 등록, @Qualifier : 자동 주입할 빈을 지정
    public PlatformTransactionManager transactionManager(@Qualifier(&quot;dataSource&quot;) DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource); //datasource를 이용한 Transaction 처리를 위한 구현체
    }

    @Bean(name = &quot;sqlSessionFactory&quot;) //수동 빈 등록, ApplicationContext : IOC엔진
    public SqlSessionFactory sqlSessionFactory(@Qualifier(&quot;dataSource&quot;) DataSource dataSource, ApplicationContext applicationContext) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); //SqlSessionFactoryBean이란 MyBatis와 DB 연동
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(&quot;classpath:/mapper/*.xml&quot;)); //mapper안에 있는 XML파일 총괄 등록
        return sqlSessionFactoryBean.getObject();
    }

    @Bean(name = &quot;sqlSessionTemplate&quot;)
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier(&quot;sqlSessionFactory&quot;) SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }


    @Bean(name = &quot;dataSourceFoo&quot;)
    @ConfigurationProperties(prefix = &quot;spring.foo.datasource&quot;)
    public DataSource dataSourceFoo() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = &quot;transactionManagerFoo&quot;)
    public PlatformTransactionManager transactionManagerFoo(@Qualifier(&quot;dataSourceFoo&quot;) DataSource dataSourceSBS) {
        return new DataSourceTransactionManager(dataSourceSBS);
    }

    @Bean(name = &quot;sqlSessionFactoryFoo&quot;)
    public SqlSessionFactory sqlSessionFactoryFoo(@Qualifier(&quot;dataSourceFoo&quot;)  DataSource dataSourceSBS, ApplicationContext applicationContext) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceFoo);
        sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(&quot;classpath:/mapper/*.xml&quot;));
        return sqlSessionFactoryBean.getObject();
    }

    @Bean(name = &quot;sqlSessionTemplateFoo&quot;)
    public SqlSessionTemplate sqlSessionTemplateFoo(@Qualifier(&quot;sqlSessionFactoryFoo&quot;) SqlSessionFactory sqlSessionFactoryFoo) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactorySBS);
    }</code></pre>
<ul>
<li>위에 소스를 보면 각각의 DataSource를 명시하고 각각의 트랜잭션 매니저, sqlSessionTemplate을 작성을 합니다. 이를 통하여 각각의 데이터베이스에 대한 통신을 하는 DAO 부분에 사용하여 여러 개의 데이터베이스와 작업을 수행을 할 수 있습니다.</li>
</ul>
<h3 id="primary">@Primary</h3>
<p>@Primary란 같은 타입의 빈을 2개 이상 생성할 때, 하나의 빈에게 더 높은 선호도를 제공하기 위해서 사용한다. kmg, foo 2개의 datasource에서 나는 빈의 이름을 지정하지 않으면 kmg를 우선순위에 부여하기 위해서 사용을 하였다. </p>
<blockquote>
<p>왜 Primary를 사용할까?</p>
</blockquote>
<ul>
<li>스프링 컨테이너가 올라갈 때, 스프링은 컴포넌트를 스캔을 합니다. 이때 스프링은 싱글톤 전략을 하기 때문에 한가지 타입의 빈은 한번만 생성된다. 하지만 bean으로 생성된 객체에서 스프링 빈이 있다면 어느 것을 생성해야 하기 때문에 <code>NoUniqueBeanDefinitionException</code>이 발생한다.</li>
</ul>
<h3 id="qualifier">@Qualifier</h3>
<ul>
<li>@Autowired 어노테이션은 스프링에서 빈에 의존성을 주입하기 위해 사용되는 방법이다. 이 방법은 아주 유용하여 매우 자주 사용된다.
스프링은 타입으로 해당 빈을 찾는다. @Autowired 를 통한 의존성 주입 시, 같은 타입의 빈이 하나 이상이라면, autowiring 할 대상이 unique 하지 않기 때문에 마찬가지로 NoUniqueBeanDefinitionException 을 던지게 된다.</li>
</ul>
<br/>

<h3 id="service">Service</h3>
<pre><code class="language-java">    @Transactional(transactionManager = &quot;transactionManagerKMG&quot;)
    public int test() {
        return testDao.testDao();
    }

    @Transactional(transactionManager = &quot;transactionManagerFoo&quot;)
    public int test() {
        return testDao.testDao();
    }</code></pre>
<ul>
<li><p>각각의 서비스 로직에 대해서 트랜잭션을 설정하여 데이터의 정합성을 맞출 수 있습니다. 만약에 dao에서 sqlSessionTemplate이 foo를 사용하고 트랜잭션을 kmg을 사용한다고 생각해보자. 그러면 데이터는 삽입이 되겠지만 트랜잭션은 동작하지 않습니다. </p>
</li>
<li><p>각각의 dao 부분과 트랜잭션을 맞추어 데이터의 acid를 맞출 수 있습니다.</p>
</li>
</ul>
<h3 id="dao">DAO</h3>
<pre><code class="language-java">@Repository
public class AdminDAO {

    private final SqlSessionTemplate sqlSessionTemplate;

    public AdminDAO(@Qualifier(&quot;sqlSessionTemplate&quot;)SqlSessionTemplate sqlSessionTemplate) {
        this.sqlSessionTemplate = sqlSessionTemplate;
    }

    // 나머지 코드...
}</code></pre>
<ul>
<li>sqlSessionTemplate에 각각의 Qualifier를 통해서 빈을 명시할 수 있습니다. 이때 명시하지 않는다면 config부분에 작성한 primary가 우선권을 얻기 때문에 선택이 됩니다. 각각의 db 작업에 맞게 sqlSessionTemplate을 선택을 해야됩니다.</li>
</ul>
<br/>


<h2 id="2-다중-데이터소스-라우팅">2. 다중 데이터소스 라우팅</h2>
<ul>
<li>스프링 2.0.1에서 <code>AbstractRoutingDataSource</code>를 제공합니다. 이것은 여러 개 DataSource에서 라우팅을 해주는 중재자 역할을 합니다.</li>
</ul>
<pre><code class="language-java">public class DynamicDataSourceContextHolder {
    private static final ThreadLocal&lt;String&gt; CONTEXT_HOLDER = new ThreadLocal&lt;&gt;();

    public static void setDataSourceKey(String dataSourceKey) {
        CONTEXT_HOLDER.set(dataSourceKey);
    }

    public static String getDataSourceKey() {
        return CONTEXT_HOLDER.get();
    }

    public static void clearDataSourceKey() {
        CONTEXT_HOLDER.remove();
    }
}</code></pre>
<ul>
<li>contextHolder를 통해서 멀티 데이터소스를 알 수 있습니다.  참조를 보유하는 것 외에도 참조를 CRUD하는 작업을 합니다. 여기서 컨텍스트는 실행 중인 스레드에 바인딩되도록 하기 위해서 <code>ThreadLocal</code>을 사용합니다.</li>
</ul>
<br/>

<pre><code class="language-java">public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {

        return DynamicDataSourceContextHolder.getDataSourceKey();
    }
}</code></pre>
<ul>
<li>DynamicRoutingDataSource는 위에서 만들었던 contextHolder에 키를 가져오는 로직을 분리를 하였습니다.</li>
</ul>
<br/>

<pre><code class="language-java">@Configuration
public class DataSourceConfig {

    @Primary
    @Bean(name = &quot;test1&quot;)
    @ConfigurationProperties(prefix = &quot;spring.kmg.datasource.hikari&quot;)
    public DataSource test1DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = &quot;test2&quot;)
    @ConfigurationProperties(prefix = &quot;spring.foo.datasource.hikari&quot;)
    public DataSource test2DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public DataSource dynamicDataSource(@Qualifier(&quot;test1&quot;) DataSource test1DataSource,
                                        @Qualifier(&quot;test2&quot;) DataSource test2DataSource) {
        DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
        Map&lt;Object, Object&gt; dataSourceMap = new HashMap&lt;&gt;();
        dataSourceMap.put(&quot;test1&quot;, test1DataSource);
        dataSourceMap.put(&quot;test2&quot;, test2DataSource);

        dynamicDataSource.setDefaultTargetDataSource(test2DataSource);
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        return dynamicDataSource;
    }

    @Bean
    public DataSourceTransactionManager transactionManager(@Qualifier(&quot;dynamicDataSource&quot;) DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

}</code></pre>
<ul>
<li><p>AbstractRoutingDataSource에서 각각의 클라이언트를 명시하고 contextHolder의 키를 반환하는 메서드를 받아옵니다. AbstractRoutingDataSource 구현 은 나머지 작업을 처리하고 적절한 DataSource를 명시하게 반환합니다.</p>
</li>
<li><p>AbstractRoutingDataSource를 구성하기 위해서 컨텍스트 맵이 필요하다. 이때 2개의 datasource를 map에 담아서 사용하고 처음에 설정하고 싶은 dataSource를 <code>setDefaultTargetDataSource</code>을 통해서 지정해 줄 수 있습니다.</p>
</li>
</ul>
<br/>

<pre><code class="language-java">@Configuration
public class MyBatisConfig {

    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier(&quot;dynamicDataSource&quot;) DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(&quot;classpath:mappers/*.xml&quot;));
        return factoryBean.getObject();
    }

    @Autowired
    @Bean(name = &quot;jdbcTemplate&quot;)
    public JdbcTemplate jdbcTemplate(@Qualifier(&quot;dynamicDataSource&quot;) DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean
    public DataSourceTransactionManager transactionManager(@Qualifier(&quot;dynamicDataSource&quot;) DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}</code></pre>
<ul>
<li><p>다음은 Dao, Transation에 대한 처리를 하는 부분입니다. 이 부분은 고민이 필요합니다. 왜냐하면 이전에 multi dataSource에서는 각각의 트랜잭션을 설정을 하여 각 서비스에 맞는 트랜잭션, sqlSessionTemplate을 사용했지만 현재는 라우팅을 사용하기 때문에 할 수 없다. </p>
</li>
<li><p>그러면 현재 DataSource에 맞는 sqlSessionTemplate 또는 트랜잭션을 사용해야 된다. 이를 처리하기 위해서 routing DataSource에서 설정된 부분을 빈으로 받아서 넣어주면 해결할 수 있습니다.</p>
</li>
</ul>
<br/>

<h2 id="3-라우팅을-통해-서비스에-적용할-수-있는-부분">3. 라우팅을 통해 서비스에 적용할 수 있는 부분</h2>
<ul>
<li><p>처음에 이 기능을 사용한 이유는 하나의 서비스가 메인 데이터베이스를 바라보는데 데이터베이스가 장애가 발생하면 서브 데이터베이스를 바라보게 만들려고 하였다.</p>
</li>
<li><p>물론 이 부분의 경우에는 애플리케이션 레벨에서 처리할 필요가 있는지에 대해서는 서비스 아키텍처에 따라서 다르지만 인프라 레벨에서 처리할 수 없는 제약사항이 있어서 애플레키이션 레벨에서 처리하기 위해 사용을 했다.</p>
</li>
<li><p>그러면 구현된 코드를 설명하기 이전에 간단하게 설명을 하겠다. 만약에 mybatis에서 db의 오류가 발생하게 된다면 <code>MybatisSystemException</code> <code>SQLSyntaxException</code>이 발생한다. 그러면 이것을 ControllerAdvice에서 예외를 잡은 다음 retry를 처리한다.</p>
</li>
</ul>
<blockquote>
<p>Retry를 처리하는 이유</p>
</blockquote>
<ul>
<li>데이터베이스가 현재 batch 작업 또는 네트워크 등 다양한 이슈로 처리하지 못한 경우에는 db의 routing을 변경하면 안된다. 명확하게 메인 데이터베이스가 문제가 생겼다는 것을 알기 위해서 메인 데이터베이스에 간단한 쿼리를 재시도를 통해서 확인한다.</li>
<li>만약에 retry를 통해서 복구 처리를 할 때 routing을 변경을 해주면 최소한의 리소스로 서비스를 안정화 시킬 수 있다.</li>
</ul>
<br/>

<h3 id="retry">Retry</h3>
<pre><code class="language-yaml">    implementation &#39;org.springframework.retry:spring-retry&#39;
    implementation &#39;org.springframework:spring-aspects&#39;</code></pre>
<pre><code class="language-java">@EnableRetry
@Configuration
public class RetryConfig {

    @Bean
    public RetryTemplate retryTemplate(){
        RetryTemplate retryTemplate = new RetryTemplate();

        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        backOffPolicy.setBackOffPeriod(2000); // 재시도 대기 시간 2초
        retryTemplate.setBackOffPolicy(backOffPolicy);

        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(5); // 재시도 횟수
        retryTemplate.setRetryPolicy(retryPolicy);
        retryTemplate.registerListener(new DefaultRetryListener());
        return retryTemplate;
    }

}
</code></pre>
<pre><code class="language-java">public class RetryListenerSupport implements RetryListener {

    public &lt;T, E extends Throwable&gt; void close(RetryContext context, RetryCallback&lt;T, E&gt; callback,
                                               Throwable throwable) {
    }

    public &lt;T, E extends Throwable&gt; void onError(RetryContext context, RetryCallback&lt;T, E&gt; callback,
                                                 Throwable throwable) {
    }

    public &lt;T, E extends Throwable&gt; boolean open(RetryContext context, RetryCallback&lt;T, E&gt; callback) {
        return true;
    }

}</code></pre>
<pre><code class="language-java">@Slf4j
public class DefaultRetryListener extends RetryListenerSupport {

    @Override
    public &lt;T, E extends Throwable&gt; boolean open(RetryContext context, RetryCallback&lt;T, E&gt; callback) {
        log.info(&quot;before call target method&quot;);
        return super.open(context, callback);
    }

    @Override
    public &lt;T, E extends Throwable&gt; void close(RetryContext context, RetryCallback&lt;T, E&gt; callback, Throwable throwable) {
        log.info(&quot;after retry&quot;);
        super.close(context, callback, throwable);
    }

    @Override
    public &lt;T, E extends Throwable&gt; void onError(RetryContext context, RetryCallback&lt;T, E&gt; callback, Throwable throwable) {
        log.info(&quot;on error&quot;);
        super.onError(context, callback, throwable);
    }
}</code></pre>
<h3 id="controlleradvice">ControllerAdvice</h3>
<pre><code class="language-java">    @org.springframework.web.bind.annotation.ExceptionHandler(SQLSyntaxErrorException.class)
    public void handleException(SQLSyntaxErrorException ex) {
        log.error(ex.toString());
         retryTemplate.execute(
                context -&gt; retryService.testDatabaseConnection() // 타겟 메서드 호출
                , context -&gt; {
                     int recover = retryService.recover(new Exception());
                     if (recover == -1) {
                         DynamicDataSourceContextHolder.setDataSourceKey(&quot;test1&quot;);
                     }
                     return recover;
                 }); // @Recover에 해당하는 로직
    }</code></pre>
<h1 id="참고">참고</h1>
<hr>
<p><a href="https://www.baeldung.com/spring-abstract-routing-data-source">https://www.baeldung.com/spring-abstract-routing-data-source</a></p>
<p><a href="https://spring.io/blog/2007/01/23/dynamic-datasource-routing">https://spring.io/blog/2007/01/23/dynamic-datasource-routing</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JUnit5, AssertJ 활용방법 및 Spring boot 테스트 코드 작성법]]></title>
            <link>https://velog.io/@geon_km/%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-rs6ri4fz</link>
            <guid>https://velog.io/@geon_km/%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-rs6ri4fz</guid>
            <pubDate>Sat, 23 Mar 2024 13:05:18 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<hr>
<ul>
<li>처음에 테스트 코드 작성한 이유는 취업을 위해서 시작을 했습니다. 하지만 시간이 지나면서 테스트 코드를 안쓰면 더 어색하고, 개인적으로 느끼고 학습한 테스트 코드의 장점과 단위 테스트를 진행을 해야되는 이유를 팀 또는 다른 사람들에게 공유하기 위해서 <code>테스트코드</code> 작성하였습니다.</li>
</ul>
<h1 id="본론">본론</h1>
<hr>
<h2 id="1-테스트를-꼭-해야하나">1. 테스트를 꼭 해야하나?</h2>
<ul>
<li>테스트 코드를 작성해야 되는 이유를 찾아보면 다음과 같이 나온다.</li>
</ul>
<pre><code class="language-text">1. 개발 과정 중 예상치 못한 문제를 미리 발견할 수 있는데, 에러를 클라이언트보다 빨리 발견할 수 있습니다.

2. 작성한 코드가 의도한 대로 작동하는지 검증할 수 있습니다.

3. 코드 수정이 필요한 상황에서 유연하고 안정적인 대응할 할 수 있게 해줍니다. 즉, 테스트 코드는 코드 변경에 대한 사이드 이펙트를 줄이는 예방책이 됩니다. 또한 코드 변경 시, 변경 부분으로 인한 영향도를 쉽게 파악할 수 있습니다.

3. 리팩토링 시 기능 구현이 동일하게 되었다는 판단을 내릴 수 있습니다.

4. 문서로서의 역할이 가능합니다. </code></pre>
<ul>
<li>물론 나도 이 장점에 대해서 공감을 합니다. 하지만 개인적으로 생각하기에 테스트를 작성해야 되는 이유는 점진적으로 커지는 서비스에서 <code>내 기능의 신뢰성을 최소한으로 검증</code>이라고 생각한다. 테스트 코드가 없다면 기능이 커지고 요구사항이 변경되면서 모든 기능의 신뢰성을 오직 내 머리 또는 수기로 작성된 문서를 통해서 해야된다. 테스트 코드를 작성하면 최소한의 나의 기능의 신뢰성을 확보할 수 있고 <code>복잡해지고 커져가는 서비스를 점진적으로 더 고도화를 시킬 수 있다고 생각합니다.</code></li>
</ul>
<h3 id="1-1-좋은-테스트란-무엇인가">1-1. 좋은 테스트란 무엇인가?</h3>
<ul>
<li><p>좋은 테스트는 <code>리팩토링 내성</code> , <code>회귀방지</code> , <code>빠른 피드백</code>, <code>유지보수성</code>이 일정 수준 이상으로 유지하고 있는 테스트가 좋다고 생각한다. 하지만 실제 테스트 코드를 작성하면 어떤 방식으로 작성을 해야되는지 고민이 된다.</p>
</li>
<li><p>개인적으로 4가지 특성에서 가장 중요한 특징은 <code>리팩토링 내성</code>이라고 생각한다. 왜냐하면 리팩토링 내성이 부족하다면 코드를 조금 변경을 하면 테스트 코드의 많은 수정이 생기게 된다. 이것은 지속적으로 테스트를 작성을 유지하며 발전하기에 힘들게 한다.</p>
</li>
<li><p>예를 들어서 <code>Spring Context</code>를 로딩하지 않고 <code>Mock</code>을 통해서 테스트를 하게 된다면 당연히 <code>빠른 피드백</code>을 얻을 수 있다. 하지만 이 경우에 <code>Mocking 인테페이스 변경</code>시 많은 코드를 수정을 하게 되어야 한다. 즉. <code>리팩토링 내성</code>이 부족하다를 의미한다.</p>
</li>
</ul>
<h2 id="2-테스트의-종류">2. 테스트의 종류</h2>
<h3 id="2-1-테스트-종류-설명">2-1. 테스트 종류 설명</h3>
<ul>
<li><p>테스트에는<code>단위 테스트</code>, <code>통합 테스트</code>, <code>기능 테스트</code>, <code>E2E 테스트</code>, <code>성능 테스트</code> 등 다양한 종류가 있다. 이번에 살펴보는 내용은 <code>단위 테스트</code>를 중점적으로 작성하려고 한다. 일반적으로 Spring에서는 테스트를 하기 위해서 <code>JUnit</code>, <code>Mockito</code>테스트가 있다. </p>
</li>
<li><p>여기서 <code>JUnit</code>, <code>Mockito</code>에 대해서 간단하게 설명하면 <code>JUnit</code>은 실제 DB와 테스트를 통하여 할 수 있다. 실제로 데이터를 테스트할 수 있기 때문에 높은 신뢰성을 가질 수 있지만 속도적인 측면에서는 비교적 느리다. <code>Mockito</code>는 자바를 사용하는 소프트웨어의 단위 테스트를 위한 모의 객체(Mock Objects) 프레임워크이다. 이를 사용하면 테스트를 더욱 격리시켜 특정 기능을 독립적으로 테스트할 수 있습니다. 가짜 객체를 사용함에 따라서 빠르게 테스트를 진행을 할 수 있지만 높은 신뢰성을 주기에 부족하다.</p>
</li>
</ul>
<h3 id="2-2-디트로이트-학파-classicist-vs-런던-학파-mockist">2-2. 디트로이트 학파 (Classicist) vs 런던 학파 (Mockist)</h3>
<blockquote>
<p>SUT : 각 테스트의 테스트 대상이 되는 객체 ( ex : Car.Class) 
MUT : 각 테스트의 테스트 대상이 되는 메서드  ( ex : Car.Class -&gt; move()) </p>
</blockquote>
<ol>
<li><p>디트로이트 학파 (고전파) </p>
<ul>
<li><code>단일 기능 (단일 클래스 또는 단일 클래스와 협력 클래스)</code>하나의 동작에 여러 의존성이 포함된다면, 그 의존성을 만들어 주어서라도 테스트를 진행하는 것이다.물론 Database와 같은 공유 의존성만큼은 ‘테스트 더블’ 을 적용할 수 있다.</li>
</ul>
</li>
<li><p>런던 학파 (런던파) : 단일 클래스</p>
<ul>
<li>철저하게 하나의 클래스 단위로 격리하여 단위 테스트를 진행하는 것이다.
<code>SUT에 협력 객체(의존성)가 존재한다면, 불변 객체(Enum, 상수 등)를 제외한 모든 협력 객체는 ‘테스트 더블’을 적용하여 SUT를 철저히 격리</code>시킨다.두 학파의 가장 큰 차이점은 ‘단위(입자성)의 정의를 어떻게 내리는지’ 에 대한 부분이다. 즉, 런던파는 한 번에 한 클래스만 테스트 되어야하고, 고전파는 SUT와 연결된 협력 객체까지 같이 테스트를 진행하게 된다.</li>
</ul>
</li>
</ol>
<blockquote>
<p>각 학파의 장단점 및 선호하는 학파에 대한 내용은 다음 게시글에 작성을 하겠다.</p>
</blockquote>
<h2 id="3-junit5">3. JUnit5</h2>
<h3 id="3-1-junit5이란-무엇인가">3-1. JUnit5이란 무엇인가?</h3>
<ul>
<li>JUnit5은 Java 기반 코드를 테스트를 할 수 있도록 하는 라이브러리이며 JUnit Platform + JUnit Jupiter + JUnit Vintage으로 구성되어있다.</li>
</ul>
<h3 id="3-2-intellij-live-template">3-2. Intellij Live template</h3>
<ul>
<li><p>테스트를 처음 접하는 사람들은 <code>실무에서 업무를 하는데 시간이 없는데 업무를 하기에도 바쁜데 테스트 코드까지 언제 작성하냐</code> 이런 이야기를 많이 한다. 코드를 치는 시간도 일종의 리소스인데 이것을 Intellij에서는 쉽게 할 수 있게 도와준다.</p>
</li>
<li><p>일단 테스트를 하고 싶은 클래스에서 <code>ctrl + command + t</code>를 입력하면 <code>create new test</code>를 선택하여 테스트를 바로 만들 수 있다.</p>
</li>
<li><p>이후 <code>setting -&gt; live template -&gt; custom 폴더를 하나 만들고 원하는 template</code>을 입력한다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/1596d187-fe22-48aa-b497-5dfdf3271a23/image.png" alt=""></p>
<pre><code class="language-java">@Test
public void $METHOD_NAME$() throws Exception{
    // given
    $END$
    // when

    // then
}</code></pre>
<h3 id="3-3-test">3-3. <strong><em>@Test</em></strong></h3>
<ul>
<li>메서드가 테스트 메서드임을 나타낸다.  JUnit 4의 @Test주석과 달리 이 주석은 어떠한 속성도 선언하지 않습니다.</li>
</ul>
<pre><code class="language-java">@Test
public void test() {

}</code></pre>
<h3 id="3-4-displayname">3-4. <strong><em>@DisplayName</em></strong></h3>
<ul>
<li>테스트가 많아지면 테스트의 변수명을 신경을 써야한다. 왜냐하면 테스트의 내용을 명확하게 읽을 수 있게 하기 위해서 이다. ( 테스트는 명세서의 역활도 하기 때문에 ) @DisplayName을 사용하면 테스트 메서드 실행 후 표시될 테스트 명을 지정할 수 있다. 이것을 한글로 하면 명확하게 테스트를 진행할 수 있다.</li>
</ul>
<pre><code class="language-java">    @Test
    @DisplayName(&quot;todo 생성&quot;)
    public void createTodo() throws Exception{
        // given
        Todo todo = Todo.builder()
                .title(&quot;title&quot;)
                .content(&quot;content&quot;)
                .build();
        // when
        Todo result = todoService.createTodo(todo);
        // then
        assertThat(result.getTitle()).isEqualTo(todo.getTitle());

    }</code></pre>
<p><img src="https://velog.velcdn.com/images/geon_km/post/ad6f770c-b34c-4e06-87bf-9be769ecc639/image.png" alt=""></p>
<h3 id="3-5-nested">3-5. <strong><em>@Nested</em></strong></h3>
<ul>
<li>@Nested는 주석이 달린 클래스가 비정적 중첩 테스트 클래스를 나타낸다. 자바 8~15까지는 클래스별 테스트 인스턴스 수명 주기를 사용 하지 않는 한 테스트 클래스에서 @BeforeAll메서드 @AfterAll를 직접 사용할 수 없습니다 . Java 16부터는 테스트 인스턴스 수명 주기 모드를 사용하여 테스트 클래스 에서 와 같이 메서드 를 선언할 수 있습니다.
<img src="https://velog.velcdn.com/images/geon_km/post/fc60f067-00d0-4f50-ba3a-433726f8f911/image.png" alt=""></li>
</ul>
<h3 id="3-6-displaynamegeneration">3-6. @DisplayNameGeneration</h3>
<ul>
<li>@DisplayName 처럼 별도의 이름을 주는 것이 아닌 코딩한 클래스, 메소드 이름을 이용해 변형시키는 어노테이션입니다.</li>
</ul>
<table>
<thead>
<tr>
<th>파라미터명</th>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>value</td>
<td>Class&lt;? extends DisplayNameGenerator&gt;</td>
<td>정의된 DisplayNameGenerator 중 하나를 사용합니다.</td>
</tr>
</tbody></table>
<p>내부 클래스로 정의된 <code>DisplayNameGenerator</code>에서 사용 가능한 방법은 다음과 같습니다:</p>
<table>
<thead>
<tr>
<th>클래스명</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Standard</td>
<td>기존 클래스 및 메소드 명을 사용합니다. (기본값)</td>
</tr>
<tr>
<td>Simple</td>
<td>괄호를 제외시킵니다.</td>
</tr>
<tr>
<td>ReplaceUnderscores</td>
<td>_(underscore)를 공백으로 바꿉니다.</td>
</tr>
<tr>
<td>IndicativeSentences</td>
<td>클래스명 + 구분자(&quot;, &quot;) + 메소드명으로 바꿉니다.</td>
</tr>
</tbody></table>
<pre><code class="language-java">class MemberTest {

    // 클래스 + 구분자 + 메서드
    @Nested
    @DisplayNameGeneration(DisplayNameGenerator.IndicativeSentences.class)
    class IndicativeSentences {

        @Test
        void test_display_name_generation() {
        }
    }

    // 뒤에 ()와 _ 가 삭제되게 나온다.
    @Nested
    @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
    class ReplaceUnderscores {

        @Test
        void test_name_generation() {
        }
    }

    // 뒤에 ()가 삭제되게 나오게 된다.
    @Nested
    @DisplayNameGeneration(DisplayNameGenerator.Simple.class)
    class Simple {

        @Test
        void test_name_generation() {
        }
    }

    // 기본 그대로 출력된다.
    @Nested
    @DisplayNameGeneration(DisplayNameGenerator.Standard.class)
    class Standard {

        @Test
        void test_name_generation() {
        }
    }

}</code></pre>
<p><img src="https://velog.velcdn.com/images/geon_km/post/30abc8ca-1b43-4979-83a8-6cda76cb22e9/image.png" alt=""></p>
<h3 id="3-7-beforeall-beforeeach-afterall-aftereach">3-7 <strong><em>@BeforeAll, @BeforeEach, @AfterAll, @AfterEach</em></strong></h3>
<ul>
<li>각 이름에서 알 수 있듯이 메서드 실행 이전, 이후에 각각 또는 전체를 실행을 시켜주는 어노테이션이다.</li>
</ul>
<pre><code class="language-java">   @BeforeEach
    void beforeEach() {
        System.out.println(&quot;@BeforeEach&quot;);
    }

    @BeforeAll
    static void beforeAll() {
        System.out.println(&quot;@BeforeAll&quot;);
    }

    @AfterAll
    static void afterAll() {
        System.out.println(&quot;@AfterAll&quot;);
    }

    @AfterEach
    void afterEach() {
        System.out.println(&quot;@AfterEach&quot;);
    }
</code></pre>
<ul>
<li><code>All</code>은 적용된 메서드는 테스트 클래스의 테스트가 실행되기 전에 단 한번만 실행된다. 여러 개의 테스트 중에서 공통적으로 처리되어야 하는 로직을 all로 분리시킬 수 있지만 테스트에 의해서 값이 변경될 수 있으니 활용에 주의를 해야된다.</li>
<li><code>Each</code>은 테스트 클래스에서 각각의 모든 테스트 메서드가 실행되기 이전, 이후에 실행되는 메서드이다. 각각의 테스트에 적용되기 때문에 앞에 테스트에 영향을 받지 않는다.</li>
</ul>
<blockquote>
<p>@All @Each 차이점</p>
</blockquote>
<ul>
<li>@All을 사용할 경우 static method이기 때문에 <code>AOP로 구현되는 @Transactional</code>이 적용되지 않는다.</li>
<li>@Each의 경우에는 하나의 트랜잭션으로 묶이기 때문에 롤백을 할 수 있다. 하지만 각각의 테스트에 반복되기 때문에 속도를 저하시킬 수 있다.</li>
</ul>
<h2 id="4-반복-테스트">4. 반복 테스트</h2>
<h3 id="4-1-repeatedtest">4-1. @RepeatedTest</h3>
<table>
<thead>
<tr>
<th>파라미터명</th>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>value</td>
<td>int</td>
<td>반복 횟수 (반드시 0보다 커야함) (필수)</td>
</tr>
<tr>
<td>name</td>
<td>String</td>
<td>반복할 때 나타나는 테스트명<br>기본값 : &quot;repetition &quot; + 현재 반복 횟수 + &quot; of &quot; + 총 반복 횟수</td>
</tr>
</tbody></table>
<p>@ReapeatedTest를 사용하면 RepetitionInfo 타입의 인자를 받을 수 있습니다. 앞에서 설명했어야 했는데 추가로 말하자면 JUnit 테스트는 기본적으로 TestInfo 타입의 인자도 받을 수 있습니다.</p>
<p>TestInfo</p>
<table>
<thead>
<tr>
<th>메소드명</th>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>getDisplayName()</td>
<td>String</td>
<td>@DisplayName 값이랑 동일</td>
</tr>
<tr>
<td>getTags()</td>
<td>Set<String></td>
<td>@Tag 배열 값</td>
</tr>
<tr>
<td>getTestClass()</td>
<td>Optional&lt;Class&lt;?&gt;&gt;</td>
<td>패키지 + 테스트 클래스명</td>
</tr>
<tr>
<td>getTestMethod()</td>
<td>Optional<Method></td>
<td>패키지명 + 테스트 클래스명 + 테스트 메소드</td>
</tr>
</tbody></table>
<p>RepetitionInfo</p>
<table>
<thead>
<tr>
<th>메소드명 / 변수명</th>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>getCurrentRepetition()</td>
<td>int</td>
<td>현재 반복 횟수</td>
</tr>
<tr>
<td>getTotalRepetitions()</td>
<td>int</td>
<td>총 반복 횟수</td>
</tr>
<tr>
<td>DISPLAY_NAME_PLACEHOLDER</td>
<td>String</td>
<td>@DisplayName 값</td>
</tr>
<tr>
<td>SHORT_DISPLAY_NAME</td>
<td>String</td>
<td>반복할 때 나타나는 테스트명<br>기본값 : &quot;repetition &quot; + 현재 반복 횟수 + &quot; of &quot; + 총 반복 횟수</td>
</tr>
<tr>
<td>LONG_DISPLAY_NAME</td>
<td>String</td>
<td>DISPLAY_NAME_PLACEHOLDER + &quot; :: &quot; + SHORT_DISPLAY_NAME</td>
</tr>
<tr>
<td>TOTAL_REPETITIONS_PLACEHOLDER</td>
<td>String</td>
<td>현재 반복 횟수</td>
</tr>
<tr>
<td>CURRENT_REPETITION_PLACEHOLDER</td>
<td>String</td>
<td>총 반복 횟수</td>
</tr>
</tbody></table>
<pre><code class="language-java">
    @RepeatedTest(value = 3, name = &quot;{displayName} - {currentRepetition}/{totalRepetitions}&quot;)
    @DisplayName(&quot;Repeating Test&quot;)
    void repeatedTest(TestInfo testInfo, RepetitionInfo repetitionInfo) {
        System.out.println(&quot;Running repetition &quot; + repetitionInfo.getCurrentRepetition()
                + &quot; of &quot; + repetitionInfo.getTotalRepetitions());
        assertEquals(2, Math.addExact(1, 1), &quot;1 + 1 should equal 2&quot;);
    }

    @RepeatedTest(5)
    void repeatedTestWithDefaults(TestInfo testInfo) {
        System.out.println(&quot;Running &quot; + testInfo.getTestMethod().get().getName());
        assertEquals(2, Math.addExact(1, 1), &quot;1 + 1 should equal 2&quot;);
    }

    @RepeatedTest(value = 5, name = &quot;Custom name {currentRepetition}/{totalRepetitions}&quot;)
    void repeatedTestWithCustomName(TestInfo testInfo) {
        System.out.println(&quot;Running &quot; + testInfo.getTestMethod().get().getName());
        assertEquals(2, Math.addExact(1, 1), &quot;1 + 1 should equal 2&quot;);
    }
</code></pre>
<h3 id="4-2-parameterizedtest">4-2. @ParameterizedTest</h3>
<ul>
<li>인자를 가독성이 정의하여 테스트 할 수 있다. @ParameterizedTest와 @ValueSource를 사용하여 다양한 파라미터 값으로 테스트를 반복적으로 실행할 수 있다.</li>
</ul>
<p>@ParameterizedTest 어노테이션은 다음과 같은 파라미터를 가집니다:</p>
<table>
<thead>
<tr>
<th>파라미터명</th>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>name</td>
<td>String</td>
<td>@DisplayName 설정</td>
</tr>
<tr>
<td>DISPLAY_NAME_PLACEHOLDER</td>
<td>String</td>
<td>@DisplayName과 동일</td>
</tr>
<tr>
<td>INDEX_PLACEHOLDER</td>
<td>String</td>
<td>현재 실행 인덱스</td>
</tr>
<tr>
<td>ARGUMENTS_PLACEHOLDER</td>
<td>String</td>
<td>현재 실행된 파라미터 값</td>
</tr>
<tr>
<td>ARGUMENTS_WITH_NAMES_PLACEHOLDER</td>
<td>String</td>
<td>현재 실행된 파라미터명 + &quot;=&quot; + 값</td>
</tr>
<tr>
<td>DEFAULT_DISPLAY_NAME</td>
<td>String</td>
<td>기본값 &quot;[&quot; + INDEX_PLACEHOLDER + &quot;] &quot; + ARGUMENTS_WITH_NAMES_PLACEHOLDER</td>
</tr>
</tbody></table>
<p>@ParameterizedTest는 단독으로 사용되진 않고 어떤 파라미터를 사용하는지에 관한 어노테이션을 추가로 선언해줘야합니다.</p>
<p>추가로 선언하지 않았을 경우 아래와 같은 에러가 발생합니다:</p>
<pre><code>org.junit.platform.commons.PreconditionViolationException: Configuration error: You must configure at least one set of arguments for this @ParameterizedTest</code></pre><p>@ValueSource 어노테이션은 다양한 타입의 파라미터를 배열로 받아서 사용할 수 있게 해줍니다. 지원되는 타입은 다음과 같습니다:</p>
<ul>
<li>short[], byte[], int[], long[]</li>
<li>float[], double[]</li>
<li>char[], boolean[]</li>
<li>String[], Class&lt;?&gt;[]</li>
</ul>
<p>각 타입명의 소문자에 &quot;s&quot;를 붙혀주면 파라미터명이 됩니다. (예: ints, strings)</p>
<p>파라미터 인자는 1개만 사용 가능하며, 2개 이상 넣을 시 에러가 발생합니다.</p>
<p>예시 코드:</p>
<pre><code class="language-java">package com.effortguy.junit5;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.assertTrue;

public class ParameterizedTestAnnotation {

    @ParameterizedTest
    @ValueSource(ints = { 1, 2, 3 })
    void testWithValueSource(int intArg) {
        assertTrue(intArg &gt; 0 &amp;&amp; intArg &lt; 4);
    }

    // @ValueSource 파라미터로 여러개 값을 넣을 수 없음
    // @ParameterizedTest
    // @ValueSource(ints = { 1, 2, 3 }, strings = {&quot;a&quot;, &quot;b&quot;, &quot;c&quot;})
    // void testWithValueSource(int intArg, string stringArg) {
    // }
}</code></pre>
<p>위의 예시 코드에서는 @ParameterizedTest와 @ValueSource를 사용하여 int 타입의 파라미터 값 1, 2, 3으로 테스트를 반복적으로 실행합니다.</p>
<p>@ValueSource에 여러 개의 파라미터를 넣으려고 시도하면 컴파일 에러가 발생합니다.</p>
<pre><code class="language-java">@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15, Integer.MAX_VALUE}) // six numbers
void isOdd_ShouldReturnTrueForOddNumbers(int number) {
    assertTrue(Numbers.isOdd(number));
}
</code></pre>
<h3 id="4-3-dynamic-test">4-3. @Dynamic Test</h3>
<ul>
<li><p>여러 테스트들이 하나의 공유 변수를 사용하면 테스트간에 강결합이 발생하고 테스트의 순서가 생기며 독립성이 보장하지 않는다는 문제가 있어서 좋은 방식이 아니다. </p>
</li>
<li><p><code>@Dynamic Test</code>은 어느 환경에서 시나리오에 따라서 변화화는 것을 테스트를 할 수 있다.</p>
</li>
<li><p>작성하는 방법</p>
<ol>
<li><p>@TestFactory 어노테이션 사용
<code>@TestFactory</code> 메소드는 테스트 케이스를 생산하는 팩토리이다. private, static은 하면 안된다.</p>
</li>
<li><p>컬렉션 반환: @TestFactory 메서드는 Stream, Collection, Iterable 또는 Iterator 를 return 해야 한다. 그렇지 않으면, JUnitException을 발생시킨다.</p>
</li>
<li><p>첫번째 인자로 테스트 이름 작성
dynamicTest는 테스트 이름과, 실행 함수 두 요소로 이루어져있다. 그 만큼 테스트 이름을 잘 작성해주는 것이 가독성을 높이는 측면에서도 중요하다.</p>
</li>
</ol>
</li>
</ul>
<pre><code class="language-java">    @DisplayName(&quot;재고 차감 시나리오&quot;)
    @TestFactory
    Collection&lt;DynamicTest&gt; stockDeductionDynamicTest() {
        // given
        Stock stock = Stock.create(&quot;001&quot;, 1);

        return List.of(
            DynamicTest.dynamicTest(&quot;재고를 주어진 개수만큼 차감할 수 있다.&quot;, () -&gt; {
                // given
                int quantity = 1;

                // when
                stock.deductQuantity(quantity);

                // then
                assertThat(stock.getQuantity()).isZero();
            }),
            DynamicTest.dynamicTest(&quot;재고보다 많은 수의 수량으로 차감 시도하는 경우 예외가 발생한다.&quot;, () -&gt; {
                // given
                int quantity = 1;

                // when // then
                assertThatThrownBy(() -&gt; stock.deductQuantity(quantity))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessage(&quot;차감할 재고 수량이 없습니다.&quot;);
            })
        );
    }</code></pre>
<h2 id="4-assertj">4. AssertJ</h2>
<ul>
<li>AssertJ의 assert기능 관련 메서드를 활용하면 메서드 체이닝의 형태로 테스트 코드를 작성하여 가독성에 도움을 주기 때문에 JUnit5의 Assertions 메서드 보다는 AssertJ메서드를 사용하자.</li>
</ul>
<h3 id="일반적인-테스트-코드-흐름">일반적인 테스트 코드 흐름</h3>
<ul>
<li>AAA 패턴 <ul>
<li>보통 테스트를 작성할 때는 given-when-then의 구조로 작성한다.</li>
</ul>
</li>
</ul>
<p>given은 테스트 데이터등을 세팅한다.</p>
<p>when은 테스트 하려는 동작을 수행한다.</p>
<p>then에서는 given-when절을 통해 나온 결과가 원하는 결과와 부합하는 지 Assertion을 통해 검증한다.</p>
<p>AssertJ는 then에서 결과검증시 활용된다.</p>
<h3 id="assertj-사용법">AssertJ 사용법</h3>
<ul>
<li>AssertJ는 다양한 방법이 있다. 이번에는 내가 자주 사용하는 기능만 소개하고 더욱 깊이있는 학습을 원하면 <a href="https://assertj.github.io/doc/">https://assertj.github.io/doc/</a> 이것에서 확인할 수 있다.</li>
</ul>
<p>AssertJ의 기본 문법 구조는 다음과 같습니다.</p>
<pre><code class="language-java">assertThat(검증하려는 대상).검증메서드(원하는 결과);</code></pre>
<p>예를 들어, 실제 값(actual)과 예상 값(expected)이 같은지 검증하려면 다음과 같이 작성할 수 있습니다.</p>
<pre><code class="language-java">assertThat(actual).isEqualTo(expected);</code></pre>
<p>문자열 검증의 경우, 다음과 같이 다양한 메서드를 활용할 수 있습니다.</p>
<pre><code class="language-java">@Test 
void simpleStringAssertions() {
    String book = &quot;The Lord of the Rings&quot;;
    assertThat(book).isNotNull()
                    .startsWith(&quot;The&quot;)
                    .contains(&quot;Lord&quot;)
                    .endsWith(&quot;Rings&quot;);
}</code></pre>
<p>테스트가 실패할 경우, 좀 더 명확한 실패 메시지를 지정하고 싶다면 <code>as()</code> 메서드를 사용할 수 있습니다.</p>
<pre><code class="language-java">@Test
void testWithFailureMessage() {
    String name = &quot;John&quot;;
    assertThat(name).as(&quot;이름을 확인해주세요. 현재 값: %s&quot;, name)
                    .isEqualTo(&quot;Jane&quot;);
}</code></pre>
<p>컬렉션이나 문자열에 특정 값이 존재하는지 검증하려면 <code>contains()</code>, <code>containsOnly()</code>, <code>containsExactly()</code> 메서드를 사용할 수 있습니다.</p>
<pre><code class="language-java">@Test
void collectionContainsTest() {
    List&lt;String&gt; fruits = Arrays.asList(&quot;apple&quot;, &quot;banana&quot;, &quot;orange&quot;);

    assertThat(fruits).contains(&quot;apple&quot;, &quot;banana&quot;);
    assertThat(fruits).containsOnly(&quot;orange&quot;, &quot;banana&quot;, &quot;apple&quot;);
    assertThat(fruits).containsExactly(&quot;apple&quot;, &quot;banana&quot;, &quot;orange&quot;);
}</code></pre>
<p>객체의 특정 필드를 추출하여 검증하려면 <code>extracting()</code> 메서드를 사용할 수 있습니다.</p>
<pre><code class="language-java">@Test
void extractingFields() {
    Person person1 = new Person(&quot;Alice&quot;, 25);
    Person person2 = new Person(&quot;Bob&quot;, 30);
    List&lt;Person&gt; people = Arrays.asList(person1, person2);

    assertThat(people).extracting(&quot;name&quot;)
                      .contains(&quot;Alice&quot;, &quot;Bob&quot;);

    assertThat(people).extracting(&quot;name&quot;, &quot;age&quot;)
                      .contains(tuple(&quot;Alice&quot;, 25),
                                tuple(&quot;Bob&quot;, 30));
}</code></pre>
<p>Soft Assertion을 사용하면 하나의 테스트 메서드 내에서 여러 개의 검증을 수행하고, 모든 검증이 끝난 후에 결과를 한 번에 확인할 수 있습니다.</p>
<pre><code class="language-java">@Test
void softAssertionExample() {
    SoftAssertions softly = new SoftAssertions();

    softly.assertThat(&quot;Gandalf&quot;).as(&quot;Character Name&quot;).isEqualTo(&quot;Gandalf&quot;);
    softly.assertThat(100).as(&quot;Power Level&quot;).isGreaterThan(90);
    softly.assertThat(&quot;Mordor&quot;).isEqualTo(&quot;Mordor&quot;);

    softly.assertAll();
}</code></pre>
<p>예외 검증은 <code>assertThatThrownBy()</code> 메서드를 사용하여 수행할 수 있습니다.</p>
<pre><code class="language-java">@Test
void exceptionTest() {
    assertThatThrownBy(() -&gt; {
        throw new IllegalArgumentException(&quot;Invalid argument!&quot;);
    }).isInstanceOf(IllegalArgumentException.class)
      .hasMessage(&quot;Invalid argument!&quot;)
      .hasMessageContaining(&quot;Invalid&quot;);
}</code></pre>
<p>객체 비교 시에는 <code>usingRecursiveComparison()</code> 메서드를 사용하여 필드를 재귀적으로 비교할 수 있습니다. 이때, <code>ignoringFields()</code>를 사용하여 비교에서 제외할 필드를 지정할 수 있습니다.</p>
<pre><code class="language-java">@Test
void objectComparisonTest() {
    Person person1 = new Person(&quot;Alice&quot;, 25);
    Person person2 = new Person(&quot;Alice&quot;, 25);

    assertThat(person1).usingRecursiveComparison()
                       .ignoringFields(&quot;id&quot;)
                       .isEqualTo(person2);
}</code></pre>
<p>이렇게 AssertJ를 활용하면 단위 테스트를 보다 쉽고 명확하게 작성할 수 있습니다. 다양한 메서드를 조합하여 필요한 검증을 수행할 수 있으며, 실패 메시지도 커스터마이징할 수 있어 테스트 결과를 이해하기 쉽습니다.</p>
<h1 id="참고">참고</h1>
<hr>
<p><a href="https://wiki.mhson.world/test/junit/junit">https://wiki.mhson.world/test/junit/junit</a></p>
<p><a href="https://junit.org/junit5/docs/current/user-guide/">https://junit.org/junit5/docs/current/user-guide/</a></p>
<p><a href="https://tecoble.techcourse.co.kr/post/2020-07-31-dynamic-test/">https://tecoble.techcourse.co.kr/post/2020-07-31-dynamic-test/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CI/CD] Jenkins + SVN + CodeDeploy를 이용한 Pipeline  Blue-Green 무중단 배포]]></title>
            <link>https://velog.io/@geon_km/CICD-Jenkins-SVN-CodeDeploy%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-Pipeline-Blue-Green-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@geon_km/CICD-Jenkins-SVN-CodeDeploy%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-Pipeline-Blue-Green-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Sat, 16 Mar 2024 16:44:50 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<hr>
<ul>
<li><p>최근 업무에서 무중단 배포를 구축하면서 학습한 내용을 공유하기 위해 글을 작성을 하였습니다. 기존에 CI/CD를 구축하지 않고 war를 fileZira 또는 SCP를 통해서 war파일을 <code>target</code>에 전달하여 스크립트로 실행하는 환경에서 무중단 배포로 바꾸는 이유는 기존에 배포를 하기 위해서는 전 직원의 업무를 10분 정도 못하는 문제와 배포를 하면서 작업의 단절되어 데이터 정합성이 맞지 않는 이슈가 있었습니다.</p>
</li>
<li><p>배포를 진행하면서 CI툴인 Git Action, Jenkins, Travis등 다양한 툴이 있습니다. 이 부분에 대해서는 현재 회사의 프로젝트에 어떤 툴이 적합한지 고민하고 적용할 생각을 가지고 있습니다. Jenkins와 Github Action에 대한 경험이 있기 때문에 이번에는 배포 스크립트, Nginx에 대해서만 설명하겠습니다.</p>
</li>
</ul>
<h1 id="본론">본론</h1>
<hr>
<h2 id="1배포-전략">1.<a href="https://dev.to/mostlyjason/intro-to-deployment-strategies-blue-green-canary-and-more-3a3">배포 전략</a></h2>
<blockquote>
<p>무중단 배포란 말 그대로 서비스가 중단되지 않는 상태로, 새로운 버전을 사용자들에게 배포하는 것을 의미합니다. 무중단 배포를 사용하기 위해서는 최소 2대 이상의 서버가 확보해야합니다.</p>
</blockquote>
<br/>

<h3 id="1-1big-bang-방색">1-1.Big Bang 방색</h3>
<ul>
<li><p>빅뱅 배포의 방식은 말 그대로 애플리케이션 전체 또는 상당한 부분을 한번에 업데이트를 의미를 합니다. 이 전략은 과거에 많이 사용했던 방식이다. 이 방식을 선택하기 위해서는 비즈니스 출시 이전에 광범위 개발 및 테스트가 전재가 되어야한다.</p>
</li>
<li><p>빅뱅 방식은 일반적으로 하나의 패키지로 개발 및 배포하며 한번 배포를 하기에 많은 시간 및 비용이 발생합니다. 이후 롤백이 불가능하거나 힘들기 때문에 실패 가능성을 최소화를 가정하며 많은 노력이 필요합니다.</p>
</li>
</ul>
<br/>


<h3 id="1-2롤링-방식--rolling-">1-2.롤링 방식 ( Rolling )</h3>
<p><img src="https://blog.kakaocdn.net/dn/wYSvc/btrjmWL1rEM/iWnT8cK13Kutm6a7qrubVk/img.gif" alt=""></p>
<blockquote>
<p>롤링 배포는 일반적으로 <code>점차</code> 이전 버전을 대체한다고 생각하면 된다. 실제 배포는 일정 기간에 걸쳐서 사용자에게 영향을 주지 않기 위해 새 버전과 이전 버전을 공존하게 하며 점진적으로 교체를 합니다.</p>
</blockquote>
<p><strong>방식 1</strong></p>
<ul>
<li>주로 AWS와 같이 서버 개수를 유연하게 조절할 수 있는 환경에서는 인스턴스를 하나 추가하고, 새로운 버전을 실행한다. 로드 밸런서에 이 인스턴스를 연결하고, 기존 구버전 어플리케이션이 실행되는 인스턴스를 하나 줄인다.</li>
</ul>
<p><strong>방식 2</strong></p>
<ul>
<li>기존의 버전 V1이 실행되고 있는 서버에 로드밸런서를 하나 때고 트래픽을 차단한다. 이 상태에서 새로운 버전으로 업데이트를 한다. 그리고 다시 로드 밸런서를 연결한다. 이를 반복하여 서서히 모든 인스턴스에 점진적으로 업데이트를 한다.</li>
</ul>
<p><strong>장점</strong></p>
<ul>
<li><p>롤링 배포 방식은 AWS elastic beanstalk 또는 k8s와 같은 오케스트레이션 도구에서 지원하여 간편하게 처리할 수 있으며 많은 서버 자원을 확보하지 않아도 무중단 배포가 가능하다.</p>
</li>
<li><p>점진적으로 새로운 버전을 출시하기 때문에 배포의 안전성이 뛰어나다.</p>
</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li><p>새 버전을 배포할 때 인스턴스의 수가 변경하기 때문에 특정 인스턴스에 트래픽이 몰릴 수 있다. </p>
</li>
<li><p>구, 신버전의 호환성 문제가 발생할 수 있기 때문에 충분한 QA가 필요하다.</p>
</li>
</ul>
<br/>

<h3 id="1-3블루-그린--blue-green-">1-3.블루 그린 ( Blue Green )</h3>
<p><img src="https://blog.kakaocdn.net/dn/XRBsk/btrjkiiLNDT/OO4IpUkXGRnSaOkp9t2aC1/img.gif" alt=""></p>
<ul>
<li>Blue Green 방식에서 Blue는 현재 운영중인 서비스 환경을 의미하고, 새롭게 배포할 환경은 Green이라고 부른다. 운영중인 구버전과 동일하게 신버전의 인스턴스를 구성하고 로드밸런서를 통해 모든 트래픽을 한번에 신버전으로 변환하는 방식을 의미를 한다.</li>
</ul>
<p><strong>장점</strong></p>
<ul>
<li>구버전의 인스턴스가 남아있어 롤백에 수월하다.</li>
<li>운영환경에서 영향을 주지 않고 새 버전으로 변경이 가능하다. ( nginx를 사용한다면 restart를 할때 1초간 영향)</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>시스템 자원이 2배로 필요하다.</li>
</ul>
<br/>

<h3 id="1-4카나리--canary-">1-4.카나리 ( Canary )</h3>
<p><img src="https://blog.kakaocdn.net/dn/co109I/btrjk20toUP/bzAkDoisBiKJj2tqT9YUFK/img.gif" alt=""></p>
<ul>
<li>카나리 배포는 점진적으로 구버전에 대한 트래픽을 신버전으로 옮기는 롤링 배포 방식과 비슷하다. 여기서 카나리 방식의 특징으로는 <strong>새로운 버전에 대한 오류를 조기에 감지한다.</strong> 즉. 소수의 인원을 새로운 애플리케이션에 할당하고 안전성이 검증이 되었을 때 기존의 트래픽을 새로운 버전으로 옮기는 방식이다. 이를 통해 안전성을 확보할 수 있다는 장점이 있다.  (A/B 테스트에 적합하다.)</li>
</ul>
<p><strong>장점</strong></p>
<ul>
<li>시스템에 대한 문제를 빠르게 감지할 수 있다.</li>
<li>A/B 테스트로 활용이 가능하다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>네트워크 트래픽 제어를 해야한다.</li>
<li>롤링 배포와 마찬가지로 신버전, 구버전의 호환성 문제가 발생할 수 있다.</li>
</ul>
<br/>


<h3 id="전략-선택">전략 선택</h3>
<ul>
<li>Blue, Green 무중단 배포를 선택을 하였습니다. 일단 어드민 시스템을 운영하기 때문에 안전성이 중요합니다. 그렇게 때문에 카나리 배포도 고려를 하였지만 Blue, Green 배포를 nginx로 사용다면 최소한의 리소스에서 저희가 원하는 무중단을 사용할 수 있기 때문에 가성비가 좋다고 생각했습니다. 또한 카나리 배포를 하였을 때 특정 사용자에 대한 부분과 어느 상황에서 안전성이 검증이 되었는가를 판단하기가 어렵다고 판단하였습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/b631adad-e2f8-42d0-9028-e1813c43b554/image.png" alt=""></p>
<ul>
<li><p>최종적으로는 온프로미스 환경에서 Nginx로 무중단 배포를 하겠습니다. 처음에는 사용자가 접속하면 Nginx가 8081포트로 사용자의 요청을 전달합니다. ( 이때  8082는 연결이 되지 않습니다. ) 새로운 배포가 필요하다면 연결이 되지 않은 8082포트 WAS에 배포하고 배포가 끝나면 구동 상태를 <code>Spring Boot Actuator</code>를 이용하여 확인하고 8081 -&gt; 8082로 사용자 트래픽을 받습니다. 위에 방식과 같이 1번 더 반복하면 8081, 8082 포트의 WAS는 모두 새로운 버전으로 업데이트가 됩니다. </p>
</li>
<li><p>이후 사용자의 트래픽을 분산시키기 위해 Nginx로 UpStream을 처리하는 방식으로 마무리를 하겠습니다.</p>
</li>
</ul>
<h2 id="2-nginx란-무엇인가">2. Nginx란 무엇인가</h2>
<p><img src="https://velog.velcdn.com/images/geon_km/post/561cad4a-cb86-43cd-8cc9-fa6d8ad626a7/image.png" alt=""></p>
<ul>
<li><p>엔진엑스는 <code>동시 접속 처리에 특화</code>된 <code>웹 서버</code>입니다. 아파치보다 동작이 단순하고 전달자 역활을 하기 때문에 동시 접속 처리에 특화가 되었습니다.</p>
</li>
<li><p>원래 엔진엑스는 아파치 앞단에서 커넥션을 줄이기 위해 만들어졌다. 이때 엔진엑스가 커넥션을 줄이는 당식으로는 정적 파일에 대한 이미지를 직접 처리할 수 있고 동적인 처리는 아파치에게 보냄으로서 커넥션을 줄일 수 있습니다. nginx에는 keep-alive설정으로 여전히 연결되어 있지만 apache에 보낼 때 별도의 요청을 보내기 때문에 커넥션을 유지할 필요가 없었습니다.</p>
</li>
</ul>
<h3 id="2-1-엔진엑스-역활">2-1. 엔진엑스 역활</h3>
<p><strong>1. 정적 파일을 처리하는 HTTP 서버로서의 역할</strong> </p>
<ul>
<li>웹서버의 역할은 HTML, CSS, Javascript, 이미지와 같은 정보를 웹 브라우저(Chrome, Iexplore, Opera, Firefox 등)에 전송하는 역할을 한다.</li>
</ul>
<p><strong>2. 리버스 프록시 역활</strong> 
두번째 역할은 리버스 프록시(reverse proxy)인데, 한마디로 말하면 클라이언트는 가짜 서버에 요청(request)하면, 프록시 서버가 배후 서버(reverse server)로부터 데이터를 가져오는 역할을 한다. 여기서 프록시 서버가 Nginx, 리버스 서버가 응용프로그램 서버를 의미한다.</p>
<p>웹 응용프로그램 서버에 리버스 프록시(Nginx)를 두는 이유는 요청(request)에 대한 버퍼링이 있기 때문이다. 클라이언트가 직접 App 서버에 직접 요청하는 경우, 프로세스 1개가 응답 대기 상태가 되어야만 한다. 따라서 프록시 서버를 둠으로써 요청을 배분하는 역할을 한다.</p>
<h3 id="2-2-l4-l7-스위치">2-2. L4, L7 스위치</h3>
<ul>
<li><p>OSI 7 Layer를 살펴보면 각 계층마다 스위치가 있다. 이 중에서 L4, L7 스위치는 비슷한 역활을 하지만 차이점이 있다. </p>
</li>
<li><p>지금 설명하고 있는 엔진엑스는 L7 스위치에 해당되는데 L4, L7 스위치에 대해 설명하고 각 역활과 차이를 설명하겠다.</p>
</li>
</ul>
<h3 id="2-3-l7-스위치-엔진엑스">2-3. L7 스위치 (엔진엑스)</h3>
<ul>
<li><p>OSI 7Layer중 Layer7을 기준으로 로드밸런싱하는 역할을 합니다. 애플리케이션 (HTTP, HTTPS, FTP, SMTP)에서 트래픽을 분산하여 URL에 따라 부하를 분산하거나 HTTP 헤더의 쿠키값에 따라서 부하를 분산하는 다양한 전략을 기반으로 클라이언트에게 세분화하여 서버에 전달한다. ( 일반적인 로드밸런서 알고리즘은 라우트 로빈 방식을 사용한다.)</p>
</li>
<li><p>정확하게는 패킷의 내용을 확인하고 그 내용에 따라 로드를 특정 서버에 분배하는 기능을 한다.</p>
</li>
<li><p>또한 L7 로드밸런서의 경우 특정한 패턴을 지닌 바이러스를 감지해 네트워크를 보호할 수 있다. -&gt; 이것은 Dos나 DDos와 같은 비정상적인 트래픽을 필터링 할 수 있다.</p>
</li>
</ul>
<h3 id="2-4-l4-스위치">2-4. L4 스위치</h3>
<ul>
<li><p>OSI 7 Layer에서 네트워크 계층이나 트랜스포트 계층의 정보를 바탕으로 정보를 로드를 분산시킨다. Real IP 즉. 탄력적 IP를 묶어서 로드밸런싱을 한다. 이때 L7과 같이 라우트 로빈 방식을 사용한다. </p>
</li>
<li><p>L4 스위치는 Fail over(장애 극복 기능)을 통하여 예비 시스템으로 자동전환되는 기능이다. 시스템 대체 작동 또는 장애 조치라고 한다. 반면 사람이 수동으로 실시하는 것을 스위치 오버라고 한다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/b471c2cb-dbac-48be-8073-54fe033a7633/image.png" alt=""></p>
<h3 id="2-5-로드밸런서-알고리즘">2-5. 로드밸런서 알고리즘</h3>
<table>
<thead>
<tr>
<th>로드 밸런싱 알고리즘</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Round Robin</td>
<td>요청을 순서대로 각 서버에 균등하게 분배하는 방식으로, 서버 커넥션 수나 응답시간에 상관없이 모든 서버를 동일하게 처리합니다. 다른 알고리즘에 비해 가장 빠릅니다.</td>
</tr>
<tr>
<td>IP 해시 방식</td>
<td>클라이언트의 IP 주소를 특정 서버로 매핑하여 요청을 처리하는 방식으로, 사용자의 IP를 해싱하여 로드를 분배하기 때문에 사용자가 항상 동일한 서버로 연결되는 것을 보장합니다.</td>
</tr>
<tr>
<td>Least Connection</td>
<td>서버에 연결되어 있는 Connection 개수만 갖고 단순비교하여 가장 적은 곳에 연결합니다.</td>
</tr>
<tr>
<td>Weighted Least Connections</td>
<td>서버에 부여된 Weight 값을 기반으로 Connection 수의 개수와 같이 고려하여 할당합니다.</td>
</tr>
<tr>
<td>Fastest Response Time</td>
<td>가장 빨리 응답하는 서버에 이용자 요구를 연결하는 방법으로, 각 서버가 패킷 형태의 요구를 송수신하는데 걸리는 시간을 측정한 것입니다.</td>
</tr>
<tr>
<td>Adaptive</td>
<td>Open 또는 Pending(계류중인) 커넥션을 적게 가지고 있는 서버로 네트웍 커넥션 방향을 지정합니다. Pending 커넥션은 Full TCP Handshake를 완성하지 않은 것으로, 이것은 초당 클라이언트 Thread의 수가 증가할 때 더욱 잘 수행됩니다.</td>
</tr>
</tbody></table>
<h3 id="2-6-l4-l7-스위치-정리">2-6. L4, L7 스위치 정리</h3>
<table>
<thead>
<tr>
<th></th>
<th>L4 로드밸런서</th>
<th>L7 로드밸런서</th>
</tr>
</thead>
<tbody><tr>
<td>계층</td>
<td>네트워크 계층 (Layer 4)</td>
<td>전송 계층 (Layer 7)</td>
</tr>
<tr>
<td>특징</td>
<td>TCP/UDP포트 정보를 바탕으로 함</td>
<td>TCP/UDP정보는 물론 HTTP의 URI, FTP의 파일명, 쿠키 정보 등을 바탕으로 함</td>
</tr>
<tr>
<td>장점</td>
<td>- 데이터 안을 들여다보지 않고 패킷 레벨에서만 로드를 분산하기 때문에 속도가 빠르고 효율적<br>- 데이터의 내용을 복호화할 필요가 없기에 안전함<br>- L7 로드밸런서보다 가격이 저렴함</td>
<td>- 상위 계층에서 로드를 분산하기 때문에 훨씬 더 섬세한 라우팅이 가능함<br>- 캐싱 기능을 제공함<br>- 비정상적인 트래픽을 사전에 필터링 할 수 있어 서비스 안정성이 높음</td>
</tr>
<tr>
<td>단점</td>
<td>- 패킷의 내용을 살펴볼 수 없기 때문에 섬세한 라우팅이 불가능함<br>- 사용자의 IP가 수시로 바뀌는 경우라면 연속적인 서비스를 제공하기 어려움</td>
<td>- 패킷의 내용을 복호화해야 하기에 부하가 많이 걸릴수 있고, 더 높은 비용을 지불해야함<br>- 클라이언트가 로드밸런서와 인증서를 공유해야 하기 때문에 공격자가 로밸런서를 통해서 클라이언트에 데이터에 접근할 보안 상의 위험성이 존재함</td>
</tr>
</tbody></table>
<br/>

<h2 id="3-무중단-배포-만들기">3. 무중단 배포 만들기</h2>
<hr>
<h3 id="3-1-ec2-생성하기">3-1. EC2 생성하기</h3>
<ul>
<li><p>실행 환경은 ubuntu 22.04 버전으로 ec2 인스턴스를 2개 생성합니다.  각각의 ec2는 각 역활이 있습니다. 일단 간단한게 ec2-A와 ec2-B라고 가정을 하겠습니다. </p>
</li>
<li><p>ec2-A에서  docker를 이용하여 jenkins를 설치합니다. ( 만약에 ec2의 인스턴스를 프리티어로 설정한다면 메모리가 부족할 수 있기 때문에 주의를 해야합니다. )</p>
</li>
<li><p>ec2-B는 jenkins에서 빌드를 누르면 실제로 사용하는 스프링 서버를 의미를 합니다.</p>
</li>
</ul>
<h3 id="3-2-ec2-a에-jenkins-설치하기">3-2. EC2-A에 Jenkins 설치하기</h3>
<h3 id="3-2-1메모리-스왑부터-진행을-하겠습니다">3-2-1.메모리 스왑부터 진행을 하겠습니다.</h3>
<pre><code class="language-shell">sudo dd if=/dev/zero of=/swapfile bs=128M count=32

sudo chmod 600 /swapfile

sudo mkswap /swapfile

sudo swapon /swapfile

sudo swapon -s

sudo vi /etc/fstab
--------------------
[ vi에 하단에 추가 ] 
/swapfile swap swap defaults 0 0
----------------------
## 용량 확인
free</code></pre>
<ul>
<li>아래와 같이 나온다면 정상적으로 변경이 되었습니다.
<img src="https://velog.velcdn.com/images/geon_km/post/493607b4-96d8-4f14-bd89-793ba45e29fa/image.png" alt=""></li>
</ul>
<h3 id="3-2-2-docker-설치">3-2-2. Docker 설치</h3>
<pre><code class="language-shell"># 오래된 버전 삭제
sudo apt-get remove docker docker-engine docker.io containerd runc

sudo apt-get update
# repository 설정
sudo apt-get -y install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

# Docker의 Official GPG Key 를 등록
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# stable repository 를 등록
echo \
  &quot;deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable&quot; | sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null

sudo apt-get update

# Docker Engine 설치
sudo apt-get -y install docker-ce docker-ce-cli containerd.io

# 설치 완료 확인, 버전 확인
docker --version

# /var/run/docker.sock 접근 권한 허용
sudo chmod 666 /var/run/docker.sock

# docker hub 로그인 id/pw 입력
docker login</code></pre>
<h3 id="3-2-3-docker를-이용하여-jenkins-설치하기">3-2-3. Docker를 이용하여 Jenkins 설치하기</h3>
<pre><code class="language-shell">docker run \
  --name jenkins-docker \
  -p 8080:8080 -p 50000:50000 \
  -v /home/jenkins:/var/jenkins_home \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /usr/bin/docker:/usr/bin/docker \
  -u root \
  -d \
  jenkins/jenkins:lts
</code></pre>
<h3 id="3-2-4-젠킨스-컨테이너-접속">3-2-4. 젠킨스 컨테이너 접속</h3>
<pre><code class="language-shell"> # jenkins 컨테이너 접속
docker exec -it [jenkins 컨테이너ID] bin/bash

# jenkins 컨테이너 log 확인
docker logs [jenkins 컨테이너ID]

apt-update
apt-get install zip
apt-get install awscli
apt-get install subversion</code></pre>
<ul>
<li>저는 svn을 사용하기 때문에 subversion을 사용을 하였습니다. 만약에 github을 사용한다면 git trigger를 통해서 해결할 수 있습니다.</li>
</ul>
<h3 id="3-2-5-젠킨스-필요한-플러그인-설치">3-2-5. 젠킨스 필요한 플러그인 설치</h3>
<ul>
<li>다음은 Jenkins 플러그인을 설치를 해야된다. 일단 간단한 플러그인은 다음과 같다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/5088e8c0-79fa-44b5-bf1e-0f7795938781/image.png" alt=""></p>
<pre><code class="language-shell">## 필요한 플러그인
AWS CodeDeploy Plugin for Jenkins / 1.23
AWS Credentials Plugin / 231.v08a_59f17d742
Pipeline: AWS Steps / 1.45
Pipeline: API
Strict Crumb Issuer Plugin / 2.1.1
AWS CodeDeploy Plugin for Jenkins</code></pre>
<h4 id="jenkins-403-no-valid-crumb-was-included-in-the-request">Jenkins: 403 No valid crumb was included in the request</h4>
<ul>
<li>Jenkins를 실행하고 사용하다보면 위에 오류가 보인다. 이 문제를 해결하기 위해서는 다음과 같다.</li>
</ul>
<ol>
<li><strong>403 No valid crumb was included in the request</strong></li>
</ol>
<blockquote>
<p>Dashboard &gt; Jenkins관리 &gt; Script Console 탭으로 이동하여</p>
</blockquote>
<pre><code class="language-python">import jenkins.model.Jenkins

def instance = Jenkins.instance
instance.setCrumbIssuer(null)

println(&#39;success&#39;) // 에러 없이 스크립트 실행여부 확인</code></pre>
<ul>
<li>이후 재시작</li>
</ul>
<ol start="2">
<li><strong>Strict Crumb Issuer Plugin</strong></li>
</ol>
<blockquote>
<p>Dashboard &gt; Jenkins관리 &gt; Plugins &gt; Available plugins &gt; Strict Crumb Issuer Plugin 설치
Dashboard &gt; Jenkins관리 &gt; Security 탭으로 이동하여
CSRF Protection 섹션으로 이동하여 아래처럼 설정을 전부 다 해제한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/geon_km/post/5dda490c-0b7b-4369-9973-436321e38d99/image.png" alt=""></p>
<ul>
<li>일반에서 localhost:8080으로 변경하기</li>
</ul>
<h3 id="3-2-6-jenkins-credentials">3-2-6. Jenkins Credentials</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/db308403-de19-4bee-af71-e317543d2d46/image.png" alt=""><img src="https://velog.velcdn.com/images/geon_km/post/56c9f131-5c14-48b8-bac0-2dcac57de62b/image.png" alt=""></p>
<ul>
<li>Jenkins 설정 &gt; Credentials에서 <code>global</code>한 환경변수를 추가한다. 이렇게 설정하면 젠킨스 item에서 보안과 확장성을 얻을 수 있다.</li>
</ul>
<h3 id="3-2-7-jenkins-pipeline">3-2-7. Jenkins Pipeline</h3>
<ul>
<li>item &gt; pipeline &gt; Configuration 클릭</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/2cc84792-ed1b-449f-b427-f31c750c7de9/image.png" alt=""></p>
<h4 id="jenkins-pipeline-script">Jenkins Pipeline Script</h4>
<pre><code class="language-shell">pipeline {
    agent any

    environment {
        AWS_REGION = &#39;ap-northeast-2&#39;
        S3_BUCKET = &#39;#{s3 이름}&#39;
        APPLICATION_NAME = &#39;#{codeDeploy이름}&#39;
        DEPLOYMENT_GROUP_NAME = &#39;#{deployGroup이름}&#39;
    }

    stages {
        stage(&#39;Checkout&#39;) {
            steps {
                dir(&#39;#{svn ip이름}&#39;) {
                    sh &#39;rm -rf *@tmp&#39;
                    checkout([
                        $class: &#39;SubversionSCM&#39;,
                        locations: [
                            [
                                credentialsId: &#39;tbnws&#39;,
                                local: &#39;.&#39;,
                                remote: &#39;#{svn ip이름}&#39;
                            ]
                        ],
                        workspaceUpdater: [$class: &#39;UpdateUpdater&#39;]
                    ])
                }
            }
            post {
                success {
                    echo &#39;Checkout stage succeeded&#39;
                }
                failure {
                    echo &#39;Checkout stage failed&#39;
                }
            }
        }

        stage(&#39;Update&#39;) {
            steps {
                dir(&#39;#{svn ip}&#39;) {
                    // sh &#39;svn upgrade&#39;
                    sh &#39;svn update&#39;
                }
            }
            post {
                success {
                    echo &#39;Update stage succeeded&#39;
                }
                failure {
                    echo &#39;Update stage failed&#39;
                }
            }
        }

        stage(&#39;Build&#39;) {
            steps {
                dir(&#39;/var/jenkins_home/workspace/#{spring 이름}&#39;) {
                    sh &#39;chmod 777 gradlew&#39;
                    sh &#39;./gradlew build&#39;
                }
            }
            post {
                success {
                    echo &#39;Build stage succeeded&#39;
                }
                failure {
                    echo &#39;Build stage failed&#39;
                }
            }
        }

        stage(&#39;Upload to S3&#39;) {
            steps {
                withAWS(region: AWS_REGION, credentials: &#39;TBNWS_AWS_Credentials&#39;) {
                    script {
                        def zipFileName = &quot;springServer-1.zip&quot;
                        dir(&#39;/var/jenkins_home/workspace/#{spring 이름}&#39;) {
                            sh &quot;zip -r ${zipFileName} build/libs/tbnws_admin_back.jar appspec.yml script/&quot;
                            sh &quot;aws s3 cp ${zipFileName} s3://${S3_BUCKET}/&quot;
                        }
                    }
                }
            }
            post {
                success {
                    echo &#39;Upload to S3 stage succeeded&#39;
                }
                failure {
                    echo &#39;Upload to S3 stage failed&#39;
                }
            }
        }

        stage(&#39;Deploy with CodeDeploy&#39;) {
            steps {
                withAWS(region: AWS_REGION, credentials: &#39;TBNWS_AWS_Credentials&#39;) {
                    script {
                        def deployCommand = &quot;aws deploy create-deployment &quot; +
                            &quot;--application-name ${APPLICATION_NAME} &quot; +
                            &quot;--deployment-config-name CodeDeployDefault.OneAtATime &quot; +
                            &quot;--deployment-group-name ${DEPLOYMENT_GROUP_NAME} &quot; +
                            &quot;--s3-location bucket=${S3_BUCKET},bundleType=zip,key=springServer-1.zip&quot;
                        sh deployCommand
                    }
                }
            }
            post {
                success {
                    echo &#39;Deploy with CodeDeploy stage succeeded&#39;
                }
                failure {
                    echo &#39;Deploy with CodeDeploy stage failed&#39;
                }
            }
        }
    }
}</code></pre>
<h2 id="3-4-codedeploy-설정">3-4. CodeDeploy 설정</h2>
<ul>
<li>해당 과정에 대해서는 <a href="https://jojoldu.tistory.com/313">https://jojoldu.tistory.com/313</a> 해당 사이트에서 얻을 수 있다.</li>
</ul>
<h2 id="3-3-private-subnet-target-ec2ec2-b에-codedeploy설치">3-3. Private Subnet Target EC2(EC2-B)에 CodeDeploy설치</h2>
<pre><code class="language-shell">sudo apt-get update

sudo apt-get install ruby

sudo apt-get install wget

# install 파일 경로는 원하는 대로 가능
cd /home/ubuntu


# Seoul region
wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install

chmod +x ./install
sudo ./install auto

# codedeploy-agent 상태 확인
sudo service codedeploy-agent status

# codedeploy-agent 서비스 시작
sudo service codedeploy-agent start
</code></pre>
<h3 id="3-4-무중단-배포하기">3-4. 무중단 배포하기</h3>
<ul>
<li>젠킨스에서 Build를 하게되면 SVN에서 update를 하여 최신 버전으로 변경하고 jar파일을 Zip 형태로 압축하고 S3에 업로드 합니다. 이후 CodeDeploy에 Build를 하게 되는데 이때 </li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/472d8659-2a7a-4160-bdf7-77b5cbce9039/image.png" alt=""></p>
<h3 id="appspecyml">appspec.yml</h3>
<pre><code class="language-shell">version: 0.0
os: linux
files:
  - source:  /
    destination: /home/ubuntu
    overwrite: yes

permissions:
  - object: /
    pattern: &quot;**&quot;
    owner: ubuntu
    group: ubuntu

hooks:
  AfterInstall:
    - location: script/deploy.sh
      timeout: 180
      runas: ubuntu
  ApplicationStart:
    - location: script/switch.sh
      timeout: 180
      runas: ubuntu
</code></pre>
<h3 id="deploysh">deploy.sh</h3>
<pre><code class="language-shell"></code></pre>
<h3 id="switchsh">switch.sh</h3>
<h3 id="validatesh">validate.sh</h3>
<pre><code class="language-shell"># update
$ sudo apt get update

# nginx 설치
$ sudo apt install nginx

# 설치 확인
$ ps -ef | grep nginx</code></pre>
<p><img src="https://velog.velcdn.com/images/geon_km/post/b820cca8-5e1a-474f-9feb-b95987355421/image.png" alt=""></p>
<ul>
<li>nginx의 설정을 위해서 /etc/nginx/nginx.conf에서 include를 추가할 수 있고 /etc/nginx/conf.d/service-url.inc를 설정한다.</li>
</ul>
<pre><code class="language-shell"># inc는 include 파일을 의미한다.
sudo vi service-url.inc

set $service_url http://127.0.0.1:8081;</code></pre>
<br/>

<h2 id="4-graceful-shutdown">4. Graceful Shutdown</h2>
<ul>
<li><p>blue green 배포에서는 상관이 없지만 다른 배포에서 구버전 애플리케이션을 죽일 때 프로세스를 죽일 때 고민을 해야됩니다. 프로세스를 바로 죽이게 하는 것과 현재 처리하고 있는 요청을 모두 완료하고 죽이는 건 시스템에 따라서 다릅니다.</p>
</li>
<li><p>하지만 복구 프로세스가 복잡하다면 고민할 필요가 있습니다. 만약에 배포를 할때 시간이 긴 프로세스를 처리하고 있다면 이 부분도 고민할 필요가 있습니다.</p>
</li>
</ul>
<br/>

<h3 id="41-kill--9sigkill--kill--15sigterm">4.1 Kill -9(SIGKILL) / kill -15(SIGTERM)</h3>
<ul>
<li>리눅스 환경에서 PID를 죽여서 프로세스를 죽입니다. </li>
</ul>
<p><strong>SIGKILL</strong></p>
<ul>
<li><code>signal + kill</code> 의 약자로, 프로세스를 <strong>강제로</strong> 죽인다.</li>
<li>signal + kill 의 약자로, 프로세스를 강제로 죽인다.<pre><code class="language-shell">$ kill -9 pid</code></pre>
<br/>

</li>
</ul>
<p><strong>SIGTERM</strong>
-- <code>signal + terminate</code>의 약자로, 프로세스를 <strong>종료하라는 신호</strong>를 보낸다.
    - 단, 이 신호는 강제성 여부가 불투명하며 <strong>종료 권고에 가깝다.</strong></p>
<pre><code class="language-shell">$ kill -15 pid</code></pre>
<br/>

<h3 id="42-spring-graceful-shutdown">4.2 Spring Graceful Shutdown</h3>
<ul>
<li><p>스프링에서 kill -9 , kill -15를 적용할 수 있습니다. 이것은 사용하면 spring을 SIGKILL을 한다면 바로 종료되고 SIGTERM은 현재 프로세스가 종료되고 스프링을 종료합니다.</p>
</li>
<li><p>다음은 application.yml에 추가할 내용입니다.</p>
<pre><code class="language-yaml">server:
shutdown: graceful # default immediate
</code></pre>
</li>
</ul>
<p>spring:
  lifecycle:
    timeout-per-shutdown-phase:10s </p>
<pre><code>- 만약에 프로세스가 time-out보다 느리다면 10초가 지난 이후에는 스프링을 종료시킨다.

&gt; Spring Boot는 2.3 버전부터 Graceful Shutdown 을 지원한다.

### 4.3 예시 코드

- 다음 코드를 살펴보면 for문을 돌면서 thread.sleep을 한다. 여기서 테스트는 for문을 돌면서 log를 찍는데 이때 kill-9를 하였을 때와 kill -15를 하였을 때 graceful shutdown이 되었는지 확인하겠다.

```java
@RestController
public class ShutDownController {
    protected final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName());

    @GetMapping(&quot;/test&quot;)
    public String test() throws InterruptedException {
        logger.info(&quot;=================start==================&quot;);
        for (int i=0; i&lt; 30; i++){
            logger.info(&quot;test2 : {}&quot; , i);
            Thread.sleep(1000);
        }
        logger.info(&quot;=================end==================&quot;);
        return &quot;success&quot;;
    }

}
</code></pre><h3 id="44-spring-kill--9">4.4 Spring Kill -9</h3>
<ul>
<li>API를 호출하고 <code>kill -9 pid</code>를 하였을 때 바로 shut down이 되는 것을 확인할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/95e68ab2-ce62-4ef4-855c-5c7c9ab53f41/image.png" alt=""></p>
<h3 id="45-spring-kill--15">4.5 Spring Kill -15</h3>
<ul>
<li>API를 호출하고 <code>kill -15 pid</code>를 하였을 때 바로 graceful shutdown이 출력되고 모든 프로세스가 끝난 이후에 Spring이 종료된다. 
<img src="https://velog.velcdn.com/images/geon_km/post/022eec44-6f6b-4ab3-9116-ab814b4ae31d/image.png" alt=""></li>
</ul>
<h3 id="46-shutdown시-데드락">4.6 Shutdown시 데드락</h3>
<ul>
<li>만약에 프로세스를 종료하는데 데드락이 걸리면 프로세스가 꺼지지 못하는 경우가 발생할 수 있다. 이때는 시스템의 요구사항에 따라서 time-out을 설정을 해야된다.</li>
</ul>
<pre><code class="language-yaml">spring:
  lifecycle:
    timeout-per-shutdown-phase:10s </code></pre>
<ul>
<li>이때 만약에 shutdown licecycle보다 실행 중인 프로세스의 작업이 더 길어지면 강제로 종료를 시킨다.<pre><code class="language-yaml">Failed to shut down 1 bean with phase value 20122369 within timeout of 2000ms
</code></pre>
</li>
</ul>
<p>Graceful shutdown with one or more requests still active</p>
<p>```</p>
<h1 id="참고">참고</h1>
<hr>
<p><a href="https://dev.to/mostlyjason/intro-to-deployment-strategies-blue-green-canary-and-more-3a3">https://dev.to/mostlyjason/intro-to-deployment-strategies-blue-green-canary-and-more-3a3</a></p>
<p><a href="https://sihyung92.oopy.io/server/nginx_feat_apache">https://sihyung92.oopy.io/server/nginx_feat_apache</a></p>
<p><a href="https://hudi.blog/springboot-graceful-shutdown/">https://hudi.blog/springboot-graceful-shutdown/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[인덱스]]></title>
            <link>https://velog.io/@geon_km/%EC%9D%B8%EB%8D%B1%EC%8A%A4</link>
            <guid>https://velog.io/@geon_km/%EC%9D%B8%EB%8D%B1%EC%8A%A4</guid>
            <pubDate>Tue, 27 Feb 2024 12:25:38 GMT</pubDate>
            <description><![CDATA[<h1 id="1서론">1.서론</h1>
<hr>
<h3 id="글을-작성하는-이유">글을 작성하는 이유</h3>
<ul>
<li><p>백엔드 개발자로 근무하면서 학습이 필요한 부분은 매우 많다. 취준을 하면서 데이터베이스 인덱스를 공부를 하였다. 그런데 추가적으로 글을 작성하는 이유는 인덱스의 중요성 때문이다.</p>
</li>
<li><p>결국 병목이 발생하는 부분은 많은 데이터가 있는 데이터베이스 부분에서 자주 발생한다.  이 부분에서 인덱스를 통하여 문제를 해결할 수 있는 경우가 매우 많았고 인덱스를 적절하게 걸어야지 성능이 향상되지 잘못되게 걸면 오히려 성능이 나빠진다. </p>
</li>
<li><p>이번에 자세하게 인덱스를 학습하여 현업에서 일을 더 잘하기 위해서 작성한다.</p>
</li>
</ul>
<br/>
<br/>

<h1 id="2본론">2.본론</h1>
<hr>
<h2 id="2-1인덱스를-사용하는-이유">2-1.인덱스를 사용하는 이유</h2>
<ul>
<li>컴퓨터 구조를 살펴보면 데이터를 저장하는 공간은 <code>디스크</code>, <code>메모리</code>가 있다. </li>
</ul>
<table>
<thead>
<tr>
<th></th>
<th>메모리</th>
<th>디스크</th>
</tr>
</thead>
<tbody><tr>
<td>속도</td>
<td>빠르다.</td>
<td>느리다.</td>
</tr>
<tr>
<td>영속성</td>
<td>전원이 공급하지 않으면 휘발</td>
<td>영속성을 지닌다.</td>
</tr>
<tr>
<td>가격</td>
<td>비쌈</td>
<td>저렴하다.</td>
</tr>
</tbody></table>
<ul>
<li>데이터베이스의 데이터는 디스크에 저장한다. 결국 데이터베이스의 데이터는 디스크에 저장되고 성능이 느리다. 이러한 문제점을 해결하기 위해서는 디스크 I/O를 최소화를 하는 것이다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/7f706716-6fa1-45e4-9c4c-153bd4ef2cc2/image.png" alt=""></p>
<ul>
<li><p>디스크에 접근하는 방식은 크게 2가지가 있다. 랜덤 방식은 무작위로 데이터를 가져오는 방식인 랜덤 I/O 그리고 연속된 Block의 데이터를 가지고 오는 순차 I/O가 있다.</p>
</li>
<li><p>대부분의 트랜잭션은 무작위 Write가 발생한다. (랜덤 I/O)</p>
</li>
<li><p>랜덤I/O방식과 순차I/O 방식의 공통적인 행동이 있는데 플래터를 돌려서 읽어야 할 데이터가 저장된
위치로 디스크 헤더를 이동시킨 다음 데이터를 읽는다. 하지만 여기서 차이가 나는 부분은 순차I/O는 1번의 시스템 콜을 호출하고 랜덤I/O는 3번의 호출을 하였다.</p>
</li>
<li><p>이렇게 살펴보면 순차 I/O를 사용하면 효율이 좋겠지만 기본적으로 랜덤 I/O를 사용하고 있는 MySQL을 현실적 특성상 순차 I/O로 변경하기 어렵다.</p>
</li>
<li><p>그래서 랜덤 I/O를 사용을 하면서 접근 Row의 수를 줄이기 위해 인덱스를 사용한다.</p>
</li>
</ul>
<br/>

<h2 id="2-2인덱스-자료구조">2-2.인덱스 자료구조</h2>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/geon_km/post/beed5622-76d9-4152-9884-92f77300b200/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/geon_km/post/0f2a5317-0d4a-4ded-9b4b-c905014efeea/image.png" alt=""></th>
</tr>
</thead>
</table>
<ul>
<li>MySQL은 기본적으로 B-Tree(Balanced Tree) 형태의 (PK, Uniqe Key, Index)로 적용이 됩니다. B-Tree는 항상 정렬된 상태로 유지하기 때문에 Write 작업을 수행 시 정렬되게 저장이 됩니다. 즉. Write 작업을 수행할 때 성능적으로 이점을 얻을 수 없지만, Read 작업을 수행을 하였을 때 성능적인 이점을 얻을 수 있습니다.</li>
</ul>
<br/>

<h3 id="왜-인덱스는-btree-자료구조를-선택을-하였는가">왜 인덱스는 B+Tree 자료구조를 선택을 하였는가?</h3>
<ul>
<li>인덱스의 핵심은 탐색, 즉. 검색 범위를 최소화 하는 것이다. 다양한 자료구조에서 인덱스의 특징에 따라서 장단점을 살펴보겠다.</li>
</ul>
<ol>
<li>HashMap</li>
</ol>
<ul>
<li>HashMap은 단건의 검색 속도는 O(1)으로 매우 빠른 속도를 가지고 있다. 그러나 범위 탐색은 O(N)이며 전방 일치 탐색이 불가능하다. <code>like &#39;AB%&#39;</code> 전방 탐색을 하기 위해서는 HashMap은 키를 꺼내서 하나하나 다 비교를 하기 때문에 적절하지 않다.</li>
</ul>
<ol start="2">
<li>List</li>
</ol>
<ul>
<li>List에서 <code>정렬되지 않은 탐색은 O(N)</code>이고 <code>정렬된 리스트의 탐색은 O(logN)</code>이다. 결국 정렬되지 않은 리스트의 정렬 시간 복잡도는 <code>O(N) ~ O(N*logN)</code>이 된다.</li>
<li>또한 삽입 / 삭제를 하는 Write 작업을 하는데 비용이 매우 높다.</li>
</ul>
<ol start="3">
<li>Tree</li>
</ol>
<ul>
<li>트리는 차수 즉. 트리의 높이에 따라 시간 복잡도가 결정이 된다. 그래서 트리의 높이를 최소화하는 것이 매우 중요하다. </li>
<li>그렇기 때문에 한쪽으로 노드가 다 치우치지 않게 균형을 잡아주는 트리를 사용한다. B Tree</li>
</ul>
<ol start="4">
<li>B+Tree</li>
</ol>
<ul>
<li>B Tree말고 일반적으로 B+Tree를 사용한다. 사실 B+Tree는 B Tree에서 진화된 버전이라고 생각하면 된다.</li>
<li>B+Tree는 삽입/삭제시 항상 균형을 이룬다. ( 여기서 B는 이진이 아니라 균형을 의미한다. )</li>
<li>또한 <code>하나의 노드가 여러 개의 자식 노드를 가질 수 있다.</code> 이를 통하여 차수를 줄일 수 있다. 이를 통하여 시간 복잡도를 낮출 수 있다.</li>
<li>마지막으로 리프노드에만 데이터가 존재한다. 이거는 B Tree와 차이점으로 B Tree는 모든 노드에 데이터가 있지만 <code>B+Tree는 리프 노드에만 데이터가 있기 때문에 연속적인 데이터 접근 시 이점을 가진다.</code><br/>




</li>
</ul>
<br/>

<h2 id="2-3인덱스-종류">2-3.인덱스 종류</h2>
<p><img src="https://velog.velcdn.com/images/geon_km/post/be485239-b1a8-4a51-963c-d3415ee9aea0/image.png" alt=""></p>
<ul>
<li><p>인덱스의 종류는 크게 <code>Clustered Index / Non Clustered Index</code>로 구분할 수 있다. InnoDB 엔진은 B+Tree 구조를 기본으로 클러스터 인덱스 구조를 가진다. 클러스터 인덱스의 리프 노드에 모든 Row 데이터를 저장한다. </p>
</li>
<li><p>Clustered Index의 특징은 하나의 테이블에 1개만 존재한다. <code>PK가 있다면 PK가 Clustered가 되고 PK가 없고 Unique Key만 있다면 Clustered가 되고 모두 없다면 Hidden Key가 생성되어 Clustered</code>가 됩니다. 이때 Hidden Key는 rowId를 의미합니다.</p>
</li>
<li><p>그리고 MySQL에서 논클러스터 키의 차이점은 논클러스터는 클러스터의 주소를 가진다. 즉. 논클러스터 인덱스는 한번 탐색하고 그 주소를 기반으로 클러스터에 들어가 조회를 수행한다.</p>
</li>
</ul>
<br/>

<h2 id="2-4다중-컬럼-인덱스">2-4.다중 컬럼 인덱스</h2>
<ul>
<li>Multiple-column index는 복합 인덱스라고 알려져 있다. 이는 테이블에 여러 컬럼을 조합해서 구성되는 인덱스를 의미한다. </li>
<li>여기서 <code>중요한 부분은 복합 인덱스는 명시된 컬럼의 순으로 정렬되어 있다는 것이다.</code></li>
<li>복합 인덱스를 설계할 때 컬럼 순서에 따라서 쿼리의 성능 차이가 나기 때문에 설계가 매우 중요하다.</li>
</ul>
<blockquote>
<p>복합 인덱스에서 만약에 인덱스로 { 이름, 시력 } 으로 인덱스를 구축하면 먼저 이름 순서로 정렬 -&gt; 시력 순으로 정렬을 한다. 이때 만약에 처음 인덱스 외에 시력으로만 조건을 걸어서 쿼리를 날리면 인덱스 풀 스캔이 발생한다.</p>
</blockquote>
<h3 id="효과적인-인덱스-디자인-전략">효과적인 인덱스 디자인 전략</h3>
<ul>
<li><p>일반적으로 복합 인덱스를 설계하는 기준은 <code>카디널리티</code>입니다. 카디널리티가 높은 컬럼을 선행 컬럼으로 선정하는 것이다. 하지만 무작정 카디널리티만 높다고 인덱스를 설정하면 안된다.</p>
</li>
<li><p>자주 사용하는 쿼리가 무엇인지, 조인에도 인덱스가 사용하는지, 선행 컬럼이 범위 기반의 쿼리로 많이 이용하는가</p>
</li>
</ul>
<h3 id="실제-테스트">실제 테스트</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/7595ba4c-c6ef-49b6-a8d8-5f98322dd7d0/image.png" alt=""></p>
<pre><code class="language-sql">create table orders(
    order_id int auto_increment,
    customer_id int,
    order_date date,
    product_id int,
    quantity int,
    status varchar(50),
    primary key(order_id)
);</code></pre>
<pre><code class="language-python">import csv
import random
from datetime import datetime, timedelta

# 데이터 생성 함수
def generate_order_data(record_count):
    statuses = [&#39;배송 중&#39;, &#39;배송 완료&#39;, &#39;주문 취소&#39;]
    start_date = datetime(2023, 1, 1)
    end_date = datetime(2023, 12, 31)

    for order_id in range(1, record_count + 1):
        customer_id = random.randint(1, 10000)
        order_date = start_date + timedelta(days=random.randint(0, (end_date - start_date).days))
        product_id = random.randint(1, 1000)
        quantity = random.randint(1, 10)
        status = random.choice(statuses)

        yield [customer_id, order_date.strftime(&#39;%Y-%m-%d&#39;), product_id, quantity, status]

# CSV 파일 저장
def save_to_csv(filename, data):
    with open(filename, &#39;w&#39;, newline=&#39;&#39;, encoding=&#39;utf-8&#39;) as file:
        writer = csv.writer(file)
        writer.writerow([&#39;customer_id&#39;, &#39;order_date&#39;, &#39;product_id&#39;, &#39;quantity&#39;, &#39;status&#39;])
        writer.writerows(data)

# 메인 실행
if __name__ == &quot;__main__&quot;:
    record_count = 1000000  # 100만 건
    filename = &#39;orders_data.csv&#39;
    data = generate_order_data(record_count)
    save_to_csv(filename, data)
    print(f&quot;{filename}에 {record_count}건의 데이터가 저장되었습니다.&quot;)</code></pre>
<ul>
<li>다음 구조를 가지는 테이블에 10,000건의 데이터를 적재하고 customer_id와 order_date에 조건을 걸어 조회를 하였을 때 테이블 풀 스캔을 하고 있는 사실을 알 수 있다. 현재 인덱스가 없기 때문인데 이제 인덱스를 추가를 하겠습니다.</li>
</ul>
<pre><code class="language-sql">create index idx_order_date_customer_id on orders(order_date, customer_id);</code></pre>
<p><img src="https://velog.velcdn.com/images/geon_km/post/8c19ae8d-fa81-4b98-bc87-122a31ac61eb/image.png" alt=""></p>
<ul>
<li>일단 선행 컬럼을 order_date로 설정하고 customer_id를 후행 컬럼으로 만들고 조회를 하면 테이블 full scan을 피했지만 rows와 filtered를 보면 996224의 데이터를 탐색하고 이 중에서 3.23%만 유효하다고 나옵니다. 일단 full scan을 피했지만 아직 완전한 쿼리가 아니라고 생각합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/ba1e1183-95bc-43d6-ad74-c0267fae6e3b/image.png" alt=""></p>
<ul>
<li>다음은 customer_id를 선행으로 만들고 실행계획을 살펴보면 6개의 row를 100% 유효하게 탐색을 하는 것을 볼 수 있다. 이를 통해 알 수 있는 부분은 선행 컬럼의 선택에 따라서 성능에 유효한 영향을 줄 수 있다고 알 수 있습니다.</li>
</ul>
<br/>

<h3 id="카디널리티를-알-수-있는-쿼리">카디널리티를 알 수 있는 쿼리</h3>
<pre><code class="language-sql">analyze table orders update histogram on customer_id, order_date with 100 buckets;

use information_schema;

select * from COLUMN_STATISTICS
where table_name = &#39;orders&#39;;</code></pre>
<ul>
<li>위에 쿼리를 통하여 buckets를 구할 수 있고 이를 통하여 카디널리티를 확인할 수 있다. 현재 예시를 살펴보면 customer_id의 buckets에서 각 분포도의 3번째 값을 살펴보면 알 수 있다.</li>
</ul>
<blockquote>
<p>저는 개인적으로 =(동등 조건)과 같은 조건은 선행으로 배치하고 &lt; &gt; , Between, order by, group by의 조건은 후행으로 자주 배치를 합니다.</p>
</blockquote>
<br/>

<h2 id="2-5커버링-인덱스">2-5.커버링 인덱스</h2>
<p><img src="https://velog.velcdn.com/images/geon_km/post/ebe2b0cb-c506-4059-9c61-506d2aa39f1f/image.png" alt=""></p>
<ul>
<li>커버링 인덱스란 읽기 쿼리 성능을 높히기 위해 사용하는 방식입니다. 인덱스를 사용하여 쿼리를 처리할 때 인덱스에 있는 컬럼만드로 충분하지 않으면 실제 레코드에 접근합니다. 하지만 커버링 인덱스를 사용하면 테이블 레코드에 접근하는 방식 없이 인덱스 수준에서만 쿼리를 처리할 수 있다.</li>
</ul>
<h3 id="커버링-인덱스는-언제-사용하나요">커버링 인덱스는 언제 사용하나요??</h3>
<ul>
<li>커버링 인덱스를 사용하는 경우는 다음과 같습니다.</li>
</ul>
<ol>
<li>특정 컬럼을 자주 조회를 해야되는 상황 : 특정 컬럼 때문에 테이블에 접근하는 것이 비용이 크게 느껴질 때 유용하다.</li>
<li>조인 연산 비용 줄이기 : 여러 테이블을 연결할 때 발생하는 비용을 줄일 수 있다.</li>
<li>일기 성능의 향상이 필요한 경우 : 인덱스 레벨에서만 필터링하여 읽기 성능 향상이 필요한 경우 적용할 수 있습니다.</li>
</ol>
<h3 id="커버링-인덱스를-무조건-사용하는게-좋나요-trade-off는">커버링 인덱스를 무조건 사용하는게 좋나요?? Trade Off는</h3>
<ul>
<li>물론 성능을 향상 시키는 장점이 있어서 처음 이 키워드를 접하였을 때 혹~~!! 하였습니다. 하지만 커버링 인덱스를 적용하는데 트레이드 오프가 있습니다.</li>
</ul>
<ol>
<li>인덱스 크기 : 인덱스는 그냥 생성이 되는 것이 아닙니다. 인덱스가 커지면 인덱스 블록에 들어갈 수 있는 데이터의 수는 줄어듭니다.</li>
<li>쓰기 비용 : 인덱스에 추가된 컬럼이 자주 업데이트 되는 경우, 추가적인 쓰기 비용이 발생을 합니다.</li>
</ol>
<blockquote>
<p>쓰기 비용이 증가하는 이유는 B+Tree를 사용하기 때문입니다. 이 자료구조를 사용하면 데이터를 정렬되게 저장하기 때문에 Write 작업을 수행하면 노드의 이동이 발생을 합니다.</p>
</blockquote>
<ol start="3">
<li>컬럼 크기 : 크기가 큰 컬럼은 인덱스로 추가하기 비효율적입니다.</li>
<li>카디널리티 : 카디널리티가 낮은 컬럼을 인덱스로 추가할 경우, 읽기 성능이 향상하지 않을 수 있습니다.</li>
</ol>
<h3 id="실제-테스트-1">실제 테스트</h3>
<ul>
<li>테스트를 위한 코드는 위에 복합 인덱스를 하는 테이블과 똑같이 진행을 하겠습니다.</li>
<li>현재 인덱스는 <code>customer_id</code>,<code>order_date</code>에 걸려있다. 이때 살펴보면 좋은 부분은 조회하는 컬럼, where, group by가 있다.</li>
</ul>
<pre><code class="language-sql">explain select customer_id, order_date
from orders
where customer_id = 2565
group by customer_id, order_date
order by order_date desc
limit 10</code></pre>
<pre><code class="language-js">[
  {
    &quot;id&quot;: 1,
    &quot;select_type&quot;: &quot;SIMPLE&quot;,
    &quot;table&quot;: &quot;orders&quot;,
    &quot;partitions&quot;: null,
    &quot;type&quot;: &quot;ref&quot;,
    &quot;possible_keys&quot;: &quot;idx_customer_order_date&quot;,
    &quot;key&quot;: &quot;idx_customer_order_date&quot;,
    &quot;key_len&quot;: &quot;5&quot;,
    &quot;ref&quot;: &quot;const&quot;,
    &quot;rows&quot;: 106,
    &quot;filtered&quot;: 100,
    &quot;Extra&quot;: &quot;Backward index scan; Using index&quot;
  }
]</code></pre>
<ul>
<li><p>해당 실행계획을 살펴보면 <code>Using Idex</code>를 살펴볼 수 있다. 이것은 커버링 인덱스를 의미한다. 이 쿼리가 왜 커버링 인덱스로 조회된 이유는 조회하는 컬럼, group by, where절이 모두 복합 인덱스로 이루어져 있고 where절에서 customer_id로 조회하여 복합 인덱스의 선형 인덱스로 적용되었기 때문이다. </p>
</li>
<li><p>이제 조회하는 컬럼, where, group by를 인덱스와 다르게 하였을 때 발생하는 문제에 대해서 알아보겠다.</p>
</li>
</ul>
<h3 id="조회하는-컬럼">조회하는 컬럼</h3>
<pre><code class="language-sql">explain select customer_id, order_date, product_id
from orders
where customer_id = 2565
order by order_date desc
limit 10;</code></pre>
<ul>
<li>해당 쿼리를 살펴보면 현재 인덱스 <code>customer_id</code>  , <code>order_date</code>가 있는데 여기서는 <code>product_id</code>가 추가가 되었습니다. 인덱스에서는 product_id에 대한 정보가 없기 때문에 결국 테이블에 접근하여 관련된 정보를 얻어와야 합니다. 결국 커버링 인덱스가 성립하지 못합니다.</li>
</ul>
<pre><code class="language-js">[
  {
    &quot;id&quot;: 1,
    &quot;select_type&quot;: &quot;SIMPLE&quot;,
    &quot;table&quot;: &quot;orders&quot;,
    &quot;partitions&quot;: null,
    &quot;type&quot;: &quot;ref&quot;,
    &quot;possible_keys&quot;: &quot;idx_customer_order_date&quot;,
    &quot;key&quot;: &quot;idx_customer_order_date&quot;,
    &quot;key_len&quot;: &quot;5&quot;,
    &quot;ref&quot;: &quot;const&quot;,
    &quot;rows&quot;: 106,
    &quot;filtered&quot;: 100,
    &quot;Extra&quot;: &quot;Backward index scan&quot;
  }
]</code></pre>
<h3 id="where">where</h3>
<ul>
<li>커버링 인덱스는 인덱스으로 결과를 처리하는 경우이다. 이때 복합 인덱스의 경우에는 처음 인덱스 이외에 후행 인덱스로 조회를 하였을 때 방식이 다르다. Mysql 8.0으로 넘어가면서 Skip scan이 생겨서 더욱 인덱스의 탐색에 도움을 주었다.</li>
</ul>
<pre><code class="language-sql">explain select customer_id, order_date
from orders
where customer_id = 2565
order by order_date desc
limit 10;</code></pre>
<pre><code class="language-sql">explain select customer_id, order_date
from orders
where order_date = &#39;2023-01-01&#39;
# where product_id = 1
order by order_date desc
limit 10;
</code></pre>
<blockquote>
<p>인덱스 스킵 스캔
MySQL 8.0 버전부터는 다중 칼럼 인덱스에서 옵티마이저가 특정 칼럼 인덱스를 건너 뛰어서 검색할 수 있도록하는 인덱스 스킵 스캔(index skip scan) 최적화 기능이 도입되었습니다. 이를 통해 인덱스를 통한 검색의 용도가 더 넓어졌습니다.</p>
</blockquote>
<h3 id="where--group-by--order-by">WHERE + GROUP BY + ORDER BY</h3>
<ul>
<li>결론부터 말하면 <code>WHERE + ORDER BY 의 경우엔 WHERE가 동등일 경우엔 ORDER BY가 나머지 인덱스 컬럼만 있어도 인덱스를 탈 수 있으나 GROUP BY + ORDER BY 의 경우엔 둘다 인덱스 컬럼을 탈수있는 조건이어야만 합니다.</code></li>
</ul>
<br/>

<h2 id="2-6형변환">2-6.형변환</h2>
<p><img src="https://velog.velcdn.com/images/geon_km/post/4f2a86ac-6d60-477f-b6b3-abf72f63fe9a/image.png" alt=""></p>
<ul>
<li>파란색인 컬럼은 단일 컬럼 인덱스가 설정이 되어져 있습니다. 그 전에 형번환에 우선순위에 대해서 설명하겠습니다.</li>
</ul>
<ol>
<li>문자와 숫자가 만나면 문자가 숫자로 형변환됩니다.</li>
<li>문자와 날짜는 양쪽으로 형변환됩니다.</li>
<li>형변환은 유리한 방향으로 이루어집니다. 즉, 데이터를 더 큰 범위의 형식으로 변환하려고 합니다.</li>
<li>변수쪽이 형변환되면 인덱스를 탈 수 없게 됩니다.</li>
</ol>
<ul>
<li>이러한 우선순위를 고려하여 쿼리를 작성해야 한다. 형변환이 발생하며 인덱스를 탈 수 있는지 여부를 판단할 수 있다.</li>
</ul>
<pre><code class="language-sql"># 1. 문자는 숫자로 형변환이 되어서 인덱스 스캔을 합니다.
select * from order where order_no = &#39;1&#39;;

# 1. order_status_cd 쪽이 문자에서 숫자로 형변환이 되어서 인덱스를 타지 않는다.
select * from order where order_status_cd = 10

# 2. 문자형은 date/time형태로 형변환을 하여 인덱스로 활용할 수 있습니다.
select * from order where order_ymdt = &#39;2024-03-24&#39;;

# 2. 상수쪽이 datetime으로 형변환이 되므로 인덱스를 활용합니다.
select * from order where ordr_ymdt = cast(&#39;2024-03-24&#39; as datetime);

# 컬럼을 substring으로 형변환이 되었기 때문에 인덱스를 탈 수 없다. 이 방식은 between을 통해 개선할 수 있습니다.
select * from order where substring(ordr_ymdt , 1, 4) = &#39;2023&#39; and substring(ordr_ymdt , 6, 2) = &#39;02&#39;;</code></pre>
<br/>

<h2 id="2-7null의-인덱싱">2-7.Null의 인덱싱</h2>
<ul>
<li>MySQL에서는 NULL 값도 인덱싱 처리가 가능합니다. 이러한 처리를 통해 IS NULL 조건으로 검색 시에도 인덱스 range scan을 통해 빠르게 처리할 수 있습니다.</li>
</ul>
<p>먼저, 테이블의 NULL 값을 인덱싱하기 위해서는 추가적인 공간이 필요합니다. 일반적으로 NULL 값을 인덱싱하기 위해서는 1bit의 추가 공간이 필요하며, 이는 인덱스 내에서 NULL 값을 표현하기 위한 용도로 사용됩니다.</p>
<p>예를 들어, 다음과 같이 테이블을 변경하여 NULL 값을 인덱싱할 수 있습니다.</p>
<pre><code class="language-sql">ALTER TABLE order MODIFY COLUMN age INT NULL;</code></pre>
<p>위와 같이 테이블의 컬럼 속성을 nullable로 변경함으로써 NULL 값을 인덱싱할 수 있게 됩니다.</p>
<p>그 후, 예를 들어 다음과 같이 NULL 값에 대한 쿼리를 수행할 때에도 인덱스를 활용하여 빠르게 처리할 수 있습니다.</p>
<pre><code class="language-sql">EXPLAIN SELECT * FROM order WHERE age IS NULL;</code></pre>
<p>위의 쿼리를 수행하면 인덱스 range scan을 통해 빠르게 NULL 값을 찾을 수 있습니다.</p>
<p>하지만, NULL 값을 가지지 않는 NOT NULL 컬럼에 대해서는 NULL 비교를 수행하는 쿼리는 불가능한 비교에 해당하므로 MySQL은 스키마 정보만으로 빠르게 Impossible WHERE임을 판단하여 처리합니다.</p>
<p>따라서, MySQL에서는 NULL 값을 인덱싱 처리할 수 있으며, 이를 통해 NULL 값을 가진 레코드를 효율적으로 조회할 수 있습니다.</p>
<br/>

<h2 id="2-8-in절-인덱스-처리">2-8. IN절 인덱스 처리</h2>
<ul>
<li><p>MySQL에서 인덱싱된 컬럼에 대한 IN 쿼리 검색은 MySQL에 내부적으로 UNION 방식으로 바꾸어서 처리를 합니다.</p>
</li>
<li><p>위에 예제를 기반으로 관련 쿼리를 만들겠습니다.</p>
</li>
</ul>
<pre><code class="language-sql">create index idx_order_customer_order on orders (customer_id, order_date);

explain  select * from orders where customer_id in (&#39;2911&#39;,&#39;6947&#39;) and order_date &gt; &#39;2023-01-01&#39;;</code></pre>
<ul>
<li>해당 쿼리는 customer_id, order_date로 이루어진 인덱스에 IN으로 인덱싱 조회를 하는 쿼리 입니다. 실제 이것의 <code>explain. analyze</code>를 통해 살펴보면 실제로 실행된 쿼리는 다음과 같습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/3dd2766d-f0ca-49b1-9770-e2535d1a7569/image.png" alt=""></p>
<pre><code class="language-sql">select * from orders customer_id = 2911 AND &#39;2023-01-01&#39; &lt; order_date
    union
select * from orders customer_id = 6947 AND &#39;2023-01-01&#39; &lt; order_date</code></pre>
<br/>


<h2 id="2-9-인덱스를-안타는-쿼리">2-9. 인덱스를 안타는 쿼리</h2>
<ul>
<li>위에 내용을 보면 인덱스에 대해서 대표적인 기능에 대해서 살펴봤습니다. 어느정도 인덱스에 대해서 학습을 하였는데 마지막으로 쿼리를 작성하면서 인덱스를 안타는 쿼리에 대해서 대표적인 부분만 설명을 하겠습니다.</li>
</ul>
<h3 id="1-함수나-연산자를-사용">1. 함수나 연산자를 사용</h3>
<ul>
<li>인덱스가 있는 열에 함수, 또는 연산을 수행하는 경우 데이터베이스는 인덱스를 사용하지 못한다. 또한 where절에 컬럼을 수정하는 경우 인덱스를 탈 수 없습니다.</li>
</ul>
<pre><code class="language-sql">select * from orders where upper(status)) = &#39;SHIPPED&#39;;
explain select * from orders where year(order_date) = 2023;
explain select * from orders where customer_id +1 = 2565;</code></pre>
<h3 id="2-like문-검색에서-와일드카드의-위치">2. LIKE문 검색에서 와일드카드의 위치</h3>
<ul>
<li>LIKE 절에서 와일드카드 %가 뒤에 있는 경우, 인덱스를 활용할 수 있지만, %가 앞이나 중간에 있을 경우는 인덱스를 활용할 수 없습니다. 이는 인덱스의 B-트리 구조와 LIKE 절의 검색 패턴이 일치하지 않기 때문입니다.</li>
</ul>
<pre><code class="language-sql"># 인덱스 적용
select * from orders where customer_name = &#39;John%&#39;;

# 인덱스 적용 X
select * from orders where customer_name = &#39;%John&#39;;

# 인덱스 적용 X
select * from orders where customer_name = &#39;%John%&#39;;</code></pre>
<h3 id="3-or절을-사용">3. OR절을 사용</h3>
<ul>
<li>OR 절을 사용하는 경우 인덱스를 활용할 수 없습니다. OR 절은 여러 개의 조건 중 하나라도 참이면 전체 조건을 참으로 판단하므로 데이터베이스가 모든 가능성을 검사하고 결과를 결합해야 합니다. 따라서 최적의 OR 조건을 뽑기 어려워 인덱스를 사용할 수 없습니다.<pre><code class="language-sql"># 인덱스 적용 X
select * from orders where customer_id = &#39;2911&#39; or order_date &gt; &#39;2023-01-01&#39;;</code></pre>
</li>
</ul>
<h3 id="4-테이블-전체를-반환">4. 테이블 전체를 반환</h3>
<ul>
<li>테이블의 전체 레코드를 반환하는 경우에는 인덱스를 사용할 필요가 없습니다. 데이터베이스가 모든 레코드를 반환해야 하므로 인덱스를 사용할 이유가 없습니다.<pre><code class="language-sql">select * from orders;</code></pre>
</li>
</ul>
<h3 id="5-컬럼의-자료형이-다른-검색">5. 컬럼의 자료형이 다른 검색</h3>
<ul>
<li>컬럼의 자료형이 다른 경우 비교하는 쿼리를 실행할 때 인덱스의 성능이 저하될 수 있습니다. 인덱스는 데이터의 값을 기반으로 정렬되어 있기 때문에 자료형 변환이 필요할 경우 성능이 저하될 수 있습니다.</li>
<li>문자와 숫자가 만나면 문자가 숫자로 형변환이 된다. 이때 문자에서 숫자로 형변환이 되지 않는 경우 인덱스를 타지 않는다.<pre><code class="language-sql">select * from order where order_status_cd = 10</code></pre>
</li>
</ul>
<h3 id="7-in-연산자를-사용한-검색에서-in-목록의-개수가-많은-경우">7. IN 연산자를 사용한 검색에서 IN 목록의 개수가 많은 경우</h3>
<ul>
<li>IN 연산자를 사용한 검색에서 IN 목록의 개수가 많을 경우 인덱스를 사용하지 않고 수행할 가능성이 높습니다. 이 경우 JOIN을 사용하여 인덱스를 효율적으로 사용할 수 있습니다.</li>
</ul>
<pre><code class="language-sql">SELECT * FROM order WHERE id IN (1, 2, 3, ..., 10000);</code></pre>
<br/>

]]></description>
        </item>
        <item>
            <title><![CDATA[@ MybatisTest 트랜잭션 동작하지 않는다?]]></title>
            <link>https://velog.io/@geon_km/MybatisTest-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EB%8F%99%EC%9E%91%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4</link>
            <guid>https://velog.io/@geon_km/MybatisTest-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EB%8F%99%EC%9E%91%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4</guid>
            <pubDate>Tue, 20 Feb 2024 12:56:19 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<hr>
<ul>
<li>Mybatis를 사용한다. 도중에 테스트 코드를 작성하면서 (@MybatisTest) 트랜잭션이 동작하지 않는 문제점을 발견을 했다. </li>
<li>테스트를 위해서 스프링부트에서 지원하는 @MybatisTest를 작성하고 write 작업을 작성을 하였습니다. 테스트를 통과하고 데이터베이스를 확인해봤는데, 롤백을 기대를 하였는데 데이터가 들어가 있었다.</li>
</ul>
<br/>


<h1 id="본론">본론</h1>
<hr>
<h2 id="이슈">이슈</h2>
<ul>
<li><p>데이터베이스에는 단위테스트를 생성하고 데이터가 쌓이고 있었다. 여기에서 나는 트랜잭션이 문제가 있다고 생각을 하였다.</p>
</li>
<li><p>그러면 코드를 보면서 문제를 살펴보겠습니다.</p>
</li>
</ul>
<pre><code class="language-java">@MybatisTest
@Transactional(value = &quot;transactionManager&quot;)
@Import({DataSourceConfig.class, LadderDAO.class})
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class LadderDAOTest</code></pre>
<ul>
<li>일단 dao 부분을 검증하는 코드이다. @MybatisTest를 살펴보면 트랜잭션이 선언이 되어져 있다. 하지만 DataSourceConfig에서 DB와 트랜잭션을 선언을 하였기 때문에 추가로 설정을 하였다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/9927a5c8-5032-455d-a5c1-4c9553bbe228/image.png" alt=""></p>
<ul>
<li>처음에는 MybatisTest의 문제라고 생각하여 @SpringbootTest를 하였지만 똑같은 결과가 발생을 하였다.</li>
</ul>
<h2 id="원인-해결">원인, 해결</h2>
<ul>
<li>일단 많은 삽질을 하였지만 결국은 스토리지 엔진의 문제였습니다. 우리가 흔히 알고있는 MySQL과 MariaDB는 비슷하고 거의 같다고 알고있다. MySQL의 5.3버전 이전에는 MyISAM을 기본적인 스토리지 엔진을 사용을 하였지만 이후부터 InnoDB을 기본적인 엔진으로 선택을 하였다. </li>
<li>이 중에서 가장 큰 차이는 트랜잭션의 지원 여부라고 생각한다. 문제는 내가 테스트를 하고 있었던 테이블은 MyISAM을 사용하고 있어서 트랜잭션이 정상적으로 동작을 하지 못하고 있었다.</li>
</ul>
<blockquote>
<p>mysql, mariadb은 백업의 기능이 없다. 그래서 테스트를 진행하기 이전에 Copy table, Insert Into를 통하여 테스트 테이블을 만들고 개인적으로 수행한다. 그런데 이때 Copy table을 통해서 생성하면 MyiSam으로 생성이 되는 경우가 있어서 문제가 발생을 하였다. </p>
</blockquote>
<pre><code class="language-sql">CREATE TABLE new_table_name LIKE old_table_name;
INSERT INTO new_table_name SELECT * FROM old_table_name;
ALTER TABLE new_table_name ADD PRIMARY KEY (column_name);</code></pre>
<br/>

<blockquote>
<p>혹시 MySQL에 대해서 궁금하면 이전에 정리한 내용을 보면 좋을거 같다.
<a href="https://velog.io/@geon_km/MySQL-8.0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98">https://velog.io/@geon_km/MySQL-8.0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</a></p>
</blockquote>
<br/>

<pre><code class="language-sql">show table status
where name = &#39;테이블 이름&#39;;
</code></pre>
<ul>
<li><p>다음 쿼리를 실행을 하면 스토리지 엔진을 확인할 수 있다. 그런데 궁금한 부분은 스토리지 엔진에 MyISAM과 InnoDB가 불규칙적으로 되어져 있었다. </p>
</li>
<li><p>이 문제는 처음에 설정의 문제라고 생각을 했지만 문제는 테이블별로 내가 저장소 엔진 설정을 하지 않았다는 점이었다. </p>
</li>
</ul>
<ul>
<li>그러면 테스트 코드에 대해서 살펴보자</li>
</ul>
<pre><code class="language-java">@Test
public void testGameLeaderboard() throws Exception {
    //given
    final String gameCode = &quot;game1&quot;;
    final String eventCode = &quot;event1&quot;;
    final String roundCode = &quot;round1&quot;;
    final String trackCode = &quot;track1&quot;;
    final String carCode = &quot;car1&quot;;
    final String playerName = &quot;Player1&quot;;
    final String country = &quot;Country1&quot;;
    final String countryCode = &quot;C1&quot;;
    final String lapTime = &quot;1:00.000&quot;;
    final int totalLaps = 10;
    final int totalTime = 600000;

    LadderVO gameInfo = LadderVO.builder()
            .game_code(gameCode)
            .event_code(eventCode)
            .round_code(roundCode)
            .track_code(trackCode)
            .car_code(carCode)
            .nickname(playerName)
            .country(country)
            .country_code(countryCode)
            .lap_time(lapTime)
            .total_time(String.valueOf(totalTime))
            .build();

    //when
    ladderDAO.insertLadderInfo(gameInfo);

    List&lt;LadderVO&gt; leaderboard = ladderDAO.getLadder();
    List&lt;LadderVO&gt; bestLeaderboard = ladderDAO.getBestLadder();
    assertAll(
            ()-&gt;assertThat(leaderboard).extracting(LadderVO::getNickname)
                    .contains(playerName),
            ()-&gt; assertThat(leaderboard).extracting(LadderVO::getGame_code)
                    .contains(gameCode),
            ()-&gt; assertThat(leaderboard).extracting(LadderVO::getEvent_code)
                    .contains(eventCode),
            ()-&gt; assertThat(leaderboard).extracting(LadderVO::getRound_code)
                    .contains(roundCode),
            ()-&gt; assertThat(leaderboard).extracting(LadderVO::getTrack_code)
                    .contains(trackCode)

    );

    assertAll(
            ()-&gt;assertThat(bestLeaderboard).extracting(LadderVO::getNickname)
                    .contains(playerName),
            ()-&gt; assertThat(bestLeaderboard).extracting(LadderVO::getGame_code)
                    .contains(gameCode),
            ()-&gt; assertThat(bestLeaderboard).extracting(LadderVO::getEvent_code)
                    .contains(eventCode),
            ()-&gt; assertThat(bestLeaderboard).extracting(LadderVO::getRound_code)
                    .contains(roundCode),
            ()-&gt; assertThat(bestLeaderboard).extracting(LadderVO::getTrack_code)
                    .contains(trackCode)
    );
}</code></pre>
<br/>

<h2 id="mybatistest">@MybatisTest</h2>
<ul>
<li>각 layer별로 테스트의 전략을 고민을 하였다. dao는 결국 db와 데이터 정합성을 측정이 가장 큰 목적이라고 생각하여 <code>@MybatisTest</code>를 통하여 순수한 mybatis의 테스트를 수행하기 위해 사용을 했다.</li>
</ul>
<h3 id="의존성">의존성</h3>
<ul>
<li>위의 테스트를 진행하기 위해서는 의존성을 먼저 build.gradle에 추가를 한다.</li>
</ul>
<pre><code class="language-java">dependencies {
    testImplementation(&quot;org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.2&quot;)
}</code></pre>
<h3 id="dao">DAO</h3>
<pre><code class="language-java">@Repository
public class MemberDao {

    private final SqlSession sqlSession;

    public CityDao(SqlSession sqlSession) {
        this.sqlSession = sqlSession;
    }

    public Member selectMemberById(long id) {
        return this.sqlSession.selectOne(&quot;selectMemberById&quot;, id);
    }

}</code></pre>
<ul>
<li>sqlsession을 통해 스프링 트랜잭션 설정에 따라 자동으로 커밋 또는 롤백을 수행하고 쓰레드에 안전한게 스프링 빈으로 주입이 될 수 있게 만든다.</li>
</ul>
<h3 id="테스트">테스트</h3>
<pre><code class="language-java">@MybatisTest
@Import(MemberDao.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class MemberDaoTest {

    @Autowired
    private MemberDao memberDao;

    @Test
    public void selectCityByIdTest() {
        Member member = cityDao.selectMemberById(1);
        assertThat(member.getName()).isEqualTo(&quot;김무건&quot;);
        assertThat(member.getState()).isEqualTo(&quot;공부중&quot;);
        assertThat(member.getCountry()).isEqualTo(&quot;한국&quot;);
    }

}</code></pre>
<ul>
<li>import를 통해서 dao에 대한 정보를 얻고 만약에 datasource로 추가적인 설정을 하였다면 그 부분도 추가하면 된다.</li>
</ul>
<h1 id="참고">참고</h1>
<hr>
<p><a href="https://jehuipark.github.io/note/transaction-do-not-work-issue">https://jehuipark.github.io/note/transaction-do-not-work-issue</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[다중 서버 환경에서 세션 불일치]]></title>
            <link>https://velog.io/@geon_km/%EB%8B%A4%EC%A4%91-%EC%84%9C%EB%B2%84-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EC%84%B8%EC%85%98-%EB%B6%88%EC%9D%BC%EC%B9%98</link>
            <guid>https://velog.io/@geon_km/%EB%8B%A4%EC%A4%91-%EC%84%9C%EB%B2%84-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EC%84%B8%EC%85%98-%EB%B6%88%EC%9D%BC%EC%B9%98</guid>
            <pubDate>Wed, 24 Jan 2024 11:07:39 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<hr>
<p><strong>[ 글을 작성한 배경 ]</strong></p>
<ul>
<li>최근 업무를 진행하면서 분산 환경에서 세션의 불일치를 해결하면서 학습한 내용을 기록하기 위해 작성했습니다.</li>
</ul>
<p>**[ 문제 상황 ] **</p>
<ul>
<li>단일 서버에서 세션을 관리하면 WAS에서 세션이 관리가 가능하다. 하지만 서버가 2개가 된다면 WAS에서 세션을 공유하지 못하기 때문에 세션 불일치가 발생을 한다.</li>
</ul>
<p>**[ 해결 ] **</p>
<ul>
<li>일반적으로 세션 스토리지 ( 영속성 )를 통하여 세션의 정합성을 해결할 수 있다. 보통 Redis를 사용하거나 RDBMS를 통해 세션을 영속하여 해결하지만 각 상황에 따라서 선택이 다르게 해야된다.</li>
</ul>
<br/>

<h1 id="본론">본론</h1>
<hr>
<ul>
<li>최근 웹 서비스는 대부분 스케일 아웃으로 서버를 확장하기 때문에 일반적으로 다중 서버를 구축합니다. 단일 서버에서는 세션 불일치의 문제가 생기지 않지만 다중 서버로 넘어가며 이 문제가 발생을 합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/576718e6-efd8-4e99-9699-b4feb04d7e67/image.png" alt=""></p>
<ul>
<li>만약에 was가 총 3대가 있다고 가정하면 만약에 유저가 요청이 들어올 때 마다 1-&gt;2-&gt;3으로 순서대로 라운드 로빈 방식으로 분산한다면 was1에 로그인을 하여 세션을 만들고 was2에 들어가면 세션을 찾을 수 없다.
이러한 문제를 세션 불일치라고 말하며 이를 해결하기 위해서는 크게 3가지 방식이 있다.</li>
</ul>
<h2 id="1-sticky-session">1. Sticky Session</h2>
<h3 id="1-1sticky-session이란">1-1.Sticky Session이란</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/6d324281-0b85-4992-8da0-0507989a1901/image.png" alt=""></p>
<ul>
<li><p>Sticky Session이란 클라이언트의 요청으로 생성된 was에 전달하는 방식을 의미를 한다. 세션 정보가 없는 유저가 요청을 한 경우 로드밸런서의 기본 알고리즘대로 요청을 전달한다.</p>
</li>
<li><p>Sticky Session경우 사용자에 대한 세션이 생성된 서버로 고정이 된다. 이때 이것을 판별하는 방식은 쿠키, IP를 기반으로 판단한다.</p>
</li>
</ul>
<h3 id="1-2장점">1-2.장점</h3>
<ul>
<li><p>세션 정합성 , 캐싱 
기존의 세션 불일치를 특정 서버로 전송하며 세션 정합성을 맞추며 캐싱을 통하여 더 빠른 응답 속도를 가질 수 있다.</p>
</li>
<li><p>고립성
사용자가 특정 서버와의 세션을 유지하면서 애플리케이션에 영향을 주지 않는다. 이를 통하여 세션 간의 간섭이 줄어들어 애플리케이션의 안전성을 향상을 시킨다.</p>
</li>
</ul>
<h3 id="1-3단점">1-3.단점</h3>
<ul>
<li><p>성능 저하, 로드 밸런서 목적 실패
처음으로는 특정 서버에 트래픽이 집중될 경우 성능에 문제가 발생한다. 로드 밸런서의 목적은 부하를 적절하게 분산이다. 하지만 Sticky Session으로 인한 한 서버에 부하가 몰리면 로드 밸런서의 목적을 달성할 수 없다.</p>
</li>
<li><p>에러 분산, 세션 유실
하나의 서버에 장애가 발생하면, 해당 서버가 가지고 있는 세션 정보의 유실된다. 이러면 다시 로그인을 해야되는 가용성의 문제를 가지게 된다.</p>
</li>
</ul>
<br/>

<h2 id="2-session-clustering">2. Session Clustering</h2>
<p><img src="https://velog.velcdn.com/images/geon_km/post/6a0ba008-ef00-4207-914e-790657733eb0/image.png" alt=""></p>
<h3 id="2-1session-clustering이란">2-1.Session Clustering이란</h3>
<ul>
<li><p>위에 sticky session은 각 서버에 세션을 저장하고 사용자를 해당 was로 보내어 오히려 성능이 안좋아지는 결과를 만들 수 있다. 세션 클러스터링 방식은 세션을 각 서버에 저장이 아닌 데이터를 복사해 데이터를 전파해 가져다 쓸 수 있는 방식이다.</p>
</li>
<li><p>클러스터 내의 모든 서버들이 세션을 공유할 수 있도록 하는 방식으로 방식에는 크게 all-to-all session replication, primary-secondary session replication방식이 있다.</p>
</li>
</ul>
<h3 id="2-2장점">2-2.장점</h3>
<ul>
<li><p>이론적 무제한 확장
이론적으로 제한없이 확장할 수 있다. 서비스 확장 가능하게 디자인된 경우 더 많은 노드를 추가하여 제한없이 증가할 수 있다. 쉽게 조정하고 리소스에 대한 비용만 지불하면 된다.</p>
</li>
<li><p>아키텍처 동일성 유지
소프트웨어 아키텍처가 동일하게 유지, 그에 따라서 프로그램 난이도가 스케일아웃에 비해서 상대적으로 단순하다.</p>
</li>
</ul>
<h3 id="2-3단점">2-3.단점</h3>
<ul>
<li><p>세션 세팅의 어려움
스케일 아웃 관점에서 새로운 서버를 만들 때 기존에 존재하는 was에 새로운 서버 ip/port를 입력해야되는 불편함이 있다.</p>
</li>
<li><p>추가 메모리 사용
서버마다 동일한 세션을 가지고 있어야 하기 때문에 서버가 확장될 수록 복제해야 할 세션 데이터가 늘어나 오버헤드로 이루어진다.</p>
</li>
<li><p>시차로 인한 세션 불일치
세션 전파 작업 중 모든 서버에 세션이 전파되기까지의 시간차로 인하여 세션 불일치의 문제가 발생할 수 있다.</p>
</li>
</ul>
<br/>

<h2 id="3-세션-스토리지-분리">3. 세션 스토리지 분리</h2>
<p><img src="https://velog.velcdn.com/images/geon_km/post/123391d0-3015-4269-a456-7c8c90c6c67e/image.png" alt=""></p>
<h3 id="3-1세션-스토리지-분리란">3-1.세션 스토리지 분리란</h3>
<ul>
<li>세션 스토리지 방식은 외부 서버에 세션을 저장하는 방식이다. 이때 일반적으로 영속성이 있는 DB에 사용한다. 주로 MySQL 또는 Redis에 저장한다. 자주 입출력이 잦은 세션 특성상 메모리에 위치한 DB를 사용한다.
이 중에서 Redis, Memcached가 있다. 둘다 {key,value}로 데이터를 저장하고 빠른 응답속도를 자랑한다. 하지만 Redis는 다양한 자료구조 및 복제, 복구에 대한 다양한 서비스를 제공한다. 이러한 특성 때문에 Redis를 사용하는게 더 많이 사용한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/1d6104de-3102-4caf-9f47-57f897a9ff86/image.png" alt=""></p>
<h3 id="업무에서는-어떤-방식을-사용하여-세션을-관리를-했는가">업무에서는 어떤 방식을 사용하여 세션을 관리를 했는가</h3>
<ul>
<li>물론 Redis를 사용하는게 성능적으로 좋은 방식인걸 알았지만 저는 RDBMS에 세션을 저장을 하였습니다. 이를 선택한 이유는 해당 프로젝트에서 Redis를 사용하는 기능이 없어 세션 분리를 위해서 Redis를 사용하는게 적절한 방식인가에 대해서 고민이 되었고 개발 인원이 부족하여 Redis를 모니터링 및 운영하기에 적절하지 않다고 판단하여 MySQL에서 세션을 관리를 하였습니다.</li>
</ul>
<h2 id="4-스프링에서-mysql에서-세션-관리하기">4. 스프링에서 MySQL에서 세션 관리하기</h2>
<ul>
<li>Spring에서는 jdbcSession을 사용하여 간단하게 세션을 관리할 수 있습니다. 일단 관련 의존성을 build.gradle에 등록을 합니다.</li>
</ul>
<pre><code class="language-java">    implementation&#39;org.springframework.boot:spring-boot-starter-jdbc&#39;
    implementation &#39;org.springframework.session:spring-session-jdbc&#39;</code></pre>
<ul>
<li>세션을 사용하기 위해서는 테이블을 생성을 해야됩니다. 일단 mybatis에서는 해당 쿼리를 생성을 하거나 다음과 같은 옵션을 설정을 해야됩니다. 아래는 application.yml 코드입니다.</li>
</ul>
<pre><code class="language-java">spring.session.store-type=jdbc
spring.session.jdbc.initialize-schema=always</code></pre>
<ul>
<li>initalize를 설정을 하거나 직접 테이블을 생성을 해야됩니다.<pre><code class="language-sql">CREATE TABLE SPRING_SESSION (
  SESSION_ID CHAR(36) NOT NULL,
  CREATION_TIME BIGINT NOT NULL,
  LAST_ACCESS_TIME BIGINT NOT NULL,
  MAX_INACTIVE_INTERVAL INT NOT NULL,
  PRINCIPAL_NAME VARCHAR(100),
  CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);
</code></pre>
</li>
</ul>
<p>CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);</p>
<p>CREATE TABLE SPRING_SESSION_ATTRIBUTES (
    SESSION_ID CHAR(36) NOT NULL,
    ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
    ATTRIBUTE_BYTES LONGVARBINARY NOT NULL,
    CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME),
    CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE
);</p>
<p>CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID);</p>
<pre><code>- 만약에 JPA를 사용한다면 sql을 만들어 claassPath로 데이터를 넣으면 된다.
```java
spring:
  datasource:
    data: classpath:schema-h2.sql # Spring Session 테이블 스키마 적용
  jpa:
    show-sql: true # JPA로 생성되는 쿼리 확인
    hibernate:
      ddl-auto: create # 프로젝트 시작시 테이블 생성
  h2:
    console:
      enabled: true
      path: /h2-console # h2 db 웹 클라이언트 접속 url
  devtools:
    livereload:
      enabled: true # 정적파일들의 실시간 갱신</code></pre><ul>
<li>이후 스키마를 적용하고 JdbcSession옵션을 활성화를 시킨다. 이거는 config파일을 하나 만들어 <code>EnableJdbcHttpSession</code>어노테이션을 추가를 시킨다.</li>
</ul>
<pre><code class="language-java">@EnableJdbcHttpSession
public class AppConfig {
}</code></pre>
<p><img src="https://velog.velcdn.com/images/geon_km/post/47a2c191-d307-443a-b314-a045d184db6d/image.png" alt=""></p>
<ul>
<li>다음과 같이 설정하면 spring_session, spring_session_attribute를 생성을 합니다. 여기에 세션을 저장을 합니다. spring_session에는 세션을 판단하고 attribute에서는 세션의 정보를 저장을 합니다. 이때 정보를 byte[] 코드로 저장을 합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/e13491ca-5be7-41d6-94b0-5b194b5566ed/image.png" alt=""></p>
<ul>
<li>db에 저장하면 세션 정합성을 생각할 수 있다. redis같은 경우에는 TTL을 통해서 맞출 수 있지만 mysql은 어떻게 맞추나 고민을 하였다.</li>
<li>jdbc-session을 사용하면 특정 시간동안 db에 select, delete를 특정 시간을 통하여 맞추기 때문에 정합성을 맞출 수 있다.</li>
</ul>
<blockquote>
<p>그런데 byte[]로 attribute_bytes를 저장하는데 어떻게 가져올 수 있니?</p>
</blockquote>
<ul>
<li><p>byte로 저장하기 때문에 나는 이것을 가져오기 위해서는 <code>Serializable</code>를 통해서 객체를 저장하면 쉽게 가져올 수 있다. </p>
</li>
<li><p><code>Serializable</code>는 자바에서 지원되는 인터페이스 중 하나로 인스턴스를 직렬화할 수 있게 해준다. 이를 통해서 객체 상태를 바이트 스트림으로 변환하여 저장할 수 있다. </p>
</li>
<li><p>따라서 해당 클래스는 객체를 역, 직렬화를 할 수 있게 된다. 이를 통하여 byte[]를 추가적인 설정을 할 필요없이 객체를 가져올 수 있습니다.</p>
</li>
</ul>
<pre><code class="language-java">public class Member implements Serializable {
    private static final long serialVersionUID = 1L;

    private String username;
    private int age;

    // 생성자, 게터, 세터 등 필요한 메서드들

    // 예시로 멤버 객체를 생성하고 세션에 저장하는 메서드
    public static Member createMember(String username, int age) {
        Member member = new Member();
        member.setUsername(username);
        member.setAge(age);
        return member;
    }
}



@RestController
public class SessionController {

    @Autowired
    private SessionRepository&lt;?&gt; sessionRepository;

    @GetMapping(&quot;/saveMemberToSession&quot;)
    public void saveMemberToSession() {
        // 멤버 객체 생성
        Member member = Member.createMember(&quot;김무건&quot;, 28);

        // 세션에 멤버 객체 저장
        Session session = sessionRepository.findById(&quot;sessionId&quot;); // 세션 ID를 실제 세션의 ID로 대체해야 합니다.
        session.setAttribute(&quot;memberAttribute&quot;, member);
        sessionRepository.save(session);
    }

    @GetMapping(&quot;/getMemberFromSession&quot;)
    public Member getMemberFromSession() {
        // 세션에서 멤버 객체 가져오기
        Session session = sessionRepository.findById(&quot;sessionId&quot;); // 세션 ID를 실제 세션의 ID로 대체해야 합니다.
        return (Member) session.getAttribute(&quot;memberAttribute&quot;);
    }
}</code></pre>
<br/>


<h1 id="결론">결론</h1>
<hr>
<ul>
<li><p>다중 서버에서 세션의 불일치를 해결하는 방법에는 여러가지가 있다. 이 중에서 세션 스토리지에서 Redis를 통해서 높은 성능을 가질 수 있지만 현재 서비스의 상황에 따라서 다양한 방식을 선택할 수 있다.</p>
</li>
<li><p>RDBMS에서 세션을 저장하면 지속적으로 세션을 탐색, 삭제를 통하여 세션 정합성을 맞추며 데이터를 byte기반으로 저장하여 보안성을 가져갈 수 있다.</p>
</li>
</ul>
<br/>


<h1 id="참고">참고</h1>
<hr>
<p><a href="https://hyuntaeknote.tistory.com/6">https://hyuntaeknote.tistory.com/6</a></p>
<p><a href="https://jojoldu.tistory.com/170">https://jojoldu.tistory.com/170</a></p>
<p><a href="https://itkjspo56.tistory.com/296">https://itkjspo56.tistory.com/296</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL 8.0 아키텍처]]></title>
            <link>https://velog.io/@geon_km/MySQL-8.0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</link>
            <guid>https://velog.io/@geon_km/MySQL-8.0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</guid>
            <pubDate>Sun, 07 Jan 2024 13:33:24 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<hr>
<ul>
<li>이전에 Real MySQL을 보면서 학습을 하였지만 SQL 레벨업 책을 읽고 추가적으로 학습이 필요한 부분을 작성을 하였다.</li>
<li>우리가 아키텍처에 대해서 알아야 하는 이유는 쿼리를 입력을 하였을 때 관계형 디비에서 (mysql) 해당 동작을 어떻게 처리하고 성능을 가질지 이해하기 위한 중요한 부분이라고 생각하여 정리를 한다.</li>
</ul>
<h1 id="본론">본론</h1>
<hr>
<h2 id="1mysql-구조">1.MySQL 구조</h2>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/geon_km/post/97ec90cb-c5d2-42f9-8b9f-45b01f42e7c3/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/geon_km/post/681cae7b-3fb4-4335-aea0-e37d7bdcd838/image.png" alt=""></th>
</tr>
</thead>
</table>
<ul>
<li>MySQL 서버는 크게 엔진과 스토리지 엔진으로 구분한다. 여기서 스토리지 엔진은 버전에 따라서 default에 따라서 다르다.</li>
</ul>
<h3 id="1-1-mysql-엔진">1-1. MySQL 엔진</h3>
<ul>
<li>MySQL은 간단하게 요약하면 (1) 쿼리를 해석하고 파싱 (2) 옵티마이저를 통해 실행 계획을 세우고 (3) I/O를 통해 읽어온 데이터를 메모리에 처리한다.</li>
</ul>
<h3 id="1-1-1-sql-파서-전처리기">1-1-1. SQL 파서, 전처리기</h3>
<ul>
<li>파서는 SQL을 받은 것을 구문 분석을 한다. 사용자가 쉼표를 잊거나 from을 잊으면 에러가 발생한다. </li>
<li>전처리기는 테이블의 실존 여부, 권한 . 등런타임에만 판단 가능한 부분을 실제로 실행 가능한 쿼리인지 판단한다.</li>
</ul>
<blockquote>
<p>즉. 파서는 정적 단계의 구문 분석과 정적 단계에서 에러를 처리하고 전처리기는 런타임 환경에서 실제로 실행 가능한 쿼리인지 판단한다.</p>
</blockquote>
<h3 id="1-1-2-sql-옵티마이저">1-1-2. SQL 옵티마이저</h3>
<ul>
<li>SQL 쿼리를 효율적으로 사용할 수 있다록 실행 계획을 만든다. 쿼리 튜닝의 핵심으로 통계정보에 따라서 달라질 . 수있다.</li>
<li>이때 여러 개의 실행 계획을 만들어 이들의 비용을 연산하여 가장 낮은 비용을 가진 실행 계획을 선택한다.</li>
<li>이때 비용을 연산하기에 데이터베이스의 메타 데이터( 테이블 구조, 인덱스, 데이터 분포 )를 분석한다. 이때 이것을 카탈로그 매니저가 옵티마이저에서 중요한 정보를 관리한다.</li>
</ul>
<h3 id="1-1-2-캐시--버퍼">1-1-2. 캐시 &amp; 버퍼</h3>
<ul>
<li>MySQL에서는 쿼리 캐시를 지원하지 않는다. </li>
</ul>
<blockquote>
<p>테이블이 수정되면, 테이블과 관련된 cache들은 제거 되며 MySQL 5.6에서 기본적으로 비활성화 되었으며, 5.7.20에서 Deprecated 되었고 8.0 버전부터 제거된 기능이다.</p>
</blockquote>
<p>MySQL 공식 홈페이지에서 나온 성능</p>
<p>비슷한 쿼리가 조금 달라서 cache 되지 않는다면, Query Cache로 인한 오버헤드가 13% 증가 (이 수치는 최악의 경우일 때를 가정하므로 실제 환경에서는 더 낮음)
단일 테이블에서 단일 row를 조회할 때 Query Cache를 사용하지 않으면 238% 빠른 결과를 얻음</p>
<ul>
<li>다른 관계형 디비에서는 쿼리 캐시를 지원하지만 MySQL에서 쿼리 캐시를 지원하지 않고 인덱스 캐시, 키 캐싱을 지원한다.</li>
</ul>
<br/>
<br/>




<h3 id="1-2-스토리지-엔진">1-2. 스토리지 엔진</h3>
<ul>
<li>디스크, 메모리에 접근하여 데이터를 Read, Write만 . 할수있다. 버퍼풀, 리두 로그, 언두 로구 등의 메모리 상의 캐싱, 버퍼 역활을 한다.</li>
</ul>
<blockquote>
<p>mysql 5.6 / mysql 8.0 </p>
</blockquote>
<ul>
<li><p>기본적으로 5.6에서는 기본적으로 MyIsam로 설정이 되어져 있고 8.0에서는 InnoDB로 변경이 되었다.</p>
</li>
<li><p>간단하게 차이점을 살펴보면 다음과 같다.</p>
</li>
<li><p>(1) MyISAM : 처음에 기본 엔진으로 사용을 하였다. 키 캐싱이라는 장점이 있고 트랜잭션을 지원하지 않는다.</p>
</li>
<li><p>(2) InnoDB : 버퍼링, FK제약 조건, 충돌 복구, 트랜잭션을 지원하여 주로 InnoDB를 사용한다.</p>
</li>
<li><p>스토리지 엔진은 오직 핸들러 API를 통해서 통신할 수 있다. <code>MySQL엔진 &lt;-&gt; 스토리지 엔진 &lt;-&gt; 디스크</code></p>
</li>
<li><p>즉. MySQL 엔진에서 쿼리 파서, 전처리기를 통해 정적, 동적 에러를 감지하고 옵티마이저로 실행 계획을 만들어 스레드 캐시, 버퍼를 통해 InnoDB 버퍼 풀을 사용하여 인덱스를 메모리에 캐싱하여 핸들러 API를 통해 스토리지 엔진과 통신하며 디스크I/O 작업을 하면 디스크에 통신을 한다.</p>
</li>
</ul>
<h2 id="2-스토리지-엔진-아키텍처">2. 스토리지 엔진 아키텍처</h2>
<p><img src="https://velog.velcdn.com/images/geon_km/post/d9858b85-6d49-4643-9246-141da0d631fa/image.png" alt=""></p>
<h3 id="2-1-pk-fk-클러스터링-지원">2-1. PK, FK 클러스터링, 지원</h3>
<ul>
<li>PK는 기본적으로 프라이머리 키를 기준으로 인덱스가 생기고 이것을 기준으로 세컨 인덱스가 주소를 사용하고 데이터를 저장한다. 또한 외래 키를 지원하여 외래키에 대한 처리를 수행한다.</li>
</ul>
<h3 id="2-2-mvcc">2-2. MVCC</h3>
<ul>
<li>다중 버전 동시성 제어를 의미한다. 이것을 사용하는 가장 큰 이유는 트랜잭션 때문이다. MVCC를 통해 데이터베이스의 동시성을 제어한다. 이때 스냅샷을 이용하여 하나의 레코드에 대한 여러 버전을 관리한다.</li>
</ul>
<h3 id="락을-사용하지-않고-mvcc를-사용하는-이유">락을 사용하지 않고 MVCC를 사용하는 이유</h3>
<ul>
<li>락을 사용하면 가장 동시성을 쉽게 처리할 . 수있지만 동시 요청이 많이 들어오면 성능에 문제가 간다. 이를 해결하기 위해서 MYSQL에서는 MVCC를 사용하여 스냅샷으로 언두 로그를 활용한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/8e884d3f-c6e2-4e38-ae49-d63e04fa2307/image.png" alt=""></p>
<ul>
<li>위 사진을 보면 커밋을 하면 언두 로그에 쌓여 커밋이 되기 이전에 언두 로그를 통해 데이터를 . 알 수 있다. 트랜잭션과 격리 수준을 보장을 한다. </li>
<li>하지만 여기서 주의할 부분은 커밋하면 현재 상태를 유지하고 백업하면 롤백하기 때문에 (1) 대량의 데이터 변경 또는 삭제, 트랜잭션이 오래 유지하는 경우에는 주의를 해야된다.</li>
</ul>
<blockquote>
<p><strong>즉. 트랜잭션은 가능한 짧게 유지하는 것이 중요하다.</strong></p>
</blockquote>
<h3 id="2-3-버퍼풀">2-3. 버퍼풀</h3>
<ul>
<li>버퍼풀은 디스크의 데이터나 인덱스 정보를 메모리에 캐시해두어 디스크 I/O가 발생하면 성능적으로 이점을 가질 수 있다. </li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/668e98c8-9342-4c4f-a862-9dab08c30e2d/image.png" alt=""></p>
<ul>
<li>MySQL은 기본적으로 Random I/O를 처리를 합니다. 만약에 Insert, Update, Delete 작업을 수행하게 되면 디스크에 많이 접근해서 작업을 처리해야 되는데 버퍼풀에 데이터를 먼저 바꾸고 그것을 한번에 처리하면 디스크 I/O가 줄어들어 성능적인 이점을 가져갈 수 있다. 이것을 지연로딩을 의미한다.</li>
</ul>
<h3 id="버퍼-풀-크기-설정하기">버퍼 풀 크기 설정하기</h3>
<ul>
<li>일반적으로 크기는 <code>innodb_buffer_pool_size</code> 변수를 통해서 설정한다. 일반적으로 물리 메모리의 50% 잡은 후에 조금씩 늘린다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/151fbdd6-2c93-443b-bfd7-d403a2718ccc/image.png" alt=""></p>
<ul>
<li>핵심은 버퍼풀의 구조는 자주 참조되는 페이지를 오래 버퍼 풀에 유지시켜 캐시 성능을 향상 시키는 것이다.
간단 한 원리는 참조되는 페이지는 MRU영역으로 보내 계속 살아남게 하고 자주 참조 되지 않으면 LRU영역으로 밀려나 제거될 것이다. </li>
</ul>
<h3 id="버퍼-풀을-크게-잡으면-생기는-오버헤드">버퍼 풀을 크게 잡으면 생기는 오버헤드</h3>
<ol>
<li><p>메모리 부족 : 시스템의 다른 프로세스나 운영 체제가 필요로 하는 메모리가 부족해질 수 있습니다. 이는 시스템 전체의 성능 저하로 이어질 수 있으며, 최악의 경우 시스템의 안정성에 영향을 줄 수도 있습니다.</p>
</li>
<li><p>스왑 활동 증가: 메모리가 부족하게 되면, 운영 체제는 디스크의 스왑 공간을 사용하게 됩니다. 스왑 활동이 증가하면 시스템의 전체 성능이 저하될 수 있습니다.</p>
</li>
<li><p>캐시 관리 오버헤드: 버퍼 풀이 너무 크면, MySQL은 버퍼 풀 내의 데이터를 관리하는 데 더 많은 CPU 자원을 소모하게 됩니다. 이는 특히 버퍼 풀 내의 데이터가 자주 변경되는 환경에서 두드러질 수 있습니다.</p>
</li>
</ol>
<h3 id="2-4-리두로그">2-4. 리두로그</h3>
<p>버퍼 풀에 commit된 데이터는 아직 디스크에 반영되지 않았으므로 서버가 비정상 종료되면 데이터가 유실 될 것이다. 이러한 부분을 보완하기 위해서 데이터가 디스크에서 처음 읽은 (클린 페이지) 상태에서 변경된 부분만 따로 리두로그라는 부분에 기록한다. 리두로그도 주기적으로 디스크에 flush한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Modern JAVA 8 람다 (feat. Effectively Final)]]></title>
            <link>https://velog.io/@geon_km/%EC%9E%90%EB%B0%94-%EB%9E%8C%EB%8B%A4</link>
            <guid>https://velog.io/@geon_km/%EC%9E%90%EB%B0%94-%EB%9E%8C%EB%8B%A4</guid>
            <pubDate>Sun, 07 Jan 2024 10:53:00 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/geon_km/post/d24a4e5a-1a5e-4729-a7bb-dfd89d84f936/image.png" alt=""></p>
<h2 id="😶🌫️함수형-프로그래밍">😶‍🌫️함수형 프로그래밍</h2>
<hr>
<p>Java는 객체지향 언어이기 때문에 기본적으로 함수형 프로그래밍이 불가능하다. 하지만 JDK8부터 Stream API와 람다식, 함수형 인터페이스 등을 지원하면서 Java를 이용해 함수형으로 프로그래밍할 수 있는 API 들을 제공해주고 있다.</p>
<p><strong>First Class Citizon</strong></p>
<ul>
<li><strong>First Class Citizon 은 아래의 속성들을 모주 만족해야 합니다.</strong></li>
</ul>
<p>• 변수에 값을 할당할 수 있어야 합니다.</p>
<p>• 함수의 파라미터로 넘겨줄 수 있어야 합니다.</p>
<p>• 함수의 반환값이 될 수 있어야 합니다.</p>
<p><a href="https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html">https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html</a></p>
<p> [Function (Java Platform SE 8 )</p>
<p>docs.oracle.com](<a href="https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html">https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html</a>)</p>
<h2 id="functionalinterface">@FunctionalInterface</h2>
<hr>
<h3 id="함수형-인터페이스">함수형 인터페이스</h3>
<ul>
<li><strong>추상 메소드가 딱 하나만 존재하는 인터페이스 이다.</strong></li>
</ul>
<blockquote>
<p>💡 public abstract가 단 하나만 있으면 가능하며 다른 형태로 정의가 되어져 있는 것은 상관이 없다.</p>
</blockquote>
<pre><code class="language-java">@FunctionalInterface
public interface PracticeLambda {
    void practice(); //public abstract 생략

    static void practice2() {
        System.out.println(&quot;연습1&quot;);
    }

    default void practice3() {
        System.out.println(&quot;연습2&quot;);
    }
}</code></pre>
<blockquote>
<p>만약에 추상 메소드가 2개 이상이면 @functionalInterface에서 컴파일 오류가 나온다.</p>
</blockquote>
<h2 id="자바에서-제공하는-함수형-인터페이스">자바에서 제공하는 함수형 인터페이스</h2>
<ul>
<li>Function&lt;T,R&gt; , UnaryOperator<T><ul>
<li>두개의 인자를 받아서 하나의 결과를 Return한다.</li>
<li>여기서 두개의 인자의 타입은 T,R 서로 다르거나 같아도 상관이 없다.</li>
<li>만약에 두개의 인자가 같은 타입이면 UnaryOperator<T>를 사용</li>
</ul>
</li>
</ul>
<p>🦖현재의 이 코드는 Function 인터페이스를 익명객체 스타일로 구현을 했다.</p>
<pre><code class="language-java">public class PracticeLamdba {
    public static void main(String[] args) {
        Function&lt;Integer, Integer&gt; pricaticeTest = new Function&lt;Integer, Integer&gt;() {
            @Override
            public Integer apply(Integer integer) {
                return integer+10;
            }
        };

        System.out.println(pricaticeTest.apply(3));
    }
}</code></pre>
<ul>
<li>값을 받아와서 apply를 오버라이드 해서 +10을 해주는 함수를 만들었다.</li>
</ul>
<p>🦖이러한 익명의 느낌을 람다로 표현이 가능하다.</p>
<pre><code class="language-java">public class PracticeLamdba {
    public static void main(String[] args) {
        Function&lt;Integer, Integer&gt; pricaticeTest = integer -&gt; integer+10;
        System.out.println(pricaticeTest.apply(3));
    }
}</code></pre>
<ul>
<li>코드가 엄청 간결하고 가독성이 높아졌다.</li>
</ul>
<h2 id="functiontr-→-apply">Function&lt;T,R&gt; → apply()</h2>
<p>→T는 입력하는 타입을 의미하며 R은 return 되는 값의 타입을 의미한다.</p>
<p>여기서 만약에 Funciotn 함수가 여러개 있을 때 Function의 다양한 기능을</p>
<p>사용할 수 있다.</p>
<ol>
<li>compose</li>
</ol>
<ul>
<li>간단하게 코드를 보여주고 설명을 하겠다.</li>
</ul>
<pre><code class="language-java">  public class PracticeLamdba {
      public static void main(String[] args) {
          Function&lt;Integer, Integer&gt; plus = (number) -&gt; number+10;

          Function&lt;Integer , Integer&gt; multiple = number2 -&gt;number2*2;

          int result=plus.compose(multiple).apply(10);
          System.out.println(result);
  }
</code></pre>
<blockquote>
<p>더하기 기능과 곱하기 기능을 만들었고 copose를 사용을 했다.
이때 result의 값은 어떻게 나올까? —&gt; 결과는 30이 나오게 된다.</p>
</blockquote>
<blockquote>
<p>Compose란
()의 기능을 먼저 연산을 하고 그 뒤에 첫번 째로 오는 기능을 연산을 한다.
multiple → 결과 → 결과 &amp; plus 라고 생각을 하면 좋다.</p>
</blockquote>
<pre><code class="language-java">public class PracticeLamdba {
    public static void main(String[] args) {
        Function&lt;Integer, Integer&gt; plus = (number) -&gt; number+10;
        Function&lt;Integer , Integer&gt; multiple = number2 -&gt;number2*2;
        int result=plus.andThen(multiple).apply(10);
        System.out.println(result);
    }
}</code></pre>
<p>andThen은 compose와 반대로 먼저오는 기능을 연산하고 그 뒤에 () 기능을 연산을 하는 것을 의미한다.</p>
<p>즉. plus → 결과 → multiple을 하며 결과는 40이 나오게 된다.</p>
<ul>
<li>BiFuntion&lt;T,U,R&gt;<ul>
<li>BiFunction은 입력값이 2개를 받고 두개의 연산에 대한 결과값 R로</li>
<li>반환하는 기능을 가진다.</li>
<li>기본적인 기능은 Function&lt;T,R&gt;과 비슷하며 입력값이 2개라는 차이만 있다.</li>
</ul>
</li>
</ul>
<h3 id="익명">익명</h3>
<pre><code class="language-java">public class PracticeLamdba {
    public static void main(String[] args) {
        BiFunction&lt;Integer,Integer,Integer&gt;biFunction = new BiFunction&lt;Integer, Integer, Integer&gt;() {
            @Override
            public Integer apply(Integer integer, Integer integer2) {
                return integer+integer2;
            }
        };
        int result=biFunction.apply(10 , 20);
        System.out.println(result);
    }
}</code></pre>
<h3 id="람다">람다</h3>
<pre><code class="language-java">public class PracticeLamdba {
    public static void main(String[] args) {
        BiFunction&lt;Integer,Integer,Integer&gt;biFunction = (integer, integer2) -&gt; integer+integer2;
        int result=biFunction.apply(10 , 20);
        System.out.println(result);
    }
}</code></pre>
<h2 id="functiontr--bifunctiontur-같은-타입">Function&lt;T,R&gt; &amp; BiFunction&lt;T,U,R&gt; 같은 타입</h2>
<aside> 💡 만약에 T ,U ,R이 다 똑같은 Integer 타입이면 다 적어야 되는가?

</aside>

<p>만약에 타입이 같으면 Function은 UnaryOperator<T>를 사용하면 된다.</p>
<p>그러면 입력 , 출력값의 타입이 모두 T로 정해진다.</p>
<p>BiFunction&lt;T,U,R&gt;은 입력 출력 모두 같으면 BinaryOperator<T>를 사용</p>
<pre><code class="language-java">public class PracticeLamdba {
    public static void main(String[] args) {
        UnaryOperator&lt;Integer&gt;unaryOperator = (n) -&gt; n+10;
        System.out.println(unaryOperator.apply(10));

        BinaryOperator&lt;Integer&gt;binaryOperator=(a,b)-&gt;a+10+b;
        System.out.println(binaryOperator.apply(10,20));
    }
}</code></pre>
<ul>
<li>Consumer<T> - apply()</li>
</ul>
<blockquote>
<p>T타입을 입력 받아서 아무값도 리턴하지 않는 함수 인터페이스를 의미</p>
</blockquote>
<h3 id="익명-1">익명</h3>
<pre><code class="language-java">public class PracticeLamdba {
    public static void main(String[] args) {
        Consumer&lt;String&gt;consumer = new Consumer&lt;String&gt;() {
            @Override
            public void accept(String s) {
                System.out.println(&quot;내 이름은 &quot;+s +&quot;입니다.&quot;);
            }
        };
        consumer.accept(&quot;김무건&quot;);
    }
}</code></pre>
<h3 id="람다-1">람다</h3>
<pre><code class="language-java">public class PracticeLamdba {
    public static void main(String[] args) {
        Consumer&lt;String&gt;consumer = (name) -&gt; System.out.println(&quot;내 이름은&quot;+name+&quot;입니다.&quot;);
        consumer.accept(&quot;김무건&quot;);
    }
}</code></pre>
<ul>
<li>Suppiler<T><ul>
<li>T 타입의 값을 제공하는 함수 인터페이스<ul>
<li>인자가 필요 없다.</li>
<li>내가 어떤 값을 받을지 결정</li>
<li>T get()</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="익명-2">익명</h3>
<pre><code class="language-java">public class PracticeLamdba {
    public static void main(String[] args) {
        Supplier&lt;String&gt; supplier = new Supplier&lt;String&gt;() {
            @Override
            public String get() {
                return &quot;김무건&quot;;
            }
        };
        System.out.println(&quot;내 이름은 &quot;+supplier.get());
    }
}</code></pre>
<h3 id="람다-2">람다</h3>
<pre><code class="language-java">public class PracticeLamdba {
    public static void main(String[] args) {
        Supplier&lt;String&gt; supplier = () -&gt; &quot;김무건&quot;;
        System.out.println(&quot;내 이름은 &quot;+supplier.get());
    }
}</code></pre>
<ul>
<li>Predicate<T><ul>
<li>T타입을 받아서 Boolean을 리턴하는 함수 인터페이스</li>
<li>함수 조합용 메소드<ul>
<li>and</li>
<li>or</li>
<li>negate()</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="익명-3">익명</h3>
<pre><code class="language-java">public class PracticeLamdba {
    public static void main(String[] args) {
      Predicate&lt;Integer&gt;predicate = new Predicate&lt;Integer&gt;() {
          @Override
          public boolean test(Integer integer) {
              return integer%2==0;
          }
      };
        System.out.println(predicate.test(10));
    }
}</code></pre>
<h3 id="람다-3">람다</h3>
<pre><code class="language-java">public class PracticeLamdba {
    public static void main(String[] args) {
      Predicate&lt;Integer&gt;predicate = integer -&gt; integer%2==0;
        System.out.println(predicate.test(10));
    }
}</code></pre>
<blockquote>
<p>만약에 다양한 조건이 있다고 생각하면 and or를 이용하면 된다.</p>
</blockquote>
<p>예를 들어서 짝수이면서 값이 4인 결과를 받고 싶다고 조건을 만들면</p>
<ol>
<li>짝수</li>
<li>값이 4</li>
</ol>
<p>이 두개의 조건을 and연산을 하면 되는데</p>
<h3 id="and">and</h3>
<pre><code class="language-java">public class PracticeLamdba {
    public static void main(String[] args) {
      Predicate&lt;Integer&gt;integerPredicate = integer -&gt; integer%2==0;
      Predicate&lt;Integer&gt;predicate = num -&gt;num==4;

      if(integerPredicate.and(predicate).test(4)){
          System.out.println(&quot;참&quot;);
      }else System.out.println(&quot;거짓&quot;);

    }
}</code></pre>
<h3 id="or">or</h3>
<pre><code class="language-java">public class PracticeLamdba {
    public static void main(String[] args) {
      Predicate&lt;Integer&gt;integerPredicate = integer -&gt; integer%2==0;
      Predicate&lt;Integer&gt;predicate = num -&gt;num==4;

      if(integerPredicate.or(predicate).test(4)){
          System.out.println(&quot;참&quot;);
      }else System.out.println(&quot;거짓&quot;);

    }
}</code></pre>
<h3 id="negate">negate()</h3>
<pre><code class="language-java">public class PracticeLamdba {
    public static void main(String[] args) {
      Predicate&lt;Integer&gt;integerPredicate = integer -&gt; integer%2==0;
      Predicate&lt;Integer&gt;predicate = num -&gt;num==4;
        System.out.println(predicate.negate().test(4));
    }
}</code></pre>
<br/>

<h2 id="effectively-final"><strong>Effectively Final</strong></h2>
<p>Java8에서 <strong>final</strong>이 붙지 않은 변수의 값이 변경되지 않는다면, 그 변수를 Effectively final이라고 합니다.</p>
<p>람다에서는 사용할 수 있는 로컬 변수는 Effectively Final만 사용이 가능하다.</p>
<pre><code class="language-java">public class EffectivelyFinal {
    public static void main(String[] args) {
        new Pint().go();
    }
}
class Pint {
    void go() {
        int baseAnInt = 111;
        Function&lt;Integer, Integer&gt; function = (num) -&gt; num + baseAnInt;
        System.out.println(baseAnInt+&quot;baseAnInt&quot;);
        System.out.println(function.apply(10)+&quot;function&quot;);
    }
}</code></pre>
<p>위에 코드를 보면 baseAnInt는 Effectively Final이다.</p>
<p>자바에서는 프로그램이 값이 변경하지 않는 수를 final로 추측을 할 수 있다.</p>
<p>여기서 람다에서 중요한 부분은 람다식에서 <strong>참조하는 로컬 변수의 값은 변경을 할 수 없다.</strong></p>
<p>이 부분은 <strong>스코프</strong>의 내용을 이해하면 더 이해하기 쉽다. ==&gt; 이 부분은 뒤에서 더 설명</p>
<p>현재 간단하게 설명을 하면 GO()메소드의 스코프와 람다의 스코프가 같다. 같은 스코프에서 동일한 변수를 지정할 수 없고 이러한 부분을 밑에 코드를 보면 알 수 있다.</p>
<blockquote>
<p><strong>스코프</strong>란  
먼저 스코프란 변수를 사용할 수 있는 범위</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/geon_km/post/d402fdd1-d9ae-4867-bf0b-9e2833416f71/image.png" alt=""></p>
<p>baseAnInt를 선언하고 람다식에서 사용하니 <strong>이미 스코프에서 정의</strong>가 되어져 있다고 나온다.</p>
<p>이것을 통해서 람다는 go메소드랑 동일한 스코프를 가지고 있다고 이해할 수 있다.</p>
<h2 id="람다-effectively-final"><strong>람다? Effectively Final?</strong></h2>
<p>람다식에서 사용되는 변수는 해당 변수를 의미하는 것이 아닌 복사본이다.</p>
<p>근본적인 이유는 <strong>생명 주기</strong>가 다르기 때문이다.</p>
<h4 id="1-로컬-변수를-생각을-해보면-메소드가-실행이-되고-끝나면-생명주기는-끝이-난다">1. 로컬 변수를 생각을 해보면 메소드가 실행이 되고 끝나면 생명주기는 끝이 난다.</h4>
<p>하지만 람다는 <strong>고차 함수</strong>여서 람다 자체를 인자로 받고 리턴을 할 수 있기 때문에 람다의 생명주기는 여전히 실행을 하고 있을 수 있다.</p>
<p><strong>즉. 로컬 변수의 생명 주기가 끝나도 람다는 로컬 변수의 값을 복사하여 가지고 있어야 한다.</strong></p>
<p><a href="https://incheol-jung.gitbook.io/docs/study/kotlin-in-action/8">https://incheol-jung.gitbook.io/docs/study/kotlin-in-action/8</a></p>
<p> [8장 고차 함수: 파라미터와 반환 값으로 람다 사용 - Incheol&#39;s TECH BLOG</p>
<p>코틀린 표준 라이브러리의 컬렉션 함수는 대부분 람다를 인자로 받는다. filter와 map은 인라인 함수다. 따라서 그 두 함수의 본문은 인라이닝되며, 추가 객체나 클래스 생성은 없다. 하지만 이 코</p>
<p>incheol-jung.gitbook.io](<a href="https://incheol-jung.gitbook.io/docs/study/kotlin-in-action/8">https://incheol-jung.gitbook.io/docs/study/kotlin-in-action/8</a>)</p>
<h4 id="2-로컬-변수와-람다의-thread를-이해하자">2. 로컬 변수와 람다의 Thread를 이해하자</h4>
<ul>
<li><p><strong>로컬 변수</strong></p>
<ul>
<li>JVM에서 <strong>Stack</strong>에 저장이 된다.</li>
<li>Stack에 따른 Thread가 배정이 되고 <strong>Thread 종료는 로컬 변수의 끝</strong>을 의미한다</li>
</ul>
</li>
<li><p><strong>람다의 쓰레드</strong></p>
<ul>
<li>람다는 <strong>별도의 Thread</strong>를 가진다.</li>
<li>만약에 Stack에 직접 접근이 가능하여 참조하고 있다고 한다면 이것은 멀티 쓰레드에 위험</li>
<li>람다가 참조한 변수의 생명 주기는 람다가 있으면 보장이 된다.</li>
</ul>
</li>
</ul>
<h2 id="스코프"><strong>스코프</strong></h2>
<p><strong><a href="https://wakestand.tistory.com/179">https://wakestand.tistory.com/179</a></strong></p>
<p> [자바 변수의 스코프가 뭔말?</p>
<p>면접 시 많이 물어보는 것이 변수의 스코프인데 스코프가 뭔 말인지 감이 안와서 어려울 수 있는데 막상 보면 단순하다 먼저 스코프란 변수를 사용할 수 있는 범위를 얘기하는데 {} 안에서 변수</p>
<p>wakestand.tistory.com](<a href="https://wakestand.tistory.com/179">https://wakestand.tistory.com/179</a>)</p>
<p>기본적인 스코프의 내용은 링크를 통해서 확인이 가능하다.</p>
<p>이 페이지에서 설명하는 스코프는 람다에서 람다 , 로컬 클래스 , 익명 클래스에 대한 스코프를 비교하고</p>
<p>람다에 대한 특징을 알아보기 위해 작성을 하고 있다.</p>
<pre><code class="language-java">class Pint {
    private int baseAnInt = 111;

    class LocalClass{
        int baseAnInt = 1;
        void localClass(){
            System.out.println(baseAnInt);//1
        }
    }

    Consumer&lt;Integer&gt;consumer = new Consumer&lt;Integer&gt;() {
        int baseAnInt=2;
        @Override
        public void accept(Integer integer) {
            System.out.println(baseAnInt);//2
        }
    };

    void go() {
        Function&lt;Integer, Integer&gt; function = (baseAnInt) -&gt; baseAnInt + baseAnInt;//111
    }
}</code></pre>
<p>로컬 클래스와 , 익명 클래스는 각각 baseAnInt의 값을 변경을 할 수 있지만 람다식은 변경이 불가능 하다.</p>
<p>이것을 통해 위에 2개와 람다식은 서로 다른 스코프를 통해 동작을 한다는 것을 알 수 있다.</p>
<p><strong>이것에 대한 자세한 내용은 밑에 블로그를 참고</strong></p>
<p><strong><a href="https://tjdtls690.github.io/studycontents/java/2022-10-24-lambda_anonymous_local_class_difference/">https://tjdtls690.github.io/studycontents/java/2022-10-24-lambda_anonymous_local_class_difference/</a></strong></p>
<p> [[자바, Java] 람다 (Lambda) - 쉐도잉 (Shadowing)</p>
<p>.</p>
<p>tjdtls690.github.io](<a href="https://tjdtls690.github.io/studycontents/java/2022-10-24-lambda_anonymous_local_class_difference/">https://tjdtls690.github.io/studycontents/java/2022-10-24-lambda_anonymous_local_class_difference/</a>)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[N+1 문제 다양한 해결법]]></title>
            <link>https://velog.io/@geon_km/N1-%EB%AC%B8%EC%A0%9C-%EB%8B%A4%EC%96%91%ED%95%9C-%ED%95%B4%EA%B2%B0%EB%B2%95</link>
            <guid>https://velog.io/@geon_km/N1-%EB%AC%B8%EC%A0%9C-%EB%8B%A4%EC%96%91%ED%95%9C-%ED%95%B4%EA%B2%B0%EB%B2%95</guid>
            <pubDate>Sun, 26 Nov 2023 12:50:39 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<hr>
<ul>
<li>JPA를 학습하면 무조건 듣는 키워드는 N+1 이다. 보통 블로그에서 소개하는 방식은 fetch join을 통하여 문제를 해결한다고 이야기한다.</li>
<li>물론 틀린 방식은 아니다. 하지만 실제 프로젝트를 만들면서 N+1 문제를 많이 만나보면서 N+1을 처리하는 방식은 여러가지가 있다. </li>
<li>상황에 따라서 N+1 문제를 처리하는 방식을 적절하게 해결하는게 성능 저하의 문제에 대응할 수 있다고 생각했다.</li>
</ul>
<blockquote>
<p>글을 시작하기 이전에 간단하게 정리하면 
1:1 연관관계 :  Fetch join
Collection 연관관계 : default_batch_fetch_size
N개의 컬렉션을 fetch join을 하면 MultipleBagFetchException이 발생한다.
특정 컬럼을 조회할 경우에 join을 하고 Projection을 Dto로 매핑을 한다.</p>
</blockquote>
<h1 id="본론">본론</h1>
<hr>
<h2 id="1-왜-n1-문제가-발생을-하나요">1. 왜 N+1 문제가 발생을 하나요?</h2>
<ul>
<li>JPA를 처음 학습하면 김영한님 강의를 보면 처음에 나오는건 관계형 데이터베이스와 객체지향 언어간의 패러다임 차이를 이야기한다. JPA에서 연관관계를 맺으면 레퍼런스를 통하여 관계가 있는 객체에 접근할 수 있다. 하지만 관계형 데이터베이스는 Select를 통해야지만 접근을 한다.</li>
</ul>
<h3 id="1-1간단한-엔티티-코드">1-1.간단한 엔티티 코드</h3>
<pre><code class="language-java">@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = &quot;MEMBER&quot;, uniqueConstraints = {
        @UniqueConstraint(name = &quot;MEMBER_EMAIL&quot;, columnNames = {&quot;email&quot;}),
})
public class Member extends BaseEntity{
    @OneToMany(mappedBy = &quot;member&quot;,fetch = FetchType.EAGER)
    private List&lt;Request&gt; requests = new ArrayList&lt;&gt;();

}
------------------------------------------------------------------------------------
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class Request {
    @ManyToOne
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;
}</code></pre>
<ul>
<li>Fetch Type은 기본적으로 ToMany에서는 Lazy, ToOne은 Eager로 설정이 되어져 있습니다.</li>
<li>일단 현재 엔티티를 살펴보면 회원(1) : 게시판(N)으로 되어져 있습니다. 그러면 이제 N+1을 발생을 시켜서 문제를 살펴보겠습니다.</li>
</ul>
<br/>

<h2 id="2-n1-문제">2. N+1 문제</h2>
<h3 id="2-1-eager에서-n1-문제">2-1. Eager에서 N+1 문제</h3>
<pre><code class="language-java">    @BeforeEach
    void setUp(){
        List&lt;Request&gt; requestArrayList = new ArrayList&lt;&gt;();
        IntStream.range(0, 10)
                .mapToObj(i -&gt; new Request(&quot;title&quot; + i))
                .forEach(requestArrayList::add);

        requestRepository.saveAll(requestArrayList);

        List&lt;Member&gt; memberArrayList = new ArrayList&lt;&gt;();

        IntStream.range(0, 10)
                .mapToObj(i -&gt; Member.builder()
                        .name(&quot;member&quot; + i)
                        .requests(requestArrayList)
                        .build())
                .forEach(memberArrayList::add);
        memberRepository.saveAll(memberArrayList);

        entityManager.clear();
    }

    @Test
    @Transactional
    public void fix() throws Exception {
        System.out.println(&quot;===============================================================&quot;);
        List&lt;Member&gt; memberList = memberRepository.findAll();
        System.out.println(&quot;===============================================================&quot;);

        assertThat(memberList.size()).isNull();
    }</code></pre>
<ul>
<li><p>간단하게 테스트 코드를 작성을 했다. 위에 @BeforeEach를 통하여 기본적으로 데이터를 세팅하고 10개의 회원을 전체를 조회하는 코드를 만들었다. 이것을 실행하면 다음과 같이 발생한다.</p>
<h1 id="sql">```sql</h1>
</li>
<li><p>회원 조회
Hibernate: select m1_0.id,m1_0.name from member_table m1_0</p>
</li>
<li><p>Request 조회
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?</p>
<h1 id="hibernate-select-r1_0member_idr1_0idr1_0title-from-request-r1_0-where-r1_0member_id">Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?</h1>
<pre><code></code></pre></li>
<li><p>바로 N+1 문제가 발생했다. 처음 회원을 조회하고 관련 Request를 10번 조회하는 쿼리가 나간다.</p>
<h3 id="2-2-lazy에서-n1문제">2-2. Lazy에서 N+1문제</h3>
</li>
<li><p>회원의 Fetch Type을 Lazy로 변경하고 똑같이 테스트를 진행하면 한번만 발생한다.</p>
<pre><code class="language-java">===============================================================
Hibernate: select m1_0.id,m1_0.name from member_table m1_0
===============================================================</code></pre>
</li>
<li><p>그러면 N+1 문제는 해결이 되었는가? 아직은 아니다. 다른 테스트 코드를 실행을 해보겠다.</p>
<pre><code class="language-java">  @Test
  @Transactional
  public void fix() throws Exception {
      System.out.println(&quot;===============================================================&quot;);
      memberRepository.findAll().stream().flatMap(member -&gt; member.getRequests().stream()
                      .map(Request::getTitle))
              .collect(Collectors.toList());
      System.out.println(&quot;===============================================================&quot;);

      assertThat(memberRepository.findAll().size()).isNull();
  }</code></pre>
<pre><code class="language-sql">===============================================================
Hibernate: select m1_0.id,m1_0.name from member_table m1_0
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
===============================================================
</code></pre>
</li>
</ul>
<pre><code>
&gt; 정리 
Eager를 사용하든 Lazy를 사용하든 결국 동일하게 발생한다. Lazy를 사용하면 단지 프록시 객체로 가져오기 때문에 N+1이 데이터 사용하는 시점으로 미루는 것이지 해결하는 것은 아니다.

&gt; N+1 발생하는 이유
jpaRepository에 정의한 인터페이스 메서드를 실행하면 JPA는 메서드 이름을 분석해서 JPQL을 생성하여 실행하게 된다. JPQL은 SQL을 추상화한 객체지향 쿼리 언어로서 특정 SQL에 종속되지 않고 엔티티 객체와 필드 이름을 가지고 쿼리를 한다. 


&lt;br/&gt;

## 3. N+1 문제 해결

### 3-1. Fetch Join + Lazy Loading
- 보통 JPA를 처음 사용하면 N+1 문제를 해결하는 방식을 Fetch Join을 사용해서 해결한다. 하지만 만능은 아니다. 일단 간단한 코드를 보고 장단점에 대해서 설명을 하겠다.

- 일단 기존의 코드를 기반으로 문제를 해결하겠다. 위에 Lazy를 처리하여도 N+1 문제가 발생을 하였다. 이걸 Lazy Loading, Fetch Join을 통하여 해결하면 다음과 같다.

&lt;br/&gt;

- Repository
```java
    @Query(&quot;select distinct m from Member m join fetch m.requests&quot;)
    List&lt;Member&gt;findAllRelatedRequest();</code></pre><ul>
<li><p>테스트 코드</p>
<pre><code class="language-java">  @Test
  @Transactional
  public void fix() throws Exception {
      System.out.println(&quot;===============================================================&quot;);
      memberRepository.findAllRelatedRequest().stream().flatMap(member -&gt; member.getRequests().stream()
                      .map(Request::getTitle))
              .collect(Collectors.toList());
      System.out.println(&quot;===============================================================&quot;);

      assertThat(memberRepository.findAll().size()).isNull();
  }</code></pre>
</li>
<li><p>기존에 findAll에서 새롭게 만든 findAllRelatedRequest로 변경을 하였다. 이렇게 변경을 하니 기존에 N+1 문제가 발생한 쿼리에서 한방 쿼리로 변경이 되었다.</p>
</li>
</ul>
<pre><code class="language-sql">===============================================================
Hibernate: select distinct m1_0.id,m1_0.name,r1_0.member_id,r1_0.id,r1_0.title 
from member_table m1_0 
join request r1_0 on m1_0.id=r1_0.member_id
===============================================================</code></pre>
<br/>

<h3 id="fetch-join에-대한-한계">Fetch Join에 대한 한계</h3>
<h3 id="1-fetch-join과-일반-join의-차이--패러다임-불일치-줄여줌-">1. Fetch join과 일반 Join의 차이 ( 패러다임 불일치 줄여줌 )</h3>
<p>N+1 문제를 fetch join으로 해결할 수 없다. 일단 fetch join에 대해서 설명하자면 우리가 알고 있는 join과 차이가 있다.</p>
<ul>
<li><p>fetch join은 orm에서 사용하며 디비 스키마를 엔티티로 자동 변환 &gt; 영속성 컨텍스트에 영속화를 해준다.</p>
</li>
<li><p>이러한 특징 덕분에 fetch join을 해서 가져온 연관 관계가 있는 1차 캐시에 저장이 되고 다시 조회를 하여도 쿼리를 수행하지 않는다.</p>
</li>
<li><p>하지만 일반 join쿼리는 단순히 sql에서 데이터를 조회하는 개념이기 때문에 영속성 컨텍스트와 관련이 없다. 이것이 패러다임의 차이이며 fetch join은 이를 줄여주는 역활을 한다.</p>
</li>
</ul>
<br/>

<h3 id="2-collection-연관관계-fetch-join시-데이터-뻥튀기-distinct-추가">2. Collection 연관관계 Fetch Join시 데이터 뻥튀기 (Distinct 추가)</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/b7c9c31c-93a6-48b9-8310-75ac5acf6e3a/image.png" alt=""></p>
<ul>
<li>위에 그림을 보면 1:n 관계가 되어져 있다. 위에 사진처럼 데이터를 중복 되어 존재함입니다. 이 때문에 fetch join을 하면 n개 만큼 관계가 생성된다.</li>
<li>이 때문에 distinct절을 활용을 해야됩니다. </li>
<li>그러면 여거서 고민할 부분이 있다.</li>
</ul>
<blockquote>
<p>SQL  Distinct / JPQL Distinct 차이</p>
</blockquote>
<ul>
<li>SQL에서 Distinct절은 DB에서 수행되며 JOIN 발생한 데이터 형태에서 각 ROW를 비교하여 다른 경우만 남긴다. 하지만 JPA의 Distinct는 엔티티객체에 대해서 Distinct를 수행을 합니다. </li>
</ul>
<br/>

<h3 id="3-n개-컬렉션-fetch-join시-multiplebagfetchexception">3. N개 컬렉션 Fetch Join시 MultipleBagFetchException</h3>
<ul>
<li><p>처음에 컬렉션을 2개를 FETCH JOIN을 하면 이 오류가 발생한다.</p>
</li>
<li><p>즉 하나만 FETCH JOIN을 해야합니다. </p>
</li>
<li><p>테스트를 위해서 엔티티를 하나 추가를 시키고 오류를 살펴보겠다.</p>
</li>
</ul>
<pre><code class="language-java">    @OneToMany(mappedBy = &quot;member&quot;,fetch = FetchType.LAZY)
    private List&lt;Request&gt; requests = new ArrayList&lt;&gt;();

    @OneToMany(mappedBy = &quot;member&quot;,fetch = FetchType.LAZY)
    private List&lt;Notice&gt; notices = new ArrayList&lt;&gt;();

    @Query(&quot;select distinct m from Member m join fetch m.requests join fetch m.notices&quot;)
    List&lt;Member&gt;findAllRelatedRequest();</code></pre>
<ul>
<li><p>이렇게 관계를 맺고 컬렉션 2개를 fetch join을 하게되면 다음과 같은 오류가 발생한다.</p>
<pre><code class="language-java">org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.example.localcache.domain.Member.notices, com.example.localcache.domain.Member.requests]

  at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:371)
  at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:246)</code></pre>
</li>
<li><p>그러면 이렇게 컬렉션을 어떻게 문제를 해결을 해야되는가?? 이것은 batch size로 해결하면 된다. 밑에서 살펴보면 된다.</p>
</li>
</ul>
<br/>

<h3 id="4-페이징-제한out-of-memory">4. 페이징 제한(Out Of Memory)</h3>
<ul>
<li>fetch join을 하여 가져온 데이터를 페이징을 처리하면 다음과 같은 오류가 발생한다.</li>
<li>왜냐하면 쿼리 수행한 결과를 모두 어플리케이션 메모리에 올려서 페이징 처리를 수행을 했기 때문이다. 만약에 만건을 가져오면 어플리케이션에 올리게 되면 메모리 문제가 발생을 합니다.<pre><code class="language-java">2022-01-16 12:37:18.309  WARN 39536 --- [           main] o.h.h.internal.ast.QueryTranslatorImpl   : HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!</code></pre>
</li>
</ul>
<br/>


<h3 id="3-2-default_batch_fetch_size-batchsize">3-2. default_batch_fetch_size, @BatchSize</h3>
<ul>
<li>Lazy Loading시 프록시 객체를 조회할 때 where in절로 묶어서 한번에 조회 할 수 있게 해주는 옵션입니다. yml에 전역 옵션으로 적용할 수 있고 @BatchSize를 통해 연관관계 BatchSize를 다르게 적용할 수 있습니다.</li>
</ul>
<blockquote>
<p>batchsize는 몇으로 적용을 해야되나요??
일반적으로 100~1000으로 설정을 합니다. 하지만 dbms에 따라서 where in절은 1000까지 제한하는 경우가 있기 때문에 1000이상은 설정하지 않는다. 그렇다고 너무 크게하면 was에서 메모리에 로딩하디에 오버헤드가 발생하기 때문에 서비스에 맞게 적절하게 설정을 해야합니다.</p>
</blockquote>
<pre><code class="language-java">  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        show_sql: true
        default_batch_fetch_size: 100

    @BatchSize(size = 10)
    @OneToMany(mappedBy = &quot;member&quot;,fetch = FetchType.LAZY)
    private List&lt;Request&gt; requests = new ArrayList&lt;&gt;();
</code></pre>
<pre><code class="language-java">===============================================================
Hibernate: select m1_0.id,m1_0.name from member_table m1_0
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
===============================================================
Hibernate: select m1_0.id,m1_0.name from member_table m1_0</code></pre>
<h3 id="fetch-join의-한계를-batch-size로-해결">Fetch join의 한계를 Batch Size로 해결</h3>
<ul>
<li>컬렉션 fetch join시 paging 문제나 여러개 컬렉션을 fetch join을 할 수 없는 문제를 해결합니다.</li>
<li>쿼리 수로는 fetch join이 한방으로 나가기 때문에 유리하다. 하지만 batch size는 in절을 하고 사이즈에 따라서 몇번의 쿼리가 발생할 수 있다. </li>
<li>데이터 전송량 관점에서는 Batch Size가 유리합니다. Fetch Join은 Join을 하고 나서 가져오기 때문에 중복 데이터를 많이 가져와야하기 때문입니다.</li>
</ul>
<blockquote>
<p>결론</p>
</blockquote>
<ol>
<li>컬렉션 n개 조회 : batch size 해결 </li>
<li>쿼리 개수 : fetch join이 1개의 쿼리로 해결 &gt; batch size는 in절 + 추가적인 쿼리 ( 최적화)</li>
<li>데이터 전송 : fetch join은 join을 하고 중복 데이터를 많이 가져옴 batch size가 유리</li>
</ol>
<br/>

<h3 id="3-3-entitygraph">3-3. @EntityGraph</h3>
<ul>
<li><p>EntityGraph는 어노테이션 방식으로 편하게 N+1 문제를 해결할 수 있다. 하지만 여기서 trade off가 발생한다. 사실은 Lazy Loading을 Eager Loading으로 부분적으로 전환하는 기능입니다.</p>
</li>
<li><p>여러 1:N 연관관계를 한번에 Join해 올 수 있습니다. FetchJoin의 경우 1개의 Collection까지만 같이 Join하여 조회할 수 있습니다.</p>
</li>
</ul>
<pre><code class="language-java">    @EntityGraph(attributePaths = {&quot;requests&quot;})
    @Query(&quot;select o from Member o&quot;)
    List&lt;Member&gt;findAllEntityGraph();</code></pre>
<ul>
<li>여기서 fetch join과 차이점은 EntityGraph는 left outer join으로 가져오고 컬렉션 fetch join을 해결할 수 있지만 중복적인 데이터 처리를 주의를 해야된다. 이를 처리하기 위해 컬렉션의 자료구조를 set 또는 jpql에서 distinct 처리가 필요하다.</li>
</ul>
<br/>

<h3 id="3-4-join연산--projection하여-특정-컬럼만-dto로-조회">3-4. join연산 &gt; Projection하여 특정 컬럼만 Dto로 조회</h3>
<pre><code class="language-sql">select new 패키지 경로.Dto(원하는 필드) 
from Member m
join m.request r
where m.id=r.id</code></pre>
<ul>
<li>장점으로는 많은 컬럼에서 projection하여 특정 컬럼만 조회를 할 수 있다. 커버링 인덱스로 처리될 수 있기 때문에 성능적인 이점을 가져올 수 있다.</li>
<li>하지만 단점으로는 영속성 컨테스트와 무관하게 동작하고 repository가 dto에 의존하기 때문에 dao 변경이 필요하다.</li>
</ul>
<br/>

<h1 id="결론">결론</h1>
<hr>
<blockquote>
<p>글을 시작하기 이전에 간단하게 정리하면 
1:1 연관관계 :  Fetch join
Collection 연관관계 : default_batch_fetch_size
N개의 컬렉션을 fetch join을 하면 MultipleBagFetchException이 발생한다.
특정 컬럼을 조회할 경우에 join을 하고 Projection을 Dto로 매핑을 한다.</p>
</blockquote>
<h1 id="참고">참고</h1>
<hr>
<p><a href="https://junhyunny.github.io/spring-boot/jpa/jpa-fetch-join-paging-problem/">https://junhyunny.github.io/spring-boot/jpa/jpa-fetch-join-paging-problem/</a></p>
<p><a href="https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1">https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTPS 적용(Feat. Certificate Manager, ELB, Route 53, 가비아)]]></title>
            <link>https://velog.io/@geon_km/Route-53-HTTPS-%EC%A0%81%EC%9A%A9Feat.-Certificate-Manager-ELB-%EA%B0%80%EB%B9%84%EC%95%84</link>
            <guid>https://velog.io/@geon_km/Route-53-HTTPS-%EC%A0%81%EC%9A%A9Feat.-Certificate-Manager-ELB-%EA%B0%80%EB%B9%84%EC%95%84</guid>
            <pubDate>Sun, 19 Nov 2023 23:29:16 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<hr>
<h3 id="https를-선택한-이유">Https를 선택한 이유</h3>
<ul>
<li>프로젝트에서 jwt를 보내기 위하여 쿠키를 사용을 한다. 이때 브라우저 크롬의 80버전 이후부터 쿠키를 보내기 위해서 sameSite, Secure설정이 필수이다. 이때 http를 통하여 보내면 크롬에서 정상적으로 쿠키를 받지 못하여 도업을 하게되었습니다.</li>
</ul>
<BR/>



<h1 id="본론">본론</h1>
<hr>
<h2 id="1-ec2-탄력적-ip-연결">1. EC2 탄력적 IP 연결</h2>
<ul>
<li><p>탄력적 ip 연결은 기존에 docker-compose 배포에 설명이 되어져 있어서 링크를 타서 보시면 됩니다.
<a href="https://velog.io/@geon_km/AWS-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1-%EB%B0%8F-%EA%B3%A0%EC%A0%95-IP%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%B0%B0%ED%8F%AC-Feat.-docker-compose">[AWS] EC2 인스턴스 생성 및 고정 IP를 이용한 클라우드 서비스 배포 Feat. docker-compose</a>
<img src="https://velog.velcdn.com/images/geon_km/post/2d254d47-00fb-46d2-9640-c5d5822f49e1/image.png" alt=""></p>
</li>
<li><p>처음에 EC2에 들어가서 인스턴스 시작을 클릭을 합니다. 이후 저는 Ubuntu 20.04 버전과 micro를 선택을 하겠습니다.
<img src="https://velog.velcdn.com/images/geon_km/post/66ce3082-4b2f-4511-a491-9835a18c3b18/image.png" alt=""></p>
</li>
</ul>
<h2 id="2-가비아-도메인-구입">2. 가비아 도메인 구입</h2>
<ul>
<li>도메인을 aws에서 구입을 할 수 있지만 가격이 비싸기 때문에 가비아에서 구매를 하겠습니다.
<img src="https://velog.velcdn.com/images/geon_km/post/0d442d5f-c41d-4243-b2f7-d8af793afcc9/image.png" alt=""></li>
<li>회원가입을 하고 도메인을 누르면 추천 도메인이 나옵니다. 이후 결제를 하고 다음과 같은 설정을 하면 됩니다.
<img src="https://velog.velcdn.com/images/geon_km/post/a46e15f0-eba5-43a6-ba1c-f5683d87f562/image.png" alt=""></li>
<li>이후 마이페이지 -&gt; 도메인 -&gt; 관리를 통하여 관리를 할 수 있습니다. 지금은 여기까지 하고 route53를 할때 추가적으로 관리를 하겠습니다.
<img src="https://velog.velcdn.com/images/geon_km/post/6e15045a-3fd9-4a47-af8f-2d828b189291/image.png" alt=""></li>
</ul>
<br/>

<h2 id="3-route53">3. Route53</h2>
<p><img src="https://velog.velcdn.com/images/geon_km/post/b695ee5f-13fc-4cfe-a2fb-e23dcfbbd70c/image.png" alt=""></p>
<ul>
<li><p>AWS에서 Route53 서비스로 들어가 호스팅 영역 -&gt; 호스팅 영역 생성 -&gt; 도메인 이름 입력 후, 생성 버튼을 누릅니다. 만들어진 호스팅 영역 인스턴스를 클릭해 상세 페이지로 이동합니다. 레코드 생성 버튼을 누른 후, 아래의 내용을 입력하고 레코드 생성 버튼을 누릅니다.</p>
</li>
<li><p>이후 레코드 생성을 눌러 레코드를 설정을 하고 값에 public ip를 입력을 합니다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/9323e552-68c4-4477-95f1-188750d976a7/image.png" alt=""></p>
<ul>
<li>이제 값/트래픽 라우팅 대상을 살펴보면 4개의 주소가 나온다. 이걸 가비아에 도메인 관리에 들어가서 선택을 합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/60ba65f7-2757-4c37-b92a-d587e5efb1db/image.png" alt=""></p>
<ul>
<li>네임서버에 1~4차에 넣습니다. 이때 뒤에 .은 제거를 해야됩니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/1e590683-af4b-4d4a-9d36-f921975a1eac/image.png" alt=""></p>
<h2 id="4-certificate-manager">4. Certificate Manager</h2>
<p><img src="https://velog.velcdn.com/images/geon_km/post/7ed0456d-93c3-4b14-99d7-d4a0c0ffe05e/image.png" alt=""></p>
<ul>
<li>AWS Certificate Manager &gt; 인증서 &gt; 인증서 요청을 선택하고 도메인 이름을 입력하고 DNS 검증을 선택을 합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/e1bc279f-16fc-4afa-af7f-ba0bff53c794/image.png" alt=""></p>
<ul>
<li>생성을 하고 Route53에서 레코드 생성을 누르면 정상적으로 발급 완료가 된다. 이때 Route53에서 확인을 해보면 새로운 컬럼이 추가가 되었을 것이다.</li>
</ul>
<h2 id="5-target-group">5. Target Group</h2>
<ul>
<li><p>대상 그룹에서 특별하게 설정은 필요가 없지만 health 부분만 신경을 쓰면 된다. 처음에 ec2에 들어가서 대상 그룹 &gt; 대상 그룹 생성을 누르고 인스턴스 , 이름 , http 80 , vpc를 설정을 하면된다. </p>
</li>
<li><p>생성을 하고 health를 보면 404로 오류가 노올텐데 이걸 편집에서 404로 변경하면 정상으로 변경이 됩니다.</p>
</li>
</ul>
<blockquote>
<p>여기서 삽질을 많이 했습니다. 404에러 처리는 이 사이트를 참고
<a href="https://may9noy.tistory.com/762">https://may9noy.tistory.com/762</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/geon_km/post/80682f21-0915-4503-b9be-c6a0f9256c5e/image.png" alt=""></p>
<h2 id="6-elb-등록-http---https">6. ELB 등록 (http -&gt; https)</h2>
<p><img src="https://velog.velcdn.com/images/geon_km/post/7503dac2-b15a-471e-a323-b244ce193b60/image.png" alt=""></p>
<ul>
<li>EC2에 로드벨런서를 선택하고 생성을 합니다. 이름을 입력하고 HTTP : 80 , HTTPS :443의 리스너를 선택을 합니다. 이후 가용영역을 2개 선택 이후 기본 인증서 선택에 ACM에서 인증서 선택 (권장)을 누르고 인증서 이름을 선택을 합니다. </li>
</ul>
<blockquote>
<p>가용 영역은 EC2 페이지에 영역을 확인할 수 있습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/geon_km/post/72a8df03-1460-460c-91a7-4e1e24f819d8/image.png" alt=""></p>
<ul>
<li>이후 HTTPS에서 라우팅 액션에 대상 그룹으로 전달 -&gt; 이전에 대상 그룹을 선택하고 
기본 SSL/TLS 서버 인증서 &gt; ACM에서 &gt; 이전에 만든 인증서를 선택을 하겠습니다.</li>
<li>여기까지 완료가 되었으면 Route53에서 새로운 레코드 생성 레코드 유형 , 대상 , 정책을 선택을 하면 끝</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/e42649e3-c32e-4840-8f1a-d1edf17c82fc/image.png" alt=""></p>
<h1 id="참고">참고</h1>
<hr>
<p><a href="https://may9noy.tistory.com/762">https://may9noy.tistory.com/762</a></p>
<p><a href="https://velog.io/@u-nij/Spring-Boot-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-AWS-EC2%EC%97%90-Route53-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0">https://velog.io/@u-nij/Spring-Boot-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-AWS-EC2%EC%97%90-Route53-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</a></p>
<p><a href="https://devlog-wjdrbs96.tistory.com/293">https://devlog-wjdrbs96.tistory.com/293</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jackson SerializationException (LocalDateTime) +  redis.serializer.SerializationException]]></title>
            <link>https://velog.io/@geon_km/Jackson-SerializationException-LocalDateTime-redis.serializer.SerializationException</link>
            <guid>https://velog.io/@geon_km/Jackson-SerializationException-LocalDateTime-redis.serializer.SerializationException</guid>
            <pubDate>Mon, 13 Nov 2023 12:04:43 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<hr>
<ul>
<li>프로젝트를 진행을 하면서 직, 역직열화를 하는 과정에서 오류가 발생을 하였습니다. 주로 LocalDateTime
<a href="https://github.com/CS-tudy/CStudy_BackEnd"> 프로젝트 링크 </a></li>
<li>주로 에러가 발생하는 과정을 총 2곳입니다. 테스트 코드를 진행을 하면서 HTTP 요청과 검증을 분리를 하였을 때 데이터 매핑과 Redis에 LocalDateTime을 캐싱 또는 적재를 하였을 때 발생을 하였습니다.</li>
</ul>
<blockquote>
<p>테스트 코드 에러</p>
</blockquote>
<pre><code class="language-java">com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module &quot;com.fasterxml.jackson.datatype:jackson-datatype-jsr310&quot; to enable handling
 at [Source: (String)&quot;[{&quot;id&quot;:1,&quot;title&quot;:&quot;제목1&quot;,&quot;content&quot;:&quot;내용1&quot;,&quot;createdDate&quot;:&quot;2023-09-28T14:30:00&quot;},{&quot;id&quot;:2,&quot;title&quot;:&quot;제목2&quot;,&quot;content&quot;:&quot;내용2&quot;,&quot;createdDate&quot;:&quot;2023-09-28T15:30:00&quot;}]&quot;; line: 1, column: 54] (through reference chain: java.util.ArrayList[0]-&gt;com.cstudy.modulecommon.dto.NoticeResponseDto[&quot;createdDate&quot;])</code></pre>
<blockquote>
<p>Redis 코드 에러</p>
</blockquote>
<pre><code class="language-java">exception is org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Unrecognized field &quot;createdDate&quot; (class com.cstudy.modulecommon.domain.member.Member), not marked as ignorable (13 known properties: &quot;requests&quot;, &quot;name&quot;, &quot;questions&quot;, &quot;memberCompetitions&quot;, &quot;memberIpAddress&quot;, &quot;version&quot;, &quot;rankingPoint&quot;, &quot;id&quot;, &quot;email&quot;, &quot;roles&quot;, &quot;password&quot;, &quot;countryIsoCode&quot;, &quot;file&quot;])
 at [Source: (byte[])&quot;{&quot;createdDate&quot;:&quot;2023-11-13&quot;,&quot;lastModifiedDate&quot;:&quot;2023-11-13&quot;,&quot;id&quot;:1,&quot;email&quot;:&quot;admin@admin.com&quot;,&quot;password&quot;:&quot;$2a$10$fVwh2NdKoNbOrm0hoAfVVeviLGC8v5Is3NQy9F/emlAmG2xCmrlqy&quot;,&quot;name&quot;:&quot;관리자&quot;,&quot;rankingPoint&quot;:0.0,&quot;memberIpAddress&quot;:null,&quot;countryIsoCode&quot;:null,&quot;version&quot;:0,&quot;file&quot;:[],&quot;questions&quot;:[],&quot;memberCompetitions&quot;:[],&quot;requests&quot;:[],&quot;roles&quot;:[{&quot;roleId&quot;:2,&quot;name&quot;:&quot;ROLE_ADMIN&quot;}]}&quot;; line: 1, column: 17] (through reference chain: com.cstudy.modulecommon.domain.member.Member[&quot;createdDate&quot;]); nested exception is com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field &quot;createdDate&quot; (class com.cstudy.modulecommon.domain.member.Member), not marked as ignorable (13 known properties: &quot;requests&quot;, &quot;name&quot;, &quot;questions&quot;, &quot;memberCompetitions&quot;, &quot;memberIpAddress&quot;, &quot;version&quot;, &quot;rankingPoint&quot;, &quot;id&quot;, &quot;email&quot;, &quot;roles&quot;, &quot;password&quot;, &quot;countryIsoCode&quot;, &quot;file&quot;])
 at [Source: (byte[])&quot;{&quot;createdDate&quot;:&quot;2023-11-13&quot;,&quot;lastModifiedDate&quot;:&quot;2023-11-13&quot;,&quot;id&quot;:1,&quot;email&quot;:&quot;admin@admin.com&quot;,&quot;password&quot;:&quot;$2a$10$fVwh2NdKoNbOrm0hoAfVVeviLGC8v5Is3NQy9F/emlAmG2xCmrlqy&quot;,&quot;name&quot;:&quot;관리자&quot;,&quot;rankingPoint&quot;:0.0,&quot;memberIpAddress&quot;:null,&quot;countryIsoCode&quot;:null,&quot;version&quot;:0,&quot;file&quot;:[],&quot;questions&quot;:[],&quot;memberCompetitions&quot;:[],&quot;requests&quot;:[],&quot;roles&quot;:[{&quot;roleId&quot;:2,&quot;name&quot;:&quot;ROLE_ADMIN&quot;}]}&quot;; line: 1, column: 17] (through reference chain: com.cstudy.modulecommon.domain.member.Member[&quot;createdDate&quot;])] with root cause
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field &quot;createdDate&quot; (class com.cstudy.modulecommon.domain.member.Member), not marked as ignorable (13 known properties: &quot;requests&quot;, &quot;name&quot;, &quot;questions&quot;, &quot;memberCompetitions&quot;, &quot;memberIpAddress&quot;, &quot;version&quot;, &quot;rankingPoint&quot;, &quot;id&quot;, &quot;email&quot;, &quot;roles&quot;, &quot;password&quot;, &quot;countryIsoCode&quot;, &quot;file&quot;])
 at [Source: (byte[])&quot;{&quot;createdDate&quot;:&quot;2023-11-13&quot;,&quot;lastModifiedDate&quot;:&quot;2023-11-13&quot;,&quot;id&quot;:1,&quot;email&quot;:&quot;admin@admin.com&quot;,&quot;password&quot;:&quot;$2a$10$fVwh2NdKoNbOrm0hoAfVVeviLGC8v5Is3NQy9F/emlAmG2xCmrlqy&quot;,&quot;name&quot;:&quot;관리자&quot;,&quot;rankingPoint&quot;:0.0,&quot;memberIpAddress&quot;:null,&quot;countryIsoCode&quot;:null,&quot;version&quot;:0,&quot;file&quot;:[],&quot;questions&quot;:[],&quot;memberCompetitions&quot;:[],&quot;requests&quot;:[],&quot;roles&quot;:[{&quot;roleId&quot;:2,&quot;name&quot;:&quot;ROLE_ADMIN&quot;}]}&quot;; line: 1, column: 17] (through reference chain: com.cstudy.modulecommon.domain.member.Member[&quot;createdDate&quot;]) </code></pre>
<ul>
<li><p>이 문제를 해결하기 위하여 검색을 하였을 때 Java 8의 LocalDateTime을 직렬, 역직렬화를 수행을 하였을 때 오류가 발생을 한다고 알게 되었습니다.</p>
</li>
<li><p>이 게시글에서 Java의 LocalDateTime의 문제점과 Redis에서 SerializationException 문제점을 해결하는 과정을 작성을 하겠습니다.</p>
</li>
</ul>
<h1 id="본론">본론</h1>
<hr>
<h2 id="레디스-역질렬화-해결법">레디스 역질렬화 해결법</h2>
<h3 id="문제-상황">문제 상황</h3>
<ul>
<li>JWT를 사용하면서 2곳의 회원의 인증을 받습니다. (1) 서비스 로직 (2) JWT 필터 위에 인증을 2개가 필요하지 않고 대용량 트래픽이 들어오면 문제가 발생할 수 있습니다.</li>
<li>이러한 문제를 Redis 캐싱을 통하여 해결을 하였지만 다음과 같은 오류가 발생을 했습니다.</li>
</ul>
<pre><code class="language-java">Resolved [org.springframework.data.redis.serializer.SerializationException:
Could not write JSON: Java 8 date/time type `java.time.LocalDateTime`
not supported by default: add Module &quot;com.fasterxml.jackson.datatype:jackson-datatype-jsr310&quot; to enable handling</code></pre>
<blockquote>
<p>밑에서 LocalDateTime의 역직렬화 문제를 해결하는 방법에 설명을 겠습니다.</p>
</blockquote>
<h3 id="문제-해결하기-위하여-노력한-방법">문제 해결하기 위하여 노력한 방법</h3>
<h3 id="1-jackson-datatype-jsr310-의존성을-추가한다">1. jackson-datatype-jsr310 의존성을 추가한다.</h3>
<ul>
<li>처음에 시도한 방식으로 LocalDatetime을 역직렬화 하기 위하여 다음과 같은 의존성을 추가를 하였지만 아직도 오류가 발생을 하였다.</li>
</ul>
<h3 id="2-custom-serializer-사용하기">2. Custom Serializer 사용하기</h3>
<blockquote>
<p>기존의 문제의 코드</p>
</blockquote>
<h3 id="stringredisserializer">StringRedisSerializer</h3>
<ul>
<li>StringRedisSerializer는 String 값을 그대로 저장을 한다. JSON 형태로 직접 인, 디코딩을 해줘야하는 단점이 있지만 클래스 타입을 지정할 필요가 없고 쓰레드간의 문제가 발생하지 않는다.</li>
</ul>
<h3 id="genericjackson2jsonredisserializer">GenericJackson2JsonRedisSerializer</h3>
<ul>
<li>객체의 클래스 지정 없이 모든 Class Type을 JSON 형태로 저장할 수 있는 Serializer이다.</li>
<li>클래스 타입에 상관 없이 모든 객체를 직렬화 한다는 장점이 있지만  Object의 class 및 package까지 전부 함께 저장하게 되어 다른 프로젝트에서 redis에 저장되어 있는 값을 사용하려면 package까지 일치시켜줘야한다.</li>
</ul>
<pre><code class="language-java"> @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext
                        .SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext
                        .SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));


        return RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration)
                .build();
    }</code></pre>
<ul>
<li>기존의 코드는 LoacalDateTime을 처리하지 못하기 때문에 ObjectMapper에 JavaTimeModule()을 추가하여 GenericJackson2JsonRedisSerializer의 파라미터로 해당 ObjectMapper를 넘겨주게 만들었습니다.</li>
</ul>
<pre><code class="language-java">    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext
                        .SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext
                        .SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer));


        return RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration)
                .build();
    }</code></pre>
<h2 id="역질렬화-문제가-발생한-테스트-코드">역질렬화 문제가 발생한 테스트 코드</h2>
<ul>
<li><p>현재 테스트 코드는 공지사항을 페이징을 처리하는 부분을 테스트를 하고 있습니다.</p>
</li>
<li><p>테스트 코드의 이해를 위해 구조에 대해 설명을 하겠습니다.
<img src="https://velog.velcdn.com/images/geon_km/post/c489919f-a3a0-4ac6-89e0-7532583a4412/image.png" alt=""></p>
</li>
<li><p>현재 테스트 코드에서는 Controller를 테스트 하는 구조입니다. 일단 mockMVC, ObjectMapper를 MockApiCaller에 추상 클래스로 분리를 하였습니다.</p>
</li>
</ul>
<pre><code class="language-java">public abstract class MockApiCaller {

    protected final MockMvc mockMvc;

    protected final ObjectMapper objectMapper;

    public MockApiCaller(MockMvc mockMvc, ObjectMapper objectMapper) {
        this.mockMvc = mockMvc;
        this.objectMapper = objectMapper;
    }

 ///-&gt; POST의 EXCEPTION을 검증하는 코드도 공통으로 사용하기 때문에 Htpp Method에 따라서 분리하여 상속을 통해 쉽게 사용할 
 ///    수 있게 관리
    public ApiResponse&lt;ErrorResponse&gt; sendPostRequest_WithAuthorization_ParseErrorResponse(String url, Object request) throws Exception {

        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post(url)
                .contentType(MediaType.APPLICATION_JSON)
                .header(&quot;Authorization&quot;, &quot;Bearer &quot; + ADMIN_USER)
                .content(objectMapper.writeValueAsString(request));

        MockHttpServletResponse response = mockMvc.perform(builder)
                .andReturn()
                .getResponse();

        ErrorResponse errorResponse = ErrorResponse.builder()
                .code(JsonPath.read(response.getContentAsString(StandardCharsets.UTF_8), &quot;$.code&quot;))
                .message(JsonPath.read(response.getContentAsString(StandardCharsets.UTF_8), &quot;$.message&quot;))
                .validation(JsonPath.read(response.getContentAsString(StandardCharsets.UTF_8), &quot;$.validation&quot;))
                .build();

        return new ApiResponse&lt;&gt;(response.getStatus(), errorResponse);
    }

    ... 생략</code></pre>
<h3 id="noticemockapicaller">NoticeMockApiCaller</h3>
<ul>
<li><p>이렇게 분리한 이유는 테스트 코드는 데이터의 정합성을 검증하는 역할 이외에 상대방에게 코드에 대해 설명하는 기능을 한다고 생각합니다.</p>
</li>
<li><p>이때 Controller에서 제일 중요한 부분은 <code>Stub</code>을 한 상태를 검증하는 부분이 제일 핵심 관심사라고 판단하여 Http 요청과 검증하는 로직을 분리를 하였습니다.</p>
<pre><code class="language-java">public class NoticeMockApiCaller extends MockApiCaller {

  public NoticeMockApiCaller(MockMvc mockMvc, ObjectMapper objectMapper) {
      super(mockMvc, objectMapper);
  }

  public ApiResponse&lt;Page&lt;NoticeResponseDto&gt;&gt; findNoticeWithPage(NoticeSearchRequestDto request) throws Exception {
      MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get(&quot;/api/notice&quot;)
              .contentType(MediaType.APPLICATION_JSON)
              .content(objectMapper.writeValueAsString(request));

      MockHttpServletResponse response = mockMvc.perform(builder)
              .andReturn()
              .getResponse();

      String jsonResponse = response.getContentAsString(StandardCharsets.UTF_8);

      JsonNode jsonNode = objectMapper.readTree(jsonResponse);
      List&lt;NoticeResponseDto&gt; content = objectMapper.readValue(jsonNode.get(&quot;content&quot;).toString(), new TypeReference&lt;&gt;() {});
      int totalPages = jsonNode.get(&quot;totalPages&quot;).asInt();
      long totalElements = jsonNode.get(&quot;totalElements&quot;).asLong();

      Page&lt;NoticeResponseDto&gt; noticePage = new PageImpl&lt;&gt;(content, PageRequest.of(0, content.size()), totalElements);

      return ApiResponse.success(response.getStatus(), noticePage);
  }
}</code></pre>
</li>
</ul>
<h3 id="noticecontrollertest">NoticeControllerTest</h3>
<ul>
<li><p>HTTP 요청과 Response를 매핑하는 apiCaller 클래스를 요청하여 반환 값을 return을 받아 assertThat으로 검증을 했습니다.</p>
<pre><code class="language-java">  @DisplayName(&quot;/api/notice 공지사항 조회 페이징&quot;)
  @Nested
  class paging_findNotice{

      private final LocalDateTime localDateTime = LocalDateTime.of(2023, 9, 28, 14, 30);

</code></pre>
</li>
</ul>
<pre><code>    @BeforeEach
    void setUp(){
        NoticeResponseDto noticeResponse1 = createNoticeResponse(1L, &quot;제목1&quot;, &quot;내용1&quot;, localDateTime);
        NoticeResponseDto noticeResponse2 = createNoticeResponse(2L, &quot;제목2&quot;, &quot;내용2&quot;, localDateTime.plusHours(1));

        List&lt;NoticeResponseDto&gt; list = new ArrayList&lt;&gt;(Arrays.asList(noticeResponse1, noticeResponse2));


        Page&lt;NoticeResponseDto&gt; pagedResponse = new PageImpl&lt;&gt;(list);

        given(noticeService.findNoticePage(anyInt(), anyInt(), any(NoticeSearchRequestDto.class)))
                .willReturn(pagedResponse);
    }

    @Test
    public void 공지사항_조회_페이징_기본_PageRequest_Default() throws Exception{
        //given
        NoticeSearchRequestDto request = NoticeSearchRequestDto.builder().build();
        //when
        ApiResponse&lt;Page&lt;NoticeResponseDto&gt;&gt; response = noticeMockApiCaller.findNoticeWithPage(request);

        //Then
        assertAll(
                ()-&gt;assertThat(response.getStatus()).isEqualTo(200),

                ()-&gt;assertThat(response.getBody().getTotalPages()).isEqualTo(1),
                ()-&gt;assertThat(response.getBody().getNumber()).isEqualTo(0),
                ()-&gt;assertThat(response.getBody().getSize()).isEqualTo(2),


                ()-&gt;assertThat(response.getBody().getContent().get(0).getId()).isEqualTo(1L),
                ()-&gt;assertThat(response.getBody().getContent().get(0).getTitle()).isEqualTo(&quot;제목1&quot;),
                ()-&gt;assertThat(response.getBody().getContent().get(0).getContent()).isEqualTo(&quot;내용1&quot;),
                ()-&gt;assertThat(response.getBody().getContent().get(0).getCreatedDate()).isEqualTo(localDateTime),

                ()-&gt;assertThat(response.getBody().getContent().get(1).getId()).isEqualTo(2L),
                ()-&gt;assertThat(response.getBody().getContent().get(1).getTitle()).isEqualTo(&quot;제목2&quot;),
                ()-&gt;assertThat(response.getBody().getContent().get(1).getContent()).isEqualTo(&quot;내용2&quot;),
                ()-&gt;assertThat(response.getBody().getContent().get(1).getCreatedDate()).isEqualTo(localDateTime.plusHours(1))
        );
    }
}</code></pre><pre><code>
- MockHttpServletResponse는 정상적으로 처리가 되었지만 다음과 같은 문제가 발생을 했다. 
![](https://velog.velcdn.com/images/geon_km/post/e3f1ac5b-d54f-417c-a52e-e524bc3d38be/image.png)


### 문제
- 해당 오류는 Java 8에 추가된 날짜/시간 타입인 LocalDate, LocalTime, LocalDateTime이 기본적으로 Jackson 라이브러리에 의해 지원되지 않기 때문에 발생하는 것입니다. 

- 이를 해결하려면 ``com.fastxml.jackson.datatype:jackson-datatype-jsr310`` 모듈 추가가 필요합니다. 해당 모듈은 Java 8의 날짜/시간 타입은 Jackson에서 처리할 수 있도록 지원해 줍니다.

- 해당 ``jsr310``을 따로 의존성을 추가를 할 필요는 없습니다. ``spring-boot-starter-json``모듈에 ``jackson-datatype-jsr310``을 가져오지만 ``ObjectMapper``에 jsr310를 추가를 하지 않기 때문입니다.

## 해결
- 문제가 발생하면 가장 도움을 많이 받으는 baeldung에서 도움을 받았다.
![](https://velog.velcdn.com/images/geon_km/post/730ad00e-79e4-4705-8922-9c7f32feca22/image.png)


```java
 ObjectMapper objectMapper = new ObjectMapper();
 objectMapper.registerModule(new JavaTimeModule());</code></pre><h3 id="javatimemodule">JavaTimeModule</h3>
<ul>
<li>JavaTimeModule은 Java 8에 도입된 새로운 날짜 및 시간 API(LocalDate, LocalTime, LocalDateTime)를 Jackson 라이브러리에서 적절하게 처리할 수 있게 해주는 모듈입니다. 기본적으로 Jackson 라이브러리는 Java 8의 새로운 날짜 및 시간 타입들을 인식하지 못하기 때문에 해당 타입들을 JSON으로 직렬화하거나 JSON에서 역직렬화할 때 문제가 발생할 수 있습니다.</li>
</ul>
<p>이러한 문제를 해결하기 위해 JavaTimeModule을 ObjectMapper에 등록하면 날짜/시간 타입들을 적절하게 직렬화하고 역직렬화할 수 있게 됩니다.</p>
<pre><code class="language-java"> public ApiResponse&lt;Page&lt;NoticeResponseDto&gt;&gt; findNoticeWithPage(String url, NoticeSearchRequestDto request) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get(url)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request));

        MockHttpServletResponse response = mockMvc.perform(builder)
                .andReturn()
                .getResponse();

        String jsonResponse = response.getContentAsString(StandardCharsets.UTF_8);
    ========================변경===================================
        objectMapper.registerModule(new JavaTimeModule());
    =================================================================

        JsonNode jsonNode = objectMapper.readTree(jsonResponse);
        List&lt;NoticeResponseDto&gt; content = objectMapper.readValue(jsonNode.get(&quot;content&quot;).toString(), new TypeReference&lt;&gt;() {});
        int totalPages = jsonNode.get(&quot;totalPages&quot;).asInt();
        long totalElements = jsonNode.get(&quot;totalElements&quot;).asLong();

        Page&lt;NoticeResponseDto&gt; noticePage = new PageImpl&lt;&gt;(content, PageRequest.of(0, content.size()), totalElements);

        return ApiResponse.success(response.getStatus(), noticePage);
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/geon_km/post/7997d096-3d06-472c-8b55-42d9b444be8a/image.png" alt=""></p>
<h1 id="참고">참고</h1>
<hr>
<p><a href="https://woo-chang.tistory.com/75">https://woo-chang.tistory.com/75</a>
<a href="https://www.baeldung.com/jackson-serialize-dates">https://www.baeldung.com/jackson-serialize-dates</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[특정 시간에 Shell Crontab을 이용한  Redis RDB 방식으로 백업]]></title>
            <link>https://velog.io/@geon_km/%ED%8A%B9%EC%A0%95-%EC%8B%9C%EA%B0%84%EC%97%90-Shell-Crontab%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-Redis-RDB-%EB%B0%A9%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EB%B0%B1%EC%97%85</link>
            <guid>https://velog.io/@geon_km/%ED%8A%B9%EC%A0%95-%EC%8B%9C%EA%B0%84%EC%97%90-Shell-Crontab%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-Redis-RDB-%EB%B0%A9%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EB%B0%B1%EC%97%85</guid>
            <pubDate>Fri, 10 Nov 2023 15:59:59 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<hr>
<ul>
<li>CStudy 프로젝트를 진행을 하면서 Redis를 사용을 하였습니다. 로컬 캐시를 사용하지 않은 이유는 다양한 자료구조, 분산처리의 유용하다고 생각하여 선택을 하였습니다.</li>
<li>Redis에는 캐싱, 회원의 점수, 문제에 대한 정보를 저장하기 때문에 데이터의 정합성이 중요합니다.</li>
<li>최근에 프로젝트를 배포하고 운영을 하면서 DB 서버 오류가 발생하여 Redis 데이터의 발생하였습니다. </li>
<li>이러한 문제를 해결하기 위하여 Redis의 데이터를 백업을 하며 학습한 내용을 정리했습니다.</li>
</ul>
<br/>

<h1 id="본론">본론</h1>
<hr>
<h2 id="redis-백업">Redis 백업</h2>
<ul>
<li>Redis 백업 방식을 살펴보면 RDB (snapshotting) 방식과 AOF (Append only file) 두가지가 있다.</li>
<li>이때 이 두가지 방식에는 차이가 있습니다.</li>
</ul>
<ol>
<li><p>RDB방식은 특정한 각격마다 메모리에 있는 레디스 데이터 전체를 디스크에 쓰는 것이다. 이를 통하여 백업에 이점을 가져갈 수 있다.</p>
</li>
<li><p>AOF 방식은 명령이 실행될때 마다 데이터를 파일에 기록하여 데이터 손실이 거의 없다.</p>
</li>
</ol>
<h3 id="redis-rdb-방식">Redis RDB 방식</h3>
<ul>
<li><p>RDB 방식은 특정 시점의 스냅샷으로 데이터를 저장한다. 재시작 시 RDB 파일이 있으면 읽어서 복구한다.
<img src="https://velog.velcdn.com/images/geon_km/post/e56ed11e-8127-4577-ac05-b8c27b3a5699/image.png" alt=""></p>
</li>
<li><p>장점</p>
</li>
</ul>
<ol>
<li>작은 파일 사이즈로 백업 파일 관리가 용이하다. ( 원격지 백업, 버전 관리 )</li>
<li>Fork를 이용해 백업하므로 서비스 중인 프로세스에 성능에 영향이 없다.</li>
<li>데이터 스냅샷 방식이므로 빠른 복구가 가능하다.</li>
</ol>
<ul>
<li>단점</li>
</ul>
<ol>
<li>스냅샷을 저장하는 시점 사이의 데이터 변경사항은 유실될 수 있다.</li>
<li>fork를 이용하기 때문에 시간이 오래 걸릴 수 있고, CPU와 메모리 자원을 많이 소모한다.</li>
<li>데이터 무결성이나 정합성에 대한 요구가 크지 않은 경우 사용 가능하다. ( 마지막 백업 시 에러 발생의 문제 )</li>
</ol>
<ul>
<li>.rdb 파일은 AOF 파일보다 사이즈가 작다는 특징이 있다. 따라서 로딩 속도가 AOF보다 빠르다.</li>
</ul>
<blockquote>
<p>RDB 방식의 저장 - Save, bgsave 방식</p>
</blockquote>
<ul>
<li>방식에는 크게 2가지가 있다. </li>
</ul>
<p>1번 Save 방식</p>
<ul>
<li>싱글 스레드로 작업을 수행을 합니다. 작업이 완ㄹ되기 까지 모든 요청이 대기하게된다.</li>
</ul>
<p>2번 bgsave 방식</p>
<ul>
<li><p>멀티 스레드 형식으로 비동기로 작업을 수행한다. redis 서비스에서 사용중인 데이터는 모두 메모리에 있는데 작업을 수행한다. 서비스 영향 없이 스냅샷으로 저장하기 위해서는 Copy-on-Write(COW) 방식을 사용한다.</p>
</li>
<li><p>자식 프로세스 fork 후 부모 프로세스의 메모리에서 실제로 변경이 발생한 부분만 복사한다. wrtie 작업이 많아서 부모 페이지 전부에 변경이 발생하게 되면 부모 페이지 전부를 복사하게 된다.</p>
</li>
<li><p>이때 자원을 많이 사용하기 때문에 cpu, memory를 체크를 해야된다. 나는 새벽 2시에 작업을 수행하게 하여 이 문제를 우회를 하였다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/f791eb7d-f46f-42ad-8d7c-ef9c17b330d8/image.png" alt=""><img src="https://velog.velcdn.com/images/geon_km/post/87832608-7e30-4c0b-8dcc-ea70bc3bdcec/image.png" alt=""></p>
<ul>
<li>일단 docker의 redis의 데이터를 백업하기 위하여 .sh를 작성을 하였습니다.<pre><code class="language-shell">#!/bin/bash
</code></pre>
</li>
</ul>
<h1 id="사용자-프로파일-로드">사용자 프로파일 로드</h1>
<p>. ~user1/.bash_profile</p>
<h1 id="redis-cli를-사용하여-백그라운드에서-스냅샷-생성">Redis CLI를 사용하여 백그라운드에서 스냅샷 생성</h1>
<p>docker exec study-redis redis-cli BGSAVE</p>
<h1 id="백그라운드-세이브-작업이-완료될-때까지-대기">백그라운드 세이브 작업이 완료될 때까지 대기</h1>
<p>sleep 10</p>
<h1 id="현재-시간을-이용하여-백업-파일명-생성">현재 시간을 이용하여 백업 파일명 생성</h1>
<p>backup_filename=&quot;dump_$(date +&quot;%Y%m%d%H%M%S&quot;).rdb&quot;</p>
<h1 id="생성된-스냅샷-파일을-호스트의-안전한-위치로-복사">생성된 스냅샷 파일을 호스트의 안전한 위치로 복사</h1>
<p>docker cp study-redis:/data/dump.rdb &quot;/home/ubuntu/CStudy_Infra/$backup_filename&quot;</p>
<pre><code>
- 여기서 ` ~user1/.bash_profile`에 대한 궁금증이 생길 수 있다. 이 부분은 Cronntab을 위해 작성을 하였다. 이 부분 때문에 2시간을 삽질을 했다.이 부분은 Crontab과 관련이 있어 밑에서 설명을 하겠습니다.

&lt;br/&gt;

### Redis AoF 방식
![](https://velog.velcdn.com/images/geon_km/post/0df294e0-debe-45ac-8eba-0b50cde94fbd/image.png)
모든 쓰기 요청에 대한 로그를 저장
재시작 시 AOF에 기록된 모든 동작을 재수행해서 데이터를 복구

장점
1. 모든 변경사항이 기록되므로 RDB 방식 대비 안정적으로 데이터 백업 가능
2. AOF 파일은 append-only 방식이므로 백업 파일이 손상될 위험이 적음
3. 실제 수행된 명령어가 저장되어 있으므로 사람이 보고 이해할 수 있고 수정 가능
- 만약 flushAll 같은 명령어가 잘못 입력되어도 파일에서 수정이 가능

단점 
1. RDB방식보다 파일 사이즈가 크다.
2. RDB 방식 대비 백업&amp;속도가 느리다. (백업 성능은 FSYNC 정책에 따라서 조절이 가능하다.)

[AOF 자세항 이야기](https://inpa.tistory.com/entry/REDIS-%F0%9F%93%9A-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%98%81%EA%B5%AC-%EC%A0%80%EC%9E%A5%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%EC%98%81%EC%86%8D%EC%84%B1)

- AOF에 대해서 자세하게 적기보다는 자세하게 나온 사이트를 통해서 알 수 있다.

&lt;br/&gt;

### RDB 방식을 선택한 이유
- 데이터의 정합성을 위해서는 AOF 방식이 좋고 많은 사람들이 선호한다. 하지만 내가 RDB 방식을 처리한 이유는 일단 파일의 크기이다. 최근 EC2로 Auto scailing을 학습하면서 aws 비용이 많이 나와 이번 db ec2는 micro로 설정을 하였습니다. 또한 메모리, cpu의 사용량이 많은 rdb방식과 백업하는 순간 데이터의 유실이 있을 수 있는 단점을 해결하기 위하여 Shell, Crontab을 이용하여 새벽 2시에 작업을 수행하면 이 문제에 대해서 해결할 수 있다고 생각했습니다.


&lt;br/&gt;

## Crontab

### CRON
- 유닉스 계열 운영체제의 JOB 스케줄러로 날짜, 시간 조건에 따라 주기적으로 특정 로직을 실행하는 프로그램입니다.

- Crontab은 Cron 설정 정보를 파일로 관리하는 기능입니다.

- Cron과 Crontab 모두 크게 용어를 구분하지 않고 스케줄러라는 의미로 사용을 한다.

### 표현식
![](https://velog.velcdn.com/images/geon_km/post/d944efa7-7fd6-4a23-9f72-a3db291e1bec/image.png)

- Cron 표현식은 총 7자리를 사용을 합니다. 왼쪽부터 초, 분, 시간, 일, 월, 요일, 연을 의미합니다.

&gt; Spring 프레임워크에서는 스케줄러 라이브러리를 사용할 때, 연을 제외한 6자리를 사용한다.
Crontab에서는 초와 연을 제외한 5자리를 사용한다.


### Shell, 쉘 스크립트
- Shell : 사용자가 입력한 명령어를 해석하고 프로그램을 실행시키는 인터페이스

- 쉘 스크립트 : 쉘로 작성한 명령어를 스크립트로 작성해, 한 줄 씩 실행하도록 만든것이다.

### 실제로 백업하기

- 일단 crontab을 설치를 해야됩니다.
```shell
# cron 설치
sudo apt update -y
sudo apt install -y cron
# cron 시작
sudo service cron start
# cron systemctl 활성화
sudo systemctl enable cron.service
# cron systemctl 등록 확인
sudo systemctl list-unit-files | grep cron
sudo service cron status</code></pre><ul>
<li><p>이후 쉘 스크립트를 하나 작성을 하겠습니다.</p>
</li>
<li><p>만약에 없다면 <code>vi redis_backup_script.sh</code>을 입력하셔서 만드시면 되지만 저는 Github Repo에 Infra에 관련된 파일이 있어서 Clone을 했습니다.</p>
</li>
<li><p><code>cat redis_backup_script.sh</code>을 통해 내용을 살펴보겠습니다.</p>
<pre><code class="language-shell">#!/bin/bash
</code></pre>
</li>
</ul>
<h1 id="사용자-프로파일-로드-1">사용자 프로파일 로드</h1>
<p>. ~user1/.bash_profile</p>
<h1 id="redis-cli를-사용하여-백그라운드에서-스냅샷-생성-1">Redis CLI를 사용하여 백그라운드에서 스냅샷 생성</h1>
<p>docker exec study-redis redis-cli BGSAVE</p>
<h1 id="백그라운드-세이브-작업이-완료될-때까지-대기-1">백그라운드 세이브 작업이 완료될 때까지 대기</h1>
<p>sleep 10</p>
<h1 id="현재-시간을-이용하여-백업-파일명-생성-1">현재 시간을 이용하여 백업 파일명 생성</h1>
<p>backup_filename=&quot;dump_$(date +&quot;%Y%m%d%H%M%S&quot;).rdb&quot;</p>
<h1 id="생성된-스냅샷-파일을-호스트의-안전한-위치로-복사-1">생성된 스냅샷 파일을 호스트의 안전한 위치로 복사</h1>
<p>docker cp study-redis:/data/dump.rdb &quot;/home/ubuntu/CStudy_Infra/$backup_filename&quot;</p>
<p>```</p>
<ul>
<li>여기서 bgsave를 통하여 redis의 데이터를 백업을 하고 시간에 따라 파일을 저장을 하게 만들었습니다.</li>
<li>여기서 user/.bash_profile을 작성하지 않으면 아마도 crontab으로 일정 주기에 따라 실행이 되지 않습니다. </li>
</ul>
<blockquote>
<p>실행이 되지 않는 이유</p>
</blockquote>
<ol>
<li>Crond 데몬은 .sh 파일을 찾지 못합니다.</li>
</ol>
<ul>
<li>cron 데몬은 root 계정이 실행했으며 root계정의 shell 환경이고 .sh는 user1계정에서 만들어졌으며 user1계정은 shell 환경입니다.</li>
<li>리눅스에서 shell 환경에서 다른 파일을 찾을 수 없습니다. Path를 잡아주면 되지만 공통 영역까지 제어하게 되면 shell을 최대한 활용하기 힘듭니다.</li>
</ul>
<ol start="2">
<li>.sh 수행할 때 user1계정에만 종속된 변수나 라이브러리를 사용</li>
</ol>
<ul>
<li>리눅스 계정이 다른 shell 환경의 변수나 라이브러리를 알 수 없습니다.</li>
<li>crond은 .sh 실행중에 user1계정의 변수나 라이브러리에서 오류가 발생하며 종류합니다.</li>
</ul>
<h3 id="cron-설정하기">Cron 설정하기</h3>
<ul>
<li>cron을 설치를 하였기 때문에 <code>crontab -e</code>를 통하여 조건을 작성을 하겠습니다.
<img src="https://velog.velcdn.com/images/geon_km/post/e539f6cc-b1de-42ee-b75e-0b55c8f53b30/image.png" alt=""></li>
<li>저는 새벽 2시에 sh를 실행하고 관련 로그를 찍게 작성을 하였습니다.</li>
<li>여기서 저장하는 방식이 조금 다른데 <code>컨트롤 +x</code>를 누르고 <code>Y</code>와 <code>엔터</code>를 눌러서 저장을 합니다.</li>
</ul>
<BR/>

<ul>
<li><p>그러면 잘 적용이 되었는지 확인을 하겠습니다. <code>crontab -l</code>을 통해 조회를 할 수 있습니다.
<img src="https://velog.velcdn.com/images/geon_km/post/e37c327f-f059-443d-8cdd-80a6a0a4292f/image.png" alt=""></p>
</li>
<li><p>이후 저는 테스트를 위하여 3분마다 저장을 하게 만들었더니 성공적으로 rdb 파일이 만들어졌습니다.
<img src="https://velog.velcdn.com/images/geon_km/post/c7967e3c-0617-4103-94c2-bdd0da8b2a72/image.png" alt=""></p>
</li>
<li><p>만약에 crontab이 작성을 했지만 정상적으로 동작을 하지 않는다면 <code>cat /var/log/syslog | grep CRON</code>을 통하여 확인할 수 있습니다.</p>
</li>
</ul>
<br/>

<h1 id="결론">결론</h1>
<hr>
<ul>
<li><p>물론 AOF 방식이 데이터의 정합성으로 좋은 방식이라고 생각하지만 현재 프로젝트의 상황과 제한된 리소스에서 RDB 방식이 더 적합하고 문제점을 Shell, Cron을 통하여 해결할 수 있다고 생각을 하였습니다.</p>
</li>
<li><p>물론 이 방식이 나중에 사람들이 많이 사용을 한다면 변경이 필요하지만 그때는 현재 방식의 문제점을 더 개선하는 <code>redis sentinel</code>을 통하여 해결을 하고 싶다.</p>
</li>
<li><p>리눅스에 대해서 명령어만 알고 있었는데 직접 shell, cron을 설정 및 작성을 하였는데 더욱 학습이 필요하다고 생각이 들었다.</p>
</li>
</ul>
<h1 id="참고">참고</h1>
<p><a href="https://sidepower.tistory.com/17">https://sidepower.tistory.com/17</a></p>
<p><a href="https://inpa.tistory.com/entry/REDIS-%F0%9F%93%9A-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%98%81%EA%B5%AC-%EC%A0%80%EC%9E%A5%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%EC%98%81%EC%86%8D%EC%84%B1">https://inpa.tistory.com/entry/REDIS-%F0%9F%93%9A-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%98%81%EA%B5%AC-%EC%A0%80%EC%9E%A5%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%EC%98%81%EC%86%8D%EC%84%B1</a></p>
<p><a href="https://ourcstory.tistory.com/63">https://ourcstory.tistory.com/63</a></p>
<p><a href="https://cloud-allstudy.tistory.com/104">https://cloud-allstudy.tistory.com/104</a></p>
<p><a href="https://jdm.kr/blog/2">https://jdm.kr/blog/2</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] CloudWatch, Lambda의 경보를 Slack으로 알림]]></title>
            <link>https://velog.io/@geon_km/AWS-CloudWatch-Lambda%EC%9D%98-%EA%B2%BD%EB%B3%B4%EB%A5%BC-Slack%EC%9C%BC%EB%A1%9C-%EC%95%8C%EB%A6%BC</link>
            <guid>https://velog.io/@geon_km/AWS-CloudWatch-Lambda%EC%9D%98-%EA%B2%BD%EB%B3%B4%EB%A5%BC-Slack%EC%9C%BC%EB%A1%9C-%EC%95%8C%EB%A6%BC</guid>
            <pubDate>Thu, 09 Nov 2023 16:42:24 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/geon_km/post/f9d6766e-7585-4d13-beb0-47a58dc6a76b/image.png" alt=""></p>
<h1 id="서론">서론</h1>
<hr>
<ul>
<li>일단 프로젝트에서 EC2에 DB를 설치하여 사용을 하고 있었습니다. 그런데 갑자기 DB 서버의  CPU가 100%가 되어서 시스템의 오류가 발생을 하였습니다.</li>
<li>시스템의 오류가 발생한 이유는 다음과 같다. CPU가 100%가 되며 서버가 다운이 되었습니다.</li>
<li>물론 서버가 다운이 되는 이유가 여러가지가 있지만 <a href="https://repost.aws/ko/knowledge-center/ec2-linux-status-check-failure">AWS 공식 사이트</a> 해당 사이트를 보면서 CPU 및 메모리 소진으로 판단을 하였습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/9374ad54-9f22-463f-a31c-ab2e526247df/image.png" alt=""></p>
<ul>
<li><p>그러면 왜 CPU가 100%가 되었는지 궁금하여 문제를 찾아봤습니다.</p>
</li>
<li><p>해당 EC2는 Micro로 설정하고 mysql, redis, mongodb를 설치를 하였습니다. 그런데 일반적으로 cpu는 평균 30~40을 유지를 하여 문제가 없다고 판단을 하였지만 갑자기 100%가 되었습니다.</p>
</li>
<li><p>해당 문제의 원인은 <code>Burstable performance instances</code>의 문제라고 생각을 하였습니다. 버스트 가능 성능 인스턴스에 대한 이해가 부족으로 인하여 서버의 오류가 발생을 하였습니다.</p>
</li>
<li><p>이번에는 문제의 원인, 해결을 하기 위한 노력, CloudWatch을 이용한 모니터링에 대하여 설명하겠습니다.</p>
</li>
</ul>
<br/>

<h1 id="본론">본론</h1>
<hr>
<h2 id="1--서버-오류가-발생한-이유-">1. [ 서버 오류가 발생한 이유 ]</h2>
<h3 id="ec2-버스트">EC2 버스트</h3>
<ul>
<li>버스트란 주로 EC2 인턴스 유형에서 사용되는 용어입니다. 버스트 가능한 인스턴스는 일시적으로 추가 리소스를 얻을 수 있는 인스턴스를 의미합니다. 즉. 인스턴스가 특별한 작업을 처리하기 위해 더 많은 리소스가 필요한 경우 유용합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/d28ee4b8-8709-48de-a6de-01e34050d3d4/image.png" alt=""></p>
<ul>
<li><p>서버를 운영하면 CPU가 갑자기 올라가는 버스트 현상을 볼 수 있다. 여기서 문제는 버스트 존만큼 CPU 사양을 높이게 되면 서버의 비용이 증가하고 평균 CPU 사용 범위로 지정을 하면 버스트 현상이 발생을 하였을 때 서버가 죽을 수 있는 문제가 있다. </p>
</li>
<li><p>이걸 해결하기 위해 버스트 가능 인스턴스의 개념을 나왔다.</p>
</li>
<li><p>EC2의 버스트 가능 인스턴스는 기본 CPU 사용율을 유지한다. 만약 많은 사용이 필요할 경우에 크레딧을 소모하여 문제를 해결하게 만들었습니다.</p>
</li>
<li><p>이때 인스턴스 유형에 따라 크레딧이 다르다.</p>
</li>
</ul>
<ul>
<li><p>예시로 현재 t3.small 타입의 인스턴스를 사용하게되면 AWS에서 시간당 24크레딧을 발급해준다. 그렇게 계속 누적되다가 일정 시간에 20%가 넘는 CPU 사용율이 발생할 경우 20% 넘었던 구간의 시간(ms)을 기준으로 크레딧을 일정량 소모하게 된다. 그리고 발급되고 있는 크레딧을 계속 누적되는 것이 아니라 특정 양만큼만 누적이 되고 더이상 누적이 안되기 때문에 크레딧 사용을 주의해야한다. 예시로 t3.small은 576의 크레딧만 누적할 수 있다. 그 이상 발급이 된 크레딧은 버려지게 된다.</p>
</li>
<li><p>여기서 조심해야 되는 부분은 공급보다 수요가 많아서 크레딧을 전부 사용하면 성능적으로 제약이 걸려서 서버의 장애를 일으킬 수 있다.</p>
</li>
</ul>
<br/>

<h2 id="2--문제를-해결하기-위하여-생각한-부분-">2. [ 문제를 해결하기 위하여 생각한 부분 ]</h2>
<h3 id="1-메모리-스왑">1. 메모리 스왑</h3>
<ul>
<li><p>메모리 스왑 (Memory Swap): 메모리 스왑은 시스템이 현재 사용하지 않는 메모리 페이지를 디스크의 스왑 공간에 저장하고, 필요한 경우에 다시 읽어와서 사용하는 메커니즘입니다. 이는 물리적인 RAM이 부족할 때 시스템의 성능을 유지하기 위해 사용됩니다.</p>
</li>
<li><p>메모리 스왑을 하는 이유</p>
</li>
</ul>
<ol>
<li><p>물리적 메모리 부족 : 현재 실행 중인 프로세스들이 사용하는 메모리 양이 물리적인 RAM의 용량을 초과하는 경우, 스왑을 사용하여 디스크의 공간을 활용해 추가 메모리를 확보할 수 있다.</p>
</li>
<li><p>프로세스 지속성 : 스왑은 메모리 부족 시 프로세스가 강제 종료되는 것을 방지하고, 시스템이 더 많은 메모리를 확보할 수 있도록 합니다.</p>
</li>
<li><p>유연성 : 스왑을 통해 더 많은 프로세스나 데이터를 메모리에 유지할 수 있어 시스템의 유연성이 증가합니다.</p>
</li>
</ol>
<br/>

<h3 id="1-2-ubuntu-2004에서-메모리-스왑을-하는-방법">1-2. Ubuntu 20.04에서 메모리 스왑을 하는 방법</h3>
<pre><code class="language-bash">sudo dd if=/dev/zero of=/swapfile bs=128M count=32
# /dev/zero에서 128MB 크기의 블록을 32개 생성하여 /swapfile에 쓰기

sudo chmod 600 /swapfile
# /swapfile의 파일 권한을 소유자만 읽기 및 쓰기 가능하도록 변경

sudo mkswap /swapfile
# /swapfile을 스왑 파티션으로 초기화

sudo swapon /swapfile
# /swapfile을 활성화하여 스왑으로 사용

sudo swapon -s
# 현재 활성화된 스왑 파티션 목록 확인

sudo vi /etc/fstab
# /etc/fstab 파일을 편집

[ vi에 하단에 추가 ] 
/swapfile swap swap defaults 0 0
# 부팅 시 자동으로 스왑을 마운트하기 위한 설정 추가

## 용량 확인
free
# 시스템의 메모리 및 스왑 사용량 확인
</code></pre>
<br/>

<h3 id="2-인스턴스-스케일-업">2. 인스턴스 스케일 업</h3>
<ul>
<li><p>서버의 오류가 발생하는 이유는 버스트의 수요와 공급이 맞지 않다고 판단하여 인스턴스를 micro -&gt; small로 유형을 변경하여 크레딧의 수를 변경을 하였습니다. </p>
</li>
<li><p>기존의 유형보다 성능이 높은 small을 선택하여 cpu의 성능이 더 좋아졌습니다.</p>
</li>
</ul>
<h3 id="cpu-credit">CPU Credit</h3>
<ul>
<li><p>정의  : AWS에서 CPU Credit은 1분동안 CPU Boost를 해줄 수 있는 갯수를 의미합니다.</p>
</li>
<li><p>크래딧은 1개의 CPU의 사용률이 100%가 되면, CPU는 BOOST 상태가 되며 1분에 1개의 크래딧을 소모를 합니다.</p>
</li>
<li><p>이때 크래딧이 없으면 성능 저하로 이어집니다. </p>
</li>
<li><p>이 부분은 마나와 비슷합니다. 적이 없을 때 적은 마나를 사용하기 때문에 마나가 충전이 되고, 적이 많으면 많은 마법을 사용하기 때문에 마나를 사용합니다. 크래딧도 똑같습니다. 트래픽을 적으로 생각하면 이해가 쉬울 거 같습니다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/571d9a60-2211-4442-926f-f59994ace372/image.png" alt=""></p>
<h3 id="3-stop-and-start">3. Stop and Start</h3>
<ul>
<li><p>간단하게 인스턴스를 정지 &amp; 시작을 의미합니다. (재실행 X) 이러한 방식으로 처리하면 크래딧이 재충전이 되기 때문에 문제를 해결할 수 있습니다.</p>
</li>
<li><p>간단하게 컴퓨터를 재부팅으로 생각하면 될거 같습니다.</p>
</li>
<li><p>현재 문제점에서 Stop and Start 방식을 사용을 하였지만 이 방식은 개발자, 관리자가 수동으로 해야되는 문제가 발생을 합니다. </p>
</li>
<li><p>또한 CPU가 증가를 하여 서버가 터지기 이전에 해야되기 때문에 지속적으로 모니터링을 해야됩니다.</p>
</li>
<li><p>그래서 CloudWatch를 통해 일정 CPU가 되었을 때 SLACK에 알림을 발송하는 방식으로 모니터링을 대체를 하
겠습니다.</p>
</li>
</ul>
<br/>


<h2 id="3--cloudwatch-lambda의-경보를-slack-알림-">3. [ CloudWatch, Lambda의 경보를 Slack 알림 ]</h2>
<h3 id="왜-cloudwatch를-사용을-하였는지">왜 CloudWatch를 사용을 하였는지?</h3>
<ul>
<li>먼저 CloudWatch에서 경보를 등록하는 이유는 서버에 문제가 발생하기 이전에 문제를 대응할 수 있게 하기 위함입니다.</li>
<li>현재 프로젝트에서는 cpu의 사용률이 특정 기준을 넘는 문제를 해결하기 위해 선택을 하였습니다.</li>
<li>또한 이것을 알림으로 지속적인 모니터링이 없이 자동화를 하기 위하여 알림 방식을 채택을 하였습니다.</li>
</ul>
<h3 id="1-simple-notification-service-sns-접속">1. Simple Notification Service (SNS) 접속</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/0bea8249-70d7-403c-aa1c-cf068b527fb2/image.png" alt=""></p>
<ul>
<li>검색창에 Simple Notification Service을 입력을 하고 사이드 바에 주제 이후에 주제 생성을 클릭을 합니다.</li>
</ul>
<h3 id="2-주제-생성">2. 주제 생성</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/7e27dff0-564d-4337-8c9d-d38c6472e179/image.png" alt=""></p>
<ul>
<li>유형은 표준을 선택을 하고 이름을 입력을 합니다.</li>
<li>이후 주제 생성 버튼을 클릭하여 주제를 생성을 합니다.</li>
</ul>
<h3 id="3-cloudwatch-생성-경보">3. CloudWatch 생성 (경보)</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/a085ff95-0ab4-4be8-955e-922a932e7b97/image.png" alt=""></p>
<ul>
<li>검색창에 Cloudwatch를 선택을 하고 이후 경보 생성을 누릅니다.</li>
</ul>
<h3 id="4-지표-선택">4. 지표 선택</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/d2821056-7e7c-4f5b-a28a-31af0a354778/image.png" alt=""><img src="https://velog.velcdn.com/images/geon_km/post/7527db64-35a0-4ffa-8781-7e27dfabff7f/image.png" alt="이미지1"> <img src="https://velog.velcdn.com/images/geon_km/post/76537003-a164-44fe-8327-d6f496862e6e/image.png" alt="이미지2"><img src="https://velog.velcdn.com/images/geon_km/post/1abd45fb-6082-44d4-8d5a-c0cf1b69de3d/image.png" alt="이미지4"></p>
<ul>
<li>생성을 누르면 지표 및 조건 지정 페이지가 나옵니다. 이후 지표 선택을 누릅니다.</li>
<li>이후 EC2를 선택하고 이후 인스턴스별 지표를 선택을 합니다.</li>
<li>마지막으로 원하는 인스턴스의 CPUUtilization을 선택한 뒤 오른쪽 아래에 있는 지표 선택을 클릭해줍니다.
<img src="https://velog.velcdn.com/images/geon_km/post/350df179-9325-4014-9da6-adc71e1b0670/image.png" alt=""></li>
<li>이후 지표에서 위에 조건을 그대로 설정하고 조건 부분에서 임계값 유형에 정적을 선택하고 보다 큼을 설정을 합니다. 이후 임계값에 원하는 cpu 조건을 입력을 합니다. 저는 70을 설정하여 CPU가 70프로 이상 올라가면 알림을 받을 수 있게 설정을 하였습니다.</li>
</ul>
<h3 id="5-작업-구성">5. 작업 구성</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/8f327636-4fba-43f1-988f-658069eb6ed6/image.png" alt=""></p>
<ul>
<li>이후 경보 상태를 선택하고 기존의 SNS 주제 선택을 유지합니다. 이후 알림 발송 부분에 이전에 만들었던 SNS를 선택을 합니다.</li>
<li>알림 정보외에 아래에 있는 부가적인 정보들은 필요하시다면 추가 설정해주시고 다음을 클릭해주시면 됩니다.</li>
</ul>
<h3 id="6-slack-연동하기-webhook">6. Slack 연동하기 (webhook)</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/03bbf66d-a60c-4c94-9ea3-327933b98533/image.png" alt=""></p>
<ul>
<li>처음에 slack에 접속하여 원하는 채널을 하나 생성하고 상단 채널의 이름을 누릅니다. 누르면 설정 부분으로 넘어갑이다. 이후 통합을 눌러 앱 -&gt; 앱 추가를 누릅니다.</li>
<li>이후 webhooks를 검색창에 입력하면 다음과 같이 나오는데 여기에 보이는 Incoming Webhooks를 설치해줍니다.<img src="https://velog.velcdn.com/images/geon_km/post/f0b6d56b-d4ce-4852-a99b-f413819c858b/image.png" alt=""><img src="https://velog.velcdn.com/images/geon_km/post/dad6e4a3-d044-4c5b-8bd3-e7eabd489a65/image.png" alt=""></li>
<li>페이지가 넘어가면 아래쪽에 적용을 원하는 채널을 선택하고 수신 웹후크 통합 앱 추가를 선택하고 url을 확인을 할 수 있습니다.<blockquote>
<p>URL은 람다에서 변수로 사용을 하기 때문에 다른 파일에 저장</p>
</blockquote>
</li>
</ul>
<h3 id="7-lambda-함수-생성">7. Lambda 함수 생성</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/75f9f91b-f9a3-4772-8cfd-57e89c9ed930/image.png" alt=""></p>
<ul>
<li>이후 AWS 사이트의 검색창에 람다 -&gt; 함수 -&gt; 함수 생성을 누릅니다.
<img src="https://velog.velcdn.com/images/geon_km/post/0cae3a00-f689-4379-a160-058bb83ea557/image.png" alt=""></li>
<li>이후 새로 작성을 누르며 함수 이름 -&gt; 런타임은 노드 16을 선택하고 -&gt; 기본 람다 권한을 선택을 합니다.</li>
<li>람다가 생성이 되면 코드를 선택을 합니다. 이후 index.js에 다음과 같은 코드를 입력을 합니다.
<img src="https://velog.velcdn.com/images/geon_km/post/b8a0d121-ffd6-40fd-95e9-b69009e4ad80/image.png" alt=""><pre><code class="language-javascript">// 구성 -&gt; 환경변수로 webhook을 받도록 합니다.
const ENV = process.env
if (!ENV.webhook) throw new Error(&#39;Missing environment variable: webhook&#39;)
</code></pre>
</li>
</ul>
<p>const webhook = ENV.webhook;
const https = require(&#39;https&#39;)</p>
<p>const statusColorsAndMessage = {
    ALARM: {&quot;color&quot;: &quot;danger&quot;, &quot;message&quot;:&quot;위험&quot;},
    INSUFFICIENT_DATA: {&quot;color&quot;: &quot;warning&quot;, &quot;message&quot;:&quot;데이터 부족&quot;},
    OK: {&quot;color&quot;: &quot;good&quot;, &quot;message&quot;:&quot;정상&quot;}
}</p>
<p>const comparisonOperator = {
    &quot;GreaterThanOrEqualToThreshold&quot;: &quot;&gt;=&quot;,
    &quot;GreaterThanThreshold&quot;: &quot;&gt;&quot;,
    &quot;LowerThanOrEqualToThreshold&quot;: &quot;&lt;=&quot;,
    &quot;LessThanThreshold&quot;: &quot;&lt;&quot;,
}</p>
<p>exports.handler = async (event) =&gt; {
    await exports.processEvent(event);
}</p>
<p>exports.processEvent = async (event) =&gt; {
    const snsMessage = event.Records[0].Sns.Message;
    const postData = exports.buildSlackMessage(JSON.parse(snsMessage))
    await exports.postSlack(postData, webhook);
}</p>
<p>exports.buildSlackMessage = (data) =&gt; {
    const newState = statusColorsAndMessage[data.NewStateValue];
    const oldState = statusColorsAndMessage[data.OldStateValue];
    const executeTime = exports.toYyyymmddhhmmss(data.StateChangeTime);
    const description = data.AlarmDescription;
    const cause = exports.getCause(data);</p>
<pre><code>return {
    attachments: [
        {
            title: `[${data.AlarmName}]`,
            color: newState.color,
            fields: [
                {
                    title: &#39;언제&#39;,
                    value: executeTime
                },
                {
                    title: &#39;설명&#39;,
                    value: description
                },
                {
                    title: &#39;원인&#39;,
                    value: cause
                },
                {
                    title: &#39;이전 상태&#39;,
                    value: oldState.message,
                    short: true
                },
                {
                    title: &#39;현재 상태&#39;,
                    value: `*${newState.message}*`,
                    short: true
                },
                {
                    title: &#39;바로가기&#39;,
                    value: exports.createLink(data)
                }
            ]
        }
    ]
}</code></pre><p>}</p>
<p>// CloudWatch 알람 바로 가기 링크
exports.createLink = (data) =&gt; {
    return <code>https://console.aws.amazon.com/cloudwatch/home?region=${exports.exportRegionCode(data.AlarmArn)}#alarm:alarmFilter=ANY;name=${encodeURIComponent(data.AlarmName)}</code>;
}</p>
<p>exports.exportRegionCode = (arn) =&gt; {
    return  arn.replace(&quot;arn:aws:cloudwatch:&quot;, &quot;&quot;).split(&quot;:&quot;)[0];
}</p>
<p>exports.getCause = (data) =&gt; {
    const trigger = data.Trigger;
    const evaluationPeriods = trigger.EvaluationPeriods;
    const minutes = Math.floor(trigger.Period / 60);</p>
<pre><code>if(data.Trigger.Metrics) {
    return exports.buildAnomalyDetectionBand(data, evaluationPeriods, minutes);
}

return exports.buildThresholdMessage(data, evaluationPeriods, minutes);</code></pre><p>}</p>
<p>// 이상 지표 중 Band를 벗어나는 경우
exports.buildAnomalyDetectionBand = (data, evaluationPeriods, minutes) =&gt; {
    const metrics = data.Trigger.Metrics;
    const metric = metrics.find(metric =&gt; metric.Id === &#39;m1&#39;).MetricStat.Metric.MetricName;
    const expression = metrics.find(metric =&gt; metric.Id === &#39;ad1&#39;).Expression;
    const width = expression.split(&#39;,&#39;)[1].replace(&#39;)&#39;, &#39;&#39;).trim();</p>
<pre><code>return `${evaluationPeriods * minutes} 분 동안 ${evaluationPeriods} 회 ${metric} 지표가 범위(약 ${width}배)를 벗어났습니다.`;</code></pre><p>}</p>
<p>// 이상 지표 중 Threshold 벗어나는 경우 
exports.buildThresholdMessage = (data, evaluationPeriods, minutes) =&gt; {
    const trigger = data.Trigger;
    const threshold = trigger.Threshold;
    const metric = trigger.MetricName;
    const operator = comparisonOperator[trigger.ComparisonOperator];</p>
<pre><code>return `${evaluationPeriods * minutes} 분 동안 ${evaluationPeriods} 회 ${metric} ${operator} ${threshold}`;</code></pre><p>}</p>
<p>// 타임존 UTC -&gt; KST
exports.toYyyymmddhhmmss = (timeString) =&gt; {</p>
<pre><code>if(!timeString){
    return &#39;&#39;;
}

const kstDate = new Date(new Date(timeString).getTime() + 32400000);

function pad2(n) { return n &lt; 10 ? &#39;0&#39; + n : n }

return kstDate.getFullYear().toString()
    + &#39;-&#39;+ pad2(kstDate.getMonth() + 1)
    + &#39;-&#39;+ pad2(kstDate.getDate())
    + &#39; &#39;+ pad2(kstDate.getHours())
    + &#39;:&#39;+ pad2(kstDate.getMinutes())
    + &#39;:&#39;+ pad2(kstDate.getSeconds());</code></pre><p>}</p>
<p>exports.postSlack = async (message, slackUrl) =&gt; {
    return await request(exports.options(slackUrl), message);
}</p>
<p>exports.options = (slackUrl) =&gt; {
    const {host, pathname} = new URL(slackUrl);
    return {
        hostname: host,
        path: pathname,
        method: &#39;POST&#39;,
        headers: {
            &#39;Content-Type&#39;: &#39;application/json&#39;
        },
    };
}</p>
<p>function request(options, data) {
    return new Promise((resolve, reject) =&gt; {
        const req = https.request(options, (res) =&gt; {
            res.setEncoding(&#39;utf8&#39;);
            let responseBody = &#39;&#39;;</p>
<pre><code>        res.on(&#39;data&#39;, (chunk) =&gt; {
            responseBody += chunk;
        });

        res.on(&#39;end&#39;, () =&gt; {
            resolve(responseBody);
        });
    });

    req.on(&#39;error&#39;, (err) =&gt; {
        reject(err);
    });

    req.write(JSON.stringify(data));
    req.end();
});</code></pre><p>}</p>
<pre><code>
### 8. 환경 변수 추가
![](https://velog.velcdn.com/images/geon_km/post/bb4106d1-4666-4ccd-b97d-1d3fc72e9344/image.png)
- 상단의 구성 탭 → 좌측 환경 변수 메뉴에서 편집 버튼을 클릭해줍니다. 그리고 다음과 같이 키에는 webhook, 값에는 위에서 만든 웹후크 url을 입력해준 뒤 아래 저장 버튼을 클릭해줍니다.

![](https://velog.velcdn.com/images/geon_km/post/0176bd1f-43e4-4a30-bf9f-67238d5e0794/image.png)

### 9. SNS 트리거 추가
- 상단에 트리거 추가를 클릭해줍니다. 트리거 대상으로 sns를 선택해주시고 sns 주제로는 위에서 만들어둔 sns 주제를 선택한 뒤 추가 버튼을 클릭해주시면 됩니다.
![](https://velog.velcdn.com/images/geon_km/post/336c80b4-4ec7-4478-ab54-b1ace1358f5c/image.png)

### 10. 테스트
- 해당 EC2에 부화를 만들어 CPU를 증가시켜 정상적으로 SLACK 알림을 발송하는지 확인을 하겠습니다.
```bash
sudo apt-get install stress

stress --cpu 1 --timeout 600</code></pre><p><img src="https://velog.velcdn.com/images/geon_km/post/5bd1531e-03a3-4be8-82c9-1ff27fbd63d6/image.png" alt=""></p>
<ul>
<li>정상적으로 알림을 받을 수 있었습니다.</li>
</ul>
<h2 id="결론">결론</h2>
<ul>
<li>현재 CPU가 100%가 되어서 서버의 오류가 발생하는 문제를 해결하기 위하여 STOP AND START 방식을 선택을 하였습니다. </li>
<li>이때 지속적인 모니터링의 문제점을 제거하기 위하여 알림 발송을 하기 위하여 Cloudwatch를 사용을 하여 자동화를 하였습니다.</li>
<li>서비스의 트래픽이 몰려서 CPU가 증가를 하였을 때 문제를 해결할 수 있지만 STOP AND START 방식에 대한 불편함을 아직도 남아있습니다.</li>
</ul>
<p>[ 개선을 생각하는 부분 ] </p>
<ul>
<li>STOP AND START 방식을 유지하기 보다는 람다를 사용하여 Cloudwatch를 통해서 알림을 보낼 때 람다 트리거를 사용하여 서버를 자동적으로 stop and start를 하도록 명령을 하여 현재 문제점을 개선을 해야된다고 생각합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Github Actions + CodeDeploy로 CI/CD 구현하기]]></title>
            <link>https://velog.io/@geon_km/Github-Actions-CI-CodeDeploy%EB%A1%9C-CICD-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-vum9u82d</link>
            <guid>https://velog.io/@geon_km/Github-Actions-CI-CodeDeploy%EB%A1%9C-CICD-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-vum9u82d</guid>
            <pubDate>Mon, 02 Oct 2023 06:18:21 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<hr>
<p><img src="https://velog.velcdn.com/images/geon_km/post/98680914-05eb-487b-a519-52c0b82b7019/image.png" alt=""></p>
<ul>
<li>안녕하세요. 이번에는 Git Action으로 배포 자동화를 수행을 하였습니다.
이전에는 Jenkins를 통해서 배포 자동화를 하였지만 Git Action으로 배포 자동화를 수행을 하였습니다.</li>
<li>Jenkins에서 Git Action으로 변경한 이유는 일단 Jenkins는 설치형입니다. 따라서 2개의 workspace가 필요하며 배포 자동화를 하기 위해서 사용하기에는 불필요한 설정이 많기 때문에 Git Action으로 쉽게 자동화를 하기 위하여 변경을 하였습니다.</li>
<li><a href="https://pos04167.tistory.com/195">Jenkins 배포 자동화 정리</a></li>
</ul>
<h1 id="본론">본론</h1>
<hr>
<h2 id="배포-방식의-고민">배포 방식의 고민</h2>
<ol>
<li>Code Deploy를 통해서 배포 자동화 ( AWS DB 사용 )</li>
</ol>
<ul>
<li>위에 방식을 사용하면 GIT ACTION을 사용하여 쉽게 배포가 가능합니다. 하지만 프로젝트에서 MYSQL, REDIS, MONGODB를 사용하기 때문에 비용적인 문제가 있기 때문에 선택을 하지 않았습니다.</li>
</ul>
<ol start="2">
<li>Docker, Docker-compose를 통한 자동화</li>
</ol>
<ul>
<li>CD Deploy를 통하여 compose로 만들어 db의 문제를 쉽게 해결할 수 있지만 이번 프로젝트에서는 AWS CodeDeploy를 사용을 하고 싶어서 선택을 하지 않았습니다.</li>
</ul>
<ol start="3">
<li>2개의 workSpace Code Deploy를 통해서 배포 자동화</li>
</ol>
<ul>
<li><p>이번 프로젝트에서 선택한 방식입니다. 하나의 ec2에 mysql, redis, mongodb를 생성을 하고 다른 하나의 ec2에 배포를 하는 방식을 사용을 하였습니다. 물론 이 방식이 최선의 방식은 아니라고 생각을 했습니다. DB 인스턴스에 문제가 생긴다면 DB의 데이터의 손실의 문제가 발생할 수 있고 확장성에서 떨어지지만 Git Action을 쉽게 학습하고 AWS의 다양한 기능을 사용하기에 적합한 방식이라고 생각하여 이 방식을 선택을 하였습니다.</p>
<br/>

</li>
</ul>
<h2 id="1-ec2-생성">1. EC2 생성</h2>
<p><img src="https://velog.velcdn.com/images/geon_km/post/62d71dff-0221-489c-a0a5-f1144b5a351d/image.png" alt=""></p>
<ul>
<li>Ubuntu 20.04 버전을 선택을 하였습니다. </li>
<li>Ubuntu에서 22 버전이 있지만 명령어가 다르기 때문에 가장 익숙한 20.04 버전을 선택을 하였습니다.</li>
<li>이후 인스턴스 유형은 <code>small</code>로 설정을 하였습니다. 기존에 docker-compose로 배포를 하였을 때 자주 ec2가 다운이 되는 문제가 발생을 하여 <code>small</code>로 변경하고 <code>메모리 swap</code>을 수행을 하였습니다.</li>
</ul>
<blockquote>
<p>메모리 Swap</p>
</blockquote>
<pre><code class="language-bash">sudo dd if=/dev/zero of=/swapfile bs=128M count=32

sudo chmod 600 /swapfile

sudo mkswap /swapfile

sudo swapon /swapfile

sudo swapon -s

sudo vi /etc/fstab
--------------------
[ vi에 하단에 추가 ] 
/swapfile swap swap defaults 0 0
----------------------
## 용량 확인
free</code></pre>
<ul>
<li><p>이후 EC2 대시보드를 살펴보면 정상적으로 생성이 된 것을 볼 수 있습니다.</p>
</li>
<li><p>이후 탄력적 IP 설정, 태그 관리를 하겠습니다.
<img src="https://velog.velcdn.com/images/geon_km/post/c88c8384-17e4-4377-b80c-14bdbd9c0ddf/image.png" alt=""></p>
</li>
<li><p>사이드에 탄력적 IP를 선택하여 EC2에 할당을 하고 이후 태그를 선택을 합니다.</p>
</li>
<li><p>인스턴스 오른쪽 클릭 -&gt; 인스턴스 설정 -&gt; 태그 관리를 선택을 하여 태그를 생성을 합니다. -&gt; 이때 태그는 나중에 DEPLOY에서 선택을 할 때 사용을 합니다.
<img src="https://velog.velcdn.com/images/geon_km/post/e674d7ee-7cd4-4c0f-b077-b52e39c4c9db/image.png" alt=""></p>
<br/>

</li>
</ul>
<h2 id="2-ec2-접속--초기-설정">2. EC2 접속 &amp;&amp; 초기 설정</h2>
<ul>
<li><p>window에서 ec2를 접속하는 방법은 편리함을 주는 mobaxTerm을 자주 사용을 합니다.</p>
</li>
<li><p>여기서 mobaxTerm에서 접속하는 방법은 다음 2개가 있습니다.</p>
<h3 id="1-mobaxterm-ssh">1. mobaXterm SSH</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/e42cebd8-b06d-4e8a-9928-f0cf067a634e/image.png" alt=""></p>
<ul>
<li>mobaxTerm에서 session -&gt; ssh를 선택을 하면 다음과 같은 화면이 나옵니다. </li>
<li>Remote Host : 탄력적 IP </li>
<li>Specify username : aws linux를 사용하면 ec2-user / ubuntu를 사용하면 <code>ubuntu</code></li>
<li>use private key를 선택을 하여 ppm 키를 클릭을 하여 접속을 합니다.</li>
</ul>
<br/>

</li>
</ul>
<h3 id="2-aws-ssh">2. AWS SSH</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/3cc12ea2-ca45-486b-91aa-6ae99bddd289/image.png" alt=""></p>
<ul>
<li><p>ec2의 연결을 선택을 하면 다음과 같은 화면이 나옵니다. SSH 클라이언트를 선택하고 빨간색의 예를 복사하여 KEY가 있는 디렉토리로 이동을 하여 선택을 합니다.
<img src="https://velog.velcdn.com/images/geon_km/post/f6160998-d7da-45c4-8c3d-22580399ae15/image.png" alt=""></p>
<br/>

</li>
</ul>
<h3 id="3-초기-설정">3. 초기 설정</h3>
<pre><code class="language-bash"># 스냅샷 update
sudo apt update &amp;&amp; sudo apt upgrade

//서울 시간으로 세팅하기
timedatectl list-timezones | grep Seoul
sudo timedatectl set-timezone Asia/Seoul

# 자바 설치
sudo apt install openjdk-11-jdk

# 자바 버전 확인
java --version

# aws 가이드라인 문서
sudo apt install ruby-full
sudo apt install wget
cd /home/ubuntu
wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto &gt; /tmp/logfile
sudo service codedeploy-agent status</code></pre>
<ul>
<li>정상적으로 codeDeploy까지 완료를 하면 다음과 같은 화면이 나옵니다.
<img src="https://velog.velcdn.com/images/geon_km/post/41c9955d-3978-410d-a1d0-664cdecac322/image.png" alt=""></li>
</ul>
<blockquote>
<p>Ubuntu Server용 CodeDeploy 에이전트 설치 (공식 문서)
<a href="https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/codedeploy-agent-operations-install-ubuntu.html">https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/codedeploy-agent-operations-install-ubuntu.html</a></p>
</blockquote>
<h2 id="3-iam-설정">3. IAM 설정</h2>
<h3 id="역할-만들기">역할 만들기</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/889a1fde-de3a-468d-b2c7-7537d70cb6ae/image.png" alt=""></p>
<ul>
<li>역할을 선택하고 역할 만들기를 선택을 합니다.</li>
<li>이후 AWS 서비스를 선택을 하고 EC2를 선택을 합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/d0692fda-6a52-47de-b6ff-c48b63c050de/image.png" alt=""></p>
<ul>
<li>이후 역할을 추가하는 부분에 2개를 추가를 하고 생성을 합니다.<blockquote>
<p>AmazonS3FullAccess
AWSCodeDeployFullAccess
<img src="https://velog.velcdn.com/images/geon_km/post/88e94e64-03e5-4830-b97b-02324eb5384b/image.png" alt=""></p>
</blockquote>
</li>
</ul>
<br/>

<h3 id="역할-ec2에-연결하기">역할 EC2에 연결하기</h3>
<ul>
<li>IAM을 만들고 EC2랑 연결한다. EC2 홈에서 EC2 욱클릭 → 보안 → IAM역할 수정</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/4f574cce-69e5-4970-916d-e8c0fe98e4ba/image.png" alt=""></p>
<br/>

<h2 id="4-s3--iam-사용자">4. S3 &amp; IAM 사용자</h2>
<h3 id="s3-만들기">S3 만들기</h3>
<ul>
<li>기존에 S3와 만드는 방식과 똑같이 S3를 설정을 하면 됩니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/1101b8c9-89d2-4447-828f-e3675ec11b94/image.png" alt=""></p>
<h3 id="iam-사용자">IAM 사용자</h3>
<ul>
<li><p>IAM을 눌러 이전의 권한이 아닌 사용자를 누르고 생성을 합니다. 이름을 입력하고 다음을 누르면 역할 설정이 나옵니다.
<img src="https://velog.velcdn.com/images/geon_km/post/3957a6e8-2c9f-4aec-9240-b65582a75949/image.png" alt=""></p>
</li>
<li><p>2개를<code>AmazonS3FullAccess</code> , <code>AWSCodeDeployFullAccess</code>를 추가를 합니다. </p>
</li>
<li><p>이후 완료를 누르고 생성된 사용자를 눌러 보안 자격 증명 -&gt; 엑시스 키 만들기를 선택 -&gt; AWS 컴퓨팅 서비스에서 실행되는 애플리케이션 를 선택을 합니다. 추가 만들어진 키 2개를 따로 저장을 합니다.</p>
</li>
</ul>
<br/>

<h2 id="5-codedeploy">5. CodeDeploy</h2>
<h3 id="iam-역할">IAM 역할</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/0dc49a2d-bbc1-4f17-9fe7-5f89b315adaa/image.png" alt=""></p>
<ul>
<li>이전에 작성한 방법처럼 IAM의 역할을 추가를 합니다. 이때 기존의 방식과 다른 점은 사용 사례에서 CodeDeploy를 선택을 합니다. </li>
</ul>
<h3 id="codedeploy-애플리케이션">CodeDeploy 애플리케이션</h3>
<ul>
<li><p>이후 CodeDeploy를 선택을 하여 애플리케이션 &gt; 애플리케이션 생성을 선택을 합니다. 
<img src="https://velog.velcdn.com/images/geon_km/post/c07ab6ff-4033-40eb-b8a9-e3a67c5217dc/image.png" alt=""></p>
</li>
<li><p>이후 애플리케이션 CSTUDY_CODE_DEPLOY &gt; 배포 그룹 생성을 선택하여 서비스 역할에 서비스 역할 입력에 이전에 만들었던 IAM을 선택을 합니다.
<img src="https://velog.velcdn.com/images/geon_km/post/4c1e84cb-9ed5-4399-bdd9-f2ae27c44844/image.png" alt=""></p>
</li>
</ul>
<blockquote>
<p>여기까지 AWS 설정은 끝났습니다.</p>
</blockquote>
<br/>

<h2 id="6-github">6. Github</h2>
<h3 id="deployyaml">deploy.yaml</h3>
<p><a href="https://github.com/CS-tudy/CStudy_BackEnd">해당 프로젝트에서 확인을 할 수 있습니다.</a></p>
<ul>
<li>프로젝트에서 root의 위치에 ./github/workflows의 <code>deploy.yaml</code>을 추가를 합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/1ba41063-efc8-4b71-b96e-21196a286b97/image.png" alt=""></p>
<blockquote>
<p>deploy.yaml</p>
</blockquote>
<pre><code class="language-yaml">name: CI-CD

# Main 브랜치에 push를 하였을 때 
on:
  push:
    branches:
      - main

## 이전에 만들었던 S3, DEPLOY_NAME, GROUP_NAME을 ENV로 따로 변수로 설정을 합니다.
## RESOURCE_PATH는 현재 프로젝트가 멀티모듈로 구성이 되어서 모놀리직 구조이면 module-api를 삭제하고
## 적용을 하면 됩니다.
env:
  S3_BUCKET_NAME: s3-cstudy
  RESOURCE_PATH: ./module-api/src/main/resources/application.yml
  CODE_DEPLOY_APPLICATION_NAME: CODE-DEPLOY-CSTUDY
  CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: CODE-DEPLOY-CSTUDY-GROUP

jobs:
  build:
      # 어떤 OS에 실행이 되는지
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2

        # JDK 설치
      - name: Set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 11


## Git Action이 실행이 되면서 동적으로 application.yml에 변수로 주입을 합니다.
## 해당 변수는 Github Setting에서 설정을 할 수 있습니다.
## 밑에 사진으로 설명을 하겠습니다.
      - name: Set yaml file
        uses: microsoft/variable-substitution@v1
        with:
          files: ${{ env.RESOURCE_PATH }}
        env:
          spring.data.mongodb.uri: ${{ secrets.MONGODB_URL }}
          spring.redis.host: ${{ secrets.REDIS_HOST }}

          spring.datasource.url: ${{ secrets.MYSQL_URL }}
          spring.datasource.username: ${{ secrets.MYSQL_USERNAME }}
          spring.datasource.password: ${{ secrets.MYSQL_PASSWD }}

          spring.mail.username: ${{ secrets.MAIL_USERNAME }}
          spring.mail.password: ${{ secrets.MAIL_PASSWORD }}


          spring.security.oauth2.client.registration.google.client-id: ${{ secrets.GOOGLE_CLIENT_ID }}
          spring.security.oauth2.client.registration.google.client-secret: ${{ secrets.GOOGLE_SECRET }}

          spring.security.oauth2.client.registration.naver.client-id: ${{ secrets.NAVER_CLIENT_ID }}
          spring.security.oauth2.client.registration.naver.client-secret: ${{ secrets.NAVER_SECRET }}

          spring.security.oauth2.client.registration.kakao.client-id: ${{ secrets.KAKAO_CLIENT_ID }}

          jwt.secretKey: ${{ secrets.JWT_SECRET_KEY }}
          jwt.refreshKey: ${{ secrets.JWT_REFRESH_KEY }}

          cloud.aws.credentials.accessKey: ${{ secrets.AWS_CREDENTIALS_ACCESS_KEY }}
          cloud.aws.credentials.secretKey: ${{ secrets.AWS_CREDENTIALS_SECRET_KEY }}

          cloud.aws.s3.bucket: ${{ secrets.S3_BUCKET }}
          cloud.aws.region.static: ${{ secrets.AWS_REGION }}

        ## 권한을 주는 명령어
      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew
        shell: bash

        ## build를 하는 명령어
        ## 모놀리직 구조는 gradle build -x test
      - name: Build with Gradle
        run: ./gradlew clean :module-api:buildNeeded --stacktrace --info --refresh-dependencies -x test
        shell: bash

        ## Zip 파일 생성: 프로젝트를 압축하여 zip 파일 생성.
      - name: Make zip file
        run: zip -r ./$GITHUB_SHA .
        shell: bash

        ## AWS 자격 증명 구성: AWS 자격 증명 설정.
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

        ## S3에 업로드: 생성된 zip 파일을 S3 버킷에 업로드.
      - name: Upload to S3
        run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip

        ## Code Deploy: AWS CodeDeploy에 배포 생성.
      - name: Code Deploy
        run: |
          aws deploy create-deployment \
          --deployment-config-name CodeDeployDefault.AllAtOnce \
          --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
          --deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
          --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$GITHUB_SHA.zip</code></pre>
<br/>

<h3 id="applicationyml-변수-설정하기">Application.yml 변수 설정하기</h3>
<ul>
<li>프로젝트의 Repository의 설정 -&gt; Security -&gt; Action에서 New Repository를 선택을 하여 해당 yml 파일에 동적으로 주입을 할 변수를 입력을 합니다.
<img src="https://velog.velcdn.com/images/geon_km/post/e1430edc-2c1f-43a4-a84b-ec23d518592b/image.png" alt=""></li>
</ul>
<br/>

<h3 id="appspecyml">appspec.yml</h3>
<pre><code class="language-bash">
## source : 인스턴스 복사 디렉토리
## destination : 인스턴스에서 파일이 복사되는 위치
## overwrite : 복사할 위치에 파일이 있는 경우 대체
version: 0.0
os: linux
files:
  - source: /
    destination: /home/ubuntu/CStudy
    overwrite: yes

## object : 권한이 지정되는 파일 또는 디렉토리
permissions:
  - object: /
    pattern: &quot;**&quot;
    owner: ubuntu
    group: ubuntu

## 파일 설치 뒤, AfterInstall에서 기존에 실행되던 애플리케이션을 종료시키고, ApplicationStart에서 새로운 애플리케이션을 실행시킨다.
hooks:
  ApplicationStart:
    - location: scripts/gh_deploy.sh
      timeout: 60
      runas: ubuntu</code></pre>
<br/>

<h3 id="gh_deploysh">gh_deploy.sh</h3>
<pre><code class="language-shell">#!/bin/bash
PROJECT_NAME=&quot;CStudy&quot;
JAR_PATH=&quot;/home/ubuntu/CStudy/module-api/build/libs/*.jar&quot;
DEPLOY_PATH=/home/ubuntu/$PROJECT_NAME/
DEPLOY_LOG_PATH=&quot;/home/ubuntu/$PROJECT_NAME/deploy.log&quot;
DEPLOY_ERR_LOG_PATH=&quot;/home/ubuntu/$PROJECT_NAME/deploy_err.log&quot;
APPLICATION_LOG_PATH=&quot;/home/ubuntu/$PROJECT_NAME/application.log&quot;
BUILD_JAR=$(ls $JAR_PATH)
JAR_NAME=$(basename $BUILD_JAR)

echo &quot;===== 배포 시작 : $(date +%c) =====&quot; &gt;&gt; $DEPLOY_LOG_PATH

echo &quot;&gt; build 파일명: $JAR_NAME&quot; &gt;&gt; $DEPLOY_LOG_PATH
echo &quot;&gt; build 파일 복사&quot; &gt;&gt; $DEPLOY_LOG_PATH
cp $BUILD_JAR $DEPLOY_PATH

echo &quot;&gt; 현재 동작중인 어플리케이션 pid 체크&quot; &gt;&gt; $DEPLOY_LOG_PATH
CURRENT_PID=$(pgrep -f $JAR_NAME)

if [ -z $CURRENT_PID ]
then
  echo &quot;&gt; 현재 동작중인 어플리케이션 존재 X&quot; &gt;&gt; $DEPLOY_LOG_PATH
else
  echo &quot;&gt; 현재 동작중인 어플리케이션 존재 O&quot; &gt;&gt; $DEPLOY_LOG_PATH
  echo &quot;&gt; 현재 동작중인 어플리케이션 강제 종료 진행&quot; &gt;&gt; $DEPLOY_LOG_PATH
  echo &quot;&gt; kill -9 $CURRENT_PID&quot; &gt;&gt; $DEPLOY_LOG_PATH
  kill -9 $CURRENT_PID
fi

DEPLOY_JAR=$DEPLOY_PATH$JAR_NAME
echo &quot;&gt; DEPLOY_JAR 배포&quot; &gt;&gt; $DEPLOY_LOG_PATH
# 만약에 실제 서비스면
nohup java -jar $DEPLOY_JAR &gt;&gt; $APPLICATION_LOG_PATH 2&gt; $DEPLOY_ERR_LOG_PATH &amp;

sleep 3

echo &quot;&gt; 배포 종료 : $(date +%c)&quot; &gt;&gt; $DEPLOY_LOG_PATH</code></pre>
<h3 id="git-action-확인하기">Git Action 확인하기</h3>
<p><img src="https://velog.velcdn.com/images/geon_km/post/34f5fbf1-e890-4f6f-90ad-68a356d412a4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/geon_km/post/085c225f-f36a-49a8-9a7a-dd356777012a/image.png" alt=""></p>
<br/>

<h2 id="7-aws-확인하기">7. AWS 확인하기</h2>
<ul>
<li><p>빌드가 정상적으로 동작을 하였으면 AWS의 S3에 ZIP 파일이 정상적으로 로딩을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/geon_km/post/4ba18abc-6035-4d96-bf14-a4c848b0fde3/image.png" alt=""></p>
</li>
<li><p>이후 Git Action에서 배포 요청을 보내고 S3에서 codeDeploy에게 zip 파일을 전달하여 EC2에 배포하는 방식으로 배포 자동화를 하였습니다.
<img src="https://velog.velcdn.com/images/geon_km/post/3620b909-24f4-4f83-be7e-4f0e1dba7430/image.png" alt=""></p>
</li>
<li><p>실제 프로젝트에서 이걸 한번 확인하기 위해서 <code>lsof -i:8080</code>을 통해서 PORT가 정상적으로 동작이 되었는지 확인하고</p>
<pre><code>ubuntu@ip-172-XX-15-XXX:~/CStudy$ lsof -i:8080
COMMAND   PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
java    32787 ubuntu   32u  IPv6 100746      0t0  TCP *:http-alt (LISTEN)</code></pre></li>
<li><p><code>cat deploy.log</code>를 통해서 배포를 확인할 수 있고 <code>cat application.log</code>를 통하여 오류를 확인을 할 수 있습니다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/013f0003-0a1d-4a18-9d9f-a5537f472e53/image.png" alt=""></p>
<ul>
<li>이후 배포의 테스트를 위해서 임시로 String을 반환하는 Test Controller를 만들어서 CURL 요청을 통하여 확인하면 정상적으로 배포가 되었는지 알 수 있습니다.</li>
</ul>
<h1 id="결론">결론</h1>
<hr>
<ul>
<li><p>이전에 Jenkins를 통해서 배포 자동화를 하였을 때 현업자와 함께 스터디를 통해서 학습을 하다보니 막히는 부분에서 도움을 많이 받았다. 하지만 이번에는 혼자서 학습하고 배포 자동화를 해보니 많은 실패가 있었지만 자료가 많아서 해결할 수 있었다.</p>
</li>
<li><p>현재는 배포 자동화만 하였지만 다음에는 VPC 설정, 오토 스케일링,  Red-Green 무중단 배포를 학습하여 블로깅을 하겠다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ Github ] Organizations Push The requested URL returned error: 403]]></title>
            <link>https://velog.io/@geon_km/Github-Organizations-Push-The-requested-URL-returned-error-403</link>
            <guid>https://velog.io/@geon_km/Github-Organizations-Push-The-requested-URL-returned-error-403</guid>
            <pubDate>Mon, 11 Sep 2023 19:08:29 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<hr>
<ul>
<li>새로운 팀과 함께 프로젝트를 하면서 이전에 만났던 문제를 또 만났다. 이걸 팀원들에게 설명을 하였고 다음에 같이 일하는 분들에게 사용할 수 있게 문제를 해결하기 위하여 작성을 한다.</li>
</ul>
<h1 id="본론">본론</h1>
<hr>
<ul>
<li>프로젝트를 하면서 개인 repository에 개발을 하지 않고 보통 Organizations에서 처리를 합니다. 이때 push를 하면 다음과 같은 에러가 자주 발생한다.
<img src="https://velog.velcdn.com/images/geon_km/post/a0a94f56-1a5a-476d-9870-13a6df55db1e/image.png" alt=""></li>
<li>403을 보고 권한에 대한 문제라고 알게 되었고 이것을 해결하기 위해서 구글링을 했다. 구글에서 방법은 크게 2가지가 있었다.</li>
</ul>
<h2 id="1-git-remote를-다시-설정하기">1. git remote를 다시 설정하기</h2>
<pre><code class="language-github">$ git remote set-url origin https://&lt;username&gt;@github.com/&lt;user-name&gt;/&lt;repository-name&gt;.git</code></pre>
<p>아래와 같이 이름을 github앞에 다시 설정하여 push를 하는 방법이다.</p>
<p>하지만 나는 이 경우에도 똑같은 에러가 발생을 했다. 이 방식을 알아보니 Organization을 fork 한 Repository에 push를 할 경우에 필요한 방법이었고 나는 다시 url를 초기 값으로 설정을 하였다.</p>
<h2 id="2-권한-설정하기">2. 권한 설정하기</h2>
<p>권한은 일단 2개 설정이 가능하다. 프로젝트 처음에 Default 권한 설정하기, Repository에서 권한을 부여하기 관리자의 경우에는 1번 권한에 대한 설정이 가능하다. 기본적으로 Setting에 들어가서 좌축을 보면 Member Privileges가 있는데 아마도 처음에는 read로 설정이 되어있다. 나는 이것을 Write로 설정을 하였다.
<img src="https://velog.velcdn.com/images/geon_km/post/a58ac6d5-ac2a-485a-ab48-06d37c9605ab/image.png" alt="">
<img src="https://velog.velcdn.com/images/geon_km/post/1357d8a0-62dc-42a0-9c16-c7c6e9708d95/image.png" alt=""></p>
<blockquote>
<p>권한을 설정을 해서 해결이 되었다고 생각을 했는데... 아직도 오류가 나왔다. </p>
</blockquote>
<ul>
<li><img src="https://velog.velcdn.com/images/geon_km/post/ac4769f9-0c99-4cf1-8ebd-038634b9ad40/image.png" alt=""></li>
</ul>
<h2 id="3-자격-증명-관리자에서-github-삭제하기">3. 자격 증명 관리자에서 Github 삭제하기</h2>
<ul>
<li>많은 자료를 찾았지만 이 방식을 찾기가 엄청 어려웠다. </li>
<li>보통 많은 오류가 이 방식으로 해결을 하였고 만약에 위와 같은 오류가 발생하면 도전을 하는 것을 추천한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/c17ccad4-6262-4b60-b258-b874643cdb66/image.png" alt=""></p>
<ul>
<li>자격 증명을 보면 아마도 Git에 관련된 증명이 많이 있다. 나는 기존에 있는 자격을 모두 지우고 다시 push를 했다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/geon_km/post/44d46458-b26f-47bd-92f4-24d2b895a9be/image.png" alt=""></p>
<h2 id="참고">참고</h2>
<hr>
<p><a href="https://data-jj.tistory.com/49">https://data-jj.tistory.com/49</a></p>
<p><a href="https://docs.github.com/ko/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/setting-base-permissions-for-an-organization">https://docs.github.com/ko/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/setting-base-permissions-for-an-organization</a></p>
<p><a href="https://velog.io/@gillog/GitHub-Organization-Team-%EA%B4%80%EB%A6%AC">https://velog.io/@gillog/GitHub-Organization-Team-%EA%B4%80%EB%A6%AC</a></p>
]]></description>
        </item>
    </channel>
</rss>