<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>smini.log</title>
        <link>https://velog.io/</link>
        <description>BE 개발자</description>
        <lastBuildDate>Sat, 11 Apr 2026 08:49:05 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>smini.log</title>
            <url>https://velog.velcdn.com/images/s_miny/profile/3b37a2c1-0643-465a-a6aa-7fbccc42e563/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. smini.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/s_miny" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[당연한 원칙, 검증하기 (상속보다 조합을 선호하라)]]></title>
            <link>https://velog.io/@s_miny/%EB%8B%B9%EC%97%B0%ED%95%9C-%EC%9B%90%EC%B9%99-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0-%EC%83%81%EC%86%8D%EB%B3%B4%EB%8B%A4-%EC%A1%B0%ED%95%A9%EC%9D%84-%EC%84%A0%ED%98%B8%ED%95%98%EB%9D%BC</link>
            <guid>https://velog.io/@s_miny/%EB%8B%B9%EC%97%B0%ED%95%9C-%EC%9B%90%EC%B9%99-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0-%EC%83%81%EC%86%8D%EB%B3%B4%EB%8B%A4-%EC%A1%B0%ED%95%A9%EC%9D%84-%EC%84%A0%ED%98%B8%ED%95%98%EB%9D%BC</guid>
            <pubDate>Sat, 11 Apr 2026 08:49:05 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>아래 실험은 우아한테크코스 레벨1 장기미션을 기준으로 실험하였다.</p>
<h2 id="1-원칙-소개">1. 원칙 소개</h2>
<p>내가 선택한 당연한 원칙은 “상속보다는 조합을 선호하라”이다.</p>
<p>이 원칙은 객체지향 설계에서 기능을 재사용하고 확장할 때, 클래스를 수직적으로 확장하는 상속보다 객체들을 수평적으로 연결하는 조합을 선호하라는 원칙이다.</p>
<p>여기서 상속과 조합이란?</p>
<ul>
<li><p>상속 (is-a 관계): 하위 클래스가 상위 클래스의 특성을 물려받아 수직적인 계층 구조를 가지고 있다. 코드를 재사용하는 가장 빠르고 쉬운 방법이지만, 부모 클래스의 내부 구현이 자식에게 노출되는 &#39;화이트박스 재사용&#39; 방식이다.</p>
</li>
<li><p>조합 (has-a 관계): 새로운 클래스가 기존 클래스의 인스턴스를 자신의 필드로 가지고 있는 구조이다. 객체의 내부 구현을 감춘 채 인터페이스를 통해서만 협력하는 &#39;블랙박스 재사용&#39; 방식이다.</p>
</li>
</ul>
<p>이 원칙을 선택한 이유는 평소 상속 대신 조합을 사용하면 &#39;수정에는 닫혀 있고 확장에는 열려 있다&#39;고 하는데, 이것이 실제 코드 레벨에서 어떻게 동작하는지 경험해 보고 싶었다. 또한, 상속이 객체의 캡슐화를 약화시킨다는 문제점을 조합으로 어떻게 해결할 수 있는지, 외부로 노출되는 메시지가 얼마나 줄어들고 객체가 안전해지는지 직접 확인해 보고자 이 원칙을 선택했다.</p>
<h2 id="2-가설과-선택-이유">2. 가설과 선택 이유</h2>
<p>이번 실험에서는 상하좌우로 한 칸씩 움직이는 이미 완성된 장기 궁 기물에 궁성 내에서만 이동 가능이라는 새로운 제약과 기능을 추가하는 상황을 가정한다. 이 요구사항을 상속과 조합으로 각각 해결해 보며 아래 가설을 검증하고자 한다.</p>
<ol>
<li>상속 대신 조합을 사용하면 기존 코드의 수정 없이도 안전하게 기능을 확장할 수 있을 것이다. (확장성)</li>
<li>상속 대신 조합을 사용하면, 부모 클래스의 불필요한 메서드 노출을 막아 객체의 인터페이스를 깔끔하게 유지할 수 있을 것이다. (캡슐화)</li>
</ol>
<hr>
<h1 id="방법">방법</h1>
<h2 id="1-비교-대상">1. 비교 대상</h2>
<p>실험의 대조군을 명확히 하기 위해 각 방식을 극단적으로 분리하여 구현했다.</p>
<ul>
<li><p>A안(상속) : 별도의 인터페이스나 전략 객체 없이, 부모 클래스(Piece)를 상속받아 자식 클래스 내부 로직을 직접 수정하며 기능을 확장.</p>
</li>
<li><p>B안(조합) : 상속을 배제하고, 독립된 MoveStrategy를 조립하여 확장. (이 과정에서 사용된 데코레이터 패턴은 중복 코드를 제거하기 위한 수단이므로, 본질적인 상속과 조합의 비교에서는 무시하고 봐도 무방하다.)</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/s_miny/post/57df72c0-e54c-4f9b-af3f-4daf602b6c75/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/s_miny/post/c8df3018-15f2-4797-bd81-175626f41847/image.png" alt=""></p>
<p><strong>[설계 구조 시각화: 상속 vs 조합]</strong></p>
<p>본 실험의 이해를 돕기 위해 핵심 클래스들 간의 관계를 다이어그램으로 시각화하였다. 전체 프로젝트 구조가 아닌, 실험의 핵심인 기물(Piece)과 이동 전략(MoveStrategy) 간의 관계를 중심으로 표현했음을 참고하길 바란다.</p>
<h2 id="2-확인-방법">2. 확인 방법</h2>
<ol>
<li>기존 코드 수정 발생 건수 (확장성) : 향후 추가 요구사항이 발생하거나 기존 로직이 변경될 때, 뜯어고쳐야 하는 기존 파일의 개수를 측정한다.</li>
<li>불필요한 메서드 노출 개수 (캡슐화) : 외부 및 내부 관점에서 본래 책임과 무관하게 노출된 메서드 개수를 측정한다.</li>
</ol>
<h2 id="3-측정-방법">3. 측정 방법</h2>
<ol>
<li><p>확장성 측정 방법 :
새로운 기능을 추가 구현할 때, 이미 완성되어 있던 기존 클래스를 열어서 내부 코드를 수정해야 하는 횟수(파일 및 메서드 개수)를 카운트한다. </p>
<p>단, 다음의 경우는 확장으로 간주하여 카운트에서 제외한다.  </p>
<p>a. 새로운 클래스 파일 생성: 기능을 담당하는 본체를 만드는 행위.
b. 의존성 주입 코드 변경: main 등에서 새로운 부품을 끼워넣기 위해 객체를 생성하고 생성자에 전달하는 코드 수정.</p>
</li>
<li><p>캡슐화 측정 방법 :
1) 외부 노출도: 클라이언트 코드에서 객체 생성 후 자동 완성(.)을 통해 노출되는 public 메서드 중, 해당 객체의 본래 책임과 무관한 메서드 개수를 카운트한다.</p>
<p>2) 내부 오염도: 하위 클래스 내부에서 this.을 입력했을 때 나타나는 부모 클래스의 protected 메서드 목록을 확인한다. 기물 고유의 역할과 상관없는 부모의 상세 구현 로직이 얼마나 노출되는지 카운트한다.</p>
</li>
</ol>
<hr>
<h1 id="결과">결과</h1>
<h3 id="1-확장성">1. 확장성</h3>
<h4 id="--확인-결과"><strong>- 확인 결과</strong></h4>
<p>새로운 요구사항인 &#39;궁성 내 이동 제약 및 대각선 이동&#39; 기능을 궁에 추가했을 때의 측정 결과이다.</p>
<table>
<thead>
<tr>
<th><strong>구분</strong></th>
<th><strong>A안 (상속)</strong></th>
<th><strong>B안 (조합)</strong></th>
<th><strong>비고</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>기존 파일 수정 횟수</strong></td>
<td><strong>2건</strong></td>
<td><strong>0건</strong></td>
<td>B안의 PieceType 수정은 DI로 간주하여 제외</td>
</tr>
<tr>
<td><strong>기존 메서드 수정 횟수</strong></td>
<td><strong>2건</strong></td>
<td><strong>0건</strong></td>
<td>A안의 isMovable 로직 재작성 등</td>
</tr>
<tr>
<td><strong>신규 클래스 생성</strong></td>
<td>0건</td>
<td>1건</td>
<td>PalaceBoundStrategy생성</td>
</tr>
</tbody></table>
<p>[A안] 상속을 통한 구현: 기존 클래스의 변경 발생</p>
<ul>
<li><p><code>Piece.java</code> (수정됨): 모든 기물이 공유하는 부모 클래스에 궁성 관련 로직이 강제로 주입됨.</p>
<pre><code class="language-java">  // Piece.java 내부
  protected boolean isPalace(Position pos) { ... } // 신규 추가
  protected boolean isPalaceDiagonalStep(...) { ... } // 신규 추가</code></pre>
</li>
<li><p><code>Gung.java</code> (수정됨): 기존 이동 로직을 지우고 새로운 요구사항에 맞게 메서드 내부를 재작성함.</p>
<pre><code class="language-java">  @Override
  protected boolean isMovable(...) {
          // 기존 1칸 이동 로직을 지우고, 부모의 궁성 로직을 가져와서 재작성
      if (!isPalace(to)) return false;
      ...
      return isPalaceDiagonalStep(from, to);
  }</code></pre>
</li>
</ul>
<p>[B안] 조합을 통한 구현: 기존 클래스 수정 없음</p>
<ul>
<li><p><code>PalaceBoundStrategy.java</code> (신규 생성): 기존 코드를 건드리지 않고 새로운 이동 제약 로직을 독립된 클래스로 분리.</p>
<pre><code class="language-java">  public class PalaceBoundStrategy implements MoveStrategy {
      private final MoveStrategy moveStrategy; // 기존 전략을 조합

      @Override
      public boolean canMove(...) {
          if (!isInPalace(to)) return false; // 제약 조건 추가
          return moveStrategy.canMove(...); // 기존 로직 호출
      }
  }</code></pre>
</li>
<li><p><code>PieceType.java</code> (DI 변경): 새로운 부품을 끼워주는 설정만 변경함 (측정 기준에 따라 확장으로 간주하여 카운트 제외).</p>
<pre><code class="language-java">  private static MoveStrategy createGungSaStrategy() {
          ...
          // 기존의 전략을 PalaceBoundStrategy로 한 번 감싸기만 함
          return new PalaceBoundStrategy(fixedStepMoveStrategy);
      }</code></pre>
</li>
</ul>
<h4 id="--가설-판단">- 가설 판단</h4>
<blockquote>
<p>&quot;상속 대신 조합을 사용하면 기존 코드의 수정 없이도 안전하게 기능을 확장할 수 있을 것이다.&quot;</p>
</blockquote>
<p>측정 결과, 조합을 사용한 B안은 기존 클래스의 수정 횟수 0으로 새로운 기능을 추가할 때 기존 코드에 영향을 주지 않는 OCP를 충족함을 의미한다. 따라서 나의 가설은 참으로 판별한다.</p>
<h3 id="2-캡슐화">2. 캡슐화</h3>
<p>캡슐화의 성공 여부를 외부 클라이언트 관점과 <strong>내부 자식 클래스 관점</strong> 두 가지로 나누어 측정했다.</p>
<h4 id="--확인-결과-1">- 확인 결과</h4>
<table>
<thead>
<tr>
<th><strong>측정 지표</strong></th>
<th><strong>A안 (상속)</strong></th>
<th><strong>B안 (조합)</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>외부 노출도 (보드 클래스 기준)</strong></td>
<td><strong>0개</strong></td>
<td><strong>0개</strong></td>
</tr>
<tr>
<td><strong>내부 오염도 (마 클래스 기준)</strong></td>
<td><strong>3개</strong></td>
<td><strong>0개</strong></td>
</tr>
</tbody></table>
<p><strong>1. 외부 노출도 측정 (Board 클래스에서 접근 시)</strong></p>
<p>Board.java에서 이동 중인 기물 객체(.movingPiece)에 대해 자동 완성(.)을 호출하여 노출되는 메서드 목록을 확인.</p>
<ul>
<li><p>외부 노출도 사진1 (상속)
  <img src="https://velog.velcdn.com/images/s_miny/post/d3c46e23-0764-4164-ba76-edeb5098915f/image.png" alt=""></p>
</li>
<li><p>외부 노출도 사진2 (조합)
  <img src="https://velog.velcdn.com/images/s_miny/post/104b8f85-ceb9-4043-bd20-85614e0cabf2/image.png" alt=""></p>
</li>
</ul>
<p>  [사진 1 &amp; 2 확인 결과]
  A안과 B안 모두 기물의 고유한 역할과 무관한 궁성 관련 메서드가 외부 클라이언트인 Board에게 노출되지 않았다.</p>
