<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>함께 자라나기</title>
    <link>https://learngoeson.tistory.com/</link>
    <description>함께 자라나기, 뿌리 깊게 자라나기</description>
    <language>ko</language>
    <pubDate>Tue, 14 Jul 2026 07:21:42 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>LEE티씨</managingEditor>
    <image>
      <title>함께 자라나기</title>
      <url>https://tistory1.daumcdn.net/tistory/5011942/attach/ba6e0f1e3a32428cb28c9dc848af2ef2</url>
      <link>https://learngoeson.tistory.com</link>
    </image>
    <item>
      <title>[아맞당!]Spring Batch 톺아보기 - 1</title>
      <link>https://learngoeson.tistory.com/88</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;534&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjIEXT/dJMcac9eytr/qrtOFoc7O3swIsbaOh9IS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjIEXT/dJMcac9eytr/qrtOFoc7O3swIsbaOh9IS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjIEXT/dJMcac9eytr/qrtOFoc7O3swIsbaOh9IS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjIEXT%2FdJMcac9eytr%2FqrtOFoc7O3swIsbaOh9IS1%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;534&quot; height=&quot;312&quot; data-origin-width=&quot;534&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 Spring Batch를 통해 &lt;b&gt;음료 데이터를 초기화&lt;/b&gt;하기 위해서 &lt;b&gt;AWS Lambda&lt;/b&gt;에 Python 코드(Crawling + OCR)를 활용하여 수동 트리거 로직을 구현했었다.&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;/b&gt; &lt;b&gt;짧은 피드백 루프(&lt;/b&gt;개선속도를 높이기 위해&lt;b&gt;)를 위해&lt;/b&gt; 로컬 Python 서버에서 크롤링과 OCR 로직을 수정하며 사용하고 있었다.&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;하지만, 개선작업이 끝난 이후 한 가지 실수를 하게되었는데 Spring Batch에 사용되는 URL을 &lt;a href=&quot;http://localhost&quot;&gt;localhost&lt;/a&gt; 주소로 기입하여 Batch가 &lt;b&gt;&lt;code&gt;Connection refused: localhost:8000&lt;/code&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;이러한 경험을 계기로 Spring Batch의 기본 동작 흐름에 대해 톺아보려고한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Batch의 동작흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch는 단순 API 처럼 한번 실행되는 애플리케이션이 아닌 &lt;b&gt;데이터 처리 파이프라인&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 흐름은 아래와 같은 순서로 처리된다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;JOB &amp;rarr; STEP &amp;rarr; (Reader -&amp;gt; Processor -&amp;gt; Writer)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SpringBoot 실행 시점에서 보면&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Spring Context를 초기화&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 단계에서 Bean생성과 더불어, @Componet, @Service, @Configuration 등 컴포넌트 관련 클래스들을 Spring IOC Container에 등록
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;tmi.)IOC Container 등록순서
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스 스캔 &amp;rarr; BeanDefinition 등록 &amp;rarr; Bean 인스턴스 생성 &amp;rarr; 의존성 주입 &amp;rarr; 초기화 콜백 &amp;rarr; 컨테이너 등록 완료&lt;/li&gt;
&lt;li&gt;에러가 발생한건 &lt;b&gt;초기화 콜백시점(@PostConstruct)&lt;/b&gt; 실행 함으로서 localhost:8000 연결 시도&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;BatchConfig&lt;/code&gt;&lt;/b&gt; 같은 배치 설정 클래스들이 이때 등록&lt;/li&gt;
&lt;li&gt;Job, Step, Reader, Processor, Writer Bean들이 &lt;b&gt;이 시점에 생성&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JobLauncher 실행&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 기동 이후 JobLauncher 탐색(아맞당! 기준으로는 수동 trigger 구조)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reader.read()&lt;/code&gt;, &lt;code&gt;processor.process()&lt;/code&gt;, &lt;code&gt;writer.write()&lt;/code&gt; 순으로 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;각 단계에서 필요한 외부 리소스 접근&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reader에서 DB/파일/REST API 호출 또느 Processor에서 외부 서버 호출 등의 작업&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;에러지점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러는 위에도 적어놨듯이 Bean 초기화 단계에서 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하자면 아래와 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;현재 데이터를 제공하는 Fast API Server의 URI에 대해 WebClient로 HTTP 통신을 수행하는 구조&lt;/li&gt;
&lt;li&gt;데이터를 받기위해 WebClient 초기화&lt;/li&gt;
&lt;li&gt;하지만 &lt;a href=&quot;Https://amatdang.cloud%EC%9D%98&quot;&gt;Https://amatdang.cloud&lt;/a&gt; 의 8000 포트는 Listen중이아님&lt;/li&gt;
&lt;li&gt;곧 바로 &lt;b&gt;&lt;code&gt;Connection Refused&lt;/code&gt; Exception&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Batch에 의한 에러라기 보단 Spring 에러에 가깝다. 하지만 Batch에서 또한 문제는 발생할 수 있기에 트러블 슈팅은 진행했어도 Batch에 대해서 좀 더 알아보자.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Batch 핵심 요소 및 코드 동작&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Batch는 아까도 말했듯 데이터를 &lt;b&gt;읽고(reader) &amp;rarr; 가공하고(processor) &amp;rarr; 처리(writer)&lt;/b&gt;하는 데이터 파이프라인이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 하나의 &lt;b&gt;&lt;code&gt;Step&lt;/code&gt;&lt;/b&gt; 이란 단위로 동작한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ItemReader&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB, CSV, API,Json 등의 데이터들을 한 건(Generic)씩 읽어온다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 정의된 한건은 비즈니스적으로 한 번에 처리하고자 코드적으로 정의된 단위&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;데이터의 &lt;b&gt;물리적 구조(DB row, 파일 라인)&lt;/b&gt;보다 &lt;b&gt;논리적 처리 단위(Item)&lt;/b&gt;에 초점을 두어 개발자가 유연한 설계를 할 수 있도록&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;ㅤ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한건에 대한 정의&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public interface ItemReader&amp;lt;T&amp;gt; { 
    T read() throws Exception;//Signature 메서드
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;read()&lt;/code&gt;&lt;/b&gt;가 호출될 때마다 &lt;b&gt;하나의 &lt;code&gt;T&lt;/code&gt; 객체&lt;/b&gt;를 반환한다.&lt;/li&gt;
&lt;li&gt;더 이상 읽을 데이터가 없다면 &lt;b&gt;&lt;code&gt;null&lt;/code&gt;&lt;/b&gt; 반환 &amp;rarr; Step Over&lt;/li&gt;
&lt;/ul&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;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt; @Bean
    public ItemReader&amp;lt;LambdaBeverageDto&amp;gt; beverageReader() {
        List&amp;lt;LambdaBeverageDto&amp;gt; list = cafeBeverageBatchService.fetchAll();
        return new ListItemReader&amp;lt;&amp;gt;(list != null ? list : List.of());
    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;public record LambdaBeverageDto(
        String brand,
        String name,
        String image,
        String beverageType,
        String beverageTemperature,
        List&amp;lt;BeverageNutritionDto&amp;gt; beverageNutritions) {

    public CafeBeverage toEntity(CafeStore cafeStore) {
        BeverageType type =
                Optional.ofNullable(beverageType)
                        .map(String::toUpperCase)
                        .map(BeverageType::valueOf)
                        .orElse(BeverageType.ANY);

        Map&amp;lt;String, BeverageNutritionDto&amp;gt; nutritionsMap =
                beverageNutritions.stream()
                        .collect(
                                Collectors.toMap(
                                        BeverageNutritionDto::size,
                                        Function.identity(),
                                        (existing, replacement) -&amp;gt; existing));

        return CafeBeverage.of(
                name,
                UUID.randomUUID(),
                cafeStore,
                image,
                type,
                beverageTemperature,
                nutritionsMap);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ItemProcessor&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reader가 읽은 데이터를 가공, 검증, 변환하는 단계&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;O process(I item) throws Exception; //**메서드 시그니처**&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력 &lt;code&gt;I&lt;/code&gt;(Reader의 출력)를 받아 가공 후 &lt;code&gt;O&lt;/code&gt;를 반환&lt;/li&gt;
&lt;li&gt;&lt;code&gt;null&lt;/code&gt;을 반환하면 Writer로 전달되지 않고 &lt;b&gt;필터링됨&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;아맞당 사용 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;    @Bean
    public ItemProcessor&amp;lt;LambdaBeverageDto, CafeBeverage&amp;gt; beverageProcessor() {
        return cafeBeverageBatchService::toEntity;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reader에서 사용한 Dto 클래스를 &lt;b&gt;&lt;code&gt;from-to&lt;/code&gt;&lt;/b&gt; 패턴 을 사용하여 Entity로 변환시켜 영속화 대상으로 만든다.&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;&lt;b&gt;ItemWriter&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Processor의 결과 데이터를 &lt;b&gt;배치로 저장(쓰기)&lt;/b&gt; 하는 단계&lt;/li&gt;
&lt;li&gt;Write의 대상은 DB영속화, API, Docs 등 다양하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;void write(List&amp;lt;? extends T&amp;gt; items) throws Exception; //**메서드 시그니처**&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 건을 한 번에 받아 처리 (성능을 위해 batchSize 단위)&lt;/li&gt;
&lt;li&gt;보통 JDBC batch insert, API bulk 요청, 파일 출력 등에 사용ㅤ&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;아맞당 사용예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Bean
    public ItemWriter&amp;lt;CafeBeverage&amp;gt; beverageWriter() {
        JpaItemWriter&amp;lt;CafeBeverage&amp;gt; writer =
                new JpaItemWriterBuilder&amp;lt;CafeBeverage&amp;gt;()
                        .entityManagerFactory(entityManagerFactory)
                        .build();

        return items -&amp;gt; {
            log.info(&quot;[DEBUG] Writing {} items to DB.&quot;, items.size());
            /*for (CafeBeverage item : items) {
                log.info(
                        &quot;[DEBUG]   - Writing beverage: '{}', with sizes: {}&quot;,
                        item.getName(),
                        item.getSizes().stream()
                                .map(s -&amp;gt; s.getSizeType().name())
                                .collect(Collectors.toList()));
            }*/ // -&amp;gt; 로그용 Loop 없어도 코드는 똑같이 동작
            writer.write(items);
        };
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;JPAItemWriter&lt;/code&gt;&lt;/b&gt;가지정해놓은 Chunk(배치 단위)로 데이터를 저장&lt;/li&gt;
&lt;li&gt;Spring Batch가 &lt;b&gt;&lt;code&gt;chunk(n)&lt;/code&gt;&lt;/b&gt; 만큼 모아서 &lt;b&gt;&lt;code&gt;items&lt;/code&gt;&lt;/b&gt; 리스트로 넘기면&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JpaItemWriter&lt;/code&gt;가 그 리스트를 &lt;b&gt;엔티티 매니저에 차례로 persist/merge&lt;/b&gt; 하고 &lt;b&gt;청크 경계에서 flush/clear&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;즉, &lt;b&gt;트랜잭션/커밋 단위 = 청크 크기&lt;/b&gt; (아맞당은 10)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;StepBuilder&lt;/code&gt;&lt;/b&gt; (step 설정)에서 지정하는 값에 의해 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;ㅤ&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  근데 데이터가 엄청나게 많다면요?&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 위의 코드는 Spring Batch가 &lt;b&gt;&lt;code&gt;writer.write(items)&lt;/code&gt;&lt;/b&gt;로 청크 단위의 리스트를 넘겨주면 &lt;b&gt;&lt;code&gt;JPAItemWriter&lt;/code&gt;&lt;/b&gt;가 내부에서 &lt;b&gt;그 리스트를 루프 돌면서 각각 persist/merge&lt;/b&gt; 하는 구조&lt;/li&gt;
&lt;li&gt;현재 아맞당에서는 많아봤자 1000건의 데이터만을 Batch로 처리한다.&lt;/li&gt;
&lt;li&gt;그렇다면 만약 데이터가 100만건이라면?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제로 Batch Excute를 진행하기위해선 &lt;b&gt;JPA Writer &amp;rarr; JDBC 기반 Writer&lt;/b&gt; WorkFlow를 구현해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;b&gt;JDBC Batch 이용&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  jpa:
    properties:
      hibernate.jdbc.batch_size: 100          # JDBC 배치 ON
      hibernate.order_inserts: true
      hibernate.order_updates: true
      hibernate.generate_statistics: false&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 처럼 단순히 JDBC의 Batch를 이용할 수 있다.&lt;/li&gt;
&lt;li&gt;하지만 PK 전략이 &lt;b&gt;IDENTITY 전략이면&lt;/b&gt; insert 배치가 깨지거나 제한된다.&lt;/li&gt;
&lt;li&gt;Hibernate가 Batch 작업 주도&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;JdbcBatchItemWriter로 전환&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;pony&quot;&gt;&lt;code&gt;@Bean
public JdbcBatchItemWriter&amp;lt;CafeBeverage&amp;gt; beverageJdbcWriter(DataSource ds) {
    return new JdbcBatchItemWriterBuilder&amp;lt;CafeBeverage&amp;gt;()
        .dataSource(ds)
        .sql(&quot;&quot;&quot;
             INSERT INTO cafe_beverage (name, brand, sugar, created_at)
             VALUES (:name, :brand, :sugar, :createdAt)
             &quot;&quot;&quot;)
        .beanMapped() // CafeBeverage의 프로퍼티를 :파라미터에 매핑
        .assertUpdates(false) // (선택) trigger 등으로 갯수 불일치 허용
        .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Spring Batch Writer가 직접 &lt;code&gt;addBatch&lt;/code&gt; 와 &lt;code&gt;executeBatch&lt;/code&gt;&lt;/b&gt; 를 보장하기 때문에 Exception Handling이 비교적 간단하다.&lt;/li&gt;
&lt;li&gt;하지만, 직접 연관관계와 Insert 쿼리를 작성해야하기에 불편함은 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;ㅤ&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  근데 아맞당에서는 왜 Chunk Size를 10으로 했어요? 너무작은디&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 주요한 이유는 데이터 정합성 때문이었다.&lt;/li&gt;
&lt;li&gt;음료 목록의 타겟이되는 &lt;b&gt;Starbucks, MegaCoffee&lt;/b&gt;등의 데이터들은 API로 제공되는 것이아니라 별도의 FastAPI 서버에서 &lt;b&gt;&lt;code&gt;Crawling&lt;/code&gt;, &lt;code&gt;OCR&lt;/code&gt;&lt;/b&gt;로 수집하고 있기에 유효성 검사를 통해서 &lt;b&gt;데이터가 부정확하다면 빠르게 RollBack하기 위해 ChunkSize를 작게 가져갔다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring Batch는 이 셋을 어떻게 돌리는가?&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;while(true) {
    item = reader.read();         // 1건 읽기
    if(item == null) break;       // 데이터 끝
    result = processor.process(item);
    if(result != null) buffer.add(result);

    if(buffer.size() == chunkSize) {
        writer.write(buffer);     // 일정 단위(chunkSize)마다 기록
        buffer.clear();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ItemReader를 통해 Item을 읽어들이고 이를 Buffer에 가공한 상태로 저장한다.&lt;/li&gt;
&lt;li&gt;Buffer가 가득찬다면 Item에 대해 Writer 작업을 수행한다.&lt;/li&gt;
&lt;/ul&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;b&gt;Spring Batch의 엔진 레벨&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 거대하게 보자면 아래와 같은 구조이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;JobLauncher.run()
   &amp;darr;
Job.execute()
   &amp;darr;
Step.execute()
   &amp;darr;
TaskletStep.execute()
   &amp;darr;
ChunkOrientedTasklet.execute()
   &amp;darr;
  ├─ ChunkProvider.provide() &amp;rarr; reader.read()
  ├─ ChunkProcessor.process() &amp;rarr; processor.process()
  └─ ChunkProcessor.write()   &amp;rarr; writer.write()&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;reader.read() &amp;rarr; processor.process() &amp;rarr; writer.write()는 단순한 메서드 호출&lt;/b&gt;이 아니라&lt;br /&gt;Spring Batch 엔진이 &lt;b&gt;청크 단위로 반복 호출하면서 트랜잭션 내에서 관리&lt;/b&gt;하는 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reader부터 다시 살펴보자&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Reader(&lt;code&gt;reader.read()&lt;/code&gt; )&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;T item = reader.read();  // 데이터 한 건 읽음
chunk.add(item);         // 청크 버퍼에 적재&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Spring Batch가 현재 Step을 시작하면, 내부적으로 &lt;code&gt;ChunkProvider&lt;/code&gt;가 작동&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reader.read()&lt;/code&gt;가 반복 호출되어 &lt;b&gt;하나씩 데이터를 읽어온다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;이는 내부 버퍼(&lt;code&gt;Chunk&lt;/code&gt;)에 임시 저장&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 우리는 ChunkSize를 10으로 저장했다고했다. 그럼 Reader가 총 10건을 읽고나서 다음 단계인 &lt;b&gt;Processo&lt;/b&gt;로 흐름이 넘어가게된다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;ㅤ&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Processor 단계(&lt;code&gt;processor.process()&lt;/code&gt;)&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Reader가 채운 Chunk(예: 10건)를 Processor가 하나씩 꺼내 변환&lt;/li&gt;
&lt;li&gt;&lt;code&gt;process(item)&lt;/code&gt;을 호출하여 비즈니스 로직 수행
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 가공&lt;/li&gt;
&lt;li&gt;필터링 (&lt;code&gt;null&lt;/code&gt; 반환 &amp;rarr; writer로 전달 안 함)&lt;/li&gt;
&lt;li&gt;유효성 검증(아맞당에선 당류가 20% 이하일때 저당 Tag)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;결과물을 List에 담는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;ㅤ&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Writer 단계 (&lt;code&gt;writer.write()&lt;/code&gt;)&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Processor가 반환한 결과물 리스트를 &lt;code&gt;writer.write()&lt;/code&gt;에 전달&lt;/li&gt;
&lt;li&gt;Writer는 일반적으로 &lt;b&gt;DB batch insert / update / 파일 쓰기 / API 전송&lt;/b&gt;을 수행&lt;/li&gt;
&lt;li&gt;모든 write가 성공하면 트랜잭션 커밋, 실패하면 롤백.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 플로우를 요약하자면 &lt;b&gt;&amp;ldquo;10건 읽고 &amp;rarr; 10건 가공 &amp;rarr; 10건 저장 &amp;rarr; 1회 커밋&amp;rdquo;&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;위의 총 3단계가 트랜잭션에 감싸져서 트랜잭션이 커밋될때까지 한번의 Loop를 수행하며, 더이상 Reader에서 읽을 데이터가 없다면(null) 해당작업이 종료된다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size14&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;b&gt;흐름 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;while (true) {
    TransactionStatus tx = txManager.begin();
    chunk = provider.provide();           
    processor.process(chunk);             
    writer.write(chunk);                  
    txManager.commit();                  
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 코드에서 예외가 발생한다면 내부적으로 &lt;b&gt;&lt;code&gt;txManager.rollback()&lt;/code&gt;&lt;/b&gt;을 수행하고&lt;b&gt;&lt;code&gt;retry&lt;/code&gt; ,&lt;code&gt;skip&lt;/code&gt;&lt;/b&gt; 등도 마찬가지&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;후기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch의 완전한 개념을 통합해서 다뤄보고자 했지만 ItemReader, Processor등 내부 코드와 동작원리가 상당히 복잡하고 어려운 구조로 되어있다.&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;심지어 중요한 개념중 하나인 Job이나 Step 내부구조에 대해선 다루지 못했다.&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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Job과 Step의 생명주기 및 실행 컨텍스트 관리, Partitioning을 활용한 대규모 데이터 병렬 처리, 실패 복구와 재시도 메커니즘 구현&lt;/b&gt;등을 다뤄보고자한다.&lt;/p&gt;</description>
      <category>Project/아맞당!</category>
      <author>LEE티씨</author>
      <guid isPermaLink="true">https://learngoeson.tistory.com/88</guid>
      <comments>https://learngoeson.tistory.com/88#entry88comment</comments>
      <pubDate>Sat, 1 Nov 2025 15:42:17 +0900</pubDate>
    </item>
    <item>
      <title>[아맞당!] GCP Docker 배포 파이프라인 개선기</title>
      <link>https://learngoeson.tistory.com/87</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 동작하던 배포 파이프라인에 난데 없이 마주치게 된 &lt;b&gt;에러 문구&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;821&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcPa7Y/btsP4JgmeAK/VxtZHSvSkV0XvHdiqq3f00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcPa7Y/btsP4JgmeAK/VxtZHSvSkV0XvHdiqq3f00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcPa7Y/btsP4JgmeAK/VxtZHSvSkV0XvHdiqq3f00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcPa7Y%2FbtsP4JgmeAK%2FVxtZHSvSkV0XvHdiqq3f00%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;695&quot; height=&quot;446&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;821&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;b&gt;에러의 내용은 현재 VM 머신의 디스크에 공간이 부족하다는 에러였다.&lt;/b&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-origin-width=&quot;235&quot; data-origin-height=&quot;214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MZxZ4/btsP6c9H2Nq/Cg1ppbEJ6kZOFzYF4c1rL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MZxZ4/btsP6c9H2Nq/Cg1ppbEJ6kZOFzYF4c1rL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MZxZ4/btsP6c9H2Nq/Cg1ppbEJ6kZOFzYF4c1rL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMZxZ4%2FbtsP6c9H2Nq%2FCg1ppbEJ6kZOFzYF4c1rL1%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;235&quot; height=&quot;214&quot; data-origin-width=&quot;235&quot; data-origin-height=&quot;214&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;이게 무슨소리지??? 싶어 VM 인스턴스에 접속하여 현재 디스크 상태를 확인했다.&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-origin-width=&quot;896&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boDRBt/btsP2Zkf9gJ/bxBJCLAS19BbS0dwtXM2oK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boDRBt/btsP2Zkf9gJ/bxBJCLAS19BbS0dwtXM2oK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boDRBt/btsP2Zkf9gJ/bxBJCLAS19BbS0dwtXM2oK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboDRBt%2FbtsP2Zkf9gJ%2FbxBJCLAS19BbS0dwtXM2oK%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;698&quot; height=&quot;220&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;282&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;b&gt;그런데 맙소사 현재 10GB짜리 머신에서 99%가량의 디스크 공간을 Docker가 사용하는 것을 확인할 수 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;b&gt;현재&lt;/b&gt;&lt;/h2&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;1280&quot; data-origin-height=&quot;822&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEMp3l/btsP4a6neL1/sMV5JWmfPNR47lUklKBEH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEMp3l/btsP4a6neL1/sMV5JWmfPNR47lUklKBEH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEMp3l/btsP4a6neL1/sMV5JWmfPNR47lUklKBEH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEMp3l%2FbtsP4a6neL1%2FsMV5JWmfPNR47lUklKBEH1%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;774&quot; height=&quot;497&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;822&quot;/&gt;&lt;/span&gt;&lt;/figure&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;b&gt;&lt;code&gt;Gihub Action&lt;/code&gt;&lt;/b&gt;을 통해 &lt;b&gt;운영 브랜치인 main&lt;/b&gt;에 코드가 병합되는 순간을 기점으로 파이프라인이 동작&lt;/li&gt;
&lt;li&gt;파이프라인에선 제일 먼저 Ubuntu, Docker, Gradle을 build하는데 필요한 정보들을 환경변수로 받고 이를 바탕으로 배포를 준비&lt;/li&gt;
&lt;li&gt;gradle build를 실행한 후 현재 도커 이미지 저장소로 사용되고 있는 GCP의 Artifact Registry에 이미지 빌드 결과를 Push한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 까지가 일반적인 &lt;b&gt;CI&lt;/b&gt;(Continuous Intergartion)과정이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이후 GCP VM에 SSH로 접속하여 기존의 도커 이미지를 중단, 삭제&lt;/li&gt;
&lt;li&gt;이후 Docker Image를 Pull 받아 새롭게 애플리케이션을 실행시키는 경량화 파이프라인을 보유하고있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 &lt;b&gt;CD&lt;/b&gt;(Continuous Delivery)를 구현한 내용이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&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;b&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;b&gt;기존 Docker에 대한 Stop, rm 작업은 해주고 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;b&gt;Trouble Shooting&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. Swap Memory?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&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;이런 상황을 AWS 인프라를 활용할 때도 겪었기에 Swap 메모리를 통한 머신의 여유공간을 확보하고자 생각했다.&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;아맞당의 서버 운영 전략상 Docker를 통해 애플리케이션을 실행&lt;/b&gt;시키기에 VM의 디스크와 메모리를 전부 사용하게 된다.&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;code&gt;no space left on device&lt;/code&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;또한 도커가 메모리를 사용하는 영역은 애플리케이션 영역 그러니까, JVM이나 공유 메모리 영역에만 사용하고 현재 문제가 되는 부분은 이미지 레이어가 저장되는 정적 리소스 위치인 &lt;b&gt;&lt;code&gt;/var/lib/docker/overlay2&lt;/code&gt;&lt;/b&gt; 이 경로에 이미지가 VM의 디스크 크기보다 물리적으로 많이 저장되어 발생한 문제였기에 이는 해결책이 아니었다.&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;b&gt;2. Prune&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 관련작업하여 Prune을 빼놓고 얘기할 순 없을것이다.&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;code&gt;Prune&lt;/code&gt;&lt;/b&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;  아 그럼 그냥 스크립트에 prune을 추가하면 되겠네&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;과연 옳바른 설계일까?&amp;rdquo;&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;매 배포 때마다 Docker 메타데이터를 쓸어버리는 건 &lt;b&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;b&gt;매 스크립트 마다 실행시 발생하는 Side Effect&lt;/b&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;b&gt;성능 저하&lt;/b&gt;: 매번 이미지/캐시를 지우면 다음 배포에서 다시 받아야 해서 pull 시간, 네트워크 IO, 디스크 IO가 그대로 비용으로 돌아온다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오버헤드&lt;/b&gt;: 불필요한 데이터를 지울 시 롤백 비용 발생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;근본 해결 X&lt;/b&gt;: 로그 폭주, 과도한 보존, 비대한 이미지 같은 근본 원인은 그대로고 매 배포마다 임시 방편처리하는 꼴&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  그래서 뭐해야하는데?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 우선 도커에서 &lt;b&gt;비용(cost)&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;나는 VM의 디스크를 과도하게 차지하는 애들을 기준으로 개선작업을 수행하고자 한다.&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;1. 로그 롤링&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&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;밑의 사진은 docker image prune 을 수행한 직후 애플리케이션을 새롭게 띄운후에 로그 크기를 측정한 커맨드이다. 직후에 실행했음에도 3MB나 차지하는 모습이 보인다.&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-origin-width=&quot;1150&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Eok2q/btsP3BpFLKi/rabjh1Z1eajWKKOZkab1C0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Eok2q/btsP3BpFLKi/rabjh1Z1eajWKKOZkab1C0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Eok2q/btsP3BpFLKi/rabjh1Z1eajWKKOZkab1C0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEok2q%2FbtsP3BpFLKi%2Frabjh1Z1eajWKKOZkab1C0%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;712&quot; height=&quot;217&quot; data-origin-width=&quot;1150&quot; data-origin-height=&quot;350&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;도커 로그를 통해서 애플리케이션의 실행시 발생한 에러 파악이나 서버의 HealthCheck용으로 많이 사용되기에 필요한 데이터이다.(버릴 수는 없다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그렇기 떄문에 만약에 &lt;b&gt;LTS(프로덕션 레벨)이 릴리스 된 상태라면&lt;/b&gt; 로그는 계속해서 쌓이게 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;보존시간(분) &amp;asymp; (max-size &amp;times; max-file) / (평균 로그 속도(MB/분))&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;이를 구하기 위해서 로그의 발생속도 또한 측정하였다. 1분을 기준으로 측정한다면 정확한? 수치를 뽑을 수 있을거라고 기대했기에 1분을 기준으로 설정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;LOG=&quot;/var/lib/docker/containers/7e510cdf7715fc7e41efa9095c1432e270b008f56dbaa14eb3f4e8622534a920/7e510cdf7715fc7e41efa9095c1432e270b008f56dbaa14eb3f4e8622534a920-json.log&quot;

if ! sudo test -f &quot;$LOG&quot;; then
  echo &quot;로그 파일을 없음 !!!!!: $LOG&quot;
  exit 1
fi

S1=$(sudo stat -c%s &quot;$LOG&quot; 2&amp;gt;/dev/null || echo 0)
sleep 60
S2=$(sudo stat -c%s &quot;$LOG&quot; 2&amp;gt;/dev/null || echo 0)

if [ &quot;$S2&quot; -ge &quot;$S1&quot; ]; then
  BYTES=$((S2 - S1))
else
  BYTES=$S2
fi

awk -v b=&quot;$BYTES&quot; 'BEGIN { printf(&quot;로그 증가 속도: %.1f MB/분\n&quot;, b/1024/1024) }'&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;b&gt;해당 작업을 통해 우선 100MB 정도의 롤링 사이즈를 지정하게되었다. (최대 100분간 로그보존)&lt;/b&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-origin-width=&quot;958&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jbsgb/btsP4allNZN/vrLupHLvoiJkFzmzKTW7zK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jbsgb/btsP4allNZN/vrLupHLvoiJkFzmzKTW7zK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jbsgb/btsP4allNZN/vrLupHLvoiJkFzmzKTW7zK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJbsgb%2FbtsP4allNZN%2FvrLupHLvoiJkFzmzKTW7zK%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;592&quot; height=&quot;205&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;332&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 조건부 Prune&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&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;b&gt;Prune을 사용할 것&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;그렇다면 이를 일정한 조건을 기준으로 실행시킨다면 기존의 파이프라인에서 Disk 부족문제와 성능저하 문제 또한 잡을 수 있을 것이다.&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;/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;b&gt;현재 아맞당 서버가 운용되고있는 GCP의 VM은 10GB이고 SpringBoot 도커 이미지 크기는 대략 6~700MB이다.&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;그렇다면 여유있게 배포 1회정도 여유를 가질 수 있도록 Disk 사용량이 85%이상이라면 Prune을 실행시키도록 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;조건부 prune&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1756063794047&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; if [ &quot;$USED&quot; -ge 85 ]; then
              echo &quot;Disk &amp;gt;= 85% &amp;rarr; light prune (images/builder cache)&quot;
              docker image prune -af --filter &quot;until=168h&quot; || true
              docker builder prune -af || true
 fi&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;weekly 주기 prune&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1756063814615&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  weekly-prune:
    name: Weekly prune on Wed 03:00 KST
    runs-on: ubuntu-latest
    if: github.event_name == 'schedule'
    steps:
      - name: Authenticate to GCP
        uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GCP_SA_KEY }}
      - name: SSH &amp;amp; Prune
        uses: google-github-actions/ssh-compute@v1
        with:
          project_id: ${{env.PROJECT_ID}}
          zone: ${{ env.VM_ZONE }}
          instance_name: ${{ env.VM_NAME }}
          user: ubuntu
          ssh_private_key: ${{ secrets.GCP_VM_SSH_PRIVATE_KEY }}
          command: |
            set -euo pipefail
            echo &quot;Weekly prune start...&quot;
            docker image prune -af --filter &quot;until=168h&quot; || true
            docker builder prune -af || true
            sudo journalctl --vacuum-time=7d || true
            echo &quot;Weekly prune done.&quot;&lt;/code&gt;&lt;/pre&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;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 아맞당의 프로덕트는 단일 서버 기준으로 운영될 것을 고려하여 &lt;b&gt;로그 롤링과&lt;/b&gt;, &lt;b&gt;디스크 85% 조건부 정리&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;b&gt;세줄 요약&lt;/b&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;b&gt;측정 후(100분 보존) 정책으로 관리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;디스크 정리는 필요하다   &lt;b&gt;상황(85%)에서만 가볍게 실행&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;무지성 청소는 성능&amp;middot;롤백 비용만 키운다   &lt;b&gt;보존/제한/조건부&lt;/b&gt;로 운영&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 배포 파이프라인 구성경험을 통해 디테일한 정책을 통해 &lt;b&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;b&gt;사용자가 사용하고 사용자의 피드백이 개발자의 결실&lt;/b&gt;이 된다. 그러니 사용자 불편함을 최소화하는 서비스를 창출하기 위해 노력해나가보자&lt;/p&gt;</description>
      <category>Project</category>
      <author>LEE티씨</author>
      <guid isPermaLink="true">https://learngoeson.tistory.com/87</guid>
      <comments>https://learngoeson.tistory.com/87#entry87comment</comments>
      <pubDate>Mon, 25 Aug 2025 04:30:47 +0900</pubDate>
    </item>
    <item>
      <title>[Redis] Redis Eviction 정책</title>
      <link>https://learngoeson.tistory.com/86</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;탄생 배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 메모리 기반의 캐시저장소로, Spring에서는 Spring Cache를 도입해서 읽기 작업의 성능을 높이는 작업을 수행할 수 있다.&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;/b&gt;에서 데이터가 계속해서 쌓이다 보면 Redis는 새로운 데이터를 저장하기 위해 기존 데이터를 삭제해야 하는 문제에 봉착하게 된다. 아니면 애플리케이션 성능에 문제를 일으킬 수도 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis의 메모리 관리 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 RAM의 용량보다 더 많은 데이터를 저장하게 되는 경우 &lt;b&gt;Swap이 발생하며 Redis의 성능 저하를&lt;/b&gt; 일으킬 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;858&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nENO0/btsMCtPqGHg/oR1o0RuF4gixbASBd37LsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nENO0/btsMCtPqGHg/oR1o0RuF4gixbASBd37LsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nENO0/btsMCtPqGHg/oR1o0RuF4gixbASBd37LsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnENO0%2FbtsMCtPqGHg%2FoR1o0RuF4gixbASBd37LsK%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;802&quot; height=&quot;555&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;858&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;물리 메모리의 용량보다 더 많은 데이터가 들어오면 swap이 발생한다.&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;운영체제에서 Swap space의 주요 기능은 물리메모리(RAM)의 양이 가득 차고 더 많은 메모리의 양이 필요할 때 Backing store(디스크)의 가상메모리에 대체한다.&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;b&gt;&lt;code&gt;MAX Memory&lt;/code&gt;&lt;/b&gt;를 별도로 지정해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 설정은 redis.conf 파일의 maxmemory 옵션을 지정해주면 된다.&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;p data-ke-size=&quot;size16&quot;&gt;이것을 &lt;b&gt;&lt;code&gt;Eviction&lt;/code&gt;&lt;/b&gt; 이라고 부르며, 이러한 방식을 만들기 위해 &amp;ldquo;maxmemory-policy&amp;rdquo; 옵션을 통해 Eviction 정책을 만들 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Eviction Policy&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. noeviction&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새 데이터를 저장해야할 때 메모리가 부족하면 오류를 반환(OOM command Not allowed)하고, 기존 키는 절대 삭제하지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐시보다는 데이터의 무결성이 중요한&lt;/b&gt; 경우에 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. allkeys-Iru&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 키를 대상으로 &lt;b&gt;LRU 알고리즘 적용&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;최근에 사용되지 않는 키를 우선적 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. volatile-lru&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만료시간이 설정된 키들을 대상으로 LRU 적용&lt;/li&gt;
&lt;li&gt;만료 설정(ttl)이 없는 경우 키 삭제 X&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. allkeys-random&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 키 중 무작위로 제거&lt;/li&gt;
&lt;li&gt;LRU 알고리즘 계산의 오버헤드를 피하고 단순 무작위 접근으로 해결&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. allkeys-lfu&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로 추가된 데이터의 용량을 확보하기 위해 사용빈도수가 가장 적은 키를 제거(최근 저장된 키라도 사용 빈도수가 적다면 대상이 될 수 있다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. volatile-lfu&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로 추가된 데이터의 용량을 확보하기 위해 TTL이 설정된 키들 중 사용빈도 수가 적은 키를 제거 (최근 저장된 키라도 사용 빈도수가 적다면 대상이 될 수 있다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. volatile-random&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로 추가된 데이터의 용량을 확보하기 위해 TTL이 설정된 키들 중 무작위로 키를 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. volatile-ttl&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로 추가된 데이터의 용량을 확보하기 위해 TTL이 짧은 키를 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;내가 Redis를 사용한 프로젝트에서의 정책&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 실시간 음원 스트리밍 플랫폼인 &lt;b&gt;&amp;lsquo;WhatSong(이하 왓송)&amp;rsquo;&lt;/b&gt;의 경우 noeviction 과 volatile- 정책들은 적합하지 않았다.&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;&lt;b&gt;우선 Redis를 어디에 활용했는가?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;음원을 스트리밍 중에 대한 방에 대한 정보 데이터들을 담기 위해 Redis를 활용했다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방에 대한 정보에는 아래와 같은 데이터가 포함된다.(설명을 위해 필드명 등 간략화)&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;public class MusicRoomPlay{
        private String musicId; // youtube Id
        private List&amp;lt;MemberDTO&amp;gt; members = new ArrayList&amp;lt;&amp;gt;(); // 현재 방에 참가한 인원정보

        ...
}&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;b&gt;위 데이터는 음악 대기열 상태를 관리하고&lt;/b&gt; 현재 재생될 음악에 대한 정보를 Client에게 전달하는 모델로 활용했다.&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;방장(Admin User)만&lt;/b&gt;이 음악을 대기열에 추가할 수 있고 일반 유저의 경우 음악 요청을 추가하고 이를 방장이 승인해야지 대기열에 추가되는 구조이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;엥 그럼 volatile- 정책이 적합할거 같은데요? 어차피 소모되는 데이터 잖아요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나 또한 이렇게 생각을 했고 각 음원 데이터에 대한 &lt;b&gt;&lt;code&gt;ttl(time-to-live)&lt;/code&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;b&gt;Youtube Open API&lt;/b&gt;를 활용하고 있었는데, Youtube의 음원 데이터만을 따로 필터링하여 추출하는 것은 정책에 위반되었기에 일반 영상 또한 포함될 수 밖에 없었다.&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;그렇기에 영상길이를 규제할 수 없었고 volatile 정책을 활용할 수 없었다.&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;b&gt;&lt;code&gt;allkeys-lru&lt;/code&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;/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;  이처럼 프로젝트의 특성에 맞추어 Redis Eviction 정책을 활용하여서 리소스에 대한 관리를 효율적으로 이뤄내야한다고 생각한다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>Tech-Interview</category>
      <author>LEE티씨</author>
      <guid isPermaLink="true">https://learngoeson.tistory.com/86</guid>
      <comments>https://learngoeson.tistory.com/86#entry86comment</comments>
      <pubDate>Fri, 7 Mar 2025 16:04:49 +0900</pubDate>
    </item>
    <item>
      <title>주요 GC 알고리즘</title>
      <link>https://learngoeson.tistory.com/85</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Serial GC&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 스레드로 동작하며 모든 애플리케이션 스레드를 중단(stop-the-world) 한 스레드가 메모리를 정리&lt;/li&gt;
&lt;li&gt;주로 메모리가 작거나 단일 코어 환경에서 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Parallel GC&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 개의 GC 스레드를 통해 &lt;code&gt;stop-the-world&lt;/code&gt; 를 빠르게 처리&lt;/li&gt;
&lt;li&gt;오래된 Java(8 이하)에서 &lt;b&gt;기본 GC&lt;/b&gt;로 많이 쓰였으며, 현재도 고성능 환경에서 선호될 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. CMS(Concurrent Mark-Sweep)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션과 병렬적으로 안쓰는 메모리를 수거했다.&lt;/li&gt;
&lt;li&gt;Java 8버전에서 주로 사용했으며 이후 Java 9부터는 &lt;code&gt;G1&lt;/code&gt; 버전 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CMS의 단편화 이슈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CMS는 힙(old)을 &lt;code&gt;Mark &amp;amp; Sweep&lt;/code&gt; 알고리즘 방식으로만 회수하기 때문에 사용이 끝난 객체 영역을 연속적으로 압축하진 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 메모리 단편화가 누적되면 새 객체 할당 시 큰 연속 공간을 찾기 아려워지고 결국 Full GC(Stop-the-world)로 이어져서 성능이 떨어진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. G1(Garbage-First) GC&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java 9부터 기본 GC&lt;/li&gt;
&lt;li&gt;힙을 region(영역) 단위로 나누어, 이용률이 낮은 영역부터 우선적으로 수집한다.&lt;/li&gt;
&lt;li&gt;Stop-the-world 시간을 예측 가능하게 줄이는데 중점&lt;/li&gt;
&lt;li&gt;범용 GC로서 Java 8 ~ 11까지 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;G1의 Pause 이슈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;G1에서 Stop-the-world 시간을 짧게 유지하려고하는데 예상치 못하게 긴 &lt;b&gt;&lt;code&gt;Pause&lt;/code&gt;&lt;/b&gt;가 발생하는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) Evacuation Pause에서의 병목&lt;/b&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;b&gt;Evacuation(객체 이동) 단계&lt;/b&gt;는 대부분 &lt;b&gt;Stop-the-world(STW)&lt;/b&gt; 형태로 진행됩니다.&lt;/li&gt;
&lt;li&gt;G1은 Marking은 상당 부분 &lt;b&gt;동시(Concurrent)로&lt;/b&gt; 수행할 수 있지만, &lt;b&gt;정리(Evacuate) 단계&lt;/b&gt;에서 객체를 새 Region으로 옮길 때 애플리케이션 스레드를 일시 중단&lt;/li&gt;
&lt;li&gt;힙이 매우 커지거나 Old 영역에 객체가 많이 쌓인 경우, &lt;b&gt;이 Evacuation이 길어지면&lt;/b&gt; Pause가 늘어날 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) To-space Exhausted(대상 영역 부족)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Evacuation 중, 복사할 객체를 담을 &amp;ldquo;새 Region(&amp;lsquo;to-space&amp;rsquo;)&amp;rdquo;이 충분치 않다면, G1은 &lt;b&gt;즉시 Full GC&lt;/b&gt; 모드로 전환할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Full GC&lt;/b&gt;는 단일 스레드 Serial 방식으로 전체 힙을 정리하므로, &lt;b&gt;Pause가 급격히 길어지는&lt;/b&gt; 최악의 시나리오가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) Mixed GC에서 Old 영역 처리량 부족&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;G1은 Young 수집과 달리, Old 영역의 객체가 많을수록 Mixed GC 사이클에서 처리해야 할 Region이 늘어난다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동시 마크&lt;/b&gt; 단계와 STW로 진행되는 &lt;b&gt;처리 단계&lt;/b&gt;(cleanup, selective evacuation)가 균형을 못 맞추면, &amp;ldquo;목표 Pause 시간을 초과&amp;rdquo;하는 상황이 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) Marking이 지연되어 &amp;ldquo;Concurrent Cycle&amp;rdquo;이 밀릴 때&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;G1의 &lt;b&gt;Concurrent Mark&lt;/b&gt;가 애플리케이션 스레드와 병행되지만, CPU가 부족하거나 객체 그래프가 복잡하면 마크가 늦어지고, 다음 Young GC가 트리거될 때 &amp;ldquo;아직 Old 영역 어떤 객체가 살아있는지 모름 &amp;rarr; 예기치 않은 추가 작업&amp;rdquo;이 발생해 &lt;b&gt;Pause가 증가&lt;/b&gt;할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. ZGC&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아주 낮은 지연시간을 목표로 설계된 GC로서 대용량 힙과 초저지연 연산을 지원한다.&lt;/li&gt;
&lt;li&gt;Java 11에서 처음 도입&lt;/li&gt;
&lt;li&gt;Java 15에서 정식 도입&lt;/li&gt;
&lt;li&gt;동시(Concurrent)로 대부분을 처리하여 &lt;code&gt;Stop-the-world&lt;/code&gt; 를 수 밀리초 수준으로 억제&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Shenandoah(셰넌도어)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저지연 GC(Concurrent+ Region 기반)&lt;/li&gt;
&lt;li&gt;'큰 GC 작업을 적은 횟수로 수행하는 것보다 작은 GC 작업을 여러번 수행하는게 더 좋다'는 개념을 적용해 만들어진 GC
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작은 단위의 GC수행을 자주하기 때문에 &lt;code&gt;Concurrency&lt;/code&gt;를 보장한다.&lt;/li&gt;
&lt;li&gt;GC가 CPU를 더 사용하는 대신 pause 시간을 줄인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Shenandoah의 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 CMS가 가진 단편화, G1이 가진 Pause 이슈를 해결했다.&lt;/li&gt;
&lt;li&gt;강력한 Concurrency와 가벼운 GC 로직으로 Heap 사이즈에 영향을 받지 않고 일정한 Pause 시간이 소요된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Single-Generational&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Shenandoah GC는 G1 GC를 기반으로 만들어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;heap을 region 별로 나누고 &lt;code&gt;mark-seep &amp;amp; evacuation&lt;/code&gt;을 진행한다. 그러나 Shenandoah GC는 G1 GC와 다르게 Generation으로 영역을 나누지 않는다. Generation을 나누게 되면 영역을 나누고 evacuation과정에서 메모리 copy가 계속 발생한다. 그러나 Generation이 나누어지지 않으면 memory copy에 대한 비용이 없어지게 되어 더 효율적&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GC는 왜 메모리를 여러 영역으로 나눌까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대부분의 객체는 생성 후 얼마 안되어 사용이 끝나지만, 반대로 한 번 오래 살아남은 객체는 상대적으로 계속살아남는다.&lt;/li&gt;
&lt;li&gt;그렇기에 이특성을 활용해서 &lt;code&gt;young&lt;/code&gt; 과 &lt;code&gt;old&lt;/code&gt; 로 분리해 관리하면 전체 GC 성능을 크게 높일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Shenandoah GC의 동작 과정&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Az8re/btsMyAO4xJ0/PMzFZ2zEG7AP50Kr6Kfdpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Az8re/btsMyAO4xJ0/PMzFZ2zEG7AP50Kr6Kfdpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Az8re/btsMyAO4xJ0/PMzFZ2zEG7AP50Kr6Kfdpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAz8re%2FbtsMyAO4xJ0%2FPMzFZ2zEG7AP50Kr6Kfdpk%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;807&quot; height=&quot;197&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Initial Marking&lt;/li&gt;
&lt;li&gt;Concurrent Marking&lt;/li&gt;
&lt;li&gt;Final Marking&lt;/li&gt;
&lt;li&gt;Concurrent Compaction&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pause후 root set을 스캔하고 &lt;b&gt;&lt;code&gt;Java 스레드와 동시에 marking&lt;/code&gt;&lt;/b&gt;을 수행한다. 그 이후 한번 더 pause를 수행하여 final marking을 수행하고 압축 마지막 단계에서 marking된 객체들을 제거. 전체적인 흐름은 CMS GC와 비슷하다.ㅌ&lt;/p&gt;</description>
      <category>JVM</category>
      <author>LEE티씨</author>
      <guid isPermaLink="true">https://learngoeson.tistory.com/85</guid>
      <comments>https://learngoeson.tistory.com/85#entry85comment</comments>
      <pubDate>Mon, 3 Mar 2025 22:50:34 +0900</pubDate>
    </item>
    <item>
      <title>사용자 증가에 따른 서버확장</title>
      <link>https://learngoeson.tistory.com/84</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;만약 서비스를 운영중인데 사용자가 계속해서 &lt;b&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;/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;code&gt;Scale-Up&lt;/code&gt; 과 &lt;code&gt;Scale-Out&lt;/code&gt; 일 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물리적으로 생각해도 사용자가 늘어난다면 그만큼 서버의 크기 또한 늘려야한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Scale-Up?&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;171&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvWKwK/btsLamdunoO/aZaeoiFHMoPw6UgCAvK4zK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvWKwK/btsLamdunoO/aZaeoiFHMoPw6UgCAvK4zK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvWKwK/btsLamdunoO/aZaeoiFHMoPw6UgCAvK4zK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvWKwK%2FbtsLamdunoO%2FaZaeoiFHMoPw6UgCAvK4zK%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;601&quot; height=&quot;171&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;171&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;b&gt;수직적 확장을&lt;/b&gt; 의미하며, 말 그대로 추가적인 RAM, CPU 등을 컴퓨터에 달아 업그레이드 하는 방식이다.&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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&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;해결책으로는 새로운 기기를 사용하는 방법인데, 서로 다른 독립적인 기기로 데이터를 옮기기 위해 기존의 데이터를 백업 시킨 후, 데이터 migration을 매번 하는 것은 매우 시간이 오래걸리고 복잡한 작업이고, 데이터의 단위는 시간이 지날수록 점점 증가하기에 이 또한 불가하다고 봐야한다.&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;code&gt;Scale-Out&lt;/code&gt; 과 비교되는 부분인데,&lt;/b&gt; 하나의 서버를 확장한 것이기에 트래픽을 감당하지 못하고 서버가 죽게될 경우 서비스는 큰 타격을 입게된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Scale-Out?&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;513&quot; data-origin-height=&quot;307&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmajZo/btsLbab099C/pneYa3ei2ZhkhHb3h1Y290/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmajZo/btsLbab099C/pneYa3ei2ZhkhHb3h1Y290/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmajZo/btsLbab099C/pneYa3ei2ZhkhHb3h1Y290/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmajZo%2FbtsLbab099C%2FpneYa3ei2ZhkhHb3h1Y290%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;513&quot; height=&quot;307&quot; data-origin-width=&quot;513&quot; data-origin-height=&quot;307&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;b&gt;수평적 확장&lt;/b&gt; 이라고도 불리며 추가적인 부품을 다는게 아닌, 새로운 서버를 추가하는 방식입니다. &lt;b&gt;&lt;code&gt;Scale-up&lt;/code&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;/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;사실 다른건 둘째치더라도 Scale-Out의 가장 큰 문제는 분산서버 환경에서 일어나는 &lt;b&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;b&gt;예를 들어,&lt;/b&gt; 사용자1의 로그인 요청을 서버1이 받아 처리를 해서 서버1에는 사용자의 데이터가 존재하지만 서버2에서는 그러한 요청을 받은 적이 없기 때문에 만약 다시 로그인을 시도했는데 서버2가 받았다면 로그인이 안되는 문제점이 발생한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;그럼 어떻게 해야돼요?&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lTVUG/btsLaVF93rQ/1O8FrM0ypEMSUdJT4eNCl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lTVUG/btsLaVF93rQ/1O8FrM0ypEMSUdJT4eNCl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lTVUG/btsLaVF93rQ/1O8FrM0ypEMSUdJT4eNCl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlTVUG%2FbtsLaVF93rQ%2F1O8FrM0ypEMSUdJT4eNCl1%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;548&quot; height=&quot;206&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Scale-out 을 사용하는 적합 케이스&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&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;b&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;b&gt; Scale-out의 장점을 적극적으로 활용&lt;/b&gt;을 할 수가 있다. 하나의 서버가 다운이 되어도 다른 서버에서 즉시 처리가 가능하기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Scale-Up을 사용하는 적합 케이스&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Scale-up&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;Scale-out처럼 다른 조건의 네트워크를 통해 통신하게 되면 지연이 되버리고 곧 전체의 장애로 번지기에 서버를 추가해도 처리 능력이 비례하여 증가하지 않기 때문에 이러한 상황이라면 Scale-up이 적합하다.&lt;/p&gt;</description>
      <category>Tech-Interview</category>
      <author>LEE티씨</author>
      <guid isPermaLink="true">https://learngoeson.tistory.com/84</guid>
      <comments>https://learngoeson.tistory.com/84#entry84comment</comments>
      <pubDate>Sun, 8 Dec 2024 22:55:46 +0900</pubDate>
    </item>
    <item>
      <title>JPA ID 생성전략</title>
      <link>https://learngoeson.tistory.com/83</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JPA에서 제공하는 ID(PK)를 생성하는 방법은 두가지가 있다.&lt;/b&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;b&gt;직접할당&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자동할당&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;직접할당&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;@Id&lt;/code&gt;&lt;/b&gt; 어노테이션으로 ID 값을 직접적으로 할당하는 방식&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;자동할당&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 흔하게 사용되는 방식으로 Id &lt;b&gt;&lt;code&gt;@GenratedValue&lt;/code&gt;&lt;/b&gt; 의 &lt;b&gt;&lt;code&gt;stretagy&lt;/code&gt;&lt;/b&gt; 옵션을 통해 설정하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에는 총 4가지방식이 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;IDENTITY&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 키 생성 방식을 DB의 DBMS에 생성 전략에 위임하는 방법이다. 해당 전략을 사용하면 엔티티를 생성할 때 &lt;b&gt;쓰기 지연&lt;/b&gt;이 적용되지 않는다.&lt;br /&gt;그 이유로는 JPA에선 엔티티를 영속하기 위해, 고유 식별자가 필요한데, IDENTITY 전략에선 식별자가 DB에 저장되어야 할당되기때문이다.(엔티티 생성시 즉시 &lt;b&gt;INSERT 쿼리&lt;/b&gt; 발생)&lt;/li&gt;
&lt;li&gt;이때 하이버네이트를 사용하는 경우에는 INSERT 쿼리의 결과를 다시 조회하지 않기 위해서 내부적으로 Statement.getGeneratedKeys를 사용할 수 있으며 INSERT 벌크 연산이 불가하다.&lt;/li&gt;
&lt;li&gt;대신, 생성되자마자 다른 트랜잭션에서 이를 참조할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SEQUENCE&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SEQUENCE 전략&lt;/b&gt;은 시퀀스 키 생성 전략을 지원하는 DB에서 사용할 수 있다.(지원안하는 DB가 존재)&lt;/li&gt;
&lt;li&gt;유일한 값을 자동으로 생성하는 전략으로 &lt;b&gt;&lt;code&gt;auto_increment&lt;/code&gt;&lt;/b&gt;와 달리 &lt;b&gt;초기 값과 한번에 증가할 크기를 설정&lt;/b&gt;할 수 있다.&lt;br /&gt;어떤 시퀀스를 사용할 것인지를 &lt;code&gt;&lt;b&gt;@SequenceGenerator&lt;/b&gt;&lt;/code&gt; 로 설정할 수 있으며, 엔티티 매니저가 영속화를 시도할 때(&lt;code&gt;em.persist()&lt;/code&gt; ) 먼저 데이터베이스 시퀀스를 이용하여 식별자를 조회한다.&lt;br /&gt;이후 조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장한다.(영속화 할때 flush가 발생하는 Identity와 달리 Commit이후 저장)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TABLE&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;키 생성 전용 테이블을 만들어 시퀀스를 흉내내는 전략.&lt;br /&gt;어떤 테이블을 사용할 것인지를 &lt;b&gt;&lt;code&gt;@TableGenerator&lt;/code&gt;&lt;/b&gt;로 설정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1733487219591&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE id_generator ( id BIGINT NOT NULL, PRIMARY KEY (id) );&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import javax.persistence.*; @Entity public class MyEntity { @Id @GeneratedValue(strategy = GenerationType.TABLE, generator = &quot;id_gen&quot;) @TableGenerator( name = &quot;id_gen&quot;, table = &quot;id_generator&quot;, pkColumnName = &quot;id&quot;, valueColumnName = &quot;id&quot;, allocationSize = 1 ) private Long id; private String name; // Getters and Setters }&lt;/code&gt;&lt;/pre&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;b&gt;AUTO&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스 방언(종류)에 따라서 IDENTITY, SEQUENCE, TABLE 중 하나를 자동으로 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;쓰기 지연?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스에서 트랜잭션을 처리할 때, 실제로 데이터에 대한 디스크 I/O가 발생하기 전에 메모리나 다른 임시 저장소에 먼저 기록하고, 일정 조건이 만족되면 나중에 디스크에 저장하는 전략을 뜻한다.&lt;/p&gt;</description>
      <category>Tech-Interview</category>
      <author>LEE티씨</author>
      <guid isPermaLink="true">https://learngoeson.tistory.com/83</guid>
      <comments>https://learngoeson.tistory.com/83#entry83comment</comments>
      <pubDate>Fri, 6 Dec 2024 21:14:25 +0900</pubDate>
    </item>
    <item>
      <title>JPA N+1 문제</title>
      <link>https://learngoeson.tistory.com/82</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA의 N + 1 문제는 연관 관계가 설정된 엔티티를 조회할 경우에, 조회된 데이터 개수(N)만큼 연관관계의 조회 쿼리가 추가로 발생하는 현상이다.&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;블로그 게시글과 댓글이 있는 경우, 게시글을 조회한 후 각 게시글마다 댓글을 조회하기 위해 추가 쿼리가 발생한다면 N + 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;&lt;b&gt;블로그 게시글이 10개라면 총 11개의 쿼리(게시글 조회 1개 + 각 게시글의 댓글 조회 10개)가 실행&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 발생 원인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패치 전략을 &lt;b&gt;&lt;code&gt;Eager Loding(즉시로딩)&lt;/code&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;이중 Data JPA에서 제공하는 &lt;b&gt;&lt;code&gt;findAll&lt;/code&gt;&lt;/b&gt; 메서드를 수행할 경우 내부적으로는 &lt;b&gt;&lt;code&gt;select e from Entity e&lt;/code&gt;&lt;/b&gt; 라는JPQL을 생성하는데, JPQL의 특성상 글로벌 패치 전략을 고려하지 않고 쿼리를 수행한다.&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;그렇기에 명시적으로 Lazy Loading으로 엔티티 패치전략을 설정하면, findAll() 메서드를 사용할때 &lt;b&gt;N + 1 문제가 발생하지않는다&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 프록시 객체를 사용할 경우에 실제 데이터가 필요하여 조회하는 쿼리가 발생하고 N + 1 문제가 발생할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;해결&lt;/b&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;N + 1 문제를 해결하기 위해서는 &lt;b&gt;&lt;code&gt;fetch join&lt;/code&gt;&lt;/b&gt;, &lt;b&gt;&lt;code&gt;@EntityGraph&lt;/code&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;b&gt;&lt;code&gt;fetch join&lt;/code&gt;&lt;/b&gt;은 연관 관계에 있는 엔티티를 한번에 즉시 로딩해주며, &lt;b&gt;&lt;code&gt;@EntityGraph&lt;/code&gt;&lt;/b&gt;도 비슷한 효과를 만들어내며, 쿼리 메서드에 해당 어노테이션을 추가해 사용할 수 있다.&lt;/p&gt;</description>
      <category>Tech-Interview</category>
      <author>LEE티씨</author>
      <guid isPermaLink="true">https://learngoeson.tistory.com/82</guid>
      <comments>https://learngoeson.tistory.com/82#entry82comment</comments>
      <pubDate>Thu, 28 Nov 2024 16:56:34 +0900</pubDate>
    </item>
    <item>
      <title>리터럴 String과 new String()의 차이</title>
      <link>https://learngoeson.tistory.com/81</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;731&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byRaUY/btsKISPEhXu/A9Mv7ltf7dlxCbOxLVtCsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byRaUY/btsKISPEhXu/A9Mv7ltf7dlxCbOxLVtCsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byRaUY/btsKISPEhXu/A9Mv7ltf7dlxCbOxLVtCsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyRaUY%2FbtsKISPEhXu%2FA9Mv7ltf7dlxCbOxLVtCsK%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;769&quot; height=&quot;439&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;731&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;b&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;/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;String Pool&lt;/b&gt; 내에 저장하고 동일한 문자열 리터럴에 대해서는 &lt;b&gt;하나의 참조를 재사용하기 때문&lt;/b&gt;인데 예를들어 아래와 같은 코드가 있다고 생각해보자&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;String str1 = &quot;Hello&quot;;
String str2 = &quot;Hello&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;여기서 str1과 str2는 동일한 문자열 리터럴 &quot;Hello&quot;를 참조한다. 따라서 str1과 str2는 같은 메모리 주소를 가리키게 되기에 참조비교인 &lt;code&gt;&lt;b&gt;==&lt;/b&gt;&lt;/code&gt; 을 사용할 경우 이는 참이된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;System.out.println(str1 == str2); // true&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;그렇기에 String Pool에 저장되는 문자열 리터럴 방식은 동일한 문자열에 대해 재사용하기에 메모리 적으로 효율성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 String Pool은 &lt;b&gt;&lt;code&gt;Heap&lt;/code&gt;&lt;/b&gt; 영역에 해당된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;new String()&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;new String&lt;/code&gt;&lt;/b&gt;으로 새로운 String 객체를 만드는 방식도 Heap 영역을 이용하지만 동일한 문자열 값도 다른 객체로 인식하기 때문에 다른 메모리 주소를 가지게된다.&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;code&gt;new String()&lt;/code&gt;&lt;/b&gt;으로 생성된 문자열은 String Pool을 거치지 않고 직접 힙 영역에 저장되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 메모리 사용량이 늘어나고, 동일한 문자열이라도 각각의 객체로 관리된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;성능&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 설명했듯 성능적인 측면에서 문자열 리터럴을 사용하는 것이 &lt;b&gt;&lt;code&gt;new String()&lt;/code&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;문자열 리터럴을 사용하면 String Pool에 의해 문자열이 재사용되므로, 메모리 할당과 가비지 컬렉션의 부담이 줄어들게된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;오버헤드를 줄이기 때문에 더 효율적인거다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;비교 연산&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;String을 다루기 때문에 비교연산자도 한번 살펴보자면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;==&lt;/code&gt;&lt;/b&gt; 은 객체의 주소값을 비교하는 반면, &lt;b&gt;&lt;code&gt;equals()&lt;/code&gt;&lt;/b&gt; 메소드는 문자열의 내용 자체를 비교한다.&lt;/p&gt;</description>
      <category>Tech-Interview</category>
      <author>LEE티씨</author>
      <guid isPermaLink="true">https://learngoeson.tistory.com/81</guid>
      <comments>https://learngoeson.tistory.com/81#entry81comment</comments>
      <pubDate>Wed, 13 Nov 2024 23:09:39 +0900</pubDate>
    </item>
    <item>
      <title>Docker와 컨테이너</title>
      <link>https://learngoeson.tistory.com/80</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;컨테이너&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Software의 실행에 필요한&amp;nbsp;&lt;b&gt;실행환경을 독립적으로 운용할 수 있도록&lt;/b&gt;&amp;nbsp;기반환경 또는 다른 실행환경과의 간섭을 막고&amp;nbsp;&lt;b&gt;실행의 독립성을 확보해주는 운영체계 수준의 격리 기술&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;컨테이너랑 가상 머신의 차이&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nYxG3/btsKvBWg6h7/e2BkKokb8gzLgjxXlNK541/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nYxG3/btsKvBWg6h7/e2BkKokb8gzLgjxXlNK541/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nYxG3/btsKvBWg6h7/e2BkKokb8gzLgjxXlNK541/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnYxG3%2FbtsKvBWg6h7%2Fe2BkKokb8gzLgjxXlNK541%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;1280&quot; height=&quot;559&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;가상머신(VM, Virtual Machine)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VM은 HyperVisor 위에서 실행되며, 이 하이퍼바이저가 컴퓨터 VM 별로 인프라 리소스를 배분하는 역할로 사용한다. 또한 각 VM에선 독립적인 &lt;b&gt;Guest OS&lt;/b&gt;를 가지고 있는데, 독립적인 플랫폼을 하나씩 증가시킬 때마다 불필요한 OS를 만드는 작업에 대해서 계속해서 해야한다.&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;/b&gt;이 떨어진다는 소리다. 또한 메모리나 자원에 관해서 유동적으로 관리 되는게 아니라 처음부터 정해놓고 실행하기에&amp;nbsp;&lt;b&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;b&gt;ps.)하이퍼바이저(Hypervisor)는 하나의 물리적 컴퓨터에서 여러 개의 가상 머신을 생성하고 관리하는 소프트웨어, 하이퍼바이저는 하드웨어와 가상 머신 간의 중재 역할&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;컨테이너&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너의 경우 하나의 Host OS위에서 마치 각각의 독립적인 프로그램처럼 관리되고 실행된다. 불필요한 OS만드는작업 및 Infra를 독립적으로 나눌 필요가 없어서 확장성이 좋고 빠르다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;컨테이너 장점 정리&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 레벨 고립&lt;/li&gt;
&lt;li&gt;VM보다 빠른 셋업&lt;/li&gt;
&lt;li&gt;VM보다 메모리 덜 소모&lt;/li&gt;
&lt;li&gt;마이그레이션, 백업, 전송이 쉬움. VM과 비교해 크기가 작기 때문이다.&lt;/li&gt;
&lt;li&gt;하드웨어와의 빠른 커뮤니케이션은 따라, 성능에 효과적일 수 있다.&lt;/li&gt;
&lt;li&gt;애플리케이션 배치와 유지보수를 향상시킨다.&lt;/li&gt;
&lt;li&gt;애플리케이션 전달 시간 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;요약&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VM은 OS를 포함한 전체 시스템 환경을 가상화하여 &lt;b&gt;독립적인 OS&lt;/b&gt;를 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 컨테이너는 무겁게 OS가 필요하지 않으며, &lt;b&gt;가볍게 애플리케이션과 그를 실행시키는 라이브러리 및 의존성&lt;/b&gt;만 가지고 있으면 됨.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;커널과 컨테이너&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&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;b&gt;커널(Kernel)&lt;/b&gt;의 경우 프로세스 단위로 격리가 일어나며, 하드웨어와 소프트웨어의 중재자 역할이다. 또한 OS에 종속적이다.&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;/b&gt;의 경우 다양한 환경에서 일관된 실행을 보장하며, 애플리케이션을 기준으로 격리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구분 커널 컨테이너&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;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;시스템 자원 관리 및 하드웨어와의 중재&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;애플리케이션을 격리된 환경에서 실행&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;자원 사용&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;상대적으로 무겁고, 많은 자원을 소모&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;경량이며, 빠르게 시작하고 종료 가능&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;격리 수준&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;프로세스 간 격리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;애플리케이션 간 격리&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;이식성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;OS에 종속적&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;다양한 환경에서 일관된 실행 가능&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Docker&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커는 컨테이너 기반의 가상화 플랫폼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 사용하면 인프라에서 애플리케이션을 분리하여&amp;nbsp;&lt;b&gt;컨테이너로 추상화&lt;/b&gt;시켜 소프트웨어를 빠르게 제공할 수 있다는 점이 장점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 앞서 말한 것 처럼 컨테이너의 성질이 있어 주어진 하나의 호스트OS안에서 여러 컨테이너를 동시에 실행 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 도커는 컨테이너의 라이프 사이클을 관리하고 어플리케이션을&amp;nbsp;&lt;b&gt;오케스트레이션&lt;/b&gt;(Work flow의 자동화)된 서비스로 배포할 수 있다.&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;&lt;b&gt;Docker의 아키텍처&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;527&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xkjeu/btsKxhPvEhg/dLuk6EjTuFPTwy0mIy4mEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xkjeu/btsKxhPvEhg/dLuk6EjTuFPTwy0mIy4mEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xkjeu/btsKxhPvEhg/dLuk6EjTuFPTwy0mIy4mEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxkjeu%2FbtsKxhPvEhg%2FdLuk6EjTuFPTwy0mIy4mEK%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;1009&quot; height=&quot;527&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;527&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker는 보이는 것처럼 Client - Server 아키텍처를 가지고 있다. 도커 클라이언트와 도커 데몬이RestApi를 사용하여 이를 통신한다.&lt;/p&gt;</description>
      <category>Tech-Interview</category>
      <author>LEE티씨</author>
      <guid isPermaLink="true">https://learngoeson.tistory.com/80</guid>
      <comments>https://learngoeson.tistory.com/80#entry80comment</comments>
      <pubDate>Mon, 4 Nov 2024 23:36:44 +0900</pubDate>
    </item>
    <item>
      <title>[HadnTris] STOMP 동작원리 정리</title>
      <link>https://learngoeson.tistory.com/79</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Websocket은 단순한 통신구조를 갖고 있기 때문에, 순수 Websocket만으로는 해당 메시지가 어떤 요청인지, 어떻게 처리해야하는지를 처리하는데 있어 까다로움을 가지고있다.&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;What is Stomp?&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;STOMP(Simple Text Oriented Messaging Protocol)&lt;/b&gt;은 메세징 전송을 효율적을 하기 위해 탄생한 웹 소켓 프로토콜 중 하나이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 브라우저에서 &lt;b&gt;발행 구독 패턴(Pub-Sub Pattern)을&lt;/b&gt; 기반으로 실시간 메시징을 지원할 수 있기 때문에 이점이 있어 이번 프로젝트에서도 테트리스들의 블록에 대한 Canvas의 위치값을 전송하기 위해 사용하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, STOMP를 사용하여 메세지의 헤더에 값을 줄 수 있어 헤더 값을 기반으로 통신 시 인증처리에 대한 이슈를 해결할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 그럼 STOMP의 동작 방식에 대해 이제 알아볼건데 그 전에 발행 구독 패턴부터 먼저 살펴보자&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  발행 구독 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵저버 패턴과 유사한 디자인 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 &lt;b&gt;&lt;code&gt;발행자(Publisher)&lt;/code&gt;와 &lt;code&gt;구독자(Subscriber)&lt;/code&gt;&lt;/b&gt;가 존재하고, 그 사이에 브로커( = Message Queue)가 존재하는 형태이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하자면, 발행자가 수신자에게 Message를 전달하는 것이아닌, 구독을 신청한 모든 수신자들에게 전달된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;발행자와 구독자의 특징&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;pub이 sub의 선언 위치나 존재를 알 필요가 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지 큐와 같은 브로커 역할을 하는 중간 지점에 메시지를 던져 두기만 하면, 브로커가 알아서 처리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;sub 역시 pub의 선언 위치나 존재를 알 필요가 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브로커에 할당된 작업만을 모니터링하고, 원하는 작업이 발생하면 할당받아 작업을 수행하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러므로 pub, sub은 서로 알 필요가 없으며, 브로커와의 통신만을 수행할 수 있다면 역할을 잘 수행하는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;브로커&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;메시지 큐&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;이를 메시지 지향 미들웨어(MOM = Message Oriented Middleware)를 지향하는 서비스라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 말했듯이 비동기처리와 확장성이 커 여러 프로세스에 메시지를 전달할 수도있으며, 장애전파 또한 일어나지않는다는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, Queue의 형태를 가지기에 트래픽이 몰리면 대기시간 지연으로 서비스가 망가질 가능성이있다.&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;Stomp의 동작방식&lt;/b&gt;&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Pm6HZ/btsIyDHEPB5/85xWhBl0vsiwxJ9rW4uIWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Pm6HZ/btsIyDHEPB5/85xWhBl0vsiwxJ9rW4uIWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Pm6HZ/btsIyDHEPB5/85xWhBl0vsiwxJ9rW4uIWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPm6HZ%2FbtsIyDHEPB5%2F85xWhBl0vsiwxJ9rW4uIWK%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;1280&quot; height=&quot;903&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;903&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;b&gt;&lt;code&gt;STOMP&lt;/code&gt; 는&lt;/b&gt; 기본적으로 &lt;b&gt;스트리밍 텍스트 지향 메시지 프로토콜이기에&lt;/b&gt; 메시지 Payload에는 &lt;b&gt;&lt;code&gt;Text&lt;/code&gt;&lt;/b&gt;나 &lt;b&gt;&lt;code&gt;Binary&lt;/code&gt;&lt;/b&gt; 데이터를 포함한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 양방향 네트워크 프로토콜 기반으로 동작을 &lt;b&gt;&lt;code&gt;발행-구독 패턴&lt;/code&gt;&lt;/b&gt;을 이용하여 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 떄문에 기본적인 구조를 가장 많이 사용되는 예시 중 하나인 채팅방에 비유해 publisher, subscriber 개념을 살펴보자&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;채팅방 생성 : pub / sub 구현을 위한 Topic이 생성됨&lt;/li&gt;
&lt;li&gt;채팅방 입장 : Topic 구독&lt;/li&gt;
&lt;li&gt;채팅방에서 메세지를 송수신 : 해당 Topic으로 메세지를 송신(pub), 메세지를 수신(sub)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 일련의 과정이 발생될 때에는 아래와 같은 부가 정보들을 Client에서 메시지 브로커 (Spring Default Broker는 Stomp Broker)가 이를 받아 처리하게된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Command 정보(Send, Subscribe 등)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Header&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Message&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Message Channel 정보&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 모든 구독과 전송은 TOPIC을 기반으로 독립적으로 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocket 서버는 비동기 이벤트 루프를 사용하여 다수의 클라이언트 연결을 동시에 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 TOPIC은 각각의 경로를 가진다고 생각하면 편하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STOMP를 사용하는 WebSocket 서버는 비동기 이벤트 루프와 메시지 브로커를 활용하여 각 &lt;b&gt;&lt;code&gt;TOPIC&lt;/code&gt;&lt;/b&gt;의 메시지를 독립적으로 비동기적으로 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해서 각 방의 메시지가 서로 간섭 없이 빠르고 효율적으로 전달된다.&lt;/p&gt;</description>
      <category>Project/HandTris 개발일지</category>
      <author>LEE티씨</author>
      <guid isPermaLink="true">https://learngoeson.tistory.com/79</guid>
      <comments>https://learngoeson.tistory.com/79#entry79comment</comments>
      <pubDate>Fri, 12 Jul 2024 21:02:50 +0900</pubDate>
    </item>
  </channel>
</rss>