<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>zioni__.log</title>
        <link>https://velog.io/</link>
        <description>꾸준히 기록하기</description>
        <lastBuildDate>Fri, 25 Sep 2026 09:12:21 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>zioni__.log</title>
            <url>https://velog.velcdn.com/images/zioni__/profile/79072745-97c2-4b7d-8fc2-8ce1109728b5/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. zioni__.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/zioni__" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[I/O 대기 작업에서 트랜잭션 범위를 좁혀야 하는 이유]]></title>
            <link>https://velog.io/@zioni__/LLM-%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%83%9C%EA%B9%85-%EB%B0%B0%EC%B9%98-187%EC%B4%88%EC%97%90%EC%84%9C-19.5%EC%B4%88%EB%A1%9C-%EC%A4%84%EC%9D%B4%EA%B8%B0</link>
            <guid>https://velog.io/@zioni__/LLM-%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%83%9C%EA%B9%85-%EB%B0%B0%EC%B9%98-187%EC%B4%88%EC%97%90%EC%84%9C-19.5%EC%B4%88%EB%A1%9C-%EC%A4%84%EC%9D%B4%EA%B8%B0</guid>
            <pubDate>Fri, 25 Sep 2026 09:12:21 GMT</pubDate>
            <description><![CDATA[<p>MOODI에서는 관광지 이미지와 설명을 바탕으로 해당 장소의 분위기를 분석하고, 무드 태그를 생성하고 있다.</p>
<p>예를 들어 관광지의 이미지와 소개 문장을 OpenAI Vision API에 전달하면, <code>COZY</code>, <code>NATURE</code>, <code>ROMANTIC</code>처럼 추천에 사용할 무드 정보를 반환받는다.</p>
<hr>
<h2 id="이미지-태깅은-어떤-흐름으로-동작할까">이미지 태깅은 어떤 흐름으로 동작할까?</h2>
<p>태깅 대상 Spot마다 다음 과정을 수행한다.</p>
<p>Spot 이미지 URL, 설명 조회
↓
OpenAI Vision API 호출
↓
무드 분석 결과 반환
↓
SpotMood 저장
↓
Spot 상태를 PUBLISHED로 변경</p>
<p>여기서 가장 오래 걸리는 구간은 <strong>OpenAI API 응답을 기다리는 시간</strong>이었다. </p>
<p>처음에는 관광지 하나씩 순서대로 태깅했다.</p>
<pre><code class="language-java">for (Spot spot : pendingSpots) {
    spotMoodTagger.tagSpot(spot);
}</code></pre>
<p>그리고 이 전체 과정을 하나의 트랜잭션으로 묶었다.</p>
<pre><code class="language-java">@Transactional(propagation = Propagation.REQUIRES_NEW)
public void tagSpot(Spot spot) {
    List&lt;String&gt; imageUrls = spotImageRepository.findBySpotId(spot.getId());
    String overview = spotTranslationRepository
        .findBySpotIdAndLocale(spot.getId(), KOREAN);

    MoodAnalysisResult result =
        moodAnalysisClient.analyze(imageUrls, overview);

    spotMoodRepository.save(spotMood);
    spot.publish();
    spotRepository.save(spot);
}</code></pre>
<h3 id="그-결과">그 결과...</h3>
<p><img src="https://velog.velcdn.com/images/zioni__/post/0425f910-160d-4f11-9a33-a5b5e60f7f72/image.png" alt=""></p>
<p>처리 시간이 정말 심각하게 오래 걸렸다.. 😅</p>
<p><strong>관광지 하나의 LLM API 응답을 기다린 뒤 다음 Spot을 처리하는 순차 구조였기 때문이다.</strong></p>
<hr>
<h2 id="트랜잭션-경계부터-분리하기">트랜잭션 경계부터 분리하기</h2>
<p>일단 가장 큰 문제는 <strong>LLM 응답을 기다리는 동안에도 DB 트랜잭션이 계속 열려 있다</strong>는 점이었다.</p>
<p>앞서 <a href="https://velog.io/@zioni__/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A0%80%EC%9E%A5%EC%86%8C%EB%A1%9C-GCP-GCS-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0">이미지 적재 과정</a>에 사용한 Virtual Thread로 여러 건을 동시에 호출하는 방식을 사용하려고 했다.</p>
<p>하지만 기존 구조 그대로 병렬화하면, 각 작업이 LLM 응답을 기다리는 <strong>2~5초 동안 DB 커넥션을 계속 점유</strong>하게 된다.</p>
<p>작업 1: DB 커넥션 점유 → LLM 응답 대기 → 저장
작업 2: DB 커넥션 점유 → LLM 응답 대기 → 저장
작업 3: DB 커넥션 점유 → LLM 응답 대기 → 저장</p>
<h3 id="핵심-문제">핵심 문제</h3>
<p>LLM API를 호출하는 동안에는 <strong>DB 작업을 하지 않으므로</strong>, 이 구간에서 트랜잭션을 유지할 이유가 없었다.</p>
<p>태깅 과정을 세 가지 단계로 나누면:</p>
<ol>
<li><strong>이미지와 설명 조회</strong></li>
<li><strong>LLM API 호출</strong></li>
<li><strong>분석 결과 저장과 Spot 상태 변경</strong></li>
</ol>
<p>이 중 <strong>원자성이 필요한 부분은 마지막 단계</strong>다.</p>
<p><strong>따라서 저장과 상태 변경만 같은 트랜잭션으로 묶는 방식으로 수정하였다.</strong></p>
<pre><code class="language-java">public long tagSpot(Spot spot) {
    // 1. DB 조회
    List&lt;String&gt; imageUrls = spotImageRepository.findBySpotId(spot.getId());
    String overview = spotTranslationRepository
        .findBySpotIdAndLocale(spot.getId(), KOREAN);

    // 2. LLM 호출
    llmSemaphore.acquireUninterruptibly();
    long llmStartNanos = System.nanoTime();

    MoodAnalysisResult result;
    try {
        result = moodAnalysisClient.analyze(imageUrls, overview);
    } finally {
        llmSemaphore.release();
    }

    long llmLatencyMs =
        (System.nanoTime() - llmStartNanos) / 1_000_000;

    // 3. 저장 구간만 트랜잭션 적용
    transactionTemplate.executeWithoutResult(status -&gt; {
        SpotMood spotMood = SpotMood.create(
            spot.getId(),
            result.vector(),
            result.tags(),
            result.confidence()
        );

        spotMoodRepository.save(spotMood);
        spot.publish();
        spotRepository.save(spot);
    });

    return llmLatencyMs;
}</code></pre>
<h3 id="변경-후-처리-흐름">변경 후 처리 흐름</h3>
<p>DB 조회
↓
커넥션 반환
↓
LLM API 호출
↓
DB 커넥션 미사용
↓
결과 저장 + 상태 변경
↓
짧은 트랜잭션 종료</p>
<p><code>TransactionTemplate</code>을 사용해 <strong>저장 구간에만 트랜잭션을 열었다.</strong> 같은 클래스 내부에서 <code>@Transactional</code> 메서드를 호출할 때 프록시가 적용되지 않는 문제도 피할 수 있었다.</p>
<hr>
<h2 id="virtual-thread로-여러-태깅-작업-실행하기">Virtual Thread로 여러 태깅 작업 실행하기</h2>
<p>이전 글들에서 본 Virtual Thread와 Semaphore를 조합한 방식이다.</p>
<ul>
<li><strong>글 1 (Chunk)</strong>: 데이터 검증을 트랜잭션 밖으로 분리</li>
<li><strong>글 2 (GCS)</strong>: Virtual Thread + Semaphore로 네트워크 I/O 병렬화</li>
</ul>
<p>여기서는 <strong>&quot;DB 트랜잭션 범위 분리&quot;</strong> 를 추가로 적용한다!</p>
<p>트랜잭션 범위를 분리한 뒤, 각 Spot 태깅 작업을 <strong>Virtual Thread로 병렬 실행</strong>했다.</p>
<pre><code class="language-java">Executors.newVirtualThreadPerTaskExecutor();</code></pre>
<h3 id="동시성-제어">동시성 제어</h3>
<p>Virtual Thread를 사용한다고 해서 <strong>모든 API 호출을 한꺼번에 보내면 안 된다.</strong> OpenAI API 요청 제한에 걸리거나, 429 오류가 발생할 수 있기 때문이다.</p>
<p>그래서 실제 LLM API 호출 구간은 <strong>Semaphore</strong> 로 제한했다.</p>
<pre><code class="language-java">Semaphore llmSemaphore = new Semaphore(concurrency);</code></pre>
<hr>
<h2 id="성능-측정-결과">성능 측정 결과</h2>
<p>동일한 태깅 작업을 대상으로 동시성 값만 변경해 처리 시간을 비교했다.</p>
<p><img src="https://velog.velcdn.com/images/zioni__/post/2bad1b7d-3279-4e6f-b083-6cec503bd706/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>동시성</th>
<th>전체 처리 시간</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>187초</td>
</tr>
<tr>
<td>10</td>
<td>19.5초</td>
</tr>
</tbody></table>
<p>동시성 10에서 전체 처리 시간이 <strong>187초에서 19.5초로 약 9.6배 단축</strong> 되었다!</p>
<p><strong>결과 분석:</strong></p>
<ul>
<li>평균 LLM 호출 시간: 약 3.5~3.9초</li>
<li>동시성 10에서 p95: 약 7초</li>
<li>429 오류: 없음</li>
</ul>
<hr>
<h2 id="정리">정리</h2>
<p>기존 구조처럼 LLM 호출까지 트랜잭션에 포함한 상태로 병렬 처리했다면, 작업 수가 늘수록 커넥션 점유 시간이 늘어나 처리 시간도 오래 걸렸을 것이다.</p>
<p><strong>핵심 최적화 기법:</strong></p>
<ul>
<li><strong>Virtual Thread</strong>: 여러 I/O 작업을 가볍게 실행</li>
<li><strong>Semaphore</strong>: LLM API 동시 요청 수 제한</li>
<li><strong>TransactionTemplate</strong>: 태그 저장과 상태 변경을 원자적으로 처리</li>
<li><strong>설정 파일</strong>: 코드 수정 없이 동시성 값 조정</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TourAPI 이미지 의존성을 줄이기 위한 GCS 도입]]></title>
            <link>https://velog.io/@zioni__/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A0%80%EC%9E%A5%EC%86%8C%EB%A1%9C-GCP-GCS-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@zioni__/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A0%80%EC%9E%A5%EC%86%8C%EB%A1%9C-GCP-GCS-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 18 Aug 2026 01:32:16 GMT</pubDate>
            <description><![CDATA[<p>MOODI에서는 관광지 사진을 제공하기 위해 관광데이터 API의 이미지 URL을 사용하고 있다.</p>
<p>기존에는 해당 URL을 DB에 그대로 저장하고 클라이언트에서 이미지를 조회하는 구조였다.</p>
<p>그런데 회의중에 팀원분이 이런 의견을 주셨다.</p>
<blockquote>
<p>이미지를 전부 외부 API에 의존하면, 해당 서버 상태에 우리 서비스도 영향을 받는 것 아닌가?</p>
</blockquote>
<p>실제로 이미지 서버 장애나 URL 변경이 발생하면 MOODI의 이미지 피드에도 바로 영향을 줄 수 있다.</p>
<p>그래서 외부 이미지를 우리 스토리지에 미리 저장해서 제공하는 방식으로 변경하기로 했다.</p>
<p>MOODI는 GCP 환경에서 운영하고 있기 때문에 이미지 저장소로 <strong>Google Cloud Storage(GCS)</strong> 를 선택했다.</p>
<hr>
<h2 id="gcp-gcs">GCP GCS</h2>
<p>Google Cloud Storage는 <strong>버킷</strong>을 주시므로 구성된 스토리지 서비스이다. 
버킷을 사용하면 스토리지 클래스와 연결된 개별 객체를 저장할 수 있을 뿐만 아니라, 폴더 내에서 개별 객체를 계층적으로 정리할 수도 있다.</p>
<blockquote>
<p><strong>버킷이란?</strong> 파일을 저장하는 큰 저장 공간을 말한다.</p>
</blockquote>
<p>예를 들어 이미지 100장을 저장한다면 버킷을 이미지마다 만드는 것이 아니라, <strong>버킷 하나를 만들고 그 안에 100개의 Object를 저장</strong>하는 식이다. </p>
<hr>
<h2 id="버킷-만들기">버킷 만들기</h2>
<h3 id="1-새-프로젝트-및-버킷-만들기">1. 새 프로젝트 및 버킷 만들기</h3>
<p><img src="https://velog.velcdn.com/images/zioni__/post/8ccfcbe2-e595-44ec-bbcd-620914e4b060/image.png" alt="">
<img src="https://velog.velcdn.com/images/zioni__/post/372a9cf4-e5d3-4c46-abe3-e0eee585cd56/image.png" alt=""></p>
<h3 id="2-권한-설정">2. 권한 설정</h3>
<p><img src="https://velog.velcdn.com/images/zioni__/post/4c39214b-6162-460a-83a4-f2cc0bc8a658/image.png" alt="">
<img src="https://velog.velcdn.com/images/zioni__/post/948313e4-16e5-408e-8c8a-570e7a852cd2/image.png" alt=""></p>
<hr>
<h2 id="이미지-적재-파이프라인-변경">이미지 적재 파이프라인 변경</h2>
<p>기존에는 CSV의 TourAPI 이미지 URL을 그대로 DB에 저장했다.</p>
<p><strong>기존:</strong></p>
<p>CSV → TourAPI URL → DB</p>
<p><strong>변경 후:</strong></p>
<p>CSV
↓
TourAPI 이미지 다운로드
↓
GCS 업로드
↓
GCS URL
↓
DB 적재</p>
<h3 id="의존성-추가">의존성 추가</h3>
<pre><code class="language-java">implementation(&quot;com.google.cloud:google-cloud-storage:2.49.0&quot;)</code></pre>
<h3 id="인터페이스-분리">인터페이스 분리</h3>
<p>애플리케이션 로직이 GCS 구현체에 직접 의존하지 않도록 업로드 인터페이스를 분리했다.</p>
<pre><code class="language-java">public interface SpotImageUploader {
    String upload(String sourceUrl, String fileName);
}</code></pre>
<p>실제 GCS 업로드는 <strong>GcsSpotImageUploader</strong> 가 담당하도록 했다.</p>
<hr>
<h2 id="이미지-15000장을-어떻게-처리할까">이미지 15,000장을 어떻게 처리할까?</h2>
<p>이미지 다운로드와 GCS 업로드는 <strong>네트워크 I/O 작업</strong>이다.</p>
<h3 id="io-작업의-특징">I/O 작업의 특징</h3>
<p>네트워크 I/O는 요청을 보낸 후 응답을 기다리는 <strong>&quot;대기 시간&quot;이 대부분</strong>이다.</p>
<p>예를 들어 이미지 하나를 처리하는데 <strong>2초</strong>가 걸린다면:</p>
<ul>
<li>다운로드 요청 → 1초 대기</li>
<li>업로드 요청 → 1초 대기</li>
</ul>
<p><strong>하지만 이 &quot;대기 시간&quot;동안 CPU는 놀고 있다.</strong></p>
<p>따라서 여러 이미지를 동시에 처리하면:</p>
<p>이미지1 다운로드 중(1초) = 이미지2<del>10도 동시 다운로드
이미지1 업로드 중(1초) = 이미지2</del>10도 동시 업로드</p>
<p><strong>이렇게 병렬 처리하면 시간을 획기적으로 단축</strong>할 수 있다.</p>
<p>그래서 <strong>Java 21 Virtual Thread</strong> 를 이용해 병렬 처리했다.</p>
<pre><code class="language-java">Executors.newVirtualThreadPerTaskExecutor();</code></pre>
<h3 id="동시성-제어">동시성 제어</h3>
<p>그런데 Virtual Thread를 사용한다고 해서 <strong>모든 요청을 동시에 보내도 되는 것은 아니었다.</strong> 
원본 이미지 서버나 GCS에 과도한 요청이 발생할 수 있기 때문이다.</p>
<p>그래서 동시 처리 개수를 <strong>최대 10개로 제한</strong>했다.</p>
<pre><code class="language-java">private static final int MAX_CONCURRENT_UPLOADS = 10;

private final Semaphore semaphore =
        new Semaphore(MAX_CONCURRENT_UPLOADS);</code></pre>
<h3 id="실행-흐름">실행 흐름</h3>
<p>1,000개 작업 생성
↓
Virtual Thread로 실행
↓
Semaphore
↓
최대 10개씩 이미지 다운로드 + GCS 업로드</p>
<hr>
<h2 id="업로드에-실패했을-때">업로드에 실패했을 때?</h2>
<p>이미지 하나가 실패했다고 해당 Spot 데이터까지 적재되지 않는 것은 피하고 싶었다. </p>
<p>따라서:</p>
<ul>
<li><strong>성공</strong> → GCS URL 저장</li>
<li><strong>실패</strong> → 기존 TourAPI URL 유지</li>
</ul>
<p>동시에 실패 건수는 <strong>별도로 집계</strong>해서 이후 확인 및 재시도가 가능하도록 했다.</p>
<h3 id="이미지-크기-제한">이미지 크기 제한</h3>
<p>또한, 외부에서 이미지를 직접 다운로드하기 때문에 <strong>비정상적으로 큰 응답을 방지하기 위해 이미지 크기는 최대 10MB로 제한</strong> 했다.</p>
<hr>
<h2 id="기존-데이터-마이그레이션">기존 데이터 마이그레이션</h2>
<p>신규 데이터는 이제 데이터를 적재하는 과정에서 바로 GCS에 저장되지만, DB에는 기존 TourAPI URL <strong>약 15,000건</strong>이 남아 있었다.</p>
<p>기존 데이터를 다시 전체 적재하는 대신 <strong>별도의 이미지 마이그레이션 로직</strong>을 만들었다.</p>
<p>기존 TourAPI URL 조회
↓
이미지 다운로드
↓
GCS 업로드
↓
DB URL 변경</p>
<p><img src="https://velog.velcdn.com/images/zioni__/post/6d5171ea-5e29-4e4d-96ce-23a398f0559e/image.png" alt=""></p>
<p>이 작업은 한 번 실행하고 종료되는 작업이므로 <strong>Cloud Run Job</strong> 으로 실행했고, 기존 이미지 데이터를 GCS로 이전시켰다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chunk 적용 후 오히려 느려진 이유 — 데이터 적재 성능 개선하기]]></title>
            <link>https://velog.io/@zioni__/Chunk-%EC%A0%81%EC%9A%A9-%ED%9B%84-%EC%98%A4%ED%9E%88%EB%A0%A4-%EB%8A%90%EB%A0%A4%EC%A7%84-%EC%9D%B4%EC%9C%A0-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%81%EC%9E%AC-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@zioni__/Chunk-%EC%A0%81%EC%9A%A9-%ED%9B%84-%EC%98%A4%ED%9E%88%EB%A0%A4-%EB%8A%90%EB%A0%A4%EC%A7%84-%EC%9D%B4%EC%9C%A0-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%81%EC%9E%AC-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 16 Aug 2026 16:31:47 GMT</pubDate>
            <description><![CDATA[<p>공모전 프로젝트 MOODI 에서 관광지 데이터를 DB에 적재하는 작업을 진행했다.</p>
<p>처음에는 JPA를 사용해 한 건씩 저장했는데, 로그를 확인해보니 <strong>약 10,000건을 적재하는 동안 약 37,000번의 DB 요청</strong>이 발생하고 있었다.</p>
<p>데이터 한 건마다 다음 작업이 반복됐기 때문이다.</p>
<pre><code class="language-text">데이터 1건
 → existsBy SELECT
 → Spot INSERT
 → Translation INSERT
 → Image INSERT</code></pre>
<p>게다가 각 데이터를 <code>REQUIRES_NEW</code> 트랜잭션으로 저장하고 있었다.
<code>REQUIRES_NEW</code>는 기존 트랜잭션이 있어도 잠시 중단하고 새 트랜잭션을 만들어서 실행한다.</p>
<p>한 행을 저장할 때마다 REQUIRES_NEW를 호출했다면, 10,000건을 저장하면서 10,000개의 트랜잭션이 각각 시작되고 커밋된 것이다.</p>
<h3 id="각각-커밋했을-때의-장점">각각 커밋했을 때의 장점</h3>
<p>장점은, 한 건이 실패해도 다른 건에 영향을 주지 않게 할 수 있다는 것이다.</p>
<p>하지만 대량 적재에서는 매번</p>
<pre><code>트랜잭션 시작 → SQL → COMMIT</code></pre><p>을 반복하니까 비용이 커진다.</p>
<p>실제로 4,877건을 기준으로 측정했을 때,</p>
<pre><code class="language-text">existsBy : 1.3초
save     : 8.3초
----------------
total    : 9.6초</code></pre>
<p>전체 시간의 약 87%가 저장 과정에서 발생했다.</p>
<h2 id="chunk란">Chunk란?</h2>
<p>기존 방식은 데이터 한 건마다 DB 작업을 수행하는 구조였다.</p>
<pre><code class="language-text">1건 처리 → commit
1건 처리 → commit
1건 처리 → commit
...</code></pre>
<p><strong>Chunk 처리</strong>는 여러 데이터를 일정 크기로 묶어서 처리하는 방식이다.</p>
<p>예를 들어 Chunk Size가 1,000이라면,</p>
<pre><code class="language-text">1~1000     → 하나의 Chunk
1001~2000  → 하나의 Chunk
2001~3000  → 하나의 Chunk
...</code></pre>
<p>처럼 처리할 수 있다.</p>
<p>이를 통해 매번 발생하던 DB 접근이나 트랜잭션 비용을 줄일 수 있다.</p>
<p>그래서 기존의 행 단위 처리를 <strong>약 1,000건 단위의 Chunk 처리</strong>로 변경했다.</p>
<p><code>existsBy</code> 역시 한 건씩 조회하지 않고 Chunk에 포함된 데이터의 존재 여부를 한 번에 조회하도록 변경했다.</p>
<pre><code class="language-text">4,877번 SELECT
     ↓
5번 SELECT</code></pre>
<p>당연히 빨라질 거라고 생각했다.</p>
<p><img src="https://velog.velcdn.com/images/zioni__/post/29288275-8612-48fb-ace3-ac23e9437b30/image.png" alt=""></p>
<p>그런데 결과는 오히려 더 느려졌다..!</p>
<h2 id="chunk로-묶었는데-왜-더-느려졌을까">Chunk로 묶었는데 왜 더 느려졌을까?</h2>
<p>원인은 데이터에 포함된 <strong>지원하지 않는 여행코스 6건</strong>이었다.</p>
<p>5개의 Chunk 중 4개는 정상 처리됐지만, 해당 데이터가 포함된 Chunk 하나가 실패했다.</p>
<p>문제는 실패 처리 방식이었다.</p>
<pre><code class="language-text">1,000건 Chunk
   ↓
잘못된 데이터 발견
   ↓
Chunk 전체 Rollback
   ↓
약 1,000건을 다시 개별 REQUIRES_NEW로 저장</code></pre>
<p>6건의 잘못된 데이터 때문에 정상 데이터까지 다시 처리하고 있었던 것이다.</p>
<p>결국 문제는 Chunk 자체가 아니라 <strong>검증과 트랜잭션의 범위</strong>에 있었다.</p>
<p>그래서 파싱과 데이터 검증을 트랜잭션 밖으로 분리했다.</p>
<pre><code class="language-text">Parsing &amp; Validation
        ↓
정상 데이터만 Chunk 생성
        ↓
DB 저장</code></pre>
<p>그 결과 문제가 있던 6건은 DB 작업 전에 제외됐고, <strong>5개의 Chunk가 모두 성공하면서 fallback도 0회</strong>가 되었다.</p>
<h2 id="jpa에서-jdbc-batch로">JPA에서 JDBC Batch로</h2>
<p>다음으로 저장 과정 자체를 개선했다.</p>
<p>일반적인 서비스 로직은 계속 JPA를 사용하되, 대량 데이터 적재만 별도의 JDBC 경로로 분리했다.</p>
<pre><code class="language-text">application/
 ├─ SpotBatchWriter
 └─ SpotImportRow

infrastructure/persistence/
 └─ SpotBatchJdbcWriter</code></pre>
<p>특히 Translation과 Image는 <code>JdbcTemplate.batchUpdate()</code>를 이용해 여러 INSERT를 묶어서 처리했다.</p>
<p>그 결과 4,877건 기준으로</p>
<p><img src="https://velog.velcdn.com/images/zioni__/post/f6a31982-d6f9-4336-90f3-324dded50c2b/image.png" alt=""></p>
<pre><code class="language-text">Before
9.6초 / 510건/s

After
3.2초 / 1,527건/s</code></pre>
<p><strong>약 3배의 처리량 개선</strong>을 확인할 수 있었다.</p>
<h2 id="upsert-전환">Upsert 전환</h2>
<p>마지막으로 <code>existsBy</code> 자체도 제거했다.</p>
<p>기존에는</p>
<pre><code class="language-text">SELECT exists
 → 없으면 INSERT
 → 있으면 SKIP</code></pre>
<p>했지만, PostgreSQL의 <code>INSERT ... ON CONFLICT DO UPDATE</code>를 사용하면 DB가 신규/기존 데이터를 판단할 수 있다.</p>
<pre><code class="language-text">UPSERT
 → 신규 데이터: INSERT
 → 기존 데이터: UPDATE</code></pre>
<p>덕분에 존재 여부를 미리 조회할 필요가 없어졌고, 재적재 시 변경된 관광지 정보도 반영할 수 있게 되었다.</p>
<h2 id="정리">정리</h2>
<p>최적화 과정은 다음과 같았다.</p>
<pre><code class="language-text">행 단위 JPA 저장
    ↓
Chunk 적용
    ↓
오히려 느려짐
    ↓
파싱/검증을 트랜잭션 밖으로 분리
    ↓
JDBC Batch
    ↓
Upsert</code></pre>
<p>처음에는 단순히 Chunk로 묶으면 빨라질 거라고 생각했다.</p>
<p>하지만 실제로는 <strong>Chunk 크기보다 트랜잭션 범위와 실패 처리 방식이 더 중요했다.</strong></p>
<p>또한 일반적인 CRUD에는 JPA를 그대로 사용하면서, 대량 적재처럼 성능이 중요한 작업만 JDBC로 분리하는 것도 하나의 방법이라는 것을 알게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[인턴 기록] WIL: 2~3주차 ]]></title>
            <link>https://velog.io/@zioni__/%EC%9D%B8%ED%84%B4-%EA%B8%B0%EB%A1%9D-WIL-23%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@zioni__/%EC%9D%B8%ED%84%B4-%EA%B8%B0%EB%A1%9D-WIL-23%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sat, 15 Aug 2026 09:24:34 GMT</pubDate>
            <description><![CDATA[<h1 id="💡이번주-학습-기록">💡이번주 학습 기록</h1>
<blockquote>
<h2 id="kotlin">Kotlin</h2>
</blockquote>
<p>자바는 클래스 파일을 JVM이 실행하며, 코틀린도 가상 머신인 JVM 에서 실행할 수 있다.</p>
<p>또한 클래스명과 파일명을 다르게 설정해도 되는 특징을 가진다.</p>
<h3 id="기본적인-변수-선언-방식">기본적인 변수 선언 방식</h3>
<pre><code class="language-jsx">val 변수명 : 타입 = 값</code></pre>
<h3 id="컬렉션-타입">컬렉션 타입</h3>
<pre><code class="language-jsx">val data1: Array&lt;Int&gt; = Array(3,{0}) </code></pre>
<h3 id="null-제어">NULL 제어</h3>
<p>코틀린은 <code>null</code> 과 <code>not null</code> 을 명확하게 구분해서 선언해야 한다.</p>
<pre><code class="language-jsx">var data2: Int?=10</code></pre>
<p>과 같이, 타입 뒤에 물음표를 추가하여 <code>Null</code> 허용으로 선언할 수 있다.</p>
<h3 id="생성자">생성자</h3>
<p>코틀린에서는 생성자를 <strong>주 생성자</strong>, <strong>보조 생성자</strong>로 구분한다.</p>
<pre><code class="language-jsx">class 클래스이름 constructor(){} //주 생성자 선언</code></pre>
<p>함수는 매개변수를 선언할 때 <code>var</code> 나 <code>val</code> 키워드를 추가할 수 없다.</p>
<p>주 생성자에서만 유일하게 <code>var</code> , <code>val</code> 키워드로 매개변수를 선언하여 클래스의 멤버 변수로 만들 수 있다. </p>
<pre><code class="language-jsx">class 클래스이름 {
    constructor()~~
}</code></pre>
<h3 id="오브젝트-클래스">오브젝트 클래스</h3>
<p>코틀린에서 오브젝트 클래스는 <strong>익명 클래스</strong> 를 만들 목적으로 사용한다.</p>
<p>선언과 동시에 객체를 생성하며, <code>object</code> 뒤에 콜론을 입력하고 그 뒤에 클래스의 상위 또는 인터페이스를 입력해야한다.</p>
<pre><code class="language-jsx">object: A {} // 클래스를 A 타입으로 선언한 것</code></pre>
<h3 id="컴패니언-클래스">컴패니언 클래스</h3>
<p>자바의 static 메서드와 비슷하다.</p>
<p>클래스 이름으로 멤버에 접근할 수 있게 하려면 <code>companion</code>  이라는 키워드로 선언해야한다.</p>
<h3 id="suspend">suspend</h3>
<p>함수 안에서 잠깐 기다렸다가 나중에 이어지는 코루틴 함수를 호출하면, 바깥 함수에도 <code>suspend</code>를 붙인다.</p>
<h3 id="runblocking">runBlocking</h3>
<p><code>runBlocking {}</code>은 코루틴 밖(테스트, <code>main</code> 등)에서 <code>suspend</code> 함수를 실행하기 위한 진입점이다.</p>
<ul>
<li>블록 내부의 코루틴이 끝날 때까지 현재 스레드는 기다린다.</li>
</ul>
<h3 id="runblocking---junit-테스트-흐름">runBlocking - JUnit 테스트 흐름</h3>
<pre><code>JUnit 테스트 시작
  → runBlocking으로 코루틴 실행
  → get 함수 호출
  → 콜백 응답이 올 때까지 테스트 대기
  → 성공하면 assert 검증
  → 테스트 종료</code></pre><p><code>runBlocking {}</code> 안은 람다 블록이다.
<code>return@runBlocking</code>의 <code>@runBlocking</code>은 어노테이션이 아니라 라벨이고, “이 블록까지만 반환한다”는 뜻이다.</p>
<pre><code>val result = runBlocking {
    if (isValid) {
        return@runBlocking &quot;성공&quot;
    }


    &quot;실패&quot;
}

return@runBlocking &quot;성공&quot;</code></pre><p>은:</p>
<p>테스트 함수 전체를 끝내는 게 아니라
runBlocking 블록의 결과를 &quot;성공&quot;으로 정한다.
따라서 result에 &quot;성공&quot;이 들어가게 된다.</p>
<h3 id="countdownlatch">CountDownLatch</h3>
<p>여러 작업이 끝날 때까지 한 스레드가 기다려야 할 때 사용한다.</p>
<pre><code>new CountDownLatch(3)</code></pre><ul>
<li>기다릴 작업 수를 <code>3</code>으로 설정</li>
<li>각 작업이 완료되면 <code>countDown()</code> 호출</li>
<li>기다리는 쪽에서 <code>await()</code> 호출</li>
<li>count가 <code>0</code>이 되면 <code>await()</code> 중인 스레드가 다시 실행됨</li>
</ul>
<hr>
<h1 id="🤝-느낀점">🤝 느낀점</h1>
<p>앞으로 업무에서 Kotlin 코드를 접하게 될 것 같다. 그동안은 자바로만 개발을 진행해서 코틀린에 대해서는 <strong>정말 기초적인 문법도 모르는 상황</strong>이었기 때문에 기본적인 지식이 필요했다.</p>
<p><strong>1. 모르는 건 혼자 찾아보지 말고 바로 물어보자..
2. 무조건 꼼꼼하게 확인! (코드작성이던 문서확인이던 모두..)</strong> </p>
<p>그리고 인턴이랑 취준 병행하는게 생각보다 쉽지 않을 것 같다..
이번주는 연휴도 있는 만큼, 다시 개인 공부를 열심히 해야할 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Firebase Hosting + Cloud Run으로 서버리스 배포 환경 구축하기]]></title>
            <link>https://velog.io/@zioni__/GCP-Cloud-Run-Firebase-Hosting%EC%9C%BC%EB%A1%9C-%EC%9D%B8%ED%94%84%EB%9D%BC-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@zioni__/GCP-Cloud-Run-Firebase-Hosting%EC%9C%BC%EB%A1%9C-%EC%9D%B8%ED%94%84%EB%9D%BC-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 10 Aug 2026 13:49:12 GMT</pubDate>
            <description><![CDATA[<p>2026 카카오×관광데이터 공모전에 참가하면서 처음으로 GCP를 직접 운영 환경에 도입하게 되었다. 
(AWS가 아닌 GCP를 쓴 이유는 프리티어 계정이 없었기 때문이다..)</p>
<p>Spring Boot로 만든 API를 어디에 배포할지 고민하다가** Google Cloud Platform<strong>의 여러 서비스를 검토했고, 최종적으로 **Cloud Run</strong>을 선택했다.</p>
<hr>
<p>GCP에서 애플리케이션을 배포하는 방식은 여러 가지가 있다. 대표적으로는:</p>
<ul>
<li>Compute Engine</li>
<li>App Engine</li>
<li>Cloud Run</li>
</ul>
<h1 id="compute-engine">Compute Engine</h1>
<blockquote>
<p>Compute Engine은 GCP에서 가장 전통적인 방식으로, Ubuntu 같은 OS가 설치된 VM을 빌려서 직접 운영한다. </p>
</blockquote>
<p>AWS의 EC2와 같은 역할을 한다.</p>
<pre><code>Internet
   ↓
Compute Engine
   ↓
Spring Boot
   ↓
Cloud SQL</code></pre><h1 id="cloud-run">Cloud Run</h1>
<p><img src="https://velog.velcdn.com/images/zioni__/post/dc2af9b4-7ee6-4873-93c3-aeab40607a06/image.png" alt=""></p>
<blockquote>
<p>Cloud Run은 요청 또는 이벤트를 통해 호출 가능한 컨테이너를 실행할 수 있는 완전 관리형 애플리케이션 플랫폼이다.</p>
</blockquote>
<p>Google Cloud 에서는 대표적으로 <strong>Cloud Functions</strong> 와 <strong>Cloud Run</strong> 이라는 2가지 Serverless 서비스를 제공하고 있다.</p>
<p>Cloud Run은, ** Docker 컨테이너를 올리면 Google이 실행 환경을 전부 관리한다!**</p>
<pre><code>Internet
   ↓
Cloud Run (컨테이너 실행)
   ↓
Spring Boot (Docker 이미지)
   ↓
Cloud SQL</code></pre><h2 id="👩💻-왜-선택했을까">👩‍💻 왜 선택했을까?</h2>
<p><img src="https://velog.velcdn.com/images/zioni__/post/0eaed6d0-444e-47e8-837b-0aa8aae7727f/image.png" alt=""></p>
<h4 id="1-컨테이너로-패키징하고-쉽게-앱을-활성화-할-수-있다">1. 컨테이너로 패키징하고 쉽게 앱을 활성화 할 수 있다.</h4>
<ul>
<li>인프라를 직접 관리 할 필요없이, <strong>Docker 이미지를 통해 쉽게 배포할 수 있다.</strong><h4 id="2-오토-스케일링을-제공한다">2. 오토 스케일링을 제공한다.</h4>
</li>
<li>요청량에 따라 <strong>자동으로 인스턴스를 증감한다</strong>. 야간에는 0개까지 줄어들어 비용 절감도 가능하다.<h4 id="3-gcp-서비스와-자연스러운-통합이-이루어진다">3. GCP 서비스와 자연스러운 통합이 이루어진다.</h4>
</li>
<li>** Cloud SQL**: 관리형 PostgreSQL 데이터베이스</li>
<li><strong>Artifact Registry</strong>: Docker 이미지 저장소</li>
<li><strong>Secret Manager</strong>: DB 자격증명 같은 민감 정보 관리</li>
<li><strong>VPC 및 IAM</strong>: 보안 정책<h4 id="4-프로젝트-규모에-맞는-구성">4. 프로젝트 규모에 맞는 구성</h4>
</li>
<li>복잡한 인프라를 구축할 이유가 없었다. </li>
</ul>
<h2 id="💥-한계점">💥 한계점</h2>
<p>Cloud Run은 리버스 프록시, SSL, 로드밸런싱 기능까지 제공하지만, 커스텀 도메인을 붙이기 위해서는 <strong>Cloud Load Balancer</strong>를 따로 만들어야 했다. 그런데, 여기서 두가지 비용이 발생한다.</p>
<ul>
<li><strong>고정 IP 할당 비용</strong>: 매월 고정 요금</li>
<li><strong>Load Balancer 운영 비용</strong>: 트래픽에 따른 요금</li>
<li><strong>설정 복잡도</strong>: 자체 서명 인증서 관리, SSL 업데이트 등</li>
</ul>
<h2 id="해결책---firebase-hosting">해결책 - Firebase Hosting</h2>
<p>대신 <strong>Firebase Hosting</strong>을 프록시로 사용하기로 결정했다.</p>
<pre><code>Internet (dev-api.moodi.kr)
   ↓
Firebase Hosting (무료)
   ↓ (rewrite 설정)
Cloud Run
</code></pre><p>Firebase Hosting의 장점:</p>
<ul>
<li>커스텀 도메인 연결</li>
<li>SSL 자동 발급/갱신</li>
<li>rewrite 기능: firebase.json에서 모든 요청을 Cloud Run으로 포워드</li>
<li>무료!</li>
</ul>
<p>firebase.json 설정은 간단하다:</p>
<pre><code>json
{
  &quot;hosting&quot;: {
    &quot;rewrites&quot;: [
      {
        &quot;source&quot;: &quot;/**&quot;,
        &quot;run&quot;: {
          &quot;serviceId&quot;: &quot;moodi-api&quot;,
          &quot;region&quot;: &quot;asia-northeast1&quot;
        }
      }
    ]
  }
}</code></pre><p>이 한 줄로 모든 HTTP/HTTPS 요청이 Cloud Run으로 가고, SSL도 자동으로 관리된다. </p>
<hr>
<h2 id="최종-정리">최종 정리</h2>
<h3 id="cloud-run-선택-이유">Cloud Run 선택 이유</h3>
<ul>
<li><strong>서버 관리 부담 없음</strong> - 인프라 관리 비용 절감</li>
<li><strong>자동 스케일링</strong> - 요청에 따른 유동적 운영</li>
<li><strong>GCP 서비스 통합</strong> - Cloud SQL, Secret Manager 등과 자연스럽게 연결</li>
<li><strong>배포 자동화</strong> - GitHub Actions + Workload Identity Federation</li>
<li><strong>비용 효율성</strong> - 작은 프로젝트 규모에 맞는 선택</li>
</ul>
<p>참고: <a href="https://docs.cloud.google.com/run/docs/overview/what-is-cloud-run?hl=ko">https://docs.cloud.google.com/run/docs/overview/what-is-cloud-run?hl=ko</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[인턴 기록] WIL: 1주차 회고]]></title>
            <link>https://velog.io/@zioni__/%EC%9D%B8%ED%84%B4-%EA%B8%B0%EB%A1%9D-WIL-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@zioni__/%EC%9D%B8%ED%84%B4-%EA%B8%B0%EB%A1%9D-WIL-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 02 Aug 2026 13:50:50 GMT</pubDate>
            <description><![CDATA[<p><strong>2026.08.02</strong></p>
<blockquote>
<p>인턴 시작!</p>
</blockquote>
<h2 id="👩💻">👩‍💻</h2>
<p><img src="https://velog.velcdn.com/images/zioni__/post/344aa742-fd95-4dc9-8b6f-08907cf6b27e/image.jpeg" alt=""> 
이번주부터 인턴을 시작하게 되었다!</p>
<p>앞으로 인턴을 하며 적어도 2주에 한번씩 회고를 작성해보려고 한다. 
회고의 목적은 &quot;기록&quot;이다. 기록하지 않으면 다 흘러가버릴 정보와 기억들이다 ..</p>
<p>이번주는 개발을 위한 세팅과 인수인계를 하며 시간을 보냈다._ (기존 코드를 보면서 이런 구조로 되어있구나~ 이런 흐름으로 진행되는구나~를 이해하는 시간이었다.)_</p>
<p>부서에서 진행중인 업무는 한마디로 <strong>검증 개발</strong>이다. 그중에서도 차량 소프트웨어 검증 개발을 다룬다.</p>
<h3 id="검증-개발verification--validation">검증 개발(Verification &amp; Validation)</h3>
<p>제품이나 소프트웨어가 기획서의 요구 사항에 맞게 올바르게 만들어졌는지, 그리고 사용자의 실제 목적을 충족하는지 체계적으로 테스트하고 확인하는 과정이다.</p>
<blockquote>
<p><strong>그렇다면, 검증과 확인의 주요 차이는 무엇일까?</strong></p>
</blockquote>
<ul>
<li><p><strong>검증(Verification)</strong>은 개발자 관점, 사양서와 설계대로 만들어졌는지 확인하는 과정을 말하며, </p>
</li>
<li><p><strong>확인(Validation)</strong>은 사용자 관점, 실제 사용자의 요구와 목적에 부합하는지 테스트하는 과정이라고 말할 수 있다.</p>
</li>
</ul>
<p><strong>QA</strong>나 <strong>Tester</strong>와 비슷한 업무라고 생각할 수 있지만, 직접 경험해보니 실제로는 개발적인 요소가 훨씬 많이 요구되는 직무라는 것을 느꼈다.</p>
<p>이번주에 관련해서 배운 것과 공부한 개념들을 정리해보려고 한다.</p>
<h2 id="💡-이번주-학습-기록">💡 이번주 학습 기록</h2>
<h3 id="ccts-란">CCTS 란?</h3>
<p>차량의 VHAL(Vehicle HAL) 프로퍼티가 명세서대로 정확하게 구현되어 있는지 검증하는 테스트 코드이다. </p>
<h3 id="aaos">AAOS</h3>
<p><strong>Android Automotive OS</strong>의 약자로, 자동차에 내장되는 인포테인먼트 플랫폼이다. (쉽게 말하면 차량 소프트웨어)</p>
<p>AAOS는 several Layer로 이루어져 있고, 각각의 레이어는 고유의 역할과 요소를 가진다.</p>
<p>Android Automotive 환경에서는 차량 내부 통신 인터페이스인 <strong>VHAL(Vehicle HAL)</strong>과 상위 *<em>IVI *</em>애플리케이션 사이를 연결하는 서비스가 필수적이다.</p>
<h3 id="can-simulator">CAN Simulator</h3>
<p>&quot;<strong>차가 없어도 차가 있는 것처럼 CAN 통신을 흉내 내는 도구</strong>&quot;라고 생각하면 된다.</p>
<p>예를 들면,</p>
<pre><code>CAN Simulator가 &quot;차량 속도 = 80km/h&quot; 메시지를 전송
테스트 프로그램이 해당 메시지를 수신
시스템이 정상적으로 속도를 표시하거나 특정 API를 호출하는지 검증</code></pre><p>이런 식으로 입력(CAN 메시지) → 시스템 동작 → 결과 검증을 자동화하는 데 많이 사용할 수 있다.</p>
<h3 id="log-level">Log Level</h3>
<p>로그 레벨은 시스템에서 로그의 중요도를 구분하기 위한 기준이다.</p>
<pre><code>ALL &lt; DEBUG &lt; INFO &lt; WARN &lt; ERROR &lt; FATAL &lt; OFF</code></pre><p>이중 운영업무에서 자주 볼 수 있는 로그는 <code>DEBUG</code>, <code>INFO</code>, <code>WARN</code>, <code>ERROR</code>이다.
<img src="https://velog.velcdn.com/images/zioni__/post/f80dd7d9-5af2-4946-9eae-067cf6481e62/image.png" alt="">
<a href="https://www.tutorialspoint.com/log4j/log4j_logging_levels.htm">https://www.tutorialspoint.com/log4j/log4j_logging_levels.htm</a></p>
<h2 id="🤝-느낀점">🤝 느낀점</h2>
<p>면접 볼 때, 면접관님이 이 업무에서 중요한건 무엇보다 <strong>꼼꼼함</strong>, <strong>커뮤니케이션 능력</strong>이라고 말씀해 주셨다. </p>
<p>업무는 명세서를 기반으로 테스트 코드를 작성하는 것에서 시작된다. 이후 오류가 발생하면 수만 줄에 달하는 로그를 직접 확인하며 분석하고 원인을 찾아야 한다. 결국 <strong>작은 단서 하나도 놓치지 않는 꼼꼼함</strong>이 검증 개발에서 가장 중요한 기본기가 된다.</p>
<p>평소 나는 다른 사람의 피드백을 잘 받아들이는 편이라고 생각했다. </p>
<p>하지만 모든 의견을 그대로 수용하기보다는, <strong>내 설계의 근거를 논리적으로 설명하고 필요한 부분은 설득하는 능력</strong> 역시 중요하다는 점을 배웠다. (무조건 고집을 부리는 것이 아니라, 근거 있는 판단을 할 수 있어야 한다는 의미이다)</p>
<p>또한 내가 짠 코드에 대해 정확하게 왜 이렇게 짰는지, 어떻게 설계한 건지 이유를 정확하게 설명할 수 있어야 한다. (물론 일반적인 소프트웨어 개발을 할 때도 당연한 부분이다!) </p>
<p><img src="https://velog.velcdn.com/images/zioni__/post/7b891272-813c-4ca7-a823-0489734b9551/image.png" alt=""></p>
<p>그리고 첫주차라 그런지 출근은 정말 힘든거구나.. 를 느꼈다 ...ㅎㅎ</p>
<p>아무튼 회사에서 다음주에 더 열심히 배워야 할 것 같다 ... 🙂</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[대규모 시스템 설계 기초] 2장 : 개략적인 규모 추정]]></title>
            <link>https://velog.io/@zioni__/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%88-2%EC%9E%A5-%EA%B0%9C%EB%9E%B5%EC%A0%81%EC%9D%B8-%EA%B7%9C%EB%AA%A8-%EC%B6%94%EC%A0%95</link>
            <guid>https://velog.io/@zioni__/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%88-2%EC%9E%A5-%EA%B0%9C%EB%9E%B5%EC%A0%81%EC%9D%B8-%EA%B7%9C%EB%AA%A8-%EC%B6%94%EC%A0%95</guid>
            <pubDate>Sun, 05 Jul 2026 03:01:28 GMT</pubDate>
            <description><![CDATA[<h2 id="2의-제곱-수">2의 제곱 수</h2>
<table>
<thead>
<tr>
<th align="right">2의 x제곱</th>
<th align="right">근사치</th>
<th>이름</th>
<th>축약형</th>
</tr>
</thead>
<tbody><tr>
<td align="right">2¹⁰</td>
<td align="right">약 1천</td>
<td>Thousand</td>
<td>1KB (Kilobyte)</td>
</tr>
<tr>
<td align="right">2²⁰</td>
<td align="right">약 100만</td>
<td>Million</td>
<td>1MB (Megabyte)</td>
</tr>
<tr>
<td align="right">2³⁰</td>
<td align="right">약 10억</td>
<td>Billion</td>
<td>1GB (Gigabyte)</td>
</tr>
<tr>
<td align="right">2⁴⁰</td>
<td align="right">약 1조</td>
<td>Trillion</td>
<td>1TB (Terabyte)</td>
</tr>
<tr>
<td align="right">2⁵⁰</td>
<td align="right">약 1000조 (10¹⁵)</td>
<td>Quadrillion</td>
<td>1PB (Petabyte)</td>
</tr>
</tbody></table>
<h2 id="모든-프로그래머가-알아야-하는-응답-지연-값">모든 프로그래머가 알아야 하는 응답 지연 값</h2>
<ul>
<li>메모리는 빠르지만 디스크는 아직도 느리다.</li>
<li>디스크 탐색은 가능한 한 피하라.</li>
<li>단순한 압축 알고리즘은 빠르다.</li>
<li>데이터를 인터넷으로 전송하기 전에 가능한 압축해야한다.</li>
<li>데이터 센터는 보통 여러 region에 분산되어 있고, 센터들 간에 데이터 주고받는 데에 시간이 소요된다.</li>
</ul>
<h2 id="고가용성">고가용성</h2>
<blockquote>
<p><strong>시스템이 오랜 시간동안 지속적으로 중단 없이 운영될 수 있는 능력</strong>을 지칭하는 용어이다.</p>
</blockquote>
<ul>
<li>고가용성은 퍼센트(percent)로 표현하는데, 100%는 시스템이 단 한 번도 중단된 적이 없었음을 의미한다.</li>
<li>대부분의 서비스가 99% ~ 100% 사이의 값을 갖는다.</li>
</ul>
<h3 id="장애-허용fault-tolerant과의-차이">장애 허용(Fault Tolerant)과의 차이</h3>
<ul>
<li>장애 허용은 장애가 생기더라도 시스템이 이상 없이 동작할 수 있도록 보장하는 특성이고, 고가용성은 빠르게 복구하는 특성이라고 한다.</li>
</ul>
<h2 id="저장소-요구량-추정">저장소 요구량 추정</h2>
<h3 id="가정">가정</h3>
<ul>
<li>월간 능동 사용자는 3억명</li>
<li>50%의 사용자가 트위터를 매일 사용</li>
<li>평균적으로 매일 2건의 트윗을 올림</li>
<li>미디어 포함 트윗은 10%</li>
<li>데이터는 5년간 보관됨</li>
</ul>
<h3 id="추정">추정</h3>
<ul>
<li><p>*<em>QPS (Query Per Second) *</em>추정치</p>
<ul>
<li><strong>Daily Active User, DAU</strong> = 3억 * 50% = 1.5억명 (50%의 사용자가 트위터를 매일 사용하므로, 월간 사용자의 50%는 매일 트위터를 사용하는 사용자의 수와 같다.)</li>
<li>QPS = 1.5억 * 2트윗/24시간/3600초 = 약 3500</li>
<li><strong>최대 QPS (Peek QPS) = 2*QPS = 약 7000</strong></li>
</ul>
</li>
<li><p>미디어 저장을 위한 저장소 요구량</p>
<ul>
<li>평균 트윗 크기 </li>
<li>tweet_id = 64바이트</li>
<li>텍스트 = 140바이트</li>
<li>미디어 = 1MB</li>
<li>미디어 저장소 요구량 = 1.5억 * 2 * 10% * 1MB = 30TB/일 </li>
<li>5년간 미디어를 보관하기 위한 저장소 요구량 : 30TB * 365 * 5 = 약 55PB</li>
</ul>
</li>
</ul>
<h3 id="팁">팁</h3>
<ul>
<li>가정은 적어두기</li>
<li>단위를 붙이기</li>
<li>계산은 근사치를 활용하기</li>
<li>QPS, 최대 QPS, 저장소 요구량, 캐시 요구량, 서버 수를 추정해야함</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[대규모 시스템 설계 기초] 1장 : 사용자 수에 따른 규모 확장성]]></title>
            <link>https://velog.io/@zioni__/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%88-1%EC%9E%A5-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%88%98%EC%97%90-%EB%94%B0%EB%A5%B8-%EA%B7%9C%EB%AA%A8-%ED%99%95%EC%9E%A5%EC%84%B1</link>
            <guid>https://velog.io/@zioni__/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%88-1%EC%9E%A5-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%88%98%EC%97%90-%EB%94%B0%EB%A5%B8-%EA%B7%9C%EB%AA%A8-%ED%99%95%EC%9E%A5%EC%84%B1</guid>
            <pubDate>Sat, 04 Jul 2026 14:31:49 GMT</pubDate>
            <description><![CDATA[<h2 id="단일-서버">단일 서버</h2>
<ol>
<li>사용자는 도메인 이름을 이용하여 웹사이트에 접속한다.<ul>
<li>이 과정에서 도메인 이름을 DNS에 질의해, IP 주소로 변환하는 과정이 필요</li>
</ul>
</li>
<li>DNS 조회 결과로 IP 주소가 반환된다.</li>
<li>해당 IP 주소로 HTTP 요청이 전달된다.</li>
<li>요청을 받은 웹 서버는 HTML 페이지나 JSON 형태의 응답을 반환한다.</li>
</ol>
<blockquote>
<p>웹 어플리케이션  ➡️ 서버 구현용 언어, 클라이언트 구현용 언어 사용
모바일 앱  ➡️ HTTP 프로토콜을 주로 이용</p>
</blockquote>
<p><br/><br/></p>
<h2 id="데이터베이스">데이터베이스</h2>
<p><strong>1. 관계형 데이터베이스 관리 시스템(RDBMS)</strong></p>
<ul>
<li>mysql, Oracle, PostgreSQL…</li>
</ul>
<p><strong>2. 비 관계형 데이터베이스</strong>
다음과 같은 경우에 선택한다.</p>
<ul>
<li>낮은 지연 응답 시간</li>
<li>다루는 데이터가 비정형일 경우</li>
<li>데이터를 직렬화하거나 역직렬화 할 수 있기만 하면 될 경우</li>
<li>아주 많은 양의 데이터를 저장할 필요가 있을 경우</li>
</ul>
<h3 id="데이터베이스-다중화">데이터베이스 다중화</h3>
<blockquote>
<p>서버 사이에 주-부 관계를 설정하고 데이터의 원본은 주 서버에, 사본은 부 서버에 저장하는 방식이다.</p>
</blockquote>
<p><strong>주 데이터베이스</strong>: 쓰기 연산, 데이터베이스 변경 명령어 실행</p>
<p><strong>부 데이터베이스</strong>: 읽기 연산. 데이터 베이스 를 변경하는 명령어는 주 데이터베이스로만 연결되어야 한다.</p>
<h3 id="장점">장점</h3>
<ul>
<li>읽기 연산은 부 데이터베이스 서버들로 분산되어 병렬 처리 되므로, 성능이 좋아진다.</li>
<li>데이터베이스 서버 가운데 일부가 파괴되어도 데이터는 안정성이 보장될 수 있다.</li>
<li>데이터를 여러 지역에 복제해 둠으로써, 하나의 데이터 베이스 서버에 장애가 발생하더라도 다른 서버에 있는 대이터를 가져와 계속 서비스 할 수 있다.</li>
</ul>
<p><br/><br/></p>
<h2 id="데이터베이스의-규모-확장">데이터베이스의 규모 확장</h2>
<p>데이터 베이스의 규모를 확장하는 데는 두가지 접근법이 있다.</p>
<h3 id="수직적-규모-확장법scale-up">수직적 규모 확장법(Scale Up)</h3>
<blockquote>
<p>기존 서버에 더 많은, <strong>고성능의 자원을 증설하는 방법</strong>이다.</p>
</blockquote>
<h3 id="수평적-규모-확장법">수평적 규모 확장법</h3>
<blockquote>
<p>샤딩이라고도 불리는데, <strong>더 많은 서버를 추가</strong>함으로써 성능을 향상시키는 방법이다.</p>
</blockquote>
<h4 id="샤딩의-주의점">샤딩의 주의점</h4>
<ol>
<li>샤드 키(Shard Key)를 잘 선택해야 한다.</li>
<li>JOIN이 어려워진다.(당연히..데이터가 나누어져 있기 때문에)</li>
<li>트랜잭션이 어려워진다. (서로 다른 DB라 분산 트랜잭션이 필요함)</li>
<li>운영이 복잡해진다.</li>
</ol>
<pre><code>인덱스

↓

쿼리 튜닝

↓

캐시(Redis)

↓

Read Replica

↓

샤딩</code></pre><p>샤딩은 구조 자체를 바꾸는 작업이라 비용이 크기 때문에 마지막 수단으로 고려하는 경우가 많다.</p>
<p><br/><br/></p>
<h2 id="로드밸런서">로드밸런서</h2>
<blockquote>
<p>부하 분산 집합에 속한 웹 서버들에게 <strong>트래픽 부하를 고르게 분산</strong>하는 역할이다.</p>
</blockquote>
<p><br/><br/></p>
<h2 id="캐시">캐시</h2>
<blockquote>
<p>캐시는 값비싼 연산 결과 또는 자주 참조되는 데이터를 메모리 안에 두고, 뒤이은 요청이 보다 빠르게 처리될 수 있도록 하는 저장소이다.</p>
</blockquote>
<ul>
<li>캐시는 <strong>휘발성 메모리</strong> 이므로, 중요한 데이터는 <strong>지속적 저장소</strong>에 두어야 한다.</li>
<li>캐시 서버를 여러개로 분산시켜 <strong>단일 장애 지점(SPOF)</strong>이 생기는 문제를 해결할 수 있다.</li>
<li><strong>캐시 메모리를 과할당</strong>하여, <strong>캐시의 성능을 개선</strong>시킬 수 있다.</li>
<li>캐시에 메모리가 가득 찼을 경우, <strong>LRU</strong>나 <strong>LFU</strong>, <strong>FIFO</strong> 정책을 사용한다.</li>
</ul>
<p><strong>단일 장애 지점</strong>: 어떤 특정 지점에서의 장애가 전체 시스템의 동작을 중단시켜 버릴 수 있는 경우, 해당 지점을 단일 장애 지점(SPOF) 이라고 부른다.</p>
<p><strong>일관성</strong>: 데이터 저장소의 원본과 캐시 내의 사본이 같은지 여부</p>
<p><strong>데이터 방출 정책</strong>: 캐시의 메모리가 찼을 때, 기존 데이터를 내보내기 위한 정책</p>
<ul>
<li>LFU : 사용된 빈도가 가장 낮은 데이터 내보내는 정책</li>
<li>LRU : 마지막으로 사용된 시점이 가장 오래된 데이터를 내보내는 정책</li>
<li>FIFO : 가장 먼저 캐시에 들어온 데이터를 가장 먼저 내보내는 정책
<br/><br/></li>
</ul>
<h2 id="cdn">CDN</h2>
<blockquote>
<p>CDN은 정적 콘텐츠를 전송하는데 쓰이는, <strong>지리적으로 분산된 서버의 네트워크</strong>이다.</p>
</blockquote>
<h3 id="동작-과정">동작 과정</h3>
<ol>
<li>사용자가 image URL을 이용해 이미지에 접근한다. </li>
<li>CDN 서버의 캐시에 이미지가 없는 경우, 서버는 원본 서버에 요청하여 파일을 가져온다.</li>
<li>원본 서버가 파일을 CDN 서버에 반환한다.</li>
<li>CDN 서버는 파일을 캐시하고 사용자에게 반환한다.</li>
</ol>
<p>➡️ 정적 컨텐츠를 웹서버가 아닌 CDN을 사용하여 더 나은 성능을 보장할 수 있고, 캐시를 사용하여 데이터베이스 부하를 줄일 수 있다. </p>
<p><br/><br/></p>
<h2 id="무상태-웹-계층">무상태 웹 계층</h2>
<blockquote>
<p>무상태 아키텍처는, <strong>서버가 사용자의 이전 요청 정보를 기억하지 않는 구조</strong>를 말한다.</p>
</blockquote>
<p>상태정보를 보관하는 서버는 클라이언트 정보(상태)를 유지하여 요청들 사이에 공유되도록 하지만, 무상태 서버에는 이런 장치가 없다.</p>
<p>상태 서버의 경우 같은 클라이언트로부터의 요청은 항상 같은 서버로 전송되어야 하는데, 이는 로드밸런서에 부담을 주게 된다.</p>
<h3 id="무상태가-필요한-이유">무상태가 필요한 이유</h3>
<p><strong>1. 서버를 쉽게 늘릴 수 있다 (Scale Out)</strong></p>
<p>트래픽이 많아졌다고 가정해보자.</p>
<p>상태를 저장하면</p>
<pre><code>Server A (세션 있음)
Server B (세션 없음)</code></pre><p>로드밸런서가 마음대로 보내면 로그인 정보가 없어서 오류가 난다.</p>
<p>그래서 <strong>Sticky Session(고정 세션)</strong> 같은 추가 작업이 필요하게 된다.</p>
<p>반면 Stateless는</p>
<pre><code>Server A
Server B
Server C
Server D</code></pre><p><strong>모든 서버가 동일하게 동작하기 때문에,</strong> 불필요하게 추가작업을 진행할 필요가 없다.</p>
<h4 id="2-장애에-강하다">2. 장애에 강하다</h4>
<blockquote>
<p>Server1 다운
⬇️
Server2가 처리</p>
</blockquote>
<p>이런 구조는 단순하고, 안정적이며, 규모 확장이 쉽다.</p>
<p><strong>3. 로드밸런싱이 쉬워진다</strong>
요청을 처리할 때 사용자별로 다른 서버로 보낼 필요성이 없기 떄문이다.
<strong>어떤 서버가 요청을 처리해도 결과는 같다.</strong></p>
<p><strong>4. 서버 메모리를 덜 사용한다</strong></p>
<p><br/><br/></p>
<h2 id="데이터-센터">데이터 센터</h2>
<p>장애가 없는 상황에서 사용자는 가장 가까운 데이터 센터로 안내되는데, 통상 이 절차를 지리적 라우팅이라고 부른다.</p>
<p>이 데이터 센터중 하나에 심각한 장애가 발생하면, 모든 트래픽은 장애가 없는 데이터 센터로 전송된다.</p>
<p>다중 데이터센터를 만들기 위해 고려해야 할 조건</p>
<ul>
<li>트래픽 우회 - 올바른 데이터 센터로 트래픽을 보내기</li>
<li>데이터 동기화 - 데이터 센터마다 별도의 데이터베이스 다중화 하기</li>
<li>테스트와 배포 - 여러 위치에서 애플리케이션 테스트, 자동화 배포가 모든 데이터 센터에 동일하게 설치되도록 하기
<br/><br/><h2 id="메세지-큐">메세지 큐</h2>
</li>
</ul>
<blockquote>
<p>메세지 큐에 일단 보관된 메시지는 소비자가 꺼낼때까지 안전히 보관된다는 특성을 보장하는 비동기 통신 컴포넌트</p>
</blockquote>
<p>한마디로 <strong>지금 당장 처리하지 않아도 되는 일을 나중에 처리하도록 중간에 맡겨놓는 시스템이</strong>다.</p>
<ul>
<li>메세지 큐를 이용하면 서비스 또는 서버 간 결합이 느슨해져서, 규모 확장성이 보장되어야 하는 안정적 애플리케이션을 구성하기 좋다.</li>
<li>Publish/Producer는 메세지 큐에 발행한다. 큐에는 보통 Consumer/Subscribe가 메세지를 꺼내서 동작을 수행한다.
<br/><br/></li>
</ul>
<h2 id="정리">정리</h2>
<ul>
<li>웹 계층은 무상태 계층으로</li>
<li>모든 계층에 다중화 도입</li>
<li>가능한 한 많은 데이터 캐시</li>
<li>여러 데이터센터 지원</li>
<li>정적 컨텐츠 → CDN 통해 서비스</li>
<li>데이터 계층 → 샤딩을 통해 그 규모를 확장할 것</li>
<li>각 계층은 독립적 서비스로 분한</li>
<li>자동화 도구 활용, 시스템 지속적 모니터링</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발 블로그 이전 👩‍💻]]></title>
            <link>https://velog.io/@zioni__/%EA%B0%9C%EB%B0%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9D%B4%EC%A0%84</link>
            <guid>https://velog.io/@zioni__/%EA%B0%9C%EB%B0%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9D%B4%EC%A0%84</guid>
            <pubDate>Thu, 02 Jul 2026 11:52:18 GMT</pubDate>
            <description><![CDATA[<p>기존에 개발 블로그로 <strong>Tistory</strong> 를 쓰고 있었는데,
<strong>Velog</strong> 로 이전하게 되었다!
<img src="https://velog.velcdn.com/images/zioni__/post/41c3fb0d-d795-4998-856e-a4240d2ccc63/image.jpeg" alt=""></p>
<blockquote>
<p>기존 블로그 : <a href="https://0618amy.tistory.com/">https://0618amy.tistory.com/</a></p>
</blockquote>
<p>취업을 준비하면서 느낀 건 .. 이력서만으로는 프로젝트에서 내가 어떤 고민을 했고, 어떤 방식으로 해결했는지에 대한 사고과정을 다 담기가 힘들다는 것이다.</p>
<p>이전부터 개발 블로그가 있긴 했지만, 경험을 정리 할 때 <strong>Notion</strong>을 더 자주 이용했었다.</p>
<h4 id="1-따로-웹브라우저-들어갈-필요-없이-접근성이-좋고">1. 따로 웹브라우저 들어갈 필요 없이 접근성이 좋고,</h4>
<h4 id="2-마크다운-형식으로-쉽고-빠르게-글-작성이-가능하기-때무네">2. 마크다운 형식으로 쉽고 빠르게 글 작성이 가능하기 때무네..</h4>
<p><img src="https://velog.velcdn.com/images/zioni__/post/11d63fb0-0968-42cc-ae3e-6ec3f5738381/image.png" alt=""></p>
<p>전부 다 깔끔하게 글 정리를 해놓은 건 아니다.
<em>약간 뒤죽박죽인 부분도 있긴 하지만 일단 나 혼자 보는거니까 .. ㅎㅎ</em></p>
<p>그런데 요즘 노션 페이지수가 많아지다 보니 오히려 찾아보기도 불편해졌고, 하나의 흐름으로 정리하기도 쉽지 않았다. (면접 준비하면서 느꼈다 ㅠㅠ)
다시 티스토리를 써볼까 했지만.. 글 작성 방식이 나에게는 불편하게 느껴졌다. </p>
<p>그래서 가독성이 좋고, 에디터가 편한 <strong>Velog</strong>로 넘어오게 되었다 !</p>
<p>기존에도 마크다운 형식으로 정리를 했었어서, 확실히 티스토리보다 편하게 느껴지는 것 같다. 옆에 미리보기로 바로 뜨는것도 좋은듯??</p>
<p>앞으로는 내가 배우고 경험한 것들을 벨로그에 꾸준히 정리해보려고 한다. 앞으로는 노션 대신 쓸 것 같다 ..!</p>
<p>취업 준비 기록도 하나씩 올려야지 .. 👩‍💻👩‍💻</p>
]]></description>
        </item>
    </channel>
</rss>