<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>The Answer is 42</title>
    <link>https://twodc.tistory.com/</link>
    <description>매일의 배움과 고민, 그리고 작은 성장을 담은 개발자의 성장일지입니다.</description>
    <language>ko</language>
    <pubDate>Sat, 1 Aug 2026 00:36:40 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>twodc</managingEditor>
    <image>
      <title>The Answer is 42</title>
      <url>https://tistory1.daumcdn.net/tistory/6327599/attach/43176ede75bd40e498abc83dfaef57dc</url>
      <link>https://twodc.tistory.com</link>
    </image>
    <item>
      <title>AWS 인프라 - ECS Fargate</title>
      <link>https://twodc.tistory.com/138</link>
      <description>&lt;h2 data-path-to-node=&quot;3&quot; data-ke-size=&quot;size26&quot;&gt;EC2를 넘어 ECS Fargate로 서버리스 컨테이너와 무중단 배포 아키텍처 구축&lt;/h2&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;저번 글에선 AWS의 가장 기본적인 컴퓨팅 리소스인 EC2 인스턴스 하나에 Docker를 띄워 서비스를 배포하는 과정을 다뤘다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;하지만 서비스를 운영하다 보니 몇 가지 한계에 부딪혔다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,0&quot;&gt;배포 시 다운타임 발생:&lt;/b&gt; 새로운 버전을 배포할 때마다 잠시 서비스를 멈춤&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,0&quot;&gt;서버 관리의 부담:&lt;/b&gt; EC2 인스턴스의 OS 보안 패치, 스케일링 등을 직접 신경 써야함.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,2,0&quot;&gt;수동 배포의 번거로움:&lt;/b&gt; 코드를 짜는 시간보다 배포 스크립트를 돌리고 확인하는 데 쓰는 에너지가 큼.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 이러한 문제를 해결하기 위해 &lt;b data-index-in-node=&quot;25&quot; data-path-to-node=&quot;7&quot;&gt;ECS Fargate&lt;/b&gt;를 도입하여 &lt;b data-index-in-node=&quot;43&quot; data-path-to-node=&quot;7&quot;&gt;서버리스(Serverless) 아키텍처&lt;/b&gt;로 전환하고,&lt;br /&gt;&lt;b data-index-in-node=&quot;72&quot; data-path-to-node=&quot;7&quot;&gt;GitHub Actions&lt;/b&gt;를 통해 &lt;b data-index-in-node=&quot;91&quot; data-path-to-node=&quot;7&quot;&gt;무중단 자동 배포 파이프라인&lt;/b&gt;을 구축한 과정을 공유한다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;8&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size23&quot;&gt;1. 전체 아키텍처 개요 (As-Is vs To-Be)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;가장 큰 변화는 &lt;b&gt;'관리형 서비스'&lt;/b&gt;의 적극적인 도입이다.&lt;br /&gt;기존에는 Public Subnet의 EC2 하나가 모든 짐을 짊어졌다면, 이번에는 역할별로 리소스를 분리하고 가용성을 높였다.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size20&quot;&gt;  전체 아키텍처 다이어그램&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Service Architecture.png&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;951&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0isfT/dJMcahC39BP/CU9043R1WQ7xS6XxUEGdKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0isfT/dJMcahC39BP/CU9043R1WQ7xS6XxUEGdKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0isfT/dJMcahC39BP/CU9043R1WQ7xS6XxUEGdKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0isfT%2FdJMcahC39BP%2FCU9043R1WQ7xS6XxUEGdKk%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;1354&quot; height=&quot;951&quot; data-filename=&quot;Service Architecture.png&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;951&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13&quot;&gt;주요 변경 포인트:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;14&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,0,0&quot;&gt;Compute:&lt;/b&gt; EC2 &amp;rarr; &lt;b data-index-in-node=&quot;15&quot; data-path-to-node=&quot;14,0,0&quot;&gt;ECS Fargate&lt;/b&gt; (서버 관리 불필요)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,1,0&quot;&gt;Network:&lt;/b&gt; Public Subnet 단일 배포 &amp;rarr; &lt;b data-index-in-node=&quot;31&quot; data-path-to-node=&quot;14,1,0&quot;&gt;Private Subnet&lt;/b&gt; 배포 (보안 강화)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,2,0&quot;&gt;Load Balancer:&lt;/b&gt; 직접 접속 &amp;rarr; &lt;b data-index-in-node=&quot;23&quot; data-path-to-node=&quot;14,2,0&quot;&gt;ALB (Application Load Balancer)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,3,0&quot;&gt;Deploy:&lt;/b&gt; 수동 스크립트 &amp;rarr; &lt;b data-index-in-node=&quot;18&quot; data-path-to-node=&quot;14,3,0&quot;&gt;GitHub Actions + ECR&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-path-to-node=&quot;15&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;16&quot; data-ke-size=&quot;size23&quot;&gt;2. 왜 ECS Fargate인가? (Server vs Serverless)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;컨테이너 오케스트레이션 도구를 선택할 때, 사실상 표준인 Kubernetes(EKS)와 AWS Native인 ECS 사이에서 고민했다.&lt;br /&gt;결론적으로 &lt;b data-index-in-node=&quot;84&quot; data-path-to-node=&quot;18&quot;&gt;ECS Fargate&lt;/b&gt;를 선택했는데, 이유는 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;19&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,0,0&quot;&gt;낮은 러닝 커브와 운영 비용:&lt;/b&gt; 쿠버네티스는 강력하지만 클러스터 자체를 관리하고 유지보수하는 데 상당한 학습 비용과 리소스가 든다. 반면 ECS는 AWS 서비스들과 가장 직관적으로 통합되어 있어 설정이 간편하다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,1,0&quot;&gt;비즈니스 로직 집중:&lt;/b&gt; 현재 단계에서는 인프라 자체를 복잡하게 관리하는 것보다, 안정적인 환경 위에서 애플리케이션 기능을 빠르게 배포하는 것이 우선이라 판단했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-path-to-node=&quot;20&quot; data-ke-size=&quot;size20&quot;&gt;Fargate Launch Type&lt;/h4&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;ECS에는 두 가지 모드가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;22&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,0,0&quot;&gt;EC2 모드:&lt;/b&gt; 내가 띄운 EC2 위에서 컨테이너를 관리 (인스턴스 관리 필요)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,1,0&quot;&gt;Fargate 모드:&lt;/b&gt; 인스턴스 없이 컨테이너 실행에 필요한 CPU/Memory만 할당받아 사용 (Serverless)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;저는 &lt;b data-index-in-node=&quot;3&quot; data-path-to-node=&quot;23&quot;&gt;인프라 관리보다 애플리케이션 로직에 집중&lt;/b&gt;하고 싶었기에, OS 관리가 전혀 필요 없는 &lt;b data-index-in-node=&quot;50&quot; data-path-to-node=&quot;23&quot;&gt;Fargate&lt;/b&gt;를 선택했다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;24&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;25&quot; data-ke-size=&quot;size23&quot;&gt;3. 배포의 시작: Dockerfile과 ECR&lt;/h3&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;Fargate는 '이미지' 기반으로 동작하기에 애플리케이션을 불변(Immutable)의 이미지로 만드는 것이 첫걸음이다.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;27&quot; data-ke-size=&quot;size20&quot;&gt;3.1 Dockerfile 최적화 (Multi-stage Build)&lt;/h4&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;빌드 환경과 실행 환경을 분리하여 이미지 크기를 줄였다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiP4-LHxeuRAxUAAAAAHQAAAAAQhxE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;&lt;span&gt;Dockerfile&lt;/span&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# Build stage
FROM gradle:8.5-jdk21 AS build
WORKDIR /app
COPY build.gradle settings.gradle gradlew ./
COPY gradle gradle
COPY src src
RUN gradle bootJar --no-daemon -x test

