<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>How to be awesome</title>
    <link>https://siyoon210.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 21 Aug 2026 10:10:56 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>siyoon210</managingEditor>
    <image>
      <title>How to be awesome</title>
      <url>https://tistory1.daumcdn.net/tistory/2959569/attach/790db3be82604add8b6150d8a31d5da4</url>
      <link>https://siyoon210.tistory.com</link>
    </image>
    <item>
      <title>AWS Lightsail 최소 사양에서 Spring Boot는 어디까지 견디는가</title>
      <link>https://siyoon210.tistory.com/204</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;월 $3.5짜리 Lightsail 인스턴스에 Spring Boot 서버를 올릴 수 있는지 알고 싶었다. 512MB 램에 2 vCPU, 스토리지 20GB짜리다. 검색으로도 LLM에 물어봐도 답은 대체로 &quot;안 된다&quot;였고, 이유는 한결같이 512MB에 JVM이 안 들어간다는 것이었다. 근거로 제시되는 숫자를 본 적이 없어서 직접 측정해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 적으면 이렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;512MB라는 메모리에도 스프링부트 운영이 가능하다. OS와 Litestream까지 올린 전체 스택이 419MB 중 256MB를 쓰고 147MB가 남았다&lt;/li&gt;
&lt;li&gt;요청수가 초당 30건 이하로 유지된다면 $3.5짜리 Lightsail 인스턴스에서 운영이 가능하다. (일반적인 HTML 페이지 호출 기준. 자세한 사항은 아래 설명)&lt;/li&gt;
&lt;li&gt;(순간 피크 기준) 요청수 초당 120건까지 성능저하 없이 처리한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;(일반적인 HTML 페이지 호출 기준. 자세한 사항은 아래 설명)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;측정 조건&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 Lightsail 인스턴스($3.5, 512MB, Amazon Linux 2023, x86_64)에서 쟀다. 요청 하나의 비용을 성분별로 나누는 부분만 Docker 컨테이너를 따로 썼다. 조건을 하나씩 고정해야 하는 측정이라 그렇다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구성 요소&lt;/td&gt;
&lt;td&gt;버전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JDK&lt;/td&gt;
&lt;td&gt;Eclipse Temurin 25.0.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Boot&lt;/td&gt;
&lt;td&gt;4.1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Framework&lt;/td&gt;
&lt;td&gt;7.0.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서블릿 컨테이너&lt;/td&gt;
&lt;td&gt;Tomcat Embed 11.0.22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;템플릿 엔진&lt;/td&gt;
&lt;td&gt;Thymeleaf 3.1.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB&lt;/td&gt;
&lt;td&gt;SQLite (WAL) + sqlite-jdbc 3.51.0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커넥션 풀&lt;/td&gt;
&lt;td&gt;HikariCP 7.0.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복제&lt;/td&gt;
&lt;td&gt;Litestream 0.5.16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;부하 생성기&lt;/td&gt;
&lt;td&gt;k6 v2.2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;spring.threads.virtual.enabled=true
spring.datasource.hikari.maximum-pool-size=8
spring.datasource.hikari.data-source-properties.journal_mode=WAL
server.compression.enabled=false
spring.thymeleaf.cache=true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM 플래그는 하나도 주지 않았다. 힙을 지정하지 않으면 JVM이 시스템 메모리를 읽어 그 25%로 잡는다. 이 인스턴스에서는 126MB다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상스레드는 사용했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQLite와 Litestream을 쓰는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용 때문이다. 관리형 데이터베이스는 이 가격대에서 인스턴스 값보다 비싸고, DB용 인스턴스를 하나 더 띄우면 최소 비용으로 서버를 올린다는 전제가 무너진다. SQLite는 별도 서버 프로세스 없이 애플리케이션 안에서 도는 파일 기반 데이터베이스라 추가 비용이 붙지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 데이터가 인스턴스 디스크의 파일 하나에 들어 있다. 인스턴스가 사라지면 데이터도 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Litestream이 그 문제를 메운다. SQLite의 변경 기록(WAL)을 계속 읽어 S3나 R2 같은 오브젝트 스토리지로 복제하는 별도 프로세스다. 애플리케이션은 평소처럼 SQLite를 쓰고, 복제는 뒤에서 알아서 돌아간다. 인스턴스를 잃어도 마지막 시점의 데이터베이스를 다시 받아올 수 있다.&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;이 조합이면 데이터베이스 몫으로 나가는 돈이 오브젝트 스토리지 요금만 남는다. 대신 인스턴스의 CPU와 메모리를 애플리케이션과 나눠 쓰게 되므로, 이 글의 측정에는 Litestream도 함께 올려서 그 몫까지 포함했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터와 시나리오&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 수는 운영 중인 커뮤니티 사이트의 규모에 맞췄다. 글 4,684건, 댓글 17,645건, 글당 평균 댓글 3.8개.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;시나리오&lt;/td&gt;
&lt;td&gt;엔드포인트&lt;/td&gt;
&lt;td&gt;응답&lt;/td&gt;
&lt;td&gt;하는 일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;page&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /page/post/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTML 102 KB&lt;/td&gt;
&lt;td&gt;쿼리 5회 + Thymeleaf 렌더링&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /json/post/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON 12 KB&lt;/td&gt;
&lt;td&gt;같은 쿼리 5회, 템플릿 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blob&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /static/blob&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;102 KB&lt;/td&gt;
&lt;td&gt;미리 만들어둔 문자열 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /db/user/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON 380 B&lt;/td&gt;
&lt;td&gt;인덱스 조회 1회&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;page&lt;/code&gt;가 주력이다. 글 상세 페이지 하나에 쿼리 5개가 나간다. (본문, 댓글 목록, 최근글, 인기글, 게시판 목록...) 나머지 셋은 비용을 성분별로 나누기 위한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gzip 같은 압축은 껐다. Cloudflare를 앞에 두면 엣지에서 압축하므로 원본 CPU를 아낄 수 있다. 참고로 gzip을 켜도 CPU 비용은 전체의 5%뿐이라, 직접 서빙한다면 켜는 편이 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 메모리 512MB도 충분히 운영 가능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 스택을 단계별로 올리며 잰 값이다.&lt;/p&gt;
&lt;table style=&quot;height: 185px;&quot; width=&quot;526&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;단계&lt;/td&gt;
&lt;td&gt;used&lt;/td&gt;
&lt;td&gt;available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px;&quot;&gt;OS&lt;/td&gt;
&lt;td style=&quot;height: 16px;&quot;&gt;110 MB&lt;/td&gt;
&lt;td style=&quot;height: 16px;&quot;&gt;298 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;OS + Spring Boot&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;232 MB&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;176 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;OS + Spring Boot + Litestream&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;240 MB&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;168 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;OS + Spring Boot + Litestream + 부하&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;256 MB&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;147 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스별로는 java 161MB, litestream 13MB다. OOM 0건, 여유 147MB.&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;명목 512MB 중 실제로 보이는 것은 419MB였다. 커널 예약 영역과 펌웨어가 먼저 가져간다. &quot;512MB에 들어가는가&quot;를 따질 때 기준은 512가 아니라 419다.&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;code&gt;free&lt;/code&gt;를 볼 때는 &lt;code&gt;free&lt;/code&gt; 열이 아니라 &lt;code&gt;available&lt;/code&gt; 을 봐야 한다. 리눅스는 남는 메모리를 전부 캐시로 쓰기 때문에 &lt;code&gt;free&lt;/code&gt;는 항상 작게 나온다. &lt;code&gt;available&lt;/code&gt;이 새 프로세스에 내줄 수 있는 실제 여유다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Lightsail에서 반드시 알아야하는 '베이스라인'&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lightsail 인스턴스는 버스트형이다. &quot;2 vCPU&quot;는 순간 최대치이지 계속 쓸 수 있는 양이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vCPU보다 베이스라인이 더 중요한데, 한줄로 요약하면 평균적으로 쓸수있는 CPU 성능 한계다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순간 5%를 넘어가는 작업도 가능하지만 평균 5%를 넘기다 보면 크레딧이 고갈되어 인스턴스 성능이 저하된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/lightsail/latest/userguide/baseline-cpu-performance.html&quot;&gt;문서&lt;/a&gt;에 플랜별 성능 베이스라인이 명시돼 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;플랜&lt;/td&gt;
&lt;td&gt;vCPU&lt;/td&gt;
&lt;td&gt;메모리&lt;/td&gt;
&lt;td&gt;성능 베이스라인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux $3.5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;512 MB&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux $5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux $10&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2 GB&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가격은 IPv6 전용 번들 기준이다. Lightsail은 IPv4 주소를 함께 주는 번들(듀얼스택)과 IPv6 주소만 주는 번들을 따로 파는데, 후자가 더 싸다. 같은 사양이 듀얼스택으로는 $5 / $7 / $12다. 성능 베이스라인은 둘이 동일하다.&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;(IPv6 전용은 IPv4 클라이언트가 서버에 직접 접속하지 못한다는 제약이 있지만, Cloudflare를 앞에 두면 해결된다. 클라이언트는 Cloudflare에 IPv4로 붙고 Cloudflare가 원본에 IPv6로 접속한다.)&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;문서에 단서가 하나 붙어 있다. *&quot;These performance baselines are per vCPU.&quot;* vCPU당 값이라는 뜻이고, 콘솔 그래프는 두 vCPU를 평균 내서 보여준다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$3.5   버스트 상한 2.00 vCPU   /   지속 보장 0.10 vCPU  (2 x 5%)&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;베이스라인은 상한이 아니라 크레딧이 늘지도 줄지도 않는 경계다. 언제든 100%까지 쓸 수 있고, 그 위로 쓰면 burst capacity가 줄고 아래로 쓰면 쌓인다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;burst capacity 계산&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/lightsail/latest/userguide/monitoring-cpu-burst-capacity.html&quot;&gt;문서&lt;/a&gt;에 공식이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;burst capacity는 &quot;CPU를 100%로 쓸 수 있는 시간&quot; 을 분 단위로 센다. 한 시간(60분) 가운데 베이스라인 비율만큼이 쌓이고, 실제로 쓴 비율만큼이 빠진다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;시간당 적립 (분)  =  베이스라인%  &amp;times; 60분
시간당 소모 (분)  =  실제 CPU%    &amp;times; 60분
최대 적립량 (분)  =  시간당 적립  &amp;times; 24시간&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$3.5 플랜이면 &lt;code&gt;5% &amp;times; 60분 = 시간당 3분&lt;/code&gt;이 쌓이고, 24시간이 지나면 72분에서 멈춘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU를 20%로 쓰면 &lt;code&gt;20% &amp;times; 60분 = 시간당 12분&lt;/code&gt;이 나간다. 버는 3분을 빼면 순소모가 9분이고, 72분을 8시간에 다 쓴다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;사용률&lt;/td&gt;
&lt;td&gt;소진까지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;줄지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;24시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;8시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;2.7시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;1.3시간&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;burst capacity가 바닥나도 &lt;a href=&quot;https://docs.aws.amazon.com/lightsail/latest/userguide/identifying-instance-burst.html&quot;&gt;추가 과금은 없다&lt;/a&gt;. 최악의 경우가 요금 폭탄이 아니라 성능 저하라는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 측정에서 &lt;code&gt;page&lt;/code&gt; 요청은 초당 120건에 CPU 20%를 썼다. 역산하면 크레딧이 줄기 시작하는 지점은 초당 약 30건이다. 그 아래로 운영하면 잔량이 마르지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 실제로 어디까지 견디는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 표대로면 초당 30건을 넘는 순간부터 크레딧이 줄고, 다 쓰면 성능이 떨어져야 한다. 그래서 크레딧을 0까지 소진시킨 상태에서 요청률을 올려가며 쟀다.&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;code&gt;page&lt;/code&gt; 시나리오(102 KB HTML, 쿼리 5회).&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 70px;&quot;&gt;목표 rps&lt;/td&gt;
&lt;td style=&quot;width: 100px;&quot;&gt;실제 처리 rps&lt;/td&gt;
&lt;td style=&quot;width: 57px;&quot;&gt;실패율&lt;/td&gt;
&lt;td style=&quot;width: 237px;&quot;&gt;p95 레이턴시&lt;/td&gt;
&lt;td style=&quot;width: 74px;&quot;&gt;인스턴스 CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 70px;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;width: 100px;&quot;&gt;8.0&lt;/td&gt;
&lt;td style=&quot;width: 57px;&quot;&gt;0%&lt;/td&gt;
&lt;td style=&quot;width: 237px;&quot;&gt;44 ms&lt;/td&gt;
&lt;td style=&quot;width: 74px;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 70px;&quot;&gt;16&lt;/td&gt;
&lt;td style=&quot;width: 100px;&quot;&gt;16.0&lt;/td&gt;
&lt;td style=&quot;width: 57px;&quot;&gt;0%&lt;/td&gt;
&lt;td style=&quot;width: 237px;&quot;&gt;42 ms&lt;/td&gt;
&lt;td style=&quot;width: 74px;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 70px;&quot;&gt;24&lt;/td&gt;
&lt;td style=&quot;width: 100px;&quot;&gt;24.0&lt;/td&gt;
&lt;td style=&quot;width: 57px;&quot;&gt;0%&lt;/td&gt;
&lt;td style=&quot;width: 237px;&quot;&gt;41 ms&lt;/td&gt;
&lt;td style=&quot;width: 74px;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 70px;&quot;&gt;40&lt;/td&gt;
&lt;td style=&quot;width: 100px;&quot;&gt;40.0&lt;/td&gt;
&lt;td style=&quot;width: 57px;&quot;&gt;0%&lt;/td&gt;
&lt;td style=&quot;width: 237px;&quot;&gt;39 ms&lt;/td&gt;
&lt;td style=&quot;width: 74px;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 70px;&quot;&gt;60&lt;/td&gt;
&lt;td style=&quot;width: 100px;&quot;&gt;60.0&lt;/td&gt;
&lt;td style=&quot;width: 57px;&quot;&gt;0%&lt;/td&gt;
&lt;td style=&quot;width: 237px;&quot;&gt;38 ms&lt;/td&gt;
&lt;td style=&quot;width: 74px;&quot;&gt;약 10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 70px;&quot;&gt;90&lt;/td&gt;
&lt;td style=&quot;width: 100px;&quot;&gt;90.0&lt;/td&gt;
&lt;td style=&quot;width: 57px;&quot;&gt;0%&lt;/td&gt;
&lt;td style=&quot;width: 237px;&quot;&gt;37 ms&lt;/td&gt;
&lt;td style=&quot;width: 74px;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 70px;&quot;&gt;120&lt;/td&gt;
&lt;td style=&quot;width: 100px;&quot;&gt;119.9&lt;/td&gt;
&lt;td style=&quot;width: 57px;&quot;&gt;0%&lt;/td&gt;
&lt;td style=&quot;width: 237px;&quot;&gt;43 ms&lt;/td&gt;
&lt;td style=&quot;width: 74px;&quot;&gt;약 20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 70px;&quot;&gt;150&lt;/td&gt;
&lt;td style=&quot;width: 100px;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;td style=&quot;width: 57px;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;td style=&quot;width: 237px;&quot;&gt;JVM 힙 부족으로 프로세스 사망&lt;/td&gt;
&lt;td style=&quot;width: 74px;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU는 인스턴스 안에서 &lt;code&gt;vmstat&lt;/code&gt;으로 관측한 사용자&amp;middot;시스템 시간의 합이고, 두 지점에서만 쟀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크레딧이 하나도 없는 상태에서 초당 120건을 p95 43ms에 처리한다.&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;초당 150건에서 프로세스가 죽었다. 로그에 남은 것은 CPU 관련 오류가 아니었다.&lt;/p&gt;
&lt;pre class=&quot;julia&quot;&gt;&lt;code&gt;Exception in thread &quot;tomcat-handler-20963&quot; java.lang.OutOfMemoryError: Java heap space&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;스레드 번호가 2만 번대까지 갔다. 처리 속도보다 요청이 빨리 들어오면서 가상 스레드가 계속 생성됐고, 각각이 102KB 응답 버퍼를 들고 있으니 126MB 힙이 버티지 못했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 운영중 크레딧 알람은 필수로 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크레딧 소진 자체는 이번 측정에서 큰 성능 저하로 이어지지 않았지만, 그 상태를 알고 있느냐는 별개 문제다. 이 상태에는 흔적이 거의 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요금이 늘지 않는다. Lightsail은 CPU 초과분을 과금하지 않는다&lt;/li&gt;
&lt;li&gt;CPU 그래프만 봐서는 구분이 안 된다. 정상적으로 버스트 중인지 잔량을 태우는 중인지 같은 그래프로 보인다&lt;/li&gt;
&lt;li&gt;헬스체크를 통과한다. 가벼운 엔드포인트는 계속 200을 반환한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/lightsail/latest/userguide/amazon-lightsail-alarms.html&quot;&gt;Lightsail은 세 가지 CPU 지표에 알람을 걸 수 있다&lt;/a&gt;.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;지표&lt;/td&gt;
&lt;td&gt;의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CPUUtilization&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CPU 사용률 (두 vCPU 평균)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BurstCapacityPercentage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;남은 burst capacity 비율&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BurstCapacityTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;남은 burst capacity를 분으로 환산&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;걸어야 할 대상은 &lt;code&gt;CPUUtilization&lt;/code&gt;이 아니라 &lt;code&gt;BurstCapacityPercentage&lt;/code&gt;다. 40%라는 CPU 값 하나로는 &quot;지금 트래픽이 많다&quot;와 &quot;잔량이 곧 바닥난다&quot;를 가릴 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. CPU는 어떤 과정에서 많이 쓸까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;page&lt;/code&gt; 요청의 비용을 성분별로 나누기 위해 조각을 하나씩 뺀 엔드포인트를 비교했다. 이 부분은 조건을 정확히 고정해야 해서 컨테이너에서 쟀다.&lt;/p&gt;
&lt;table style=&quot;height: 96px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;엔드포인트&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;응답크기&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;DB 쿼리 요청수&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;렌더링&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;100 rps 콘솔 CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;/static/blob&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;102 KB&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;0회&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;5.24%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;/json/post/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;12 KB&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;5회&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Jackson&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;8.50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;/page/post/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;102 KB&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;5회&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Thymeleaf&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;12.12%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 성분이 분리된다.&lt;/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;DB가 가장 큰 몫이다. 템플릿 렌더링은 15%뿐이고, 102KB를 그냥 내보내는 것도 24%에 그친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 크기의 영향도 생각보다 작다. 240KB짜리 페이지를 따로 쟀는데 크기가 2.3배인데 비용은 13%만 늘었다. 바이트 수보다 요청당 고정 작업이 비용을 지배한다.&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;VPS 하나를 최대한 쓴다는 것은 DB도 같은 인스턴스에 둔다는 뜻이고, 그러면 쿼리 CPU가 애플리케이션과 같은 예산에서 나간다. 위 비중이 그 크기를 보여준다. 저사양 인스턴스에서 &lt;code&gt;EXPLAIN QUERY PLAN&lt;/code&gt;을 확인하는 것이 다른 어떤 최적화보다 효과가 크다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DB Write와 Litestream은 영향이 크지 않음&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회할 때마다 조회수를 올리는 패턴, 그리고 SQLite를 복제하는 Litestream을 차례로 얹고 7 rps에서 쟀다.&lt;/p&gt;
&lt;table style=&quot;height: 75px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구성&lt;/td&gt;
&lt;td&gt;콘솔 CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;읽기 전용&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;1.99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;+ 조회수 UPDATE&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;1.85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;+ Litestream&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;2.20%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋 다 노이즈 범위 안이다. WAL 모드에서 인덱스 단건 UPDATE를 초당 몇 건 하는 것은 SQLite에 부담이 아니고, 그 정도 쓰기가 만드는 WAL은 몇 KB라 Litestream이 읽어 올릴 것도 거의 없다.&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;메모리는 다르다. Litestream은 실제 인스턴스에서 13MB를 상주 사용했다. CPU는 무시할 수 있지만 메모리는 여유 147MB의 9%다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 수치는 로컬 파일로 복제하고 쓰기가 초당 몇 건일 때 그렇다는 뜻이다. S3나 R2로 올리면 네트워크와 TLS 비용이 붙고, 쓰기가 많아지면 WAL도 그만큼 커진다. 전체 테이블 &lt;code&gt;COUNT&lt;/code&gt;를 쓰기 락 안에서 재계산하는 패턴처럼 무거운 쓰기는 비용 구조가 다르고 여기서 재지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;월 $3.5짜리 Lightsail 인스턴스에서 Spring Boot + SQLite + Litestream을 돌린 결과다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;메모리&lt;/td&gt;
&lt;td&gt;419MB 중 256MB 사용, 147MB 여유. OOM 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;지속 가능한 평균&lt;/td&gt;
&lt;td&gt;초당 약 30건 (이 아래면 크레딧이 쌓인다)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;크레딧 소진 상태 처리량&lt;/td&gt;
&lt;td&gt;초당 120건, p95 43ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;붕괴 지점&lt;/td&gt;
&lt;td&gt;초당 150건, JVM 힙 부족&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;512MB에 JVM이 안 들어간다&quot;는 말은 사실이 아니었다. 플래그를 주지 않으면 JVM이 시스템 메모리의 25%로 힙을 잡기 때문에 오히려 작게 시작한다. 부하가 몰려 프로세스가 죽은 순간에도 인스턴스에는 78MB가 남아 있었다.&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;CPU 베이스라인은 알아둬야 하지만 두려워할 것은 아니다. $3.5 인스턴스 베이스라인 5%라면 평균 CPU 사용량이 5%로 지속되면 된다.&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;GC 튜닝이나 네이티브 이미지 (GraalVm)는 이 규모에서 필요 없다. 기본 설정으로 충분했고, 별도로 잰 튜닝 구성은 저부하에서 기본값과 차이가 없거나 오히려 메모리를 더 썼다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한계&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관측 구간이 짧다. 요청률당 1분씩 쟀다. 크레딧 초과 상태를 수 시간 지속했을 때도 같은 성능이 나오는지는 확인하지 않았다&lt;/li&gt;
&lt;li&gt;비용 분해는 컨테이너 기준이다. 성분별 비중은 상대값이라 그대로 옮겨가지만, 절대 CPU 수치는 인스턴스와 다르다&lt;/li&gt;
&lt;li&gt;무거운 쓰기 패턴을 안 쟀다. 단건 UPDATE는 측정되지 않았지만, 전체 테이블 &lt;code&gt;COUNT&lt;/code&gt;나 같은 행에 쓰기가 몰리는 경우는 비용 구조가 다르다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/lightsail/latest/userguide/baseline-cpu-performance.html&quot;&gt;Understand baseline CPU performance and burst capacity accrual for Lightsail instances&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/lightsail/latest/userguide/monitoring-cpu-burst-capacity.html&quot;&gt;Monitor CPU burst capacity for your Lightsail instance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/lightsail/latest/userguide/identifying-instance-burst.html&quot;&gt;Identify when your Lightsail instance bursts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/lightsail/latest/userguide/amazon-lightsail-alarms.html&quot;&gt;Metric alarms in Lightsail&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AWS</category>
      <category>aws</category>
      <category>lightsail</category>
      <author>siyoon210</author>
      <guid isPermaLink="true">https://siyoon210.tistory.com/204</guid>
      <comments>https://siyoon210.tistory.com/204#entry204comment</comments>
      <pubDate>Fri, 14 Aug 2026 21:16:29 +0900</pubDate>
    </item>
    <item>
      <title>AI 바이브코딩과 함께 사라진 '테스트의 고통'</title>
      <link>https://siyoon210.tistory.com/203</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;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;&quot;테스트는 통과하는데, 왜 시스템은 점점 더 구려지는 걸까?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;16&quot; data-ke-size=&quot;size23&quot;&gt;1. 원래 '높은 결합도'의 가장 큰 벌칙은 '테스트의 불가능'이었다&lt;/h3&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;굳이 객체지향을 고민하고, 결합도를 낮추려 애썼던 가장 현실적인 이유는 고상한 철학 때문이 아니었다. &lt;b data-index-in-node=&quot;61&quot; data-path-to-node=&quot;17&quot;&gt;결합도가 높은 나쁜 구조를 만들면 당장 테스트 코드를 작성하는 것 자체가 고문이었기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;어떤 클래스가 내부 세부 구현에 강하게 얽혀 있거나 온갖 상태에 의존하고 있으면 테스트 코드를 짜다가 화가 난다. &quot;아, 도저히 테스트를 못 짜겠다. 구조를 쪼개야지.&quot;&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;즉, '테스트 작성의 괴로움'은 나쁜 아키텍처와 높은 결합도를 막아주는 가장 강력한 즉각적 피드백 장치(억제기)였다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;21&quot; data-ke-size=&quot;size23&quot;&gt;2. AI라는 깡패가 억제기를 부숴버렸다&lt;/h3&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;하지만 요즘 AI는 완전히 다르다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;심하게 꼬인 결합도와 내부 상태, 엉망인 의존 관계를 가져다줘도 AI는 불평 한마디 하지 않는다. &lt;b data-index-in-node=&quot;63&quot; data-path-to-node=&quot;23&quot;&gt;무지막지한 깡생산성(체급)으로 밀어붙여 어떻게든 작동하는 200줄짜리 테스트 코드를 3초 만에 뱉어낸다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;나쁜 구조가 마땅히 받아야 할 페널티(테스트 작성 불가능)를 AI가 무력으로 대신 막아주는 셈이다. 결합도가 높든 말든 AI가 뚫어버리니 개발자는 착각에 빠진다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&quot;테스트가 통과하네? 내 코드 구조에 문제없나 보다.&quot;&lt;/p&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 AI의 압도적 생산성 뒤로, 시한폭탄 같은 높은 결합도의 코드들이 차곡차곡 쌓여간다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;28&quot; data-ke-size=&quot;size23&quot;&gt;3. AI가 짠 테스트 코드도 결국 '사람이 평생 고쳐야 할 코드'다&lt;/h3&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;여기서 간과하는 사실이 있다. &lt;b data-index-in-node=&quot;20&quot; data-path-to-node=&quot;29&quot;&gt;AI가 3초 만에 짠 테스트 코드도 결국 프로덕션 코드의 연장선이며, 사람이 계속 읽고 유지보수해야 할 부채라는 점이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;30&quot; data-ke-size=&quot;size16&quot;&gt;높은 결합도를 AI가 억지로 뚫어낸 테스트는 당연히 그 나쁜 구조의 세부 구현을 그대로 캡처하고 있다. 프로덕션 로직을 조금만 바꿔도 AI가 깡으로 뚫어둔 테스트 수십 개가 모조리 터져나간다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;30&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;생성 비용은 0원에 수렴할지 몰라도, &lt;b data-index-in-node=&quot;21&quot; data-path-to-node=&quot;31&quot;&gt;읽고, 이해하고, 유지보수하는 비용은 폭발한다.&lt;/b&gt; 나쁜 결합도를 AI의 힘으로 덮어두었더니, 그 결과물이 오히려 나쁜 구조를 영원히 고치지 못하게 만드는 벽이 되어 돌아온다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;(이렇게 '배설'된 코드들을 테스트코드라는 이유만으로 동료 개발자에게 코드리뷰로 들이미는것이 과연 정당한가? 너무 괴롭다.)&lt;/p&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;33&quot; data-ke-size=&quot;size23&quot;&gt;4. 그렇다면 우리는 무엇을 해야 하는가?&lt;/h3&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;팀원들에게 &quot;AI 쓰지 말고 아키텍처를 고민하면서 직접 테스트 코드를 짭시다&quot; 라고 말하는 건 비현실적이다. AI의 생산성을 포기하자는 주장은 아무도 듣지 않는다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;35&quot; data-ke-size=&quot;size16&quot;&gt;대신 내가 선택한 액션 아이템은 &quot;테스트 코드 작성 시 아키텍처와 스킬을 선언하게 만드는 것&quot;이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;AI가 테스트 코드를 대신 작성해 줄 수는 있지만, &lt;b data-index-in-node=&quot;29&quot; data-path-to-node=&quot;36&quot;&gt;그 코드가 품고 있는 아키텍처적 부채를 설명하고 선언하는 건 오직 개발자 자신만 가능&lt;/b&gt;하다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;37&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;37,0,0&quot;&gt;결합도 및 픽스처 자진 신고:&lt;/b&gt; 이 테스트를 돌리기 위해 무리하게 무거운 픽스처(Fixture) 객체나 장황한 전/후처리를 몰아넣고 있진 않은가?&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;37,1,0&quot;&gt;코드 스멜 선언:&lt;/b&gt; AI가 만들어준 테스트 코드가 지나치게 길거나 세부 구현을 흉내 내고 있다면, 단순히 PR에 올리는 게 아니라 &quot;현재 프로덕션 코드가 결합도가 높아 테스트가 장황함&quot;을 명시하기.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;37,2,0&quot;&gt;아키텍처 의도 말하기:&lt;/b&gt; AI가 뱉은 코드를 그대로 제출하지 말고, 이 테스트가 어떤 스킬(상태 검증, 행위 검증 등)을 사용했고 왜 이런 구조를 선택했는지 동료들에게 설명하기.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 시대에 좋은 개발자의 진짜 역량은 테스트 코드를 쳐내는 속도가 아니라고 생각한다. AI가 무식한 깡생산성으로 결합도를 뚫어버렸을 때, 그 장황한 테스트 코드 뒤에 숨은 '나쁜 구조의 경고 신호'를 알아채고 프로덕션 코드를 리팩토링할 수 있는 안목이 진짜 역량이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;42&quot; data-ke-size=&quot;size16&quot;&gt;AI가 내 테스트 코드를 대신 짜줄 수는 있어도, 내 시스템의 아키텍처까지 책임져주진 않는다.&lt;/p&gt;</description>
      <category>Development</category>
      <author>siyoon210</author>
      <guid isPermaLink="true">https://siyoon210.tistory.com/203</guid>
      <comments>https://siyoon210.tistory.com/203#entry203comment</comments>
      <pubDate>Wed, 29 Jul 2026 08:31:13 +0900</pubDate>
    </item>
    <item>
      <title>디아2 하다가 갑자기 OCR 공부</title>
      <link>https://siyoon210.tistory.com/202</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;디아블로2를 다시 시작했다. 예전이나 지금이나 이 게임의 진입장벽은 역시 '아이템 가치'를 아는 것에서 시작된다. 초보자 입장에서는 줍는 족족 커뮤니티나 거래 사이트를 뒤져야 하는데, 이게 여간 번거로운 게 아니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;2&quot; data-ke-size=&quot;size16&quot;&gt;&quot;그냥 화면을 찍으면 거래사이트 정보와 AI가 바로 가치를 알려줄 순 없을까?&quot;&lt;/p&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;단순한 호기심에서 시작된 이 고민이 생각보다 깊은 삽질의 시작이 될 줄은 몰랐다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;4&quot; data-ke-size=&quot;size23&quot;&gt;기존 OCR과 비전 모델의 한계&lt;/h3&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;처음엔 요즘 잘나가는 일반 OCR 모델과 비전(Vision) 모델에 냅다 스크린샷을 던져봤다. 결과는 참담했다. 그나마 비전 모델이 맥락을 짚어내긴 하지만, 실사용하기엔 정확도가 턱없이 낮았다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;디아블로2 특유의 반투명한 툴팁 배경과 독특한 폰트가 문제였다. 인간의 눈에는 명확해 보여도, 기계 입장에서는 배경 노이즈와 글자가 뒤섞인 지옥도였을 테니까. 결국 일반적으로 튜닝된 OCR로는 답이 없다는 결론에 도달했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;나는 OCR(광학 문자 인식)에 대해 무지하다. 어떤 단계로 처리해야 효율적인지, 요즘 트렌드는 뭔지 하나도 모르는 상태에서 무작정 리서치에 뛰어들었다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;가장 먼저 눈에 들어온 건 PaddleOCR이었다. 커스텀 학습이 가능하다기에 덤벼봤는데, 아이템 문자를 제대로 인식시키려면 텍스트 라인을 한 줄씩 잘라서 학습시켜야 한다는 사실을 알게 됐다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;10&quot; data-ke-size=&quot;size23&quot;&gt;노가다로 일단 테스트&lt;/h3&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;샘플로 120개 정도의 툴팁 라인을 확보했다. 한땀 한땀 이미지를 수정하며 학습 데이터를 만들었다. 진짜 못할 짓이다 싶었지만, 결과가 궁금해서 돌려봤다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1011.png&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;56&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baL49a/dJMcahQ49cS/YPJQYg8tGvWJKhgp8PyfrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baL49a/dJMcahQ49cS/YPJQYg8tGvWJKhgp8PyfrK/img.png&quot; data-alt=&quot;이렇게 하나씩 잘라서 훈련시켜야함..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baL49a/dJMcahQ49cS/YPJQYg8tGvWJKhgp8PyfrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaL49a%2FdJMcahQ49cS%2FYPJQYg8tGvWJKhgp8PyfrK%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;400&quot; height=&quot;56&quot; data-filename=&quot;1011.png&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;56&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이렇게 하나씩 잘라서 훈련시켜야함..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12&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;12,0,0&quot;&gt;결과:&lt;/b&gt; 학습 데이터 내에서는 꽤 정확하게 맞춘다. 과적합(Overfitting)일지도 모르겠지만, 어쨌든 '학습시키면 된다'는 희망은 봤다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,1,0&quot;&gt;문제:&lt;/b&gt; 제대로 된 성능을 내려면 최소 1,000개 이상의 데이터가 필요하다는데, 120개 만드는 데도 진이 다 빠졌다. 자동화가 절실했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;리서치를 좀더 해보니&amp;nbsp;&lt;b data-index-in-node=&quot;13&quot; data-path-to-node=&quot;14&quot;&gt;YOLO&lt;/b&gt;와 &lt;b data-index-in-node=&quot;19&quot; data-path-to-node=&quot;14&quot;&gt;OpenCV&lt;/b&gt;라는 도구를 알게되었다. 이걸 활용하면 내 노가다를 획기적으로 줄일 수 있을 것 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;15&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;15,0,0&quot;&gt;YOLO:&lt;/b&gt; 전체 화면에서 아이템 툴팁 박스가 어디 있는지 영역을 찾아내는 데 사용.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,1,0&quot;&gt;OpenCV:&lt;/b&gt; 찾아낸 툴팁 박스 안에서 글자 라인별로 이미지를 정교하게 크롭(Crop)하는 데 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;이 과정은 단순히 학습 데이터를 만들 때뿐만 아니라, 실제 프로그램이 동작하는 '추론' 단계에서도 필수적이라는 걸 깨달았다. 아직 OpenCV가 내 생각만큼 깔끔하게 라인을 따줄지는 테스트해봐야겠지만, 적어도 '무식한 노가다'에서 '공학적인 접근'으로 한 걸음 나아간 기분이다.&lt;/p&gt;</description>
      <author>siyoon210</author>
      <guid isPermaLink="true">https://siyoon210.tistory.com/202</guid>
      <comments>https://siyoon210.tistory.com/202#entry202comment</comments>
      <pubDate>Thu, 5 Mar 2026 08:17:32 +0900</pubDate>
    </item>
    <item>
      <title>유닛 테스트에서 '비즈니스 Fixture 객체'를 버려야 하는 이유</title>
      <link>https://siyoon210.tistory.com/201</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;유닛테스트 코드를 짜다 보면 어느덧 검증 로직보다 '테스트를 위한 준비 과정'이 더 길어지는 순간을 마주한다. 특히 특정 비즈니스 상태를 재현하기 위해 여러 도메인 모델을 조합하고 상태를 주입한 &lt;b data-index-in-node=&quot;107&quot; data-path-to-node=&quot;3&quot;&gt;픽스처(Fixture) 객체&lt;/b&gt;를 공용으로 만들어 쓰기 시작할 때, 불행은 시작된다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;흔히 픽스처라고 하면 데이터베이스 연결 상태나 환경 변수 같은 인프라적 설정도 있지만 내가 경계하는 것은 &lt;b data-index-in-node=&quot;62&quot; data-path-to-node=&quot;4&quot;&gt;비즈니스 상태를 미리 정의해둔 객체 덩어리&lt;/b&gt;다. 편리할 것 같지만 사실 이것은 테스트 코드의 건강을 해치는 독이 될 때가 많다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;4&quot; data-ke-size=&quot;size23&quot;&gt;1. 픽스처 객체는 테스트 코드의 의도를 알기 어렵게 만든다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공용 픽스처 객체를 사용하는 테스트는 불투명하다. 테스트 코드를 읽을 때, `setupUser()`가 반환하는 픽스처 객체의 어떤 필드가 이 테스트의 성공과 실패를 결정하는지 한눈에 들어오지 않는다. 유저는 VIP 등급인가? 연령이 만 19세 미만인가? 아니면 단순히 이름이 비어 있는 상태인가?&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;테스트는 그 자체로 명세서가 되어야 하지만 상세한 상태가 픽스처 객체 내부에 숨겨지는 순간 테스트 코드는 해석이 필요한 암호문으로 변질된다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;7&quot; data-ke-size=&quot;size23&quot;&gt;2. 픽스처 객체는 필연적으로 여러 테스트가 의존한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 요구사항은 끊임없이 변한다. 공용 픽스처 객체에 의존하는 테스트가 100개라고 가정해 보자. 비즈니스 규칙이 바뀌어 픽스처 객체의 기본 생성 로직이나 상태 값을 하나 수정하는 순간, 관련 없는 100개의 테스트가 도미노처럼 무너진다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;이때부터 개발자는 테스트 코드를 '신뢰'하는 것이 아니라 '수발'들게 된다. 테스트를 고치는 시간이 프로덕션 코드를 짜는 시간보다 길어진다면 그 테스트는 이미 자산을 넘어 부채가 된 것이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;픽스처 객체는 생성되는 순간부터 엄연한 &lt;b data-index-in-node=&quot;22&quot; data-path-to-node=&quot;12&quot;&gt;관리의 대상&lt;/b&gt;이다. 프로덕션 코드의 도메인 모델이 변경될 때마다 픽스처도 동기화해줘야 한다. 다양한 비즈니스 케이스를 대응하기 위해 FixtureA, FixtureB, ComplexFixtureC가 늘어나기 시작하면, 정작 중요한 비즈니스 로직보다 픽스처의 계보를 파악하는 데 더 많은 에너지를 쏟게 된다. 관리 비용이 기하급수적으로 비싸지는 기법인 셈이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;10&quot; data-ke-size=&quot;size23&quot;&gt;3. 그럼에도 불구하고 유닛테스트에 픽스처를 써야한다면.. 프로덕션 코드를 의심해보자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 비즈니스 상태를 굳이 객체로 만들어 주입할 필요가 있을까? 유닛 테스트의 본질은 '격리'에 있다. 복잡한 연관 관계나 비즈니스 상태를 가진 객체가 필요하다면, 실제 객체를 생성할 것이 아니라 모킹(Mocking)을 활용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모킹을 사용하면 테스트가 검증하고자 하는 &lt;b data-index-in-node=&quot;23&quot; data-path-to-node=&quot;16&quot;&gt;특정 상태&lt;/b&gt;만 명시적으로 드러낼 수 있다. 테스트 가독성은 올라가고, 실제 객체의 내부 구현 변경으로부터 테스트를 보호할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 모킹을 하기에 너무 힘들다라는 생각이 든다면, 그것은 픽스처 객체가 필요한 게 아니라 &lt;b data-index-in-node=&quot;87&quot; data-path-to-node=&quot;18&quot;&gt;프로덕션 코드가 설계적으로 실패했다는 신호&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;지나친 역할 분담 실패:&lt;/b&gt; 하나의 클래스가 너무 많은 비즈니스 규칙을 품고 있어 테스트 준비가 복잡한 것일 수 있다.&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복적인 설정과 복잡한 픽스처가 필요하다는 사실은 역설적으로 코드를 분리하라는 시스템의 신호이다. 픽스처 객체로 이 신호을 덮지 말고, 프로덕션 코드를 격리하고 분리하는 리팩토링을 시작해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;픽스처 객체는 편리해 보이지만 장기적으로는 테스트의 가독성을 해치고 관리 비용을 폭증시킨다. 통합 테스트처럼 반드시 실제 객체 간의 흐름을 봐야 하는 경우가 아니라면 비즈니스 상태는 모킹으로 대체하자. 그리고 모킹조차 힘들 만큼 코드가 꼬여 있다면 픽스처를 만들지 말고 코드를 쪼개자. 그것이 테스트 코드와 프로덕션 코드 모두를 살리는 길이다.&lt;/p&gt;</description>
      <category>Development</category>
      <category>FIXTURE</category>
      <category>mock</category>
      <category>유닛테스트</category>
      <author>siyoon210</author>
      <guid isPermaLink="true">https://siyoon210.tistory.com/201</guid>
      <comments>https://siyoon210.tistory.com/201#entry201comment</comments>
      <pubDate>Sun, 25 Jan 2026 09:51:19 +0900</pubDate>
    </item>
    <item>
      <title>ORM은 레거시가 될까?</title>
      <link>https://siyoon210.tistory.com/200</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;어느덧 개발자들 사이에서 JPA같은 ORM 기술은 언어를 가리지 않고 선택이 아닌 필수 교양이 되었다. 개발자 면접에서도 영속성 컨텍스트의 원리나 N+1 문제의 해결책을 묻는 것이 당연한 코스가 되었다. 하지만 문득 의문이 들었다. 당연하게 믿고 있는 이 도구가 사실은 레거시가 되어가지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. ORM은 왜 대세가 되었을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORM을 배우는 이유는 명확했다. 지루하고 반복적인 CRUD 쿼리 작성을 자동화하여 개발 효율을 높이고, 테이블 중심이 아닌 객체지향적인 사고로 비즈니스 로직을 구현하기 위함이다.&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 data-index-in-node=&quot;8&quot; data-path-to-node=&quot;5&quot;&gt;AI가 코딩하는 시대&lt;/b&gt;다. 과거에는 수십 개의 컬럼을 매핑하고 쿼리를 짜는 것이 '고비용 작업'이었지만, 이제는 AI에게 스키마만 던져주면 최적화된 SQL과 DTO 매핑 코드를 1초 만에 뱉어낸다. 반복적인 코드 작성이 더 이상 개발자의 발목을 잡는 '비용'이 아니게 된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향적인 사고? 내가 본 실무를 기반으로 얘기해보자면 판타지에 가깝다. 우리가 만드는 대부분의 엔티티(Entity)는 결국 DB 테이블과 1:1로 매핑되어 사용된다. 이름만 엔티티일 뿐, 사실상 DB의 데이터를 담아 나르는 DTO 역할에 그친다. 누군가는 DTO가 아니라고 말하며 엔티티내에 캡슐화된 메서드 몇개 넣었다고 그것을 객체지향프로그래밍이라고 부를지 모르겠지만 나는 아니라고 본다. 이미 테이블 주도 개발을 한뒤에 객체지향 특성을 일부 넣는다고 그것이 객체지향 프로그래밍이라고 하기 어렵다. ORM으로 객체지향프로그래밍이 불가능하다고 말하는것이 아니다. 현실적으로 엔티티를 정말 객체지향적으로 설계하기 어렵다는 말이다.&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;엔티티와 도메인 객체를 완벽히 분리하여 순수한 도메인 주도 설계(DDD)를 실천하는 프로젝트는 너무 드물다. 만약 그 정도로 철저히 분리할 수 있는 구조라면, 역설적으로 그 매핑의 브릿지 역할을 굳이 무거운 ORM이 맡을 필요도 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 편의를 가장한 문제 발생기, 그리고 비정상적인 학습 비용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORM은 개발을 편하게 해준다고 약속하지만, &lt;b&gt;그 대가로 개발자의 머릿속을 복잡하게 만든다&lt;/b&gt;. 단순히 쿼리 몇 줄 안 쓰는 대가로 우리가 지불해야 하는 학습 비용은 과연 정당한가?&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;JPA를 제대로 쓰기 위해 우리는 '영속성 컨텍스트'라는 거대한 블랙박스를 공부해야 한다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;N+1문제,&lt;span&gt; OSIV(Open Session In View), 더티 체킹&lt;/span&gt;&lt;/span&gt;, 1차 캐시.. 이름만 들어도 머리 아픈 이 개념들은 사실 DB의 기본 동작과는 거리가 먼, ORM이라는 도구가 만들어낸 인위적인 규칙들이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 JPA를 열심히 학습한 다음에 시니어들은 뭐라고 말하는가?&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;'결국 DB를 잘 알아야지..'&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;DB에 대한 학습도 추가적으로 해야한다. 그것도 ORM 이전시대와 다를바 없는 수준으로..&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;ORM이 뒤에서 어떤 쿼리를 생성하고 있는지 로그를 뒤져보며 성능을 튜닝하고, 영속성 컨텍스트의 복잡한 생명주기를 공부할 시간에 차라리 AI를 활용해 &lt;b data-index-in-node=&quot;84&quot; data-path-to-node=&quot;8&quot;&gt;명시적인 SQL&lt;/b&gt;을 작성하는 것이 훨씬 생산적이지 않을까?&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;AI는 SQL뿐만 아니라 그 SQL을 담을 매핑 코드까지 완벽하게 짜준다. JPA 구현을 학습할 필요가 없어진 것이다. 예측 가능성은 개발자를 학습의 굴레에서 해방시키고 코드를 가볍게 만든다. 내가 쓴 쿼리만 나간다는 확신, 그것이 주는 심리적 안정감은 ORM의 마법보다 훨씬 값질수 있다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;6&quot; data-ke-size=&quot;size26&quot;&gt;대충 마무리 하자면..&lt;/h2&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;기술은 시대의 요구에 따라 변한다. 한때 SQL의 지옥에서 우리를 구원해준 ORM은 분명 고마운 존재였다. 하지만 AI가 SQL을 대신 써주고, 도구의 복잡도가 본질인 비즈니스 로직을 압도하는 지금, 우리는 다시 단순함과 명확함으로 회귀해야 할 때가 아닐까.&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;마법 뒤에 숨어 ORM 철학을 공부하기보다, 투명한 코드로 시스템을 완전히 장악하는 것. 그것이 AI 시대를 살아가는 백엔드 개발자의 경쟁력이 되지 않을까 조심히 예측해본다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;다음 프로젝트에 JPA를 과감히 버리고 Spring JDBC를 사용해보고 싶다. 이에 대한 회고를 할수 있으면 좋겠다.&lt;/p&gt;</description>
      <category>Development</category>
      <category>jpa</category>
      <category>orm</category>
      <author>siyoon210</author>
      <guid isPermaLink="true">https://siyoon210.tistory.com/200</guid>
      <comments>https://siyoon210.tistory.com/200#entry200comment</comments>
      <pubDate>Sat, 27 Dec 2025 04:42:54 +0900</pubDate>
    </item>
    <item>
      <title>Next.js와 Supabase를 접고 다시 스프링으로</title>
      <link>https://siyoon210.tistory.com/199</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;나는 회사에서는 자바와 스프링을 주로 쓰는 백엔드 개발자로 일하지만, 개인적인 사이드 프로젝트에 대한 관심이 꾸준히 있었다. 그래서 개발 속도가 빠르고 유지보수가 편한 기술 스택을 따로 알아보고 공부하기 시작했다. 그러다가 Supabase와 Next.js로 프로젝트들을 만들기 시작했고, 개발 속도 측면에서는 꽤 만족스러웠다.&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;Supabase와 Vercel을 사용하면 서버비용 0원으로 시작할 수 있었고 서버 관리 부담도 거의 없어서 꽤 괜찮다고 생각했다. 프리티어를 넘는 트래픽이 오면 25달러와 20달러 정도는 충분히 낼 만한 프로젝트가 되리라 착각하기도 했다.&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;br /&gt;프리티어를 겨우 넘었을 뿐인데 매달 약 50달러의 비용이 발생했고, MAU가 크게 늘지 않으면서 이 고정비가 계속 유지되었다. 광고 수익은 5~10달러 수준이었기 때문에 사이드 프로젝트에 매달 40~50달러를 지출하는 구조는 지속 가능하지 않다는 결론에 도달했다. 게다가 &lt;b&gt;Supabase 스토리지 비용은 특히나 사악하게 비쌌다.&lt;/b&gt; 비싼 걸 알면서도 귀찮아서 도입한 내 잘못이 맞지만, 그래도 너무 비싸다는 생각을 하지 않을 수 없었다. (혹여나 Supabase를 쓰려거든 반드시 스토리지는 반드시 S3를 붙여야한다.)&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;리액트와 Next.js에 대한 개인적인 회고도 만족스럽지 않았다. 리액트보다는 Next.js에 대한 불만족이 더 큰데 &lt;span style=&quot;background-color: #ffffff; color: #0a0a0a; text-align: start;&quot;&gt;하이드레이션을 사용해서 프론트와 백엔드코드가 난잡하게 얽히기 시작한다. 물론 내가 메이저하게 사용하지 않고 경험과 지식이 모자라서 그럴수도 있다. ㅠㅠ..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;사이드 프로젝트에서는 메인 기술스택과 다른 언어나 프레임워크를 사용하면 개발적인 견문이 넓어질 것이라는 기대가 있었다. 실제로 구현 과정에서 새로운 경험을 쌓기는 했지만, 어느 순간부터는 깊이 있는 공부보다는 구현 자체에만 집중하게 되었고, 그 과정에서 더 깊은 학습이 이뤄지지 않는 한계를 느끼게 되었다. 어찌보면 당연할수도 있다. 빠르게 개발하려고 고른 스택이니 공부할 시간이 있겠는가?&lt;/p&gt;
