<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>max-ph.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 02 Nov 2026 09:09:14 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>max-ph.log</title>
            <url>https://velog.velcdn.com/images/max-ph/profile/f5707d58-9858-4fb5-a9e1-613c9b33da59/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. max-ph.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/max-ph" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[트러블 슈팅] 가만히 둔 EC2 가 5개월 만에 504 를 뱉은 이유 (JVM 메모리, Swap, CPU 크레딧)]]></title>
            <link>https://velog.io/@max-ph/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@max-ph/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</guid>
            <pubDate>Mon, 02 Nov 2026 09:09:14 GMT</pubDate>
            <description><![CDATA[<h1 id="1-배경">1. 배경</h1>
<blockquote>
<p><a href="https://github.com/bongtubaekseo/BongBaek-Server">봉투백서</a></p>
</blockquote>
<p>봉투백서는 사회 초년생을 위해 경조사비를 상황에 맞게 추천해주고, 관리해주는 서비스입니다.
AWS EC2 서비스인 t2.micro(1GB) 인스턴스를 사용했고, docker-compose를 사용해 아래와 같은 컨테이너를 돌렸습니다.</p>
<pre><code>NAMES            설명
bongbaek1        Spring Boot 3.5 / Java 21 API 서버. 8081 포트
bongbaek2        bongbaek1 과 같은 이미지. 8082 포트. 로드밸런싱을 위한 이중화 컨테이너
nginx            리버스 프록시. 80/443 을 받아 두 서버로 least_conn 분배

redis            인증 토큰, 캐시 저장소

prometheus       15초마다 actuator 와 nginx exporter 지표 수집
grafana          Prometheus 대시보드. 
dozzle           컨테이너 로그 웹 뷰어
nginx-exporter   nginx stub_status 를 Prometheus 지표로 변환</code></pre><p>1월 배포 인해 변경사항은 없었으나, 6월부터 CPU 크레딧이 고갈되며 모든 요청에 대해 504 time out 에러를 뱉는 문제가 생겼습니다.</p>
<h1 id="2-문제-상황">2. 문제 상황</h1>
<ul>
<li>1월 6일 배포 후 5개월간 5xx 에러 0건</li>
<li>6월 20일 → Docker 헬스체크 요청이 timeout(10초)에 걸렸고, 이날부터 사용자 요청 모두 504 time out 및 에러 서버 제외 로직 때문에, 503 nginx error까지 발생함</li>
</ul>
<h1 id="3-원인-분석">3. 원인 분석</h1>
<h3 id="3-1-무엇이-time-out을-유발했는가">3-1. 무엇이 time out을 유발했는가?</h3>
<p><strong>메모리 초과로 스래싱 발생</strong>
    - EBS 읽기: 5/13 814만/일 → 7월 초 2,700만/일
    - 스래싱으로 인해 CPU 사용량이 5월 초 <code>8.6%</code> -&gt; 6월 초 평균 <code>10.5%</code>로 증가
    - 6월 20일에 남은 크레딧 모두 소진된 이후 요청 처리 속도가 감소</p>
<p><strong>스래싱으로 CPU 사용량 증가 -&gt; 상한선을 넘어 CPU 크레딧 고갈</strong>
    - 1gb로 돌리기 힘든 8개 컨테이너를 운용하기 위해 Swap Memory 전략을 사용
    - t2.migro는 CPU 사용량 10% 이상부터 크레딧 소진이 발생</p>
<h3 id="3-2-가만히-둔-서버인데-왜-메모리-초과가-발생했는가">3-2. 가만히 둔 서버인데 왜 메모리 초과가 발생했는가?</h3>
<ul>
<li>프로젝트를 마무리하면서 메트릭 기록을 백업해 두지 않아, 얼만큼 메모리를 사용했는지 찾아볼 수 없었습니다.. 그래서 추청된 원인을 나열해 보려고 합니다. </li>
</ul>
<p><strong>Swap Memory로도 해결할 수 없는 상시 사용 메모리</strong></p>
<ul>
<li>Swap Memory 부족한 램의 해결책이 아닙니다
OOM과 같은 메모리 사용률이 불규칙 할 때 서버의 다운 등을 해결하는데는 용이하지만, 
상시 가용해야하는 컨테이너를 돌리기 위해서는 인스턴스의 램 크기(t2.micro=<code>1gb</code>)내에서 동작 해야합니다</li>
</ul>
<p><strong>JVM 컨테이너의 메모리 사용량 증가</strong></p>
<ul>
<li>8개의 컨테이너를 동시에 작동시키기에는 t2.micro는 부족한 메모리 환경이었다. JVM 컨테이너(bongbaek1, bongbaek2)는 기본 상한인 ram 한도 25%까지 자랄 수 있습니다
문제를 조치하는 시점에서 메모리 사용률이 약 <code>390MB</code>이었다 -&gt; Spring boot 컨테이너 가동 시 처음 메모리 사용량은 평균 <code>200MB</code>로 추정</li>
</ul>
<p>두 컨테이너로만 <code>800MB</code>를 사용했기에 버틸 수 있는 메모리 수준을 넘어섰고, 사용자 요청, healthcheck와 같은 상시 요청 처리가 disk로 넘아간 것이 주된 원인이라고 생각합니다</p>
<h1 id="4-해결-방법-검토">4. 해결 방법 검토</h1>
<p><strong>1. JVM 옵션 설정</strong></p>
<ul>
<li>컨테이너 설정으로 제한<pre><code class="language-yaml"># docker-compose.yml
</code></pre>
</li>
</ul>
<p>services:
  bongbaek1:
    container_name: bongbaek1
    image: bongbaek-server:latest
    restart: unless-stopped            # OOM 으로 죽으면 다시 띄움
    mem_limit: 450m                    # 컨테이너 램 상한. 넘으면 즉시 OOM kill
    memswap_limit: 450m                # 램+스왑 합산 상한. mem_limit 과 같게 → 스왑 사용 금지
    environment:
      JAVA_TOOL_OPTIONS: &quot;-XX:MaxRAMPercentage=50 -XX:+ExitOnOutOfMemoryError&quot;
                                       # MaxRAMPercentage=50 : 힙 상한 = 컨테이너 한도의 50% (225MB)
                                       # ExitOnOutOfMemoryError : OOM 시 즉시 종료 → restart 로 재기동</p>
<pre><code>
- 빌드 설정으로 제한</code></pre><p>// build.gradle
tasks.named(&#39;bootBuildImage&#39;) {
    imageName = &#39;bongbaek-server&#39;
    environment = [
        &#39;BP_JVM_VERSION&#39;      : &#39;21&#39;,   // 이미지에 넣을 JDK 버전
        &#39;BPL_JVM_THREAD_COUNT&#39;: &#39;50&#39;,   // 메모리 계산에 쓸 스레드 수. 기본 250, 줄이면 힙에 더 배정
        &#39;BPL_JVM_HEAD_ROOM&#39;   : &#39;10&#39;    // 한도 중 JVM 이 비워둘 비율(%). glibc 등 계산 밖 메모리 여유분
    ]
}</p>
<pre><code>**2. 컨테이너 단일화**
- 이중화 전략(무중단 배포와 가용성을 위해 컨테이너 이중화를 선택)을 위해 2개의 API 컨테이너를 상시 가동했으나, 1gb로는 어려운 구조였던 거 같습니다
- 배포 때에만 2개를 띄어 배포 안정성과 무중단을 구현하는 방향이 좋았을 거 같다

**3. 모니터링 스택을 클라우드로 변경**
dozzle, grafana, prometheus, nginx-exporter 컨테이너를 모두 합치면 메모리 사용량이 `300~350MB` 입니다.
해당 스택을 **grafana cloud**로 변경한다면, 위 스택들로 이루고자 하는 nginx, api 서버 메트릭 관리와 로그 저장까지 가능합니다.
무료 버전 한도는 14일 보관, 로그 50GB/월, 대시보드 1,000 수준으로 14일 이내로 로그 메트릭 저장하는 파이프라인만 구성한다면 기존과 동일한 수준에서 처리가 가능합니다

# 5. 회고

**- 백업 데이터 소실**
- 인스턴스를 삭제 하면서, 로그는 백업했지만 메트릭에 관한 정보를 추출하지 않았습니다.
이로인해 언제부터 크레딧이 왜 고갈됐으며, 어떤 문제가 메모리 스래싱을 야기했는지 파악할 수 없었습니다. 

**- observerbility 부족**</code></pre><h1 id="nginxconf">nginx.conf</h1>
<p>upstream backend {
    least_conn;
    server bongbaek1:8080 max_fails=3 fail_timeout=10s;
    server bongbaek2:8080 max_fails=3 fail_timeout=10s;
}
proxy_connect_timeout 3s;
proxy_read_timeout    20s;
proxy_next_upstream   error timeout http_502 http_503 http_504;</p>
<p>```</p>
<ul>
<li><p>서버 요청 장애에 대한 로드밸런싱 전략은 존재했으나 해당 문제가 발생 시 책임자가 볼 수 있는 alert 기능이 부재했습니다</p>
</li>
<li><p>또한, 지속적인 healthcheck 기능은 있었으나 문제 발생 시 최종 재시작하는 대응책이 없었습니다</p>
</li>
</ul>
<p>[출처] 
<strong>Swap Memory</strong>
<a href="https://docs.cloud.google.com/kubernetes-engine/docs/how-to/node-memory-swap?hl=ko">https://docs.cloud.google.com/kubernetes-engine/docs/how-to/node-memory-swap?hl=ko</a>
<a href="https://docs.aws.amazon.com/ko_kr/AmazonECS/latest/developerguide/container-swap.html">https://docs.aws.amazon.com/ko_kr/AmazonECS/latest/developerguide/container-swap.html</a></p>
<p><strong>JVM Memory 증가</strong>
<a href="https://mainmethod0126.github.io/java/Analyzing-JAVA-Memory-Leak-Issues">https://mainmethod0126.github.io/java/Analyzing-JAVA-Memory-Leak-Issues</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블 슈팅] 가용성을 위한 time out, retry 전략 사용하기]]></title>
            <link>https://velog.io/@max-ph/%EA%B0%80%EC%9A%A9%EC%84%B1%EC%9D%84-%EC%9C%84%ED%95%9C-time-out-retry-%EC%A0%84%EB%9E%B5-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@max-ph/%EA%B0%80%EC%9A%A9%EC%84%B1%EC%9D%84-%EC%9C%84%ED%95%9C-time-out-retry-%EC%A0%84%EB%9E%B5-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 03 Oct 2026 07:30:09 GMT</pubDate>
            <description><![CDATA[<h2 id="1-배경">1. 배경</h2>
<blockquote>
<p><a href="https://github.com/yourssu/signal-backend">signal-backend</a></p>
</blockquote>
<p><em>2026-1학기 축제 기간 데이팅 서비스인 &#39;signal&#39;의 observer 서버가 조용히 죽는 사고가 발생했습니다.</em></p>
<ul>
<li><strong>흐름</strong>: 입금, 프로필 관련 로그 파일을 watchdog이 확인한 뒤 Slack으로 알림을 발송합니다</li>
<li><strong>구조</strong>: 인증코드로 계좌이체 → 서버에서 인증코드를 식별하고 이체내역을 검증한 뒤 티켓을 발급합니다</li>
</ul>
<p>Slack <code>발급 실패 알림</code>을 통해 서버 개발자 외에 다른 운영자 분들도 CS를 처리할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/max-ph/post/49ebd1fe-c523-4653-bb88-a73d190f1cca/image.png" alt=""></p>
<p>watchdog 서버가 멈추면 로그를 수동으로 확인한 뒤 처리해야 하기 때문에,
<strong><em>alert를 안정적으로 유지하는 것은 signal 서비스의 중요한 부분이라고 할 수 있습니다.</em></strong></p>
<h2 id="2-문제-상황">2. 문제 상황</h2>
<ul>
<li>1일차 17:17 ~ 17:59 → 메인 프로세스 로그가 남지 않았고, 결제는 들어왔으나 Slack 알림은 0건이었습니다</li>
<li>2일차 00:30 ~ 02:33 → observer가 멈춰 2시간 동안 알림이 없었습니다</li>
<li>2일차 16:06 ~ 17:12 → observer가 죽었고, 재시작을 두 번 해도 살아나지 않았습니다</li>
<li>세 번 모두 프로세스는 살아 있어서 <strong>바로</strong> 인지하지 못했습니다</li>
<li>휴대폰으로 Toss 입금 확인 메시지는 오는데 Slack 알림이 오지 않아 문제가 있음을 인지했습니다</li>
</ul>
<h2 id="3-원인-가능성-분석">3. 원인 가능성 분석</h2>
<h3 id="로그-소실">로그 소실</h3>
<ul>
<li>로그 파일이 4KB 단위로 잘린 흔적이 남아 있었습니다</li>
<li>디스크 공간 부족으로 로그 쓰기가 실패했을 가능성이 높습니다</li>
</ul>
<h3 id="observer-멈춤">observer 멈춤</h3>
<ul>
<li>watchdog은 싱글 스레드 기반 모니터링 라이브러리입니다 → Slack 요청에 timeout 정책이 없어, 응답이 오지 않으면 스레드가 무한 대기 상태에 빠졌을 가능성이 높습니다</li>
</ul>
<h3 id="observer-즉사">observer 즉사</h3>
<ul>
<li>잘린 로그에 깨진 글자가 섞여 있었습니다 → 읽다가 예외가 발생해 스레드가 종료되었습니다</li>
<li>재시작해도 같은 줄을 다시 읽어서 또 죽었습니다 → 재시작이 소용없었던 이유입니다</li>
</ul>
<h2 id="4-해결-방법">4. 해결 방법</h2>
<h3 id="timeout--retry">timeout / retry</h3>
<ul>
<li>Slack 요청에 timeout 10초를 적용해 무한 대기를 방지했습니다</li>
<li>실패하면 최대 3번 재시도합니다</li>
<li>그래도 실패하면 디스크에 저장해 두었다가 다음 전송이 성공할 때 함께 재전송합니다</li>
</ul>
<h3 id="로그-읽기">로그 읽기</h3>
<ul>
<li>어디까지 읽었는지 저장합니다 → 처음부터 다시 읽지 않고 재시작 후에도 이어서 읽습니다</li>
<li>깨진 글자가 있어도 죽지 않고 넘어갑니다</li>
</ul>
<h3 id="observer-admin-로그-분리">observer, admin 로그 분리</h3>
<ul>
<li>기존에는 메인 API 서버 로그만 남았습니다</li>
<li>observer, admin 컨테이너를 분리해 컨테이너별로 로그가 따로 남도록 개선했습니다</li>
</ul>
<h3 id="healthcheck--재시작">healthcheck / 재시작</h3>
<ul>
<li>healthcheck가 3번 연속 실패하면 Slack alert를 보내고 해당 컨테이너를 자동 재시작합니다</li>
<li>재시작 후에도 unhealthy 상태가 지속되면 자동 재시작하지 않고 Slack 서버 로그 채널에 실패 알림을 전송합니다</li>
<li>컨테이너를 분리했기 때문에 observer, admin이 재시작되어도 메인 API 서버에는 영향이 없습니다</li>
</ul>
<h3 id="system-metric-monitoring">system metric monitoring</h3>
<ul>
<li>클라우드 서버의 disk 용량이 부족해지면 Slack alert가 오도록 구현했습니다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/max-ph/post/7ab15249-c7e1-41ac-80ec-fc23c43e792e/image.png" alt=""></p>
<h2 id="5-회고">5. 회고</h2>
<p>원인을 확정하지 못한 이유는 watchdog 로그를 남기지 않아 어떤 이유로 멈췄는지 알 수 없었기 때문이고,
즉각적으로 대응하지 못한 이유는 서버 상태를 확인하는 Observability(관측 가능성)가 없었기 때문입니다.</p>
<p>2026-1학기 observer 관련 문제는 모두 &quot;<strong>가용성</strong>&quot;과 관련된 문제였습니다.</p>
<p><strong>MTTD</strong> (Mean Time To Detect): 장애를 얼마나 빨리 알아채는가?
<strong>MTTR</strong> (Mean Time To Recovery): 장애를 얼마나 빨리 복구하는가?</p>
<p>watchdog이 꺼져도 티켓 발급은 서버가 처리하기 때문에 사용자 장애로 이어지지는 않았습니다. 대신 이번 문제를 통해</p>
<ol>
<li>timeout, retry를 이용해 장애 자체를 예방하는 방식을 선택했고,</li>
<li>healthcheck, 자동 재시작, monitoring alert를 이용해 <strong>MTTD와 MTTR</strong>을 줄이는 아키텍처로 개선했습니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[유저수!=0인 서비스 만들기 (PSF 방법론)]]></title>
            <link>https://velog.io/@max-ph/%EC%8B%A4%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9E%88%EB%8A%94-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@max-ph/%EC%8B%A4%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9E%88%EB%8A%94-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 29 Sep 2026 15:10:52 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>개발자를 꿈꾸는 컴공 학생이라면 포트폴리오에 넣을 멋진 프로젝트 하나 만들기를 꿈꾼다.</p>
<p>나도 늘 그래왔다.
기술 스택 정하고, ERD 설계하고, API 명세 쓰고, 배포까지 긴 노력을 들이며 당당하게 서비스를 출시한다.</p>
<p>그런데 배포하고 나서 대시보드를 열어보면...</p>
<pre><code>DAU : 1
가입자 : 나, 팀원, 지인</code></pre><p><em>서버는 잘 돌아가는데 쓰는 사람은 없는, 예쁜 쓰레기가 되었다.</em></p>
<p>돌아보면 지금까지 했던 프로젝트는 전부 이런 순서였다.</p>
<pre><code>아이디어 떠올림 → 기능 정리 → 개발 → 배포 → (아무도 안 씀) → 포트폴리오에 적음</code></pre><p>기술적으로는 배운 게 많았다.
그런데 면접에서 &quot;그래서 <strong>왜</strong> 이 기술을 쓰게 되었나요?&quot;라는 질문을 받으면 할 말이 없었다.
트래픽이 없으니 성능 개선도, 장애 대응도, 사용자 피드백 반영도 전부 <strong>가정</strong>일 뿐이었다.</p>
<p>SW마에스트로 과정에서 아이디어를 선정할 때도 처음에는 창의적인 솔루션만 생각했다.
&quot;이런 거 있으면 좋지 않을까?&quot;로 시작해서 바로 멘토님들께 피드백을 요청했다.</p>
<p>그때 제일 많이 들은 말이 이거였다.</p>
<blockquote>
<p><strong>&quot;페르소나 유저 만나봤나요? 가설 검증 해봤어요?&quot;</strong></p>
</blockquote>
<p><strong>가설검증</strong>. 머리로는 이해되지만, 막상 하라면 이보다 어려울 수가 없다.
누구를 만나야 하는지, 뭘 물어봐야 하는지, 어디까지 확인해야 검증했다고 말할 수 있는지 전혀 갈피를 잡을 수 없다.</p>
<p>물론 정답은 없다. 데이터를 분석하더라도 기준은 주관적일 수밖에 없고, 특히 데이터가 적은 초기 서비스일수록 스스로의 판단이 더 중요해진다.</p>
<p>아래에서는 마에스트로에서 프로젝트를 선정하며 문제 가설을 검증했던 과정을 회고해보려고 한다.</p>
<hr>
<h2 id="1-psf-방법론이란">1. PSF 방법론이란?</h2>
<blockquote>
<p><strong>PSF(Problem-Solution Fit)</strong> : 내가 정의한 문제가 실제로 존재하고, 내 해결책이 그 문제를 푸는지 확인하는 단계</p>
</blockquote>
<pre><code>Problem-Solution Fit  →  Product-Market Fit  →  Scale
(문제가 진짜 있나?)      (계속 쓰고 돈 내나?)     (키우기)</code></pre><p>대부분의 프로젝트는 문제 가설을 건너뛰고 바로 <strong>Solution</strong>부터 만든다.
머릿속으로 대충 문제를 정의하고, 그걸 해결하는 독창적인 아이디어에만 집중하게 된다.</p>
<p>가장 큰 함정은 그 문제가 <strong>내 머릿속에만 있을 수 있다</strong>는 것이다.
피땀 흘려 만든 서비스를 지인한테 설명해도 뜨뜻미지근한 반응이 돌아오는 건,
지인은 그 문제에 반응하는 <strong>페르소나</strong>도 아닐 뿐더러, 서비스가 그 문제를 정확히 해결해 주는지 검증조차 해보지 않았기 때문이다.</p>
<p>PSF에서 확인하는 핵심은 두 가지다.</p>
<pre><code>1. 문제가 실존하는가?   → 이 불편을 실제로 겪는 사람이 있나
2. 해결책에 반응하는가? → 해결책을 줬을 때 실제로 행동하나</code></pre><p>린 스타트업과 맘 테스트(The Mom Test) 같은 고객 개발(Customer Development) 방법론에서 공통으로 강조하는 원칙은 하나다.</p>
<p><strong>&quot;의견이 아니라 행동을 본다.&quot;</strong></p>
<p>&quot;이런 앱 있으면 쓰실 거예요?&quot;라고 물으면 대부분 &quot;네, 좋을 것 같아요&quot;라고 답한다.
그런데 그 사람이 실제로 쓸지는 전혀 다른 문제다.</p>
<hr>
<h2 id="2-문제가-실존하는지-확인하는-방법">2. 문제가 실존하는지 확인하는 방법</h2>
<p>그 당시 세웠던 여러 문제 가설 중, 서로 다른 방법으로 검증한 3가지를 소개하려고 한다.</p>
<pre><code>시각장애인 안마원   →  만들기 전에 인터뷰
헬스장 환불       →  프로토타입 만들고 반응 측정
셋로그 소개팅     →  직접 주최하기</code></pre><hr>
<h3 id="2-1-원장님-이-부분이-힘드신-거-맞을까요">2-1. 원장님, 이 부분이 힘드신 거 맞을까요?</h3>
<p>첫 번째는 <strong>아무것도 만들기 전에</strong> 사용자를 만나러 가는 방식이었다. 이때 가지고 있던 것은 <strong>문제 가설</strong>과 <strong>시장조사</strong> 데이터뿐이었다.</p>
<pre><code>가설: 시각장애인 원장은 예약 관리, 바우처 행정 같은 전산 업무가 어려워서
      어쩔 수 없이 인건비와 불편을 감수하고 있을 것이다.</code></pre><p>맘 테스트 원칙대로 아이디어는 숨기고, 최근에 실제로 어떻게 하고 있는지를 묻는 질문지를 만들었다.</p>
<pre><code>- 손님은 주로 어떤 경로로 오시나요?
- 도와주시는 분이 대신 해주는 업무는 어떤 게 있나요?
- 그 업무를 직접 해보신 적 있으세요? 그때 어떠셨어요?
- 핸드폰은 어떻게 쓰시나요?
- 마법의 지팡이가 있다면 뭘 바꾸고 싶으세요?</code></pre><p>첫 관문은 늘 이런 인터뷰에 응해줄 <strong>사람을 찾는 것</strong>이다.</p>
<p><img src="https://velog.velcdn.com/images/max-ph/post/12ca71ef-05c8-4611-9f76-8dcb19c79060/image.png" alt=""></p>
<p>집 근처 시각장애인 안마원 정보를 찾아보고, 번호로 문자를 보내거나 전화를 걸었다.
모르는 사람의 인터뷰 요청에 시간을 내줄 이유는 없고, 거절은 당연한 반응이다.
여러 번 거절당한 끝에, 흔쾌히 인터뷰에 응해주신 원장님을 만날 수 있었다.</p>
<p><strong>인터뷰에서 들은 것</strong></p>
<p>업무는 이미 잘 돌아가고 있었다.
예약은 전화와 문자로 원활했고, 청소나 안내는 활동지원사 한 분이 돕고 있었다.</p>
<p>오히려 예상 못 한 답이 나왔다. 문자 입력 방식을 묻자 이렇게 답했다.</p>
<blockquote>
<p>음성으로 하면 기계가 발음을 못 알아들어서 오타가 날 수 있잖아요.
눈이 안 보이니까 철자도 모른다는 인식을 손님들한테 남기면 안 되니까,
긴 문장은 블루투스 키보드나 컴퓨터로 작성해서 보내요.</p>
</blockquote>
<p>접근성 문제를 <strong>이미 스스로 해결하고 있었고</strong>, 그 이유는 편의가 아니라 <strong>전문가로서의 자존심</strong>이었다.</p>
<p>마법의 지팡이 질문에는 이렇게 답했다.</p>
<blockquote>
<p>보행을 좀 자유롭게 할 수 있으면 좋겠죠. 모르는 길 갈 때가 불편하긴 해요.</p>
</blockquote>
<p>가장 큰 불편은 업무가 아니라 일상생활 속에 있었다. 일은 실내에서 하니 큰 어려움이 없었던 것이다.</p>
<p>마지막으로 온라인 홍보를 편하게 해주면 쓰시겠냐고 물었다.</p>
<blockquote>
<p>한 번 잘 만든다고 되는 게 아니라 계속 관리를 해야 되잖아요.
손님 후기나 경험담을 남기고 싶은 마음은 있는데, 아직 시행은 안 하고 있어요.</p>
</blockquote>
<p><strong>PSF 회고</strong></p>
<pre><code>문제가 실존하는가?
  - 전산 업무 불편     ✗  이미 활동지원사, 키보드로 해결 중
  - 온라인 홍보 관리   △  &quot;마음은 있다&quot;는 의견. 지금 쓰는 시간이나 돈은 없음

해결책에 반응하는가?
  - 확인 불가</code></pre><p>홍보 질문은 사실 내가 해결책을 먼저 꺼낸 거였다.
&quot;마음은 있는데 아직 안 하고 있다&quot;는 맘 테스트 기준으로 <strong>가장 약한 신호</strong>다.
정말 아픈 문제라면 이미 뭐라도 하고 있었을 것이다.</p>
<p>팀원이 인터뷰한 다른 안마원도 비슷했다. 예약은 전화와 기억으로, 전산 업무는 가족이 대신 하고 있었다.</p>
<p>결론은 <strong>우리가 풀 만큼 아픈 문제는 없다</strong>는 것이었다.
코드 한 줄 쓰기 전에 알게 된 게 인터뷰의 가장 큰 수확이었다.</p>
<hr>
<h3 id="2-2-헬스장-환불-안해주면-어떡하죠">2-2. 헬스장 환불, 안해주면 어떡하죠..?</h3>
<p>두 번째는 방식을 바꿨다.
헬스장 환불로 고생하는 사람은 한곳에 모여 있지 않아서 직접 찾아가기 어렵다.
그래서 <strong>핵심 기능만 있는 프로토타입</strong>을 먼저 만들고, 행동 데이터로 확인하기로 했다.</p>
<p>아이디어 정리는 대략 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/max-ph/post/54f5b5af-bd31-4637-b7c0-1a7633bcd13a/image.png" alt=""></p>
<p>헬스장 환불은 법적 기준이 명확하다. 위약금은 10%까지만, &quot;환불 불가&quot; 약관은 무효다.
그런데도 사람들은 법을 모르고, 절차가 귀찮아서 적당히 합의하거나 포기할 거라고 봤다.</p>
<p>이 내용을 바탕으로 최소 기능의 프로토타입을 구현했다. 바이브 코딩이 쉬워지면서, 고객 의견을 모으는 것보다 최소 기능을 만드는 게 더 빠를 때도 있는 것 같다.
링크 - <a href="https://claimauto.aphelion.ai.kr/">claimauto</a></p>
<p>지금 상황을 입력하면 법적 기준에 맞는 환급액을 보여주고, 헬스장에 보낼 문서까지 만들어주는 서비스다.</p>
<pre><code>결제 금액, 이용 기간 입력
    ↓
법적 기준으로 환급액 자동 계산
    ↓
헬스장에 보낼 PDF 청구서 발급</code></pre><p>만든 링크는 헬스장 환불로 고민하는 사람이 있을 만한 커뮤니티에 직접 올렸다.</p>
<p><img src="https://velog.velcdn.com/images/max-ph/post/9ee4accc-bbac-42cf-bc9a-a92591c9219f/image.png" alt="">
<em>왼쪽부터 에브리타임, 지역 커뮤니티, 지식인 답변</em></p>
<p>그리고 <strong>Mixpanel을 붙여서</strong> 사람들이 핵심 기능까지 오는지 확인했다.</p>
<p><strong>일별 지표 (4/27 ~ 5/4)</strong></p>
<table>
<thead>
<tr>
<th>날짜</th>
<th align="right">방문자</th>
<th align="right">폼 제출</th>
<th align="right">PDF 저장</th>
</tr>
</thead>
<tbody><tr>
<td>4/27</td>
<td align="right">67명</td>
<td align="right">5명</td>
<td align="right">2명</td>
</tr>
<tr>
<td>4/28</td>
<td align="right">73명</td>
<td align="right">5명</td>
<td align="right">0명</td>
</tr>
<tr>
<td>4/29</td>
<td align="right">19명</td>
<td align="right">1명</td>
<td align="right">0명</td>
</tr>
<tr>
<td>4/30</td>
<td align="right">16명</td>
<td align="right">2명</td>
<td align="right">1명</td>
</tr>
<tr>
<td>5/1</td>
<td align="right">7명</td>
<td align="right">0명</td>
<td align="right">0명</td>
</tr>
<tr>
<td>5/2</td>
<td align="right">3명</td>
<td align="right">1명</td>
<td align="right">1명</td>
</tr>
<tr>
<td>5/3</td>
<td align="right">17명</td>
<td align="right">2명</td>
<td align="right">0명</td>
</tr>
<tr>
<td>5/4</td>
<td align="right">6명</td>
<td align="right">0명</td>
<td align="right">0명</td>
</tr>
<tr>
<td><strong>합계</strong></td>
<td align="right"><strong>208명</strong></td>
<td align="right"><strong>16명</strong></td>
<td align="right"><strong>4명</strong></td>
</tr>
</tbody></table>
<pre><code>방문 208명  →  폼 제출 16명 (7.7%)  →  PDF 저장 4명 (폼 제출자의 25%)</code></pre><p><strong>PSF 회고</strong></p>
<pre><code>문제가 실존하는가?
  - △  8일 동안 208명이 들어왔고 16명은 자기 계약 정보를 직접 입력했다
         환불로 고민하는 사람은 분명히 있다

해결책에 반응하는가?
  - ✗  환급액 확인까지는 왔지만 PDF 청구서를 받은 건 4명
         &quot;얼마 받을 수 있나&quot;는 궁금해도, 그걸 들고 헬스장과 싸울 생각까지는 없었다</code></pre><p>문제 자체는 있었다. 하지만 헬스장 환불은 1년에 한 번 겪을까 말까 한 문제고,
이 해결책은 그 순간의 <strong>궁금증</strong>은 풀어줬지만 <strong>행동</strong>까지 이어지진 못했다.</p>
<p>그래도 이번엔 &quot;잘 안 됐다&quot;가 아니라
<strong>어느 단계에서 멈췄는지</strong>를 숫자로 말할 수 있었다. 측정을 붙인 덕분이다.</p>
<hr>
<h3 id="2-3-셋로그로-소개팅-한-번-주선해볼까-ㅎㅎ">2-3. 셋로그로 소개팅 한 번 주선해볼까.. ㅎㅎ</h3>
<p>세 번째는 인스타 릴스 하나에서 시작했다.</p>
<blockquote>
<p>셋로그 앱으로 단체 소개팅을 하는 게 유행이다.</p>
</blockquote>
<p>셋로그는 친구끼리 하루 일상을 짧은 영상으로 공유하는 앱이다.
정해진 규칙은 없다. 원하는 사람을 초대해서 원할 때 올리면 된다.</p>
<p>이걸 소개팅에 쓰는 걸 보면서 이런 생각이 들었다.</p>
<blockquote>
<p>&quot;사람에게 호감을 느끼게 하는 건 결국 프로필이 아니라 일상 속에서 나오는 매력 아닐까?&quot;</p>
</blockquote>
<p>그래서 자유로운 셋로그 앱 위에 <strong>우리만의 형식</strong>을 얹었다.
남녀 3:3으로 방을 만들고, 딱 3일 동안 서로의 일상을 공유하고, 마지막 날 마음에 드는 사람을 고른다.</p>
<p>이번엔 앱을 먼저 만들지 않았다.
<strong>이미 있는 셋로그 앱으로 소개팅을 직접 주최했다.</strong>
코드 한 줄 없이 사람이 손으로 서비스를 돌린 셈이다.</p>
<pre><code>에브리타임, 인스타 모집
    ↓
신청 폼
    ↓
3:3 방 편성 → 셋로그에서 3일간 일상 공유
    ↓
최종 선택 → 서로 고르면 연락처 전달</code></pre><p><img src="https://velog.velcdn.com/images/max-ph/post/a784659d-b476-4e77-b552-e6cc8119705c/image.png" alt=""></p>
<p>소개팅 자체는 사실 이미 존재하는 시장이기 때문에,
이번 검증의 질문은 &quot;사람들이 이걸 할까?&quot;가 아니었다.</p>
<pre><code>문제:   외모와 스펙으로 빠르게 판단하고, 매칭 뒤엔 억지로 대화를 이어가야 하는
        기존 소개팅 방식에 지친 사람이 있는가?
해결책: 3일 동안 서로의 일상을 보고 고르는 방식에
        실제로 돈과 시간을 쓰는가?</code></pre><p>처음엔 방이 3일 동안 제대로 굴러가는지만 확인했고, 이후 기수마다 더 세분화된 지표를 트래킹하게 되었다.</p>
<pre><code>신청 → 참가비 입금 → 3일간 영상 업로드 수 → 최종 선택 비율 → 서로 매칭률 → 재참가 수 → 신청 ...</code></pre><p>기수가 끝날 때마다 최종 선택과 함께 설문도 받았다. 지표와 설문을 정리하면 이렇다.</p>
<p><strong>PSF 회고</strong></p>
<pre><code>문제가 실존하는가?
  - ✓  참가비를 내고 신청했다. 누적 120명
       최근 1년 데이팅 앱 사용 경험은 7% (7~8기 설문)
       → 앱 대신 소개팅, 과팅을 받던 사람들이 이 방식을 선택했다

해결책에 반응하는가?
  - ✓  3일 동안 영상을 올리고 최종 선택까지 왔다. 누적 27쌍 매칭
  - ✓  상대를 고른 이유 1위는 외모(44%)가 아니라 영상 속 댓글과 반응(52%) (7~8기 설문)
       → &quot;일상을 보고 고른다&quot;는 가설이 행동으로 확인됐다
  - △  업로드를 안 한 이유 1위는 &quot;올릴 게 없어서&quot;
       → 방식은 맞는데, 올리게 만드는 장치가 필요했다</code></pre><p>세 번의 검증 중 처음으로 문제와 해결책 둘 다 <strong>행동</strong>으로 확인됐다.
다음 기수에 다시 신청하는 사람도 생기기 시작했다. 우리 방식을 좋아해 주는 첫 팬이었다.</p>
<p>그런데 여기서 한계가 보였다.
셋로그는 남의 앱이라 기능을 바꿀 수도, 로그를 볼 수도 없었다. 업로드 수 같은 지표도 전부 손으로 셌다.
PSF는 확인했지만, 해결책을 더 좋게 만들려면 <strong>우리가 직접 바꿔가며 검증할 수 있는 판</strong>이 필요했다.</p>
<p>운영하며 막힌 곳이 그대로 우리 앱의 출발점이 됐다.</p>
<pre><code>셋로그로 운영하며 막힌 곳                  →  우리 앱에서 바꾸는 부분
기능 실험이 제한적이며 로그도 못 봄          →  미션, 질문, 선택 기능을 직접 수정하며 데이터로 검증
후기 중 &quot;궁금한 걸 물어볼 방법이 없음&quot; 응답   →  방 안 채팅, 질문 미션, 익명 질문 기능 추가
홍보, 신청, 운영, 후기를 모두 외부에 의존    → 앱 내에 참가자 사이클을 관리</code></pre><p>머릿속에서 정한 기능이 아니라, 몇 기수를 손으로 돌리면서 부딪힌 문제에서 나온 솔루션(기능)들이다.</p>
<p>지금은 셋로그로 하던 소개팅을 우리가 만든 앱 <a href="https://apps.apple.com/kr/app/%ED%95%98%EB%A3%A8%ED%95%98%EB%A3%A8/id6790147623">하루하루</a>에서 운영하고 있다.
기존에 진행한 셋로그 소개팅 참가자들 덕분에 앱 내 베타 테스트 약 30명을 진행할 수 있었고, 우리의 가장 큰 고민은 <strong>&quot;사용자가 0명이면 어떡하지?&quot;</strong>같은 추상적인 고민이 아니라 &quot;신규 참가자 노출을 위해 현재 문제점은 무엇인가?&quot;와 <strong>&quot;기존 앱 설치 유저의 재참가를 위해 부족한 점은 무엇인가?&quot;</strong>처럼 구체적인 문제가 되었다.</p>
<p><strong>세 아이디어 모두, 서비스를 만들기 전에 유저를 먼저 만나보며 판단할 수 있었다.</strong></p>
<hr>
<h2 id="3-회고">3. 회고</h2>
<pre><code>안마원      인터뷰          → 문제 가설이 틀렸음을 확인했다
헬스장      프로토타입+측정  → 문제에는 중간 정도 반응했으나, 행동으로 이어지지 않았다
셋로그      직접 주최        → 사람이 돈과 시간을 쓰는 걸 봤고, 팬이 생겼다</code></pre><p>뒤로 갈수록 사용자가 내놓는 게 커졌다.</p>
<pre><code>말  &lt;  클릭  &lt;  시간과 돈</code></pre><p>그리고 PSF를 확인하고 나서야 &quot;무엇을 만들지&quot;가 정해졌다.
셋로그 사례에서 앱의 기능은 아이디어 회의가 아니라, 운영하며 막힌 곳에서 나왔다.
개발은 검증의 끝이 아니라, 검증을 계속하기 위한 도구였다.</p>
<p>물론 다른 의견도 있을 수 있다. 
안마원 인터뷰에서 가장 큰 불편이 일상생활 속 불편함이었다면 그 문제를 더 파고들면 좋겠다고 생각하는 사람도 있을 것이고, 
헬스장 문제도 다른 솔루션을 만들어 인터뷰해가며 발전시키면 좋은 서비스가 될 수도 있을 것이다.</p>
<p>이 과정의 핵심은 &quot;문제가 존재하는가? → 해결책이 이 문제에 적합한가?&quot;를 끊임없이 자문해야 한다는 것이다.
개발자는 주어진 <strong>문제</strong>를 어떻게 해결할지에 집중하는 경향이 있다. 하지만 정작 <strong>&quot;그 문제가 실존하는가?&quot;</strong>라는 질문에 답해본 사람은 많지 않다고 생각한다.</p>
<p>실사용자가 있는 서비스를 만들고 싶거나, 대 바이브 코딩 시대에 <strong>토이 프로젝트</strong>가 아닌 돈을 버는 서비스를 혼자서 기획해보고 싶다면, 코딩보다 언제나 먼저 답해야 하는 질문이 있다.</p>
<p><strong>&quot;이 문제 때문에 지금 이미 시간이나 돈을 쓰고 있는 사람이 있는가?&quot;</strong></p>
<p>개발은 언제나 그다음이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA] N+1 문제란 무엇인가? (프록시 객체를 곁들인)]]></title>
            <link>https://velog.io/@max-ph/JPA-N1-%EB%AC%B8%EC%A0%9C%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-%ED%94%84%EB%A1%9D%EC%8B%9C-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EA%B3%81%EB%93%A4%EC%9D%B8</link>
            <guid>https://velog.io/@max-ph/JPA-N1-%EB%AC%B8%EC%A0%9C%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-%ED%94%84%EB%A1%9D%EC%8B%9C-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EA%B3%81%EB%93%A4%EC%9D%B8</guid>
            <pubDate>Fri, 26 Jun 2026 10:13:51 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문제-정의-n1-문제란-무엇인가">1. 문제 정의: N+1 문제란 무엇인가?</h2>
<p>본격적인 이야기에 앞서, 알고가야 할 전제가 있다</p>
<p><strong>&quot;RDB의 성능은 쿼리 실행 횟수와 직결된다.&quot;</strong></p>
<p>일반적으로 한 개의 복잡한 쿼리가 여러 개의 단순한 쿼리를 반복해서 보내는 것보다 성능이 훨씬 뛰어나다</p>
<p>N+1 문제는 SQL, RDB의 결함이 아니라, <strong>ORM을 사용할 때 발생하는 부작용</strong>이다. 주로 1:N이나 N:N 관계에서 발생하고, N+1이라는 단어에서 오해할 수 있는 부분이 있다</p>
<blockquote>
<p>&quot;N이 자식 객체(댓글)의 개수를 뜻하나요? ❌</p>
</blockquote>
<p> <strong>N+1에서 N은 자식의 수가 아니라, 조회된 &#39;부모 객체의 개수&#39;를 의미한다.</strong> 
1:N 에서의 N은 자식 테이블의 다중 특성을 의미하고, N+1은 부모 객체의 수(N)를 뜻한다</p>
<p>글자로만 봐서는 감이 잘 안 올 건데,
<strong>JPA</strong> 사용하는 상황에서 게시글(Post)과 댓글(Comment) 조회를 예로 들어 실질적인 시나리오를 살펴보자</p>
<h2 id="2-n1-시나리오">2. N+1 시나리오</h2>
<p>우선 Post 와 Comment가 1:N 관계를 맺고,</p>
<pre><code class="language-jsx">public class Post {

...

private List&lt;Comment&gt; comments;
}</code></pre>
<p><code>Post</code>는 <code>Comment</code>를 멤버로 가지고 있다</p>
<p>이때 JPA를 활용하여 서비스 계층에서 모든 <code>Post</code>를 가져온다고 하자</p>
<pre><code class="language-jsx">// 1. 부모 전체 조회 (쿼리 1번)
List&lt;Post&gt; posts = postJpaRepository.findAll(); 

for (Post post : posts) {
    // 2. 가방 껍데기만 가져오므로 쿼리 안 나감
    List&lt;Comment&gt; comments = post.getComments(); 

    // 3. 댓글과 상관없는 데이터만 사용 (N+1 발생 안 함)
    System.out.println(post.getTitle()); 
}</code></pre>
<p>그럼 지금 상황에서 <code>post</code>에 있는 <code>comments</code>는 어떤 데이터가 들어가 있을까?</p>
<p>바로 <strong>‘프록시 객체’</strong>이다. 여기서 프록시 객체에 대해 조금은 알고 가자.</p>
<h2 id="2-1-프록시-객체">2-1. 프록시 객체</h2>
<p>실제 엔티티 객체 대신 데이터베이스 조회를 지연시키기 위해 넣어두는 가짜 객체를 뜻한다</p>
<p><strong>@ManyToOne</strong></p>
<ul>
<li><strong>상속 구조</strong>: <code>Post$HibernateProxy$abc123...</code> (Post의 자식 클래스)</li>
<li><strong>필드 구성</strong>:<ul>
<li><code>Id</code>: 이미 알고 있는 식별자 값.</li>
<li><code>Target</code>: 실제 데이터가 채워질 진짜 객체의 참조 변수 (처음엔 <code>null</code>).</li>
</ul>
</li>
<li><strong>OneToMany</strong>: <code>List</code> 자체를 <strong>컬렉션 래퍼</strong>로 저장</li>
</ul>
<pre><code>### i) 진짜 객체 넣으면 안되나요? 
매번 `comment`에 실제 객체를 넣게 되면, post만 필요한 상황에서도  `comment` 조회하기 때문에 불필요한 리소스를 사용하게 된다 

### ii) null로 놔두면 안되나요?    
그렇게 된다면, `comment`를 필요로 하는 메서드에 항상 `null` 체크를 해야 한다</code></pre><pre><code class="language-jsx">if (post.getComments != null) {

    post.getComments;

    ...
}</code></pre>
<p>JPA는 자식 객체를 프록시로 넣어둠으로써, <code>comment</code>가 필요할 때만 DB 조회를 통해 성능상 이점을 취한다</p>
<p>다시 돌아가서,</p>
<pre><code class="language-jsx">// 1. 부모 전체 조회 (쿼리 1번)
List&lt;Post&gt; posts = postJpaRepository.findAll(); 
List&lt;PostResponse&gt; result = new ArrayList&lt;&gt;();

for (Post post : posts) {
    List&lt;CommentResponse&gt; commentDtos = post.getComments().stream()   
    // 2. stream() 코드 실행 중 실제 DB 접근 필요성 생김
            .map(CommentResponse::new)
            .toList();

  ...
}
</code></pre>
<p>프록시 객체가 들어있는 상황에서 실제 <code>comment</code>정보를 처리하는 코드가 실행되면 어떻게 될까?
DB 조회 필요성이 생기고 실제 <code>comment</code>데이터를 가져오는 쿼리가 날아가게 된다 </p>
<p>근데 여기서 만약 100개의 <code>post</code>에 대한 <code>comments</code>를 가져와야 한다면,</p>
<pre><code class="language-jsx">JPA:
post1.getComments.stream() 실행 때 ⇒ comments가 프록시네? comments 가져오는 쿼리 실행해야지~

post2.getComments.stream() 실행 때 ⇒ comments가 프록시네? comments 가져오는 쿼리 실행해야지~

post3.getComments.stream() 실행 때 ⇒ comments가 프록시네? comments 가져오는 쿼리 실행해야지~

.

.

.

post100.getComments.stream() 실행 때 ⇒ comments가 프록시네? comments 가져오는 쿼리 실행해야지~</code></pre>
<p>총 100번의 추가 쿼리가 발생한다</p>
<p><code>100</code>개의 <code>post</code>, <code>comment</code>를 가져오는 상황에서 101번의 쿼리가 실행된 것이다</p>
<p>서론에서 말한 것처럼, DB 성능의 쿼리의 수(DB와 커넥션 수)와 직접적으로 관련있다. 1~2번 쿼리로 가져올 수 있는 데이터를 101번의 쿼리로 가져온다면, 치명적인 병목 현상이 생길 수 있으며 10,000개의 post를 읽는 상황에서는 10,001개의 쿼리를 보내는 상황이 생긴다 </p>
<p>따라서 N+1 문제는 리팩터링의 요소가 아닌, 당장 고쳐야 할 버그의 일종이라고 할 수 있다</p>
<h2 id="3-해결법">3. 해결법</h2>
<h3 id="3-1-fetch-join">3-1. Fetch Join</h3>
<p><code>comment</code>가 필요한 로직에 사용할 Repository 메서드를 추가로 작성한다</p>
<pre><code class="language-jsx">public interface PostJpaRepository extends JpaRepository&lt;Post, String&gt; {
    // 한 번의 쿼리로 Post와 연관된 Comment를 모두 JOIN해서 가져온다.
    @Query(&quot;SELECT p FROM Post p JOIN FETCH p.comments&quot;)
    List&lt;Post&gt; findAllWithComments();
    // Comments 정보 필요없을 때
    List&lt;Post&gt; findAll();
}</code></pre>
<h3 id="3-2-batch-size">3-2 Batch Size</h3>
<p>1:N 관계에서 Fetch Join을 쓰면 Pageable을 사용한 DB 레벨의 페이징 처리가 불가능하다는 한계가 있다
이때 유용하게 쓰는 방법이 <code>Batch Size</code>다</p>
<pre><code>// application.yml 설정
spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100</code></pre><p>이 설정을 적용하면 루프를 돌며 자식 엔티티를 조회할 때 쿼리를 매번 한 장씩 날리지 않는다 
대신 설정한 사이즈만큼 자식 엔티티들의 부모 ID를 모아서 IN 절 쿼리 한 방으로 묶어서 가져온다</p>
<p>결과적으로 101번 나갈 쿼리가 딱 2번(Post 전체 조회 1번 + IN 절을 사용한 Comment 묶음 조회 1번)으로 줄어들며, 페이징 기능도 안전하게 사용할 수 있게 된다</p>
<h3 id="3-3-onetomany-삭제">3-3 OneToMany 삭제</h3>
<p>N+1 문제가 일어나는 이유인 <code>@OneToMany</code>를 사용하지 않는 방법이다
단일 인스턴스가 여러 자식 엔티티들을 알고 있는 상태 자체가 일반적인 구조는 아니다
DB 스키마도 1:N이라면 comment가 post.id를 갖고 있는 형태가 일반적인 구조이다</p>
<p><code>@OneToMany</code>은,</p>
<ul>
<li><code>post.getComments()</code>와 같은 객체지향적 설계 혹은</li>
<li>고아 객체의 관리 측면에서 쓰기 좋은 상황이 존재하지만</li>
</ul>
<p>ORM 장단점을 고려하지 않고 무분별하게 사용하는 것은 서버의 부작용을 초래할 수 있다</p>
<h2 id="--ps">- PS</h2>
<p>저는 사실 <code>@OneToMany</code>뿐 아니라, <code>@ManyToOne</code>도 쓰지 않는 느슨한 연관관계를 주로 사용합니다 </p>
<pre><code class="language-jsx">public class Comment {

  private String id;
  private String postId;

  ...</code></pre>
<p><code>Order</code> <code>OrderItem</code>처럼 생명주기가 같은 관계는 JPA annotation을 활용해 강제시키는 것이 <code>Orphan</code>객체나 영속성 관리 측면에서 편하기에 <code>@ManyToOne</code>을 사용하며 그 외의 관계는 default로 느슨하게 가져가는 편이다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] Client와 Validation 통일하기]]></title>
            <link>https://velog.io/@max-ph/Spring-Boot-Client%EC%99%80-Validation-%ED%86%B5%EC%9D%BC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@max-ph/Spring-Boot-Client%EC%99%80-Validation-%ED%86%B5%EC%9D%BC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 18 Jun 2026 08:54:22 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>api를 설계할 때는 많은 제약 조건이 필요하다</p>
<blockquote>
<p>A 타입은 INT이고 2,000보다 커야한다
B는 다른 데이터들과 중복되면 안되지만, Nullable 해야한다
C는 빌드된 enum 값에 한해서만 요청해야하며, 
D는 <code>nation</code> 테이블에 정의된 name에 대해서만 요청을 해야한다</p>
</blockquote>
<p><strong>Spring Boot</strong>로 개발을 하게 되면 <code>JPA</code> 라이브러리와 <code>@Valid</code> 어노테이션 등을 통해 데이터의 제약 조건을 설정했던 경험이 있을 것이다</p>
<p>많은 조건이 있는 상황에서 클라이언트와 해당 정보를 누가/어떻게/어디까지 공유해야 할지 공부하고 적용했던 경험을 공유하려 한다</p>
<h2 id="배경-지식">배경 지식</h2>
<h3 id="1-제약constraint-vs-규칙rule">1. 제약(Constraint) vs 규칙(Rule)</h3>
<p>*<em>타입 제약 (Type Constraint) &amp; 유일성 제약 (Uniqueness Constraint) &amp; 도메인 제약(Domain Constraint) *</em></p>
<p>제약(Constraint)은 모두 DB 테이블 생성 시, 스키마에 정의하여 만들 수 있습니다.</p>
<pre><code>CREATE TABLE users (
    -- 1. 타입 제약 (BIGINT, VARCHAR, INT 등)
    id BIGINT AUTO_INCREMENT,
    email VARCHAR(255) NOT NULL,
    birth_year INT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    -- PRIMARY KEY 제약 (기본키)
    CONSTRAINT pk_user_id PRIMARY KEY (id),

    -- 2. 유일성 제약 조건 (UNIQUE)
    -- 중복된 이메일이 들어오는 것을 DB 레벨에서 절대적으로 막아줍니다.
    CONSTRAINT uk_user_email UNIQUE (email),

    -- 3. 도메인 범위 제약 조건 (CHECK)
    -- 자바의 @Min(2000) 처럼, 2000 이상 정수만 저장되도록 DB가 검증합니다.
    CONSTRAINT ck_user_birth_year CHECK (birth_year &gt;= 2000)
);</code></pre><p>DB 수준에서 제약을 걸지만, Spring Boot에서는 DB 커넥션을 최소화하기 위해 WAS 수준에서 제약 조건을 검증하고 예외처리한다</p>
<p>*<em>비즈니스 규칙 (Business Rule)
*</em>
도메인 규칙 (Domain Rule)이라고도 한다
제약은 특정 필드 상태에 대해 강제하는 정적인 특징을 가진다
이에 반해 비즈니스 규칙은 객체의 상태에 따라 처리 규칙이 바뀌는 동적인 규칙이다</p>
<pre><code class="language-Java">// Order 엔티티 
public void changeAddress(String newAddress) {
    // 비즈니스 규칙 검증
    if (this.status == OrderStatus.DELIVERING) {
        throw new IllegalStateException(&quot;배송 중, 주소를 변경할 수 없습니다.&quot;);
    }

    this.address = newAddress;
}</code></pre>
<p>여러 제약과 규칙들 모두 데이터 무결성을 위해 정확히 설계해야 하며, 해당 정보가 클라이언트와 공유가 돼야 UI/UX 수준에서도 의도된 설계가 가능하다</p>
<h3 id="2-동적-데이터-vs-정적-데이터">2. 동적 데이터 vs 정적 데이터</h3>
<p>서버의 여러 변수들 중에는 고정되어있는 값과 수시로 변경되는 값이 존재한다
예를 들어, mbti는 E/I, N/S, F/T, P/J 처럼 고정돼 있는 16가지 enum 형태로 관리 할 수 있다
반면 국가 정보는 내 서비스가 관리하는 국가 상태에 따라 삭제되기도 하고 값이 수정되기도 하는 동적인 데이터에 가깝다
<code>동적=런타임 시점, 정적=컴파일 시점</code>에 결정된다고 이해하면 편하다</p>
<h3 id="3-json-schema">3. JSON Schema</h3>
<p><a href="https://json-schema.org/overview/what-is-jsonschema">https://json-schema.org/overview/what-is-jsonschema</a></p>
<p>JSON 데이터의 구조, 타입, 제약 조건을 정의하고 유효성을 검증하기 위한 표준 형식이다</p>
<pre><code>{
  &quot;$schema&quot;: &quot;https://json-schema.org/draft/2020-12/schema&quot;,
  &quot;$id&quot;: &quot;https://example.com/user-registration.schema.json&quot;,
  &quot;title&quot;: &quot;UserRegistration&quot;,
  &quot;type&quot;: &quot;object&quot;,

  // 1. 필수 값 설정 및 허용되지 않은 뜬금없는 필드 원천 차단
  &quot;required&quot;: [&quot;username&quot;, &quot;age&quot;, &quot;password&quot;, &quot;couponCount&quot;, &quot;depositUnit&quot;, &quot;roles&quot;, &quot;interests&quot;],
  &quot;additionalProperties&quot;: false,

  &quot;properties&quot;: {
    // 2. 문자열(String) 패턴 및 길이 검증
    &quot;username&quot;: {
      &quot;type&quot;: &quot;string&quot;,
      &quot;minLength&quot;: 4,
      &quot;maxLength&quot;: 16,
      &quot;pattern&quot;: &quot;^[a-z0-9-_]+$&quot;,
      &quot;description&quot;: &quot;유저 ID: 4~16자의 영문 소문자, 숫자, 특수문자(-, _)만 허용&quot;
    },
    &quot;password&quot;: {
      &quot;type&quot;: &quot;string&quot;,
      &quot;format&quot;: &quot;password&quot;,
      &quot;minLength&quot;: 8,
      &quot;description&quot;: &quot;비밀번호: 최소 8자리 이상 필수&quot;
    },
    // 3. 숫자(Number/Integer) 범위 및 배수 검증
    &quot;age&quot;: {
      &quot;type&quot;: &quot;integer&quot;,
      &quot;minimum&quot;: 19,
      &quot;maximum&quot;: 120,
      &quot;description&quot;: &quot;나이: 성인 인증을 위해 19세 이상, 120세 이하만 허용&quot;
    },
    &quot;couponCount&quot;: {
      &quot;type&quot;: &quot;integer&quot;,
      &quot;minimum&quot;: 0,
      &quot;maximum&quot;: 5,
      &quot;description&quot;: &quot;보유 쿠폰 수: 0개에서 최대 5개까지만 보유 가능&quot;
    },
    &quot;depositUnit&quot;: {
      &quot;type&quot;: &quot;integer&quot;,
      &quot;minimum&quot;: 1000,
      &quot;multipleOf&quot;: 1000,
      &quot;description&quot;: &quot;충전 단위: 최소 1,000원 이상이며, 무조건 1,000원 단위(배수)로만 가능&quot;
    },
    // 4. 열거형(Enum) 및 배열(Array) 데이터 정적 검증
    &quot;roles&quot;: {
      &quot;type&quot;: &quot;array&quot;,
      &quot;minItems&quot;: 1,
      &quot;uniqueItems&quot;: true,
      &quot;items&quot;: {
        &quot;type&quot;: &quot;string&quot;,
        &quot;enum&quot;: [&quot;USER&quot;, &quot;VIP&quot;, &quot;ADMIN&quot;]
      },
      &quot;description&quot;: &quot;권한: 최소 1개 이상의 권한이 필요하며, 지정된 내부 등급(enum) 외에는 입력 불가 및 중복 금지&quot;
    },
    &quot;interests&quot;: {
      &quot;type&quot;: &quot;array&quot;,
      &quot;minItems&quot;: 1,
      &quot;maxItems&quot;: 3,
      &quot;uniqueItems&quot;: true,
      &quot;items&quot;: {
        &quot;type&quot;: &quot;string&quot;
      },
      &quot;description&quot;: &quot;관심사 태그: 최소 1개, 최대 3개까지만 등록 가능하며 태그 간 중복 불가능&quot;
    }
  }
}</code></pre><h2 id="swagger-api--validation-전달">Swagger API &amp; Validation 전달</h2>
<blockquote>
<p><strong>단일 진실 공급원(Single Souce of Truth)</strong>
자료의 중복, 비적합성을 해결하기 위해 자료의 스키마, 정보 등을 한 곳에서만 생성/수정 하는 방법</p>
</blockquote>
<p>기본적인 웹/앱, 클라이언트-서버 분리 환경에서는 서버 측이 스키마 정보의 단일 공급원이 되며, Swagger와 직접 작성한 <code>메타 데이터</code> API를 통해 전달한다</p>
<p>필드들의 타입, 유일성, 도메인 제약 모두 <strong>Swagger</strong>를 통해 공유할 수 있다
<img src="https://velog.velcdn.com/images/max-ph/post/b9862c87-c17a-4edb-97ec-588cdd2f1104/image.png" alt=""></p>
<p>Swagger 문서도 Json Schema를 따르기 때문에 모든 Validation을 관리할 수 있을 거 같지만, 동적으로 변하는 데이터를 관리할 수 없다</p>
<p>Swagger 문서 정보는 Spring Boot에서 사용된 <code>io.swagger</code>, <code>jakarta</code> annotation을 통해 문서화를 만들기 때문에 소스코드 레벨 시점에 문서가 결정된다 
또한, DB에 저장하는 동적 데이터 특성 상 Swagger에서 어떤 입력값을 허용하는지 알려주지 못한다</p>
<p>따라서 동적 요청 데이터나 카테고리 등은 직접 작성한 메타 데이터 API를 통해 응답해줬다
<code>GET ~/meta/nations</code>을 통해 동적 나라 데이터를, 
<img src="https://velog.velcdn.com/images/max-ph/post/0feb8446-947f-432b-baeb-c5870b491487/image.png" alt="">
<code>GET ~/meta/exam-types</code>로 각기 다른 어학성적 스키마를 동적으로 응답했다
<img src="https://velog.velcdn.com/images/max-ph/post/54da32bd-a178-404a-84a2-74020fdb5df8/image.png" alt="">
Json Schema는 아니지만,
학교의 실제 이름과 영어 카테고리를 매핑하는 enum 정보도 
<code>GET ~/meta/majors</code>로 전달했다
<img src="https://velog.velcdn.com/images/max-ph/post/5cab0af6-69ef-4bb8-a271-586d59c75633/image.png" alt=""></p>
<h3 id="-ps">-PS</h3>
<p><code>openapi-generator</code>나 <code>MSW(Mock Server Worker)</code> 처럼 이름만 들어본 개념들이 있지만, 클라이언트 정확히 어떤 방식으로 해당 문서를 활용하는지는 모르겠다
따라서 위 방식이 클라이언트 친화적(?)인지는 추후 공부하면 좋겠다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] 모니터링 왜 필요한가?(Prometheus+Grafana)]]></title>
            <link>https://velog.io/@max-ph/Spring-Boot-docker%EB%A1%9C-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81%ED%95%98%EA%B8%B0-PrometheusGrafana</link>
            <guid>https://velog.io/@max-ph/Spring-Boot-docker%EB%A1%9C-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81%ED%95%98%EA%B8%B0-PrometheusGrafana</guid>
            <pubDate>Sun, 12 Apr 2026 09:58:44 GMT</pubDate>
            <description><![CDATA[<h2 id="1-모니터링-왜-필요한가">1. 모니터링, 왜 필요한가?</h2>
<blockquote>
<p>프로젝트를 완성했다.</p>
</blockquote>
<p>테스트 코드도 작성했고, CI CD 파이프라인도 잘 작동했고, 
postman으로 배포 서버 API 수기 테스트도 성공했다.</p>
<p>배포를 무사히 마치고 며칠 뒤....
<img src="https://velog.velcdn.com/images/max-ph/post/a938b2a9-461c-4ea2-8fa4-80050bf958db/image.png" alt=""></p>
<p><em>서버는 응답이 없었고, 정작 나는 언제부터 꺼졌는지 알지 못했다.</em></p>
<p>고가용성이 높은 서비스를 만들기 위해서는 <strong>Auto Scale-out</strong>, <strong>테스트 코드</strong> 그리고 <strong>Massege Queue</strong> 같은 <strong>&quot;예방 전략&quot;</strong>도 중요하지만, 모니터링과 알림을 통한 <strong>&quot;관측 전략&quot;</strong>도 필수적이다.</p>
<p>아무리 안정적인 서버를 구축하더라도,
문제는 반드시 발생한다.</p>
<p>중요한 건 “문제가 발생했는가”가 아니라
<strong>“언제, 얼마나 빨리 인지했는가”</strong>다.</p>
<p>모니터링과 Alert이 잘 구성된 시스템은
문제가 발생하자마자 신호를 받고,
영향이 커지기 전에 대응할 수 있다.</p>
<h2 id="2-메트릭이란">2. 메트릭이란?</h2>
<p><strong>Observability(관측 가능성) : 서버 안에서 무슨 일이 일어나는지 파악할 수 있는 능력</strong></p>
<p>크게는 3가지의 관측 지표가 있다.</p>
<pre><code>Metrics  → 숫자로 상태를 본다
Logging  → 사건의 맥락을 추적한다
Tracing  → 요청의 흐름을 따라간다</code></pre><p>Logging과 Tracing도 정말 중요한 지표이지만, Metric을 중심으로 설명하려 한다.</p>
<p><strong>Metric</strong>을 한마디로 <strong>&quot;시스템 상태를 숫자로 표현한 측정값&quot;</strong>이다.
아래와 같은 데이터가 흔히 모니터링 전략에서 수집하는 Metric 데이터이다.</p>
<pre><code>CPU 사용률 (70%)
메모리 사용량 (1GB)
초당 요청 수 (RPS: 120)
에러율 (1.2%)
응답 시간 (200ms)</code></pre><p>숫자로 표현된 값이기에 쉽게 수치화가 가능하며, 수치화된 시계열 데이터를 토대로 Prometheus와 Grafana 전략을 통해 시각화가 가능해진다.</p>
<pre><code>Micrometer (측정 -&gt; Metric)
    ↓  
Prometheus (시계열 저장)
    ↓
Grafana (시각화)</code></pre><h2 id="3-어떤-메트릭을-수집할-것인가">3. 어떤 메트릭을 수집할 것인가?</h2>
<p>흔히 RED method와 USE method 전략이 있다. 
RED method는 API 측면을 모니터링하고,</p>
<pre><code>지표             의미
Rate        초당 요청 수
Errors      에러 비율
Duration    응답 시간</code></pre><p>USE method는 시스템적인 측면을 모니터링한다. CPU, Memory, Network 사용률 등이 있다</p>
<pre><code>지표             의미
Utilization  이용률 또는 사용률
Saturation   포화도
Errors       에러</code></pre><h2 id="4-micrometer--prometheus--grafana-전략">4. Micrometer + Prometheus + Grafana 전략</h2>
<p>스프링에는 Micrometer 외부 라이브러리를 활용하여 쉽게 Metric을 수집할 수 있다. </p>
<pre><code>메트릭                                설명
http_server_requests_seconds     API 응답 시간, 에러율
hikaricp_connections_active         DB 커넥션 풀 사용 중
hikaricp_connections_pending    커넥션 대기 중 요청
jvm_memory_used_bytes             JVM 힙 사용량
process_cpu_usage                 CPU 사용률
jvm_gc_pause_seconds             GC pause time</code></pre><p>하지만 Micrometer 혼자서는 해당 Metric을 저장할 수 없다. 
이를 시계열 형태로 저장해주는 역할이 <strong>Prometheus</strong>이다.</p>
<p>Prometheus는 주기적으로 Micrometer로부터 Metric을 Pull받아오는 형식이다. 
그리고 이 데이터를 통해 시간 그래프를 만드는 것이 <strong>Grafana</strong>의 역할이다.</p>
<pre><code>[앱]                                                       이상 수치 발견
  ├─ Micrometer ──→ Prometheus ──→ Grafana  (숫자 메트릭) -&gt; discord 알림
  └─ Logback ──────→ Loki ────────→ Grafana  (텍스트 로그)
                                       ↑
                              하나의 Grafana에서
                              메트릭 + 로그 동시에 확인 

[외부]                                        (서버 장애 시)
  Uptime Kuma ──→ /actuator/health 30초마다 ──→ discord 알림</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[SW마에스트로 17기 합격 후기]]></title>
            <link>https://velog.io/@max-ph/SW%EB%A7%88%EC%97%90%EC%8A%A4%ED%8A%B8%EB%A1%9C-17%EA%B8%B0-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@max-ph/SW%EB%A7%88%EC%97%90%EC%8A%A4%ED%8A%B8%EB%A1%9C-17%EA%B8%B0-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Tue, 07 Apr 2026 07:43:14 GMT</pubDate>
            <description><![CDATA[<p>작년 SW마에스트로 16기를 지원하지 않았던 걸 후회했었다. 어차피 떨어질거라는 생각이 컸고, 더 준비되면 지원하자고 생각했었다.
그러나 한 해가 지나며 느꼈던 점은 얼마나 준비했든 지원할 때면 늘 불안했고, 합격했을 때는 &quot;이게 붙네&quot;의 연속이었다.
1년이 지나고 소마 17기를 지원했을 때도 여전히 부족하다고 느꼈고, 최종합격 때에도 실감이 나지 않았다.
떨어지더라도 준비하는 과정 속에서 큰 성장이 있기 때문에
하고 싶은 일이 생긴다면 주저하지 않고 도전하면 좋겠습니다.</p>
<h2 id="선발과정">선발과정</h2>
<p><img src="https://velog.velcdn.com/images/max-ph/post/f99cfcec-d566-47b4-b424-691e29e01866/image.png" alt=""></p>
<p>길게 보면 두 달간의 지원절차를 거치게 된다.</p>
<h2 id="서류지원">서류지원</h2>
<ul>
<li><p>AI 및 SW분야의 전문성을 키우기 위해 몰입했던 경험과 도전이 무엇인지, 또한 이러한 성장과정을 통해 얻은 배움은 무엇인지를 서술하여 주시기 바랍니다. (최대 1000자)</p>
</li>
<li><p>AI·SW마에스트로 과정 참여를 통해 어떠한 프로젝트를 수행하고 싶은가요? 해당 프로젝트를 수행하기 위한 계획과 이루고자 하는 목표가 무엇인지 구체적으로 서술하여 주시기 바랍니다. (최대 1000자)</p>
</li>
</ul>
<p>서류전형은 100% 합격이라는 말을 들었지만, 면접 때 질문이 들어 올 수 있으니 어느정도는 구체적으로 작성했다.</p>
<h2 id="1차-코딩테스트-228">1차 코딩테스트 (2/28)</h2>
<p>가장 준비를 많이 했던 것 같다. 코테 준비를 오랫동안 쉬기도 했고, 2차 코딩테스트와도 직접적으로 연관돼 있기에 서류 제출부터 바로 백준이랑 프로그래머스를 풀기 시작했다.</p>
<p>코테 준비 기간동안 97문제 정도 풀었고, 대부분은 처음 준비해본 SQL 문제를 70문제 정도 푼 것 같다.  </p>
<p>SQL 문제는 프로그래머스에서 LV1, LV2를 다 풀었고, 시험 직전에는 SQL 고득점 Kit에서 부족한 유형 위주로 풀었다.</p>
<p>알고리즘은 &#39;sw마에스트로 홈페이지&#39;에 명시된 알고리즘을 더 집중적으로 준비했다.</p>
<p>구현, 그래프이론, DP 등의 경우는 백준 실버1<del>골드4 위주로 연습했고,
탐색, 자료구조은 실버3</del>실버1 정도로 시험 때 실수만 하지 않도록 준비했다.</p>
<p><img src="https://velog.velcdn.com/images/max-ph/post/21e5151a-b46c-42cc-a20c-de990a84f4ba/image.png" alt=""></p>
<p>1차 코테 난이도는 전반적으로 평이했고 3문제를 풀었다.
SQL 한 문제 제외하고 나머지 문제는 모두 구현 문제라고 느꼈다.</p>
<p>내 기준에서는 모든 문제가 고비였지만 SQL과 첫 번째 문제가 코테 열심히 준비한 분에게는 쉬웠을 거라 생각했고, 3솔이 통과할지 걱정했었다.</p>
<h2 id="2차-코딩테스트-37">2차 코딩테스트 (3/7)</h2>
<p>2차 코테 준비는 하지 못했었다. 개강, 동아리 프로젝트 그리고 같은 날 치르는 SQLD 업보가 쌓여서, 전날에 SQL 고득점 Kit 문제 4개 정도와 구현, BFS 문제 정도만 풀었던 거 같다. </p>
<p>1차 코딩테스트 대비 확실히 어려웠다고 느꼈고, 결과적으로 2문제를 풀었다. </p>
<p>SQL 문제의 경우, DENSE_RANK() 활용 문제가 나왔다.
프로그래머스에서는 아예 사용하지 않은 함수였는데, SQLD 준비하면서 살짝 공부한 개념덕분에 온몸 비틀기로 해결할 수 있었다.</p>
<p>그 다음으로 1번 구현 문제를 풀었고, 40분 정도 남았던거 같다.
전날 그래프 문제를 풀었지만, 정작 시험에서는 지문이 그나마 이해하기 쉬었던 DP문제를 시도했다. 
끝나기 직전 예외 케이스 하나를 제외하고 모두 통과했고, 조금이라도 어필하기 위해 예외 케이스 처리를 위한 수정 리스트를 주석으로 적었다. </p>
<h2 id="심층면접-319322">심층면접 (3/19~3/22)</h2>
<p>다행히 2차 코딩테스트 합격 메일을 받았고, 그때부터 준비하느라 8일정도 면접 준비를 했다. </p>
<p>면접은 면접관 6명과 면접자 5명으로 구성되었고 5명 기준 1시간 15분 정도 진행했다.
처음, 면접자들의 3분 포트폴리오 발표로 시작한다.</p>
<p>포트폴리오 발표는 자기소개의 연장선 느낌이었고, 면접자들 모두 각자 준비한 방식(프로젝트 구조 설명, 자신의 기술스택 설명 등)으로 강점을 어필했다. </p>
<p>나는 포트폴리오 발표에서 AI 활용 능력와 협업 능력을 어필했다.
그러나 다른 면접자들에 비해 기술 어필이 부족했고 발표 내용이 전반적으로 추상적이라 포폴 발표 끝나고 바로 망했다고 생각했다. </p>
<p>컬처핏 질문은 사실상 없었던 거 같다.</p>
<p>내가 컬처핏을 준비했던 방법은 LLM을 활용하여 sw마에스트로의 핵심가치를 분석하고, 이를 기반으로 이전 기수 면접 질문들과 예상 질문 그리고 질문에 맞는 답변 방향 리스트를 만들었고, 틈틈히 말하는 연습을 했다. </p>
<p><strong>&#39;나는 끝까지 할 사람이다&#39;를 증명</strong> — 열정 없이 이탈할 것 같은 사람을 가장 먼저 걸러냄
<strong>구체적인 경험</strong> — &quot;열심히 했다&quot;가 아니라 &quot;그래서 어떤 결과가 나왔다&quot;
<strong>창업/임팩트 방향성</strong> — 취업이 목표인 사람보다 뭔가를 만들고 싶은 사람을 찾음</p>
<p>그러나 면접에서는 모두 포폴, 순발력 기반의 질문이 나왔다.</p>
<h3 id="면접-질문">면접 질문</h3>
<ul>
<li>프로젝트 시연 전, 클라우드 계정이 해킹 당해서 서버가 다운 됐을 때 어떻게 대처하실건가요?</li>
<li>소마에서 엑셀(면접자마다 다른 예시)을 만들어야 합니다. 팀을 어떻게 구성하고, 당신은 어떤 역할을 맡을건가요?</li>
<li>그 외에는 전부 포트폴리오에 명시한 프로젝트 위주로 질문했다.<ul>
<li>팀 프로젝트에서 면접자가 기여한 도메인</li>
<li>사용한 기술 스택과 해당 스택 설명</li>
<li>다른 대체 기술과의 장단점 비교 및 해당 스택 선택 이유</li>
</ul>
</li>
</ul>
<p>면접자가 많다 보니 개인에게 가는 질문은 5~7개 정도인거 같다.
마지막 자신의 강점을 말하라는 질문에서도 협업과 소통의 강점을 어필했다.</p>
<p>면접 후 기술적인 질문들 전반적으로 난잡하게 설명했다고 생각했고, 떨어지지 않을까 싶었다.</p>
<p><img src="https://velog.velcdn.com/images/max-ph/post/e4d495d5-d4c2-433f-9931-b54148dd3556/image.jpg" alt=""></p>
<p>합격 이유를 생각해 보자면,
컬처핏 질문이 없었던 만큼 포폴 발표부터 마지막 강점 어필까지 일관되게 협업 능력을 어필한 부분이 면접관들분에게 좋게 다가간거 같고, 
잡핏 질문에서는 위축되지 않고 아는 부분 다 말하는 점에서 최대한 선방하지 않았나 싶다.</p>
<p>소마가 아니더라도 모든 면접 준비는 <strong>운동 많이 되는 것 같다</strong>.
지금까지 해온 것을 정리하고 내가 아는 것과 모르는 것을 확실히 분류하며 헷갈리는 부분을 제대로 공부하고 준비하는 과정이 도움이 많이 됐다.</p>
]]></description>
        </item>
    </channel>
</rss>