# Runtime stage
FROM eclipse-temurin:21-jre
WORKDIR /app
# 보안을 위해 root가 아닌 별도 유저 생성
RUN groupadd -r spring &amp;amp;&amp;amp; useradd -r -g spring spring
USER spring:spring
COPY --from=build /app/build/libs/*.jar app.jar
EXPOSE 8080
ENV JAVA_OPTS=&quot;-Xms256m -Xmx512m -XX:+UseContainerSupport&quot;
ENTRYPOINT [&quot;sh&quot;, &quot;-c&quot;, &quot;java $JAVA_OPTS -Dspring.profiles.active=${SPRING_PROFILE:-default} -jar app.jar&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-path-to-node=&quot;30&quot; data-ke-size=&quot;size20&quot;&gt;3.2 ECR (Elastic Container Registry)&lt;/h4&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;빌드된 이미지를 저장할 저장소이다. Docker Hub의 Private Repository와 같지만, AWS IAM과 연동되어 ECS가 이미지를 가져올 때 별도의 인증 번거로움이 줄어든다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;32&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;33&quot; data-ke-size=&quot;size23&quot;&gt;4. 핵심 구성 요소: Task Definition과 Service&lt;/h3&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;ECS를 이해하려면 &lt;b&gt;Task Definition(작업 정의)&lt;/b&gt;과 &lt;b data-index-in-node=&quot;39&quot; data-path-to-node=&quot;34&quot;&gt;Service&lt;/b&gt;의 개념을 잡아야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;35&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;35,0,0&quot;&gt;Task Definition:&lt;/b&gt; &quot;어떤 컨테이너를 띄울 것인가?&quot; (청사진)
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;35,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용할 이미지 URI&lt;/li&gt;
&lt;li&gt;CPU/Memory 할당량 (Fargate는 이 설정에 따라 과금이 결정)&lt;/li&gt;
&lt;li&gt;환경 변수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;35,1,0&quot;&gt;Service:&lt;/b&gt; &quot;어떻게 실행하고 유지할 것인가?&quot;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;35,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Task의 개수 (Desired Count)&lt;/li&gt;
&lt;li&gt;로드 밸런서 연결&lt;/li&gt;
&lt;li&gt;배포 전략 (Rolling Update 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 oddventure-task라는 패밀리 이름으로 정의하고, DB 접속 정보나 API 키 같은 민감 정보는 코드에 하드코딩하지 않고 실행 시점에 SSM에서 안전하게 주입받도록 구성했다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;37&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;38&quot; data-ke-size=&quot;size23&quot;&gt;5. 네트워크 구성과 보안 (Private Subnet &amp;amp; NAT Gateway)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;39&quot; data-ke-size=&quot;size16&quot;&gt;아키텍처 그림을 보면 ECS Task들이 &lt;b data-index-in-node=&quot;23&quot; data-path-to-node=&quot;39&quot;&gt;Private Subnet&lt;/b&gt;에 위치해 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;40&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;40,0,0&quot;&gt;보안:&lt;/b&gt; 외부에서 직접 접근이 불가능하여 데이터베이스나 애플리케이션이 공격에 노출될 위험이 줄어든다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;40,1,0&quot;&gt;트래픽 흐름:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;40,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;40,1,1,0,0&quot;&gt;Inbound:&lt;/b&gt; 사용자는 인터넷 &amp;rarr; &lt;b data-index-in-node=&quot;20&quot; data-path-to-node=&quot;40,1,1,0,0&quot;&gt;ALB (Public Subnet)&lt;/b&gt; &amp;rarr; ECS Task (Private Subnet) 경로로 접속한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;40,1,1,1,0&quot;&gt;Outbound:&lt;/b&gt; ECS Task가 외부 API(Steam, Grid, Groq 등)를 호출하거나 ECR에서 이미지를 당겨올 때는 &lt;b data-index-in-node=&quot;73&quot; data-path-to-node=&quot;40,1,1,1,0&quot;&gt;NAT Gateway&lt;/b&gt;를 통해 나간다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;41&quot; data-ke-size=&quot;size16&quot;&gt;Private Subnet에 있는 DB나 서버에 접근해서 디버깅해야 할 때는 다이어그램 좌측 하단에 있는 &lt;b data-index-in-node=&quot;60&quot; data-path-to-node=&quot;41&quot;&gt;Bastion Host&lt;/b&gt;를 통해 터널링하여 접근하도록 구성했다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;42&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;43&quot; data-ke-size=&quot;size23&quot;&gt;6. 무중단 배포의 핵심: ALB와 Rolling Update&lt;/h3&gt;
&lt;p data-path-to-node=&quot;44&quot; data-ke-size=&quot;size16&quot;&gt;기존 EC2 배포의 가장 큰 불만이었던 '배포 중 서비스 중단'은 ECS의 &lt;b data-index-in-node=&quot;42&quot; data-path-to-node=&quot;44&quot;&gt;Rolling Update&lt;/b&gt; 전략으로 해결되었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;45&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;새로운 버전의 Task가 실행&lt;/li&gt;
&lt;li&gt;ALB가 새 Task의 Health Check( /actuator/health )를 수행&lt;/li&gt;
&lt;li&gt;상태가 'Healthy'가 되면 트래픽을 새 Task로 흘려보냄&lt;/li&gt;
&lt;li&gt;기존 버전의 Task를 종료(Draining)함&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;46&quot; data-ke-size=&quot;size16&quot;&gt;이 과정 덕분에 사용자는 배포가 일어나는지조차 모르게 서비스를 계속 이용할 수 있다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;47&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;48&quot; data-ke-size=&quot;size23&quot;&gt;7. 자동화의 완성: GitHub Actions&lt;/h3&gt;
&lt;p data-path-to-node=&quot;49&quot; data-ke-size=&quot;size16&quot;&gt;이제 코드를 main 브랜치에 푸시하기만 하면 이 모든 과정이 자동으로 일어난다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;50&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;50&quot;&gt;워크플로우 요약:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;51&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;51,0,0&quot;&gt;Checkout &amp;amp; Login:&lt;/b&gt; 코드 체크아웃 및 AWS ECR 로그인&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;51,1,0&quot;&gt;Build &amp;amp; Push:&lt;/b&gt; Docker 이미지를 빌드하고 ECR에 푸시 (태그는 Git SHA 활용)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;51,2,0&quot;&gt;Deploy:&lt;/b&gt; 최신 Task Definition을 다운받아 새 이미지로 교체 후 ECS 서비스 업데이트&lt;/li&gt;
&lt;/ol&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiP4-LHxeuRAxUAAAAAHQAAAAAQiBE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# .github/workflows/ecs-deploy.yml
    - name: Fill in the new image ID in the Amazon ECS task definition
      id: task-def
      uses: aws-actions/amazon-ecs-render-task-definition@v1
      with:
        task-definition: task-definition.json
        container-name: oddventure-container
        image: ${{ steps.login-ecr.outputs.registry }}/oddventure-repo:${{ github.sha }}

    - name: Deploy Amazon ECS task definition
      uses: aws-actions/amazon-ecs-deploy-task-definition@v1
      with:
        task-definition: ${{ steps.task-def.outputs.task-definition }}
        service: oddventure-service
        cluster: oddventure-cluster
        wait-for-service-stability: true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-path-to-node=&quot;53&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;54&quot; data-ke-size=&quot;size23&quot;&gt;8. 마치며: EC2에서 Fargate로 넘어오며 느낀 점&lt;/h3&gt;
&lt;p data-path-to-node=&quot;55&quot; data-ke-size=&quot;size16&quot;&gt;EC2에서 직접 Docker를 띄우던 방식에서 ECS Fargate로 넘어오면서 느낀 장단점은 명확했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;56&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;56,0,0&quot;&gt;장점:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;56,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;56,0,1,0,0&quot;&gt;운영 스트레스 해소:&lt;/b&gt; 서버가 죽었나? 디스크가 찼나? 걱정할 필요가 없어짐&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;56,0,1,1,0&quot;&gt;안정성:&lt;/b&gt; Multi-AZ와 자동화된 로드 밸런싱으로 서비스가 훨씬 견고해짐&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;56,0,1,2,0&quot;&gt;배포 속도:&lt;/b&gt; 클릭 한 번(Merge)으로 배포가 완료되니 개발 생산성이 비약적으로 상승&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;56,1,0&quot;&gt;단점(고려사항):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;56,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;56,1,1,0,0&quot;&gt;초기 설정 난이도:&lt;/b&gt; VPC, Subnet, IAM Role, Security Group 등 네트워크와 권한에 대한 이해가 필수적이라 초기 러닝 커브가 있었다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;56,1,1,1,0&quot;&gt;비용:&lt;/b&gt; 소규모 트래픽에서는 t2.micro 같은 EC2 인스턴스보다 Fargate 비용이 조금 더 나올 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;57&quot; data-ke-size=&quot;size16&quot;&gt;이번 마이그레이션을 통해 인프라에 대한 걱정을 덜고, &lt;b data-index-in-node=&quot;30&quot; data-path-to-node=&quot;57&quot;&gt;Oddventure&lt;/b&gt;의 핵심 비즈니스 로직 개발에 더 집중할 수 있게 되었다.&lt;/p&gt;</description>
      <category>TIL</category>
      <author>twodc</author>
      <guid isPermaLink="true">https://twodc.tistory.com/138</guid>
      <comments>https://twodc.tistory.com/138#entry138comment</comments>
      <pubDate>Fri, 2 Jan 2026 17:14:48 +0900</pubDate>
    </item>
    <item>
      <title>AWS 인프라 기초 설계</title>
      <link>https://twodc.tistory.com/137</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;AWS 인프라 기초 설계 &amp;middot; VPC &amp;middot; RDS &amp;middot; &lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;ElastiCache&lt;/span&gt; &amp;middot; EC2로 배포하기&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Spring Boot 애플리케이션을 실제 서비스 환경에 배포하려면 단순히 EC2 서버에 JAR 파일을 올리는 것만으로는 부족하다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;네트워크(VPC), 데이터베이스(RDS), 캐시(&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;ElastiCache&lt;/span&gt;), 애플리케이션 실행 환경(EC2)을&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;어떻게 설계하고 연결했는지에 따라 서비스의 안정성과 확장성이 결정된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 글에서는 AWS에서 &lt;b&gt;VPC + RDS(MySQL) + &lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;ElastiCache&lt;/span&gt; + EC2&lt;/b&gt;를 구성하고 연결한 과정을 정리한다.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;EC2 배포 아키텍쳐.png&quot; data-origin-width=&quot;571&quot; data-origin-height=&quot;421&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vuKvV/dJMcahJHsCU/iAFKMZauXFFFQj9U3wmDFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vuKvV/dJMcahJHsCU/iAFKMZauXFFFQj9U3wmDFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vuKvV/dJMcahJHsCU/iAFKMZauXFFFQj9U3wmDFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvuKvV%2FdJMcahJHsCU%2FiAFKMZauXFFFQj9U3wmDFk%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;571&quot; height=&quot;421&quot; data-filename=&quot;EC2 배포 아키텍쳐.png&quot; data-origin-width=&quot;571&quot; data-origin-height=&quot;421&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;＃전체 아키텍처 개요&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이번에 구성한 인프라의 핵심 요소는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;VPC &lt;/b&gt;-&amp;nbsp;독립된 가상 네트워크&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;퍼블릭 서브넷&lt;/b&gt; - 인터넷과 통신되는 영역&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;프라이빗 서브넷&lt;/b&gt; - 외부에서 직접 접근할 수 없는 내부 전용 영역&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;RDS(MySQL) &lt;/b&gt;-&amp;nbsp;서비스 데이터 저장소&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;ElastiCache&amp;nbsp;&lt;/b&gt;-&amp;nbsp;세션&amp;nbsp;및&amp;nbsp;캐시&amp;nbsp;담당&amp;nbsp;Redis&amp;nbsp;호환&amp;nbsp;인메모리&amp;nbsp;캐시&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;EC2&lt;/b&gt; - Spring Boot 애플리케이션 실행 서버&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;보안 그룹&lt;/b&gt; - 인바운드/아웃바운드 트래픽 제어&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;애플리케이션 요청은 다음 흐름으로 동작한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;User &amp;rarr; Internet &amp;rarr; EC2 &amp;rarr; RDS / ElastiCache&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;모든 내부 통신은 &lt;b&gt;VPC 내부 IP&lt;/b&gt;로 이루어지고, 외부에 노출되는 것은 EC2의 퍼블릭 엔드포인트뿐이다.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Lt3O3/dJMcaacIV51/eoGhkvYKVghklYnT01rYak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Lt3O3/dJMcaacIV51/eoGhkvYKVghklYnT01rYak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Lt3O3/dJMcaacIV51/eoGhkvYKVghklYnT01rYak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLt3O3%2FdJMcaacIV51%2FeoGhkvYKVghklYnT01rYak%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;830&quot; height=&quot;578&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;＃VPC&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;VPC(Virtual Private Cloud)는 AWS 안에서 사용하는 &lt;b&gt;나만의 가상 사설 네트워크&lt;/b&gt;다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;쉽게 말해, 내 서비스의 모든 리소스가 들어가는 &lt;b&gt;전용 네트워크 구역&lt;/b&gt;이라고 보면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;VPC를 쓰는 이유&lt;/span&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;내 서비스 리소스를 &lt;b&gt;다른 계정/다른 서비스와 논리적으로 완전히 분리&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;퍼블릭/프라이빗 서브넷을 나눠 &lt;b&gt;외부에 공개할 것과 숨길 것을 분리&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;보안 그룹, NACL, 라우팅 테이블로 &lt;b&gt;트래픽 흐름을 세밀하게 제어&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;＃RDS&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;RDS는 AWS에서 제공하는 관리형 관계형 데이터베이스 서비스다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;직접 EC2에 MySQL을 설치하는 대신, &lt;b&gt;백업, 패치, 장애 조치&lt;/b&gt;까지 AWS가 관리해주는 것이 특징이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;왜 EC2에 직접 MySQL을 설치하지 않았는가?&lt;/span&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;백업/복구/스냅샷을 직접 관리해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;장애 발생 시 수동으로 재부팅/장애조치가 필요하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;애플리케이션과 DB가 같은 서버에 있으면 &lt;b&gt;리소스 경합&lt;/b&gt;이 생긴다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;반면 RDS를 사용하면,&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;자동 백업, 스냅샷, 멀티 AZ, 모니터링 등 운영 기능을 대부분 AWS가 제공&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;애플리케이션 서버(EC2)와 &lt;b&gt;역할 분리&lt;/b&gt;가 명확해진다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Spring Boot에서 RDS 연결 설정 예시&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;spring:
 datasource:
  url: jdbc:mysql://oddventure-db.xxxxxx.ap-northeast-2.rds.amazonaws.com:3306/oddventure
  username: oddventure_user
  password: &amp;lt;RDS_PASSWORD&amp;gt;
  driver-class-name: com.mysql.cj.jdbc.Driver&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;＃ElastiCache&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;443&quot; data-start=&quot;293&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;ElastiCache는 AWS에서 제공하는 &lt;b&gt;관리형 인메모리 캐시 서비스&lt;/b&gt;로, &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;고속 읽기&amp;middot;쓰기가 필요한 데이터를 안정적으로 처리할 수 있도록 설계된 서비스다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;589&quot; data-start=&quot;445&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;RDS가 디스크 기반의 영속 저장소라면, &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;ElastiCache는 &lt;b&gt;속도를 위해 메모리에 데이터를 저장하는 캐시 계층&lt;/b&gt; 역할을 한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 구조를 통해 자주 조회되는 데이터는 캐시에서 빠르게 응답하고, &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;필요한 경우에만 RDS에 접근하도록 구성했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;589&quot; data-start=&quot;445&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;745&quot; data-start=&quot;591&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이번 프로젝트에서 ElastiCache의 내부 엔진으로는 &lt;b&gt;Valkey&lt;/b&gt;를 사용했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Valkey는 Redis와 &lt;b&gt;명령어, 프로토콜, 클라이언트까지 모두 호환되는 Redis 계열 인메모리 데이터베이스&lt;/b&gt;로,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기능적인 사용 방식은 사실상 Redis와 거의 동일하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1062&quot; data-start=&quot;747&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Valkey를 선택한 배경에는 &lt;b&gt;Redis 라이선스 변경 이슈&lt;/b&gt;가 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;2024년 이후 Redis는 기존의 자유로운 BSD 라이선스에서 &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;상업적 사용에 제약이 있는 형태로 라이선스 정책이 변경되었고,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이로 인해 AWS, Google, Azure 같은 주요 클라우드 벤더들이 Redis를 기존 방식 그대로 서비스하기 어려워졌다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그 대안으로 Redis 7.2 기반의 오픈소스 포크 프로젝트인 &lt;b&gt;Valkey가 공식적으로 채택되었고&lt;/b&gt;,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;AWS 역시 ElastiCache의 Redis 대안으로 &lt;b&gt;Valkey 계열을 적극적으로 지원&lt;/b&gt;하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1274&quot; data-start=&quot;1064&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;다만 개인적으로는 지금까지의 프로젝트에서는 &lt;b&gt;Redis만 사용해왔고&lt;/b&gt;,&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1274&quot; data-start=&quot;1064&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;번 프로젝트에서는 AWS 환경에서의 공식 대응 방향을 따라가 보기 위해 &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;ElastiCache + Valkey 조합을 선택했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;실제 사용 경험 측면에서는 Redis와 체감 차이는 거의 없었고, &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기존 Redis 클라이언트 설정과 코드도 거의 그대로 사용할 수 있었다.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1527&quot; data-start=&quot;1384&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;결론적으로 기능만 보면 Redis와 큰 차이는 없지만, &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;운영 관점과 장기적인 라이선스 리스크까지 고려했을 때&lt;br /&gt;ElastiCache + Valkey 조합이 더 안전한 선택&lt;/b&gt;이라고 판단해 &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이번 프로젝트에서는 해당 구성을 캐시 계층으로 채택했다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt; 캐시 계층이 필요한 이유는 다음과 같다. &lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1647&quot; data-start=&quot;1607&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;로그인 세션을 DB에만 저장하면 요청마다 DB I/O가 발생한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1704&quot; data-start=&quot;1648&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;자주 조회되지만 자주 바뀌지 않는 데이터는 DB 대신 캐시에서 조회하는 것이 훨씬 효율적이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1749&quot; data-start=&quot;1705&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;실시간 트래픽이 몰릴 때 DB 부하를 완충해주는 역할을 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Spring Boot Redis 설정 예시&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;spring:
 data:
  redis:
   host: oddventure-cache.xxxxxx.ap-northeast-2.cache.amazonaws.com
   port: 6379&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;＃EC2 - Spring Boot 애플리케이션 실행 서버&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;EC2는 흔히 말하는 클라우드 상의 리눅스 서버다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이번 구성에서는 Spring Boot 애플리케이션을 실행하는 &lt;b&gt;애플리케이션 서버&lt;/b&gt; 역할을 담당한다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;EC2에서 수행하는 역할&lt;/span&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;빌드된 `oddventure` 애플리케이션 JAR/Docker 컨테이너 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;외부로부터 HTTP 요청 수신 (예: 80/8080 포트)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;내부 네트워크를 통해 RDS/ElastiCache에 접근&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;보안 그룹 설정 개요&lt;/span&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;EC2 보안 그룹&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;인바운드: HTTP(80), 혹은 테스트용 8080, SSH(22) 제한된 IP만 허용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;아웃바운드: RDS/&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;ElastiCache &lt;/span&gt;포트로의 통신 허용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;RDS 보안 그룹&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;인바운드: MySQL(3306), &lt;b&gt;EC2 보안 그룹&lt;/b&gt;에서만 접근 허용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt; &lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;ElastiCache&lt;/span&gt;&amp;nbsp;보안 그룹&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;인바운드: 6379, &lt;b&gt;EC2 보안 그룹&lt;/b&gt;에서만 접근 허용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;마치며&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이번 글에서는 &lt;b&gt;VPC &amp;middot; RDS(MySQL) &amp;middot; &lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;ElastiCache&lt;/span&gt; &amp;middot; EC2&lt;/b&gt;를 사용해&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Spring Boot 프로젝트를 AWS에 배포하면서 경험한 아키텍처 설계를 정리했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;단순히 EC2에 서비스 하나 띄우는 수준을 넘어서, &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;네트워크(VPC) &amp;middot; 데이터베이스(RDS) &amp;middot; 캐시(&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;ElastiCache&lt;/span&gt;) &amp;middot; 애플리케이션 서버(EC2)&lt;/b&gt;가 &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;서로 어떤 역할을 맡고, 실제 트래픽이 들어왔을 때 어떻게 상호작용하는지까지 눈에 그려보는 것이 목표였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;다만 현재 구조는 EC2 단일 인스턴스 기반이기 때문에, 무중단&amp;nbsp;배포,&amp;nbsp;자동&amp;nbsp;확장,&amp;nbsp;롤백&amp;nbsp;측면에서는&amp;nbsp;한계가&amp;nbsp;있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;다음 단계에서는 이 구조를 기반으로&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;ECS(Fargate) 기반 컨테이너 배포, ALB를 이용한 무중단 배포, Prometheus/Grafana를 이용한 모니터링&lt;/b&gt;까지 확장해 볼 계획이다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <author>twodc</author>
      <guid isPermaLink="true">https://twodc.tistory.com/137</guid>
      <comments>https://twodc.tistory.com/137#entry137comment</comments>
      <pubDate>Mon, 8 Dec 2025 10:56:01 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot 성능 최적화 10</title>
      <link>https://twodc.tistory.com/129</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그 처리 &amp;middot; 모니터링 고도화 (ELK &amp;middot; Prometheus &amp;middot; Grafana)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 성능을 개선하기 위해서는 단순히 코드를 최적화하는 것뿐 아니라, 서비스를 운영하면서 발생하는 지표와 로그를 정교하게 관측해야 한다.&lt;br /&gt;관측(Observability)은 장애 감지, 성능 병목 추적, 트래픽 분석, 자원 사용량 최적화까지 서비스 운영의 핵심 요소다.&lt;br /&gt;이 글에서는 실무에서 가장 많이 사용하는 모니터링 구성인 &lt;b&gt;ELK + Prometheus + Grafana&lt;/b&gt; 중심으로 로그 &amp;middot; 메트릭 운영 전략을 정리한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;＃로그(Log) 시스템 설계의 기본 원칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그는 많다고 좋은 것이 아니며, 적다고 안전한 것도 아니다.&lt;br /&gt;로그는 &amp;ldquo;정확한 정보 &amp;middot; 적절한 양 &amp;middot; 구조화된 형태&amp;rdquo;의 3가지를 만족해야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;로그 설계 3원칙&lt;/h4&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; (JSON 권장)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레벨 기반 관리&lt;/b&gt; (INFO / WARN / ERROR)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중복 로그 금지&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 로그는 검색, 필터링, 분석이 쉽기 때문에 ELK&amp;middot;Loki 같은 로그 시스템과 결합될 때 강력한 장점을 가진다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Spring Boot JSON 로그 예시&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;logging:
  pattern:
    console: &quot;{\&quot;time\&quot;: \&quot;%d{ISO8601}\&quot;, \&quot;level\&quot;: \&quot;%p\&quot;, \&quot;msg\&quot;: \&quot;%m\&quot;}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 Logback JSON Encoder 또는 Logstash Encoder를 많이 사용한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃ELK 스택(Logstash &amp;middot; Elasticsearch &amp;middot; Kibana)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ELK는 대규모 로그 시스템의 표준 구성이다.&lt;br /&gt;로그를 수집(Logstash) &amp;rarr; 저장(Elasticsearch) &amp;rarr; 분석(Kibana) 하는 구조로 이루어진다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ELK 구성 요소&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Logstash&lt;/b&gt; &amp;ndash; 로그 수집기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Elasticsearch&lt;/b&gt; &amp;ndash; 로그 검색 엔진&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kibana&lt;/b&gt; &amp;ndash; 시각화 및 분석 UI&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ELK가 필요한 이유&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;/li&gt;
&lt;li&gt;장애 분석&amp;middot;API 추적&amp;middot;사용자 행동 분석에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Logstash 파이프라인 예시&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;input { beats { port =&amp;gt; 5044 } }

filter {
  json { source =&amp;gt; &quot;message&quot; }
}

output {
  elasticsearch {
    hosts =&amp;gt; [&quot;http://elasticsearch:9200&quot;]
    index =&amp;gt; &quot;service-log-%{+YYYY.MM.dd}&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 규모가 커질수록 로그 파이프라인의 성능 영향이 커지므로, JSON 기반 구조화가 매우 중요하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃Prometheus로 메트릭 수집&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus는 시계열 메트릭(TSDB)을 저장하는 모니터링 시스템으로, Spring Boot Actuator와 결합하면 서버 성능을 상세히 분석할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Spring Boot Actuator 연동&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  metrics:
    enable:
      jvm: true
      system: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus는 HTTP 엔드포인트(`/actuator/prometheus`)를 주기적으로 스크랩한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Prometheus가 제공하는 주요 지표&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JVM Heap / Non-Heap 사용량&lt;/li&gt;
&lt;li&gt;CPU 사용률&lt;/li&gt;
&lt;li&gt;GC 횟수 및 시간&lt;/li&gt;
&lt;li&gt;HTTP 요청 수 및 응답 시간&lt;/li&gt;
&lt;li&gt;DB 커넥션 풀 상태(HikariCP)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC 튜닝, 장애 분석, 성능 최적화에서 Prometheus 메트릭은 사실상 필수다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃Grafana로 대시보드 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Grafana는 Prometheus 메트릭을 시각화하는 도구로, 실시간 서버 상태를 한눈에 확인할 수 있게 해준다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실무에서 반드시 구성하는 대시보드&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 응답 속도 (p95 / p99)&lt;/li&gt;
&lt;li&gt;DB 커넥션 풀 사용률&lt;/li&gt;
&lt;li&gt;Redis hit/miss 추이&lt;/li&gt;
&lt;li&gt;트래픽 급증 탐지&lt;/li&gt;
&lt;li&gt;GC Pause 시간 변화&lt;/li&gt;
&lt;li&gt;오류 로그 수집 대시보드&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Grafana 알람(Alert) 구성&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API p99 &amp;gt; 1초&lt;/li&gt;
&lt;li&gt;CPU &amp;gt; 80%&lt;/li&gt;
&lt;li&gt;메모리 사용률 &amp;gt; 85%&lt;/li&gt;
&lt;li&gt;레디스 응답 시간 증가&lt;/li&gt;
&lt;li&gt;Elasticsearch 색인 지연 증가&lt;/li&gt;
&lt;/ul&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃로그 &amp;middot; 메트릭 &amp;middot; 트레이싱 통합 (OpenTelemetry)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenTelemetry는 로그, 메트릭, 트레이스를 통합 관리하는 최신 관측성 표준이다.&lt;br /&gt;특히 마이크로서비스 환경에서는 개별 서비스의 흐름을 추적하는 데 필수적이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OpenTelemetry가 제공하는 기능&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산 트레이싱(traceId 기반)&lt;/li&gt;
&lt;li&gt;서비스 간 호출 관계 분석&lt;/li&gt;
&lt;li&gt;오류 발생 지점 위치 파악&lt;/li&gt;
&lt;li&gt;API 레벨 지연 원인 추적&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 규모가 커질수록 Logging만으로는 병목 분석이 어려우며,&lt;br /&gt;OpenTelemetry 기반 분산 트레이싱이 점점 필수화되는 추세다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃운영 환경에서의 실전 전략&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) API 병목 감지&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Grafana p99 증가 감지&lt;/li&gt;
&lt;li&gt;ELK에서 해당 API 로그 집중 조회&lt;/li&gt;
&lt;li&gt;Prometheus에서 DB/Redis 지연 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 장애 원인 분석&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ELK에서 ERROR 로그 패턴 분석&lt;/li&gt;
&lt;li&gt;OpenTelemetry로 호출 흐름 추적&lt;/li&gt;
&lt;li&gt;GC Pause 시간과 메모리 지표 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 트래픽 급증 대응&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Rate Limit 경고 알람&lt;/li&gt;
&lt;li&gt;API p99 지연 감지&lt;/li&gt;
&lt;li&gt;Redis &amp;middot; DB 커넥션 풀 증가 여부 확인&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;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 &amp;middot; 메트릭 &amp;middot; 트레이싱은 단순한 기록이 아니라&lt;br /&gt;&lt;b&gt;서비스 성능, 장애 대응, 운영 안정성의 핵심 지표&lt;/b&gt;이다.&lt;br /&gt;Spring Boot 서비스에서는 ELK + Prometheus + Grafana가 가장 실용적이며,&lt;br /&gt;필요에 따라 OpenTelemetry로 확장하면 더 높은 수준의 관측성을 구축할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해서 Spring Boot 성능 최적화 시리즈를 1편부터 10편까지 정리해보았다.&lt;br /&gt;개별 요소들을 하나씩 살펴보면서, 단순히 &amp;ldquo;빠르게 만드는 법&amp;rdquo;이 아니라&lt;br /&gt;어떤 부분이 실제 서비스 성능에 영향을 주고, 어떤 기준으로 선택해야 하는지를 더 명확하게 이해할 수 있었다.&lt;br /&gt;앞으로&amp;nbsp;이&amp;nbsp;내용들을&amp;nbsp;프로젝트나&amp;nbsp;운영&amp;nbsp;환경에&amp;nbsp;하나씩&amp;nbsp;적용해보면서&amp;nbsp;더&amp;nbsp;깊게&amp;nbsp;다듬어갈&amp;nbsp;예정이다.&lt;/p&gt;</description>
      <category>TIL</category>
      <author>twodc</author>
      <guid isPermaLink="true">https://twodc.tistory.com/129</guid>
      <comments>https://twodc.tistory.com/129#entry129comment</comments>
      <pubDate>Wed, 12 Nov 2025 21:27:22 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot 성능 최적화 9</title>
      <link>https://twodc.tistory.com/128</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;API 보안 &amp;middot; 인증 구조 성능 최적화 (JWT &amp;middot; 세션 &amp;middot; OAuth2)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 보안 구조는 단순히 인증(Authenticate)과 인가(Authorize)를 처리하는 것을 넘어,&lt;br /&gt;성능 &amp;middot; 확장성에도 직접적인 영향을 준다.&lt;br /&gt;잘못 설계된 인증 로직은 API 성능 병목, 세션 누락, 캐시 미스 증가 등 다양한 문제를 일으킨다.&lt;br /&gt;이 글에서는 Spring Boot 기반 서비스에서 자주 사용되는 인증 구조(JWT, 세션, OAuth2)의 성능 최적화 전략을 정리한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;＃JWT vs 세션 vs OAuth2 &amp;mdash; 무엇을 선택해야 할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 방식은 서비스 성격에 따라 적절한 선택이 필요하다.&lt;br /&gt;성능, 확장성, 보안 요구사항을 기준으로 비교해보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;JWT의 장점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버에 세션 저장 필요 없음 &amp;rarr; 확장성 높음&lt;/li&gt;
&lt;li&gt;Redis 없이도 멀티 서버 운영 가능&lt;/li&gt;
&lt;li&gt;API Gateway&amp;middot;모바일 앱에서 사용하기 좋음&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;세션(Session) 방식 장점&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;Token 탈취 대비 안전&lt;/li&gt;
&lt;li&gt;백오피스처럼 내부 서비스에서 편리&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OAuth2가 필요한 경우&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;3rd-party에게 권한(scope) 기반 액세스 제공&lt;/li&gt;
&lt;li&gt;Access Token + Refresh Token 구조가 필요한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 API 서비스는 대부분 다음 조합을 사용한다:&lt;br /&gt;&lt;b&gt;&amp;ldquo;OAuth2 (소셜 로그인) &amp;rarr; JWT 발급 &amp;rarr; API 인증&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃JWT 성능 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 서버 부하가 적지만, 서명(Signature) 검증이 CPU 비용을 많이 사용한다.&lt;br /&gt;잘못된 방식으로 검증하면 고트래픽 환경에서 병목이 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) JWT 파싱 규칙&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매 요청마다 DB 조회 금지&lt;/li&gt;
&lt;li&gt;Black List 저장 방식 최소화&lt;/li&gt;
&lt;li&gt;헤더에서 파싱 시 정규식 사용 금지(비용 높음)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) HS256 &amp;rarr; ES256 고려&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HS256(HMAC) 검증은 빠르지만 키 공유 방식이라 위험할 수 있다.&lt;br /&gt;서버 확장성과 보안 측면에서 ES256(ECDSA) 방식이 점점 증가하는 추세다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) Access Token + Refresh Token 전략&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Access Token은 15~30분, Refresh Token은 7~30일로 설정하는 것이 일반적이다.&lt;br /&gt;토큰 재발급에 Redis를 사용하면 보안과 성능을 높일 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;redis.set(&quot;refresh:{userId}&quot;, refreshToken, Duration.ofDays(14));&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) JWT Claim 최소화&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;userId, role 정도로 최소 구성&lt;/li&gt;
&lt;li&gt;Claim이 많으면 토큰 길어지고 네트워크 비용 증가&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃세션(Session) 기반 인증 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 기반 인증은 로그인 상태를 서버가 관리한다는 점에서 성능에 영향을 줄 수 있다.&lt;br /&gt;특히 세션 저장소 선택이 매우 중요하다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 로컬 메모리 세션 금지&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 서버 환경에서는 절대 사용하면 안 된다.&lt;br /&gt;재배포/스케일아웃 시 로그인이 강제 초기화된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) Redis 세션 스토리지 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 높은 처리량과 TTL 관리 기능이 있어 세션 저장소로 이상적이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;spring:
  session:
    store-type: redis&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 세션 용량 최적화&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;세션 값은 userId, role 정도로 최소화&lt;/li&gt;
&lt;li&gt;TTL은 서비스 특성에 맞게 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 기반 인증은 로그인 유지가 잘 되어야 하는 백오피스&amp;middot;내부 시스템에서 특히 유리하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃OAuth2 기반 로그인 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth2는 인증 서버와 통신하는 구조라 기본적으로 비용이 크다.&lt;br /&gt;특히 다음 부분에서 성능 차이가 발생한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 사용자 정보 요청 최소화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth2 로그인 과정에서 &amp;ldquo;프로필 조회 API&amp;rdquo; 호출을 최소화해야 한다.&lt;br /&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;/li&gt;
&lt;li&gt;로그인 이후에는 자체 DB 사용자 정보 사용&lt;/li&gt;
&lt;li&gt;소셜 API 재호출 절대 금지&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) Refresh Token 저장 전략&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 DB/Redis에 암호화하여 저장&lt;/li&gt;
&lt;li&gt;탈취 방지를 위해 rotation 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) JWT와 결합&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth2 로그인 &amp;rarr; Access Token &amp;rarr; 자체 JWT 발급&lt;br /&gt;대규모 API 구조에서 가장 많이 쓰이는 형태다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃필터(Filter) &amp;middot; 인터셉터(Interceptor) 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 로직이 느리면 전체 API가 병목된다.&lt;br /&gt;특히 필터는 모든 요청마다 실행되기 때문에 최적화가 필수적이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) DB 접근 금지&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 Filter에서 DB 조회가 발생하면 고트래픽 환경에서는 절대 버틸 수 없다.&lt;br /&gt;반드시 JWT 자체 검증 또는 캐시 기반 인증을 사용해야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 정규식 기반 URI 검사 금지&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규식은 매우 비싸다. prefix 매칭으로 대체해야 한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;if (path.startsWith(&quot;/public/&quot;)) { 
    chain.doFilter(request, response);
    return;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 인증 성공/실패 Logging 최소화&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그는 batch 형태로 모아서 저장&lt;/li&gt;
&lt;li&gt;로그 때문에 IO가 증가하면 역효과&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃Redis + JWT + OAuth2 조합 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 서비스에서 가장 많이 사용되는 인증 구조는 다음과 같다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OAuth2로 최초 사용자 인증&lt;/li&gt;
&lt;li&gt;자체 서버에서 JWT 발급&lt;/li&gt;
&lt;li&gt;Refresh Token은 Redis에 저장&lt;/li&gt;
&lt;li&gt;Access Token만으로 대부분의 API 인증 처리&lt;/li&gt;
&lt;li&gt;필요 시 Rate Limit / Circuit Breaker로 보호&lt;/li&gt;
&lt;/ul&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;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 인증 구조는 단순한 보안 문제를 넘어 성능과 확장성에도 직접적인 영향을 준다.&lt;br /&gt;JWT는 무상태 인증으로 확장성이 뛰어나고, 세션은 내부 서비스에서 편리하며, OAuth2는 외부 인증 연동에 적합하다.&lt;br /&gt;각 방식의 장단점을 이해하고 최적의 조합을 선택해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 &lt;b&gt;대규모 로그 처리 및 모니터링 최적화 (ELK &amp;middot; Prometheus &amp;middot; Grafana)&lt;/b&gt;를 다룬다.&lt;br /&gt;실전 운영 환경에서 필수적인 로깅 및 관측성(O11y) 전략을 정리할 예정이다.&lt;/p&gt;</description>
      <category>TIL</category>
      <author>twodc</author>
      <guid isPermaLink="true">https://twodc.tistory.com/128</guid>
      <comments>https://twodc.tistory.com/128#entry128comment</comments>
      <pubDate>Tue, 11 Nov 2025 21:02:34 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot 성능 최적화 8</title>
      <link>https://twodc.tistory.com/127</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;WebClient 기반 비동기 처리 최적화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API 호출은 많은 서비스에서 가장 큰 지연 요인이다.&lt;br /&gt;동기 RestTemplate은 요청마다 스레드를 점유하기 때문에, 트래픽이 증가하면 스레드 고갈(thread exhaustion) 문제가 발생한다.&lt;br /&gt;Spring WebClient는 비동기&amp;middot;논블로킹 방식으로 외부 API를 처리하여 더 높은 동시성과 효율을 제공한다.&lt;br /&gt;이 글에서는 WebClient의 핵심 개념과 성능 최적화 전략을 정리한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;＃RestTemplate과 WebClient 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestTemplate은 동기 방식으로, 요청마다 스레드가 점유된다.&lt;br /&gt;반면 WebClient는 이벤트 기반 비동기 구조로, 요청 대기 시간 동안 스레드를 반환하므로 높은 동시성을 처리할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;RestTemplate 한계&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 동안 스레드 점유 &amp;rarr; 동시 요청 수 제한&lt;/li&gt;
&lt;li&gt;느린 외부 API가 전체 서비스 속도를 늦춤&lt;/li&gt;
&lt;li&gt;멀티 IO 작업에 비효율적&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;WebClient 장점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;논블로킹 IO 기반&lt;/li&gt;
&lt;li&gt;스레드 점유 없음 &amp;rarr; 높은 동시성&lt;/li&gt;
&lt;li&gt;멀티 외부 API 호출에 최적&lt;/li&gt;
&lt;li&gt;Stream 처리 및 SSE(Server Sent Event)까지 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Virtual Thread와 WebClient를 비교하면, WebClient는 &amp;ldquo;지연 시간이 긴 외부 API 병렬 호출&amp;rdquo;에서 더 강력하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃WebClient 기본 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 3.x에서는 WebClient를 기본 HTTP 클라이언트로 권장한다.&lt;br /&gt;커넥션 재사용, 타임아웃, 풀 설정이 성능에 큰 영향을 준다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;WebClient Bean 등록&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean
public WebClient webClient() {
    return WebClient.builder()
            .baseUrl(&quot;https://api.example.com&quot;)
            .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;간단한 GET 요청&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Mono&amp;lt;UserResponse&amp;gt; user = webClient.get()
    .uri(&quot;/user/{id}&quot;, id)
    .retrieve()
    .bodyToMono(UserResponse.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mono는 단일 결과, Flux는 여러 개의 스트림 결과를 의미한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃성능 최적화 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebClient 성능은 HTTP 커넥션 풀 설정과 타임아웃 설정에 크게 영향을 받는다.&lt;br /&gt;적절한 커넥션 수는 시스템의 동시 API 호출량에 따라 달라진다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 커넥션 풀 크기 조절&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;HttpClient httpClient = HttpClient.create()
        .secure()
        .compress(true)
        .keepAlive(true)
        .responseTimeout(Duration.ofSeconds(3))
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000);

WebClient client = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 타임아웃 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API가 느릴 경우 서비스 전체가 느려지는 것을 방지한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;.responseTimeout(Duration.ofSeconds(2))&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) Keep-Alive 활성화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매 요청마다 TCP 커넥션을 새로 생성하면 비용이 크다.&lt;br /&gt;Keep-Alive를 통해 커넥션을 재사용하면 속도가 크게 개선된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃멀티 외부 API 호출 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebClient는 IO 대기 중 스레드를 붙잡지 않기 때문에,&lt;br /&gt;여러 외부 API를 병렬 호출할 때 성능 차이가 극적으로 나타난다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;병렬 호출 예시&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Mono&amp;lt;A&amp;gt; a = webClient.get().uri(&quot;/a&quot;).retrieve().bodyToMono(A.class);
Mono&amp;lt;B&amp;gt; b = webClient.get().uri(&quot;/b&quot;).retrieve().bodyToMono(B.class);

Mono&amp;lt;Result&amp;gt; result = Mono.zip(a, b)
        .map(tuple -&amp;gt; new Result(tuple.getT1(), tuple.getT2()));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestTemplate에서는 2개의 API를 직렬로 호출해 2배 시간이 걸리지만,&lt;br /&gt;WebClient는 병렬 처리로 대기 시간을 크게 줄일 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃Resilience4j와의 조합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebClient는 비동기이기 때문에 Circuit Breaker와 Rate Limit 전략과 함께 사용할 때 효과가 크다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Circuit Breaker 적용&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Mono&amp;lt;User&amp;gt; response = webClient.get()
    .uri(&quot;/user&quot;)
    .retrieve()
    .bodyToMono(User.class)
    .transformDeferred(CircuitBreakerOperator.of(circuitBreaker));&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Retry 적용&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;.retryWhen(Retry.backoff(3, Duration.ofMillis(200)))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API가 불안정해도 WebClient + Resilience4j 조합이면 안정적인 서비스를 유지할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃Virtual Thread와 WebClient 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 21 Virtual Thread와 WebClient는 모두 동시성 처리 능력이 뛰어나지만, 사용 목적이 다르다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Virtual Thread가 적합한 상황&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 중심의 내부 API&lt;/li&gt;
&lt;li&gt;외부 API 호출이 적은 서비스&lt;/li&gt;
&lt;li&gt;기존 동기 코드 유지하고 싶을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;WebClient가 적합한 상황&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 API 호출이 매우 많을 때&lt;/li&gt;
&lt;li&gt;API 병렬 호출이 필요한 서비스&lt;/li&gt;
&lt;li&gt;IO 대기 시간이 긴 API&lt;/li&gt;
&lt;li&gt;SSE 또는 스트리밍이 필요한 케이스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, WebClient는 외부 IO 중심 서비스에서 가장 강력하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebClient는 Spring Boot에서 외부 API 성능을 획기적으로 개선할 수 있는 핵심 기술이다.&lt;br /&gt;스레드를 점유하지 않는 비동기 구조 덕분에 대규모 트래픽 환경에서도 안정적이고 빠른 응답을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 &lt;b&gt;API 보안 및 인증 구조 성능 최적화 (JWT, 세션, OAuth2)&lt;/b&gt;를 다룬다.&lt;br /&gt;보안과 성능을 동시에 충족시키는 실전 전략을 정리해볼 예정이다.&lt;/p&gt;</description>
      <category>TIL</category>
      <author>twodc</author>
      <guid isPermaLink="true">https://twodc.tistory.com/127</guid>
      <comments>https://twodc.tistory.com/127#entry127comment</comments>
      <pubDate>Mon, 10 Nov 2025 21:56:11 +0900</pubDate>
    </item>
    <item>
      <title>11/7</title>
      <link>https://twodc.tistory.com/126</link>
      <description>&lt;h2 data-end=&quot;153&quot; data-start=&quot;96&quot; data-ke-size=&quot;size26&quot;&gt;Spring AI 기반 e스포츠 챗봇 고도화&lt;/h2&gt;
&lt;p data-end=&quot;395&quot; data-start=&quot;161&quot; data-ke-size=&quot;size16&quot;&gt;처음엔 &amp;ldquo;오늘 경기 있어요?&amp;rdquo; 같은 질문에 DB를 조회해 답하는 단순 챗봇이었다.&lt;/p&gt;
&lt;p data-end=&quot;395&quot; data-start=&quot;161&quot; data-ke-size=&quot;size16&quot;&gt;실제 사용 흐름에선 일정&amp;middot;승률&amp;middot;핫 키워드&amp;middot;외부 소식까지 다양하게 묻는다.&lt;/p&gt;
&lt;p data-end=&quot;395&quot; data-start=&quot;161&quot; data-ke-size=&quot;size16&quot;&gt;Spring AI의 tool calling을 시작으로, 도메인별 툴 분리, 기존 서비스 연동, 외부 API 확장까지 단계적으로 넓혀 간 과정을 간단하게 정리했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-end=&quot;431&quot; data-start=&quot;402&quot; data-ke-size=&quot;size23&quot;&gt;도메인 별 툴 분리&lt;/h3&gt;
&lt;p data-end=&quot;625&quot; data-start=&quot;432&quot; data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;AI가&lt;span&gt; &lt;/span&gt;&lt;/span&gt;사용자 질문 의도를 분석해 적절한 도메인 툴로 연결해주는 것이었다.&lt;/p&gt;
&lt;p data-end=&quot;625&quot; data-start=&quot;432&quot; data-ke-size=&quot;size16&quot;&gt;일정은 `query_schedule`,&lt;/p&gt;
&lt;p data-end=&quot;625&quot; data-start=&quot;432&quot; data-ke-size=&quot;size16&quot;&gt;날짜 지정은 `query_schedule_by_date`,&lt;/p&gt;
&lt;p data-end=&quot;625&quot; data-start=&quot;432&quot; data-ke-size=&quot;size16&quot;&gt;승률은 `analyze_winning_rate`,&lt;/p&gt;
&lt;p data-end=&quot;625&quot; data-start=&quot;432&quot; data-ke-size=&quot;size16&quot;&gt;인기 키워드는 `query_hot_keywords`,&lt;/p&gt;
&lt;p data-end=&quot;625&quot; data-start=&quot;432&quot; data-ke-size=&quot;size16&quot;&gt;외부 소식은 `query_cs2_news`로 툴을 명확히 나눴다.&lt;/p&gt;
&lt;h4 data-end=&quot;645&quot; data-start=&quot;627&quot; data-ke-size=&quot;size20&quot;&gt;서비스 호출&lt;/h4&gt;
&lt;pre id=&quot;code_1762697415610&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CallResponseSpec call = chatClient
        .prompt(prompt)
        .system(system)
        .tools(scheduleTools, winRateTools, hotKeywordTools, cs2NewsTools)
        .call();

String answer = call.content();&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;툴 정의&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1762697503400&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Tool(
        name = &quot;query_schedule&quot;,
        description = &quot;날짜 표현(오늘/내일/어제/이번주/지난주/주말)을 해석해 해당 기간의 경기 일정을 조회한다.&quot;
)
public ScheduleResponse querySchedule(
        @ToolParam(description = &quot;예: 오늘/내일/어제/이번주/지난주/주말&quot;, required = false)
        String when
) {
    ...

    return ScheduleResponse.of(items, w, start, end);
}&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-end=&quot;1496&quot; data-start=&quot;1488&quot; data-ke-size=&quot;size20&quot;&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;- 수동 JSON 오케스트레이션 없이, Spring AI의 tool calling을 통해 유지보수와 기능 추가 모두 간단해졌다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-end=&quot;1640&quot; data-start=&quot;1606&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기존 &amp;ldquo;핫키워드 서비스&amp;rdquo;를 툴로 연결&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1732&quot; data-start=&quot;1641&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이미 있던 핫키워드 서비스를 툴로 연결하면 챗봇이 &amp;ldquo;요즘 뭐가 인기야?&amp;rdquo; 같은 질문에 자연스럽게 대응할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1762697702799&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Tool(
        name = &quot;query_hot_keywords&quot;,
        description = &quot;최근 인기 있는 e-sports 관련 검색 키워드를 조회한다. (팀명, 리그명 등)&quot;
)
public HotKeywordsResponse queryHotKeywords() {
    return hotKeywordsService.getHotKeywords();
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-end=&quot;2290&quot; data-start=&quot;2253&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;외부 API 확장&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;2373&quot; data-start=&quot;2291&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;외부 Web API를 호출하는 툴을 추가해 e-sports 패치/새 소식 같은 요청을 지원했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1762697924357&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Tool(
        name = &quot;query_cs2_news&quot;,
        description = &quot;Counter-Strike 2 관련 최신 뉴스 5개를 조회한다. (Steam 공식 API 기반)&quot;
)
public List&amp;lt;Cs2NewsItem&amp;gt; queryCs2News() {
    int count = 5;
    RestTemplate restTemplate = new RestTemplate();
    String url = String.format(STEAM_NEWS_API, count);

    SteamNewsResponse response = restTemplate.getForObject(url, SteamNewsResponse.class);

    if (response == null || response.appnews() == null) {
        throw new IllegalStateException(&quot;Steam API 응답이 비어있습니다.&quot;);
    }

    return response.appnews().newsitems();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt; 모델이 &amp;ldquo;CS2 최근 소식 알려줘&amp;rdquo; 같은 요청을 받으면, 단순 문자열 응답이 아니라 실제 Steam API를 호출해 최신 뉴스 데이터를 가져온다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;AI가 단순히 텍스트를 생성하는 수준을 넘어, 실시간 데이터를 직접 조합해 답변하는 단계로 발전한 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-end=&quot;3096&quot; data-start=&quot;3078&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;마치며&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;3096&quot; data-start=&quot;3078&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt; 처음엔 단순한 질의응답 수준이던 챗봇이었지만, Spring AI의 Tool Calling을 적용하면서 구조적으로 확장 가능한 형태로 발전했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;핵심은 모델이 직접 도구를 선택해 호출한다는 점이었다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 덕분에 컨트롤러는 오직 사용자 질문 + 사용할 툴 목록만 전달하면 되고, 실제 어떤 도메인 로직이 실행될지는 AI가 문맥을 기반으로 판단한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1029&quot; data-start=&quot;722&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그리고 다음 단계로는 LangGraph 기반의 확장을 계획하고 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;LangGraph는 Tool Calling보다 한 단계 발전된 방식으로, AI가 툴을 순차적 혹은 병렬적으로 조합해 실행할 수 있게 한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;예를 들어 &amp;ldquo;이번주 인기팀 경기 일정 알려줘&amp;rdquo;라는 요청이 들어왔을 때,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;① `query_hot_keywords()` &amp;rarr; ② `query_schedule_by_team()`&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;순서로 툴을 자동 연결해 호출하는 그래프 형태의 실행 플로우를 구성할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;1183&quot; data-start=&quot;1031&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이처럼 LangGraph를 적용하면, 단일 챗봇을 넘어 도메인 간 협력형 AI 시스템으로 발전할 수 있을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;3096&quot; data-start=&quot;3078&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <author>twodc</author>
      <guid isPermaLink="true">https://twodc.tistory.com/126</guid>
      <comments>https://twodc.tistory.com/126#entry126comment</comments>
      <pubDate>Fri, 7 Nov 2025 21:40:12 +0900</pubDate>
    </item>
    <item>
      <title>11/6</title>
      <link>https://twodc.tistory.com/125</link>
      <description>&lt;h2 data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Spring AI 챗봇 오류 트러블슈팅 기록&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;프로젝트 진행 중 발생했던 `tool_use_failed` 오류와 시간대(Timezone) 불일치로 인한 경기 조회 누락 문제를 정리했다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;tool_use_failed 오류&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;문제 상황&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;챗봇 개선 중 서버에서 모델 툴 호출 실패 오류가 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1762695606263&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;code&quot;: &quot;tool_use_failed&quot;, &quot;failed_generation&quot;: &quot;&amp;lt;function=query_schedule{&quot;when&quot;:&quot;오늘&quot;}...&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;원인 분석&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1762695449450&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[툴 사용 원칙]
일정/승률/예측 등 사실 확인이 필요하면 제공된 툴을 사용한다.
한 번에 여러 툴이 필요하지 않다면 호출을 최소화한다.

[툴 사용 예시]
사용자: 오늘 경기 있어요?
도구호출: query_schedule({&quot;when&quot;:&quot;오늘&quot;})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;1. few-shot prompt에 들어간 툴 사용 예시 텍스트를 모델이 그대로 응답 본문에 붙여 출력&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;2. Groq이 `&amp;lt;function=...&amp;gt;` 형태를 함수 호출 시도로 잘못 인식 &amp;rarr; 파싱 실패&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;3. HTTP 400 (`tool_use_failed`) 발생&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;4. Spring AI에서 예외를 전파하면서 Postman에서는 500 서버 오류로 노출됨&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;해결 방법&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;few-shot prompt를 모델이 혼동하지 않도록 명확한 규칙 기반으로 재작성했다.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1762695518441&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[툴 사용 원칙]
경기 일정이 필요하면 query_schedule 또는 query_schedule_by_date를 사용한다.
팀&amp;middot;리그의 승률이 필요하면 analyze_winning_rate를 사용한다.
최근 인기 있는 팀/리그를 묻는다면 query_hot_keywords를 사용한다.
한 번에 여러 툴이 필요하지 않다면 호출을 최소화한다.

[툴 사용 예시]
사용자: 안녕하세요! &amp;rarr; 툴 호출 없이 최종 답만 생성
사용자: 오늘 경기 있어요? &amp;rarr; query_schedule({&quot;when&quot;:&quot;오늘&quot;})
사용자: 11월 4일 경기 일정 알려줘 &amp;rarr; query_schedule_by_date({&quot;month&quot;:11,&quot;day&quot;:4})
사용자: Nexus 승률 알려줘 &amp;rarr; analyze_winning_rate({&quot;teamA&quot;:&quot;Nexus&quot;})
사용자: 요즘 인기 있는 팀은 어디야? &amp;rarr; query_hot_keywords()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그 결과,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;모델이 예시 문장을 그대로 출력하지 않고 &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;실제 툴 호출을 수행하여 정상처리되어&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;서버 500 오류가 해결됐다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Timezone 불일치 문제&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;문제 상황&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;경기 일정 조회 시 DB에는 데이터가 존재함에도 &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;챗봇은 &amp;ldquo;오늘 경기가 없습니다&amp;rdquo;라고 응답하는 현상 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;로컬에서는 정상적으로 조회되는 것을 확인했다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;원인 분석&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;1. DB에는 UTC 기준으로 `start_time` 저장&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;2. 조회 로직은 KST(`LocalDateTime.now()`) 기준으로 실행&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;3. DB 조회 시 `between` 조건의 기준 시각이 서로 달라 조회 누락 발생&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp;※ 예: &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;KST 기준 `2025-11-04T00:00` &amp;rarr; &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;UTC 기준 `2025-11-03T15:00`&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;해결 방법&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;조회 전, KST &amp;rarr; UTC 변환 로직을 추가해 DB 기준과 일치시켜줬다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;private List findMatchesByKstRange(LocalDateTime startKst, LocalDateTime endKst) {
	ZoneId UTC = ZoneOffset.UTC;
	LocalDateTime startUtc = startKst.atZone(KST).withZoneSameInstant(UTC).toLocalDateTime();
	LocalDateTime endUtc = endKst.atZone(KST).withZoneSameInstant(UTC).toLocalDateTime();

	return matchRepository.findByStartTimeBetweenOrderByStartTimeAsc(startUtc, endUtc);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그 결과,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;UTC/KST 간 시간대 일관성 확보해서&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;실제 DB와 조회 범위가 일치함으로써&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;경기 일정이 정상적으로 반환된다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;마치며&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이번 두 가지 오류는 모두 AI 통합 환경에서 자주 발생하는 전형적인 문제였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;`tool_use_failed` &amp;rarr; 프롬프트 설계 단계에서의 모델 오인식&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Timezone 불일치 &amp;rarr; 시스템 간 데이터 일관성 문제&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;두 사례를 통해 모델이 어떻게 해석하고 동작하는가에 대한 이해를 넓힐 수 있었으며, 프롬프트 설계와 시스템 시간대 관리가 안정적인 챗봇 운영에 핵심적임을 확인했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <author>twodc</author>
      <guid isPermaLink="true">https://twodc.tistory.com/125</guid>
      <comments>https://twodc.tistory.com/125#entry125comment</comments>
      <pubDate>Thu, 6 Nov 2025 22:15:02 +0900</pubDate>
    </item>
    <item>
      <title>11/5</title>
      <link>https://twodc.tistory.com/124</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;CoT와 React 패턴 적용&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-end=&quot;167&quot; data-start=&quot;141&quot; data-ke-size=&quot;size23&quot;&gt;CoT(Chain-of-Thought) 적용&lt;/h3&gt;
&lt;h4 data-end=&quot;177&quot; data-start=&quot;169&quot; data-ke-size=&quot;size20&quot;&gt;CoT란?&lt;/h4&gt;
&lt;p data-end=&quot;303&quot; data-start=&quot;179&quot; data-ke-size=&quot;size16&quot;&gt;CoT(Chain-of-Thought)는 사용자의 질문에 대해 &lt;b&gt;단계적 사고 절차를 모델 내부에서 수행&lt;/b&gt;하도록 유도하는 프롬프트 기법이다.&lt;br /&gt;핵심은 &lt;b&gt;생각은 내부에서 정리하고, 최종 답만 출력&lt;/b&gt;하게 만드는 것이다.&lt;/p&gt;
&lt;p data-end=&quot;322&quot; data-start=&quot;305&quot; data-ke-size=&quot;size16&quot;&gt;사고 절차 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1762696357293&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;의도 파악 &amp;rarr; 필요한 데이터 확인 &amp;rarr; 도구 선택 및 실행 &amp;rarr; 결과 통합 &amp;rarr; 검증&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-end=&quot;394&quot; data-start=&quot;383&quot; data-ke-size=&quot;size20&quot;&gt;CoT 적용 전&lt;/h4&gt;
&lt;p data-end=&quot;468&quot; data-start=&quot;396&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;안녕하세요!&amp;rdquo; 같은 단순한 인사에도 장황한 답변이 반복되었고, 불확실한 경우에는 &amp;ldquo;정보가 부족합니다.&amp;rdquo;를 과도하게 남발했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;337&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4NudK/dJMcafx9YmK/oUINB6ZudHg137paKGn3sK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4NudK/dJMcafx9YmK/oUINB6ZudHg137paKGn3sK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4NudK/dJMcafx9YmK/oUINB6ZudHg137paKGn3sK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4NudK%2FdJMcafx9YmK%2FoUINB6ZudHg137paKGn3sK%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;924&quot; height=&quot;337&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;337&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1762696290667&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String prompt = &quot;&quot;&quot;
    아래는 사용자와 챗봇 간의 최근 대화 내용입니다.
    대화의 흐름을 이해하고, 사용자의 질문에 답변하세요.

    대화 내역:
    %s

    사용자 요청:
    %s

    응답 지침:
    - 대화의 맥락을 유지하면서 자연스럽게 응답하세요.
    - 경기, 팀 관련 데이터는 사실에 기반해 답변하세요.
    - 불확실한 정보일 경우 추측하지 말고 &quot;정보가 부족합니다.&quot;라고 답변하세요.
    - 답변은 한국어로 작성하세요.
    &quot;&quot;&quot;.formatted(historyText, userMessage);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;879&quot; data-start=&quot;811&quot; data-ke-size=&quot;size16&quot;&gt;이전에는 단순히 &amp;ldquo;정보 부족 시 응답 제한&amp;rdquo;만 설정되어 내부적 사고 과정 없이 즉각적인 문장 생성이 이루어졌다.&lt;/p&gt;
&lt;h4 data-end=&quot;897&quot; data-start=&quot;886&quot; data-ke-size=&quot;size20&quot;&gt;CoT 적용 후&lt;/h4&gt;
&lt;p data-end=&quot;970&quot; data-start=&quot;899&quot; data-ke-size=&quot;size16&quot;&gt;아래와 같이 모델 내부 사고 절차와 검증 체크리스트, 최종 출력 규칙을 명시한 시스템 프롬프트를 추가했다.&lt;/p&gt;
&lt;pre id=&quot;code_1762696435952&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String system = &quot;&quot;&quot;
            너는 e스포츠 도메인 어시스턴트다.

            [사고 지침(내부)]
            - 질문을 1) 의도 파악 &amp;rarr; 2) 필요 데이터 확인 &amp;rarr; 3) 툴 선택/인자 결정 &amp;rarr; 4) 결과 통합 &amp;rarr; 5) 검증 체크리스트 순으로 생각한다.
            - 필요한 경우에만 툴을 호출한다. 툴 결과의 범위/단위/시간대를 점검한다.
            - 모순되면 재검토하고, 데이터가 부족하면 부족하다고 말한다.

            [출력 지침(외부)]
            - 최종 한국어 답변만 출력한다. 중간 사고 과정, 단계적 이유, 체크리스트는 출력하지 않는다.
            - 간결하지만, 사용자에게 실용적인 요약/근거(출처 유형)를 포함한다.

            [툴 사용 원칙]
            - 일정/승률/예측 등 사실 확인이 필요하면 제공된 툴을 사용한다.
            - 한 번에 여러 툴이 필요하지 않다면 호출을 최소화한다.
            &quot;&quot;&quot;;

String prompt = &quot;&quot;&quot;
            [최근 대화]
            %s
        
            [사용자 요청]
            %s
        &quot;&quot;&quot;.formatted(historyText, userMessage);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qbco0/dJMcacVJMLs/x0YKuTsQKLtOEoAs8t3Bw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qbco0/dJMcacVJMLs/x0YKuTsQKLtOEoAs8t3Bw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qbco0/dJMcacVJMLs/x0YKuTsQKLtOEoAs8t3Bw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqbco0%2FdJMcacVJMLs%2Fx0YKuTsQKLtOEoAs8t3Bw1%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;499&quot; height=&quot;192&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G0Erp/dJMcagDPStB/T3bGQGbQfkKQUnkzcpKzA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G0Erp/dJMcagDPStB/T3bGQGbQfkKQUnkzcpKzA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G0Erp/dJMcagDPStB/T3bGQGbQfkKQUnkzcpKzA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG0Erp%2FdJMcagDPStB%2FT3bGQGbQfkKQUnkzcpKzA0%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;919&quot; height=&quot;192&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;1647&quot; data-start=&quot;1560&quot; data-ke-size=&quot;size16&quot;&gt;이후부터는 모델이 응답 전 내부적으로 의도 분석 &amp;rarr; 도구 판단 &amp;rarr; 결과 검증 단계를 거치며, 불필요한 문장 없이 핵심적인 답변만 출력하게 되었다.&lt;/p&gt;
&lt;h4 data-end=&quot;1659&quot; data-start=&quot;1654&quot; data-ke-size=&quot;size20&quot;&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;- 잘못된 데이터나 시간대 불일치 상황에서도 추론 및 검증 후 올바른 툴 호출 수행&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;h3 data-end=&quot;1833&quot; data-start=&quot;1820&quot; data-ke-size=&quot;size23&quot;&gt;React 적용 정리&lt;/h3&gt;
&lt;h4 data-end=&quot;1845&quot; data-start=&quot;1835&quot; data-ke-size=&quot;size20&quot;&gt;React란?&lt;/h4&gt;
&lt;p data-end=&quot;1999&quot; data-start=&quot;1847&quot; data-ke-size=&quot;size16&quot;&gt;React(Reason + Act)는 LLM이 질문을 이해하고 필요한 정보를 판단한 뒤, 적절한 도구를 호출하여 데이터를 얻고 결과를 관찰해 최종 답을 작성하는 일련의 사고 절차를 말한다.&lt;br /&gt;즉, &amp;ldquo;사고 &amp;rarr; 행동 &amp;rarr; 관찰 &amp;rarr; 최종 답&amp;rdquo;의 구조를 구현한 것이다.&lt;/p&gt;
&lt;h4 data-end=&quot;2014&quot; data-start=&quot;2006&quot; data-ke-size=&quot;size20&quot;&gt;구성 요소&lt;/h4&gt;
&lt;p data-end=&quot;2055&quot; data-start=&quot;2016&quot; data-ke-size=&quot;size16&quot;&gt;React 구조에서는 모델이 여러 종류의 도구를 상황에 맞게 활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- DB 조회 도구&lt;/b&gt;: 경기 일정, 승패 기록, 승률 요약 등 내부 데이터 접근&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 외부 API 도구&lt;/b&gt;: 외부 일정, 뉴스, 배당 정보 등 Web API 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 계산/정규화 도구&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;: 승률 계산, 날짜 해석 등 비즈니스 로직 처리&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-end=&quot;2283&quot; data-start=&quot;2258&quot; data-ke-size=&quot;size20&quot;&gt;Spring AI Tool Calling&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수동 JSON 오케스트레이션 없이, Spring AI가 제공하는 tool calling을 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Tool 메서드로 툴을 노출하고 모델이 필요 시 자동으로 툴을 호출하고 결과를 재주입해 최종 답을 만든다.&lt;/p&gt;
&lt;h4 data-end=&quot;2450&quot; data-start=&quot;2440&quot; data-ke-size=&quot;size20&quot;&gt;툴 정의 예시&lt;/h4&gt;
&lt;pre id=&quot;code_1762696626101&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Tool(
        name = &quot;query_schedule&quot;,
        description = &quot;날짜 표현(오늘/내일/어제/이번주/지난주/주말)을 해석해 해당 기간의 경기 일정을 조회한다.&quot;
)
public ScheduleResponse querySchedule(
        @ToolParam(description = &quot;예: 오늘/내일/어제/이번주/지난주/주말&quot;, required = false)
        String when
) {
    String w = (when == null || when.isBlank()) ? &quot;오늘&quot; : when.trim();
    LocalDate today = LocalDate.now();

    LocalDateTime start;
    LocalDateTime end;

    // 중간 로직...

    List&amp;lt;Match&amp;gt; matches = matchRepository.findByStartTimeBetweenOrderByStartTimeAsc(start, end);

    if (matches.isEmpty()) {
        return ScheduleResponse.noMatch(w, start, end, &quot;해당 기간에 등록된 일정이 없습니다.&quot;);
    }

    List&amp;lt;ScheduleItem&amp;gt; items = matches.stream()
            .map(ScheduleItem::of)
            .toList();

    return ScheduleResponse.of(items, w, start, end);
}&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&lt;span style=&quot;color: #000000; font-size: 1.25em; letter-spacing: -1px;&quot;&gt;모델 호출 예시&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1762696646420&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CallResponseSpec call = chatClient
                .prompt(prompt)
                .system(system)
                .tools(scheduleTools, winRateTools)
                .call();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-end=&quot;3451&quot; data-start=&quot;3443&quot; data-ke-size=&quot;size20&quot;&gt;동작 흐름&lt;/h4&gt;
&lt;p data-end=&quot;3451&quot; data-start=&quot;3443&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 프롬프트 구성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;system에는 CoT 지침(의도 &amp;rarr; 정보 확인 &amp;rarr; 선택적 도구 사용 &amp;rarr; 결과 통합 &amp;rarr; 검증 &amp;rarr; 최종 출력)을 포함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prompt에는 최근 대화와 사용자의 질문을 포함한다.&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;2. 툴 스키마 노출&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`@Tool`이 붙은 메서드는 Spring AI가 자동으로 스키마를 생성하여 모델에 전달한다.&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;3. 모델 판단(Reason)&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;ldquo;오늘&amp;rdquo;, &amp;ldquo;이번주&amp;rdquo;)가 있을 경우 모델이 더 정확히 선택한다.&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;4. 도구 실행(Act)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델이 툴 사용을 결정하면 Spring AI가 해당 메서드를 자동 실행한다.&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;5. 결과 관찰 및 응답 생성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;툴의 결과가 모델 입력에 다시 주입되고, 모델이 이 데이터를 바탕으로 최종 한국어 응답을 생성한다.&lt;/p&gt;
&lt;h4 data-end=&quot;3959&quot; data-start=&quot;3951&quot; data-ke-size=&quot;size20&quot;&gt;적용 효과&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 도구 호출 시 직접 JSON 파싱/오케스트레이션 불필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 모델이 상황에 따라 동적으로 도구를 선택해 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- CoT 지침과 결합 시 불필요한 호출 최소화, 응답 품질 향상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Spring 환경에서 AI + 비즈니스 로직 간 결합도 최소화&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-end=&quot;4131&quot; data-start=&quot;4125&quot; data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-end=&quot;4206&quot; data-start=&quot;4133&quot; data-ke-size=&quot;size16&quot;&gt;이번 CoT와 React 적용을 통해 챗봇이 단순 텍스트 생성 수준을 넘어 사고하고 행동하는 에이전트로 발전했다.&lt;/p&gt;
&lt;p data-end=&quot;4206&quot; data-start=&quot;4133&quot; data-ke-size=&quot;size16&quot;&gt;CoT는 내부 사고의 구조화를, React는 외부와의 상호작용(도구 실행)을 담당한다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;4339&quot; data-start=&quot;4277&quot; data-ke-size=&quot;size16&quot;&gt;결국 두 요소를 함께 설계함으로써 Spring AI 기반 챗봇의 응답 정확도와 일관성이 한 단계 향상되었다.&lt;/p&gt;</description>
      <category>TIL</category>
      <author>twodc</author>
      <guid isPermaLink="true">https://twodc.tistory.com/124</guid>
      <comments>https://twodc.tistory.com/124#entry124comment</comments>
      <pubDate>Wed, 5 Nov 2025 21:34:15 +0900</pubDate>
    </item>
    <item>
      <title>11/4</title>
      <link>https://twodc.tistory.com/123</link>
      <description>&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;AI 챗봇 구현&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt; 진행 중인 프로젝트에서 AI를 활용한 대화형 챗봇을 구현했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;단순히 텍스트 응답을 생성하는 수준을 넘어, 비동기 이벤트 흐름 기반의 실시간 응답 구조를 설계하는 것이 핵심 목표였다. &lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;전체 설계 개요&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;가장 중요한 설계 목표는 &lt;b&gt;비동기 메시징 기반의 실시간 응답 파이프라인&lt;/b&gt;을 구축하는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;사용자의 입력은 Redis Pub/Sub을 통해 비동기로 전달되고,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;AI는 Groq API를 통해 자연어 응답을 생성한 뒤 WebSocket을 통해 클라이언트로 다시 전달한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;사용자 입력
   &amp;darr;
RedisPublisher &amp;rarr; chat:{userId}:input
   &amp;darr;
ChatMessageSubscriber &amp;rarr; ChatbotService.reply()
   &amp;darr;
Groq API &amp;rarr; 자연어 응답 생성
   &amp;darr;
RedisPublisher &amp;rarr; chat:{userId}:output
   &amp;darr;
RedisSubscriber &amp;rarr; /topic/chat/{userId} 브로드캐스트
   &amp;darr;
프론트엔드 WebSocket 수신&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;Redis Pub/Sub를 활용한 이유&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;Redis Pub/Sub을 사용한 이유는 AI 연산을 사용자 요청과 완전히 분리하기 위함이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;AI 연산은 상대적으로 시간이 많이 걸리는 작업이기 때문에,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;HTTP 요청-응답 구조로 처리하면 서버 자원을 지속적으로 점유하게 되어 성능 저하가 발생하기 쉽다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;Pub/Sub 구조를 적용하면 입력 이벤트 발생 &amp;rarr; 메시지 발행 &amp;rarr; 비동기 처리 &amp;rarr; 결과 발행으로 흐름을 분리할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;또한, Redis는 메모리 기반이므로 빠르고 가벼운 메시지 전달이 가능하다.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;ChatMessageSubscriber&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;사용자의 입력은 `chat:{userId}:input` 채널로 발행되고,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;이 채널을 구독 중인 `ChatMessageSubscriber`가 메시지를 받아 AI 호출을 트리거한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class ChatMessageSubscriber implements MessageListener {

    private final ChatbotService chatbotService;
    private final RedisPublisher redisPublisher;

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(message.getChannel());
        String body = new String(message.getBody());

        log.info(&quot;[Redis 구독] channel={}, message={}&quot;, channel, body);

        String[] parts = channel.split(&quot;:&quot;);
        Long userId = Long.valueOf(parts[1]);
        String aiResponse = chatbotService.reply(userId, body);

        redisPublisher.publish(&quot;chat:&quot; + userId + &quot;:output&quot;, aiResponse);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;이 과정을 통해 사용자의 요청은 HTTP 요청 없이 Redis 이벤트를 통해 처리된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;이는 높은 처리량과 응답 지연 최소화를 동시에 달성하는 구조이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;ChatbotService&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;챗봇은 대화 맥락을 유지하면서 자연스러운 응답을 생성한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: left;&quot;&gt;또한, 사용자가 &amp;ldquo;승률&amp;rdquo;, &amp;ldquo;확률&amp;rdquo; 등의 특정 키워드를 포함한 요청을 보낼 경우,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: left;&quot;&gt;챗봇은 DB에서 경기 전적을 조회해 통계 기반 데이터를 분석해 응답을 생성한다.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public String reply(Long userId, String userMessage) {
    if (WINNING_RATE_KEYWORDS.stream().anyMatch(userMessage::contains)) {
        return handleWinningRate(userMessage);
    }

    List&amp;lt;String&amp;gt; history = chatHistoryService.getRecentMessages(userId);
    String historyText = String.join(&quot;\n&quot;, history);

    String prompt = &quot;&quot;&quot;
            아래는 사용자와 챗봇 간의 최근 대화 내용입니다.
            대화의 흐름을 이해하고, 사용자의 질문에 답변하세요.

            대화 내역:
            %s

            사용자 요청: %s

            응답 지침;
            - 대화의 맥락을 유지하면서 자연스럽게 응답하세요.
            - 경기, 팀 관련 데이터는 사실에 기반해 답변하세요.
            - 불확실한 정보일 경우 추측하지 말고 &quot;정보가 부족합니다.&quot;라고 답변하세요.
            - 답변은 한국어로 작성하세요.
            &quot;&quot;&quot;.formatted(historyText, userMessage);

    ChatResponse chatResponse = chatClient.prompt(prompt)
            .system(&quot;너는 e스포츠 데이터 기반 AI 챗봇이다. 사용자와의 맥락을 기억한다.&quot;)
            .call()
            .chatResponse();

    Generation result = chatResponse.getResult();
    String aiAnswer = result.getOutput().getText();

    chatHistoryService.addMessage(userId, &quot;user&quot;, userMessage);
    chatHistoryService.addMessage(userId, &quot;assistant&quot;, aiAnswer);

    return aiAnswer;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;Redis 대화 히스토리&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;AI가 일관된 맥락을 유지하려면 과거 대화가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;이를 위해 Redis List를 이용하여 사용자의 최근 20개 대화를 저장하고, 7일 후 자동 만료되도록 구성했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;redisTemplate.opsForList().rightPush(key, json);
redisTemplate.opsForList().trim(key, -MAX_HISTORY, -1);
redisTemplate.expire(key, Duration.ofDays(7));
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;이 방식은 빠르면서도 메모리 낭비가 없고, 불필요한 데이터 누적을 방지한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;WebSocket으로 결과 전달&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;AI의 응답은 RedisPublisher를 통해 다시&amp;nbsp;&amp;nbsp;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: left;&quot;&gt;`chat:{userId}:output`&lt;/span&gt; 채널로 발행되고,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;`ChatMessageOutputSubscriber`가 이를 수신해 WebSocket을 통해 클라이언트에 전달한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class ChatMessageOutputSubscriber implements MessageListener {

    private final SimpMessagingTemplate messagingTemplate;

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(message.getChannel());
        String body = new String(message.getBody());

        log.info(&quot;[Redis 구독] channel={}, message={}&quot;, channel, body);

        String[] parts = channel.split(&quot;:&quot;);
        String userId = parts[1];

        String destination = &quot;/topic/chat/&quot; + userId;
        messagingTemplate.convertAndSend(destination, body);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;프론트엔드는 `/topic/chat/{userId}`를 구독하여 실시간으로 응답을 받는다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;이를 통해 HTTP 없이 양방향 대화형 통신이 가능해진다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;전체 흐름 요약&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;[사용자 입력]
   &amp;darr;
RedisPublisher &amp;rarr; chat:{userId}:input
   &amp;darr;
ChatMessageSubscriber
   &amp;darr;
ChatbotService.reply()
   ├─ Redis: 대화 히스토리 조회
   ├─ Groq AI 호출
   ├─ (선택) 승률 분석 수행
   └─ Redis: 히스토리 저장
   &amp;darr;
RedisPublisher &amp;rarr; chat:{userId}:output
   &amp;darr;
RedisSubscriber &amp;rarr; /topic/chat/{userId} 전송
   &amp;darr;
프론트엔드 WebSocket으로 실시간 응답 표시
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;마치며&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;이번 구조는 Redis를 통해 입력과 출력을 분리하고,&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt; Groq AI를 통해 자연어 기반 응답을 생성하며,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;WebSocket으로 실시간 소통이 가능한 대화형 환경을 구현했다.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;실시간성과 확장성을 만족하면서, AI 기반 서비스의 백엔드 아키텍처를 한 단계 고도화할 수 있었다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <author>twodc</author>
      <guid isPermaLink="true">https://twodc.tistory.com/123</guid>
      <comments>https://twodc.tistory.com/123#entry123comment</comments>
      <pubDate>Tue, 4 Nov 2025 20:01:23 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot 성능 최적화 7</title>
      <link>https://twodc.tistory.com/122</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;트래픽 급증 대비 아키텍처 패턴 (Rate Limit &amp;middot; Circuit Breaker &amp;middot; Bulkhead)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스는 항상 일정한 트래픽을 받지 않는다.&lt;br /&gt;이벤트, 광고, 점검 알림, 외부 봇 접근 등으로 인해 순간적인 트래픽 급증이 발생할 수 있다.&lt;br /&gt;이때 API와 서버가 이를 제대로 처리하지 못하면 장애로 직결된다.&lt;br /&gt;본 글에서는 고트래픽 상황에서 시스템을 보호하기 위한 아키텍처 패턴들을 정리한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;＃Rate Limiting&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rate Limiting은 &amp;ldquo;특정 사용자 또는 IP가 너무 많은 요청을 보내지 못하도록 제한하는 기술&amp;rdquo;이다.&lt;br /&gt;봇 공격, 반복 재시도, 과도한 호출로 인한 장애를 예방하는 데 효과적이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;적용 방식&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IP 또는 사용자 단위 요청 제한&lt;/li&gt;
&lt;li&gt;특정 API별 제한 (예: 로그인, 회원가입)&lt;/li&gt;
&lt;li&gt;외부 API 호출 제한&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Redis 기반 Rate Limit 예시&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;INCR user:{id}:count
EXPIRE user:{id}:count 60&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 1분 안에 일정 횟수를 넘으면 차단할 수 있다.&lt;br /&gt;Spring Cloud Gateway 또는 NGINX에서도 쉽게 설정이 가능하다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 알고리즘&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Fixed Window&lt;/li&gt;
&lt;li&gt;Sliding Window&lt;/li&gt;
&lt;li&gt;Token Bucket (가장 일반적)&lt;/li&gt;
&lt;li&gt;Leaky Bucket&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃Circuit Breaker&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Circuit Breaker는 외부 API 또는 내부 서비스가 장애 상태일 때, 추가 호출을 차단하여 전체 시스템 장애로 확산되는 것을 방지한다.&lt;br /&gt;Netflix Hystrix 이후 현재는 &lt;b&gt;Resilience4j&lt;/b&gt;가 표준이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&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;b&gt;Closed&lt;/b&gt;: 정상 상태, 요청 통과&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Open&lt;/b&gt;: 장애 감지 &amp;rarr; 모든 요청 차단&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Half-Open&lt;/b&gt;: 일부 요청만 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Resilience4j 예시&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;CircuitBreakerConfig config = CircuitBreakerConfig.custom()
        .failureRateThreshold(50)
        .waitDurationInOpenState(Duration.ofSeconds(5))
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Circuit Breaker는 장애 확산 방지에 매우 효과적이며&lt;br /&gt;외부 API, 결제 시스템, 인증 서버 연동 시 필수적이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃Bulkhead&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bulkhead 패턴은 시스템의 리소스를 &amp;ldquo;격리&amp;rdquo;하여, 특정 기능에서 장애가 발생해도 전체 서비스에 영향을 주지 않도록 한다.&lt;br /&gt;선박의 격벽 구조에서 유래한 개념이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;왜 필요한가?&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 API가 느려지면 전체 스레드가 고갈될 수 있음&lt;/li&gt;
&lt;li&gt;외부 API 지연이 서비스 전체를 마비시킴&lt;/li&gt;
&lt;li&gt;장시간 대기 요청이 스레드 풀을 점유&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;격리 전략&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API마다 별도 스레드 풀 구성&lt;/li&gt;
&lt;li&gt;서비스 기능을 독립 실행 환경으로 분리&lt;/li&gt;
&lt;li&gt;DB 연결 풀 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시: CPU&amp;middot;IO 작업 분리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 작업은 CPU Pool, IO 작업은 IO Pool로 분리한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;ExecutorService cpuPool = Executors.newFixedThreadPool(8);
ExecutorService ioPool = Executors.newCachedThreadPool();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring WebFlux나 WebClient 사용 시 Bulkhead는 더욱 중요한 안정성 기법이 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃Fallback 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fallback은 Circuit Breaker와 함께 사용되는 보조 전략이다.&lt;br /&gt;장애 발생 시 대체 데이터 또는 기본 응답을 제공해 사용자 경험을 보호한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 API 실패 &amp;rarr; 캐시 데이터 반환&lt;/li&gt;
&lt;li&gt;일부 기능 &amp;rarr; &amp;ldquo;잠시 후 다시 시도해주세요&amp;rdquo; 기본 메시지&lt;/li&gt;
&lt;li&gt;랭킹/리뷰 서비스 &amp;rarr; TTL 지난 이전 데이터라도 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Resilience4j Fallback 예시&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@CircuitBreaker(name = &quot;external&quot;, fallbackMethod = &quot;fallback&quot;)
public Response callApi() {
    return externalApi.call();
}

public Response fallback(Throwable t) {
    return new Response(&quot;temporary data&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;＃실제 서비스에서의 적용 예시&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;로그인/인증&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IP 단위 Rate Limit&lt;/li&gt;
&lt;li&gt;OTP&amp;middot;SMS 전송 제한&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;랭킹/피드 API&lt;/h4&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;li&gt;CDN 캐싱&lt;/li&gt;
&lt;li&gt;Bulkhead로 격리&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;외부 결제/인증 API&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Circuit Breaker 필수&lt;/li&gt;
&lt;li&gt;Fallback 데이터 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;대량 조회 서비스&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis Rate Limit&lt;/li&gt;
&lt;li&gt;Bulkhead로 스레드 풀 격리&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;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽 급증은 언제든 발생할 수 있으며, 이를 제대로 대비하지 않으면 전체 서비스 장애로 이어진다.&lt;br /&gt;Rate Limit, Circuit Breaker, Bulkhead는 서로 보완적으로 작동하며, 시스템의 안정성과 탄력성을 크게 높인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 &lt;b&gt;WebClient 기반 비동기 처리 최적화&lt;/b&gt;를 다룬다.&lt;br /&gt;Virtual Thread와 비교했을 때 WebClient가 어떤 상황에서 적합한지도 함께 설명할 예정이다.&lt;/p&gt;</description>
      <category>TIL</category>
      <author>twodc</author>
      <guid isPermaLink="true">https://twodc.tistory.com/122</guid>
      <comments>https://twodc.tistory.com/122#entry122comment</comments>
      <pubDate>Mon, 3 Nov 2025 19:23:01 +0900</pubDate>
    </item>
  </channel>
</rss>