&lt;p data-end=&quot;567&quot; data-start=&quot;359&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;567&quot; data-start=&quot;359&quot; data-ke-size=&quot;size16&quot;&gt;이런 고민들이 쌓이던 시점에 Supabase와 Vercel을 벗어나기 위해 마이그레이션을 고민하게 되었고, 그 과정에서 루비 온 레일즈에 자연스럽게 관심이 생겼다. 언어도 프레임워크도 익숙하지 않았지만, 레일즈의 빠른 개발 속도는 상당히 매력적으로 다가왔다. 게다가 hotwired 기술을 활용하면 리액트 없이도 SPA 같은 웹앱을 만들 수 있다는 점이 신선하게 느껴졌다.&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;AI를 활용한 코딩 만족도&lt;/b&gt;가 너무 높아졌다는 점이었다. 마이그레이션 비용도 줄었고, 코드 작성 시간도 크게 단축되면서 &lt;b&gt;굳이 익숙하지 않은 Next.js나 레일즈를 개발 편의를 위해 도입할 필요가 없다&lt;/b&gt;는 결론에 이르렀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 현재 진행 중인 프로젝트는 스프링부트로 마이그레이션하기로 결정했다. 서버도 DigitalOcean이나 AWS Lightsail로 옮기기로 했다. 이렇게 하면 순수 서버비는 월 5~10달러 정도로 예상되었고, 그러면 광고 수익과 거의 비슷해져 개인 비용 부담이 거의 사라지게 된다.&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;(참고로 레일즈의 hotwired 기술도 스프링부트에서 사용할수 있다.)&amp;nbsp; &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/web/webmvc-view/mvc-fragments.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-framework/reference/web/webmvc-view/mvc-fragments.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 유지보수에 일정한 리소스가 들어가겠지만, 익숙한 기술 스택이고 회사에서는 담당하지 않는 부분(배포과정, 스프링시큐리티 등)까지 직접 다루게 되기 때문에 오히려 메인 스택과의 시너지도 기대된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사의 마이크로서비스 환경에서는 안정적인 서버 자원을 기반으로 일하다 보니 어떤 부분에서는 안일해지기도 했는데, 사이드 프로젝트에서는 낮은 서버 자원을 다뤄야 하기 때문에 메모리 관리와 성능에 대한 고민도 다시 하게 되었다. (회사에서는 기본 힙 사이즈가 2~4GB였지만, 라이트세일에서는 256MB의 힙사이즈도 운영이 어느정도 가능한지 고민해본다..ㅋㅋ)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 이번 경험을 통해 깨달은 점은, 사이드 프로젝트는 새로운 기술을 배우는 재미도 중요하지만 &lt;b&gt;지속 가능하고 효율적인 개발 환경도&lt;/b&gt; 중요하다는 것이었다. 익숙하지 않은 기술로 속도를 내기보다, 익숙한 스프링부트와 저렴한 서버 환경으로 안정적인 개발과 운영을 병행하는 것이 장기적으로 훨씬 현실적이고 만족스러운 선택이길 기대한다.&lt;/p&gt;</description>
      <category>Development</category>
      <category>nextJS</category>
      <category>supabase</category>
      <category>Vercel</category>
      <category>스프링부트</category>
      <author>siyoon210</author>
      <guid isPermaLink="true">https://siyoon210.tistory.com/199</guid>
      <comments>https://siyoon210.tistory.com/199#entry199comment</comments>
      <pubDate>Tue, 18 Nov 2025 08:30:22 +0900</pubDate>
    </item>
    <item>
      <title>Exception throw를 두려워하지 말자.</title>
      <link>https://siyoon210.tistory.com/198</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;최근 코드를 보다 보니 문득 드는 생각이 하나 있었다.&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;왜 이렇게 Exception &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt;가 숨어있지?&amp;rdquo;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;백엔드 코드를 짜다 보면, 서비스 로직이라는 게 있고, 그 외의 &amp;lsquo;도구적인 작업&amp;rsquo;들이 있다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;DB를 쓰거나, 외부 API를 호출하거나, 캐시를 쓰거나, 이벤트를 전송하는 그런 것들.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;근데 문제는... &lt;b&gt;이런 도구적인 부분에서 너무 과도한 예외 처리를 해버린다는 거였다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;물론 의도는 좋았다. 장애가 안 나게 막으려는 거니까.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;근데 이게 꼭 좋은 건 아니라는 생각이 들었다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;왜냐면 예외를 그 자리에서 &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt;하고, 자체적으로 fallback을 처리해버리면&lt;/span&gt;&lt;br /&gt;&lt;span&gt;그건 &lt;b&gt;서비스 로직 바깥에서 서비스 로직을 대신 결정해버리는 셈&lt;/b&gt;이었으니까.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어보자.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;DB 저장이 실패했을 때, 외부 API 호출이 실패했을 때&lt;/span&gt;&lt;br /&gt;&lt;span&gt;그 상황에서 &amp;ldquo;fallback으로 빈값을 반환한다&amp;rdquo;거나 &amp;ldquo;그냥 로그만 찍고 무시한다&amp;rdquo;는 결정은,&lt;/span&gt;&lt;br /&gt;&lt;span&gt;사실 도메인 관점에서 굉장히 중요한 로직이었다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;어떤 경우에는 진짜 API 실패로 이어져야 했고,&lt;/span&gt;&lt;br /&gt;&lt;span&gt;어떤 경우에는 &amp;ldquo;괜찮아, 다음 단계로 진행해도 돼&amp;rdquo;라고 판단해야 했다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;그 판단은 결국 &lt;/span&gt;&lt;b&gt;&lt;span&gt;서비스 로직의 몫&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이지, DB나 API 호출 함수 내부의 몫이 아니었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 오히려 &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt;를 더 적극적으로 고민해야 한다고 봤다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&amp;ldquo;여기서 뭘 막을까?&amp;rdquo;보다 &amp;ldquo;이 실패를 위로 올렸을 때 서비스가 뭘 해야 하지?&amp;rdquo;를 고민하는 게 맞다고 생각했다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;그게 올바른 서비스 설계라는 생각을 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 코드를 보면 Output port (Repository, API Client..)쪽에&amp;nbsp;&lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt;가 너무 많았다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;마치 &amp;ldquo;예외는 절대 밖으로&amp;nbsp; 보내선 안 돼!&amp;rdquo; 라고 느껴졌다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;근데 그건 기계적인 방어 코드일 뿐, 서비스의 의미를 담은 로직은 아니었다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt;의 남발은 오히려 코드의 구조를 이해하기 흐리게 만드는 것 같았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;더 나아가 Checked Exception도 한 번쯤은 생각해봐야 하지 않을까.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;실패 가능성이 명확하고, 호출자가 그걸 반드시 처리해야 하는 경우라면.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;하지만 또 이걸 너무 남용하면 오히려 코드가 복잡해지고, &lt;/span&gt;&lt;span&gt;Checked Exception을 안티패턴처럼 보는 분위기도 있으니까&lt;/span&gt;&lt;br /&gt;&lt;span&gt;이건 조심스럽게 접근해야 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결론적으로 하고싶은말은, &lt;/span&gt;&lt;span&gt;도구는 도구일 뿐이다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;DB, API, 캐시, 이벤트 - 이런 건 서비스 로직을 구현하기 위한 수단이었고,&lt;/span&gt;&lt;br /&gt;&lt;span&gt;그 실패를 어떻게 다룰지는 온전히 서비스의 결정이 되어야 했다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;그래서 &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt;를 줄이고, &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt;를 고민해야 한다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;예외는 막는 게 아니라, &lt;/span&gt;&lt;span&gt;의미 있게 다루어져야 한다.&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Development</category>
      <category>예외</category>
      <category>예외처리</category>
      <author>siyoon210</author>
      <guid isPermaLink="true">https://siyoon210.tistory.com/198</guid>
      <comments>https://siyoon210.tistory.com/198#entry198comment</comments>
      <pubDate>Sun, 2 Nov 2025 12:23:08 +0900</pubDate>
    </item>
    <item>
      <title>Spring Batch에서 JSON 형식 JobParameter 전달하기</title>
      <link>https://siyoon210.tistory.com/197</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;개&lt;/span&gt;&lt;span&gt;발 환경&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-indent=&quot;0&quot;&gt;&lt;span&gt;&lt;span&gt;Spring Boot&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;3.&lt;/span&gt;&lt;span&gt;5.0&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-indent=&quot;0&quot;&gt;&lt;span&gt;&lt;span&gt;Spring Batch&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;5.&lt;/span&gt;&lt;span&gt;2.&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-indent=&quot;0&quot;&gt;&lt;span&gt;&lt;span&gt;Java&lt;/span&gt;&lt;/span&gt;&lt;span&gt;: 2&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;문제 상황&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Spring Batch Job을 실행할 때 아래와 같은 JSON 문자열을 파라미터로 전달하고 싶었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761970492205&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;myType&quot;: &quot;MY_TYPE_VALUE&quot;,
  &quot;myArray&quot;: [
    {
      &quot;key&quot;: {&quot;id&quot;: &quot;001&quot;, &quot;category&quot;: &quot;MY_CATEGORY_VALUE&quot;},
      &quot;param&quot;: {&quot;foo&quot;: &quot;002&quot;, &quot;bar&quot;: &quot;sampleValue&quot;}
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만&amp;nbsp;일반적인&amp;nbsp;방식으로&amp;nbsp;전달하면&amp;nbsp;다음과&amp;nbsp;같은&amp;nbsp;에러가&amp;nbsp;발생했습니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;JobParametersConversionException: Unable to parse job parameter&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;왜 JSON 전달이 안 될까?&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;DefaultJobParametersConverter의 한계&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Spring Batch의&lt;span&gt; DefaultJobParametersConverter&lt;/span&gt;&lt;/span&gt;&lt;span&gt;는 다음과 같은 형식으로 파라미터를 파싱합니다:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;paramKey=value,type&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를&amp;nbsp;들어:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;count=100,long&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;name=test,string&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는&amp;nbsp;JSON에도&amp;nbsp;,&amp;nbsp;(쉼표)가&amp;nbsp;있다는&amp;nbsp;것입니다!&lt;/p&gt;
&lt;pre id=&quot;code_1761970782051&quot; class=&quot;erlang-repl&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;{&quot;name&quot;:&quot;test&quot;,&quot;age&quot;:30}
        &amp;uarr; 이 쉼표를 타입 구분자로 인식!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;Spring Batch의 JsonJobParametersConverter (이거 안씀 주의)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Spring Batch 5.x부터&lt;span&gt; org.springframework.batch.core.converter.JsonJobParametersConverter&lt;/span&gt;&lt;/span&gt;&lt;span&gt;가 제공됩니다.&lt;/span&gt;&lt;span&gt;하지만 이것은&lt;span&gt;&amp;nbsp;제가&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; 원하는 방식과 차이가 있었는데&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;Spring Batch JsonJobParametersConverter는 JobParameters 전체&lt;/span&gt;&lt;/span&gt;&lt;span&gt;를 JSON으로 직렬화/역직렬화하는 용도입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761970950847&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;{
  &quot;paramName&quot;: {
    &quot;value&quot;: &quot;actualValue&quot;,
    &quot;type&quot;: &quot;java.lang.String&quot;,
    &quot;identifying&quot;: true
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해서 기존 파라미터를 value, type을 명시하고, 다시 한번 이를 문자열 형식으로 escape 시켜야 하므로 파라미터 생김새가 지나치게 복잡한 문제가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;해결 방법: 커스텀 Converter&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Json 형식의 파라미터를 구분하고 무조건&lt;span&gt; String&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;타입으로 처리하는, 즉 쉼표 파싱을 건너뛰는 커스텀 컨버터를 구현합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예시 코드에서는 'JsonStr' 으로 끝나는 프로퍼티를 String 타입으로 판단하도록 했습니다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761974348151&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.converter.DefaultJobParametersConverter;
import org.springframework.stereotype.Component;

import java.util.Properties;

/**
 * Json 형식 문자열 파라미터를 String으로 인식하기 위한 커스텀 JobParametersConverter
 * 
 * DefaultJobParametersConverter 구현상 ',' 로 value와 type을 구분하는데
 * JSON은 구조상 ','가 있어서 별도 처리가 필요함
 * 
 * JsonStr로 끝나는 모든 프로퍼티는 자동으로 String으로 등록됩니다.
 * 예: batchRequestJsonStr -&amp;gt; batchRequestJsonStr (String 타입)
 */
@Component
public class JsonStringAvailableJobParametersConverter extends DefaultJobParametersConverter {

    private static final String JSON_STR_SUFFIX = &quot;JsonStr&quot;;

    @Override
    public @NonNull JobParameters getJobParameters(@Nullable Properties properties) {
        if (properties == null || properties.isEmpty()) {
            return super.getJobParameters(properties);
        }

        // JsonStr로 끝나는 프로퍼티와 아닌 프로퍼티 분리
        Properties jsonStrProperties = new Properties();
        Properties otherProperties = new Properties();
        
        for (String key : properties.stringPropertyNames()) {
            if (key.endsWith(JSON_STR_SUFFIX)) {
                jsonStrProperties.setProperty(key, properties.getProperty(key));
            } else {
                otherProperties.setProperty(key, properties.getProperty(key));
            }
        }

        // 다른 Properties는 DefaultJobParametersConverter에 위임
        JobParameters baseParams = super.getJobParameters(otherProperties);
        
        // JsonStr로 끝나는 프로퍼티들을 String으로 추가
        JobParametersBuilder builder = new JobParametersBuilder(baseParams);
        for (String key : jsonStrProperties.stringPropertyNames()) {
            String value = jsonStrProperties.getProperty(key);
            builder.addString(key, value);
        }

        return builder.toJobParameters();
    }
}&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;커스텀 Converter를 Bean으로 등록하고 사용처에서 아래와 같이 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;@Value(&quot;#{jobParameters['batchRequestJsonStr']}&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&gt;배치 실행시 문자열 전송을 위해서 escape 처리는 필요합니다&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761975212303&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;--spring.batch.job.name=myBatchJob
batchRequestJsonStr={\&quot;myType\&quot;:\&quot;MY_TYPE_VALUE\&quot;,\&quot;myArray\&quot;:[{\&quot;key\&quot;:{\&quot;id\&quot;:\&quot;001\&quot;,\&quot;category\&quot;:\&quot;MY_CATEGORY_VALUE\&quot;},\&quot;param\&quot;:{\&quot;foo\&quot;:\&quot;002\&quot;,\&quot;bar\&quot;:\&quot;sampleValue\&quot;}}]}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;장점&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-indent=&quot;0&quot;&gt;&lt;span&gt;복잡한 JSON 객체를 파라미터로 전달 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-indent=&quot;0&quot;&gt;&lt;span&gt;기존 파라미터 처리 방식은 그대로 유지&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-indent=&quot;0&quot;&gt;&lt;span&gt;확장 가능: 여러 JSON 파라미터 추가 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-indent=&quot;0&quot;&gt;&lt;span&gt;명시적:&lt;span&gt; 'JsonStr'&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;접미사로 의도 표현&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring Framework</category>
      <category>job parameter</category>
      <category>json</category>
      <category>Spring Batch</category>
      <author>siyoon210</author>
      <guid isPermaLink="true">https://siyoon210.tistory.com/197</guid>
      <comments>https://siyoon210.tistory.com/197#entry197comment</comments>
      <pubDate>Sat, 1 Nov 2025 13:24:05 +0900</pubDate>
    </item>
    <item>
      <title>MSA에서 헥사고널 아키텍처가 적절하지 않은 이유</title>
      <link>https://siyoon210.tistory.com/196</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;의존 역전에 대한 고찰&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향의 가장 큰 장점은 의존 방향을 우리가 원하는 대로 제어하기 쉽다는 점이다. 예를 들어 A &amp;rarr; B 구조에서 B의 변동 가능성이 클 경우, A &amp;rarr; I &amp;larr; B 형태로 구조를 바꿔서 &lt;b&gt;A를 변화와 확장으로부터 보호하는 것&lt;/b&gt;, 그게 핵심이다. 여기서 중요한 건 A와 I가 같은 바운더리 안에 있어야 한다는 점이다. 왜냐하면 I는 A가 원하는 인터페이스이기 때문이다. 그런데 의존을 역전시켰다고 하면서 I와 B를 같은 바운더리에 위치시키는 경우가 종종 보이는데 이는 의존 역전에 대한 오해다.&lt;/p&gt;
&lt;p data-end=&quot;802&quot; data-start=&quot;536&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;873&quot; data-start=&quot;804&quot; data-ke-size=&quot;size16&quot;&gt;의존 역전의 본질은 인터페이스를 사용했다는 것 자체가 아니라,&lt;b&gt; 변동에 대한 제어 주도권을 역전시켜 가져왔다는 점&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;966&quot; data-start=&quot;875&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;966&quot; data-start=&quot;875&quot; data-ke-size=&quot;size26&quot;&gt;마이크로서비스에서의 의존역전&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA의 기술적 성장은 안정화되어 가고 있지만, 개발자들의 인식은 그에 미치지 못하는 경우가 많다. 대부분의 마이크로서비스는 원래 하나의 모놀리식 애플리케이션이었고, 결국 하나의 비즈니스 문제를 해결하기 위한 것이라는 사실을 잊어서는 안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1364&quot; data-start=&quot;1136&quot; data-ke-size=&quot;size16&quot;&gt;여기서 중요한 개념이 도메인이다. 도메인은 우리가 해결하고자 하는 &lt;b&gt;비즈니스 문제&lt;/b&gt;이지, 특정 서버나 애플리케이션이 아니다. 그런데도 종종 개발자들은 도메인을 하나의 서버로 대입시키고 기술적 경계에만 집중한다. 이러다보면 실무에서 다른 마이크로서비스와의 협업이 어색해질수 있다. &lt;b&gt;도메인은 여러 마이크로서비스의 묶음으로 봐야 자연스럽다.&lt;/b&gt; 왜냐하면 우리가 해결해야 하는 비즈니스 문제는 특정 서비스 하나에 국한되지 않기 때문이다.&lt;/p&gt;
&lt;p data-end=&quot;1364&quot; data-start=&quot;1136&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1500&quot; data-start=&quot;1366&quot; data-ke-size=&quot;size16&quot;&gt;MSA를 다루는 개발자들이 도메인을 하나의 거시적인 비즈니스로 인식하지 못하고, 자신이 관리하는 서비스에서 다른 서비스와의 결합도를 낮추는 데에만 집중한다는 것이다. 그런데 인터페이스 하나 달랑 만든다고 해서 결합도가 낮아졌다고 생각하는 건 굉장한 착각이다. 실무 경험이 있다면 조금만 생각해보자 의존하는 다른 서버의 의존성을 떨어트린다고 해서 진짜 내가 관리하는 서버의 도메인 객체들은 보호받는가? 아닐 확률이 높다. &lt;b&gt;결국 하나의 거시적 도메인이라면 강결합을 받아드리는것이 자연스럽다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1500&quot; data-start=&quot;1366&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1670&quot; data-start=&quot;1502&quot; data-ke-size=&quot;size16&quot;&gt;진짜 의존 역전이라면, 내가 호출하는 외부 서비스의 변화와 확장에도 불구하고, &lt;b&gt;제어 주도권이 나의 서비스에 있어야 한다.&lt;/b&gt; 그런데 현실적으로 이는 거의 불가능하다. 왜냐하면 &lt;b&gt;기획의 시선에서는 마이크로서비스 하나하나에 관심이 없고, 전체 비즈니스에 관심이 있기 때문이다.&lt;/b&gt; 그리고 그게 당연한 현실이다.&lt;/p&gt;
&lt;p data-end=&quot;1849&quot; data-start=&quot;1672&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1849&quot; data-start=&quot;1672&quot; data-ke-size=&quot;size16&quot;&gt;결국 의존 역전이 의미 있는 순간은 다음과 같다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1975&quot; data-start=&quot;1879&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1910&quot; data-start=&quot;1879&quot;&gt;인프라스트럭처 혹은 온전한 외부 서비스(예: PG, 지도 API)&lt;/li&gt;
&lt;li data-end=&quot;1947&quot; data-start=&quot;1911&quot;&gt;관리하는 서버가 의존하는 기능에 대해 온전히 제어권을 갖고 의존 역전이 가능할때&lt;/li&gt;
&lt;li data-end=&quot;1975&quot; data-start=&quot;1948&quot;&gt;명백한 변화 가능성과 확장이 예측될 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마이크로서비스에서 헥사고널 아키텍처가 어색한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 본론이다. 왜 마이크로서비스 환경에서는 헥사고널 아키텍처가 어색할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2257&quot; data-start=&quot;2060&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하나의 서버가 하나의 도메인이 될 수 없기 때문&lt;/b&gt;이다. MSA는 의도적으로 서버를 쪼갠 것이고, 그 과정에서 도메인이 함께 쪼개졌다고 착각하는 경우가 많다. 하지만 도메인은 여전히 하나의 비즈니스 문제이고, 여러 서비스에 걸쳐 흩어져 있다. 하나의 서비스에 헥사고널 아키텍처를 적용하면 &lt;b&gt;강제로 도메인을 분리한 듯한 어색함&lt;/b&gt;이 발생한다.&lt;/p&gt;
&lt;p data-end=&quot;2257&quot; data-start=&quot;2060&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2364&quot; data-start=&quot;2259&quot; data-ke-size=&quot;size16&quot;&gt;코드상으로는 보호된 것처럼 보여도, 실제론 그렇지 않다. 왜냐하면 기획자가 정의한 도메인은 하나의 서버나 하나의 코드베이스로 표현될 수 있는 개념이 아니기 때문이다.&lt;/p&gt;
&lt;p data-end=&quot;2364&quot; data-start=&quot;2259&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2364&quot; data-start=&quot;2259&quot; data-ke-size=&quot;size16&quot;&gt;헥사고널 아키텍처를 적용해서 나만의 완벽한 세상을 구축했다고 착각할 수도 있다. 어떤 확장이 와도 우아하게 대처할 수 있을 것처럼 느껴진다. 하지만 현실은 기획자의 피드백 한 마디에 그 세계는 쉽게 무너진다. 결국 그들이 상상한 &amp;lsquo;비즈니스 문제&amp;rsquo;가 개발지의 아키텍처 경계를 넘나들게 만들기 때문이다.&lt;/p&gt;
&lt;p data-end=&quot;2364&quot; data-start=&quot;2259&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2587&quot; data-start=&quot;2492&quot; data-ke-size=&quot;size16&quot;&gt;그리고 헥사고널 아키텍처는 비용이 많이 든다.&lt;span&gt;&amp;nbsp;패키지&lt;/span&gt; 수만 봐도 일반적인 레이어드 아키텍처보다 4배 이상 많아진다. (&lt;a href=&quot;https://youtu.be/g6Tg6_qpIVc?si=4O7tJRWnXaii1uQX&amp;amp;t=1823&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://youtu.be/g6Tg6_qpIVc?si=4O7tJRWnXaii1uQX&amp;amp;t=1823&lt;/a&gt;) 작은 팀, 빠른 개발, 빈번한 피드백 루프가 필요한 MSA 환경에서는 상당히 부담스러운 구조다. 외부 시스템에 대한 의존 영향을 줄이고 싶다면 꼭 헥사고널 아키텍처일 필요는 없다. Adapter나 ACL(anti-corruption layer)만으로도 충분히 해결 가능한 경우가 많다. 스프링 같은 DI 프레임워크를 사용해서 비즈니스 컴포넌트를 관리한다면 테스트 작성에도 어려움이 없다.&lt;/p&gt;
&lt;p data-end=&quot;2587&quot; data-start=&quot;2492&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2587&quot; data-start=&quot;2492&quot; data-ke-size=&quot;size16&quot;&gt;헥사고널 아키텍처는 훌륭한 개념이다. 하지만 모든 곳에 적용할 수 있는 만능 도구는 아니다. 특히 마이크로서비스 환경에서는, 아키텍처보다는 비즈니스 문제의 흐름과 현실적인 제어권 분포를 더 먼저 고민해야 한다. 진짜 의존 역전은 인터페이스 몇 개로 해결되지 않는다. 현실에 맞는 구조를 고민해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;2587&quot; data-start=&quot;2492&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Development</category>
      <category>MSA</category>
      <category>헥사고널 아키텍처</category>
      <author>siyoon210</author>
      <guid isPermaLink="true">https://siyoon210.tistory.com/196</guid>
      <comments>https://siyoon210.tistory.com/196#entry196comment</comments>
      <pubDate>Tue, 1 Jul 2025 07:36:11 +0900</pubDate>
    </item>
    <item>
      <title>Next.js + Vercel 환경에서 한글 검색 구현 삽질기록</title>
      <link>https://siyoon210.tistory.com/195</link>
      <description>&lt;h2 data-end=&quot;177&quot; data-start=&quot;112&quot; data-ke-size=&quot;size26&quot;&gt;Next.js + Vercel 환경에서 &lt;b&gt;한글 검색 구현기&lt;/b&gt;: 삽질기부터 직접 bigram 구현까지&lt;/h2&gt;
&lt;p data-end=&quot;354&quot; data-start=&quot;179&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Next.js + Vercel&amp;nbsp;환경에서 한글 검색 기능 구현&lt;/b&gt;이 의외로 골치 아픈 일이었고 이를 정리하기 위해서 공유합니다.&lt;/p&gt;
&lt;p data-end=&quot;421&quot; data-start=&quot;356&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;460&quot; data-start=&quot;428&quot; data-ke-size=&quot;size23&quot;&gt;1) LIKE 검색? 간단하지만 성능에 불안 요소&lt;/h3&gt;
&lt;p data-end=&quot;533&quot; data-start=&quot;462&quot; data-ke-size=&quot;size16&quot;&gt;처음엔 누구나 WHERE title LIKE '%검색어%'로 시작합니다.&lt;br /&gt;간단하죠. 하지만 다음과 같은 문제가 있습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;619&quot; data-start=&quot;535&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;572&quot; data-start=&quot;535&quot;&gt;인덱스를 타지 못해 데이터가 많아지면 검색 속도가 확 느려짐&lt;/li&gt;
&lt;li data-end=&quot;594&quot; data-start=&quot;573&quot;&gt;전체 스캔이 발생해 비용이 비쌈&lt;/li&gt;
&lt;li data-end=&quot;619&quot; data-start=&quot;595&quot;&gt;단순 포함 검색이라 의미 기반 검색 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;635&quot; data-start=&quot;621&quot; data-ke-size=&quot;size16&quot;&gt;그래서 바로 포기했습니다.&lt;/p&gt;
&lt;p data-end=&quot;635&quot; data-start=&quot;621&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;691&quot; data-start=&quot;642&quot; data-ke-size=&quot;size23&quot;&gt;2) PostgreSQL tsvector? &amp;hellip;한글 형태소 분석은 미지원&lt;/h3&gt;
&lt;p data-end=&quot;767&quot; data-start=&quot;693&quot; data-ke-size=&quot;size16&quot;&gt;PostgreSQL의 tsvector + to_tsvector 조합은 풀텍스트 검색을 위해 매우 강력한 기능입니다.&lt;br /&gt;예시:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1746357356452&quot; class=&quot;pgsql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM posts WHERE to_tsvector('english', title) @@ plainto_tsquery('english', 'search');&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;969&quot; data-start=&quot;878&quot; data-ke-size=&quot;size16&quot;&gt;하지만 문제는 한글입니다. PostgreSQL 기본 설정에선 영어, 프랑스어 등은 지원하지만 &lt;b&gt;한국어는 형태소 분석기가 없어서 의미 단위로 쪼갤 수 없습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;969&quot; data-start=&quot;878&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1007&quot; data-start=&quot;971&quot; data-ke-size=&quot;size16&quot;&gt;그래서 tsvector를 그대로 쓸 수 없다는 한계가 있었죠.&lt;/p&gt;
&lt;p data-end=&quot;1007&quot; data-start=&quot;971&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;2331&quot; data-start=&quot;2311&quot; data-ke-size=&quot;size23&quot;&gt;tsvector?&lt;/h3&gt;
&lt;p data-end=&quot;2434&quot; data-start=&quot;2333&quot; data-ke-size=&quot;size16&quot;&gt;간단히 말해 tsvector는 &lt;b&gt;검색을 빠르게 하기 위한 텍스트 인덱스 포맷&lt;/b&gt;입니다.&lt;br /&gt;단어 단위로 쪼개서 인덱싱하고, tsquery로 빠르게 검색할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-end=&quot;1007&quot; data-start=&quot;971&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1053&quot; data-start=&quot;1014&quot; data-ke-size=&quot;size23&quot;&gt;3) 형태소 분석기 라이브러리 사용? &amp;rarr; Vercel에서 막힘&lt;/h3&gt;
&lt;p data-end=&quot;1146&quot; data-start=&quot;1055&quot; data-ke-size=&quot;size16&quot;&gt;여러 형태소 분석기를 활용하려 했지만, Vercel은 &lt;b&gt;서버리스 환경&lt;/b&gt;이라:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1195&quot; data-start=&quot;1148&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1165&quot; data-start=&quot;1148&quot;&gt;별도 라이브러리를 설치하거나&lt;/li&gt;
&lt;li data-end=&quot;1195&quot; data-start=&quot;1166&quot;&gt;자바나 C++ 기반으로 실행되는 것 자체가 불가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1266&quot; data-start=&quot;1197&quot; data-ke-size=&quot;size16&quot;&gt;Vercel은 무조건 빠르고 짧게 끝나는 함수 중심으로 돌아가기 때문에, 별도 형태소 분석기는 애초에 구조적으로 어렵습니다.&lt;/p&gt;
&lt;p data-end=&quot;1266&quot; data-start=&quot;1197&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1309&quot; data-start=&quot;1273&quot; data-ke-size=&quot;size23&quot;&gt;4) ETRI 형태소 분석기 사용? &amp;rarr; IP 차단됨  &lt;/h3&gt;
&lt;p data-end=&quot;1357&quot; data-start=&quot;1311&quot; data-ke-size=&quot;size16&quot;&gt;다음 시도로 &lt;b&gt;ETRI 공개 API&lt;/b&gt;를 활용했는데, 여기서도 문제가 발생합니다. (&lt;a href=&quot;https://aiopen.etri.re.kr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://aiopen.etri.re.kr/&lt;/a&gt;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1462&quot; data-start=&quot;1359&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1394&quot; data-start=&quot;1359&quot;&gt;서버리스는 호출할 때마다 &lt;b&gt;서버 IP가 바뀌는 구조&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1431&quot; data-start=&quot;1395&quot;&gt;일정 횟수 초과되면 &quot;다중 IP 접근으로 인한 차단&quot; 발생&lt;/li&gt;
&lt;li data-end=&quot;1462&quot; data-start=&quot;1432&quot;&gt;결과적으로 ETRI API도 지속적으로 쓸 수 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1510&quot; data-start=&quot;1469&quot; data-ke-size=&quot;size23&quot;&gt;5) pg_bigm 확장 모듈? &amp;rarr; Supabase에서 미지원&lt;/h3&gt;
&lt;p data-end=&quot;1598&quot; data-start=&quot;1512&quot; data-ke-size=&quot;size16&quot;&gt;PostgreSQL에는 pg_bigm이라는 강력한 Bigram 기반 검색 확장이 있습니다.&lt;br /&gt;한글처럼 &lt;b&gt;공백 없이 붙어 있는 언어&lt;/b&gt;에서 유용하죠.&lt;/p&gt;
&lt;p data-end=&quot;1705&quot; data-start=&quot;1600&quot; data-ke-size=&quot;size16&quot;&gt;하지만 Supabase에서는 아직 pg_bigm 확장을 &lt;b&gt;공식 지원하지 않습니다&lt;/b&gt;.&lt;br /&gt;설치하려면 커스텀 서버를 써야 하고, 그건 Supabase를 쓰는 이유 자체를 흐리게 만들죠.&lt;/p&gt;
&lt;p data-end=&quot;1705&quot; data-start=&quot;1600&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1705&quot; data-start=&quot;1600&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Bigram?&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1705&quot; data-start=&quot;1600&quot; data-ke-size=&quot;size16&quot;&gt;Bigram은 문자열을 &lt;b&gt;두 글자씩 끊어서 분석하는 방식&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1705&quot; data-start=&quot;1600&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1754&quot; data-start=&quot;1712&quot; data-ke-size=&quot;size23&quot;&gt;최종 해결책: 직접 Bigram + tsvector 조합 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 선택한 방법은 &lt;b&gt;직접 Bigram으로 쪼개서 tsvector 형태로 저장&lt;/b&gt;하고, 이를 활용하는 방식이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre id=&quot;code_1746357561627&quot; class=&quot;maxima&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// 예: &quot;한국어검색&quot; &amp;rarr; [&quot;한국&quot;, &quot;국어&quot;, &quot;어검&quot;, &quot;검색&quot;]
function toBigram(str: string): string[] {
  const grams = [];
  for (let i = 0; i &amp;lt; str.length - 1; i++) {
    grams.push(str.slice(i, i + 2));
  }
  return grams;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 결과를 미리 tsvector 타입 형태로 변환해서 별도 컬럼에 저장해두고, 검색 키워드도 bigram 형태로 만들어 tsquery를 수행합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ 컬럼은 tsvector 타입이어야 하고, gin 인덱스를 걸어야 성능상 이점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;2577&quot; data-start=&quot;2570&quot; data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-end=&quot;2648&quot; data-start=&quot;2579&quot; data-ke-size=&quot;size16&quot;&gt;Next.js + Vercel + Supabase 조합에서 &lt;b&gt;한글 검색을 제대로 구현하기 위한 삽질기&lt;/b&gt;를 정리해봤습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2774&quot; data-start=&quot;2650&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2666&quot; data-start=&quot;2650&quot;&gt;LIKE는 성능상 한계&lt;/li&gt;
&lt;li data-end=&quot;2701&quot; data-start=&quot;2667&quot;&gt;PostgreSQL 기본 tsvector는 한글 미지원&lt;/li&gt;
&lt;li data-end=&quot;2734&quot; data-start=&quot;2702&quot;&gt;외부 형태소 분석기/서비스는 서버리스에서 사용이 어려움&lt;/li&gt;
&lt;li data-end=&quot;2774&quot; data-start=&quot;2735&quot;&gt;그래서 결국 Bigram + tsvector 방식으로 직접 해결&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2820&quot; data-start=&quot;2776&quot; data-ke-size=&quot;size16&quot;&gt;비슷한 환경에서 고민 중이라면, 이 글이 조금이나마 도움이 되었으면 좋겠습니다.&lt;/p&gt;</description>
      <category>Development</category>
      <category>Next.js</category>
      <category>supabase</category>
      <category>Vercel</category>
      <category>한글검색</category>
      <category>한글형태소</category>
      <author>siyoon210</author>
      <guid isPermaLink="true">https://siyoon210.tistory.com/195</guid>
      <comments>https://siyoon210.tistory.com/195#entry195comment</comments>
      <pubDate>Sun, 4 May 2025 20:23:44 +0900</pubDate>
    </item>
  </channel>
</rss>