<p><strong>2. 내부 오염도 측정 (Ma 클래스 내부에서 접근 시)</strong></p>
<p>하위 클래스인 Ma.java 내부 메서드에서 this.을 호출하여 자식 클래스가 강제로 알게 되는 부모의 메서드 목록을 확인.</p>
<ul>
<li><p>내부 오염도 사진3 (상속)</p>
<p>  <img src="https://velog.velcdn.com/images/s_miny/post/74e551c7-c900-4903-9d4f-205fc962890d/image.png" alt=""></p>
<p>[사진 3 확인 결과]
마는 궁성 규칙을 받는 기물이 아니지만, 부모 클래스(Piece)로부터 궁성을 체크하는 상세 구현 방법을 강제로 알게됐다. 또한 마 고유의 책임과 무관한 isPalace(), isPalaceDiagonalStep(), isStraightLine()의 메서드가 노출되는 내부 인터페이스 오염이 발생했다.</p>
</li>
</ul>
<ul>
<li><p>내부 오염도 사진4 (조합)</p>
<p> <img src="https://velog.velcdn.com/images/s_miny/post/b2cdea0e-273d-4fa4-bd01-50f8d956a1ed/image.png" alt=""></p>
<p> [사진 4 확인 결과]</p>
<pre><code> 이동 전략을 독립된 객체로 분리하여 조합한 결과, 기물 객체는 전략의 상세 구현을 모르는 상태였다. 따라서 전략 클래스 내부에서 this.을 입력해도 자신의 본연의 책임만 보일 뿐, 다른 규칙의 존재는 알 수 없었다.</code></pre></li>
</ul>
<h4 id="--가설-판단-1">- 가설 판단</h4>
<blockquote>
<p>&quot;상속 대신 조합을 사용하면, 부모 클래스의 불필요한 메서드 노출을 막아 객체의 인터페이스를 깔끔하게 유지할 수 있을 것이다.&quot;</p>
</blockquote>
<p>측정 결과, 상속을 사용한 A안은 외부 캡슐화에는 성공했으나 하위 클래스인 Ma 내부에서 본래 책임과 무관한 메서드가 3개 노출되는 내부 인터페이스 오염이 발생하였다. 반면 조합을 사용한 B안은 내부 오염가 0으로 정보 은닉을 성공했다.</p>
<p>특히, A안은 하위 클래스가 자신이 사용하지 않는 메서드에 의존하도록 강제되는 구조를 가짐으로써 객체지향 원칙인 인터페이스 분리 원칙(ISP)을 위반하고 있음을 확인하였다. 따라서 나의 가설은 참으로 판별한다.</p>
<hr>
<h1 id="고찰">고찰</h1>
<h2 id="1-해석">1. 해석</h2>
<ul>
<li><p>수정 범위의 차이와 안정성:
결과에서 보듯 상속은 기능을 하나 더할 때마다 Piece나 Gung 같은 기존 클래스를 계속 열어서 수정해야 한다. 이 행동은 잘 돌아가던 장기판을 망가뜨릴 수 있는 위험이 있다. 반면 조합은 기존 코드를 한 줄도 안 건드리고 새로운 전략 클래스만 추가하면 돼서, 전체 로직의 안정성을 해치지 않고 기능을 추가할 수 있는 의미다.</p>
</li>
<li><p>불필요한 지식 차단:
상속 내부 오염도가 3개라는 것은, Ma 클래스를 만드는 개발자가 궁성 규칙까지 강제로 알아야 한다는 뜻이다. 반면 조합으로 오염도를 0으로 만든 것은 각 기물이 자기 역할에만 집중하게 만든 것이다. 결과적으로 코드를 읽을 때 불필요한 정보가 안 보여서 훨씬 읽기 편하고 실수할 확률도 줄어든다.</p>
</li>
<li><p>유연한 설계:
결과적으로 이번 실험은 상속보다 조합이라는 원칙이 단순히 이론이 아님을 보여준다. 조합을 쓰면 사용하지 않는 기능에는 의존하지 않고(ISP), 기존 코드는 닫아둔 채 확장(OCP)하는 게 실제로 가능하다는 것을 수치로 증명한 것이다.</p>
</li>
</ul>
<h2 id="2-한계점">2. 한계점</h2>
<h3 id="단일-시나리오의-국한"><strong>단일 시나리오의 국한</strong></h3>
<p>이 실험은 장기 미션 중 궁성 내 이동 제약이라는 단 하나의 기능 추가 시나리오만을 가지고 진행되었다. 이 시나리오에서는 조합의 우수성이 명확히 드러났으나, 장기의 다른 규칙이나 다른 도메인의 요구사항 추가 시에도 조합이 항상 상속보다 유리할지에 대해서는 현재 실험의 데이터만으로 일반화하기 어렵다. 결국 이번 실험은 시나리오의 표본이 너무 적다는 한계가 있다.</p>
<h2 id="3-결론">3. 결론</h2>
<p>이번 실험은 당연하다고 믿었던 원칙을 의심해 보고, 두 설계 방식이 가진 명확한 트레이드 오프를 코드 레벨에서 직접 경험하는 것이였다.</p>
<p>실험 결과, 규칙의 확장이 빈번한 복잡한 도메인에서는 조합이 주는 유연함과 정보 은닉이 크다는 것을 확인했다. 그러나 객체 간의 관계가 매우 단순하고 구조가 영구적으로 변하지 않을 것이 보장되는 정적인 도메인이라면, 상속이 제공하는 코드의 직관성과 빠른 구현 속도 또한 충분히 선택할만한 가치가 있음을 깨달았다.</p>
<p>결국 좋은 설계란 특정 원칙을 전적으로 믿는 것이 아니라고 생각한다. &quot;내 도메인이 얼마나 안정적인가?&quot;를 객관적으로 판단하여, 상속의 간결함과 조합의 견고함 중 현재의 상황에 가장 적절한 기술을 골라 쓰는 설계자의 안목이 가장 중요하다는 사실을 이번 실험을 통해 알게되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우아한테크코스 8기 합격 후기 - 4개월간의 기록]]></title>
            <link>https://velog.io/@s_miny/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-8%EA%B8%B0-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0-4%EA%B0%9C%EC%9B%94%EA%B0%84%EC%9D%98-%EA%B8%B0%EB%A1%9D</link>
            <guid>https://velog.io/@s_miny/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-8%EA%B8%B0-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0-4%EA%B0%9C%EC%9B%94%EA%B0%84%EC%9D%98-%EA%B8%B0%EB%A1%9D</guid>
            <pubDate>Thu, 05 Feb 2026 02:18:23 GMT</pubDate>
            <description><![CDATA[<h2 id="먼저-그날의-감정과-이야기">먼저, 그날의 감정과 이야기</h2>
<p>짧게 합격 썰을 풀고,
이후에 내가 지나온 4개월간의 기록을 정리해보려고 한다.</p>
<p>먼저 1차 합격을 받았을 때는 붙어도 안 붙어도 코딩테스트 공부는 하자는 마음으로 이미 공부를 하고 있었다.</p>
<p>다른 사람들과 같이 준비하며 결과 당일 이메일을 기다렸다.</p>
<p>이 순간도 엄청 떨렸지만, 
그때는 몰랐다.
이보다 더 떨리는 순간이 올 줄은.</p>
<p><img src="https://velog.velcdn.com/images/s_miny/post/583894c8-9f1d-4c2a-9bf9-f1578d466d7e/image.png" alt=""></p>
<p>다른 사람들의 말을 들어보니
합격 메일이 정말 늦게 오는 분들도 많다고 했다.</p>
<p>그런데 나는 비교적 일찍 메일이 도착했고,
그렇게 1차 합격 통보를 받게 되었다.</p>
<p>메일을 보자마자
지난 3개월간의 노력이 나에게 보답을 해주는 것 같았다.</p>
<p>하지만 여기서 기뻐하기엔 아직 이르다고 생각했다.
다시 마음을 다잡고
최종 코딩테스트 준비에 집중했다.</p>
<hr>
<p>최종 코딩테스트를 보고 난 뒤에는
너무 아쉽고, 잘하지 못한 것 같다는 생각에
상실감에 빠져 있었다.</p>
<p>그래서 솔직히 말하면
최종 합격 결과날이 오지 않았으면 좋겠다고 생각했다.</p>
<p>하지만 결국 그날은 왔고,
1차 합격 때보다 체감상 5배는 더 떨렸다.</p>
<p>혼자 보기에는 너무 떨려서
친형과 함께 메일을 열어보았다.</p>
<p><img src="https://velog.velcdn.com/images/s_miny/post/83c01ae4-c56a-4c86-8f96-64e454cb963b/image.png" alt=""></p>
<h3 id="최종-합격을-하였다">최종 합격을 하였다..!!</h3>
<p>너무 좋아서 소리를 질렀고,
그 이후의 기억은 잘 나지 않는다.</p>
<hr>
<h2 id="프리코스-요약">프리코스 요약</h2>
<p>프리코스를 진행하며 내가 어떻게 변해왔는지를 중심으로 짧게 정리해보려고 한다.</p>
<h4 id="자소서-나를-어떻게-설명해야-할지-몰랐던-시기">자소서: 나를 어떻게 설명해야 할지 몰랐던 시기</h4>
<p>나 자신을 안다고 생각했지만,
글로 나를 설명하는 건 전혀 다른 문제라는 걸 처음 느꼈다.
이 시기에는 “잘 보이기”보다 “솔직하게 쓰는 것”이 더 어렵게 느껴졌다.</p>
<hr>
<h4 id="프리코스-1주차-기능-단위-커밋을-처음-시작하다">프리코스 1주차: 기능 단위 커밋을 처음 시작하다.</h4>
<ul>
<li><a href="https://velog.io/@s_miny/%EC%9A%B0%ED%85%8C%EC%BD%94-8%EA%B8%B0-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%EB%A1%9D">우테코 8기 프리코스 1주차 회고록</a></li>
</ul>
<p>“일단 돌아가게 만들자”에서
“어떻게 기록하고, 어떻게 쪼갤 것인가”로 
관점이 바뀌기 시작한 시기였다.</p>
<hr>
<h4 id="프리코스-2주차-에러를-만나면-5분간-생각해보기">프리코스 2주차: 에러를 만나면 5분간 생각해보기</h4>
<ul>
<li><a href="https://velog.io/@s_miny/%EC%9A%B0%ED%85%8C%EC%BD%94-8%EA%B8%B0-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%EB%A1%9D">우테코 8기 프리코스 2주차 회고록</a></li>
</ul>
<p>에러를 만나자마자 검색하는 대신,
왜 이런 에러가 났는지 먼저 생각해보는 습관이 생겼다.</p>
<p>정답을 빠르게 찾는 것보다
과정을 이해하려는 시도를 더 하게 되었다.</p>
<hr>
<h4 id="프리코스-3주차-읽기-좋은-코드-좋은-설계가-엄청-중요하다는-걸-처음-체감">프리코스 3주차: 읽기 좋은 코드, 좋은 설계가 엄청 중요하다는 걸 처음 체감</h4>
<ul>
<li><a href="https://velog.io/@s_miny/%EC%9A%B0%ED%85%8C%EC%BD%94-8%EA%B8%B0-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%EB%A1%9D">우테코 8기 프리코스 3주차 회고록</a></li>
</ul>
<p>코드의 길이보다
책임의 경계와 역할 분리가 훨씬 중요하다는 것을
처음으로 체감한 주차였다.</p>
<hr>
<h4 id="프리코스-4주차-오픈미션-혼자서-끝까지-도전하고-성장한-경험">프리코스 4주차 오픈미션: 혼자서 끝까지 도전하고 성장한 경험</h4>
<p>오픈미션은 벨로그에 일지처럼 회고를 작성했기 때문에
이 글에 모두 담기에는 너무 방대하다.</p>
<p>다만 확실한 건,
누군가의 정답을 따라가는 게 아니라
내 선택에 내가 책임져야 했던 경험이었다는 점이다.</p>
<hr>
<h4 id="최종-코딩테스트-완벽보다-도전하며-선택의-이유를-설명하는-시험">최종 코딩테스트: 완벽보다, 도전하며 선택의 이유를 설명하는 시험</h4>
<p>너무 떨려서
따로 기록을 남겨둘 여유조차 없었다.</p>
<p>하지만 지금 돌아보면
이 시험은 완벽한 정답을 요구했다기보다는
왜 이런 선택을 했는지를 설명할 수 있는지 보고 있었던 것 같다.</p>
<hr>
<h2 id="프리코스-마음가짐">프리코스 마음가짐</h2>
<p>프리코스 기간 동안 나는 끊임없이 스스로에게 질문을 던졌다.</p>
<h4 id="설계에-대한-질문">설계에 대한 질문</h4>
<ul>
<li>어떻게 하면 좋은 설계로 개발을 진행할 수 있을까?</li>
<li>이 책임은 정말 이 객체의 책임일까?</li>
<li>한 객체가 너무 많은 역할을 하고 있지는 않을까?</li>
</ul>
<h4 id="코드를-읽는-사람에-대한-질문">코드를 읽는 사람에 대한 질문</h4>
<ul>
<li>다른 사람이 내 코드를 보면 쉽게 이해할 수 있을까?</li>
<li>메서드 이름만 보고 의도가 드러날까?</li>
</ul>
<h4 id="기술에-대한-질문">기술에 대한 질문</h4>
<ul>
<li>이 오류는 왜 발생한 걸까?</li>
<li>스프링 프레임워크는 정확히 어떤 역할을 할까?</li>
<li>자바에서 내가 아직 모르는 영역은 무엇일까?</li>
</ul>
<p>답이 바로 나오지 않더라도 
주눅 들지 않으려고 했다.</p>
<p>왜 답이 나오지 않는지에 대한 이유를 다시 찾으며 
멈추지 않으려고 노력했다.</p>
<hr>
<h2 id="글을-마치며">글을 마치며</h2>
<p>이 글이
나중에 프리코스를 준비하거나
결과를 기다리고 있는 누군가에게
조금이라도 도움이 되었으면 좋겠다.</p>
<p>부족하다고 느꼈던 순간들이
반드시 감점만 되는 것은 아니었다.</p>
<p>중요했던 건,
그 순간에도 고민을 멈추지 않았는지였다고 생각한다.</p>
<p>나는 이제 우아한테크코스 8기를 시작한다.
프리코스에서 가졌던 질문과 태도를 잃지 않고,</p>
<p>8기 합격자분들과 
서로 질문 하고,
서로 성장하며, 
몰입하는 시간을 보내고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LazyInitializationException 500 오류]]></title>
            <link>https://velog.io/@s_miny/LazyInitializationException-500-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@s_miny/LazyInitializationException-500-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Sat, 22 Nov 2025 06:32:28 GMT</pubDate>
            <description><![CDATA[<h2 id="이-글을-쓰게-된-이유">이 글을 쓰게 된 이유</h2>
<p>우테코 오픈 미션 개발을 마친 뒤 Postman으로 API 테스트를 하던 중 예상치 못한 <code>500 Internal Server Error</code>가 발생했다. </p>
<p>Postman에서는 단순히 <code>500</code>만 표시될 뿐 원인을 알 수 없었기 때문에 <strong>서버 로그를 직접 확인</strong>했는데, 그 안에는 <code>LazyInitializationException</code>이라는 <strong>낯선 오류가 찍혀 있었다.</strong></p>
<p><strong>“이게 왜 터진 걸까?”라는 궁금증이 생겼고</strong> 이를 이해하기 위해 <strong>JPA의 동작 원리를 깊게 파고들었다.</strong></p>
<p>이 글은 그 과정에서 <strong>내가 정리한 공부 기록이자 회고</strong>이다.</p>
<hr>
<h2 id="발생-배경">발생 배경</h2>
<p>개발이 끝나고 도서 단건 조회 API(<code>/api/books/{id}</code>)를 호출했을 때 Postman에서 갑자기 <code>500 Internal Server Error</code>가 떨어졌다.</p>
<p><strong>처음에는 단순한 DTO 변환 문제나 JSON 직렬화 과정에서의 오류라고 생각했다.</strong></p>
<p>하지만 <strong>응답에는 에러 메시지도 설명도 없었고</strong> 단순히 <code>500</code>만 표시되었기 때문에 <strong>원인을 전혀 알 수 없었다.</strong></p>
<p>이 찝찝함을 해결하기 위해 서버 로그를 직접 확인해 보기로 결정했다.</p>
<hr>
<h2 id="로그-확인---단-한-줄이-문제의-실체를-알려줬다">로그 확인 - 단 한 줄이 문제의 실체를 알려줬다.</h2>
<p>IntelliJ Run 탭에서 서버 로그를 확인하자 엄청나게 긴 스택트레이스가 출력되었다.
그중에서도 시선을 사로잡은 문장은 아래 한 줄이었다.</p>
<pre><code class="language-yaml">org.hibernate.LazyInitializationException: failed to lazily initialize a collection ... no Session</code></pre>
<p>그리고 그 아래에는 정확히 어떤 부분에서 문제가 발생했는지 위치까지 찍혀 있었다.</p>
<pre><code class="language-yaml">at smiinii.object_oriented_library.dto.book.BookResponse.from(BookResponse.java:24)
at smiinii.object_oriented_library.controller.BookController.getBook(BookController.java:44)</code></pre>
<p>이 두 줄을 보고 처음으로 상황이 명확해지기 시작했다.</p>
<ul>
<li>Lazy 로딩된 필드에 접근했다</li>
<li>그런데 그 시점에는 세션(트랜잭션)이 이미 닫혀 있었다</li>
<li>그래서 Hibernate가 데이터를 불러오지 못하고 예외를 던졌다</li>
</ul>
<p>즉, <strong>컨트롤러 계층에서 엔티티의 LAZY 필드에 접근했기 때문에 터진 오류였다.</strong></p>
<hr>
<h2 id="원인-분석---왜-세션이-닫힌-상태였을까">원인 분석 - 왜 세션이 닫힌 상태였을까?</h2>
<p>그렇다면 자연스럽게 다음 질문이 생겼다.</p>
<blockquote>
<p>“컨트롤러에서는 왜 세션이 닫힌 상태였을까?”
“트랜잭션은 어디서 끝나는 걸까?”</p>
</blockquote>
<p>이를 이해하기 위해 JPA 트랜잭션과 LAZY 동작 방식을 다시 정리해 보았다.</p>
<h3 id="1-서비스-계층에서-트랜잭션이-종료된다">1) 서비스 계층에서 트랜잭션이 종료된다.</h3>
<p>내 서비스 메서드는 기본적으로 <code>@Transactional(readOnly = true)</code>로 감싸져 있었다.</p>
<pre><code class="language-java">public Book getBook(Long id) {
    return bookRepository.findById(id).orElseThrow(...);
}</code></pre>
<p>서비스 메서드가 종료되면 트랜잭션도 함께 종료되고 <strong>JPA의 영속성 컨텍스트(DB 세션)도 함께 닫힌다.</strong></p>
<h3 id="2-lazy는-필요할-때-데이터를-가져온다">2) LAZY는 필요할 때 데이터를 가져온다.</h3>
<p><code>Book</code> 엔티티의 소장본 목록은 이렇게 설정해 두었다.</p>
<pre><code class="language-java">@OneToMany(..., fetch = FetchType.LAZY)
private List&lt;StoredBook&gt; storedBooks;</code></pre>
<p>즉, <code>Book</code>을 조회할 때는 <code>storedBooks</code>까지 가져오지 않고
나중에 <code>book.getStoredBooks().size()</code>같은 메서드를 호출하는 순간 DB에 접근한다.</p>
<h3 id="3-하지만-컨트롤러에서는-이미-세션이-닫혀-있었다">3) 하지만 컨트롤러에서는 이미 세션이 닫혀 있었다.</h3>
<p>문제가 된 코드는 다음과 같았다.</p>
<pre><code class="language-java">@GetMapping(&quot;/{id}&quot;)
public ResponseEntity&lt;BookResponse&gt; getBook(@PathVariable Long id) {
    Book book = bookService.getBook(id); // 여기서 트랜잭션 끝남
    return ResponseEntity.ok(BookResponse.from(book)); // Lazy 접근 발생
}</code></pre>
<pre><code class="language-java">public static BookResponse from(Book book) {
    return new BookResponse(
        book.getId(),
        book.getTitle(),
        book.getAuthor(),
        book.getStoredBooks().size() // ← DB 접근 필요 → 세션 없음 → 예외 발생
    );
}</code></pre>
<ul>
<li>서비스 메서드 호출 → 트랜잭션 종료</li>
<li>컨트롤러에서 DTO 변환 중 LAZY 필드 접근</li>
<li>DB 접근 시도 → 이미 세션 종료 → <code>LazyInitializationException</code> 발생</li>
</ul>
<p>문제의 근본 원인은 <strong>트랜잭션 밖(컨트롤러)에서 지연 로딩 필드에 접근했다는 점이었다.</strong></p>
<h2 id="그럼-왜-저장된-필드id-title-author는-문제-없었을까">그럼 왜 저장된 필드(id, title, author)는 문제 없었을까?</h2>
<p>재미있게도 <code>BookResponse.from(book)</code> 내부에서
<code>book.getId()</code> / <code>book.getTitle()</code> / <code>book.getAuthor()</code>는 정상 작동했다</p>
<p>이유는 간단하다.</p>
<ul>
<li><code>id</code>, <code>title</code>, <code>author</code> 같은 필드는 엔티티 자체의 컬럼 값이라 <code>Book</code>을 조회하는 시점에 이미 DB에서 함께 가져온 상태다.</li>
<li>반면 <code>storedBooks</code>는 <code>@OneToMany(fetch = FetchType.LAZY)</code>로 매핑된 연관 컬렉션이라 <code>Book</code>을 조회할 때 실제 데이터 대신 “나중에 필요하면 불러오겠다”는 프록시만 들고 있다.</li>
</ul>
<p>그래서 컨트롤러에서 <code>BookResponse.from(book)</code>을 호출하면</p>
<pre><code class="language-java">book.getId();        // 이미 메모리에 있는 값
book.getTitle();     // 이미 메모리에 있는 값
book.getAuthor();    // 이미 메모리에 있는 값
book.getStoredBooks().size(); // ← 이때 처음으로 DB 접근 필요</code></pre>
<p>이 마지막 줄에서 <code>Hibernate</code>는 <strong>“아, 이제 진짜 storedBooks가 필요하구나. 그럼 DB에서 조회할게.”라고 시도</strong>하지만 
그 순간에는 <strong>이미 트랜잭션과 세션이 종료된 상태라 DB에 접근할 수 없다.</strong></p>
<p>그래서 <code>LazyInitializationException</code>이 발생한다.</p>
<hr>
<h2 id="해결-과정---수정-전--수정-후-비교">해결 과정 - 수정 전 / 수정 후 비교</h2>
<h3 id="수정-전--엔티티를-컨트롤러까지-끌고-와서-변환">수정 전 – 엔티티를 컨트롤러까지 끌고 와서 변환</h3>
<ul>
<li><p><strong>Service (수정 전)</strong></p>
<pre><code class="language-java">@Transactional(readOnly = true)
public Book getBook(Long bookId) {
  return bookRepository.findById(bookId)
      .orElseThrow(() -&gt; new IllegalArgumentException(&quot;존재하지 않는 도서입니다.&quot;));
}</code></pre>
</li>
<li><p><strong>Controller (수정 전)</strong></p>
<pre><code class="language-java">@GetMapping(&quot;/{id}&quot;)
public ResponseEntity&lt;BookResponse&gt; getBook(@PathVariable Long id) {
  Book book = bookService.getBook(id);
  return ResponseEntity.ok(BookResponse.from(book));
}</code></pre>
</li>
</ul>
<p>컨트롤러에서 Lazy 접근 → <code>LazyInitializationException</code></p>
<h3 id="수정-후---서비스-계층에서-dto-변환까지-처리-정석-구조">수정 후 - 서비스 계층에서 DTO 변환까지 처리 (정석 구조)</h3>
<ul>
<li><p><strong>Service (수정 후)</strong></p>
<pre><code class="language-java">@Transactional(readOnly = true)
public BookResponse getBook(Long bookId) {
  Book book = bookRepository.findById(bookId)
      .orElseThrow(() -&gt; new IllegalArgumentException(&quot;존재하지 않는 도서입니다.&quot;));

  return BookResponse.from(book); // 트랜잭션 안에서 Lazy 해결 가능
}</code></pre>
</li>
<li><p><strong>Controller (수정 후)</strong></p>
<pre><code>@GetMapping(&quot;/{id}&quot;)
public ResponseEntity&lt;BookResponse&gt; getBook(@PathVariable Long id) {
  return ResponseEntity.ok(bookService.getBook(id)); // DTO만 다룸
}</code></pre></li>
<li><p><strong>Lazy 로딩 문제 해결</strong></p>
</li>
<li><p><strong>계층 분리가 명확해짐</strong></p>
</li>
<li><p><strong>컨트롤러가 더 이상 JPA 내부 동작에 의존하지 않음</strong></p>
</li>
</ul>
<hr>
<h2 id="배운점">배운점</h2>
<p><strong>이번 오류는 단순한 버그 해결이 아니라</strong> 내가 지금까지 공부해 온 영속성 컨텍스트, 지연 로딩, 연관관계 개념이 <strong>실제 코드 흐름에서 어떻게 작동하는지 처음으로 명확하게 체감</strong>한 순간이었다.</p>
<p>오픈미션 기간 동안 아래 글들을 통해 이론은 여러 번 공부했었다.</p>
<ul>
<li><a href="https://velog.io/@s_miny/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%EC%9D%80-%EB%98%90-%EB%AD%90%EC%95%BC">JPA 영속성 컨텍스트는 또 뭐야?</a></li>
<li><a href="https://velog.io/@s_miny/JPA-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EA%B0%80-%EB%AD%90%EC%95%BC">JPA 연관관계가 뭐야?</a></li>
</ul>
<p>하지만 이번 오류를 만나기 전까지는 <strong>솔직히 “왜 서비스 밖(컨트롤러)에서 엔티티를 사용하면 안 되는지” 그 이유를 완전히 이해하지 못했다.</strong></p>
<p>이번 경험을 통해 나는 짧지만 확실한 두 가지를 배웠다.</p>
<p>1) <strong>트랜잭션이 끝나면 영속성 컨텍스트도 함께 사라진다.</strong>
그동안 머리로만 이해했던 문장이 실제 코드 흐름과 함께 하나로 연결됐다.
트랜잭션 밖에서 Lazy 필드에 접근하면 당연히 터질 수밖에 없다는 것.</p>
<p>2) <strong>“엔티티는 컨트롤러까지 절대 가져가지 말라”는 조언의 진짜 이유</strong>
이제는 그저 규칙이 아니라 지연 로딩·트랜잭션·계층 구조가 모두 맞물리는 설계 원칙이라는 걸 확실히 알게 됐다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>이번 경험은 단순히 한 번의 예외를 해결하는 것이 아니라 JPA의 내부 동작과 계층 구조의 역할을 몸으로 이해하게 만든 중요한 계기였다.</p>
<p>앞으로는 <strong>엔티티의 생명 주기와 트랜잭션 경계를 더 신중하게 바라보며</strong> 
<strong>서비스와 컨트롤러가 맡아야 할 책임을 명확히 구분</strong>하는 설계를 지속해서 실천하고자 한다.</p>
<p>작은 오류 하나가 나의 설계 관점 전체를 흔든 경험이었고 이 깨달음은 앞으로의 개발 과정에서 큰 자산이 될 것 같다.</p>
<h2 id="참조">참조</h2>
<p><a href="https://jerecord.tistory.com/197">LazyInitializationException 왜 발생하고, 어떻게 해결할까🧐
</a><a href="https://strong-park.tistory.com/entry/orghibernatelazyinitializationexception-could-not-initialize-proxy-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0">orghibernatelazyinitializationexception-could-not-initialize-proxy 에러 해결 방법</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[처음 만난 Mockito, 스프링 서비스 테스트는 이렇게 시작했다.]]></title>
            <link>https://velog.io/@s_miny/%EC%B2%98%EC%9D%8C-%EB%A7%8C%EB%82%9C-Mockito-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%8A%94-%EC%9D%B4%EB%A0%87%EA%B2%8C-%EC%8B%9C%EC%9E%91%ED%96%88%EB%8B%A4</link>
            <guid>https://velog.io/@s_miny/%EC%B2%98%EC%9D%8C-%EB%A7%8C%EB%82%9C-Mockito-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%8A%94-%EC%9D%B4%EB%A0%87%EA%B2%8C-%EC%8B%9C%EC%9E%91%ED%96%88%EB%8B%A4</guid>
            <pubDate>Mon, 17 Nov 2025 13:34:48 GMT</pubDate>
            <description><![CDATA[<h2 id="이-글을-쓰게된-이유">이 글을 쓰게된 이유</h2>
<p>도메인 객체들을 모두 만들고 서비스 레이어 코드를 작성하던 중 문득 이런 생각이 들었다.</p>
<blockquote>
<p>“도메인 테스트는 했는데… 서비스 코드는 어떻게 테스트하지?”</p>
</blockquote>
<p>구글링을 하다 보니 대부분의 예시에서 <strong>Mockito</strong>라는 도구가 등장했다.
나는 지금까지 <strong>프리코스에서 단위 테스트 위주로만 개발했고 DB나 외부 협력 객체를 따로 Mock 해야 하는 상황을 마주한 적이 없었다.</strong></p>
<p>그러나 Mockito를 알고 나서 “아 서비스 테스트는 이렇게 하는 거구나!” 하고 감탄했다.</p>
<p>이 글은 내가 <strong>처음 Mockito를 접하며 정리한 내용을 기록해두기 위한 글</strong>이다.
나중에 다시 돌아왔을 때도 한눈에 이해할 수 있도록 최대한 쉽게 풀어 썼다.</p>
<hr>
<h2 id="mockito는-어떤-상황에서-등장하는가">Mockito는 어떤 상황에서 등장하는가?</h2>
<p>Mockito는 <strong>단위 테스트(Unit Test)</strong>에서 등장한다.</p>
<p>단위 테스트란?</p>
<ul>
<li>하나의 클래스 또는 메서드만 정확히 검증하는 테스트</li>
<li>외부 시스템(DB, 네트워크, 파일 시스템 등)이 개입하면 테스트가 느리고 불안정해짐</li>
</ul>
<p><strong>외부 의존성을 끊고</strong> + <strong>원하는 상황을 마음대로 만들고 싶을 때</strong>
→ Mocking 프레임워크 = <code>Mockito</code>가 등장한다.</p>
<hr>
<h2 id="mockito란">Mockito란?</h2>
<p><code>Mockito</code>는 테스트 중에 &quot;<strong>협력 객체의 행동을 마음대로 조작할 수 있는 가짜 객체(Mock)</strong>&quot;를 만들어주는 프레임워크이다.</p>
<p>Mock 객체는 </p>
<ul>
<li><strong>실제 객체처럼 생겼지만</strong></li>
<li><strong>실제 기능은 없고</strong></li>
<li><strong>내가 정의한 방식으로만 동작</strong>한다.</li>
</ul>
<p>즉, <code>Mockito</code>는 <strong>테스트에서 현실에서는 만들기 어려운 상황을 즉시 재현하는 기술을 제공</strong>한다.</p>
<p><code>Mock</code>을 쓰면 다음과 같은 상황을 연출할 수 있다.</p>
<ul>
<li>DB 조회 결과를 강제로 없다/있다로 만들기</li>
<li>저장 메서드가 호출되면 오류를 던지게 만들기</li>
<li>특정 값이 입력되면 특정 객체를 반환하게 만들기</li>
<li>외부 API가 timeout 났다는 상황 만들기</li>
<li>메일 발송 메서드를 호출했는지 검증하기</li>
</ul>
<p>서비스 테스트에서 매우 강력한 도구가 되는 이유다.</p>
<hr>
<h2 id="mock--stub--spy--fake-차이">Mock / Stub / Spy / Fake 차이</h2>
<p><code>Mockito</code>는 단순히 가짜 객체(Mock)만 만드는 게 아니라, 상황 조작을 위한 다양한 개념들이 있다.</p>
<h3 id="1-mock">1. Mock</h3>
<ul>
<li><strong>실제 객체를 흉내낸 껍데기</strong></li>
<li>내부 로직 없음</li>
<li><code>Repository</code>, <code>Policy</code> 등을 <code>Mock</code>으로 만든다</li>
</ul>
<p>예:</p>
<pre><code class="language-java">@Mock
MemberRepository memberRepository;</code></pre>
<h3 id="2-stub">2. Stub</h3>
<ul>
<li><code>Mock</code>에게 “이렇게 동작해라”라고 <strong>행동을 정의하는 것</strong></li>
<li>테스트의 “상황 만들기” 기능</li>
</ul>
<p>예:</p>
<pre><code class="language-java">given(memberRepository.findById(1L))
        .willReturn(Optional.of(Member.overdueMember(1L)));</code></pre>
<h3 id="3-spy">3. Spy</h3>
<ul>
<li>실제 객체 + <code>Mock</code>이 섞인 형태</li>
<li>실제 메서드도 실행되지만 <strong>필요한 부분만 Mocking 가능</strong></li>
<li>도메인 검증보다는 정책 객체 부분적 실험할 때 사용</li>
</ul>
<p>예:</p>
<pre><code class="language-java">@Spy
LoanPolicy loanPolicy;</code></pre>
<h3 id="4-fake">4. Fake</h3>
<ul>
<li>실제 동작을 단순화한 테스트용 구현체</li>
</ul>
<p>예:
<code>InMemoryRepository</code> (<strong>직접 구현하는 것</strong>, <code>Mockito</code>가 만들지는 않음)</p>
<hr>
<h2 id="그럼-mockito는-왜-필요한가">그럼 Mockito는 왜 필요한가?</h2>
<p>서비스는 보통 이런 흐름으로 동작한다.</p>
<blockquote>
<p>서비스 로직
   ↓
Repository 조회
   ↓
도메인 규칙 판단
   ↓
Repository 저장
   ↓
외부 후처리</p>
</blockquote>
<p>여기서 핵심은..
서비스는 <strong>여러 객체들과 협력</strong>하며 동작하고 이 객체들은 보통 <strong>DB 연결, 네트워크 통신 같은 느리고 무거운 작업</strong>이다.</p>
<p>그래서 테스트에서 이걸 실제로 호출하면 이런 문제가 생긴다.</p>
<ul>
<li><strong>DB 초기 세팅 필요</strong> → 테스트 느림</li>
<li><strong>외부 API의 응답이 매번 달라짐</strong> → 테스트 불안정</li>
<li><strong>네트워크 오류, 메일 서버 문제</strong> → 테스트 깨짐</li>
<li><strong>특정 상황(예: “연체 회원”)을 만들기 어려움</strong></li>
</ul>
<p>따라서, 테스트에서는 “<strong>상황을 내 마음대로 연출할 수 있는 가짜 객체(Mock)</strong>”가 훨씬 유리하다.</p>
<p>이걸 자동으로 만들어주는 라이브러리가 <strong>Mockito</strong>다.</p>
<hr>
<h2 id="mockito의-핵심-기능-3가지">Mockito의 핵심 기능 3가지</h2>
<p>Mockito로 테스트할 때 꼭 이해해야 하는 세 가지 기능.</p>
<h3 id="1-mock-생성-mock">1. <strong>Mock 생성</strong> (<code>@Mock</code>)</h3>
<p><strong>가짜 객체를 만든다.</strong></p>
<pre><code class="language-java">@Mock
MemberRepository memberRepository;</code></pre>
<h3 id="2-stub-행동-정의">2. <strong>Stub (행동 정의)</strong></h3>
<p>Mock이 어떻게 반응해야 할지 정의한다.</p>
<pre><code class="language-java">given(memberRepository.findById(1L))
        .willReturn(Optional.of(member));</code></pre>
<p>또는 예외 상황도 조작 가능하다.</p>
<pre><code class="language-java">given(storedBookRepository.findById(10L))
        .willThrow(new BookNotFoundException());</code></pre>
<p>즉, <strong>테스트의 상황을 만드는 역할</strong></p>
<h3 id="3-verify-호출-검증">3. <strong>Verify (호출 검증)</strong></h3>
<p>서비스가 Mock에게 “정확히 어떤 메시지를 보냈는지” 확인한다.</p>
<pre><code class="language-java">verify(mailSender).sendWelcomeMail(email);</code></pre>
<p>또는 호출되면 안 되는 경우에는</p>
<pre><code class="language-java">verify(loanRepository, never()).save(any());</code></pre>
<p>즉, <strong>서비스의 행동을 검증하는 역할</strong></p>
<hr>
<h2 id="내-bookservicetest에-mockito가-어떻게-쓰였는지">내 BookServiceTest에 Mockito가 어떻게 쓰였는지</h2>
<p>위에서 <code>Mockito</code>의 개념을 모두 정리했다면,
이제 실제로 내가 작성한 <code>BookServiceTest</code>에 그 개념들이 어떻게 적용되었는지 살펴보자.</p>
<p>특히 처음 보면 어렵게 느껴지는 두 부분</p>
<ul>
<li><code>ArgumentCaptor</code></li>
<li><code>thenAnswer</code></li>
</ul>
<p>이 둘은 서비스 테스트에서 매우 자주 쓰이기 때문에 꼭 이해하고 넘어가면 좋다.</p>
<p>아래는 실제 코드 일부다.</p>
<h3 id="전체-코드-일부">전체 코드 (일부)</h3>
<pre><code class="language-java">@ExtendWith(MockitoExtension.class)
class BookServiceTest {

    @Mock
    private BookRepository bookRepository;

    private BookService bookService;

    @BeforeEach
    void setUp() {
        bookService = new BookService(bookRepository);
    }

    @Test
    @DisplayName(&quot;registerBook: 새 도서를 등록하면 Book.registerNew로 생성된 엔티티를 저장하고 생성된 ID를 반환한다&quot;)
    void savesBookAndReturnsId() throws Exception {
        // given
        String title = &quot;클린 코드&quot;;
        String author = &quot;로버트 마틴&quot;;
        int initialCount = 3;

        when(bookRepository.save(any(Book.class))).thenAnswer(invocation -&gt; {
            Book book = invocation.getArgument(0);
            Field idField = Book.class.getDeclaredField(&quot;id&quot;);
            idField.setAccessible(true);
            idField.set(book, 1L);
            return book;
        });
        // when
        Long savedId = bookService.registerBook(title, author, initialCount);
        // then
        assertThat(savedId).isEqualTo(1L);

        ArgumentCaptor&lt;Book&gt; captor = ArgumentCaptor.forClass(Book.class);
        verify(bookRepository, times(1)).save(captor.capture());

        Book savedBookArg = captor.getValue();
        assertThat(savedBookArg.getTitle()).isEqualTo(title);
        assertThat(savedBookArg.getAuthor()).isEqualTo(author);
        assertThat(savedBookArg.getStoredBooks().size()).isEqualTo(initialCount);
    }

    @Test
    @DisplayName(&quot;addStoredBooks: 기존 도서에 소장본을 추가하면 개수가 증가한다&quot;)
    void increasesCopyCount() {
        // given
        Book book = Book.registerNew(&quot;클린 코드&quot;, &quot;로버트 마틴&quot;, 1);
        Long bookId = 1L;

        when(bookRepository.findById(bookId)).thenReturn(Optional.of(book));
        // when
        bookService.addStoredBooks(bookId, 2);
        // then
        assertThat(book.getStoredBooks().size()).isEqualTo(3);
        verify(bookRepository, times(1)).findById(bookId);
        verify(bookRepository, never()).save(any());
    }
}</code></pre>
<h3 id="1-mock-생성-mock-1">1. Mock 생성 (@Mock)</h3>
<pre><code class="language-java">@Mock
private BookRepository bookRepository;</code></pre>
<p>실제 DB와 연결되는 진짜 <code>BookRepository</code>를 쓰지않고 <code>Mockito</code>가 만들어준 가짜 레포지토리를 테스트에 주입한다.</p>
<h3 id="2-stub---원하는-상황-만들기">2. Stub - 원하는 상황 만들기</h3>
<ul>
<li><p><code>findById</code>가 특정 <code>Book</code>을 반환하게 만들기</p>
<pre><code class="language-java">when(bookRepository.findById(bookId))
      .thenReturn(Optional.of(book));</code></pre>
</li>
<li><p><code>save()</code>가 <code>DB</code>처럼 ID를 채워 넣게 만들기 <strong>(thenAnswer)</strong></p>
</li>
</ul>
<p>이 부분이 처음 보면 가장 어렵다.</p>
<pre><code class="language-java">when(bookRepository.save(any(Book.class))).thenAnswer(invocation -&gt; {
    Book book = invocation.getArgument(0);
    Field idField = Book.class.getDeclaredField(&quot;id&quot;);
    idField.setAccessible(true);
    idField.set(book, 1L);
    return book;
});</code></pre>
<p>여기서 하고 싶은 건 단 하나다.</p>
<blockquote>
<p>JPA처럼 <code>save()</code>가 호출되면 <code>Book</code> 객체에 <code>id</code> 값이 자동으로 채워지도록 흉내내기.</p>
</blockquote>
<p>하지만 테스트에서는 <code>DB</code>가 없기 때문에 JPA가 실제로 <code>ID</code>를 넣어주지 않는다.
그래서 <strong>테스트 코드에서 직접 넣어줘야 하는데 그걸 가능하게 하는 것</strong>이 <code>thenAnswer</code>다.</p>
<p><code>thenAnswer</code>를 흐름을 보면</p>
<ul>
<li><code>invocation.getArgument(0)</code> : <code>save()</code>에 전달된 <code>Book</code> 객체</li>
<li><code>reflection</code>으로 id 필드 접근 가능하게 만들기 (<a href="https://velog.io/@s_miny/%EC%99%9C-JPA%EC%9D%98-Entity%EB%8A%94-%EA%B8%B0%EB%B3%B8-%EC%83%9D%EC%84%B1%EC%9E%90%EA%B0%80-%EB%B0%98%EB%93%9C%EC%8B%9C-%ED%95%84%EC%9A%94%ED%95%A0%EA%B9%8C#%EC%9E%A0%EA%B9%90-%EB%A6%AC%ED%94%8C%EB%A0%89%EC%85%98reflection%EC%9D%B4%EB%9E%80">리플렉션이 궁금하면 클릭</a>)</li>
<li>해당 <code>Book</code> 객체의 id 값을 <code>1L</code>로 강제 세팅하기</li>
<li>그 <code>Book</code>을 반환하기</li>
</ul>
<p>이렇게 단계를 나눠보면 단순하다.</p>
<p>즉, “<strong><code>save()</code>를 호출하면 <code>Book</code> 객체에 <code>id=1L</code>을 넣어서 반환해라</strong>” 라는 Custom 행동을 정의한 것이다.</p>
<h3 id="3-verify---서비스가-repository에-어떤-메시지를-보냈는지-검증">3. Verify - 서비스가 Repository에 어떤 메시지를 보냈는지 검증</h3>
<ul>
<li><p>서비스가 <code>save()</code>를 실제로 한 번 호출했는지 확인</p>
<pre><code class="language-java">verify(bookRepository, times(1)).save(any());</code></pre>
</li>
<li><p>호출되면 안 되는 경우 확인</p>
<pre><code class="language-java">verify(bookRepository, never()).save(any());</code></pre>
</li>
<li><p>전달된 <code>Book</code> 객체가 정확했는지 확인 <strong>(ArgumentCaptor)</strong></p>
<pre><code class="language-java">ArgumentCaptor&lt;Book&gt; captor = ArgumentCaptor.forClass(Book.class);
verify(bookRepository).save(captor.capture());
Book savedBook = captor.getValue();</code></pre>
<p>이 부분도 처음보면 햇갈릴 수 있다.</p>
</li>
</ul>
<p>이것을 사용하는 이유는 <code>Repository.save()</code>가 “호출되었다”만 검사하면 충분하지 않다.
그렇기에 우리는 <strong>서비스가 생성한 <code>Book</code> 객체의 값이 정확한지도 확인해야 한다.</strong></p>
<p>예를 들어 실제로:</p>
<ul>
<li><code>title</code>이 같은지</li>
<li><code>author</code>가 같은지</li>
<li>초기 소장본이 정확히 생성되었는지</li>
</ul>
<p>이런 값 검증은 <code>save()</code>에 전달된 <code>Book</code> 객체를 직접 꺼내서 확인해야 가능하다.</p>
<p>그래서 <strong>ArgumentCaptor</strong>가 등장한다.</p>
<p>즉, ArgumentCaptor 역할은 <code>Repository.save()</code>에 <strong>전달된 그 객체 그대로를 테스트 코드로 가져와서 검증할 수 있게 해주는 도구</strong></p>
<hr>
<h2 id="마무리">마무리</h2>
<p>예전에는 테스트 코드를 작성하지 않고 프로그램을 만들다 보니 “이게 왜 안 되지?”, “이게 왜 되지?”와 같은 혼란을 자주 겪곤 했다.
이번 경험을 통해 그런 상황을 막기 위해서는 결국 <strong>내가 의도한 대로 코드가 동작하는지 스스로 증명하는 과정</strong>이라는 것을 느꼈다.</p>
<p>서비스 레이어 테스트는 처음이라 낯설고 시간이 오래 걸렸지만 그 과정에서 얻은 배움은 예상보다 훨씬 크고 값졌다.
앞으로 더 복잡한 기능을 만들 때에도 이번 경험을 기반 삼아 <strong>테스트를 통해 설계를 검증하고 예측 가능한 코드를 작성하는 개발자</strong>로 계속 성장하고 싶다.</p>
<h2 id="참조">참조</h2>
<p><a href="https://khdscor.tistory.com/79">Springboot - 서비스 단위 테스트</a>
<a href="https://velog.io/@hellonayeon/spring-boot-service-layer-unit-testcode">[Spring Boot] Service 계층의 단위 테스트 코드 작성</a>
<a href="https://mangkyu.tistory.com/145">[Spring] JUnit과 Mockito 기반의 Spring 단위 테스트 코드 작성법 (3/3)</a>
<a href="https://velog.io/@gyeol9012/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%EB%B2%95">스프링 테스트코드 작성법</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 영속성 컨텍스트은 또 뭐야?]]></title>
            <link>https://velog.io/@s_miny/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%EC%9D%80-%EB%98%90-%EB%AD%90%EC%95%BC</link>
            <guid>https://velog.io/@s_miny/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%EC%9D%80-%EB%98%90-%EB%AD%90%EC%95%BC</guid>
            <pubDate>Fri, 14 Nov 2025 06:46:35 GMT</pubDate>
            <description><![CDATA[<h2 id="이-글을-쓰게-된-이유">이 글을 쓰게 된 이유</h2>
<p>이 글은 <a href="https://velog.io/@s_miny/JPA-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EA%B0%80-%EB%AD%90%EC%95%BC">JPA 연관관계가 뭐야?!</a> 라는 질문에서 출발했다.</p>
<p>우테코 오픈 미션을 진행하면서 JPA 연관관계를 공부하던 중 “엔티티끼리는 분명히 참조로 연결해두었는데 <strong>이게 정확히 어떤 시점에 DB에 반영되는 걸까?</strong>” 라는 궁금증이 생겼다.</p>
<p>특히 연관관계를 설정할 때 <strong>“JPA는 어떤 기준으로 엔티티의 상태를 추적하고, 언제 DB에 쓰는 걸까?”</strong> 이 부분이 명확히 이해되지 않았다.</p>
<p>이 궁금증을 따라가다 보니 자연스럽게<br><strong>영속성 컨텍스트(Persistence Context)</strong> 라는 개념과 마주하게 됐다.</p>
<p>처음에는 단순히 외워야 할 개념 정도로 생각했지만
공부할수록 이 개념이 <strong>JPA의 근본적인 동작 원리</strong>를 이해하는 핵심이라는 걸 깨달았다.</p>
<p>그래서 이 글은 “<strong>JPA가 어떻게 엔티티를 관리하며, 왜 그렇게 동작하는가</strong>”를 한눈에 볼 수 있도록 정리한 내용이다.</p>
<p>미래의 내가 다시 돌아왔을 때 “아, 그래서 JPA가 이렇게 작동하는 거였지”하고 바로 이해할 수 있도록 <strong>나를 위한 복습용 아카이브</strong>이기도 하다.</p>
<hr>
<h2 id="jpa-영속성-컨텍스트란">JPA 영속성 컨텍스트란?</h2>
<p>JPA의 모든 동작은 영속성 컨텍스트(Persistence Context)를 중심으로 이루어진다.
이 컨텍스트는 말 그대로 엔티티를 “영속” 상태로 관리하는 메모리 공간이다.
즉, <strong>엔티티를 단순히 생성하는 것이 아니라, JPA가 해당 객체를 추적하고 변경 사항을 관리하도록 등록하는 영역</strong>이다.</p>
<hr>
<h2 id="엔티티-생명주기-4단계">엔티티 생명주기 4단계</h2>
<p><img src="https://velog.velcdn.com/images/s_miny/post/24e8f5f5-0dfe-4cf5-b73d-2058431dba73/image.png" alt=""></p>
<p>JPA의 엔티티는 <code>New</code> → <code>Managed</code> → <code>Detached</code> → <code>Removed</code> 네 가지 상태를 가진다.
이 그림은 그 상태들이 어떤 메서드에 의해 이동하는지 그리고 JPA 내부에서 어떤 일이 일어나는지를 보여준다.</p>
<h3 id="new비영속-상태">New(비영속 상태)</h3>
<ul>
<li>단순히 <code>new</code>로 생성된 객체</li>
<li>아직 영속성 컨텍스트에 등록되지 않은 상태</li>
<li>JPA가 관리하지 않는다 → DB와 전혀 관련 없음</li>
</ul>
<pre><code class="language-java">Book book = new Book(&quot;자바의 정석&quot;, &quot;남궁성&quot;);</code></pre>
<p><strong>- 내부 동작</strong></p>
<ul>
<li>메모리 안에만 존재</li>
<li><code>EntityManager</code>는 이 객체를 전혀 모름</li>
<li><code>flush()</code>나 <code>commit()</code>과 무관</li>
</ul>
<p><code>new</code>로 만든 객체는 JPA 입장에서 그냥 일반 자바 객체일 뿐이다.</p>
<h3 id="영속managed">영속(Managed)</h3>
<p><code>em.persist(book)</code> 을 호출하면 비영속 → 영속 상태로 전환된다.
즉, 이제 <strong>JPA가 이 객체를 영속성 컨텍스트(1차 캐시) 안에서 관리</strong>한다.</p>
<pre><code class="language-java">em.persist(book);</code></pre>
<p><img src="https://velog.velcdn.com/images/s_miny/post/c3883a22-e66a-4578-b735-375e5054e1d1/image.png" alt=""></p>
<p><strong>- 내부 동작</strong></p>
<ul>
<li>JPA가 <code>book</code>을 1차 캐시(Map 구조) 에 등록</li>
<li><code>@Id(PK)</code> 값을 키로 관리</li>
<li><code>flush()</code> 시점에만 DB에 실제 <code>INSERT</code> 실행</li>
<li>이후부터 JPA는 <code>book</code>의 필드 변경을 감시(Dirty Checking)</li>
</ul>
<pre><code class="language-java">book.setTitle(&quot;자바의 정석 - 개정판&quot;); // 변경만 해도 JPA가 감지
em.flush(); // UPDATE SQL 자동 실행</code></pre>
<p><strong>영속 상태가 되면 엔티티의 변경이 SQL로 자동 반영된다.</strong></p>
<h3 id="detached준영속-상태">Detached(준영속 상태)</h3>
<p><code>detach()</code>, <code>clear()</code>, <code>close()</code>를 호출하면 <strong>영속성 컨텍스트에서 엔티티가 제거되어 더 이상 JPA가 관리하지 않는다</strong>.</p>
<pre><code class="language-java">em.detach(book); // 개별 엔티티 분리</code></pre>
<p><strong>- 내부 동작</strong></p>
<ul>
<li>1차 캐시에서 제거됨</li>
<li>변경 감지(Dirty Checking) 중단</li>
<li>이후에 필드를 수정해도 DB에는 반영되지 않음</li>
</ul>
<p><code>Detached</code>는 말 그대로 “<strong>영속성 컨텍스트와의 연결이 끊어진 상태</strong>”다.</p>
<p>다시 영속 상태로 복구하려면 <code>merge()</code>를 사용한다.</p>
<pre><code class="language-java">Book mergedBook = em.merge(book); // 준영속 → 영속</code></pre>
<p><code>merge()</code>는 기존 객체를 다시 관리 상태로 복구하지만 내부적으로는 새로운 영속 객체를 만들어 반환한다.</p>
<h3 id="removed삭제-상태">Removed(삭제 상태)</h3>
<p><code>em.remove(book)</code>을 호출하면 엔티티가 삭제 예약 상태가 된다.
즉시 삭제되지 않고, <code>flush</code> 시점에 <code>DELETE</code> SQL이 실행된다.</p>
<pre><code class="language-java">em.remove(book);</code></pre>
<p><strong>- 내부 동작</strong></p>
<ul>
<li>영속성 컨텍스트에서 해당 객체에 “삭제 플래그”를 표시</li>
<li><code>flush()</code> 또는 <code>commit()</code> 시 실제 <code>DELETE FROM book ...</code> 쿼리 실행</li>
<li>삭제 후 1차 캐시에서도 제거</li>
</ul>
<p><code>remove()</code>는 즉시 DB에서 지워지는 게 아니라 트랜잭션 커밋 시점에 반영된다.</p>
<h3 id="flush와-db의-관계">flush()와 DB의 관계</h3>
<p>그림에서 보듯 <code>Managed</code> 상태의 엔티티만이 DB와 직접적인 연결을 가진다.</p>
<p>JPA는 <code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code> 쿼리를 즉시 보내지 않고 &#39;<strong>쓰기 지연 SQL 저장소(Write-behind)</strong>&#39;라는 공간에 모아둔다.</p>
<pre><code class="language-java">em.persist(bookA);
em.persist(bookB);
// 아직 INSERT 없음

em.flush(); // INSERT 2개 SQL 한 번에 실행
em.commit(); // 트랜잭션 커밋</code></pre>
<ul>
<li><code>flush()</code> = 영속성 컨텍스트 ↔ DB 상태 동기화</li>
<li><code>commit()</code> = <code>flush()</code> + 실제 트랜잭션 종료</li>
</ul>
<hr>
<h2 id="그럼-스프링에서-persist는-언제-호출-되는가">그럼 스프링에서 persist는 언제 호출 되는가?</h2>
<p>영속성 컨텍스트를 이해하려면 하나 더 중요한 사실이 있다.
스프링 환경에서 우리가 흔히 사용하는</p>
<pre><code class="language-java">bookRepository.save(book);</code></pre>
<p>이 코드는 내부적으로 JPA의 <code>EntityManager.persist(book)</code> 또는 <code>merge(book)</code>를 호출한다.</p>
<ul>
<li>새 엔티티 → <code>persist()</code></li>
<li>기존 엔티티 → <code>merge()</code></li>
</ul>
<p>즉, <code>save()</code><strong>가 호출되는 시점이 엔티티가 비로소 영속(Managed) 상태가 되는 순간이다.</strong></p>
<hr>
<h2 id="마무리">마무리</h2>
<p>나는 예전에는 코드가 돌아가기만 하면 된다고 생각했었다.
하지만 이제는 “<strong>왜 그렇게 동작하는가?</strong>, <strong>그 안에서 어떤 원리가 작동하는가?</strong>”를 먼저 질문하게 되었다. </p>
<p>이 경험을 통해 우아한테크코스가 추구하는 <strong>이해를 기반으로 한 성장</strong>이라는 가치가 실제로 어떤 의미인지 직접 경험하게 되었다. </p>
<p>이제는 단순히 기능을 구현하는 개발자가 아니라 <strong>기능 뒤의 원리까지 스스로 설명할 수 있는 개발자</strong>로 성장하고 싶다는 마음이 생겼다.</p>
<h2 id="참조">참조</h2>
<p><a href="https://harrislee.tistory.com/104">스프링 영속성 관리 (영속성 컨텍스트)</a>
<a href="https://velog.io/@neptunes032/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%EB%9E%80">JPA 영속성 컨텍스트란?</a>
<a href="https://ittrue.tistory.com/254#google_vignette">[JPA] 영속성 컨텍스트(Persistence Context)란? - 개넘 정리 및 사용법
</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 연관관계가 뭐야?!]]></title>
            <link>https://velog.io/@s_miny/JPA-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EA%B0%80-%EB%AD%90%EC%95%BC</link>
            <guid>https://velog.io/@s_miny/JPA-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EA%B0%80-%EB%AD%90%EC%95%BC</guid>
            <pubDate>Fri, 14 Nov 2025 06:14:58 GMT</pubDate>
            <description><![CDATA[<h2 id="이-글을-쓰게-된-이유">이 글을 쓰게 된 이유</h2>
<p>이번 프로젝트에서 <code>StoredBook</code>, <code>StoredBooks</code>, <code>Reservation</code>, <code>Reservations</code>를 먼저 구현하고 이제 <code>Book</code>을 만들던 중 고민이 생겼다.</p>
<p>예를 들어 “자바의 정석 - 남궁성”이라는 책이 있다고 하면 이 책은 여러 권의 소장본을 가질 수 있다. 그렇다면 관리자가 <strong>책을 등록할 때 이 책의 소장본들을 함께 등록해야 할 텐데 그 과정을 코드로 어떻게 표현</strong>해야 할지 막막했다.</p>
<p>“책이 여러 소장본을 가진다”는 건 너무 자연스러운 문장이지만 막상 구현하려 하니 책과 소장본 중 어느 쪽이 관계를 맺고 누가 누구를 생성해야 하는지가 불분명했다. 그래서 *<em>처음에는 단순하게 <code>StoredBook</code>이 <code>Book</code>을 주입받아 관계를 맺는 방식을 떠올렸다. *</em> 즉, 소장본이 자신이 속한 책을 알고 있으면 되지 않을까? 하는 생각이었다.</p>
<p>하지만 곧 “<strong>이 방식이 데이터베이스에서도 올바르게 연관관계로 적용될까?</strong>”라는 의문이 생겼다. 그 이유는 내가 객체 간 관계를 단순히 자바 코드 내부의 참조 수준으로만 이해하고 있었기 때문이다. 객체 간 연결이 실제로 DB에서는 외래키로 어떻게 저장되고 관리되는지 몰랐다.</p>
<p>즉, “<strong>객체 간의 연결이 실제 데이터로 어떻게 매핑되는지”에 대한 이해가 부족했다.</strong> 
이 깨달음이 나를 JPA 연관관계 매핑 공부로 이끌었다.</p>
<p>결국 이 글은 단순히 JPA 연관관계를 정리하기 위한 글이 아니라 “이해하지 못한 채 코드를 작성했던 과거의 나”를 돌아보고 앞으로 더 나은 설계를 하기 위한 성장의 기록이다.</p>
<hr>
<h2 id="jpa-연관관계-매핑이란">JPA 연관관계 매핑이란?</h2>
<p>객체 모델의 참조(Reference)와 데이터베이스의 외래키(Foreign Key)를 <strong>논리적으로 연결하는 기술</strong>이다.</p>
<p>간단히 말하면 <strong>자바 객체끼리는 “참조(Reference)”</strong>로 연결되고 <strong>데이터베이스 테이블은 “외래키(Foreign Key)”</strong>로 연결된다.</p>
<p>이 둘은 표현 방식이 다르기 때문에 JPA가 두 세계를 자동으로 변환(매핑) 해주는 역할을 한다.</p>
<p>JPA에서 연관관계를 매핑할 때는 3가지 핵심 개념을 반드시 고려해야 한다.</p>
<ol>
<li><strong>방향 (Direction)</strong></li>
<li><strong>다중성 (Multiplicity)</strong></li>
<li><strong>연관관계의 주인 (Owner)</strong></li>
</ol>
<hr>
<h3 id="1-방향direction---단방향-vs-양방향">1. 방향(Direction) - 단방향 vs 양방향</h3>
<p><strong>객체는 참조를 통해 서로를 바라본다.</strong> 하지만 <strong>DB 테이블은 항상 양방향으로 참조할 수 없다.</strong>
그래서 객체 간 “참조 방향”은 설계자가 명시적으로 정해야 한다.</p>
<h3 id="2-다중성multiplicity---일대일-일대다-다대일-다대다">2. 다중성(Multiplicity) - 일대일, 일대다, 다대일, 다대다</h3>
<p>연관관계는 실제 데이터 모델링 구조와 직결된다.</p>
<ul>
<li><strong>1:1 (OneToOne)</strong> → 예: 여권 ↔ 사람</li>
<li><strong>1:N (OneToMany)</strong> → 예: 책 ↔ 소장본</li>
<li><strong>N:1 (ManyToOne)</strong> → 예: 소장본 ↔ 책</li>
<li><strong>N:N (ManyToMany)</strong> → 예: 학생 ↔ 과목 (하지만 실무에서는 중간 테이블로 풀어낸다)</li>
</ul>
<h3 id="3-연관관계의-주인-owner">3. 연관관계의 주인 (Owner)</h3>
<p>JPA에서 “주인”이라는 개념은 객체지향보다 데이터베이스 설계 관점에 가깝다.
<strong>연관관계의 주인 = 외래키를 직접 가지고 있는 엔티티</strong>이다.</p>
<ul>
<li><code>@JoinColumn</code>이 붙은 쪽이 주인</li>
<li>주인만이 <code>DB</code>의 외래키 값을 변경할 수 있다.</li>
<li>반대편(<code>mappedBy</code>)은 읽기 전용이다.</li>
</ul>
<p>내 코드를 예시로 보이겠다.</p>
<p><strong>- 주인 코드</strong></p>
<pre><code class="language-java">@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = &quot;book_id&quot;, nullable = false)
private Book book;</code></pre>
<ul>
<li><p><code>name = &quot;book_id&quot;</code> : <code>stored_book</code> 테이블에 생성될 실제 외래키 컬럼 이름을 <code>book_id</code>로 지정한다.</p>
</li>
<li><p><code>nullable = false</code> : 외래키 컬럼(<code>book_id</code>)이 <code>null</code>이면 안 된다는 뜻이다. 즉, 모든 <code>StoredBook</code>은 반드시 <code>Book</code>에 속해야 한다.</p>
</li>
<li><p><code>fetch</code>는 뒤에서 따로 다루도록 하겠다.</p>
</li>
</ul>
<p><strong>- 비주인 코드</strong></p>
<pre><code class="language-java">@OneToMany(mappedBy = &quot;book&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
private List&lt;StoredBook&gt; storedBooks = new ArrayList&lt;&gt;();</code></pre>
<ul>
<li><p><code>mappedBy = book</code> : 
이 코드는 <code>Book</code>이 <strong>JPA 연관관계의 비주인이라는 선언</strong>이다. 
외래키는<code>StoredBook</code> 테이블에 존재하기 때문에 <code>Book</code> 엔티티는 <strong>단순히 읽기 전용으로 관계를 본다</strong>는 뜻이다.</p>
</li>
<li><p><code>cascade = CascadeType.ALL</code> : 
이건 <strong>영속성 전이(Persistence Cascade) 개념</strong>이다.
<code>Book</code>이 영속화되면 그 안에 포함된 모든 <code>StoredBook</code>도 자동으로 영속 상태가 된다.</p>
</li>
<li><p><code>orphanRemoval = true</code> : 
이건 말 그대로 <strong>고아 객체 제거(Orphan Removal) 기능</strong>이다.
<code>Book</code>과 <code>StoredBook</code>의 관계가 끊어지면 해당 <code>StoredBook</code> 엔티티를 자동으로 DB에서 <code>DELETE</code> 한다.</p>
</li>
</ul>
<hr>
<h2 id="fetch-전략--언제-데이터를-로딩할까">Fetch 전략 — 언제 데이터를 로딩할까?</h2>
<h3 id="--lazy지연-로딩">- LAZY(지연 로딩)</h3>
<pre><code class="language-java">@ManyToOne(fetch = FetchType.LAZY)
private Book book;</code></pre>
<p><strong>JPA는 실제로 <code>Book</code>이 필요할 때까지 DB 쿼리를 날리지 않는다.</strong>
이 상태에서는 <code>book</code> 필드에 <strong>프록시 객체(가짜 객체)가 들어있고 *<em><code>book.getTitle()</code>이 *</em>호출되는 시점에 <code>SELECT</code> 쿼리가 실행</strong>된다.</p>
<ul>
<li>장점: 성능 최적화, 필요할 때만 로딩</li>
<li>단점: 영속성 컨텍스트 밖에서 접근하면 <code>LazyInitializationException</code> 발생 가능</li>
</ul>
<p>즉, <code>LAZY</code>는 기본이고, 필요한 시점에만 <code>fetch join</code>으로 조정하는 것이 실무 표준이다.</p>
<h3 id="--eager즉시-로딩">- EAGER(즉시 로딩)</h3>
<pre><code class="language-java">@ManyToOne(fetch = FetchType.EAGER)
private Book book;</code></pre>
<p>엔티티를 조회할 때마다 항상 <code>JOIN</code> 쿼리로 함께 가져온다.
편리하지만, 관계가 많을수록 성능이 급격히 떨어지고 N+1 쿼리 문제가 쉽게 발생한다.</p>
<hr>
<h2 id="내-코드-전후-비교">내 코드 전후 비교</h2>
<h3 id="이전-코드실패-원인">이전 코드(실패 원인)</h3>
<pre><code class="language-java">public class StoredBook {

    private long id;
    private StoredBookStatus status;

    private Book book;

    private StoredBook(Book book, StoredBookStatus status) {
        this.book = book;
        this.status = status;
    }

    public static StoredBook createAvailable(Book book) {
        return new StoredBook(book, StoredBookStatus.AVAILABLE);
    }

    public static StoredBook createOnHold(Book book) {
        return new StoredBook(book, StoredBookStatus.ON_HOLD);
    }
}</code></pre>
<p>당시에는 생성자에서 <code>this.book = book</code>으로 연결하면 “<strong>자바 객체에서 연결되니 DB에서도 자동으로 외래키가 저장되겠지?</strong>”라고 생각했다.</p>
<p>하지만 실제 실패 원인은 생성자 방식이 아니라 연관관계 매핑 어노테이션이 없었다는 점이었다.</p>
<h3 id="변경-후-코드정상-작동">변경 후 코드(정상 작동)</h3>
<pre><code class="language-java">@Entity
public class StoredBook {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Enumerated(EnumType.STRING)
    private StoredBookStatus status;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;book_id&quot;, nullable = false)
    private Book book;

    protected StoredBook() {}

    private StoredBook(Book book, StoredBookStatus status) {
        this.book = book;
        this.status = status;
    }

    public static StoredBook createAvailable(Book book) {
        return new StoredBook(book, StoredBookStatus.AVAILABLE);
    }

    public static StoredBook createOnHold(Book book) {
        return new StoredBook(book, StoredBookStatus.ON_HOLD);
    }
}</code></pre>
<p>이제 JPA는 다음을 정확히 인식한다.</p>
<ul>
<li>이 필드는 <code>Book</code>과의 다대일 연관관계다.</li>
<li>실제 DB 컬럼은 <code>book_id</code>로 만들어야 한다.</li>
<li><code>StoredBook</code>이 <code>persist</code>될 때 <code>book_id</code> FK를 자동으로 세팅해야 한다.</li>
</ul>
<p>그래서 DB에도 정확히 외래키가 저장된다.</p>
<hr>
<h2 id="새롭게-궁금해진-점">새롭게 궁금해진 점</h2>
<p>연관관계 매핑을 공부하면서 자연스럽게 이런 의문이 들었다.</p>
<blockquote>
<p>“JPA는 도대체 어떤 시점에 어떤 기준으로 엔티티의 필드 값을 DB에 반영할까?”</p>
</blockquote>
<p>이 과정을 따라가다 보니 처음으로 <strong>영속성 컨텍스트(Persistence Context)</strong>라는 개념에 도달하게 되었다.</p>
<p>자세한 동작 원리는 이 글의 범위를 넘어서기에 다음 글에 이어서 작성하겠다.</p>
<p><a href="https://velog.io/@s_miny/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%EC%9D%80-%EB%98%90-%EB%AD%90%EC%95%BC">JPA 영속성 컨텍스트는 또 뭐야..?</a></p>
<hr>
<h2 id="참조">참조</h2>
<p><a href="https://ws-pace.tistory.com/221">Spring Data JPA 정리3_연관관계 매핑(일대일, 다대일, 일대다, 다대다)</a>
<a href="https://jaeseo0519.tistory.com/133">[Spring Boot] Concept part - JPA 연관관계 매핑 기초</a>
<a href="https://amaran-th.github.io/Spring/%5BJPA%5D%20%EC%97%B0%EA%B4%80%20%EA%B4%80%EA%B3%84%20%EB%A7%A4%ED%95%91/">[JPA] 연관 관계 매핑</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[java.time을 다시 만나다.]]></title>
            <link>https://velog.io/@s_miny/java.time-%EC%9D%84-%EB%8B%A4%EC%8B%9C-%EB%A7%8C%EB%82%98%EB%8B%A4</link>
            <guid>https://velog.io/@s_miny/java.time-%EC%9D%84-%EB%8B%A4%EC%8B%9C-%EB%A7%8C%EB%82%98%EB%8B%A4</guid>
            <pubDate>Sun, 09 Nov 2025 14:21:14 GMT</pubDate>
            <description><![CDATA[<h2 id="javatime을-쓰게-된-계기">java.time을 쓰게 된 계기</h2>
<p>우테코 오픈미션을 하던 중 예약 기능을 만들면서 &quot;보류 기한은 3일&quot;이라는 정책이 생겼다. 처음엔 단순히 <code>int day = 3;</code> 이런 식으로 처리할까 했지만 이건 그냥 숫자일 뿐 &#39;시간&#39;이라는 의미를 표현하지 못한다는 점이 마음에 걸렸다.</p>
<p>그래서 시간을 표현하는 방법을 고민하던 중<code>java.time</code> 패키지를 떠올렸다.</p>
<p>이번 여름방학 때 『자바의 정석』을 읽으며 <code>LocalDateTime</code>이나 <code>Duration</code>같은 클래스 이름은 한 번쯤 본 기억이 있었다. 하지만 그땐 단순히 &quot;아 이런 게 있구나&quot;정도로만 읽었지 내가 이걸 직접 프로젝트에서 쓰게 될 줄은 정말 몰랐다.</p>
<p>이번엔 단순히 읽고 지나가는 게 아니라 책에서 본 개념을 직접 코드로 써보며 배우는 진짜 도전이었다.</p>
<p>이 글에서는 내가 공부한 <code>java.time</code>의 일부 핵심 개념들과 그것을 내 코드에 적용하면서 느꼈던 생각과 배움을 함께 풀어보려 한다.</p>
<hr>
<h2 id="javatime이란">java.time이란?</h2>
<p><code>java.time</code> 패키지는 자바 8부터 추가된 현대적 시간 API다.
날짜, 시간, 시점, 시간대 같은 개념을 분리해 더 명확하고 테스트 가능한 방식으로 다룰 수 있게 해준다.</p>
<p>여러가지 클래스가 있지만 내가 프로젝트에 직접적으로 사용한 클래스들을 중점으로 내용을 정리했다.</p>
<h3 id="localdatetime---지역-기준의-날짜와-시간">LocalDateTime - 지역 기준의 날짜와 시간</h3>
<blockquote>
<p>&quot;2025-11-09 22:30&quot; 같은 형태로 사람이 인식할 수 있는 시간을 표현한다.</p>
</blockquote>
<ul>
<li><p><strong>생성</strong></p>
<ul>
<li><code>now()</code> / <code>now(Clock)</code></li>
<li><code>of(year, month, day, hour, minute[, second[, nano]])</code></li>
<li><code>ofInstant(Instant, ZoneId)</code></li>
</ul>
</li>
<li><p><strong>연산</strong></p>
<ul>
<li><code>plusDays</code>/<code>Hours</code>/<code>Minutes</code>/<code>Seconds(...)</code></li>
<li><code>plus(Duration)</code> / <code>minus(Duration)</code></li>
<li><code>withYear</code>/<code>Month</code>/<code>DayOfMonth</code>/<code>Hour</code>/<code>Minute</code>/<code>Second(...)</code></li>
</ul>
</li>
<li><p><strong>비교</strong></p>
<ul>
<li><code>isBefore</code>/ <code>isAfter(ChronoLocalDateTime)</code></li>
<li><code>isEqual(ChronoLocalDateTime)</code></li>
</ul>
</li>
<li><p><strong>변환/포맷</strong></p>
<ul>
<li><code>atZone(ZoneId)</code></li>
<li><code>toLocalDate()</code> / <code>toLocalTime()</code></li>
<li><code>format(DateTimeFormatter)</code></li>
</ul>
</li>
<li><p><strong>주의</strong>
  <strong>타임존이 없다.</strong> 타임존 필요하면 <code>atZone(ZoneId)</code> 또는 <code>ZonedDateTime</code> 사용.</p>
</li>
</ul>
<p>다음 코드는 사용 예시이다.</p>
<pre><code class="language-java">LocalDateTime now = LocalDateTime.now(); // 현재 시각
LocalDateTime threeDaysLater = now.plusDays(3); // 3일 뒤
LocalDateTime custom = LocalDateTime.of(2025, 11, 9, 22, 30); // 직접 생성</code></pre>
<h3 id="duration---시간-간격">Duration - 시간 간격</h3>
<blockquote>
<p>&quot;얼마 동안 지속되는가?&quot;를 표현하는 클래스</p>
</blockquote>
<ul>
<li><p><strong>생성</strong></p>
<ul>
<li><code>ofDays</code>/<code>Hours</code>/<code>Minutes</code>/<code>Seconds</code>/<code>Millis</code>/<code>Nanos(...)</code></li>
<li><code>between(Temporal startInclusive, Temporal endExclusive)</code></li>
</ul>
</li>
<li><p><strong>연산</strong></p>
<ul>
<li><code>plus(...)</code> / <code>minus(...)</code></li>
<li><code>multipliedBy(long)</code> / <code>dividedBy(long)</code></li>
<li><code>isZero()</code> / <code>isNegative()</code> / <code>isPositive()</code></li>
</ul>
</li>
<li><p><strong>사용패턴</strong></p>
<ul>
<li><code>LocalDateTime.plus(Duration)</code> / <code>minus(Duration)</code></li>
<li><code>Instant.plus(Duration)</code> / <code>minus(Duration)</code></li>
</ul>
</li>
<li><p><strong>주의</strong>
  <strong>달/년</strong>은 가변 길이라 <code>Duration</code>에 없다. (그럴 땐 <code>Period</code>)</p>
</li>
</ul>
<p>다음 코드는 사용 예시이다.</p>
<pre><code class="language-java">Duration threeDays = Duration.ofDays(3);    // 3일
Duration twoHours = Duration.ofHours(2);    // 2시간
LocalDateTime holdUntil = LocalDateTime.now().plus(threeDays); // 지금 + 3일</code></pre>
<h3 id="clock---현재-시각-공급자테스트-주입용">Clock - 현재 시각 공급자(테스트 주입용)</h3>
<blockquote>
<p>&quot;지금 몇 시인지&quot;를 직접 코드에 의존하지 않게 해준다.</p>
</blockquote>
<ul>
<li><p><strong>생성</strong></p>
<ul>
<li><code>systemDefaultZone()</code> / <code>system(ZoneId)</code></li>
<li><code>fixed(Instant, ZoneId)</code> ← 테스트용 고정 시계</li>
<li><code>offset(Clock, Duration)</code> ← 기준 시계에서 오프셋</li>
</ul>
</li>
<li><p><strong>사용</strong></p>
<ul>
<li><code>LocalDateTime.now(clock)</code></li>
<li><code>Instant.now(clock)</code></li>
</ul>
</li>
<li><p><strong>주의</strong>
  운영은 <code>systemDefaultZone()</code> 같은 실시간 시계
  테스트는 <code>fixed(...)</code>로 시간을 멈춘다.</p>
</li>
</ul>
<p>다음 코드는 사용 예시이다.</p>
<pre><code class="language-java">Clock systemClock = Clock.systemDefaultZone(); // 실제 시스템 시계
Clock fixedClock = Clock.fixed(
                LocalDateTime.of(2025, 11, 12, 10, 0)
                .toInstant(ZoneOffset.UTC), ZoneId.of(&quot;UTC&quot;)); // 테스트용 고정 시계
LocalDateTime now = LocalDateTime.now(systemClock);</code></pre>
<h3 id="instant---utc-기준-절대-시점">Instant - UTC 기준 절대 시점</h3>
<blockquote>
<p>&quot;세계 공통의 지금 이 순간&quot;을 나타냄.</p>
</blockquote>
<ul>
<li><p><strong>생성</strong></p>
<ul>
<li><code>now()</code> / <code>now(Clock)</code></li>
<li><code>ofEpochSecond(long)</code> / <code>ofEpochMilli(long)</code></li>
</ul>
</li>
<li><p><strong>연산</strong></p>
<ul>
<li><code>plus(Duration)</code> / <code>minus(Duration)</code></li>
<li><code>isBefore</code>/ <code>isAfter(Instant)</code></li>
</ul>
</li>
<li><p><strong>변환</strong></p>
<ul>
<li><code>LocalDateTime.ofInstant(Instant, ZoneId)</code></li>
<li><code>ZonedDateTime.ofInstant(Instant, ZoneId)</code></li>
</ul>
</li>
<li><p><strong>주의</strong>
  <strong>사람 친화적 포맷 아님</strong> → 화면/도메인에는 보통 <code>LocalDateTime</code>로 변환</p>
</li>
</ul>
<p>다음 코드는 사용 예시이다.</p>
<pre><code class="language-java">Instant now = Instant.now(clock);          // 절대 시각
Instant after = now.plus(Duration.ofDays(3)); // 3일 뒤 절대 시각</code></pre>
<h3 id="zoneid---시간대">ZoneId - 시간대</h3>
<blockquote>
<p>&quot;어느 지역 기준으로 시간을 계산할 것인가?&quot;를 결정한다.</p>
</blockquote>
<ul>
<li><p><strong>얻기</strong></p>
<ul>
<li><code>of(&quot;Asia/Seoul&quot;)</code>, <code>of(&quot;UTC&quot;)</code></li>
<li><code>systemDefault()</code> ← 서버의 기본 시간대</li>
</ul>
</li>
<li><p><strong>사용</strong></p>
<ul>
<li><code>LocalDateTime.ofInstant(instant, zoneId)</code></li>
<li><code>LocalDateTime.atZone(zoneId)</code></li>
</ul>
</li>
<li><p><strong>주의</strong>
  <strong>서버/배포 환경의 기본 타임존이 바뀌면</strong> <code>systemDefault()</code> <strong>결과가 달라질 수 있다.</strong>
정책적으로 고정하려면 <code>ZoneId.of(&quot;Asia/Seoul&quot;)</code>을 명시.</p>
</li>
</ul>
<p>다음 코드는 사용 예시이다.</p>
<pre><code class="language-java">ZoneId seoulZone = ZoneId.of(&quot;Asia/Seoul&quot;);
LocalDateTime seoulTime = LocalDateTime.ofInstant(Instant.now(), seoulZone);</code></pre>
<hr>
<h2 id="내-reservation-코드에-적용">내 Reservation 코드에 적용</h2>
<p>공부한 내용을 실제 코드에 녹였다.
예약 관련된 모든 곳에 많이 사용되었지만 일부만 보여주겠다. 
그중에서 예약 선두를 찾아 보류(<code>HOLD_READY</code>) 상태로 전환하고 3일 뒤 만료 시각을 계산하는 메서드를 보여주겠다.</p>
<pre><code class="language-java">public void promoteHeadToHoldReady(Long storedBookId, Duration holdDuration, Clock clock) {
    // 1) 선두(QUEUED) 예약 찾기
    Reservation head = reservations.stream()
            .filter(Reservation::isQueued)
            .findFirst()
            .orElseThrow(() -&gt; new IllegalStateException(&quot;대기 중인 예약이 없습니다.&quot;));

    // 2) 현재(절대 시각) + 보류 기간(정책) → 만료 시각 계산
    Instant now = Instant.now(clock);

    LocalDateTime holdUntil = LocalDateTime.ofInstant(
            now.plus(holdDuration),
            ZoneId.systemDefault()
    );

    // 3) 예약 상태 전이: HOLD_READY + 소장본/기한 지정
    head.prepareHold(storedBookId, holdUntil);
}</code></pre>
<p>코드의 이해를 돕기 위해 추가로 정보를 주겠다.</p>
<ul>
<li><code>Duration</code> = 보류 정책(3일)</li>
<li><code>Clock</code> = 현재 시각 주입(테스트에서 고정 가능)</li>
</ul>
<hr>
<h2 id="마무리">마무리</h2>
<p>이번 오픈 미션을 통해 또 한 번 새로운 도전을 하게 되었다.
여름방학 때 책으로만 봤던 <code>java.time</code>이 단순한 개념이 아니라
실제 코드 속에서 살아 움직이는 도구로 쓰이게 된 것이다.</p>
<p>그때는 “이런 게 있구나” 하고 가볍게 넘겼던 개념이 지금은 내 도메인의 언어가 되어 있었다.
이 과정에서 <code>java.time</code>을 더 깊이 이해하게 된 것도 좋았지만 그보다 더 크게 느낀 건 <strong>“배움에 헛된 건 하나도 없다”</strong>는 사실이었다.</p>
<p>자바의 정석을 읽을 때 나는 <code>java.time</code>을 대충 훑고 지나쳤다.
그때 조금만 더 집중해서 공부했더라면 이번 미션이 더 수월했을지도 모르겠다는 생각이 든다. 하지만 동시에 이렇게 새로운 도전 속에서 다시 배우게 된 경험이 나중에 <code>java.time</code>을 사용할 때 훨씬 큰 자산이 될 거라 믿는다.</p>
<p>앞으로 무엇을 공부하든, 어떤 경험을 하든 그 어느 것도 헛된 시간은 없다는 마음으로 나아가고 싶다.</p>
<p>마음 한켠에 이 말을 꼭 가지고 다녀야겠다.</p>
<blockquote>
<p>“<strong>모든 배움은 결국 어딘가에서 연결된다.</strong>”</p>
</blockquote>
<h2 id="참고">참고</h2>
<p><a href="https://kephilab.tistory.com/107">16. Java 자바 [API] - java.time 패키지</a>
<a href="https://cocococo.tistory.com/entry/Java-%EB%82%A0%EC%A7%9C-%EB%B0%8F-%EC%8B%9C%EA%B0%84-API-Date-and-Time-API-%EC%82%AC%EC%9A%A9-%EB%B0%A9%EB%B2%95">[Java] 날짜 및 시간 API (Date and Time API) 사용 방법</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[왜 JPA의 Entity는 기본 생성자가 반드시 필요할까?]]></title>
            <link>https://velog.io/@s_miny/%EC%99%9C-JPA%EC%9D%98-Entity%EB%8A%94-%EA%B8%B0%EB%B3%B8-%EC%83%9D%EC%84%B1%EC%9E%90%EA%B0%80-%EB%B0%98%EB%93%9C%EC%8B%9C-%ED%95%84%EC%9A%94%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@s_miny/%EC%99%9C-JPA%EC%9D%98-Entity%EB%8A%94-%EA%B8%B0%EB%B3%B8-%EC%83%9D%EC%84%B1%EC%9E%90%EA%B0%80-%EB%B0%98%EB%93%9C%EC%8B%9C-%ED%95%84%EC%9A%94%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Sat, 08 Nov 2025 08:59:56 GMT</pubDate>
            <description><![CDATA[<p>이 글은 우테코 오픈미션을 진행하던 도중 예상치 못한 JPA 오류를 마주하고 그 원인과 학습 내용을 정리한 글이다.</p>
<p>처음엔 단순히 “코드가 잘못된 건가?” 정도로 생각했지만 결국 JPA가 내부적으로 객체를 어떻게 생성하는지를 이해하게 된 계기가 되었다.</p>
<hr>
<h2 id="갑자기-뜬-오류">갑자기 뜬 오류?</h2>
<p><img src="https://velog.velcdn.com/images/s_miny/post/30553d9b-47ed-4cc1-8bbd-c9e94cd3ebcf/image.png" alt=""></p>
<p>개발중에 갑자기 이런 오류 문구가 떴다. 분명 나는 잘 작성한거 같은데..라는 생각으로 눌러서 확인을 해봤다.</p>
<p><img src="https://velog.velcdn.com/images/s_miny/post/cab15bcb-ed82-424c-88b1-039a2a992393/image.png" alt="">
원인은 StoredBook에 public 또는 protected 기본 생성자가 있어야 한다는 뜻이였다.</p>
<p>그래서 내 코드를 확인해 보았다.</p>
<pre><code class="language-java">package smiinii.object_oriented_library.domain;

import jakarta.persistence.*;

@Entity
public class StoredBook {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Enumerated(EnumType.STRING)
    private StoredBookStatus status;

    private long bookId;

    private StoredBook(long bookId, StoredBookStatus status) {
        this.bookId = bookId;
        this.status = status;
    }

    public static StoredBook createAvailable(long bookId) {
        return new StoredBook(bookId, StoredBookStatus.AVAILABLE);
    }

    public static StoredBook createOnHold(long bookId) {
        return new StoredBook(bookId, StoredBookStatus.ON_HOLD);
    }
}</code></pre>
<p>역시나 기본 생성자가 없었다. 근데 저는 기본 생성자가 없어도 다른 생성자들이 있으니깐 상관없는거 아니야?라는 생각이였다. 순수 자바로 코딩을 할 때는 기본 생성자가 없어도 작동에 문제가 없었는데 JPA를 사용하니 오류가 생긴 것이였다.</p>
<hr>
<h2 id="그럼-왜-jpa는-기본-생성자가-필요할까">그럼 왜 JPA는 기본 생성자가 필요할까?</h2>
<h3 id="순수-자바">순수 자바</h3>
<p>순수 자바에서는 개발자가 <code>new</code>로 직접 만든다.</p>
<pre><code>StoredBook storedBook = new StoredBook(1L, StoredBookStatus.AVAILABLE);</code></pre><p>프로그램이 이 코드를 직접 실행해서 객체를 생성하기 때문에 기본 생성자가 없어도 아무 문제가 없다.</p>
<h3 id="jpa">JPA</h3>
<p>하지만 JPA는 DB에서 데이터를 꺼낼 때 리플렉션(Reflection)이라는 기술로 객체를 생성한다.
즉, <code>new StoredBook()</code>을 우리가 호출하지 않고 JPA 내부 코드가 자동으로 객체를 만드는 것이다.</p>
<pre><code class="language-java">// JPA 내부 동작 개념
Class&lt;?&gt; clazz = StoredBook.class;
Object entity = clazz.getDeclaredConstructor().newInstance();</code></pre>
<p>이렇게 호출할 때 기본 생성자가 없으면 JPA가 객체를 만들 수 없다. 그래서 내 코드에서 오류가 난 것이다.</p>
<h3 id="잠깐-리플렉션reflection이란">잠깐, 리플렉션(Reflection)이란?</h3>
<p>리플렉션은 프로그램이 자기 자신을 분석하고 조작할 수 있는 자바의 기능이다.
즉 <strong>클래스 이름만 알고 있어도 런타임에 그 클래스의 필드나 메서드, 생성자 등에 접근할 수 있다.</strong></p>
<p>예를 들어 JPA는 아래처럼 동작한다.</p>
<pre><code class="language-java">Class&lt;?&gt; clazz = Class.forName(&quot;smiinii.object_oriented_library.domain.StoredBook&quot;);
Object entity = clazz.getDeclaredConstructor().newInstance();</code></pre>
<p>위 코드는 “클래스 이름 문자열”로부터 실제 객체를 만드는 과정이다.
이렇게 <strong>리플렉션을 사용하면 new 없이도 객체를 생성할 수 있지만 그 전제 조건이 바로 ‘기본 생성자가 존재해야 한다’</strong>는 점이다.</p>
<h3 id="기본-생성자는-protected가-좋다">기본 생성자는 protected가 좋다?</h3>
<p>기본 생성자를 <code>public</code>으로 두면 외부에서 마음대로 사용할 수 있다.
이건 도메인 규칙을 깨뜨릴 위험이 있다. 
그래서 외부 코드가 함부로 <code>new</code> 하지 못하게 막으면서 JPA 내부에서는 리플렉션으로 접근할 수 있도록 <code>protected StoredBook()</code>이라는 형태를 권장한다.</p>
<hr>
<h2 id="수정한-코드">수정한 코드</h2>
<pre><code class="language-java">package smiinii.object_oriented_library.domain;

import jakarta.persistence.*;

@Entity
public class StoredBook {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Enumerated(EnumType.STRING)
    private StoredBookStatus status;

    private long bookId;

    protected StoredBook() {}

    private StoredBook(long bookId, StoredBookStatus status) {
        this.bookId = bookId;
        this.status = status;
    }

    public static StoredBook createAvailable(long bookId) {
        return new StoredBook(bookId, StoredBookStatus.AVAILABLE);
    }

    public static StoredBook createOnHold(long bookId) {
        return new StoredBook(bookId, StoredBookStatus.ON_HOLD);
    }

    public void loan() {
        if (status == StoredBookStatus.LOANED) {
            throw new IllegalStateException(&quot;이미 대출 중입니다.&quot;);
        }
        if (status == StoredBookStatus.ON_HOLD) {
            throw new IllegalStateException(&quot;다른 회원이 예약 중입니다.&quot;);
        }
        this.status = StoredBookStatus.LOANED;
    }

    public void returnBook(boolean isReservation) {
        if (status != StoredBookStatus.LOANED) {
            throw new IllegalStateException(&quot;대출 중인 도서만 반납할 수 있습니다.&quot;);
        }
        if (isReservation) {
            this.status = StoredBookStatus.ON_HOLD;
            return;
        }
        this.status = StoredBookStatus.AVAILABLE;
    }

    public StoredBookStatus getStatus() {
        return status;
    }
}</code></pre>
<hr>
<h2 id="마무리">마무리</h2>
<p>이번 오류를 단순히 “기본 생성자가 없어서 생긴 문제”로 끝내지 않고 왜 필요한지를 직접 찾아보고 이해하는 과정에서 프레임워크(JPA)가 내부에서 어떻게 동작하는지를 조금 더 깊게 볼 수 있었다.</p>
<p>이번 경험을 통해 <strong>JPA가 객체를 생성하고 관리하는 방식(리플렉션 기반)을 이해하게 되었고</strong> “코드를 내가 작성하지 않아도 내부에서는 어떤 일이 일어나는가”를 한 단계 더 생각하게 되었다.</p>
<p>앞으로는 <strong>단순히 오류를 해결하는 것에 그치지 않고 그 원리를 직접 확인하고 정리하는 습관을 계속 유지하고 싶다.</strong>
이런 과정을 반복하다 보면 단순히 코드를 “사용하는 개발자”가 아니라 “이해하고 설명할 수 있는 개발자”로 성장할 수 있을 거라 생각한다.</p>
<h2 id="참고">참고</h2>
<p><a href="https://it-jin-developer.tistory.com/60">JPA의 Entity는 기본 생성자가 왜 반드시 필요할까?</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 데이터 타입이 뭐야?!]]></title>
            <link>https://velog.io/@s_miny/JPA-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85%EC%9D%B4-%EB%AD%90%EC%95%BC</link>
            <guid>https://velog.io/@s_miny/JPA-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85%EC%9D%B4-%EB%AD%90%EC%95%BC</guid>
            <pubDate>Sat, 08 Nov 2025 05:55:25 GMT</pubDate>
            <description><![CDATA[<h2 id="jpa-데이터-타입-정리">JPA 데이터 타입 정리</h2>
<p>이 글은 우테코 프리코스 오픈미션을 진행하던 중 JPA의 벽에 부딪혀 공부한 내용을 정리한 글이다. 그 중에서도 <strong>임베디드 타입(Embedded Type)</strong> 에 대해 집중적으로 다뤘다.</p>
<p>이전에는 아무것도 모르고 단순히 다들 사용하니깐 해야지라고 생각했다. 하지만 이번에는 직접 찾아보고 공부하고 사용해보며 <strong>JPA가 데이터를 다루는 두 가지 타입</strong>을 이해하게 되었다.</p>
<p>특히 <strong>순수 자바로 객체 간 협력을 설계할 때와 스프링에서 JPA를 통해 협력할 때의 개념적 차이</strong>를 많이 느꼈다. <em>(자세한 내용은 마지막에 설명하겠다.)</em></p>
<h2 id="jpa에서-데이터를-다루는-두-가지-타입">JPA에서 데이터를 다루는 두 가지 타입</h2>
<ul>
<li><p><strong>엔티티(Entity)</strong> : 식별자(@Id)가 있어서 변화를 추적할 수 있는 객체
ex) <code>@Entity</code></p>
</li>
<li><p><strong>값 타입(Value Type)</strong> : 고유 식별자가 없고 그 자체로 값으로만 존재하는 객체
ex) <code>@Embeddable</code>, <code>@Embedded</code></p>
</li>
</ul>
<h3 id="--엔티티entity">- 엔티티(Entity)</h3>
<p>엔티티는 <strong>데이터베이스의 테이블과 직접 연결되는 객체</strong>다.
식별자(<code>@Id</code>)가 있고, <strong>데이터가 바뀌더라도 같은 식별자를 가지면 같은 객체로 인식</strong>된다.</p>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;
    private int age;
}</code></pre>
<ul>
<li><code>@Entity</code> : 이 클래스는 DB 테이블에 매핑된다는 뜻</li>
<li><code>@Id</code> : 이 필드는 기본 키(Primary Key)</li>
<li><code>@GeneratedValue</code> : 자동 증가 설정</li>
</ul>
<h3 id="--값-타입value-type">- 값 타입(Value Type)</h3>
<p>값 타입은 <strong>엔티티의 생명주기에 종속되는 객체</strong>다.
<strong>엔티티가 삭제되면 함께 삭제</strong>되고, <strong>JPA에서 변경 추적이 불가능</strong>하다.</p>
<p>엔티티와 달리 <strong>식별자가 없으며</strong>,
데이터가 바뀌면 그건 “다른 값”으로 간주된다.</p>
<pre><code class="language-java">@Embeddable // 값 타입 정의
public class Address {
    private String city;
    private String street;
    private String zipcode;
}</code></pre>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @Embedded // 값 타입 포함
    private Address address;
}</code></pre>
<hr>
<h2 id="값-타입의-세-가지-종류">값 타입의 세 가지 종류</h2>
<p>값 타입 안에서도 세 가지로 나뉜다.</p>
<ul>
<li><p><strong>기본값 타입</strong> : 자바 기본 타입이나 래퍼 타입
ex) <code>int</code>, <code>String</code>, <code>Boolean</code></p>
</li>
<li><p><strong>임베디드 타입(Embedded Type)</strong> : 여러 값을 묶어 하나의 의미로 표현
ex) <code>Address</code>, <code>Period</code>, <code>Money</code></p>
</li>
<li><p><strong>값 타입 컬렉션(Collection Value Type)</strong> : 값 타입을 여러 개 보관하는 컬렉션
ex) <code>List&lt;Address&gt;</code>, <code>Set&lt;String&gt;</code></p>
</li>
</ul>
<h3 id="--기본값-타입">- 기본값 타입</h3>
<p>그냥 자바의 기본 자료형이다.
DB와 바로 매핑되기 때문에 별다른 어노테이션이 필요 없다.</p>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name; // 기본값 타입
    private int age;     // 기본값 타입
}</code></pre>
<h3 id="--임베디드-타입자세히">- 임베디드 타입(자세히)</h3>
<pre><code class="language-java">@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;
}</code></pre>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    @Embedded
    private Address address;
}</code></pre>
<p>사실 <code>city</code>, <code>street</code>, <code>zipcode</code>를 <code>Member</code> 엔티티에 바로 넣어도 된다.
하지만 이렇게 하면 ‘주소’라는 개념이 코드 곳곳에 흩어져 <strong>응집력이 떨어지고, 유사한 필드가 여러 엔티티에서 중복 선언될 가능성이 생긴다</strong>.</p>
<p>이를 해결하기 위해 JPA는 임베디드 타입(<code>@Embeddable</code> / <code>@Embedded</code>) 을 제공한다.</p>
<ul>
<li><strong>임베디드 타입의 장점</strong></li>
</ul>
<blockquote>
<ul>
<li>DB 테이블은 그대로, 객체만 논리적으로 분리된다.</li>
</ul>
</blockquote>
<ul>
<li>한 번 만들어두면 여러 엔티티에서 재사용 가능하다.</li>
<li>관련된 필드를 묶어 응집력을 높이고 의미 있는 단위로 다룰 수 있다.</li>
<li>내부 메서드를 통해 의미 있는 값을 도출할 수 있다.
여러 필드를 묶어 하나의 단위로 다루는 복합 값 타입이다.</li>
</ul>
<p>결과적으로 <code>Member</code> 테이블 하나만 생성되고 <code>Address</code> 필드들이 같은 테이블에 함께 매핑된다.</p>
<table>
<thead>
<tr>
<th>id</th>
<th>name</th>
<th>city</th>
<th>street</th>
<th>zipcode</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>철수</td>
<td>서울</td>
<td>강남로</td>
<td>12345</td>
</tr>
</tbody></table>
<h3 id="--값-타입-컬렉션">- 값 타입 컬렉션</h3>
<p>값 타입을 여러 개 가질 때 사용한다.</p>
<pre><code>@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @ElementCollection
    @CollectionTable(name = &quot;favorite_city&quot;, joinColumns = @JoinColumn(name = &quot;member_id&quot;))
    private List&lt;String&gt; favoriteCities = new ArrayList&lt;&gt;();
}</code></pre><hr>
<h2 id="하지만-값-타입은-변경-추적이-되지-않는다">하지만 값 타입은 변경 추적이 되지 않는다.</h2>
<p>JPA는 식별자(<code>@Id</code>) 를 기준으로 변경을 추적한다.
하지만 값 타입은 식별자가 없기 때문에 “어떤 값이 바뀌었는지”를 알 수 없다.</p>
<p>그래서 값이 바뀌면 JPA는 안전하게 전체 삭제 후 새로 삽입한다.</p>
<pre><code class="language-java">member.getAddress().setCity(&quot;Busan&quot;); // 감지되지 않음
member.setAddress(new Address(&quot;Busan&quot;, &quot;해운대&quot;, &quot;6623&quot;)); // 교체로 인식</code></pre>
<p>이런 특성 때문에 값 타입은 불변 객체로 설계하는 것이 일반적이다. setter를 없애고, 생성자로만 값을 설정해 사이드 이펙트를 방지한다.</p>
<hr>
<h2 id="엔티티-컬렉션이-필요한-경우">엔티티 컬렉션이 필요한 경우</h2>
<p>값 타입 컬렉션은 단순하지만, <strong>부분 수정이나 상태 추적이 필요한 경우에는 부적합</strong>하다.
이럴 땐 <strong>엔티티 컬렉션</strong>(<code>@OneToMany</code>) 으로 설계해야 한다.</p>
<p>아래는 내가 실제로 사용했던 코드 예시다. 
<em>(처음에 <code>@Embeddable</code>만 사용하고 <code>@OneToMany</code>를 사용하지 않아서 오류가 생겼었다... 아래는 해결한 코드이다.)</em></p>
<pre><code class="language-java">@Entity
public class StoredBook {
    @Id @GeneratedValue
    private Long id;
    private String code;
    private String status;
}</code></pre>
<pre><code class="language-java">@Embeddable
public class StoredBooks {
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List&lt;StoredBook&gt; storedBooks = new ArrayList&lt;&gt;();
}</code></pre>
<pre><code class="language-java">@Entity
public class Book {
    @Id @GeneratedValue
    private Long id;
    private String title;

