<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발 일기</title>
    <link>https://writerroom.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 20 Aug 2026 16:03:52 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>flow123</managingEditor>
    <item>
      <title>파이썬으로 뉴스 피드 수집하기</title>
      <link>https://writerroom.tistory.com/385</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;최근 미국 주식을 조금씩 투자하면서, 세계 경제에 대해 관심을 갖게 되었다. 어느 때보다도 미국이 세계 경제의 중심에 서있고 트럼프의 정책 방향에 주가가 주도되는 것을 보고 있다. 경기의 흐름을 정확히 예측할 수는 없겠지만, 적어도 시장의 흐름을 이해는 하고 싶었다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; letter-spacing: 0px;&quot;&gt;슈카월드나, 언론사 공식 채널들이 매우 유용하지만, 나는 영상을 보는 것보다는 핵심 뉴스만 3분 안에 읽는 것을 선호한다&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그래서 파이썬으로 주요 매체 뉴스 피드를 크롤링하는 프로젝트를 만들게 되었다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt; 매일 관심 키워드를 주제로 뉴스를 구독하고, 신뢰할 만한 다양한 매체 (+ 정치 성향이 다양한) 에서 타이틀과 요약 중심으로 빠르게 훑는 사이트다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;기술 선택 (초기 셋업 단계여서 계속 추가 예정)&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 메인 언어 - Python : 크롤링에 적합 하기도 하고, 해당 코드는 저장/조회가 복잡하거나 엔티티가 많은 주제가 아니어서, Python 으로 간결하게 짜는 게 효율적이라고 생각했다. 지난 2년 반동안 주로 자바/코틀린만 해왔기 때문에, 파이썬으로 애플리케이션을 만들어보고 싶은 개인적인 바람도 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fast API 프레임워크와 SQL Alchemy 를 함께 사용했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Airflow - 선택한 주기에 맞게 Crawl&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1171&quot; data-start=&quot;1128&quot;&gt;&lt;span&gt;단순한 스케줄링으로는 Spring Scheduler로도 충분하다. 현업에서 많이 썼기 때문에 러닝 커브도 없었다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1253&quot; data-start=&quot;1172&quot;&gt;&lt;span&gt;&lt;b&gt;하지만 버전 관리, DAG 모니터링, UI 추적, 재실행/알림 등 더 정교한 기능을 제공하기에, &lt;/b&gt;Airflow를 선택했다&lt;/span&gt;&amp;nbsp;&lt;span data-state=&quot;closed&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://www.upsolver.com/blog/apache-airflow-when-to-use-it-when-to-avoid-it-while-building-a-data-lake?utm_source=chatgpt.com&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;medium.com&lt;/span&gt;&lt;span&gt;+14&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Docker&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;기능 구현&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트는 짧게 아래 사이클을 마무리하고, 추가 기능을 만들것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 프로젝트는 도커 세팅 (readme.md 참고)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. RSS 파서 구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 저장/조회 API 구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 프론트 구현&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 인프라 구축 및 운영 배포&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 스프링, 자바 만을 경험했다보니 그러한 관점에서 파이썬이 새롭게 느껴지는 부분을 자주 언급할 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 docker-compose.yml 파일 등 환경 세팅에 대해 짧게 소개해보겠다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;환경 세팅&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 docker 세팅은 read.me 를 참고하자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. postgres 서비스에서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;volumes 에 두 라인이 있다. 첫번째 라인은 컨테이너 최초 기동 시 1회 작동하며, 도커 postgres 가 참고해서 테이블을 생성하기 위함이다.&amp;nbsp;도커 컨테이너는 종료 시, 데이터 백업을 위해서 'postgres-db-volume:/var/lib/postgresql/data' 와&amp;nbsp; 같은 설정이 필수다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754228955831&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  postgres:
    image: postgres:13
    container_name: rss_postgres
    environment:
      POSTGRES_USER: rssuser
      POSTGRES_PASSWORD: rsspass
      POSTGRES_DB: rss
    ports:
      - &quot;5432:5432&quot;
    volumes:
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
      - postgres-db-volume:/var/lib/postgresql/data

  rss_api:
    image: apache/airflow:2.7.2
    container_name: rss_api
    environment:
      AIRFLOW__CORE__EXECUTOR: LocalExecutor
      AIRFLOW__CORE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres:5432/airflow
      AIRFLOW__CORE__FERNET_KEY: ''
      AIRFLOW__CORE__DAGS_ARE_PAUSED_AT_CREATION: 'false'
      AIRFLOW__CORE__LOAD_EXAMPLES: 'false'
    depends_on:
      - postgres
    volumes:
      - ./dags:/opt/airflow/dags
      - ./logs:/opt/airflow/logs
    command: webserver
    ports:
      - &quot;8080:8080&quot;
    env_file:
      - .env

  airflow-scheduler:
    image: apache/airflow:2.7.2
    container_name: rss_scheduler
    depends_on:
      - postgres
    volumes:
      - ./dags:/opt/airflow/dags
      - ./logs:/opt/airflow/logs
    command: scheduler

  pgadmin:
    image: dpage/pgadmin4
    container_name: pgadmin
    restart: always
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@example.com
      PGADMIN_DEFAULT_PASSWORD: admin123
    ports:
      - &quot;5050:80&quot;
    depends_on:
      - postgres

volumes:
  postgres-db-volume:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;env 파일은 spring 의 application.yml 처럼 DB 정보를 기입한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754229681921&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ENV=local

DB_USER=rssuser
DB_PASSWD=1234
DB_HOST=postgres
DB_PORT=5432
DB_NAME=db&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;load_dotenv()&lt;span&gt;는&lt;/span&gt; &lt;span&gt;호스트&lt;/span&gt; &lt;span&gt;머신&lt;/span&gt; &lt;span&gt;기준이다. env 파일이 컨테이너 내부 경로에 존재해야, &lt;/span&gt;env &lt;span&gt;파일의&lt;/span&gt; &lt;span&gt;내용을&lt;/span&gt; &lt;span&gt;자동으로&lt;/span&gt; &lt;span&gt;컨테이너&lt;/span&gt; &lt;span&gt;환경&lt;/span&gt; &lt;span&gt;변수로&lt;/span&gt; &lt;span&gt;전달할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1754229735226&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import os
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
from dotenv import load_dotenv

load_dotenv()  # .env 파일 불러오기

user = os.getenv(&quot;DB_USER&quot;)
passwd = os.getenv(&quot;DB_PASSWD&quot;)
host = os.getenv(&quot;DB_HOST&quot;)
port = os.getenv(&quot;DB_PORT&quot;)
db = os.getenv(&quot;DB_NAME&quot;)

DATABASE_URL = f&quot;postgresql://{user}:{passwd}@{host}:{port}/{db}&quot;

