<?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>Fri, 26 Jun 2026 10:13:51 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[[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>