    @Embedded
    private StoredBooks storedBooks = new StoredBooks();
}</code></pre>
<p>이 구조를 사용하면</p>
<ul>
<li><code>Book</code>을 저장하면 <code>StoredBook</code>도 함께 저장되고 (<code>cascade = ALL</code>),</li>
<li><code>Book</code>에서 제거하면 <code>StoredBook</code>도 DB에서 삭제되며 (<code>orphanRemoval = true</code>)</li>
<li>각 <code>StoredBook</code>은 독립적인 엔티티로 변경 추적이 가능하다.</li>
</ul>
<hr>
<h2 id="마무리">마무리</h2>
<p>이전에는 <code>@Entity</code> 와 <code>@Embedded</code> 의 차이를 모르고 사용했지만,
이번 오픈미션을 계기로 “이 데이터가 진짜 값인지, 아니면 엔티티인지”를 판단하는 것이 설계의 핵심이라는 걸 배웠다.</p>
<p>JPA는 단순히 데이터를 저장하는 기술이 아니라,
<strong>객체의 관계와 생명주기를 어떻게 설계할 것인지 스스로 고민하게 만드는 도구</strong>였다.</p>
<p>그리고 한 가지 더 느낀 점은,
순수 자바에서의 객체 협력과 스프링 JPA에서의 객체 협력은 전혀 다르다는 것이다.</p>
<p>순수 자바로 우테코 프리코스 미션을 진행할 때는 <strong>객체가 서로 메시지를 주고받으며 책임을 수행하고, 협력 구조를 명시적으로 드러내는 방식</strong>으로 설계했다.</p>
<p>하지만 스프링 JPA로 넘어오자 객체 협력이 단순한 호출 관계가 아니었다.
엔티티 간의 관계는 어노테이션으로 정의되고 값 타입을 다루는 방식이나 일급 컬렉션의 생성조차 JPA의 규칙을 따라야 했다.</p>
<p>순수 자바에서 객체들이 자유롭게 메시지를 주고받던 때와 달리
여기서는 <strong>영속성 컨텍스트와 데이터베이스라는 규칙 안에서 간접적으로 협력하는 구조</strong>였다. </p>
<p>그 변화가 새로우면서도 낯설었고, 동시에 신기했다.</p>
<p>그 과정에서 “<strong>JPA가 단순한 ORM이 아니라 객체와 데이터의 세계를 연결하는 기술</strong>”이라는 걸 온몸으로 느꼈다.</p>
<p>앞으로도 이런 차이를 이해하며 기술을 단순히 사용하는 개발자가 아니라 이해하고 설계하는 개발자로 성장하고 싶다.</p>
<hr>
<h2 id="참고">참고</h2>
<p><a href="https://jiwondev.tistory.com/231">JPA #9 값 타입, 컬렉션, 임베디드 타입</a>
<a href="https://velog.io/@stpn94/JPA-Embedded-Embeddable-%EC%86%8D%EC%84%B1%EC%9D%98-%EC%9E%AC%EC%A0%95%EC%9D%98">PA Embedded, Embeddable, 속성의 재정의</a>
<a href="https://jojoldu.tistory.com/559">JPA 사용시 @Embedded 주의사항</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Gradle이 뭐야?]]></title>
            <link>https://velog.io/@s_miny/Gradle%EC%9D%B4-%EB%AD%90%EC%95%BC</link>
            <guid>https://velog.io/@s_miny/Gradle%EC%9D%B4-%EB%AD%90%EC%95%BC</guid>
            <pubDate>Fri, 07 Nov 2025 06:06:22 GMT</pubDate>
            <description><![CDATA[<h2 id="gradle-태스크task-테스트-정리">Gradle, 태스크(Task), 테스트 정리</h2>
<p>이번 우테코 8기 프리코스에 새로 생긴 오픈 미션을 진행하면서 Gradle 관련 오류가 일어나서 그 과정에 공부한 내용을 정리하려고 한다. 
Gradle은 단순히 의존성을 주입하는 도구가 아니라 빌드 전체의 실행 흐름을 제어하는 시스템이다.
이번 포스트에서는 Gradle의 핵심 개념인 태스크(Task) 와 테스트 설정(<code>test {}</code> vs <code>tasks.named(&#39;test&#39;) {}</code>) 차이에 대해 정리했다.</p>
<h2 id="gradle이란">Gradle이란?</h2>
<p>컴파일, 테스트, 패키징, 실행, 배포의 작업을 모두 자동화 시켜주는 Groovy 언어 기반 <strong>오픈소스 빌드 도구</strong>이다.</p>
<h2 id="build란">Build란?</h2>
<p>Build는 소스코드를 실행 가능한 어플리케이션으로 준비시키는 일련의 과정이다.</p>
<p>흔히 빌드와 함께 나오는 컴파일, 링크 등의 용어는 모두 빌드의 하위 과정이다.
각각의 소스코드 파일을 기계어로 컴파일한 뒤 의존하는 소스코드를 연결시켜 실행 가능한 상태로 만들어주는 것이다.</p>
<p>따라서 Build는 <strong>코드 실행 전까지의 모든 과정</strong>을 일컫는다.</p>
<h2 id="gradle-빌드-단계">Gradle 빌드 단계</h2>
<p>Gradle 빌드는 3단계로 이루어져있다.</p>
<p>Gradle은 빌드를 실행하기 전 모든 태스크를 한꺼번에 만드는 것이 아니라 아래 세 단계를 거쳐 필요한 태스크만 실제로 생성하고 실행한다.</p>
<h3 id="1-초기화-initialization-">1. 초기화 (Initialization) :</h3>
<p><strong>어떤 프로젝트를 빌드할지 결정</strong>하고 각 프로젝트에 대한 <code>Project</code> 객체를 생성하는 단계이다. 프로젝트 정보는 <code>setting.gradle</code> 파일에서 구성한다.</p>
<h3 id="2-구성-configuration-">2. 구성 (Configuration) :</h3>
<p>각 프로젝트의 <code>build.gradle</code> 파일을 읽어** 태스크(Task)를 생성**하고 태스크 간의 의존 관계를 구성하는 단계이다.
<code>plugins {}</code> 블록이 실행되고 플러그인이 자동으로 여러 태스크를 등록하는 시점도 바로 이 단계다.</p>
<p>즉, 구성 단계는 필요한 모든 의존성을 가져오고 작업을 준비하는 단계이다.</p>
<h3 id="3-실행-execution-">3. 실행 (Execution) :</h3>
<p>앞선 단계에서 <strong>준비된 태스크들이 실제로 수행되는 시점</strong>이다.
예를 들어 컴파일(compile), 테스트(test), 패키징(package) 등의 작업이 이 단계에서 실행된다.</p>
<blockquote>
<p>_주의! _
<strong>소스코드 실행(run)은 빌드 단계에 포함되지 않는다는 것</strong>이다. 빌드는 애플리케이션이 실행되기 직전까지의 과정이며 코드를 실제로 수행(run)하는 것은 별도의 단계이다.</p>
</blockquote>
<p>즉, </p>
<ul>
<li>빌드(build) 는 코드를 검증하고 준비하는 과정</li>
<li>실행(run) 은 검증된 코드를 실제로 동작시키는 과정</li>
</ul>
<p>그렇다면 의문이 생긴다.</p>
<blockquote>
<p>“테스트 코드도 결국 코드인데, 왜 빌드 단계에 포함될까?”</p>
</blockquote>
<p>그 이유는 빌드의 본질이 “코드가 수행되기 전 잠재적인 오류를 검증하는 과정”이기 때문이다. 테스트는 실제 애플리케이션 실행 전에 코드의 정확성과 안정성을 확인하는 절차이다. 따라서 빌드 과정의 일부로 포함된다.</p>
<h2 id="구성-단계에서-말하는-태스크task란">구성 단계에서 말하는 태스크(Task)란?</h2>
<p><strong>Gradle의 빌드는 여러 개의 작업 단위(Task) 로 구성</strong>된다.
각 Task는 “무엇을, 언제, 어떻게” 실행할지를 정의한 하나의 객체이다.
예를 들어 아래와 같은 명령을 수행한다.</p>
<ul>
<li><strong>compileJava</strong> : Java 코드를 컴파일</li>
<li><strong>test</strong> : JUnit 테스트 실행</li>
<li><strong>bootJar</strong> : Spring Boot 실행 JAR 생성</li>
</ul>
<p>정리: Task = “<strong>Gradle이 수행해야 할 구체적인 작업 단위</strong>”</p>
<h2 id="gradle은-플러그인-기반-시스템">Gradle은 플러그인 기반 시스템</h2>
<p>이런 태스크들은 대부분 우리가 직접 만드는 것이 아니라 <code>plugins {}</code> 블록에 등록된 플러그인들이 자동으로 생성한다.
예를 들어 현재 프로젝트의 <code>build.gradle</code>에서는 다음과 같은 플러그인을 사용하고 있다.</p>
<ul>
<li><p><code>java</code> → <code>compileJava</code>, <code>processResources</code>, <code>classes</code>, <code>test</code>, <code>jar</code> 등의 태스크 생성</p>
</li>
<li><p><code>org.springframework.boot</code> → <code>bootJar</code>, <code>bootRun</code>, <code>bootBuildImage</code> 등의 태스크 생성</p>
</li>
</ul>
<p>이처럼 <code>build.gradle</code> 파일에 태스크 코드가 직접 보이지 않더라도 <code>Gradle</code>은 플러그인을 읽는 <code>Configuration</code> 단계에서 이 태스크들을 자동으로 등록한다.</p>
<h2 id="gradle에서의-test--vs-tasksnamedtest-">Gradle에서의 test {} VS tasks.named(&#39;test&#39;) {}</h2>
<p>이번 오픈 미션을 진행하면서 겪었던 오류이자, 이 글을 작성하게 된 직접적인 계기이기도 하다.
문제 해결 과정에서 <code>Gradle</code>의 동작 원리를 깊이 있게 이해할 수 있었고 지금은 이 경험이 꽤 값진 학습이었다고 생각한다. 😊</p>
<p>암튼 이 두 코드의 차이와 오류가 난 이유를 설명하고 글을 마치겠다.</p>
<p>Gradle이 처음에는 다음과 같이 작성되어있었다.</p>
<pre><code class="language-java">tasks.named(&#39;test&#39;) {
    useJUnitPlatform()
}</code></pre>
<p>하지만 테스트 코드를 작성하고 실행할 때마다 계속해서 “<strong>No tests were found</strong>” 라는 메시지가 출력되었다.
여러 블로그를 찾아보며 원인을 분석한 결과 <code>Gradle</code>의 테스트 태스크 설정 시점에 문제가 있었던 것이다.</p>
<p>그래서 다음과 같이 코드를 수정했다.</p>
<pre><code class="language-java">test {
    useJUnitPlatform()
}</code></pre>
<p>이후에는 테스트가 정상적으로 동작했다.</p>
<hr>
<p>Gradle에서 테스트 설정을 하는 방법에는 두 가지가 있다.
둘 다 <code>test</code> 태스크를 구성하지만 적용 시점과 동작 방식이 다르다.</p>
<h3 id="test-">test {}</h3>
<ul>
<li>이미 생성된 <code>test</code> 태스크를 즉시 설정한다.</li>
<li>설정이 바로 적용되어 직관적이지만 유연성이 낮다.</li>
</ul>
<h3 id="tasksnamedtest-">tasks.named(&#39;test&#39;) {}</h3>
<ul>
<li>나중에 생성될 <code>test</code> 태스크에 설정을 예약한다.</li>
<li>‘지연 구성(lazy configuration)’ 방식이라 유연하지만 플러그인 로딩 시점이 꼬이면 설정이 반영되지 않을 수 있다.</li>
</ul>
<p>예를 들어 <code>Spring Boot</code>나 <code>Java</code> 플러그인의 등록 시점이 늦어지면 <code>tasks.named(&#39;test&#39;) {}</code>로 예약한 설정이 제때 적용되지 않아 <code>useJUnitPlatform()</code>이 누락되고 결과적으로 “No tests were found” 오류가 발생할 수 있다.</p>
<h2 id="마무리">마무리</h2>
<p>이번 경험을 통해 단순히 오류를 해결하는 데 그치지 않고 <code>Gradle</code>의 태스크 생성·구성 시점과 지연 구성 개념을 명확히 이해할 수 있었다.
앞으로는 단순히 “작동하게 만드는” 데서 멈추지 않고, 왜 그렇게 작동하는지를 이해하며 성장하고 싶다.</p>
<h2 id="참고자료">참고자료</h2>
<p><a href="https://velog.io/@joychae714/%ED%95%99%EC%8A%B5%EC%A0%95%EB%A6%AC-Gradle-%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90">[학습정리] Gradle 알고 쓰자!</a>
<a href="https://www.inflearn.com/community/questions/1071921/tasks-named-x27-test-x27-%EC%99%80-%EA%B7%B8%EB%83%A5-test?srsltid=AfmBOorH7R0Y-4F0dR6Kl4_jEH1QYd6U0x5u2_9acXMcAfTjkclFlFEz">tasks.named(&#39;test&#39;) { 와 그냥 test{}</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우테코 8기 프리코스 3주차 회고록]]></title>
            <link>https://velog.io/@s_miny/%EC%9A%B0%ED%85%8C%EC%BD%94-8%EA%B8%B0-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@s_miny/%EC%9A%B0%ED%85%8C%EC%BD%94-8%EA%B8%B0-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Sun, 02 Nov 2025 09:17:06 GMT</pubDate>
            <description><![CDATA[<h2 id="3주차-미션--로또">3주차 미션 : 로또</h2>
<p>3주차 미션은 로또 게임이었다.
이번 주차는 설계 과정뿐만 아니라 코딩 과정에서도 유독 많은 고민을 했다.</p>
<p>사람들과 코드 리뷰를 통해 다양한 시각을 접하고, ‘함께-나누기’와 ‘토론하기’를 적극적으로 활용하며 생각의 폭이 한층 넓어졌다. 또한 2주차에 받았던 피드백을 되짚어보며 나의 부족했던 부분이 조금씩 보이기 시작했다.
그 덕분에 설계에 더 많은 시간을 들이게 되었고 구현을 하면서도 “이렇게 하면 안 될 것 같은데…” 하는 의문이 계속 따라다녔다.</p>
<p>이번 회고는 그 고민에 대한 나의 답을 찾아가는 과정의 기록이다.
먼저 2주차 미션에서 느꼈던 아쉬움을 3주차에서는 어떻게 행동으로 옮겼는지부터 이야기해보겠다.</p>
<hr>
<h2 id="2주차의-아쉬움을-3주차에서-행동으로">2주차의 아쉬움을 3주차에서 행동으로</h2>
<h3 id="--에러가-났을-때-디버깅으로-해결하기">- 에러가 났을 때 &#39;디버깅&#39;으로 해결하기</h3>
<p>2주차 때는 에러가 발생하면 약 5분 정도 코드를 살펴보며 문제를 찾았다.
하지만 솔직히 <strong>“운이 좋았다”</strong>는 느낌이 컸다.</p>
<p>그래서 3주차에는 에러가 발생하면 무조건 먼저 디버깅을 통해 에러를 찾고 해결하자라는 목표를 세웠다.
그 결과 이번 주차에서는 모든 에러를 디버깅으로 추적하며 원인을 정확히 파악하고 해결했다.</p>
<p>그중에서도 가장 처음 테스트 오류가 일어나 디버깅을 시도했던 예시를 공유하고 싶다.</p>
<pre><code class="language-java">private void validateLottoRange(List&lt;Integer&gt; numbers) {
        if ((numbers.size() &lt; LOTTO_NUMBER_MIN) || (numbers.size() &gt; LOTTO_NUMBER_MAX)) {
            throw new IllegalArgumentException(&quot;[ERROR] 로또 번호의 범위는 &quot; + LOTTO_NUMBER_MIN
                    + &quot; ~ &quot; + LOTTO_NUMBER_MAX + &quot; 입니다.&quot;);
     }
 }</code></pre>
<p>위 코드는 <code>Lotto</code> 객체에서 로또 번호의 범위를 검증하는 메서드이다.</p>
<p>테스트 코드는 다음과 같았다.</p>
<pre><code class="language-java">@Test
@DisplayName(&quot;로또 번호의 범위를 벗어나면 예외가 발생한다.&quot;)
void lottoNumberRangeTest() {
       // given &amp; when &amp; then
     assertThatThrownBy(() -&gt; new Lotto(List.of(1, 2, 3, 4, 5, 46)))
                  .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining(&quot;로또 번호의 범위는&quot;);
}</code></pre>
<p>그런데 테스트가 통과하지 않았다.
분명히 검증 메서드는 문제가 없어 보였는데 왜 실패하지..?</p>
<p><img src="https://velog.velcdn.com/images/s_miny/post/f4510249-b4bb-4deb-858a-55bcf21ffe7a/image.png" alt=""></p>
<p>이때 처음으로 디버깅 모드를 실행했다.
if문에 브레이크포인트를 걸고 조건을 살펴보니 46이 들어왔는데도 true가 아닌 false가 반환되고 있었다.</p>
<p>자세히 보니 <code>numbers.size()</code>를 비교하고 있었던 것!
번호의 범위를 검사하는게 아니라 리스트 크기를 검사하고 있었던 것이었다.. 😅</p>
<p>그래서 다음과 같이 코드를 수정했다.</p>
<pre><code class="language-java">private void validateLottoRange(List&lt;Integer&gt; numbers) {
        for (int number : numbers) {
            if ((number &lt; LOTTO_NUMBER_MIN) || (number &gt; LOTTO_NUMBER_MAX)) {
                throw new IllegalArgumentException(&quot;[ERROR] 로또 번호의 범위는 &quot; + LOTTO_NUMBER_MIN
                        + &quot; ~ &quot; + LOTTO_NUMBER_MAX + &quot; 입니다.&quot;);
            }
        }
    }</code></pre>
<p>수정 후 테스트를 다시 실행하니 바로 통과했다!!</p>
<p>이 예시는 아주 단순해 보일 수 있지만 나에게는 정말 큰 도전이었다.
‘<strong>코드의 동작을 눈으로 확인하며 문제를 추적한다</strong>’는 경험을 처음으로 했기 때문이다.</p>
<p>이후로 더 어려운 에러들을 마주했지만 이 첫 경험 덕분에 두려움 없이 차근차근 원인을 찾고 해결할 수 있었다.</p>
<p>앞으로도 에러를 두려워하지 않고 코드를 이해하며 디버깅으로 차근차근 해결하는 개발자로 성장하고 싶다.</p>
<hr>
<h2 id="2주차-피드백을-통해-변화된-3주차">2주차 피드백을 통해 변화된 3주차</h2>
<p>3주차 미션을 시작하기 전 2주차 때 받았던 피드백을 천천히 다시 읽어보았다.
그중에서도 유난히 마음에 깊게 남은 피드백 세 가지가 있었다.</p>
<p><img src="https://velog.velcdn.com/images/s_miny/post/9da11ff7-9cb3-4dbd-ab16-144cee8ff65c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/s_miny/post/217e1c62-0b0d-461a-abbf-c20bbaeaec6a/image.png" alt=""></p>
<p>나는 지금까지 README에는 기능 구현 목록만 작성해왔다. 그 기능 목록 안에 클래스 설계와 구현 내용이 섞여 있었다.
게다가 이름을 짓기 애매하면 변수명에 자료형을 그대로 붙이는 습관도 있었다... 😅</p>
<p>이 피드백을 마음에 새기고 3주차 미션을 시작할 때 가장 먼저 이를 직접 개선해보기로 목표를 세웠다.</p>
<h3 id="피드백을-수용하며-실천한-점">피드백을 수용하며 실천한 점</h3>
<ol>
<li><p><strong>README 구성 방식 개선</strong>
설계를 시작하면서 <code>README</code>를 새롭게 작성했다.
먼저 <code>프로젝트 소개</code>와 <code>설계 목표</code>를 정리하고 이후 <code>실행 예시</code> → <code>기능 구현 목록</code> → <code>설계 구조 및 책임 분리</code> 순으로 구성했다.</p>
<p>모든 내용을 한눈에 보기 어려워 <a href="https://github.com/smiinii/java-lotto-8/tree/smiinii">README 링크</a>를 첨부한다.</p>
</li>
<li><p><strong>의미 중심 네이밍으로 개선</strong>
변수명과 메서드명에 자료형을 전혀 사용하지 않았다.
예를 들어 <code>NumberList</code> 같은 이름 대신 <code>winningNumbers</code>처럼 의미 중심으로 표현했다.</p>
</li>
</ol>
<h3 id="피드백을-실천하며-깨달은-점">피드백을 실천하며 깨달은 점</h3>
<ul>
<li><p><strong>README의 중요성</strong>
README를 상세하게 작성하면 코드를 처음 보는 사람도 프로젝트의 흐름을 빠르게 파악할 수 있다.
단순한 정리 문서가 아니라 “이 프로젝트가 어떤 문제를 해결하고 어떤 구조로 접근했는가”를 보여주는 프로젝트의 얼굴이라는 걸 느꼈다.</p>
</li>
<li><p><strong>기능 목록과 설계 분리의 효과</strong>
기능 구현 목록에는 무엇을 해야 하는가가 명확히 보이고 구조 설계 부분에서는 어떻게 설계했는가가 드러나면서 전체 구조가 한눈에 정리되었다.
이 분리가 코드 작성 방향을 잡는 데 큰 도움이 되었다.</p>
</li>
<li><p><strong>의미 중심 이름의 힘</strong>
자료형 대신 의미로 이름을 짓다 보니 변수명만 봐도 역할이 명확하게 드러나고 코드의 가독성이 확연히 높아졌다.
단순한 네이밍 규칙의 문제가 아니라 의도를 표현하는 코드로 가는 과정이었다.</p>
</li>
</ul>
<p>이 피드백들을 단순히 _“하지 말라니까 안 해야지”_라고 받아들이지 않았다.
“왜 이런 피드백이 나왔는지”를 이해하고 <strong>그 이유를 내 것으로 만드는 과정에서 진짜 성장이 있었다.</strong></p>
<p>앞으로도 매주 주어지는 <strong>피드백을 단순히 수정 지시로 보지 않고 그 속에 담긴 의도를 해석하고 체화하는 개발자</strong>가 되고 싶다.</p>
<hr>
<h2 id="새로운-도전-enum">새로운 도전 Enum</h2>
<p>3주차 미션 프로그래밍 요구 사항 3번에는 </p>
<blockquote>
<p>Java Enum을 적용하여 프로그램을 구현한다.</p>
</blockquote>
<p>라는 요구 사항이 있었다.</p>
<p>‘이걸 안 쓰면 문제를 못 푸는 건가?’라는 생각이 들 정도로 왜 꼭 <code>Enum</code>을 써야 하는지 이해가 잘 되지 않았다.</p>
<p>하지만 <strong>일단 도전을 해보는 게 배우는 길</strong>이라고 생각했다.
<em>“일단 써보자. 대신 왜 써야 하는지는 직접 느껴보자!”</em>
그렇게 마음을 먹고 설계를 시작했다.</p>
<h3 id="enum-사용법을-몰라서-고생한-시간">Enum 사용법을 몰라서 고생한 시간</h3>
<p>처음에는 단순히 상수들을 <code>Enum</code>에 넣으려고 했다.</p>
<pre><code class="language-java">public enum Rank {
    FIRST, SECOND, THIRD, FOURTH, FIFTH, NONE
}</code></pre>
<p>그런데 이렇게 만들고 나니 전혀 쓸모가 없었다.
등수를 계산하려면 여전히 <code>if문</code>으로 개수를 비교해야 했고 Enum을 왜 써야 하는지 더 혼란스러웠다.</p>
<p>그래서 하나씩 찾아보며 사용법을 익혔다.
Enum도 필드, 생성자, 메서드를 가질 수 있다는 걸 알게 되었고 그제서야 조금 감이 오기 시작했다.</p>
<p><em>&quot;아 단순한 상수의 집합이 아니라 값과 행동을 함께 담는 객체처럼 쓸 수 있구나!&quot;</em></p>
<p>이걸 깨닫고 나서 <code>Rank</code>를 이렇게 다시 설계했다.</p>
<pre><code class="language-java">public enum Rank {
    FIFTH(3, false, 5000),
    FOURTH(4, false, 50000),
    THIRD(5, false, 1500000),
    SECOND(5, true, 30000000),
    FIRST(6,false, 2000000000),
    NONE(0, false, 0);

    private final int winningNumberCount;
    private final boolean isBonusNumber;
    private final long prizeMoney;

    Rank(int winningNumberCount, boolean isBonusNumber, long prizeMoney) {
        this.winningNumberCount = winningNumberCount;
        this.isBonusNumber = isBonusNumber;
        this.prizeMoney = prizeMoney;
    }
}</code></pre>
<p>여기서 한 가지 생각이 떠올랐다.</p>
<p><em>&quot;순위를 계산하는 책임은 Rank가 갖는 게 맞지 않을까?&quot;</em></p>
<p>그렇게 Rank안에 등수를 판별하는 행동을 넣었다.</p>
<pre><code class="language-java">public static Rank from(int winningNumbersMatchCount, boolean bonusNumberMatchResult) {
        if (winningNumbersMatchCount == 6) return FIRST;
        if (winningNumbersMatchCount == 5 &amp;&amp; bonusNumberMatchResult) return SECOND;
        if (winningNumbersMatchCount == 5) return THIRD;
        if (winningNumbersMatchCount == 4) return FOURTH;
        if (winningNumbersMatchCount == 3) return FIFTH;
        return NONE;
    }</code></pre>
<p>이걸 작성하면서 처음으로 &quot;Enum이 진짜 객체처럼 행동할 수 있구나&quot;라는 걸 체감했다.</p>
<p>예전 같았으면 &#39;등수를 판별하는 심판 클래스&#39;를 따로 만들어서 수많은 <code>if문</code>으로 조건을 검사했을 것이다.</p>
<p>하지만 <code>Enum</code>을 쓰니 각 <code>Rank</code> 상수가 <strong>자신의 규칙을 직접 알고 있어서</strong> 등수 판단 이라는 책임이 자연스럽게 <code>Rank</code> 내부로 모였다.</p>
<h3 id="이를-통해-배운점">이를 통해 배운점</h3>
<p>이번 도전을 통해 <code>Enum</code>은 단순한 상수 모음이 아니라 <strong>의미 있는 상태와 행동을 함께 가진 객체</strong>라는 걸 배웠다.</p>
<p>그리고 그 덕분에 다음과 같은 장점을 직접 느꼈다.</p>
<ul>
<li>등수 관련 정보와 로직이 한 곳에 모인다. =&gt; <strong>응집도 향상</strong></li>
<li>코드가 규칙 자체를 설명한다. =&gt; <strong>가독성 향상</strong></li>
</ul>
<p>만약 <code>Enum</code>을 쓰지 않았다면 <code>ResultCalculator</code> 안에서 등수를 일일이 <code>if문</code>으로 비교하고 상금 계산을 별도의 메서드에서 따로 처리했을 것이다.</p>
<p>그랬다면 규칙이 조금만 바뀌어도 여러 곳의 코드를 수정해야 했을 것이고 등수 정보와 로직이 흩어져 구조가 훨씬 복잡해졌을 것이다.</p>
<p>하지만 <code>Enum</code>으로 <code>Rank</code>를 정의하니 당첨 규칙과 상금 정보가 한곳에 모여 응집도가 높아졌고 코드가 스스로 규칙을 설명하는 구조가 되었다.</p>
<p>이 경험을 통해 <code>Enum</code>은 단순히 문법적인 요구사항이 아니라
<strong>설계의 의도를 더 명확히 드러내고 유지보수성을 높여주는 도구</strong>라는 걸 확실히 느꼈다.</p>
<hr>
<h2 id="getter를-지양한다는-말의-진짜-의미를-깨닫다">getter를 지양한다는 말의 진짜 의미를 깨닫다</h2>
<p>디스코드 ‘토론하기’ 채널을 둘러보다가 “getter를 지양해야 한다”는 글을 우연히 읽게 되었다.</p>
<p>처음에는 단순히 “_값을 가져오는 메서드일 뿐인데, 왜 지양해야 하지?_” 하는 생각이 들었다.</p>
<p>하지만 글을 읽어 내려가면서 “<strong>객체의 내부 상태를 꺼내 계산하는 행위 자체가 결국 캡슐화를 깨뜨리고 책임이 흩어지게 만든다</strong>”는 설명이 마음에 깊게 남았다.</p>
<h3 id="내-코드-속-getter">내 코드 속 getter</h3>
<p>그 내용을 떠올리며 내 코드를 천천히 살펴보던 중 <code>LottoIssuer</code> 클래스의 다음 코드가 눈에 들어왔다.</p>
<pre><code class="language-java">int count = money.getMoney() / LottoPrice.UNIT;</code></pre>
<p>처음에는 아무 문제 없어 보였지만 곰곰이 생각해보니 ‘로또를 살 수 있는 개수를 계산하는 책임’은 <code>Money</code>의 몫이었다.
<code>LottoIssuer</code>는 단지 “돈으로 로또를 발행하는 역할”만 하면 되는 객체다.</p>
<p>그래서 <code>Money</code> 객체에 아래 메서드를 추가했다.</p>
<pre><code class="language-java">public int purchasableCount() {
    return money / LottoPrice.UNIT;
}</code></pre>
<p>그리고 <code>LottoIssuer</code>에서는 이렇게 수정했다.</p>
<pre><code class="language-java">int count = money.purchasableCount();</code></pre>
<p>이제 <code>LottoIssuer</code>는 <code>Money</code>의 내부 상태를 알 필요가 없어졌다.
그저 &quot;너로 몇 장 살 수 있니?&quot;하고 메시지를 보내는 형태로 바뀐 것이다.</p>
<h3 id="그렇다고-getter가-항상-나쁜-건-아니다">그렇다고 getter가 항상 나쁜 건 아니다</h3>
<p>이 과정에서 깨달은 또 하나의 중요한 점은 <code>getter</code>를 완전히 없애야 한다는 건 아니라는 것이다.</p>
<p>객체가 단순히 상태를 외부에 전달해야 하는 역할이라면 예를 들어 <code>View</code>나 <code>DTO</code>로 데이터를 전달할 때는 <code>getter</code>가 자연스럽고 필요하다.</p>
<blockquote>
<p>“비즈니스 로직을 위해 객체의 내부 상태를 끌어다 쓰는 getter”는 지양해야 하지만, 
“표현 계층으로 데이터를 전달하기 위한 getter”는 오히려 명확한 의도를 드러낸다.</p>
</blockquote>
<p>결국 중요한 건 <strong>“getter를 쓰느냐 안 쓰느냐”가 아니라 “이 getter가 객체의 책임을 침범하는가?”</strong>를 판단할 수 있는 감각이었다.</p>
<h3 id="이를-통해-느낀점">이를 통해 느낀점</h3>
<p>이 경험을 통해 “getter를 지양하라”는 말의 진짜 의미를 이해했다.</p>
<p>처음엔 단순히 “getter를 쓰지 말자”는 문법적 조언으로만 들렸지만 사실 그 말은 “<strong>객체의 상태를 외부에서 읽어 계산하지 말고 그 행동을 객체 스스로 하게 만들어라.</strong>”라는 철학적인 메시지였다.</p>
<p><code>getter</code>를 썼다는 건 “이 객체의 상태를 내가 알아야 한다”는 뜻이고 그 순간 이미 객체의 자율성을 침범하고 있는 셈이다.</p>
<p>이번 경험으로 <code>Money</code>와 <code>LottoIssuer</code>의 역할이 훨씬 명확해졌고 서로의 의존도도 줄어들었다.
<code>getter</code>를 없애는 게 목적이 아니라 <strong>객체가 스스로 행동하도록 만들고 필요할 때만 상태를 안전하게 드러내는 것</strong>이 진짜 객체지향이라는 걸 배웠다.</p>
<p>앞으로는 &quot;<strong>왜 <code>getter</code>를 써야 하는가</strong>&quot;를 스스로 설명할 수 있는 개발자가 되고싶다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>우아한테크코스 3주차 미션을 통해 이번에도 많은 성장을 경험했다.
물론 아직 내가 보지 못한 문제나 약점도 분명 있을 것이다.
하지만 회고와 메타인지를 반복하며 나는 분명 한 걸음씩 성장하고 있다고 믿는다.</p>
<p>4주차에는 어떤 미션이 주어질지 모르지만 또 어떤 문제를 마주하고 어떤 배움을 얻게 될지 기대가 된다.</p>
<p>이 마음을 유지하며 배움이 주는 즐거움을 잊지 않고 읽기 쉬운 코드와 변화에 강한 설계를 만들어가는 개발자가 되고 싶다.
앞으로도 계속 도전하고 실패하더라도 다시 도전하며 성장의 과정을 멈추지 않을 것이다.</p>
<p>이상으로 3주차 회고를 마치겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우테코 8기 프리코스 2주차 회고록]]></title>
            <link>https://velog.io/@s_miny/%EC%9A%B0%ED%85%8C%EC%BD%94-8%EA%B8%B0-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@s_miny/%EC%9A%B0%ED%85%8C%EC%BD%94-8%EA%B8%B0-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Sun, 26 Oct 2025 08:26:43 GMT</pubDate>
            <description><![CDATA[<h2 id="2주차-과제--자동차-경주">2주차 과제 : 자동차 경주</h2>
<p>2주차 프리코스 미션은 자동차 경주 게임이었다.
마침 학교 중간고사 기간과 겹쳐 정말 정신없는 한 주였지만,
그럼에도 불구하고 설계하고 코드를 작성하는 시간만큼은 오히려 즐거웠다.
<del>그냥... 학교 시험이 힘들었을 뿐이다</del> 😅</p>
<p>이번 미션은 1주차보다 요구사항이 늘어나면서 설계 단계의 중요성을 더욱 크게 느낄 수 있었다.
“어떻게 객체를 나누고 책임을 분리할 것인가”를 고민하는 시간이 많아졌고,
그 과정 자체가 개발에 더욱 몰입을 할 수 있게 해준 것이였다.</p>
<p>이번 회고는 1주차에 느꼈던 아쉬운 점을 어떻게 보안했는지에 대해 먼저 시작하겠다.</p>
<hr>
<h2 id="1주차에-아쉬웠던-점">1주차에 아쉬웠던 점,,,</h2>
<h3 id="1-기능-단위-커밋-그리고-세분화된-히스토리">1. 기능 단위 커밋, 그리고 세분화된 히스토리</h3>
<p>1주차에서 아쉬웠던 점 중 하나는 기능 커밋과 테스트 커밋을 분리하지 못한 것이었다.
이번에는 이를 반드시 개선해보자 마음먹고 <strong>기능 → 테스트 → 문서(README)</strong> 순으로 세분화하여 커밋을 남겼다.</p>
<figure align="center">
  <img src="https://velog.velcdn.com/images/s_miny/post/3fc61cb8-e332-4ee0-b003-5bee3350cb68/image.png">
  <figcaption><small>기능 단위 커밋 사진</small></figcaption>
</figure>

<p>결과적으로 커밋 개수가 엄청나게 많아졌지만 그만큼 개발 과정이 세밀하게 기록되어 있다는 것을 느꼈다.</p>
<figure align="center">
  <img src="https://velog.velcdn.com/images/s_miny/post/2928f918-6614-48b2-9afb-e8daa1178c9c/image.png">
  <figcaption><small>커밋 개수 사진</small></figcaption>
</figure>

<p>&#39;처음엔 이런 간단한 문제에 비해 너무 오바인가?&#39;라고만 생각했지만 미션을 마치고 나서는 이 방식의 진짜 장점을 깨달았다.</p>
<ol>
<li><p><strong>언제든지 안전하게 되돌릴 수 있다.</strong>
이전에는 여러 기능을 한꺼번에 커밋해 실수했을 때 되돌리기 어려웠다.
하지만 커밋 단위가 작아지니, 실수한 부분만 정확히 롤백할 수 있었다.</p>
</li>
<li><p><strong>객체의 역할과 책임이 명확해졌다.</strong>
기능을 나누며 커밋하다 보니, 각 객체가 “무엇을 해야 하는지”
더 또렷하게 구분할 수 있었다. 자연스럽게 SRP(단일 책임 원칙)에 가까워졌다.</p>
</li>
<li><p><strong>기능별 테스트가 쉬워졌다.</strong>
작은 단위로 커밋해두면 테스트도 기능별로 실행할 수 있다.
덕분에 어떤 부분에서 오류가 났는지 빠르게 파악할 수 있었다.</p>
</li>
<li><p><strong>개발 흐름이 하나의 문서가 되었다.</strong>
커밋 로그를 보면 그 주의 개발 태도와 과정이 그대로 남아 있었다.
하나하나의 커밋이 나의 사고흐름을 기록하는 문서처럼 느껴졌다.</p>
</li>
</ol>
<hr>
<h3 id="2-설계-단계의-변화--책임을-명확히-나누기">2. 설계 단계의 변화 – 책임을 명확히 나누기</h3>
<p>1주차에서는 입력 정책과 책임 경계를 명확히 확정하지 못한 것이 아쉬웠다.
그래서 이번에는 설계 단계에서 README를 <code>입력(Input)</code>  <code>출력(Output)</code> <code>기능(Feature)</code> 으로 구분하는 것에 더해 각 기능이 어떤 객체의 책임인지까지 표시해두었다.</p>
<figure align="center">
  <img src="https://velog.velcdn.com/images/s_miny/post/81f5bd3e-0542-4d95-b6ed-131f39416361/image.png">
  <figcaption><small>README 사진</small></figcaption>
</figure>

<p>이 과정을 거치니 개발 단계에서 훨씬 수월하게 구현할 수 있었고, <strong>“설계를 잘 해두면 개발은 자연스럽게 따라온다”</strong>는 것을 몸소 느꼈다.</p>
<hr>
<h3 id="3-controller-분리와-서비스-계층-도입">3. Controller 분리와 서비스 계층 도입</h3>
<p>또한 이번 미션에서는 <code>Controller</code>를 분리할지 여부를 직접 판단해보았다.
1주차에서는 <code>Application</code>이 모든 역할(입출력, 도메인)을 맡았지만 이번에는 <code>Controller</code>를 별도로 분리해보며 도메인의 협력 흐름과 입출력 로직을 한눈에 관리할 수 있도록 했다. </p>
<p><code>Controller</code> 분리 여부 판단을 말하기전에 분리를 하면서 느꼈던 생각들은 먼저 말하겠다.</p>
<pre><code class="language-java">package racingcar.controller;

import racingcar.domain.Round;
import racingcar.domain.racer.Racer;
import racingcar.domain.racer.Racers;
import racingcar.domain.racer.RacingCar;
import racingcar.domain.racer.RacingCars;
import racingcar.service.RacingService;
import racingcar.view.InputView;
import racingcar.view.OutputView;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class RacingController {

    private InputView inputView;
    private OutputView outputView;
    private RacingService racingService;

    public RacingController(InputView inputView, OutputView outputView, RacingService racingService) {
        this.inputView = inputView;
        this.outputView = outputView;
        this.racingService = racingService;
    }

    public void run() {
        String input = inputView.readCarName();
        List&lt;String&gt; inputList = racingService.parseInputs(input);

        String roundCount = inputView.readRoundCount();
        Round round = new Round(roundCount);

        List&lt;Racer&gt; racerList = makeRacer(inputList);
        Racers racers = new RacingCars(racerList);

        racingStart(racers, round);
    }

    private List&lt;Racer&gt; makeRacer(List&lt;String&gt; inputList) {
        List&lt;Racer&gt; racerList = new ArrayList&lt;&gt;();
        for (int i = 0; i &lt; inputList.size(); i++) {
            racerList.add(new RacingCar(inputList.get(i)));
        }
        return racerList;
    }

    private void racingStart(Racers racers, Round round) {
        for (int i = 0; i &lt; round.getRoundNumber(); i++) {
            Map&lt;String, Integer&gt; roundResult = racingService.roundStart(racers);
            outputView.printRoundResult(roundResult);
        }
        outputView.printGameResult(racingService.gameEnd(racers));
    }
}
</code></pre>
<p>처음에는 <code>Controller</code>에 모든 객체를 주입하다 보니 인자가 너무 많아졌는데, 이를 개선하기 위해 <code>Service</code> 계층을 따로 만들어 입출력을 제외한 도메인 간 협력 로직을 맡기도록 했다.
이 과정을 통해 MVC 구조의 진짜 의도, 즉 “각 계층이 무엇을 책임져야 하는지”를 명확히 이해할 수 있었다.</p>
<pre><code class="language-java">package racingcar.service;

import racingcar.domain.moverule.MoveRule;
import racingcar.domain.racer.Racers;
import racingcar.domain.result.Result;
import racingcar.util.Parser;
import racingcar.util.generator.NumberGenerator;

import java.util.List;
import java.util.Map;

public class RacingService {

    private final Parser parser;
    private final MoveRule moveRule;
    private final Result result;

    public RacingService(Parser parser, MoveRule moveRule, Result result) {
        this.parser = parser;
        this.moveRule = moveRule;
        this.result = result;
    }

    public List&lt;String&gt; parseInputs(String input) {
        return parser.parseCarNames(input);
    }

    public Map&lt;String, Integer&gt; roundStart(Racers racers) {
        racers = racers.moveAll(moveRule);
        return result.roundResult(racers);
    }

    public String gameEnd(Racers racers) {
        return result.gameResult(racers);
    }
}</code></pre>
<p>이번 미션에서는 <code>Controller</code>와 <code>Service</code>의 역할을 분리해서 코드의 응집도를 높이고 결합도를 낮춰봤다.
<code>Controller</code>는 입출력 흐름 제어와 사용자와의 인터페이스만 담당하게 하고, <code>Service</code>는 도메인 객체 간 협력(비즈니스 로직)을 맡도록 하니 자연스럽게 의존성 방향이 “<code>View</code> → <code>Controller</code> → <code>Service</code> → <code>Domain</code>”으로 흘러갔다.</p>
<p>이전에는 <code>Controller</code>가 너무 많은 책임을 가지고 있어서 코드가 복잡해졌는데, 로직을 <code>Service</code>로 분리하고 나니 구조가 한결 깔끔해졌다. 그 과정을 거치면서 “Controller는 흐름 제어자이지 계산자가 아니다” 라는 말을 진짜 몸으로 느꼈다.</p>
<p>그러나 이번 미션에서 과연 <code>Controller</code>를 분리했을 때 큰 이점을 얻었는가에 대해서는 다소 아쉬움이 남았다. <code>Controller</code>를 직접 분리하고 적용해보면서 지금은 ‘Controller’라는 이름보다 ‘GameManager’처럼 역할이 더 드러나는 이름이 오히려 가독성을 높인다는 생각이 들었다. 이 과정을 통해 <strong>클래스의 이름은 코드의 의도를 가장 명확히 드러내는 수단</strong>이고 <strong>모든 설계 원칙은 상황과 맥락에 맞게 적용할 때 비로소 의미가 생긴다</strong>는 걸 깨달았다.</p>
<p>이번 경험을 통해 객체의 책임을 명확히 나누고, 상황에 맞는 구조를 설계하는 습관이 정말 중요하다는 걸 다시 한 번 느꼈다.</p>
<hr>
<h2 id="이번-주차에-추가된-나만의-약속">이번 주차에 추가된 나만의 약속</h2>
<figure align="center">
  <img src="https://velog.velcdn.com/images/s_miny/post/f59b51bc-3acb-4ebd-be92-0b4524709760/image.png">
  <figcaption><small>1주차 피드백 일부</small></figcaption>
</figure>

<p>2주차 시작전에 1주차 피드백을 쭉 다 읽었다. 내가 잘 지켰던 부분도 있고 내가 지키지 못했던 부분도 있었다. 그 중에서도 나는 </p>
<blockquote>
<p>에러를 만나면 바로 검색하지 말고 5분간 스스로 원인을 추측해보기</p>
</blockquote>
<p>이 부분에 눈길이 확 갔다. 지금까지는 에러가 나면 에러 메시지를 읽지도 않고 검색을 하였다. 살짝의 찔림과 양심 때문인지는 몰라도 저 문구가 쌔게 들어왔다. 그래서 이번 주차에 추가로 에러를 만나면 에러 메시지를 읽고 내가 직접 찾아서 해결해보자! 라는 목표를 세웠다. </p>
<p>개발을 하면서 에러가 많이 나타나지는 않았지만<del>(설계를 잘했나? ㅎㅎ)</del>
가장 기억에 남았던 걸 하나 보여주겠다. Controller에서 각 도메인간의 협력을 생각하며 코드를 짜던 중 발생했던 오류이다.</p>
<h3 id="1-테스트-실행-중-아래와-같은-assertionerror가-발생했다">1. 테스트 실행 중 아래와 같은 AssertionError가 발생했다.</h3>
<pre><code class="language-java">java.lang.AssertionError:
Expecting actual:
  &quot;경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)
시도할 횟수는 몇 회인가요?
실행 결과

최종 우승자 : pobi, woni&quot;
to contain:
  [&quot;pobi : -&quot;, &quot;woni : &quot;, &quot;최종 우승자 : pobi&quot;]
but could not find:
  [&quot;pobi : -&quot;, &quot;woni : &quot;]
</code></pre>
<p>출력 결과를 보면 테스트는 <code>pobi : -</code>, <code>woni :</code> 을 기대했지만
실제 프로그램에서는 <code>최종 우승자</code>만 출력되고 있었다.</p>
<h3 id="2-원인-추측-5분간-스스로-생각해보기">2. 원인 추측 (5분간 스스로 생각해보기)</h3>
<p>검색하지 않고 코드를 직접 따라가며 흐름을 확인했다.
<code>RacingController</code>의 <code>racingStart()</code> 메서드를 살펴보던 중 다음 코드가 눈에 들어왔다.</p>
<p><strong>문제 코드 (수정전)</strong></p>
<pre><code class="language-java">private void racingStart(Racers racers, Round round) {
    for (int i = 0; i &lt; round.getRoundNumber(); i++) {
        racingService.roundStart(racers); // 반환값을 사용하지 않음
        Map&lt;String, Integer&gt; roundResult = result.roundResult(racers);
        outputView.printRoundResult(roundResult);
    }
    outputView.printGameResult(racingService.gameEnd(racers));
}</code></pre>
<p><code>racingService.roundStart(racers)</code> 내부에서는</p>
<p>자동차들이 이동된 <strong>새로운 <code>Racers</code> 객체</strong>를 반환하고 있었지만 이 반환값을 다시 <code>racers</code> 변수에 <strong>갱신하지 않았던 것</strong>이 문제였다. (<del>내가 설계 해놓고 Controller 코드 짜면서 잊어버렸다...ㅠ</del>)
즉, 불변 객체로 설계된 <code>Racers</code>가 이동 후의 상태를 외부로 전달하지 못한 것이다.</p>
<p><strong>문제 코드 (수정 후)</strong></p>
<pre><code class="language-java">private void racingStart(Racers racers, Round round) {
    for (int i = 0; i &lt; round.getRoundNumber(); i++) {
        racers = racingService.roundStart(racers); // 반환값으로 갱신
        Map&lt;String, Integer&gt; roundResult = result.roundResult(racers);
        outputView.printRoundResult(roundResult);
    }
    outputView.printGameResult(racingService.gameEnd(racers));
}</code></pre>
<p><code>racingService.roundStart()</code>의 반환값을 <code>racers</code>에 재할당하여 각 라운드마다 최신 상태를 유지하도록 수정했다.
수정 후 테스트를 다시 실행하자 다음과 같이 기대한 출력이 정상적으로 표시되었다.</p>
<pre><code>실행 결과
pobi : -
woni :
최종 우승자 : pobi</code></pre><h3 id="에러를-직접-찾아-고치면서-느낀점">에러를 직접 찾아 고치면서 느낀점</h3>
<p>출력 결과와 코드를 비교하면서 메서드의 흐름을 직접 따라가 본 끝에 객체의 반환 구조가 원인이었음을 스스로 찾아낼 수 있었다.
그 결과, 단순히 오류를 고치는 데서 끝나지 않고 <strong>“왜 이 문제가 생겼는가”를 논리적으로 설명할 수 있게 되었다.</strong></p>
<p>이 과정을 통해 정답을 빨리 찾기보다는 <strong>코드의 동작 원리를 이해하고, 문제를 구조적으로 분석하게</strong> 되었다.
검색으로는 단순히 에러의 형태만 알 수 있지만 직접 코드를 추론하며 따라가면 <strong>내 코드가 왜 그렇게 동작하는지를 스스로 이해할 수 있다.</strong></p>
<p>앞으로도 에러를 만나면 무조건 검색부터 하기보다 출력 결과와 코드의 흐름을 대조하며 원인을 추측해 보는 시간을 먼저 갖으려고 한다.
또한, 이번 오류는 운이 좋게 빨리 발견하였다고 생각한다. 그래서 다음부터는 오류를 찾지 못한다면 디버깅을 사용해보려고 한다.
그 과정을 통해 단순히 문제를 해결하는 개발자가 아니라 <strong>문제를 이해하고 설명할 수 있는 개발자</strong>로 성장하고자 한다.</p>
<hr>
<h2 id="아쉬운-점feat추상화">아쉬운 점(feat.추상화)</h2>
<p>이번 자동차 경주 미션에서는 추상화의 방향과 순서에 대해 깊이 고민하게 되었다.
처음 설계할 때는 객체지향적으로 보이게 하겠다는 생각이 강했다.
그래서 자연스럽게 <code>Racer(자동차)</code>와 <code>Racers(자동차 집합)</code>를 먼저 추상화했다.
그때는 자동차의 역할을 분리한 것이 올바른 접근이라고 생각했다.</p>
<figure align="center">
  <img src="https://velog.velcdn.com/images/s_miny/post/acf8f20d-5963-4b7d-96df-e0ae7d5c6983/image.png">
  <figcaption><small>README 일부</small></figcaption>
</figure>

<p>Racer 코드</p>
<pre><code class="language-java">package racingcar.domain.racer;

import racingcar.domain.moverule.MoveRule;
import racingcar.util.generator.NumberGenerator;

public interface Racer {
    String getName();
    int getDistance();
    Racer move(MoveRule moveRule);
}</code></pre>
<p>Racers 코드</p>
<pre><code class="language-java">package racingcar.domain.racer;

import racingcar.domain.moverule.MoveRule;
import racingcar.util.generator.NumberGenerator;

import java.util.List;

public interface Racers {
    Racers moveAll(MoveRule moverule);
    List&lt;Racer&gt; getRacers();
}</code></pre>
<p>하지만 구현을 진행하면서 점점 핵심 로직인 <code>이동(move)</code>이 오히려 구체 클래스 내부에 묶여버린 것을 깨달았다.
이 문제의 핵심은 “어떻게 달릴 것인가”, 즉 <code>이동 규칙(MoveRule)</code>이었는데 나는 초기에 “누가 달리는가<code>(자동차 구조)</code>”에만 집중한 셈이었다.
결과적으로 추상화 자체가 잘못된 건 아니었지만 <strong>추상화의 우선순위를 잘못 판단한 것이 이번 미션의 아쉬운 부분이었다.</strong></p>
<p>이후 리팩터링 과정에서 <code>MoveRule</code>까지 추상화하며 이동 조건이 바뀌더라도 도메인을 수정하지 않아도 되는 구조로 개선했다.
예를 들어, 난수 대신 사용자가 직접 조건을 정의하거나, 다른 이동 규칙으로 바꾸더라도 <code>MoveRule</code> 구현체만 교체하면 된다.
그때서야 비로소 “<strong>이 문제에서 진짜 추상화가 필요한 부분은 바로 여기였구나</strong>”라는 걸 느꼈다.</p>
<p>이번 경험을 통해 “추상화는 얼마나 하느냐보다, 무엇부터 해야 하느냐가 더 중요하다”는 교훈을 얻었다.
다음 미션에서는 <strong>객체의 역할과 책임을 먼저 명확히 분리한 뒤</strong>  그 안에서 <strong>변화 가능성이 높은 것을 찾아 추상화하는 순서로 설계</strong>해볼 생각이다.
이 아쉬움이 다음 설계의 밑거름이 될 것이다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>1주차에서 느꼈던 아쉬움을 대부분 해소할 수 있었던 한 주였다.
기능 단위 커밋, 명확한 책임 분리, <code>Controller–Service</code> 구조 정리 등
지난 회고에서 다짐했던 부분들을 실제 코드로 구현해냈다는 점이 큰 의미였다.</p>
<p>물론 <code>move</code> 추상화처럼 설계 우선순위를 잘못 판단한 아쉬움도 있었지만,
그 과정을 통해 설계 단계에서 <strong>추상화를 결정하기 전에 무엇이 변할 수 있는지 한 번 더 고민</strong>해야겠다고 다짐하는 계기가 되었다.
이번 과제에서는 운이 좋게 빠르게 오류를 발견하여 디버깅을 사용하지 않았다... 
그래서 다음 과제에서는 오류를 찾지 못한다면 꼭 <strong>디버깅</strong>을 사용해보는게 목표이다!</p>
<p>이번 경험들을 다음 미션의 설계 밑거름 삼아,
더 단단한 구조와 명확한 의도를 가진 코드를 작성해 나갈 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우테코 8기 프리코스 1주차 회고록]]></title>
            <link>https://velog.io/@s_miny/%EC%9A%B0%ED%85%8C%EC%BD%94-8%EA%B8%B0-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@s_miny/%EC%9A%B0%ED%85%8C%EC%BD%94-8%EA%B8%B0-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Fri, 17 Oct 2025 07:41:41 GMT</pubDate>
            <description><![CDATA[<p>드디어 우테코 프리코스 1주차가 시작되었다.
그동안 회고를 하더라도 단순히 머릿속으로만 정리하거나 노션에 짧게 메모하는 수준이었다.
하지만 이번 프리코스가 시작되면서 벨로그를 통해 ‘제대로 된 회고록’을 써보자고 마음먹었다.
이 회고록을 통해 스스로를 돌아보는 힘을 키워, 한 단계 더 성장하는 개발자가 되고 싶다.</p>
<hr>
<h2 id="1주차-과제--문자열-덧셈-계산기">1주차 과제 : 문자열 덧셈 계산기</h2>
<p>1주차의 미션은 문자열 덧셈 계산기였다.
프리코스의 진행 방식과 기능 요구사항은 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/s_miny/post/0f51d2ef-e1c3-4c44-a862-471ff7bf8dc3/image.png" alt="">
<img src="https://velog.velcdn.com/images/s_miny/post/10ffa00c-776d-4082-9113-423e81e2be03/image.png" alt=""></p>
<p>우테코 프리코스를 시작하기 전, 『객체지향의 사실과 오해』를 읽으며 설계 감각을 키웠고, 이전 기수 문제를 풀며 연습을 해왔다.
그런데 진행 방식을 읽던 중</p>
<blockquote>
<p>기능 단위로 커밋하는 방식으로 진행한다.</p>
</blockquote>
<p>라는 문장에 눈이 딱 멈췄다. ‘이거다’ 싶었다. 
이전에는 객체지향 설계에만 몰두하느라 커밋 관리가 엉망이었고, 커밋 타이밍을 잡지 못해 히스토리를 엉성하게 남겼다.
그 경험이 후회로 남아 있었기에, 이번 프리코스 1주차를 시작하면서 &#39;<strong>기능 단위 커밋을 지키며 객체지향적으로 설계하자!!</strong>&#39;는 목표를 세웠다.</p>
<h2 id="과정을-돌아보며">과정을 돌아보며</h2>
<p>기능 단위 커밋을 실천하기 위해 우선 기능 목록을 작성했다. (<del>물론 완벽하지 않다... 나중에 이유가 나옴...</del>)
<img src="https://velog.velcdn.com/images/s_miny/post/cdf66da1-339f-442b-8cb2-c0322cebcfad/image.png" alt=""></p>
<p>처음엔 “그냥 기능 하나 만들고 커밋하면 되겠지”라고 생각했다.
그래서 가장 단순한 기능인 <strong>‘빈 문자열 입력 시 0 반환’</strong>을 먼저 구현했다.</p>
<pre><code class="language-java">package calculator.domain;

public class Calculator {

    public int calculate(String input) {
        String resultValue = convertEmptyToZero(input);
        return Integer.parseInt(resultValue);
    }

    private String convertEmptyToZero(String input) {
        if (input.isEmpty()) {
            return &quot;0&quot;;
        }
        return input;
    }
}</code></pre>
<p>그런데 막상 커밋을 하려는데 문득 의문이 들었다.
“기능 단위 커밋은 좋지만, <strong>코드가 제대로 동작하는지도 확인하지 않고 커밋하는 게 맞을까?</strong>”
그 순간, 나는 테스트 코드를 작성해야 한다는 생각에 이르렀다.</p>
<pre><code class="language-java">@Test
    void 빈문자열을_입력하면_0을_반환한다() {
        Calculator c = new Calculator();
        assertThat(c.calculate(&quot;&quot;)).isEqualTo(0);
    }</code></pre>
<p>기능 단위 커밋을 지키려는 시도가 결국 테스트 코드 작성으로 이어졌다.
<strong>이때 소름이 돋았다.</strong>
“<em>기능 단위 커밋을 지키다 보니, 자연스럽게 어렵게만 보였던 TDD의 첫걸음을 밟고 있구나</em>..”</p>
<p>물론 완벽한 TDD는 아니었다.
하지만 테스트 코드를 작성하며 기능 단위로 검증하고, 확신을 가지고 커밋할 수 있었다.</p>
<p>또 하나 놀라운 점은, 기능 단위로 커밋하다 보니 예외나 빠진 기능이 자연스럽게 떠오른다는 것이었다.
그래서 README를 여러 번 수정했고, 처음엔 이게 맞나 싶었지만 
나중에 찾아보니 이것이 바로 <strong>문서 주도 개발(Doc-driven Development)</strong> 이었다.</p>
<blockquote>
<p>문서도 코드처럼 살아 움직여야 한다.</p>
</blockquote>
<p>결국, 기능 단위 커밋을 실천하면서 자연스럽게
테스트 코드와 문서 주도 개발이라는 두 가지 습관까지 얻을 수 있었다.
<img src="https://velog.velcdn.com/images/s_miny/post/788f4cad-792a-435b-a2a6-8c613220fbcd/image.png" alt=""></p>
<p>그리고 한 가지 더 고민했던 부분이 있었다.
바로 입출력 흐름을 어디서 처리할지에 대한 문제였다.
이번 과제는 규모가 작기 때문에 굳이 <code>Controller</code>를 분리하지 않고, <code>Application</code> 클래스에서 입출력과 도메인 흐름을 함께 처리했다.</p>
<pre><code class="language-java">package calculator;

import calculator.domain.Calculator;
import calculator.domain.Delimiter;
import calculator.view.IO;

public class Application {
    public static void main(String[] args) {
        IO io = new IO();
        Delimiter delimiter = new Delimiter();
        Calculator calculator = new Calculator(delimiter);

        String input = io.input();
        int result = calculator.calculate(input);
        io.printResult(result);
    }
}
</code></pre>
<p>예외는 도메인 내부에서 충분히 검증되도록 구현했기에, 별도로 <code>try-catch</code>로 잡을 필요는 없다고 판단했다.
다만, 이 과제에서 이 구조가 정말 최선인지에 대해서는 아직 확신이 없다. 예외를 명확히 전달하고 구조를 분리하는 관점에서 보면 <code>Controller</code>를 도입하는 게 더 좋을 수도 있을 것 같기도 하다. 다음 과제에서는 <code>Controller</code>를 만들어 직접 비교해볼 예정이다.</p>
<h2 id="-리펙터링-메모---delimiter-↔-operand-책임-재정의">+ 리펙터링 메모 - Delimiter ↔ Operand 책임 재정의</h2>
<p>구현 중 뒤늦게 보니, <code>Operand</code>를 만들어두고도 숫자 검증을 <code>Delimiter</code>에서 하고 있었다.
<code>VALIDATE_DEFAULT</code>가 <code>0-9</code> 범위까지 포함해 숫자/음수 여부를 사실상 판단하고 있었던 것이다.</p>
<p>Before (문제 코드 부분)</p>
<pre><code class="language-java">private static final String DEFAULT_DELIMITER = &quot;[&quot; + BASE_DELIMS + &quot;]&quot;;
private static final String VALIDATE_DEFAULT = &quot;.*[^0-9&quot; + BASE_DELIMS + &quot;].*&quot;;

private List&lt;String&gt; splitByDefaultDelimiter(String inputs) {
        if (inputs.matches(VALIDATE_DEFAULT)) { // 숫자/구분자 외 문자면 예외
            throw new IllegalArgumentException(&quot;기본 구분자(&quot; + BASE_DELIMS + &quot;)만 허용됩니다.&quot;);
        }
        validateDelimiterSequence(inputs);
        return Arrays.stream(inputs.split(DEFAULT_DELIMITER)).toList();
}</code></pre>
<p>After (개선 코드, 핵심만)</p>
<pre><code class="language-java">// 기본 구분자(, :) 외 기호 금지 (하이픈은 허용해 음수는 Operand에서 판별)
private static final String HAS_NON_DEFAULT_DELIMS =
    &quot;.*[\\p{Punct}&amp;&amp;[^,:\\-]].*&quot;;

// 공백/기본 구분자 제외 토큰 + 올바른 배치(선행/후행/연속 금지)
private static final String TOKEN = &quot;[^\\s,:]+&quot;;
private static final String WELL_FORMED = &quot;^&quot; + TOKEN + &quot;(?:[, :]&quot; + TOKEN + &quot;)*$&quot;;

private void assertOnlyDefaultDelims(String s) {
    if (s.matches(HAS_NON_DEFAULT_DELIMS)) {
        throw new IllegalArgumentException(&quot;정해진 구분자 외 문자는 사용할 수 없습니다.&quot;);
    }
}

private void assertWellFormed(String s) {
    if (!s.matches(WELL_FORMED)) {
        throw new IllegalArgumentException(&quot;구분자 사용이 올바르지 않습니다. (선행/후행/연속 금지)&quot;);
    }
}</code></pre>
<p>정책 정리</p>
<ul>
<li><p>Delimiter: “구분자 형식/배치 검증 + 분리”만 한다</p>
<ul>
<li>기본 구분자 <code>, :</code>만 허용 및 하이픈 <code>-</code>는 토큰 문자로 허용</li>
<li>구분자 배치 규칙: <code>TOKEN ( DELIM TOKEN )*</code> (선행/후행/연속 금지)</li>
<li>커스텀 헤더 <code>&quot;//&quot; + &quot;\\n&quot;</code> 지원, 커스텀 구분자는 <code>,</code>로 정규화</li>
</ul>
</li>
<li><p>Operand: “숫자/음수 검증” 전담</p>
</li>
</ul>
<p>테스트 검증</p>
<pre><code class="language-java">// Delimiter 단위
assertThatThrownBy(() -&gt; delimiter.split(&quot;1;2;3&quot;))  // 기본 구분자 외 문자
    .isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -&gt; delimiter.split(&quot;1,,2&quot;))   // 연속 구분자
    .isInstanceOf(IllegalArgumentException.class);
assertThat(delimiter.split(&quot;//;\\n1;2,3&quot;))
    .containsExactly(&quot;1&quot;, &quot;2&quot;, &quot;3&quot;);                // 커스텀 정상 + 정규화

// Operand 단위
assertThatThrownBy(() -&gt; new Operand(&quot;-2&quot;))         // 음수
    .isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -&gt; new Operand(&quot;a&quot;))          // 비숫자
    .isInstanceOf(IllegalArgumentException.class);</code></pre>
<p>Delimiter가 숫자/음수까지 검증하던 문제를 확인하고 리펙터하였다.
“형식/배치”는 Delimiter, “숫자/음수”는 Operand로 책임을 분리하고, 정책상 기본 구분자(, :) 외 기호는 차단하되 하이픈(-)은 토큰 문자로 허용했다.
덕분에 &quot;1,-2,3&quot;은 Delimiter를 통과하고 Operand에서 음수로 예외가 나며, 테스트 책임이 명확해졌다.</p>
<p>다음부터는 입력 정책(무엇을 허용/금지할지)과 책임 경계(누가 무엇을 검증할지)를 사전 체크리스트로 확정하고, 이를 기준으로 설계·리팩터링을 수행하겠다.</p>
<h2 id="배운점">배운점</h2>
<p>이번 1주차에서 가장 크게 배운 것은 두 가지다.</p>
<ol>
<li><p>테스트 기반 개발의 시작점</p>
<ul>
<li>기능 단위 커밋을 위해 테스트 코드를 작성했고, 그 과정에서 TDD의 개념을 체감했다.</li>
</ul>
</li>
<li><p>문서 주도 개발의 중요성</p>
<ul>
<li>README를 지속적으로 업데이트하며 문서가 코드의 흐름과 함께 진화해야 함을 느꼈다.</li>
</ul>
</li>
</ol>
<p>처음엔 단순히 “기능 단위 커밋을 잘 해보자”에서 출발했지만,
결과적으로 테스트와 문서화까지 이어진 커다란 배움을 얻었다.</p>
<h2 id="아쉬운-점">아쉬운 점</h2>
<ol>
<li><p>테스트 코드 작성이 아직 미숙해, 단위 테스트의 개념을 더 공부할 필요가 있다.</p>
</li>
<li><p>커밋 시 ‘기능 커밋’과 ‘테스트 커밋’을 분리하지 못했다.</p>
</li>
<li><p>Controller 분리에 대한 판단이 남았다.</p>
</li>
<li><p>구현 전 입력 정책과 책임 경계를 확정하지 못했다.</p>
</li>
</ol>
<h2 id="마무리">마무리</h2>
<p>1주차는 단순한 과제 이상의 경험이었다.
‘잘 작동하는 코드’보다 ‘읽기 좋은 코드’의 중요성을 체감했고,
기능 단위 커밋이라는 습관이 생각보다 강력한 학습 도구임을 알게 되었다.</p>
<p>다음 주에는 이번에 부족했던 점을 보완하며,
더 읽기 쉽고 협력하기 좋은 코드를 만드는 개발자로 성장하고 싶다.</p>
]]></description>
        </item>
    </channel>
</rss>