class Base(DeclarativeBase):
    pass&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;db.py&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너에 잘 등록되었는지 확인하려면, 아래 명령어로 확인해보자.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPuPN0/btsPF3TkPvy/W5qWYk2BVx4RY4geP2aaK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPuPN0/btsPF3TkPvy/W5qWYk2BVx4RY4geP2aaK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPuPN0/btsPF3TkPvy/W5qWYk2BVx4RY4geP2aaK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPuPN0%2FbtsPF3TkPvy%2FW5qWYk2BVx4RY4geP2aaK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;653&quot; height=&quot;170&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;212&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 아래에서 확인할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/jieun-dev1/rss&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/jieun-dev1/rss&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>flow123</author>
      <guid isPermaLink="true">https://writerroom.tistory.com/385</guid>
      <comments>https://writerroom.tistory.com/385#entry385comment</comments>
      <pubDate>Sun, 3 Aug 2025 23:04:24 +0900</pubDate>
    </item>
    <item>
      <title>스프링 배치로 병렬 처리 시 TaskExecutor 설정</title>
      <link>https://writerroom.tistory.com/384</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;환경: SpringBatch 4.3.7&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;상황&lt;/b&gt;&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;배치 잡을 실행할 때,&amp;nbsp; TaskExecutor 에 ThreadPoolTaskExecutor 를 적용하여 병렬로 동작한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이때, 풀사이즈를 조정하던 중에, 배치 Job 종료 이후에도 프로세스가 종료되지 않는 이슈가 있었다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot; data-darkreader-inline-color=&quot;&quot;&gt;원래 Job 을 로컬에서 실행할 때, Job Completed 가 된 후, Finished [Job 이름] 메시지와 함께 프로세스가 종료된다. 그런데 종료가 일어나지 않았고, &lt;/span&gt;프로세스에 SIGINT(인터럽트 시그널)를 보내 실행을 중단(interrupt)해야만 종료할 수 있었다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이를 해결하기 위해서 원인을 찾고, 풀사이즈를 조정한 내용을 정리해보았다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;분석&lt;/b&gt;&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 설정&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;    @Bean
    @JobScope
    fun saveTransactonStep() = stepBuilderFactory[&quot;saveTransactonStep&quot;]
		.chunk&amp;lt;TransactionDto, List&amp;lt;TransactionDto&amp;gt;&amp;gt;(chunkSize)
        .reader(transactionReader())
        .processor(transactionProcessor())
		.writer(transactionWriter())        
        .taskExecutor(customTaskExecutor())
        .throttleLimit(2)
        .build()


    @Bean
    fun customTaskExecutor(): ThreadPoolTaskExecutor {
        val executor = ThreadPoolTaskExecutor()
        executor.corePoolSize = 2
        executor.maxPoolSize = 2
        executor.initialize()
        return executor
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- throttleLimit: &lt;span style=&quot;color: #333333; text-align: start;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&amp;nbsp;&lt;/span&gt;Spring Batch에서 병렬 처리 시 동시에 실행되는 Tasklet 수를 제한하는 설정값. 최대 몇개의 thread 로 병렬 처리할 것인지를 의미한다. 이렇게 함으로써, DB 커넥션 등 리소스에 과부하가 걸리는 것을 방지한다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;In the case of an asynchronous taskExecutor the number of concurrent tasklet executions can be throttled(beyond any throttling provided by a thread pool) The throttle limit should be less than the data source pool size used in the job repository for this step.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- corePoolSize: 항상 유지되는 최소 스레드 수&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- maxPoolSize: 최대 스레드 수&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;개발 환경에서는 2로 설정해두었다. 실시간 확장성에 대응하는 게 중요하다면, max 와 core 에 차이를 두는게 유리할 것이다. 하지만 해당 배치는 1년에 6번 이하로실행되는 배치다. 정해진 데이터를 병렬 처리하며, 시스템 자원 역시도 EC2 인스턴스 1대로 고정되어 있어서&amp;nbsp; 자원을 고정했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;ThreadPoolTaskExecutor 는 어떻게 동작하는가?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Implementing this interface also indicates that the TaskExecutor.execute(Runnable) method will not execute its Runnable in the caller's thread but rather asynchronously in some other thread. Extended interface for asynchronous TaskExecutor implementations, offering support for Callable&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;execute(Runnable) 메서드를 호출하면 절대 호출 스레드에서 즉시 실행되지 않고, 다른 스레드 풀(worker thread)에서 비동기로 처리 내부적으로 비동기 쓰레드 풀을 운영한다.&amp;nbsp; ThreadPoolTaskExecutor는 AsyncTaskExecutor 인터페이스를 구현한다. 스레드풀은 한 번 만든 스레드를 재활용하기에, 스레드 생성&amp;middot;소멸 오버헤드가 거의 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot; data-darkreader-inline-color=&quot;&quot;&gt;StepBuilder 의 default task Executor 는 원래 단일 스레드 이다. (별도 정의를 하지 않는다면 동기 방식이다. 배치에서 ThreadPoolExecutor 를 사용 시, Tasklet 또는 chunk가&amp;nbsp;&lt;/span&gt;&lt;b&gt;다른 쓰레드에서 병렬로 실행 된다&lt;/b&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot; data-darkreader-inline-color=&quot;&quot;&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JStack 적용하여 상태 파악하기&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프로세스가 종료되지 않고 있다면, 특정 스레드가 문제가 되었을 것이다. JStack 을 써서, 덤프를 뜨고 현재 상태를 파악해보도록 하자.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Jstack 은 캡처 도구와 같이, 당시 순간의 상태만 캡쳐한다. EC2 에서 로그로 Job completed 가 뜨는 것을 확인한 후, 다음 명령어로 덤프를 만들었다. (현재 개발 환경은 EC2 에서 배치를 띄우므로, 개발 EC2 에 접속해서 확인했다.)&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;[batch@tax-return-batch]$which jstack&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;jps -l&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;[batch@tax-return-batch]$ jps -l&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;17968 batch-0.0.1-SNAPSHOT.jar&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;jstack 17968 &amp;gt; 0620-dump.txt&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Dump 모양은 대략 이렇다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;원인을 파악해보니, Non-Daemon thread 가 WAITING 상태로 남아있어서, JVM 이 종료가 안된것이 문제였다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;non-daemon 임을 알 수 있는 이유는, 다른 스레드들은 모두 HikariPool-2 housekeeper&quot; #51 daemon prio=5 o 처럼 prio 앞에 daemon 이 써있기 때문이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2025-06-22 13:09:54 Full thread dump OpenJDK 64-Bit Server VM (17.0.11+0 mixed mode, emulated-client, sharing)....&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;taxreturn-batch-thread1&quot; #57 prio=5 os_prio=31 cpu=619.51ms elapsed=66.51s tid=0x0000000159c05200 nid=0x9d13 waiting on condition [0x000000016ff56000]&lt;b&gt;&lt;u&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;java.lang.Thread.State: WAITING (parking)&lt;/u&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;at jdk.internal.misc.Unsafe.park(java.base@17.0.11/Native Method) - parking to wait for &amp;lt;0x000000070102b5b8&amp;gt; (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(java.base@17.0.11/LockSupport.java:341) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(java.base@17.0.11/AbstractQueuedSynchronizer.java:506) at java.util.concurrent.ForkJoinPool.unmanagedBlock(java.base@17.0.11/ForkJoinPool.java:3465) at java.util.concurrent.ForkJoinPool.managedBlock(java.base@17.0.11/ForkJoinPool.java:3436) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@17.0.11/AbstractQueuedSynchronizer.java:1625) at java.util.concurrent.LinkedBlockingQueue.take(java.base@17.0.11/LinkedBlockingQueue.java:435) at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@17.0.11/ThreadPoolExecutor.java:1062) at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@17.0.11/ThreadPoolExecutor.java:1122) at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@17.0.11/ThreadPoolExecutor.java:635) at java.lang.Thread.run(java.base@17.0.11/Thread.java:840)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;taxreturn-batch-thread2&quot; ...&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JVM 은 언제 종료될까?&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JVM은 &lt;b&gt;모든 non-daemon thread가 종료되어야&lt;/b&gt; 종료된다. daemon thread만 남아 있으면 JVM은 자동 종료 되지만,&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;실행 중인 non-daemon 이 있다면 shutdown() 호출을 통해 명시적으로 정리되어 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;즉 여기서는 Spring Context 종료 시, executor가 shutdown 되어야 하는 것이다. 그렇지 않으면 스레드가 남아서 jvm 종료가 되지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Since daemon threads are meant to serve user threads and are only needed while they are running, they won&amp;rsquo;t prevent the JVM from exiting once all user threads have finished their execution.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp;https://www.baeldung.com/java-daemon-thread&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해결&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재 corePoolSize 와 maxPoolSize 값이 동일하다. 현재 살아있는 스레드는 모두 코어스레드이고,&amp;nbsp;이들은 기본적으로&amp;nbsp;&lt;b&gt;keepAlive 시간과 관계없이 영원히 살아 있게 된다. ThreadPoolExecutor 에서&amp;nbsp;&lt;/b&gt;setAllowCoreThreadTimeOut(false)가 기본값이기 때문이다. 따라서&amp;nbsp;아래 두 개 설정을 추가한다. 아래 두 설정 추가만으로도 문제는 해결이 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;-&amp;nbsp; setAllowCoreThreadTimeOut(true)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 설정을 추가하면, 코어 스레드도 유휴 상태가 일정 시간(keepAliveSeconds) 이상 지속됨&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp;- setKeepAliveSeconds(10)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;스레드가&amp;nbsp;10초간&amp;nbsp;아무&amp;nbsp;일도&amp;nbsp;하지&amp;nbsp;않으면&amp;nbsp;종료되도록&amp;nbsp;설정&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;thread 가 작업 상태인 상황이 있을수 있으니, task 가 완료되기를 기다려줄 수 있도록 설정을 추가한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- setWaitForTasksToCompleteOnShutdown : 동작 중인 thread 강제 종료 하지 않고, 지정한 시간 동안 기다려줌.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- setAwaitTerminationSeconds&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1752396346320&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration 
class BatchConfiguration() {

@Bean
fun taskExecutor(): ThreadPoolTaskExecutor {
    val executor = ThreadPoolTaskExecutor()
    executor.corePoolSize = 2
    executor.maxPoolSize = 2
    executor.setAllowCoreThreadTimeOut(true)
    executor.setKeepAliveSeconds(10)
    executor.setWaitForTasksToCompleteOnShutdown(true) 
    executor.setAwaitTerminationSeconds(20)            
    executor.setThreadNamePrefix(&quot;batch-thread-&quot;)
    executor.initialize()
    return executor
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-darkreader-inline-color=&quot;&quot;&gt;테스트를 위해 ShutDown Logger 를 추가했다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1752396359574&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component 
class ShutdownLogger {
	@PreDestroy
	fun onShutDown() {
		println(&quot;[@PreDestroy] ThreadPoolTaskExecutor Shutdown hook at ${LocalDateTime.now()}&quot;)
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;keepAliveSeconds 에 10초로 제한하자, 10초가 지나고 shutDown 완료되었다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #953b34;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;u&gt;2025-06-22 13:28:40.619&lt;/u&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;INFO 9457 --- [&amp;nbsp; restartedMain] o.s.b.c.l.support.SimpleJobLauncher&amp;nbsp; &amp;nbsp; &amp;nbsp; : Job: [SimpleJob: [name=saveTaxReturnTransactionJob]] completed with the following parameters: [{=, toId=2100, masterId=69, run.id=414, year=2024, fromId=1500}] and the following status: [COMPLETED] in 7s726ms&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #953b34;&quot; data-darkreader-inline-color=&quot;&quot;&gt;  [ShutdownHook] JVM shutdown hook called&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;u&gt;&lt;span style=&quot;color: #953b34;&quot; data-darkreader-inline-color=&quot;&quot;&gt;2025-06-22 13:28:50.369&lt;/span&gt;&lt;/u&gt;&amp;nbsp; INFO 9457 --- [ionShutdownHook] d.t.c.c.c.EmbeddedRedisConfiguration &amp;nbsp; &amp;nbsp; : Embedded redis stopped&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #953b34; font-family: 'Noto Sans Light';&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;u&gt;  [@PreDestroy] ThreadPoolTaskExecutor Shutdown hook at 2025-06-22T13:28:50.490396&lt;/u&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;  [Spring] ApplicationContext is shutting down at 2025-06-22T13:28:50.490575&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2025-06-22 13:28:50.490&amp;nbsp; INFO 9457 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2025-06-22 13:28:50.491&amp;nbsp; INFO 9457 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource &amp;nbsp; &amp;nbsp; &amp;nbsp; : HikariPool-1 - Shutdown initiated...&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2025-06-22 13:28:50.495&amp;nbsp; INFO 9457 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource &amp;nbsp; &amp;nbsp; &amp;nbsp; : HikariPool-1 - Shutdown completed.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2025-06-22 13:28:50.495&amp;nbsp; INFO 9457 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource &amp;nbsp; &amp;nbsp; &amp;nbsp; : HikariPool-2 - Shutdown initiated...&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2025-06-22 13:28:50.495&amp;nbsp; INFO 9457 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource &amp;nbsp; &amp;nbsp; &amp;nbsp; : HikariPool-2 - Shutdown completed.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- Spring Batch 4.3.7 공식 문서&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Tech/배워서 남주기</category>
      <author>flow123</author>
      <guid isPermaLink="true">https://writerroom.tistory.com/384</guid>
      <comments>https://writerroom.tistory.com/384#entry384comment</comments>
      <pubDate>Sun, 13 Jul 2025 17:49:12 +0900</pubDate>
    </item>
    <item>
      <title>스프링 배치 병렬 처리 가능하도록 변경하기.</title>
      <link>https://writerroom.tistory.com/380</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이전 편: &lt;a href=&quot;https://writerroom.tistory.com/379&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://writerroom.tistory.com/379&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;상황&lt;/b&gt; &lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;공제 가맹점이라는 테이블 특성 상, Hit 가 적을 수 밖에 없다. (결제가 어떤 가맹점에서 일어날 지는 예측하기 어렵기 때문이다) &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-darkreader-inline-bgcolor=&quot;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #c8c3bc; text-align: start;&quot; data-darkreader-inline-color=&quot;&quot;&gt;가맹점의 연말 정산 유형을 매핑하는 과정은 성능 개선의 여지가 적다고 판단했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-darkreader-inline-bgcolor=&quot;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-darkreader-inline-bgcolor=&quot;&quot; data-darkreader-inline-color=&quot;&quot;&gt;Processor 에서 가맹점의 연말 정산 유형 확인, 할인 여부 등 정산에 필요한 다양한 정보를 조회한다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-darkreader-inline-bgcolor=&quot;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-darkreader-inline-bgcolor=&quot;&quot; data-darkreader-inline-color=&quot;&quot;&gt;이 작업을 단 건으로 처리하다보니, 병목이 되고 있어서 멀티 쓰레드에서 동시에 실행해서 처리 속도를 높여보기로 했다&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-darkreader-inline-bgcolor=&quot;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;병렬 처리&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;배치를 병렬로 구성할 때, 두 가지 방법을 생각해보았다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;코드 변경 포인트가 많은지, 코드 제어가 얼마나 가능한지 등을 중점적으로 보았다. &lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 149px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;방법&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;장점&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;단점&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 113px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 113px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;배치 코드 자체에 적용 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(Task Executor)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 113px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;-Spring Batch 내부에서 병렬 처리되므로 관리 용이&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;-배치 실행 중 실패 시 Spring Batch의 Retry &amp;amp; Skip 기능 활용 가능&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;-하나의 Job 안에서 컨트롤 가능&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 113px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Reader 등 모든 요소가 병렬 처리 가능하도록 변경 필요&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;'&amp;amp;' 을 사용하며&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;fromId-toId 로 물리적으로 데이터 범위 나눠 실행&amp;nbsp;&lt;br /&gt;(&lt;span style=&quot;font-family: 'Noto Sans Light'; background-color: #efefef; color: #333333; text-align: start;&quot; data-darkreader-inline-bgcolor=&quot;&quot; data-darkreader-inline-color=&quot;&quot;&gt;젠킨스 스크립트)&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위 방법보다 구현이 간단하다.&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;-상세한 코드 제어가 어려움&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;-&lt;a href=&quot;https://writerroom.tistory.com/379&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;(이전 글)&lt;/a&gt; preloadStep 에서 모든 가맹점 데이터를 레디스에 한번 올리고, 작업 후에 삭제한다. 여러 스레드에서 이 Step 을 병렬로 하다보면, 데이터 정합성 문제가 생긴다.&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2번 방식으로 하면, 아래와 같은 식으로 스크립트를 짜면 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&amp;amp; 연산자는 리눅스/유닉스 기반 쉘 스크립트에서 사용되며, 특정 명령어를 백그라운드에서 실행할 때 활용된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Jenkins 에서&amp;nbsp;&lt;b&gt;Build Step&lt;/b&gt;을 추가하면서, Execute Shell 에 아래와 같이 쓰면 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot; data-darkreader-inline-color=&quot;&quot;&gt;여러 프로세스를 병렬로 실행할 수&lt;span&gt;&amp;nbsp;있다&lt;/span&gt;&lt;/span&gt;.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741162430330&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java -jar batch-job.jar --fromId=1 --toId=10000 &amp;amp;
java -jar batch-job.jar --fromId=10001 --toId=20000 &amp;amp;
java -jar batch-job.jar --fromId=20001 --toId=30000 &amp;amp;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 위의 단점에서 언급한 것처럼, Redis 의 데이터를 사용 후 삭제 하는 과정에서 정합성 문제가 생긴다 (&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;font-family: 'Noto Sans Light'; background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://writerroom.tistory.com/379&quot; data-darkreader-inline-bgcolor=&quot;&quot; data-darkreader-inline-color=&quot;&quot;&gt;이전 글&lt;/a&gt;)&amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;병렬처리가 필요한 부분은 정산 내역을 저장하는 Step 이다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 위처럼 스크립트를 쓰면, Job 전체에 병렬이 적용된다.  따라서 정산 내역 저장 이후, Redis 를 저장/삭제하는 Step 도 함께 병렬 처리 된다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1번 스레드가 작업을 완료하고, 데이터를 지우고 있다고 해보자. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이때, 5번 스레드는 아직 작업 중이라면?&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1번 스레드가 가맹점 일부를 삭제해서, 5번 스레드는 해당 가맹점을 못찾는 문제가 발생 할 수 있다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Task Executor 방법으로 하면, 정산 내역 저장 Step 에만 병렬을 적용하면 된다. 따라서, 위의 문제는 일어나지 않는다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;병렬 처리에 필요한 작업&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1) 멤버 변수를 변경하는 작업이 있는가? 있다면, static 하지 않게 바꿔준다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;-&amp;gt; &amp;nbsp;현재 코드는 non-static 하게 구성되어 있어서, 바꿀 것이 없었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2) JPACursorItemReader &amp;rarr; JpaPagingItemReader 변경 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Cursor 는 기본적으로 싱글 스레드이다. DB 커서를 한개만 열기 때문이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;반면에, Paging 방식에선, 페이지네이션으로 여러 스레드가 다른 페이지에 접근할 수 있어서 병렬 처리가 가능하다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot; data-darkreader-inline-color=&quot;&quot;&gt;대신 paging 은 읽는 데이터가 늘어날 수록 offset 이 커져서, 속도가 느려지는 경향이 있다 (이때, page size 를 충분히 크게 하여.&amp;nbsp; commit 주기를 줄이면 도움이 된다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사실 두 리더 모두, 대량 처리에는 적합하지 않다고 분류된 글을 보았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 지난 작업에서 JpaCursorItemReader 를 사용했을 때, 읽기 시간이 20초 정도로 적었기 때문에 성능면에서는 문제가 없었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;읽기가 조금 늘어나더라도, 현재 Processor 가 핵심 병목이기 때문에, 감수할 수 있다고 판단했다. ZeroOffsetReader 같은 것을 별도로 생성하지는 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;코드&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1)Reader 가 병렬 처리 가능하도록 변경&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;무상태 처리를 해줘야, 멀티 쓰레드에서 사용 가능함. &lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;The implementation is thread-safe in between calls to &lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://docs.spring.io/spring-batch/docs/current/api/org/springframework/batch/item/support/AbstractItemCountingItemStreamItemReader.html#open(org.springframework.batch.item.ExecutionContext)&quot; data-darkreader-inline-color=&quot;&quot;&gt;AbstractItemCountingItemStreamItemReader.open(ExecutionContext)&lt;/a&gt;, but remember to use&amp;nbsp;&lt;b&gt;saveState=false&amp;nbsp;&lt;/b&gt;if used in a multi-threaded client (no restart available)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;출처: 스프링 공식 문서&amp;nbsp;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-darkreader-inline-color=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;As-Is&amp;nbsp;&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741161080165&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Bean
    @StepScope
    fun transactionReader(
        @Value(&quot;#{jobParameters[fromDate]}&quot;) fromDate: String = &quot;&quot;,
        @Value(&quot;#{jobParameters[toDate]}&quot;) toDate: String = &quot;&quot;
    ): JpaCursorItemReader&amp;lt;transactionDto&amp;gt; {
        return JpaCursorItemReaderBuilder&amp;lt;TransactionDto&amp;gt;()
            .name(&quot;transactionReader)
            .entityManagerFactory(entityManagerFactory)
            .queryString(
                &quot;&quot;&quot;
                    select new com.example.batch.business.taxreturn.dto.transactionDto(
                          member.id,
                          t.transactionAmount,
                          t.transactionType
                    )
                    from transaction t
                    join member 
              		on member.id = t.member_id 
                    where t.transaction_date between :fromDate and :toDate
                    &quot;&quot;&quot;.trimIndent()
            ).parameterValues(
                mapOf(
                    &quot;fromDate&amp;rdquo; to fromDate,
                    &quot;toDate&amp;rdquo; to toDate
                )
            )
            .build()
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;To-Be&amp;nbsp;&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JpaPagingItemReader 로 변경&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;saveState(false), pageSize 지정.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1741161047780&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Bean
    @StepScope
    fun transactionReader(
        @Value(&quot;#{jobParameters[fromDate]}&quot;) fromDate: String = &quot;&quot;,
        @Value(&quot;#{jobParameters[toDate]}&quot;) toDate: String = &quot;&quot;
    ): JpaPagingItemReader&amp;lt;TransactionDto&amp;gt; {

        return JpaPagingItemReaderBuilder&amp;lt;TransactionDto&amp;gt;()
            .name(&quot;transactionReader&quot;)
            .entityManagerFactory(entityManagerFactory)
            .queryString(
                &quot;&quot;&quot;
                    select new com.example.batch.business.taxreturn.dto.TransactionDto(
                          member.id,
                          t.transactionAmount,
                          t.transactionType
                    )
                    from transaction t
                    join member 
              		on member.id = t.member_id 
                    between :fromDate and :toDate
                    &quot;&quot;&quot;.trimIndent()
            ).parameterValues(
                mapOf(
                    &quot;fromDate&amp;rdquo; to fromDate,
                    &quot;toDate&amp;rdquo; to toDate
                )
            )
            .build()
            .saveState(false)
            .pageSize(pageSize)
            .build()
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이때, maxPoolSize 는 Hikari Pool Size 보다 적게 설정해야 SQL connection timeout 나지 않는다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741161264074&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Bean
    fun customTaskExecutor(): ThreadPoolTaskExecutor {
        val executor = ThreadPoolTaskExecutor()
        executor.corePoolSize = maxPoolSize
        executor.maxPoolSize = maxPoolSize
        executor.setThreadNamePrefix(&quot;taxreturn-batch-thread&quot;)
        executor.initialize()
        return executor
    }

    @Bean
    @JobScope
    fun saveTransactionStep() = stepBuilderFactory[&quot;saveTransactionStep&quot;]
        .chunk&amp;lt;TransactionDto, List&amp;lt;TransactionDto&amp;gt;&amp;gt;(chunkSize)
        .reader(transactionReader())
        .processor(transactionProcessor())
        .writer(transactionWriter(taxReturnTransactionRepository))
        .taskExecutor(customTaskExecutor())
        .throttleLimit(maxPoolSize)
        .build()&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-darkreader-inline-bgcolor=&quot;&quot; data-darkreader-inline-color=&quot;&quot;&gt;출처&amp;nbsp;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-darkreader-inline-bgcolor=&quot;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/docs/current/api/org/springframework/batch/item/database/JpaPagingItemReader.html&quot;&gt;https://docs.spring.io/spring-batch/docs/current/api/org/springframework/batch/item/database/JpaPagingItemReader.html&lt;/a&gt;&lt;/p&gt;</description>
      <category>Tech/배워서 남주기</category>
      <author>flow123</author>
      <guid isPermaLink="true">https://writerroom.tistory.com/380</guid>
      <comments>https://writerroom.tistory.com/380#entry380comment</comments>
      <pubDate>Sat, 16 Nov 2024 22:25:25 +0900</pubDate>
    </item>
    <item>
      <title>Redis Pipelining 으로 26만개 데이터 한번에 Insert 하기</title>
      <link>https://writerroom.tistory.com/379</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;문제 &lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2024년 국내 결제 건을 연말 정산 하여, 국세청에 제출한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결제팀에서는 보통 매일 정산을 하지만, 연말정산은 한 번에 1년치를 정산하기 때문에&lt;span style=&quot;color: #000000;&quot; data-token-index=&quot;1&quot;&gt; 배치의 소요 시간이 매우 길다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이때&lt;u&gt; 정산 내역을 만드는 배치의 소요 시간이 매우 길었다.&amp;nbsp;&amp;nbsp;&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;참고로 복잡한 비즈니스 로직을 수행하는 processor 에서 소모한 시간이 대부분이다. Reader/writer는 I/O 작업 위주라서 영향이 거의 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Read Time: 20초&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Process Time: 6시간 10분&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Write Time: 4분&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;긴 배치 시간은&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1)지속적인 모니터링으로 작업 효율성을 떨어트리며 (오후에 실행 했을 때, 퇴근 후에도 봐야 했음) ,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2) 최악의 상황에서는 다른 배치가 실행되지 못하는 위험이 있다 (현재&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;여러 배치가 한 EC2 인스턴스에서 실행되기 때문)&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot; data-token-index=&quot;0&quot;&gt;로직&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1)올해 국내 결제 내역 조회(Reader)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;u&gt;*2-5는 Processor&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2)결제 건의 가맹점 번호를 기반으로 공제 타입 확인&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3)이미 연말정산 된 거래인지 중복 확인&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4)할인,그룹, 거래 타입 분류 등 기타 상세 내역 확인&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;5)정산에 필요한 정보를 여러 테이블에서 조합&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;6)저장&amp;nbsp;(Writer)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4가지 Processor 로직 중에서, 2-5번은 여러 테이블을 조합하며 여러 조건이 포함된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이번 블로그에서는 2번 로직을 개선하는 과정을 정리했다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;접근&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;As-Is&lt;/b&gt;&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재는 DB 인덱스 칼럼 조회 방식이지만 Disk I/O 보다 빠른 메모리 조회로 변경하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;To-Be&lt;/b&gt;&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Job 시작 단계에서 전체 가맹점 데이터를 Redis에 미리 적재하는 Step 추가&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;각 결제 건에서는 &lt;b&gt;인메모리에서 바로 조회&lt;/b&gt;하도록 구조를 변경&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;인메모리 스토리지 Redis 를 선택한 이유&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1)가맹점 번호가 유일한 식별자여서 Redis key-value 구조와 잘 맞았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예시&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;scilab&quot;&gt;&lt;code&gt;key : 'merchant:012345' 
value:'{&quot;name&quot;:&quot;영동시장 반찬가게&quot;,&quot;businessRegisterNumber&quot;:&quot;100100100&quot;,&quot;type&quot;:&quot;market&quot;}'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2)데이터 속성 - 정합성 유지가 용이&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;가맹점 테이블은 원본 엑셀 데이터를 변환해서 DB 에 insert 하는 것으로, 한 번에 준비되며 이후에 변경되지 않는다. 데이터가 변경됨을 고려해 캐시 무효화 전략을 걱정할 필요가 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3)높은 재사용과 캐시 효과 기대&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1차 배치 실행 시 동일 가맹점에서 여러 결제 건이 발생한 경우가 많았다. 이에 캐시 효과도 있을 것으로 판단했다. 하지만, 26만건을 하나씩 적재하게 되면, 26만 번의 네트워크 왕복이(Round-Trip-Time) 발생하여 속도가 느려졌다. 이를 해결하기 위해 추가로&amp;nbsp;Redis Pipeline 을 적용했고, 3초로 시간을 단축할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Redis Pipelining 이란?&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;improving performance by issuing multiple commands at once without &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;waiting for the response to each individual command.&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;각 command 실행 &amp;rarr; 응답 대기를 반복하는 대신, 한번에 여러 명령을 실행하여 성능을 향상한다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Redis 는 클라이언트-서버 모델, 즉 요청/응답 프로토콜을 사용하는 TCP 서버이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;따라서, 클라이언트가 서버에 요청을 보내고, 서버로 부터 응답을 받는데 이때 대개 동기/블로킹 방식으로 작동한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; (참고로 레디스는 기본적으로 블로킹 I/O 방식을 사용하지만, 비동기/논블로킹 옵션도 제공)&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Pipelining 의 장점&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;RTT 개선 -&amp;gt; 네트워크 비용 감소&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;요청-응답을 할 때 걸리는 시간을 RTT(Round Trip Time) 이라고 한다. 파이프라이닝을 사용할 때, 요청/응답 서버는 클라이언트가 이전 응답을 아직 읽지 않았더라도, 새 요청을 처리할 수 있다. 즉 응답을 아예 받지 않고서도 서버에 여러 명령을 보낼 수 있고, 응답을 한번에 읽을 수 있다&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Redis 의 동작&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;레디스에서 명령을 실행하면, 뒷단에서는 아래 과정이 진행된다.&amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;자료 구조 접근 (list, set 등 value 로 쓰인 자료 구조를 read, update, or delete)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;응답 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Socket I/O&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이때 1/2번은 비용이 매우 적다. 레디스는 인메모리 스토리지 이므로, 디스크를 오가는 것보다 비용이 적기 때문이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 3번은 network 비용, 즉 socket I/O 가 일어나기 때문에 비용이 크다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9qcf4/btsMBhzNPsQ/lbLExolcoYKYZPjYx1JchK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9qcf4/btsMBhzNPsQ/lbLExolcoYKYZPjYx1JchK/img.png&quot; data-alt=&quot;실제 소켓 통신이 일어나는 과정 https://helloworld-88.tistory.com/215&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9qcf4/btsMBhzNPsQ/lbLExolcoYKYZPjYx1JchK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9qcf4%2FbtsMBhzNPsQ%2FlbLExolcoYKYZPjYx1JchK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;457&quot; height=&quot;318&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;874&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 소켓 통신이 일어나는 과정 https://helloworld-88.tistory.com/215&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;br /&gt;Socket I/O 를 이용하면, 시스템이 운영 체제 커널과 소통한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;네트워크를 통해 데이터를 보내거나 받을 때, 프로세스는 시스템 콜을 보낸다 (read(), write())&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이때 user space (프로그램이 실행되는) &amp;larr;&amp;mdash;&amp;rarr; Kernel space (OS 가 low-level 운영하는)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사이의 컨텍스트 스위칭이 발생하면서, System 이 잠시 멈춰야 하기 때문에, 속도 지연이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Pipelining 을 하면, 시스템 콜 한번에, 여러개의 command 가 적용되니 같은 시간동안 많은 쿼리를 보낼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;적용&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;(1) 적재하기&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이때, 많은 명령을 한 번에 보내는 것보다는 적절한 수로 나누어 보내야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;클라이언트가 Pipelining 으로 명령을 보낼 때, 서버는 응답을 큐에 넣어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메모리를 너무 많이 소모하지 않게, 네트워크 전송 시에도 적당한 양으로 나눠서(ex. 10000개) 배치로 보내야 하는 것이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 하면 속도는 큰 차이가 없지만, 10000개 만큼의 응답만 queue 하면 되므로, 추가 메모리 소모가 적다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741153357991&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;    @Bean
    @JobScope
    fun preloadMerchantStep(
    ) = stepBuilderFactory[&quot;preloadMerchantStep&quot;]
        .tasklet { _, _ -&amp;gt;
            for (i in merchants.indices step redisProperties.pipelineChunkSize) {
                val chunkedMerchants = merchants.subList(i, Math.min(merchants.size, i + redisProperties.pipelineChunkSize))

                redisTemplate.executePipelined { connection -&amp;gt;
                    val stringRedisConn = connection as StringRedisConnection
                    chunkedMerchants.forEach { merchant -&amp;gt;
                        val key = taxReturnTypeService.createCardAcceptorCodeKeyForMerchant(cardAcceptorCode = merchant.cardAcceptorCode)
                        stringRedisConn.setEx(
                            key, redisProperties.timeToLive, objectMapper.writeValueAsString(MerchantDetailDto.from(merchant)))
                    }
                }
                logger.info { &quot;Index ${i + 1} -${i + chunkedMerchants.size} 가맹점이 적재되었습니다&quot; }
            }
            RepeatStatus.FINISHED
        }.build()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;(&lt;/b&gt;&lt;b&gt;2) 삭제하기&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해당 Redis 인스턴스는 여러 프로젝트가 공용으로 사용하고 있다. 배치 실행 시에만 쓰이는 데이터이니, 사용 후에는 리소스 최적화를 위해 지워준다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저장 할 때는, 키를 알고 있기 때문에 setEx 를 호출해서 키와 TTL 을 설정한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;삭제할 때는 키를 찾아서 지워줘야 한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Redis의&amp;nbsp; Keys * 는 모든 키를 한번에 반환해서, 서버 부하가 발생할 수 있어서 운영환경에서 쓰면 안된다. 반면 Scan 은 Cursor 기반으로 지정된 count 만큼 점진적으로 삭제하며, 다음에 사용될 커서를 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이때 match 를 써서 패턴 기반으로도 검색 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1)do-while 블록에서, cursor가 0 이 될 때까지 반복한다. (시간 복잡도는 O(N). 즉 30만이다)&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2)Redis 커넥션을 통해 Scan 명령어를 실행하고, Cursor 를 반환한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741153475522&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    fun deleteKeysAfterJob() {
        val patterns = listOf(&quot;\\${MERCHANT_BY_CARD_ACCEPTOR_CODE_CACHE}_*&quot;, &quot;\\${MERCHANT_BY_CARD_CATEGORY_CODE_CACHE}_*&quot;)
        var deletedCount = 0
        patterns.forEach { pattern -&amp;gt;
            var cursor: String
            do {
                val scanOptions = ScanOptions.scanOptions().count(redisProperties.pipelineChunkSize.toLong()).match(pattern).build()
                val scanResult: Cursor&amp;lt;ByteArray&amp;gt; = redisTemplate.execute { connection -&amp;gt;
                    val stringRedisConn = connection as StringRedisConnection
                    stringRedisConn.scan(scanOptions)
                } as Cursor&amp;lt;ByteArray&amp;gt;

                val keysToDelete = mutableListOf&amp;lt;String&amp;gt;()

                scanResult.forEachRemaining { keyBytes -&amp;gt; keysToDelete.add(keyBytes.toString(Charsets.UTF_8)) }
                if (keysToDelete.isNotEmpty()) {
                    redisTemplate.executePipelined { connection -&amp;gt;
                        val stringRedisConn = connection as StringRedisConnection
                        keysToDelete.forEach { key -&amp;gt;
                            stringRedisConn.del(key)
                        }
                        null
                    }
                    deletedCount += keysToDelete.size
                }
                cursor = scanResult.cursorId.toString()
                logger.info { &quot;deleted key size: ${keysToDelete.size} &quot; }
                scanResult.close()
            } while (cursor != &quot;0&quot;)
        }
        logger.info { &quot;Total keys deleted: $deletedCount&quot; }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;제약&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;운영 환경에서는 약 10만 건의 거래 데이터가 존재했지만, 보안 정책 상 개발 환경에서는 유사한 규모의 데이터를 준비할 수 없었다. 별도의 스테이징 환경도 없어서, 실질적인 성능 확인은 운영 환경에서 배포 후 한 번만 가능했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;개발 환경 테스트 결과&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결제 건당 가맹점 매칭 로직 기준,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;V1: MySQL Inde 조건 조회 &amp;rarr; 평균 0.03 초&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;V2: Redis Key 조회 &amp;rarr; 평균 0.01 초 (3배의 성능 개선)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;운영 환경 결과 및 회고&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;운영 환경 에서는 성능 개선 효과가 거의 없었다. 실제&amp;nbsp;결제 건 중 공제 대상 가맹점과 매칭 되는 비율이 낮아서, 캐시 hit 이 적었기 때문이다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;따라서 2차로 병렬을 적용하여, 성능을 개선했다. (다음 편: &lt;a href=&quot;https://writerroom.tistory.com/380&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://writerroom.tistory.com/380&lt;/a&gt; )&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;출처&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/use/pipelining/&quot;&gt;https://redis.io/docs/latest/develop/use/pipelining/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis/pipelining.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-data/redis/reference/redis/pipelining.html&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://toss.tech/article/25301&quot;&gt;https://toss.tech/article/25301&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://helloworld-88.tistory.com/215&quot;&gt;https://helloworld-88.tistory.com/215&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Tech/배워서 남주기</category>
      <author>flow123</author>
      <guid isPermaLink="true">https://writerroom.tistory.com/379</guid>
      <comments>https://writerroom.tistory.com/379#entry379comment</comments>
      <pubDate>Mon, 4 Nov 2024 21:35:45 +0900</pubDate>
    </item>
    <item>
      <title>10년 뒤의 나</title>
      <link>https://writerroom.tistory.com/376</link>
      <description>&lt;div style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot;&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;10년 뒤 당신은 어떤 엔지니어가 되고 싶나요?&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;자소서 질문으로 자주 받았던 질문이다. 그런데 요즘은 꼭 10년은 아니어도, 내가 어떤 사람이 되어야 하는가 자주 생각한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;사실 비즈니스에 있었을 때는 이런 생각을 크게 하지 않았고, 주위에 하는 사람도 별로 없었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;- 모두가 아는 것이라고 할 지라도, 실수할만한 포인트를 항상 인지하고 치명적인 실수를 하지 않는 개발자가 되고 싶다.&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;(ex. gitignore, application.yml의 설정, ddl-auto 같이 실수하면 큰일나는 것들..)&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;- 공식 문서만으로 개발할 수 없더라도, 공식 문서를 자주 들여다보고 싶다&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;-&amp;gt; 정말 다행인 것은, 이제는 영어로 문서를 읽는 게 꽤 할만하다는 것이다. 코딩을 처음 시작할 때는 사람들이 너는 영어를 잘 하니까, 빠르게 읽겠다. 하고 부러워하곤 했다. 하지만 그건 전혀 사실이 아니다 (적어도 나는 그랬다) 영어 원어민이면,&amp;nbsp; 원서로 된 의학 논문을 쉽게 읽는가?&amp;nbsp; 읽기를 잘하려면 언어 뿐만 아니라 그 주제에 대해 잘 알고 있어야 한다.&amp;nbsp;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;- 내가 배운 것을, 블로그에 1년에 8편은 쓰고 싶다 (시작은, 완벽하지 않더라도, 부족하더라도 우선 정리하는 것에서 시작하고 싶다. 완벽한 글을 쓰려고 하면, 너무 오래 걸리고 결국 하지 않게 되었으니까.) &amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;-3년 뒤에는 내가 배운 것을 30분 이상 발표할 수 있는 사람이 되고 싶다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;지난 달 주말. 도커 실습 강연에 간 적이 있었다. 모의로 도커를 만들어보는 실습이었다. 미리 환경을 세팅해야 했기에, 강연장에 도착해서 랩탑을 열어 이것저것 깔고 있었는데. 퐁신하게 곱슬거리는 갈색 머리의 키 큰 남자가 크리스피 도넛 상자를 옆구리에 끼고 저벅 저벅 걸어왔다.&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;Would you like some&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #4d5156; text-align: left;&quot;&gt;Doughnuts? 도넛 먹을랭?&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;그 날 하루종일 도넛 좀 먹겠냐는 말을 30번은 들었던 것 같다. 그가 강의실에 들어오는 모든 사람에게 도넛을 권했기 때문이다 (사실 거의 먹는 사람이 없었는데 그는 굽히지 않고 한 명 한 명 다 물어봐주었다)&amp;nbsp;그의 이름은 아드리안이었는데, 실리콘 밸리에서 일하는 시니어 개발자였다. 잠시 한국에 머무는 동안 초청을 받아서 강연을 했고, 도커에 관심 있던 나는 별 생각없이 갔는데, 그의 강연은 올해 개발자로서 인상적인 순간 중 하나였다.&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;그는 '참 쉽죠?' 밥 아저씨처럼 말했다.&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;도커를 모르는 사람도 알아 듣게 쉬운 단어로 쏙쏙 들어오게 설명했다. 초등학생도 알아듣게 설명하라는 게 저런 말이구나. 그를 보며 깨달았다. 그는 수강생들 하나하나 눈 맞추면서 계속 이해했는지 확인했다. 기술적인 이해나 능숙함도 그렇지만, 나는 그가 타인을 배려하거나, 청중들이랑 계속 양방향으로 소통하는 게 정말 인상적이었다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;조금은 부끄럽기도 했다. 요즘 나는 일할 때, 마음의 여유가 정말 없기 때문이다.&amp;nbsp; 올해는 글도 많이 쓰지 못했다. 내가 실력이 늘어야, 다른 동료들을 도와줄 수 있는 품이 생길텐데. 마음같지 않아서 조급했다. 나는 저런 시니어가 될 수 있을까? 코딩을 잘하는 것도 잘하는것이지만, 마음에 공간이 있는 시니어가 되고 싶다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;일을 하다보면, 지칠 때가 종종 온다. 일이 많아서, 제대로 쉬지 못해서 이기도 하고, 성장하지 않는 기분이어서 이기도 하고, 자기 효능감을 느끼는 순간보다, 그렇지 않은 순간이 압도적으로 많아서 이기도 하다. 그래서 내가 왜 이 일을 해야 하고, 이게 어떤 의미가 있는지를 자주 생각한다. 얼마 전에는 내가 잘 알고 있는 송금 관련해서, 동료에게 설명해주는 시간이 있었는데. 내가 잘 알고 있었으니 편안했고, 내가 이 일을 잘한다는 확신이 있었고. 그게 참 기분이 좋았다.&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;그게 사람에게 일이 꼭 필요한 이유라고 생각한다. 내가 더 큰 존재에 기여하고 있다는 기분.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;지금 나는 서비스의 성장에만 집중하고 있지만, 추후에는, 개인의 성장에 기여하는 개발자로 조금씩 시간을 쌓아보고 싶다.&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;동료가 힘들어 보일 때, 커피 마시자고 시간을 내주는 사람이 되고 싶다. 그게 꼭 발표를 하거나, 훌륭한 사수가 되는 것이 아니더라도. 내가 배운 것들, 시행착오한 걸 잘 정리해보고. 내 일 조금 더 잘 파악해보고, 상대의 눈높이에 계속 맞춰서 설명하는 걸 잘하는 사람이 되고 싶다.&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;그게 내가 앞으로 가고 싶은 방향성이다.&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;</description>
      <author>flow123</author>
      <guid isPermaLink="true">https://writerroom.tistory.com/376</guid>
      <comments>https://writerroom.tistory.com/376#entry376comment</comments>
      <pubDate>Tue, 10 Sep 2024 21:02:01 +0900</pubDate>
    </item>
    <item>
      <title>코루틴 이해하기</title>
      <link>https://writerroom.tistory.com/371</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;팀 코드에는 다른 시스템에(ex. 결제 -&amp;gt; 회원 서비스) 메시지를 전달할 때, 코루틴을 활용한다. 해당 코드를 작업 하면서, 코루틴을 공부한 내용을 정리해두었다.&amp;nbsp; 코루틴을 이해하려면, 먼저 동기와 비동기를 이해해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;동기와 비동기&amp;nbsp;&lt;/span&gt;&lt;/h4&gt;
&lt;table id=&quot;a6193369-0f7d-4756-8737-c9a7135ecabe&quot; style=&quot;border-collapse: collapse; width: 101.163%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr id=&quot;9b9c3f16-7b07-4dbf-9f11-b7f807d1a987&quot;&gt;
&lt;td id=&quot;Zt&amp;#96;k&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;특성&lt;/span&gt;&lt;/td&gt;
&lt;td id=&quot;@rfL&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;동기 프로그래밍&lt;/span&gt;&lt;/td&gt;
&lt;td id=&quot;cNL|&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;비동기 프로그래밍&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2c217adc-1d6c-4571-b77e-7bbdbf79d812&quot;&gt;
&lt;td id=&quot;Zt&amp;#96;k&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;실행 순서&lt;/span&gt;&lt;/td&gt;
&lt;td id=&quot;@rfL&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;작업이 순차적으로 실행됨 (이전 작업 완료 이후)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;작업 간 순서가 중요하고, 결과가 즉시 필요할 때.&lt;/span&gt;&lt;/td&gt;
&lt;td id=&quot;cNL|&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;한 작업의 완료를 기다리지 않고, 다음 작업으로 넘어간다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;작업이 완료되면, 코루틴/콜백 등을 통해 처리.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;da3dfb68-9cb0-4612-8e42-c436da7f061c&quot;&gt;
&lt;td id=&quot;Zt&amp;#96;k&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;블로킹 여부&lt;/span&gt;&lt;/td&gt;
&lt;td id=&quot;@rfL&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;호출된 스레드를 차단함.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;완료될 때까지 호출된 스레드가 기다려야 함.&lt;/span&gt;&lt;/td&gt;
&lt;td id=&quot;cNL|&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;호출된 스레드를 차단하지 않고 즉시 제어 반환하므로,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;다른 작업을 그동안 진행함.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;70c45cbe-1910-4e0a-8481-aa7a37c06907&quot;&gt;
&lt;td id=&quot;Zt&amp;#96;k&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;코드 복잡성&lt;/span&gt;&lt;/td&gt;
&lt;td id=&quot;@rfL&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;비교적 간단하고 직관적&lt;/span&gt;&lt;/td&gt;
&lt;td id=&quot;cNL|&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;복잡하며 관리가 필요&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;0a72d498-1d18-4568-9a3c-1b60bfc55263&quot;&gt;
&lt;td id=&quot;Zt&amp;#96;k&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;응답성&lt;/span&gt;&lt;/td&gt;
&lt;td id=&quot;@rfL&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;작업 중 응답이 차단될 수 있음&lt;/span&gt;&lt;/td&gt;
&lt;td id=&quot;cNL|&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;작업 중에도 응답성을 유지함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;e8783a46-37e5-40b3-bafa-39547d2d1e8c&quot;&gt;
&lt;td id=&quot;Zt&amp;#96;k&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;적합한 작업&lt;/span&gt;&lt;/td&gt;
&lt;td id=&quot;@rfL&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;단순한 작업, 순차적 의존성이 있는 작업&lt;/span&gt;&lt;/td&gt;
&lt;td id=&quot;cNL|&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;외부시스템과의 통신/I/O 바운드 작업, UI 응답성 요구 작업&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;코루틴이란?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;코틀린에서 비동기 프로그래밍을 구현하는 방식이다. 공식 가이드의 서문에서는 코루틴을 이렇게 설명한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;A coroutine is not bound to any particular thread.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;It may suspend its execution in one thread and resume in another one.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Coroutines can be thought of as light-weight threads, but there is a number of important differences that make their real-life usage very different from threads.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;코루틴은 특정 스레드에서만 작동하지 않는다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;A 스레드에서 실행되다가 중단하고, B 스레드에서 실행할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;코루틴은 경량 스레드를 묶어놓은 것과 같다. (의역하자면) 하지만 코루틴이 동작하는 방법은 스레드와 매우 다르다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;If you write the same program using threads (remove runBlocking, replace launch with&amp;nbsp;&lt;br /&gt;thread, and replace&amp;nbsp;&amp;nbsp;delay with&amp;nbsp;&amp;nbsp;Thread.sleep), it will consume a lot of memory. Depending on your operating system, JDK version, and its settings, it will either throw an out-of-memory error or start threads slowly so that there are never too many concurrently running threads.&lt;/blockquote&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;코루틴은 JVM 스레드와 비교할 때, 리소스 소모량이 적다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;스레드는 메모리를 많이 소모한다.&amp;nbsp;코루틴을 스레드처럼 쓰면 OOM 을 발생시킬 것이다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;주요 개념&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Coroutine Scope (코루틴 스코프)&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;문서에서는 structured concurrency 라고 칭하는데,&amp;nbsp;코틀린이 코루틴을 다루는 방식은 구조적이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;- 코루틴은 특정 코루틴 스코프 에서만 정의/실행 될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;- 코루틴 스코프는 코루틴의 생명 주기를 관리한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;- 코루틴 스코프는 스코프 내의 모든 코루틴이 완료되어야, 완료할 수 있다. 이렇게, 코드에서 에러가 나는 것을 모두 감지하고 누락하는 경우가 없게 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;- launch: 코루틴 빌더&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;- delay: 스레드를 블록하지 않으면서 (non-blocking), 다른 코루틴들이 해당 스레드를 사용하게 해줌.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;- runBlocking: 코루틴 스코프의 구현체 중 하나.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp; &amp;nbsp;runBlocking 내의 모든 코루틴이 실행 완료될 때까지&amp;nbsp;스레드가 블록됨&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp; &amp;nbsp;runBlocking 은 자주 사용되지 않는데, 이는 스레드가 비싼 리소스이고, 블로킹 하는 것이 비효율적이기 때문이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;코루틴에서 비동기적으로 사용하려면 suspend 를 써야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;suspend 를 쓰면, 코드 실행을 suspend(중지) 했다가, resume(재개) 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;참고&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://kotlinlang.org/docs/coroutines-overview.html&quot;&gt;https://kotlinlang.org/docs/coroutines-overview.html&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1723698623012&quot; contenteditable=&quot;false&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bToj9t/hyWOfzcKgD/Dumf3OFdr5Y0uEvkAc83Sk/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400&quot; data-og-url=&quot;https://kotlinlang.org/docs/coroutines-overview.html&quot; data-og-source-url=&quot;https://kotlinlang.org/docs/coroutines-overview.html&quot; data-og-host=&quot;kotlinlang.org&quot; data-og-description=&quot;&quot; data-og-title=&quot;Coroutines | Kotlin&quot; data-og-type=&quot;website&quot; data-ke-align=&quot;alignCenter&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://kotlinlang.org/docs/coroutines-overview.html&quot; data-source-url=&quot;https://kotlinlang.org/docs/coroutines-overview.html&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;Coroutines | Kotlin&lt;/p&gt;
&lt;p style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;kotlinlang.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>flow123</author>
      <guid isPermaLink="true">https://writerroom.tistory.com/371</guid>
      <comments>https://writerroom.tistory.com/371#entry371comment</comments>
      <pubDate>Thu, 20 Jun 2024 20:11:04 +0900</pubDate>
    </item>
    <item>
      <title>Graceful Shutdown 이해하고 적용하기</title>
      <link>https://writerroom.tistory.com/365</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Graceful Shutdown 이란?&lt;/span&gt;&lt;/u&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;스프링 부트의 공식문서에서는 아래와 같이 설명한다. 해석해보면 이런 뜻이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;GracefulShutdown 은 애플리케이션 컨텍스트를 종료하고, 빈을 중단하는 초기 단계에서 일어난다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;중단하는 과정에서는 Grace Period 를 제공하는 타임아웃을 사용한다. Grace Period 동안 &lt;u&gt;기존 요청들은 실행을 마칠 수 있지만, 새로운 요청은 허용되지 않는다.&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;It occurs as part of closing the application context and is performed in the earliest phase of stopping SmartLifecycle beans. This stop processing uses a timeout which provides a grace period during which existing requests will be allowed to complete but no new requests will be permitted.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;스프링 부트의 기본 WAS 톰캣에서 처리하는 방식을 보자 &lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;(아래 공식 문서 참고)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;톰캣에서는 shutDownGracefully 메서드를 통해서 Graceful Shutdown 이 시작된다. 새로운 요청은 네트워크 레벨에서 막기 시작하며 (기존에 남아있던 커넥션에 들어오는 요청 포함), &lt;u&gt;Shutdown 이 완료 되면 애플리케이션이 알 수 있도록&amp;nbsp; CallBack 이 호출된다.&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;public void shutDownGracefully(GracefulShutdownCallback callback)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Initiates a graceful shutdown of the Tomcat web server. Handling of new requests is prevented and the given callback is invoked at the end of the attempt. The attempt can be explicitly ended by invoking stop(). Once shutdown has been initiated Tomcat will reject any new connections&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;적용&lt;/span&gt;&lt;/u&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;application.yml&amp;nbsp; 에 다음 설정을 추가한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;현재 사용 중인 스프링 부트 2.7.7 의 ShutDown-phase 의 기본값은 30초이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이렇게 시간을 설정해주면, 데드락처럼 서로를 무한으로 대기하는 상황을 막아준다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;server:
    shutdown: graceful

# 타임아웃 시간 설정 
spring
	lifecycle:
		timeout-per-shutdown-phase: 10s&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;테스트&lt;/span&gt;&lt;/u&gt;&lt;/h2&gt;
&lt;pre class=&quot;crystal&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

private val log = KotlinLogging.logger {}

@RestController
class TestController {
    @GetMapping(&quot;/sleep&quot;)
    fun sleep(@RequestParam millis: Long) {
        try {
            log.info { &quot;[Started] sleeping for &quot; + millis + &quot;millisec&quot; }
            Thread.sleep(millis)
        } catch (e: InterruptedException) {
            Thread.currentThread().interrupt()
            log.info { &quot;Interrupted during sleep&quot; }
        }
        log.info { &quot;[Completed] slept for &quot; + &quot;$millis&quot; + &quot;millisec&quot; }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프로세스 종료&lt;/span&gt;&lt;/u&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;- Kill - 9 (SIGKILL) {PID} 프로세스를 강제로 종료한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;- Kill - 15(SIGTERM) {PID} 프로세스를 Graceful 하게 종료한다. 종료하기 전에 리소스를 정리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;&amp;nbsp;PID 를 찾을 때는,&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;- lsof -i :{port 번호} :&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;graceful shutdown 적용 전&lt;/span&gt;&lt;/u&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;PID 확인:&amp;nbsp; lsof -i :8080&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;10초가 지나기 전에 SIGKILL 로 종료해보자. 작업을 마무리하지 못한채 종료된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;356&quot; data-origin-height=&quot;26&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uBrsz/btsHFnE279P/AAJOzFYjq8TDokuioLHarK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uBrsz/btsHFnE279P/AAJOzFYjq8TDokuioLHarK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uBrsz/btsHFnE279P/AAJOzFYjq8TDokuioLHarK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuBrsz%2FbtsHFnE279P%2FAAJOzFYjq8TDokuioLHarK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;356&quot; height=&quot;26&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;356&quot; data-origin-height=&quot;26&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;79&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bllaYe/btsHEj4GCES/dK1nIpeQHkBWs7sFJAnFi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bllaYe/btsHEj4GCES/dK1nIpeQHkBWs7sFJAnFi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bllaYe/btsHEj4GCES/dK1nIpeQHkBWs7sFJAnFi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbllaYe%2FbtsHEj4GCES%2FdK1nIpeQHkBWs7sFJAnFi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1242&quot; height=&quot;79&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;79&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;SIGTERM 으로 종료&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;4.png&quot; data-origin-width=&quot;289&quot; data-origin-height=&quot;23&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xr7bu/btsHExn5jn9/KJ1WJiXC9mFbAl3KJ2b6Pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xr7bu/btsHExn5jn9/KJ1WJiXC9mFbAl3KJ2b6Pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xr7bu/btsHExn5jn9/KJ1WJiXC9mFbAl3KJ2b6Pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxr7bu%2FbtsHExn5jn9%2FKJ1WJiXC9mFbAl3KJ2b6Pk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;289&quot; height=&quot;23&quot; data-filename=&quot;4.png&quot; data-origin-width=&quot;289&quot; data-origin-height=&quot;23&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Exception 발생이 추가되긴 하지만, 역시 작업을 중단하고 종료가 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;5.png&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;97&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ysv2I/btsHEkbu2IE/kdcsMORsMcpiHRoDIVzSl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ysv2I/btsHEkbu2IE/kdcsMORsMcpiHRoDIVzSl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ysv2I/btsHEkbu2IE/kdcsMORsMcpiHRoDIVzSl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fysv2I%2FbtsHEkbu2IE%2FkdcsMORsMcpiHRoDIVzSl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1266&quot; height=&quot;97&quot; data-filename=&quot;5.png&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;97&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;graceful shutdown 적용 후&lt;/span&gt;&lt;/u&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;10초 동안 sleep 을 걸었는데, 만약 10초의 작업이 끝나기 전에 프로세스 종료 요청이 온다면 어떻게 될까? (&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;요청이 종료된 이후, Shutdown을 완료한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSjkvk/btsHEjwR8CD/sK1Kok4WfgdhCqYHyYJJsk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;91&quot; data-filename=&quot;6.png&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;위의 결과는 sleep 시간보다 shutdown phase가 길거나 같을 때에 해당한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;10초동안 Sleep 을 걸었는데, shutdown phase 가 그보다 짧은 5초라면 어떨까?&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;7.png&quot; data-origin-width=&quot;1427&quot; data-origin-height=&quot;93&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sGqZW/btsHENRJcX0/zzJyjr6xga4AeyCgdZ2ZWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sGqZW/btsHENRJcX0/zzJyjr6xga4AeyCgdZ2ZWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sGqZW/btsHENRJcX0/zzJyjr6xga4AeyCgdZ2ZWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsGqZW%2FbtsHENRJcX0%2FzzJyjr6xga4AeyCgdZ2ZWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1427&quot; height=&quot;93&quot; data-filename=&quot;7.png&quot; data-origin-width=&quot;1427&quot; data-origin-height=&quot;93&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;br /&gt;5초 간의 타임아웃 이후에도 여전히 bean 이 살아있다. 따라서 하나 이상의 요청이 살아있기 때문에, Graceful shutdown 이 중단된다. 즉, 살아있는 요청을 완료하는 걸 우선시하기 때문에, 데이터 손실이나 정합성이 깨지는 것을 막을 때 유리하다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;출처&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/reference/web/graceful-shutdown.html&quot;&gt;https://docs.spring.io/spring-boot/reference/web/graceful-shutdown.html&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/api/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.html&quot;&gt;https://docs.spring.io/spring-boot/api/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.html&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://medium.com/@arneg0shua/파일-업로드-도중-배포를-진행해도-괜찮을까-graceful-shutdown-적용편-453ae9f29dd1&quot;&gt;https://medium.com/@arneg0shua/파일-업로드-도중-배포를-진행해도-괜찮을까-graceful-shutdown-적용편-453ae9f29dd1&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://velog.io/@dongvelop/Springboot-Graceful-Shutdown&quot;&gt;https://velog.io/@dongvelop/Springboot-Graceful-Shutdown&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java&amp;amp;Spring/Spring</category>
      <author>flow123</author>
      <guid isPermaLink="true">https://writerroom.tistory.com/365</guid>
      <comments>https://writerroom.tistory.com/365#entry365comment</comments>
      <pubDate>Mon, 27 May 2024 09:26:31 +0900</pubDate>
    </item>
    <item>
      <title>Unix 커맨드라인 다루기</title>
      <link>https://writerroom.tistory.com/359</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;팀에서는 파일을 다루는 업무가 많기 때문에, 파일을 읽고, 조회하고, 리눅스 명령어를 다룰 일이 많다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스는 유닉스 계열 운영체제이다. 쉘은 유닉스 및 리눅스 시스템에서 사용자가 시스템과 상호작용하게 해준다. 쉘을 통해 파일 시스템을 탐색하고, 파일을 열고, 편집하고, 프로그램을 실행할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;u&gt;쉘 이란?&lt;/u&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉘은 사용자가 입력한 명령을 해석해 커널에 전달하거나, 커널의 처리 결과를 사용자에게 전달하는 역할을 한다 (&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://velog.io/@jinyongp/%EC%89%98-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&quot;&gt;쉘 스크립트 프로그래밍 (velog.io)&lt;/a&gt;)&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;u&gt;Shebang 이란?&lt;/u&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;셔뱅&lt;/b&gt;(shebang)은&amp;nbsp;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%ED%95%B4%EC%8B%9C_%EA%B8%B0%ED%98%B8&quot;&gt;해시 기호&lt;/a&gt;와&amp;nbsp;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%8A%90%EB%82%8C%ED%91%9C&quot;&gt;느낌표&lt;/a&gt;(#!)로 이루어진 문자 시퀀스로,&amp;nbsp;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8_%EC%96%B8%EC%96%B4&quot;&gt;스크립트&lt;/a&gt;의 맨 처음에 온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%9C%A0%EB%8B%89%EC%8A%A4_%EA%B3%84%EC%97%B4&quot;&gt;유닉스 계열&lt;/a&gt;&amp;nbsp;운영 체제에서 셔뱅이 있는 스크립트는 프로그램으로서 실행되며,&amp;nbsp;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%A1%9C%EB%8D%94_(%EC%BB%B4%ED%93%A8%ED%8C%85)&quot;&gt;프로그램 로더&lt;/a&gt;가 스크립트의 첫 줄의 나머지 부분을 인터프리터 지시자(interpreter directive)로 구문 분석한다. 즉, 지정된 인터프리터 프로그램이 대신 실행되어 스크립트의 실행을 시도할 때 처음 사용되었던 경로를 인수로서 넘겨주게 된다.이를테면 스크립트의 경로가&amp;nbsp;ideaProjects/prep/02_commandline/sh_script.sh이고 다음의 줄로 시작한다면:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719841225615&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*bin/bash 경로의 본 쉘 또는 호환 쉘을 이용하여 파일을 실행한다는 뜻.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 사용하려는 명령어 해석기가 무엇인지 알려주는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램 로더는 프로그램&amp;nbsp;/bin/sh를 대신 실행하되 위의 경로를 첫 번째 인수로 넘겨준다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;u&gt;변경권한 주기&lt;/u&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 생성만 했다면, 실행 시 접근 거부되는 경우가 된다. 실행 권한 추가가 필요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-03 오후 10.31.32.png&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/el3Yap/btsIj6i0VTf/fcKa4kS3w0Gek3DQBTMhF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/el3Yap/btsIj6i0VTf/fcKa4kS3w0Gek3DQBTMhF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/el3Yap/btsIj6i0VTf/fcKa4kS3w0Gek3DQBTMhF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fel3Yap%2FbtsIj6i0VTf%2FfcKa4kS3w0Gek3DQBTMhF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;824&quot; height=&quot;142&quot; data-filename=&quot;스크린샷 2023-06-03 오후 10.31.32.png&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;142&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 확인&lt;/p&gt;
&lt;pre id=&quot;code_1719841323192&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ls-al&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;user(사용자),group(그룹) ,other (다른 사용자) 권한&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소유권 (Owners) : 특정 파일 또는 디렉터리를 소유하는 권리&lt;/li&gt;
&lt;li&gt;허가권 (Permissions) : 특정 파일 또는 디렉터리의 접근 권한을 설정 하는 권리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WGsG8/btsIkgr7uIy/IH91QlYhZl7H7opvK60KTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WGsG8/btsIkgr7uIy/IH91QlYhZl7H7opvK60KTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WGsG8/btsIkgr7uIy/IH91QlYhZl7H7opvK60KTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWGsG8%2FbtsIkgr7uIy%2FIH91QlYhZl7H7opvK60KTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;606&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 권한 확인&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-04 오후 5.30.09.png&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/egAnEM/btsIkPni0c2/UbKmE47VESCNhLpbXtW490/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/egAnEM/btsIkPni0c2/UbKmE47VESCNhLpbXtW490/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/egAnEM/btsIkPni0c2/UbKmE47VESCNhLpbXtW490/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FegAnEM%2FbtsIkPni0c2%2FUbKmE47VESCNhLpbXtW490%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;864&quot; height=&quot;136&quot; data-filename=&quot;스크린샷 2023-06-04 오후 5.30.09.png&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-04 오후 5.38.18.png&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;76&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G8aKJ/btsIjfm7EeH/pCbfYTp4fnsonrHycUY6G0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G8aKJ/btsIjfm7EeH/pCbfYTp4fnsonrHycUY6G0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G8aKJ/btsIjfm7EeH/pCbfYTp4fnsonrHycUY6G0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG8aKJ%2FbtsIjfm7EeH%2FpCbfYTp4fnsonrHycUY6G0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;722&quot; height=&quot;76&quot; data-filename=&quot;스크린샷 2023-06-04 오후 5.38.18.png&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;76&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) [문자모드] 실행 속성 추가&lt;/p&gt;
&lt;pre id=&quot;code_1719841527396&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;chmod +x sh_script.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;chmod: 파일이나 디렉터리(directory)의 권한 (permission) 을 변경하는 명령어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;r은 읽기(read), w는 쓰기(write), x 는 실행(execution)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-04 오후 5.18.44.png&quot; data-origin-width=&quot;942&quot; data-origin-height=&quot;68&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d174Ov/btsIihzcmXP/MFSj3EQ9olGKMxcpPfTc7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d174Ov/btsIihzcmXP/MFSj3EQ9olGKMxcpPfTc7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d174Ov/btsIihzcmXP/MFSj3EQ9olGKMxcpPfTc7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd174Ov%2FbtsIihzcmXP%2FMFSj3EQ9olGKMxcpPfTc7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;942&quot; height=&quot;68&quot; data-filename=&quot;스크린샷 2023-06-04 오후 5.18.44.png&quot; data-origin-width=&quot;942&quot; data-origin-height=&quot;68&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) [숫자모드]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능한 경우의 수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;r: 4 w: 2 x:1&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rwx = 4+2+1 = 7&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rw- = 4+2+0 = 6&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;r-x = 4+0+1= 5&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;r&amp;mdash; = 4+0+0 = 4&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-wx = 0+2+1 = 3&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-w- = 0+2+0 = 2&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;mdash;x = 0+0+1 = 1&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 bash_script.sh.txt 의 경우, bash와 sh 파일의 사용자에게 모든 권한 / 그룹과 다른 사용자는 읽기 권한만&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;VIM 으로 파일 편집할 때, 유용한 단축키&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-04 오후 10.24.57.png&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9co12/btsIkM5bjCG/0x6EDK7Dp2wUuERXJDsQm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9co12/btsIkM5bjCG/0x6EDK7Dp2wUuERXJDsQm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9co12/btsIkM5bjCG/0x6EDK7Dp2wUuERXJDsQm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9co12%2FbtsIkM5bjCG%2F0x6EDK7Dp2wUuERXJDsQm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;402&quot; height=&quot;130&quot; data-filename=&quot;스크린샷 2023-06-04 오후 10.24.57.png&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;실행 취소: u&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 앞으로: ctr + r&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백스페이스 대체 삭제&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커서 위에서 문자열 삭제: x&lt;/li&gt;
&lt;li&gt;전체 라인 삭제: dd&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 사항 저장&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Insert &amp;rarr; CommandMode: Esc&lt;/li&gt;
&lt;li&gt;&amp;lsquo;:w&amp;rsquo; + Enter&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vim 나가기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Esc &amp;rarr; command 모드 진입&lt;/li&gt;
&lt;li&gt;:q + Enter&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마우스 대체&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 내에서 라인의 시작으로 이동: 0&lt;/li&gt;
&lt;li&gt;파일 내에서 라인의 끝으로 이동: $&lt;/li&gt;
&lt;li&gt;파일 내에서 제일 처음 라인으로 이동: gg&lt;/li&gt;
&lt;li&gt;파일 내에서 제일 끝 라인으로 이동: G&lt;/li&gt;
&lt;li&gt;한 단어 만큼 앞으로 이동: w&lt;/li&gt;
&lt;li&gt;한 단어 만큼 뒤로 이동: b&lt;/li&gt;
&lt;li&gt;라인 넘버 표시: :set number&lt;/li&gt;
&lt;li&gt;라인 넘버 표시 취소: :set nonu&lt;/li&gt;
&lt;li&gt;한 문장 끝으로 이동: ctrl + -&amp;gt;&lt;/li&gt;
&lt;li&gt;파일 내에서 단어 검색
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Esc 로 command mode 진입&lt;/li&gt;
&lt;li&gt;검색어 뒤에 &amp;lsquo;/&amp;rsquo; + Enter&lt;/li&gt;
&lt;li&gt;다음 발생 위치로 넘어가려면 n&lt;/li&gt;
&lt;li&gt;다음 발생 위치로 넘어가려면 N&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/셔뱅&quot;&gt;https://ko.wikipedia.org/wiki/셔뱅&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.lesstif.com/lpt/linux-chmod-93127208.html&quot;&gt;https://www.lesstif.com/lpt/linux-chmod-93127208.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스의 파일 권한 읽는 법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tttap.tistory.com/246&quot;&gt;https://tttap.tistory.com/246&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.freecodecamp.org/news/bash-scripting-tutorial-linux-shell-script-and-command-line-for-beginners/&quot;&gt;https://www.freecodecamp.org/news/bash-scripting-tutorial-linux-shell-script-and-command-line-for-beginners/&lt;/a&gt;&lt;/p&gt;</description>
      <author>flow123</author>
      <guid isPermaLink="true">https://writerroom.tistory.com/359</guid>
      <comments>https://writerroom.tistory.com/359#entry359comment</comments>
      <pubDate>Sun, 30 Jul 2023 12:32:22 +0900</pubDate>
    </item>
    <item>
      <title>OOM 재현 후, 힙 덤프 분석하기</title>
      <link>https://writerroom.tistory.com/358</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;메모리 에러가 발생하자 선배가 Heap Dump 를 분석하는 것을 보았다. 특히 메모리 누수나, Out Of Memory 같은 이슈가 있을 때 깊이 분석해보기 위해 사용한다고 한다. 처음 알게 된 개념을&amp;nbsp; 실습해보고자, 인위적으로 OOM 을 발생시킨 후 힙 덤프를 떠보았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;순서&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;1) &lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;SpringMVC로 간단한 프로젝트를 만들기&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2) Heap Dump 만들어서 읽기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3) 가비지 컬렉터를 바꿔보기&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4) System.gc() 호출해보기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;5) StringTableSize 바꿔보기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;실습 환경&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Java 17 |&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring 3.1.1&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;먼저 자바 애플리케이션을 실행한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;./gradlew build 명령어 실행 시, build/libs 에 jar 파일이 생성된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;java -jar demo-0.0.1-SNAPSHOT.jar 으로 jar 실행하면, 톰캣이 Application 을 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-24 오후 11.45.47.png&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Urycv/btsIiNRZSvh/YseF8WuzJLs2t6lFJ2BYE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Urycv/btsIiNRZSvh/YseF8WuzJLs2t6lFJ2BYE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Urycv/btsIiNRZSvh/YseF8WuzJLs2t6lFJ2BYE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUrycv%2FbtsIiNRZSvh%2FYseF8WuzJLs2t6lFJ2BYE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1454&quot; height=&quot;446&quot; data-filename=&quot;스크린샷 2023-06-24 오후 11.45.47.png&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;참고로 bootjar 를 빌드하는 것은 application 에 필요한 실행 가능한 jar 파일을 실행하는 것이다. gradlew build 는 bootjar 를 포함해 전반적인 빌드를 진행한다는 차이가 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1719844072514&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gradlew bootjar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;데이터를 과도하게 조회하여 OOM 을 임의로 일으키고, 힙 덤프를 분석해볼 것이다. 코드는 아래와 같다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1719844196396&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/oom&quot;)
    public void triggerOOM() throws InterruptedException {
        try {
            List&amp;lt;Item&amp;gt; itemList = new ArrayList&amp;lt;&amp;gt;();

            for (long i = 0; i &amp;lt; 100000000L; i++) {
                itemList.add(new Item(i, &quot;Item&quot; + i, 1000));
            }
        } catch (OutOfMemoryError error) {
            logger.error(&quot;OOM Error Occurred&quot;, error);
            throw new CustomException(&quot;out of Memory occurred&quot;, error);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-25 오후 1.48.14 (1).png&quot; data-origin-width=&quot;1113&quot; data-origin-height=&quot;173&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bc14Y/btsIiUDu0mw/kY3cPRKHJvSiACQK2pmudK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bc14Y/btsIiUDu0mw/kY3cPRKHJvSiACQK2pmudK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bc14Y/btsIiUDu0mw/kY3cPRKHJvSiACQK2pmudK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBc14Y%2FbtsIiUDu0mw%2FkY3cPRKHJvSiACQK2pmudK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1113&quot; height=&quot;173&quot; data-filename=&quot;스크린샷 2023-06-25 오후 1.48.14 (1).png&quot; data-origin-width=&quot;1113&quot; data-origin-height=&quot;173&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-25 오후 2.10.18.png&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;69&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bel0m2/btsIkzLFpcE/QxgZe2ma5QC2TjkkisXYD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bel0m2/btsIkzLFpcE/QxgZe2ma5QC2TjkkisXYD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bel0m2/btsIkzLFpcE/QxgZe2ma5QC2TjkkisXYD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbel0m2%2FbtsIkzLFpcE%2FQxgZe2ma5QC2TjkkisXYD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;949&quot; height=&quot;69&quot; data-filename=&quot;스크린샷 2023-06-25 오후 2.10.18.png&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;69&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;HeapDump 를 만드는 이유&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;사용하지 않는 메모리가 쌓이면 메모리 누수가 발생할 수 있다. 이때 Heap 영역을 모니터링해서 어떤 객체가 문제를 일으키는 지 분석한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;준비&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;HeapDump 를 만들기 위해, Mat을 설치 한다 (&lt;a href=&quot;https://www.eclipse.org/mat/&quot;&gt;https://www.eclipse.org/mat/&lt;/a&gt;)&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;인텔리제이의 edit configurations &amp;rarr; vm options 에 아래 코드를 추가하면, 루트 디렉토리에 OOM 발생 시마다, heap dump 파일을 생성한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1719844373368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Xmx50m 
-Dfile.encoding=UTF-8 
-Dconsole.encoding=UTF-8 
-XX:+HeapDumpOnOutOfMemoryError&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;OOM 아래에 Dumping heap to ... 라는 로그에서 덤프 파일이 생성되고 있음을 알 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-26 오후 7.54.55 (1).png&quot; data-origin-width=&quot;2224&quot; data-origin-height=&quot;688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4kcb6/btsIjyNKOLA/1J8eYKTt9oq3KsLJeOSKo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4kcb6/btsIjyNKOLA/1J8eYKTt9oq3KsLJeOSKo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4kcb6/btsIjyNKOLA/1J8eYKTt9oq3KsLJeOSKo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4kcb6%2FbtsIjyNKOLA%2F1J8eYKTt9oq3KsLJeOSKo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2224&quot; height=&quot;688&quot; data-filename=&quot;스크린샷 2023-06-26 오후 7.54.55 (1).png&quot; data-origin-width=&quot;2224&quot; data-origin-height=&quot;688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;Mat 의 File &amp;gt; Open Heap Dump&amp;nbsp;에서 생성된 덤프 &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;파일을 열자.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;사용량이 약 80% 에 달하는 곳을 찾아가 보면, Item 객체가 힙의 대다수를 채워서, oom 을 일으켰다는 것을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;G1GC (Java 17 default GC)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-26 오후 7.58.29 (1).png&quot; data-origin-width=&quot;1602&quot; data-origin-height=&quot;1216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcCmu7/btsIjJBEZrb/0LLHliypdVvYQoZKDpBa51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcCmu7/btsIjJBEZrb/0LLHliypdVvYQoZKDpBa51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcCmu7/btsIjJBEZrb/0LLHliypdVvYQoZKDpBa51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcCmu7%2FbtsIjJBEZrb%2F0LLHliypdVvYQoZKDpBa51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1602&quot; height=&quot;1216&quot; data-filename=&quot;스크린샷 2023-06-26 오후 7.58.29 (1).png&quot; data-origin-width=&quot;1602&quot; data-origin-height=&quot;1216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;ZGC&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-27 오전 8.30.55 (1).png&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;1370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckfAmK/btsIj9GRtEs/NAKjUFYknN0sPweP1ShMr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckfAmK/btsIj9GRtEs/NAKjUFYknN0sPweP1ShMr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckfAmK/btsIj9GRtEs/NAKjUFYknN0sPweP1ShMr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckfAmK%2FbtsIj9GRtEs%2FNAKjUFYknN0sPweP1ShMr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1434&quot; height=&quot;1370&quot; data-filename=&quot;스크린샷 2023-06-27 오전 8.30.55 (1).png&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;1370&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;ZGC: G1GC 와 달리 stop the world 를 10ms 이상 하지 않는다. 앱을 실행하면서 가비지 컬렉션을(살아있는 객체를 스캔하고, 한 리전에서 다른 리전으로 객체를 옮길 때) 동시에 하도록 디자인 되었다고 한다. ZGC 는 이렇게 동시에 하면서 stop the world (pause) 를 최소화 할 수 있고, 동시 처리를 위해 메모리를 더 요구할 수도 있다. G1GC 도 ZGC처럼 앱을 실행하면서 가비지 컬렉션을 하지만, ZGC 만큼 pause 를 최소화 하도록 특화된 것은 아니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-27 오전 8.27.43 (1).png&quot; data-origin-width=&quot;1620&quot; data-origin-height=&quot;1266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cj3VYq/btsIkk2pAmy/HYReimMLLyEJgVXcVIAUJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cj3VYq/btsIkk2pAmy/HYReimMLLyEJgVXcVIAUJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cj3VYq/btsIkk2pAmy/HYReimMLLyEJgVXcVIAUJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcj3VYq%2FbtsIkk2pAmy%2FHYReimMLLyEJgVXcVIAUJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1620&quot; height=&quot;1266&quot; data-filename=&quot;스크린샷 2023-06-27 오전 8.27.43 (1).png&quot; data-origin-width=&quot;1620&quot; data-origin-height=&quot;1266&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;br /&gt;지표를 보면, Shallow Heap, Retained Heap 이라는 용어가 등장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Shallow Heap은 해당 오브젝트가 단독으로 차지하는 메모리&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Retained Heap: Retained Heap이란 해당 오브젝트와 연결된 모든 객체를 포함한 메모리 점유량 이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;ShallowHeap 이 G1GC 대비 약 10만개 늘어났다. (하지만 GC간의 차이는 다양한 요인/튜닝이 영향을 미칠 수 있기 때문에, G1GC가 항상 객체를 더 많이 만든다고 단정짓기는 조심스럽다)&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;System.gc() 란?&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;system gc 는 major gc 를 호출한다. 따라서 stop the world 에 시간 소요가 많아지는 위험이 있을 수 있다. 일반적으로 gc()를 명시적으로 호출하는 것은 권장되지 않는다. 보통 애플리케이션에서 해결할 수 있는 이슈가 많다. 그래서&amp;nbsp; JVM 의 GC 메커니즘에 의존하는 것이 권고된다. gc() 를 호출하더라도, JVM 이 즉시 실행하지 않을 가능성도 있다. JVM 자체 알고리즘이 있기 때문에, 언제 얼마나 자주 GC 를 할지 결정하기 때문이다. (참고: &lt;a style=&quot;letter-spacing: 0px;&quot; href=&quot;https://www.baeldung.com/java-system-gc&quot;&gt;https://www.baeldung.com/java-system-gc)&amp;nbsp;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;System.gc() 호출&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;코드에 gc()를 추가한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1719890994675&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/oom&quot;)
    public void triggerOOM() throws InterruptedException {
        try {
            List&amp;lt;Item&amp;gt; itemList = new ArrayList&amp;lt;&amp;gt;();

            for (long i = 0; i &amp;lt; 100000000L; i++) {
                itemList.add(new Item(i, &quot;Item&quot; + i, 1000));
            }
        } catch (OutOfMemoryError error) {
            logger.error(&quot;OOM Error Occurred&quot;, error);
            long startTime = System.nanoTime();
            System.gc();
            long endTime = System.nanoTime();
            logger.info((endTime - startTime) / 1000000 + &quot;milliseconds took for GC&quot;);
            throw new CustomException(&quot;out of Memory occurred&quot;, error);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;코드내에서 gc() 를 호출했을 때, 이때 생긴 힙덤프 75337과, gc()를 호출하지 않은 6133을 비교해보면 우선 gc()를 호출한 시점에서 2-3분 뒤에 gc() 가 일어났다 (gc() 호출 안한 경우도 그렇다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;gc()가 코드에 없을 때도, jvm 이 알아서 gc()를 해준다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;아래 &lt;a href=&quot;https://www.baeldung.com/java-system-gc&quot;&gt;아티클을&lt;/a&gt; 보면, before throwing an&amp;nbsp;OutOfMemoryError,&amp;nbsp;the JVM will perform a full GC.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이라고 했는데, 여기서는 error 를 터뜨리고 난 뒤에 (18:17:51) fullGC (18:20)가 발생했다 (타임 스탬프에 찍힌 시간으로 비교)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-27 오후 6.21.24 (1).png&quot; data-origin-width=&quot;1856&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brDo6N/btsIiSsgT95/xAurt00CRE7ay0x1xAR5X0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brDo6N/btsIiSsgT95/xAurt00CRE7ay0x1xAR5X0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brDo6N/btsIiSsgT95/xAurt00CRE7ay0x1xAR5X0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrDo6N%2FbtsIiSsgT95%2FxAurt00CRE7ay0x1xAR5X0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1856&quot; height=&quot;300&quot; data-filename=&quot;스크린샷 2023-06-27 오후 6.21.24 (1).png&quot; data-origin-width=&quot;1856&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-27 오후 6.20.38 (1).png&quot; data-origin-width=&quot;2220&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YHH1n/btsIjwoTVdP/jK627AKFXbSpOmXyKStKLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YHH1n/btsIjwoTVdP/jK627AKFXbSpOmXyKStKLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YHH1n/btsIjwoTVdP/jK627AKFXbSpOmXyKStKLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYHH1n%2FbtsIjwoTVdP%2FjK627AKFXbSpOmXyKStKLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2220&quot; height=&quot;498&quot; data-filename=&quot;스크린샷 2023-06-27 오후 6.20.38 (1).png&quot; data-origin-width=&quot;2220&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;StringTableSize 바꿔보기&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;현재 코드는 string 객체를 많이 생성하는 상황이다. string table size 의 한계에 도달했을 때도 oom 이 일어날 수 있다고 한다. 다만 이는 메모리를 많이 요구하는 작업이기도 하니 조심해서 써야 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;interning 이란? &lt;b&gt;String&lt;/b&gt;&amp;nbsp;Constant Pool에 자기자신이 갖고있는 리터럴이 있으면 해당 Constant Pool를 참조하는 리터럴을 반환하고 그렇지 않으면 새로운&amp;nbsp;&lt;b&gt;String&lt;/b&gt;&amp;nbsp;Constant Pool을 생성후 그곳을 참조하는 리터럴을 반환한다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Java 7에서는 60013 이 디폴트이고, Java 11 이후로는 기본 크기가 60,013에서 131,072로 증가되었다.&amp;nbsp; 2^17 이기도 하다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;디폴트 (최대한 조건을 비슷하게 해보려고 1분 내에 덤프를 떠봤다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;StringTableSize=&lt;/span&gt;&lt;b&gt;131072&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-27 오후 11.58.05.png&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;677&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FPPMa/btsIiwCS6Ee/EgpXfrfKgxCs1DLK4Aw4eK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FPPMa/btsIiwCS6Ee/EgpXfrfKgxCs1DLK4Aw4eK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FPPMa/btsIiwCS6Ee/EgpXfrfKgxCs1DLK4Aw4eK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFPPMa%2FbtsIiwCS6Ee%2FEgpXfrfKgxCs1DLK4Aw4eK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;894&quot; height=&quot;677&quot; data-filename=&quot;스크린샷 2023-06-27 오후 11.58.05.png&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;677&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: left;&quot;&gt;Stringtablesize 를 두 배로 늘려보자 .&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1719928173498&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java -XX:StringTableSize=262144&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;아래 결과를 분석해보면,&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;객체 수는 약 2%감소 (264871-259552/264871)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;힙 사이즈는 약 2% 감소((11999984-11729032)/11999984 )했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;string table 사이즈가 커져서 인스턴스를 더 담을 수 있게 되고, 그 중 재활용 가능한 인스턴스가 많아져서 객체 수/힙 사이즈가 감소한 게 아닐까 추측해본다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-27 오후 11.55.17.png&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;1380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q9Zpl/btsIiRmDppF/SLFx2yVHtw6aLsvM8UgClk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q9Zpl/btsIiRmDppF/SLFx2yVHtw6aLsvM8UgClk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q9Zpl/btsIiRmDppF/SLFx2yVHtw6aLsvM8UgClk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ9Zpl%2FbtsIiRmDppF%2FSLFx2yVHtw6aLsvM8UgClk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1782&quot; height=&quot;1380&quot; data-filename=&quot;스크린샷 2023-06-27 오후 11.55.17.png&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;1380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start; font-family: 'Nanum Gothic';&quot;&gt;&lt;br /&gt;코드는 아래에서 확인할 수 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://github.com/jieun-dev1/Fundamentals/commit/fbb8289817b07b82236b37bb9ce8fb29224ff477&quot;&gt;https://github.com/jieun-dev1/Fundamentals/commit/fbb8289817b07b82236b37bb9ce8fb29224ff477&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;참고&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a style=&quot;letter-spacing: 0px;&quot; href=&quot;https://techblog.woowahan.com/2628/&quot;&gt;https://techblog.woowahan.com/2628/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://steady-coding.tistory.com/591&quot;&gt;https://steady-coding.tistory.com/591&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://www.samsungsds.com/kr/insights/1232762_4627.html&quot;&gt;https://www.samsungsds.com/kr/insights/1232762_4627.html&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/11/gctuning/z-garbage-collector1.html#GUID-A5A42691-095E-47BA-B6DC-FB4E5FAA43D0&quot;&gt;https://docs.oracle.com/en/java/javase/11/gctuning/z-garbage-collector1.html#GUID-A5A42691-095E-47BA-B6DC-FB4E5FAA43D0&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://www.oracle.com/java/technologies/javase/gc-tuning-6.html#other_considerations&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.oracle.com/java/technologies/javase/gc-tuning-6.html#other_considerations&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <author>flow123</author>
      <guid isPermaLink="true">https://writerroom.tistory.com/358</guid>
      <comments>https://writerroom.tistory.com/358#entry358comment</comments>
      <pubDate>Sun, 25 Jun 2023 10:54:27 +0900</pubDate>
    </item>
    <item>
      <title>소켓과 친해지기</title>
      <link>https://writerroom.tistory.com/357</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 팀에서 다루는 서버의 절반은 TCP 통신 기반이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 쓰이지만 익숙치 않은 개념이어서,&amp;nbsp;TCP 통신과 관련해서 기초 네트워크 개념을 정리해보았다. 실무에서 자주 쓰이는 패킷도 들여다 보고자 한다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IO와 NIO 의 차이&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;810&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3Jkql/btsIlTJ6lt9/qz4g8y1Q1EJLJ0ZZiRBHh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3Jkql/btsIlTJ6lt9/qz4g8y1Q1EJLJ0ZZiRBHh0/img.png&quot; data-alt=&quot;출처: https://www.slideshare.net/kslisenko/networking-in-java-with-nio-and-netty-76583794&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3Jkql/btsIlTJ6lt9/qz4g8y1Q1EJLJ0ZZiRBHh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3Jkql%2FbtsIlTJ6lt9%2Fqz4g8y1Q1EJLJ0ZZiRBHh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1858&quot; height=&quot;810&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;810&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://www.slideshare.net/kslisenko/networking-in-java-with-nio-and-netty-76583794&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 Socket 은 blocking IO 로 동작한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IO는 스트림(Stream)이라는&amp;nbsp;&lt;b&gt;단방향 통로&lt;/b&gt;를 생성해서 외부 데이터와 통신, 연결 클라이언트 수가 적고 대용량, 순차처리에 적합하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NIO는 채널(Channel)이라는&amp;nbsp;&lt;b&gt;양방향 통로&lt;/b&gt;를 생성해서 외부 데이터와 통신, 연결 클라이언트 수가 많고 소규모 빠른 대응에 적합하다.&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IO&lt;/td&gt;
&lt;td&gt;NIO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스트림방식 Non-buffer&lt;/td&gt;
&lt;td&gt;버퍼방식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동기방식&lt;/td&gt;
&lt;td&gt;동기/비동기 모두 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;블로킹 방식&lt;/td&gt;
&lt;td&gt;블록킹/논블록킹 모두 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;순수 Java NIO로 TCP Echo 서버 만들어보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Selector : 여러개의 채널에서 발생하는&amp;nbsp;&lt;b&gt;이벤트(연결이 생성됨, 데이터가 도착함 등)&lt;/b&gt;&amp;nbsp;를 모니터링할 수 있는 객체&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ServerSocketChannel: 들어오는 TCP 연결을 수신할 수 있고, 멀티 쓰레드에 안전한 채널&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SocketChannel: 위의 채널에서 연결을 가져와서, accept() 메서드로 새로운 커넥션을 위한 socket channel 을 반환&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 코드&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EchoServer&lt;/h3&gt;
&lt;pre id=&quot;code_1719929627883&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.example;

import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class EchoServer {
    public static void main(String[] args) throws IOException {
        //open(): create a Selector object / channel
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //bind port, set selectable channel to non-blocking mode, register channel to the selector
        serverSocketChannel.bind(new InetSocketAddress(&quot;localhost&quot;, 8080));
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        System.out.println(&quot;server is ready&quot;);

        while (true) {
            selector.select();
            Set&amp;lt;SelectionKey&amp;gt; selectionKeys = selector.selectedKeys();
            Iterator&amp;lt;SelectionKey&amp;gt; iter = selectionKeys.iterator();
            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                if (key.isAcceptable()) {
                    register(selector, serverSocketChannel);
                }

                if (key.isReadable()) {
                    answerWithEcho(buffer, key);
                }
                iter.remove();
            }

        }
    }

    public static void register(Selector selector, ServerSocketChannel serverSocketChannel) throws IOException {
        SocketChannel client = serverSocketChannel.accept();
        client.configureBlocking(false);
        client.register(selector, SelectionKey.OP_READ);
    }

    private static void answerWithEcho(ByteBuffer buffer, SelectionKey key) throws IOException {
        //SocketChannel extends SelectableChannel
        SocketChannel client = (SocketChannel) key.channel();
        int point = client.read(buffer);
        //end of the input stream. no more bytes available to be read || may indicate error
        if (point == -1) {
            System.out.println(&quot;Not accepting client messages anymore&quot;);
            client.close();
        } else {
        	//읽기/쓰기 작업의 범위를 설정 (시작-끝) 
            //socketChannel(client)에 write(buffer) 전, 
            //클라이언트에게 데이터를 전송하기 전에 write mode 로 전환하는 역할
            //read(buffer) 후에 호출. &amp;rarr; position을 0으로 세팅해서 버퍼의 시작부터 읽도록 함.
            buffer.flip();
            client.write(buffer);
            buffer.clear();
        }
    }

    public static Process start() throws IOException, InterruptedException {
        String javaHome = System.getProperty(&quot;java.home&quot;);
        String javaBin = javaHome + File.separator + &quot;bin&quot; + File.separator + &quot;java&quot;;
        String classPath = System.getProperty(&quot;java.class.path&quot;);
        String className = EchoServer.class.getCanonicalName();
        ProcessBuilder builder = new ProcessBuilder(javaBin, &quot;-cp&quot;, classPath, className);
        return builder.start();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EchoClient&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1719929844642&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.example;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class EchoClient {
    private static SocketChannel client;
    private static ByteBuffer buffer;
    private static EchoClient instance;

    public static EchoClient start() {
        if(instance == null)
            instance = new EchoClient();
        return instance;
    }

    public static void stop() throws IOException {
        client.close();
        buffer = null;
    }

    private EchoClient() {
        try {
            client = SocketChannel.open(new InetSocketAddress(&quot;localhost&quot;, 8080));
            buffer = ByteBuffer.allocate(256);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String sendMessage(String msg) {
        buffer = ByteBuffer.wrap(msg.getBytes());
        String response = null;
        try {
            client.write(buffer);
            buffer.clear();
            client.read(buffer);
            response = new String(buffer.array()).trim();
            System.out.println(&quot;response=&quot; + response);
            buffer.clear();

        } catch (IOException e) {
            e.printStackTrace();
        }
        return response;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EchoTest&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719929862059&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.example;

import org.junit.*;


import java.io.IOException;

import static org.junit.Assert.assertEquals;

public class EchoTest {
    Process server;
    EchoClient client;

    @Before
    public void setup() throws IOException, InterruptedException {
        server = EchoServer.start();
        client = EchoClient.start();
    }

    @Test
    public void givenServerClient_whenServerEchosMessage_thenCorrect(){
        String resp1 = client.sendMessage(&quot;hello&quot;);
        String resp2 = client.sendMessage(&quot;world&quot;);
        assertEquals(&quot;hello&quot;, resp1);
        assertEquals(&quot;world&quot;, resp2);
    }

    @After
    public void close() throws IOException {
        server.destroy();
        client.stop();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;tcpdump로 패킷 까보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 트래픽은 네트워크를 통해 전송되는 데이터의 흐름이다. 서버와 클라이언트 간의 통신이 이루어질 때, 데이터는 여러 개의 작은 패킷으로 나뉘어 전송된다. tcpdump는 이 패킷들을 캡처하고, 캡처된 패킷이 담긴 덤프를 분석해서, TCP 통신에서 실제로 어떤 데이터가 오가는지 확인한다. . 애플리케이션 타임아웃의 원인을 분석할 때도 사용된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드로 EchoClient 를 살짝 바꿔서 clientServer 를 띄운다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719929957359&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EchoClientV2 {
    private static SocketChannel client;
    private static ByteBuffer buffer;
    private static EchoClientV2 instance;

    public static void main(String[] args) throws IOException {
        start();
        sendMessage(&quot;Hello&quot;);
        sendMessage(&quot;world&quot;);
        stop();
    }

    public static EchoClientV2 start() {
        if(instance == null)
            instance = new EchoClientV2();
        return instance;
    }

    public static void stop() throws IOException {
        client.close();
        buffer = null;
    }

    private EchoClientV2() {
        try {
            client = SocketChannel.open(new InetSocketAddress(&quot;localhost&quot;, 8080));
            buffer = ByteBuffer.allocate(256);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static String sendMessage(String msg) {
        buffer = ByteBuffer.wrap(msg.getBytes());
        String response = null;
        try {
            client.write(buffer);
            buffer.clear();
            client.read(buffer);
            response = new String(buffer.array()).trim();
            System.out.println(&quot;response=&quot; + response);
            buffer.clear();

        } catch (IOException e) {
            e.printStackTrace();
        }
        return response;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719930144980&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo tcpdump -i loO port 8080&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 호스트에서 포트 8080으로 들어오거나 나가는 모든 트래픽을 모니터링 할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2746&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dhtx6/btsIlzSEJ8k/qEkv47wkyjwLxwKtyHgxVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dhtx6/btsIlzSEJ8k/qEkv47wkyjwLxwKtyHgxVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dhtx6/btsIlzSEJ8k/qEkv47wkyjwLxwKtyHgxVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDhtx6%2FbtsIlzSEJ8k%2FqEkv47wkyjwLxwKtyHgxVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2746&quot; height=&quot;624&quot; data-origin-width=&quot;2746&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-X 를 붙이면, ASCII 형식으로 볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719930302494&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo tcpdump -i lo0 port 8080 -X&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예제에서는 client &amp;rarr; Server : Hello 메시지를 보냈고, Server가 echo 로서 그대로 응답한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 패킷에서도 확인할 수 있음.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2788&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AsUHn/btsIjLG8rwV/4hIqsU0xR62FvNFjFEAsK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AsUHn/btsIjLG8rwV/4hIqsU0xR62FvNFjFEAsK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AsUHn/btsIjLG8rwV/4hIqsU0xR62FvNFjFEAsK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAsUHn%2FbtsIjLG8rwV%2F4hIqsU0xR62FvNFjFEAsK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2788&quot; height=&quot;620&quot; data-origin-width=&quot;2788&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Address already in use 일 때, 기존 Process 를 종료해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 &lt;a href=&quot;http://localhost:8080&quot;&gt;localhost:8080&lt;/a&gt; 에 Socket 을 바인드 하려고 하지만, 현재 다른 프로세스에 의해 사용 중이다는 뜻이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스 점유 중인 PID 찾기 : lsof -i :{port 번호}&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스 죽이기 : kill -9 {PID}&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2772&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BgMsx/btsIlUPNcYA/WXoJe3AWkGrzcrcK0IUB1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BgMsx/btsIlUPNcYA/WXoJe3AWkGrzcrcK0IUB1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BgMsx/btsIlUPNcYA/WXoJe3AWkGrzcrcK0IUB1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBgMsx%2FbtsIlUPNcYA%2FWXoJe3AWkGrzcrcK0IUB1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2772&quot; height=&quot;774&quot; data-origin-width=&quot;2772&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CLI에서 netstat으로 포트와 PID 찾아서 kill 해보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드: &lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://github.com/jieun-dev1/Fundamentals/tree/feature/socket-communication&quot;&gt;https://github.com/jieun-dev1/Fundamentals/tree/feature/socket-communication&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.cloudflare.com/ko-kr/learning/network-layer/what-is-a-packet/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.cloudflare.com/ko-kr/learning/network-layer/what-is-a-packet/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kouzie.github.io/java/java-NIO/#java-nio&quot;&gt;https://kouzie.github.io/java/java-NIO/#java-nio&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/java-nio-selector&quot;&gt;https://www.baeldung.com/java-nio-selector&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.lesstif.com/linux-core/unix-domain-socket&quot;&gt;https://www.lesstif.com/linux-core/unix-domain-socket&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>flow123</author>
      <guid isPermaLink="true">https://writerroom.tistory.com/357</guid>
      <comments>https://writerroom.tistory.com/357#entry357comment</comments>
      <pubDate>Sun, 18 Jun 2023 15:53:02 +0900</pubDate>
    </item>
  </channel>
</rss>