<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>loaded_diaper.log</title>
        <link>https://velog.io/</link>
        <description>끝까지 가면 내가 다 이겨</description>
        <lastBuildDate>Mon, 29 Jun 2026 03:14:41 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>loaded_diaper.log</title>
            <url>https://velog.velcdn.com/images/loaded_diaper/profile/945e6299-d16c-43ec-9e8c-6b7571a3737a/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. loaded_diaper.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/loaded_diaper" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[CH5 플러스 스프링 모의면접 질문범위]]></title>
            <link>https://velog.io/@loaded_diaper/CH5-%ED%94%8C%EB%9F%AC%EC%8A%A4-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%AA%A8%EC%9D%98%EB%A9%B4%EC%A0%91-%EC%A7%88%EB%AC%B8%EB%B2%94%EC%9C%84</link>
            <guid>https://velog.io/@loaded_diaper/CH5-%ED%94%8C%EB%9F%AC%EC%8A%A4-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%AA%A8%EC%9D%98%EB%A9%B4%EC%A0%91-%EC%A7%88%EB%AC%B8%EB%B2%94%EC%9C%84</guid>
            <pubDate>Mon, 29 Jun 2026 03:14:41 GMT</pubDate>
            <description><![CDATA[<h1 id="ch5-플러스-스프링-모의면접-예상질문-정리">CH5 플러스 스프링 모의면접 예상질문 정리</h1>
<blockquote>
<p>QueryDSL, Cache, Redis, 인덱스, 쿼리 튜닝, 동시성, AOP, 테스트 영역에서 나올 수 있는 면접 질문과 핵심 답변을 정리했습니다.</p>
</blockquote>
<hr>
<h1 id="querydsl-영역">QueryDSL 영역</h1>
<h2 id="q1-querydsl의-booleanexpression을-활용한-동적-쿼리-작성-방식을-설명해주세요">Q1. QueryDSL의 BooleanExpression을 활용한 동적 쿼리 작성 방식을 설명해주세요.</h2>
<p><strong>A.</strong></p>
<p>QueryDSL에서는 검색 조건을 <code>BooleanExpression</code> 메서드로 분리해서 동적 쿼리를 만들 수 있습니다.</p>
<p>예를 들어 검색어, 카테고리, 상태값이 있을 때 값이 존재하면 조건을 반환하고, 값이 없으면 <code>null</code>을 반환합니다.</p>
<pre><code class="language-java">private BooleanExpression keywordContains(String keyword) {
    if (keyword == null || keyword.isBlank()) {
        return null;
    }

    return product.name.contains(keyword);
}</code></pre>
<p>그리고 <code>where()</code>에 여러 조건을 넣으면 필요한 조건만 적용됩니다.</p>
<pre><code class="language-java">.where(
    keywordContains(keyword),
    categoryEq(category),
    statusEq(status)
)</code></pre>
<p>이 방식은 조건이 많아져도 코드가 깔끔하고, 각 조건을 메서드로 분리할 수 있어 유지보수에 좋습니다.</p>
<h3 id="꼬리질문-1-booleanexpression을-null로-반환하는-방식이-왜-null-safe하다고-하나요">꼬리질문 1. BooleanExpression을 null로 반환하는 방식이 왜 null-safe하다고 하나요?</h3>
<p>QueryDSL의 <code>where()</code>는 여러 조건 중 <code>null</code>인 조건을 무시합니다.</p>
<p>그래서 검색 조건이 없을 때 억지로 <code>1=1</code> 같은 조건을 만들 필요가 없습니다.</p>
<p>즉, 조건이 있으면 적용되고 없으면 제외되기 때문에 동적 쿼리를 안전하고 간결하게 작성할 수 있습니다.</p>
<h3 id="꼬리질문-2-querydsl-없이-jpa-specification으로도-동적-쿼리를-만들-수-있는데-querydsl을-선택한-이유가-있나요">꼬리질문 2. QueryDSL 없이 JPA Specification으로도 동적 쿼리를 만들 수 있는데, QueryDSL을 선택한 이유가 있나요?</h3>
<p>JPA Specification도 동적 쿼리를 만들 수 있지만, QueryDSL은 타입 안정성이 좋고 코드 가독성이 더 좋다고 느꼈습니다.</p>
<p>특히 복잡한 검색 조건, 정렬, 페이징, 조인 쿼리를 작성할 때 QueryDSL은 Java 코드처럼 읽히기 때문에 유지보수에 유리합니다.</p>
<p>또한 QClass를 기반으로 필드명을 컴파일 시점에 확인할 수 있어 문자열 기반 쿼리보다 실수를 줄일 수 있습니다.</p>
<hr>
<h2 id="q2-querydsl로-페이징을-구현할-때-count-쿼리를-분리해서-최적화하는-이유가-무엇인가요">Q2. QueryDSL로 페이징을 구현할 때 count 쿼리를 분리해서 최적화하는 이유가 무엇인가요?</h2>
<p><strong>A.</strong></p>
<p>페이징을 할 때는 실제 데이터를 가져오는 content 쿼리와 전체 개수를 구하는 count 쿼리가 필요합니다.</p>
<p>하지만 content 쿼리에 필요한 join이 count 쿼리에는 필요하지 않은 경우가 많습니다.</p>
<p>그래서 count 쿼리를 따로 작성하면 불필요한 join을 줄여 성능을 개선할 수 있습니다.</p>
<pre><code class="language-text">content query: 실제 목록 조회
count query: 전체 개수 조회</code></pre>
<p>데이터가 많아질수록 count 쿼리 비용도 커지기 때문에, 별도로 최적화하는 것이 중요합니다.</p>
<h3 id="꼬리질문-1-count-쿼리에서-불필요한-join을-제거하면-왜-성능이-개선되나요">꼬리질문 1. count 쿼리에서 불필요한 join을 제거하면 왜 성능이 개선되나요?</h3>
<p>join이 많으면 DB가 여러 테이블을 결합하고 조건을 비교해야 합니다.</p>
<p>하지만 count 쿼리는 보통 “조건에 맞는 데이터 개수”만 필요하기 때문에, 응답 DTO를 만들기 위한 join이나 fetch join은 필요 없는 경우가 많습니다.</p>
<p>예를 들어 상품 목록 조회에서 판매자 이름을 보여주기 위해 seller를 join했다면, 전체 상품 개수를 세는 count 쿼리에서는 seller join이 필요 없을 수 있습니다.</p>
<p>이런 join을 제거하면 DB가 읽어야 하는 데이터와 연산량이 줄어 성능이 개선됩니다.</p>
<h3 id="꼬리질문-2-offset-페이징과-no-offset-페이징의-성능-차이가-데이터가-많아질수록-커지는-이유를-설명해주세요">꼬리질문 2. Offset 페이징과 No-Offset 페이징의 성능 차이가 데이터가 많아질수록 커지는 이유를 설명해주세요.</h3>
<p>Offset 페이징은 <code>offset</code>만큼 데이터를 건너뛴 뒤 필요한 데이터를 가져옵니다.</p>
<p>예를 들어 100만 번째 데이터부터 20개를 가져오려면 DB는 앞의 100만 개를 건너뛰는 작업을 해야 합니다.</p>
<p>반면 No-Offset 페이징은 마지막으로 조회한 ID나 기준값 이후의 데이터를 조회합니다.</p>
<pre><code class="language-text">Offset 방식:
LIMIT 20 OFFSET 1000000

No-Offset 방식:
WHERE id &lt; 마지막으로 조회한 id
LIMIT 20</code></pre>
<p>데이터가 많아질수록 Offset 방식은 건너뛰는 비용이 커지고, No-Offset 방식은 인덱스를 활용해 다음 데이터를 바로 찾을 수 있어 더 효율적입니다.</p>
<hr>
<h1 id="cache-영역">Cache 영역</h1>
<h2 id="q3-cacheable을-적용할-때-캐시-키를-어떻게-설계했나요">Q3. @Cacheable을 적용할 때 캐시 키를 어떻게 설계했나요?</h2>
<p><strong>A.</strong></p>
<p>캐시 키는 같은 요청이면 같은 캐시를 사용하고, 다른 요청이면 다른 캐시를 사용하도록 설계해야 합니다.</p>
<p>예를 들어 상품 검색 결과를 캐싱한다면 검색어, 카테고리, 정렬 조건, 페이지 번호, 페이지 크기 등을 키에 포함해야 합니다.</p>
<pre><code class="language-java">@Cacheable(
    value = &quot;productSearch&quot;,
    key = &quot;#keyword + &#39;:&#39; + #category + &#39;:&#39; + #page + &#39;:&#39; + #size&quot;
)</code></pre>
<p>캐시 키를 너무 단순하게 만들면 서로 다른 검색 조건이 같은 캐시를 공유하는 문제가 생길 수 있습니다.</p>
<p>반대로 너무 복잡하게 만들면 캐시 재사용률이 떨어질 수 있습니다.</p>
<h3 id="꼬리질문-1-캐시가-적용된-메서드를-같은-클래스-내부에서-호출했을-때-캐시가-동작하지-않는-경우가-있습니다-원인이-무엇인가요">꼬리질문 1. 캐시가 적용된 메서드를 같은 클래스 내부에서 호출했을 때 캐시가 동작하지 않는 경우가 있습니다. 원인이 무엇인가요?</h3>
<p>Spring의 <code>@Cacheable</code>은 프록시 기반 AOP로 동작합니다.</p>
<p>외부에서 프록시 객체를 통해 메서드를 호출해야 캐시 로직이 적용됩니다.</p>
<p>하지만 같은 클래스 내부에서 <code>this.method()</code>처럼 호출하면 프록시를 거치지 않기 때문에 <code>@Cacheable</code>이 동작하지 않을 수 있습니다.</p>
<p>이것을 Self-invocation 문제라고 합니다.</p>
<h3 id="꼬리질문-2-cacheput과-cacheevict를-동시에-쓰는-방식과-db-갱신-후-캐시를-무효화하는-방식-중-어떤-기준으로-선택하나요">꼬리질문 2. @CachePut과 @CacheEvict를 동시에 쓰는 방식과, DB 갱신 후 캐시를 무효화하는 방식 중 어떤 기준으로 선택하나요?</h3>
<p><code>@CachePut</code>은 DB를 갱신한 뒤 캐시도 즉시 최신 값으로 갱신할 때 사용합니다.</p>
<p><code>@CacheEvict</code>는 DB 갱신 후 기존 캐시를 삭제하고, 다음 조회 시 다시 캐싱하게 할 때 사용합니다.</p>
<table>
<thead>
<tr>
<th>방식</th>
<th>사용 상황</th>
</tr>
</thead>
<tbody><tr>
<td><code>@CachePut</code></td>
<td>갱신 결과가 명확하고 캐시에 바로 반영하고 싶을 때</td>
</tr>
<tr>
<td><code>@CacheEvict</code></td>
<td>여러 캐시 키에 영향이 있거나 갱신 범위가 넓을 때</td>
</tr>
</tbody></table>
<p>검색 결과 캐시처럼 하나의 데이터 변경이 여러 페이지, 여러 검색 조건에 영향을 주는 경우에는 캐시를 직접 갱신하기보다 무효화하는 방식이 더 안전합니다.</p>
<hr>
<h2 id="q4-redis-ttl-설정-기준을-어떻게-결정했나요">Q4. Redis TTL 설정 기준을 어떻게 결정했나요?</h2>
<p><strong>A.</strong></p>
<p>Redis TTL은 데이터의 변경 빈도와 실시간성이 얼마나 중요한지 기준으로 결정합니다.</p>
<p>자주 바뀌지 않고 조회가 많은 데이터는 TTL을 길게 가져가고, 자주 바뀌는 데이터는 짧게 가져갑니다.</p>
<p>예를 들어 다음과 같이 설정할 수 있습니다.</p>
<table>
<thead>
<tr>
<th>데이터</th>
<th>TTL 기준</th>
</tr>
</thead>
<tbody><tr>
<td>상품 상세</td>
<td>비교적 길게</td>
</tr>
<tr>
<td>인기 검색어</td>
<td>짧게</td>
</tr>
<tr>
<td>재고 정보</td>
<td>매우 짧게 또는 캐시 제외</td>
</tr>
<tr>
<td>이벤트 상품 목록</td>
<td>이벤트 기간과 변경 빈도에 맞게</td>
</tr>
</tbody></table>
<p>특히 재고처럼 정확성이 중요한 데이터는 캐시보다 DB나 Redis 원자 연산을 기준으로 처리하는 것이 안전합니다.</p>
<h3 id="꼬리질문-1-ttl이-동시에-만료된-다수의-키에-대량-요청이-몰리면-어떤-문제가-생기나요">꼬리질문 1. TTL이 동시에 만료된 다수의 키에 대량 요청이 몰리면 어떤 문제가 생기나요?</h3>
<p>여러 캐시가 동시에 만료되고 요청이 몰리면 많은 요청이 한꺼번에 DB로 향할 수 있습니다.</p>
<p>이것을 Cache Stampede 또는 Cache Avalanche라고 합니다.</p>
<p>완화 방법은 다음과 같습니다.</p>
<ul>
<li>TTL에 랜덤 값을 추가해 만료 시점을 분산</li>
<li>캐시 재생성 시 락을 사용해 한 요청만 DB 조회</li>
<li>인기 데이터는 만료 전에 미리 갱신</li>
<li>너무 짧은 TTL을 피함</li>
</ul>
<h3 id="꼬리질문-2-redis-캐시와-db-데이터가-불일치하는-상황이-실제로-생길-수-있는-시나리오를-하나-들어보세요">꼬리질문 2. Redis 캐시와 DB 데이터가 불일치하는 상황이 실제로 생길 수 있는 시나리오를 하나 들어보세요.</h3>
<p>예를 들어 상품 가격을 DB에서 수정했는데 기존 상품 상세 캐시를 삭제하지 않으면 문제가 생길 수 있습니다.</p>
<p>DB에는 10,000원으로 수정되었지만 Redis에는 이전 가격인 12,000원이 남아 있을 수 있습니다.</p>
<p>이 상태에서 사용자가 상품을 조회하면 오래된 가격을 보게 됩니다.</p>
<p>이런 문제를 줄이려면 DB 수정 후 관련 캐시를 무효화하거나 TTL을 짧게 설정해야 합니다.</p>
<hr>
<h1 id="인덱스--쿼리-튜닝-영역">인덱스 / 쿼리 튜닝 영역</h1>
<h2 id="q5-복합-인덱스를-설계할-때-컬럼-순서를-어떤-기준으로-결정했나요">Q5. 복합 인덱스를 설계할 때 컬럼 순서를 어떤 기준으로 결정했나요?</h2>
<p><strong>A.</strong></p>
<p>복합 인덱스는 자주 사용되는 조회 조건과 정렬 조건을 기준으로 설계합니다.</p>
<p>일반적으로 다음 기준을 고려합니다.</p>
<ol>
<li>자주 사용되는 조건인지</li>
<li>동등 조건인지 범위 조건인지</li>
<li>카디널리티가 높은 컬럼인지</li>
<li>정렬에 사용되는 컬럼인지</li>
<li>실제 쿼리의 where, order by 순서와 맞는지</li>
</ol>
<p>예를 들어 프로모션 상품을 상태와 시작 시간 기준으로 조회한다면 다음과 같은 인덱스를 고려할 수 있습니다.</p>
<pre><code class="language-sql">(status, start_at, id)</code></pre>
<p>단, 무조건 컬럼을 많이 넣는 것이 좋은 것은 아니고, 실제 실행 계획을 보고 결정해야 합니다.</p>
<h3 id="꼬리질문-1-복합-인덱스에서-선행-컬럼을-조건에-포함하지-않고-후행-컬럼만-조회하면-어떻게-동작하나요">꼬리질문 1. 복합 인덱스에서 선행 컬럼을 조건에 포함하지 않고 후행 컬럼만 조회하면 어떻게 동작하나요?</h3>
<p>복합 인덱스는 왼쪽부터 순서대로 사용됩니다.</p>
<p>예를 들어 <code>(status, category_id, created_at)</code> 인덱스가 있을 때 <code>category_id</code>만 조건에 사용하면 인덱스를 제대로 활용하기 어렵습니다.</p>
<pre><code class="language-sql">-- 인덱스 활용에 유리
WHERE status = &#39;OPEN&#39; AND category_id = 1

-- 선행 컬럼이 빠져 인덱스 활용이 어려울 수 있음
WHERE category_id = 1</code></pre>
<p>그래서 복합 인덱스를 만들 때는 실제 쿼리에서 자주 함께 사용되는 조건 순서를 고려해야 합니다.</p>
<h3 id="꼬리질문-2-카디널리티가-낮은-컬럼에-단독-인덱스를-생성하는-것이-비효율적인-이유를-설명해주세요">꼬리질문 2. 카디널리티가 낮은 컬럼에 단독 인덱스를 생성하는 것이 비효율적인 이유를 설명해주세요.</h3>
<p>카디널리티가 낮다는 것은 값의 종류가 적다는 뜻입니다.</p>
<p>예를 들어 성별, 판매 상태처럼 값의 종류가 적은 컬럼은 인덱스를 타도 많은 행을 읽어야 할 수 있습니다.</p>
<p>이 경우 DB 입장에서는 인덱스를 사용하는 것보다 테이블 전체를 읽는 것이 더 빠를 수도 있습니다.</p>
<p>그래서 카디널리티가 낮은 컬럼은 단독 인덱스보다 다른 조건과 함께 복합 인덱스로 설계하는 경우가 많습니다.</p>
<hr>
<h2 id="q6-explain-실행-계획에서-type-컬럼이-all로-표시될-때-어떤-의미이고-어떻게-접근해서-개선했나요">Q6. EXPLAIN 실행 계획에서 type 컬럼이 ALL로 표시될 때 어떤 의미이고, 어떻게 접근해서 개선했나요?</h2>
<p><strong>A.</strong></p>
<p><code>type</code>이 <code>ALL</code>로 표시된다는 것은 Full Table Scan을 의미합니다.</p>
<p>즉, 조건에 맞는 데이터를 찾기 위해 테이블 전체를 읽는다는 뜻입니다.</p>
<p>개선할 때는 다음 순서로 접근합니다.</p>
<ol>
<li>실제 쿼리의 <code>WHERE</code>, <code>ORDER BY</code>, <code>JOIN</code> 조건 확인</li>
<li>사용 가능한 인덱스가 있는지 확인</li>
<li>인덱스 컬럼 순서가 쿼리 조건과 맞는지 확인</li>
<li>불필요한 join이나 select 컬럼 제거</li>
<li>데이터 분포와 카디널리티 확인</li>
<li>다시 <code>EXPLAIN</code>으로 실행 계획 확인</li>
</ol>
<p>단순히 인덱스를 추가하는 것이 아니라, 실제 쿼리 패턴과 실행 계획을 함께 봐야 합니다.</p>
<h3 id="꼬리질문-1-인덱스가-존재하는데도-옵티마이저가-full-scan을-선택하는-경우가-있습니다-어떤-상황인가요">꼬리질문 1. 인덱스가 존재하는데도 옵티마이저가 Full Scan을 선택하는 경우가 있습니다. 어떤 상황인가요?</h3>
<p>인덱스가 있어도 옵티마이저가 Full Scan이 더 빠르다고 판단하면 인덱스를 사용하지 않을 수 있습니다.</p>
<p>대표적인 경우는 다음과 같습니다.</p>
<ul>
<li>조건에 맞는 데이터가 너무 많을 때</li>
<li>카디널리티가 낮은 컬럼일 때</li>
<li>인덱스 컬럼에 함수나 연산을 적용했을 때</li>
<li>LIKE 검색이 <code>%keyword</code>처럼 앞에 와일드카드로 시작할 때</li>
<li>통계 정보가 오래되었을 때</li>
<li>조회 컬럼이 많아 결국 테이블 접근이 많이 필요한 경우</li>
</ul>
<h3 id="꼬리질문-2-인덱스를-추가했는데-오히려-쓰기-성능이-나빠질-수-있습니다-어떤-트레이드오프가-있나요">꼬리질문 2. 인덱스를 추가했는데 오히려 쓰기 성능이 나빠질 수 있습니다. 어떤 트레이드오프가 있나요?</h3>
<p>인덱스는 조회 성능을 높일 수 있지만, 쓰기 작업에는 비용이 추가됩니다.</p>
<p>데이터를 INSERT, UPDATE, DELETE할 때 테이블뿐만 아니라 인덱스도 함께 갱신해야 하기 때문입니다.</p>
<p>따라서 인덱스가 많아질수록 저장 공간도 늘어나고 쓰기 성능은 떨어질 수 있습니다.</p>
<p>그래서 인덱스는 “조회에 자주 사용되는 쿼리”를 기준으로 필요한 만큼만 추가해야 합니다.</p>
<hr>
<h1 id="동시성-영역">동시성 영역</h1>
<h2 id="q7-재고-차감처럼-동시성-이슈가-생기는-도메인-로직에-낙관적-락과-비관적-락-중-무엇을-선택했고-그-이유가-무엇인가요">Q7. 재고 차감처럼 동시성 이슈가 생기는 도메인 로직에 낙관적 락과 비관적 락 중 무엇을 선택했고, 그 이유가 무엇인가요?</h2>
<p><strong>A.</strong></p>
<p>재고 차감처럼 초과 판매가 발생하면 안 되는 로직에서는 비관적 락을 선택할 수 있습니다.</p>
<p>비관적 락은 데이터를 수정하기 전에 먼저 락을 걸어서 다른 트랜잭션이 동시에 수정하지 못하게 막습니다.</p>
<p>재고가 적고 요청이 몰리는 선착순 이벤트 상황에서는 충돌 가능성이 높기 때문에, 낙관적 락보다 비관적 락이 더 단순하고 안전할 수 있습니다.</p>
<p>다만 비관적 락은 락 대기 시간이 길어지면 성능 저하가 발생할 수 있으므로 트랜잭션 범위를 짧게 가져가야 합니다.</p>
<h3 id="꼬리질문-1-낙관적-락에서-충돌이-발생했을-때-재시도-로직은-어떻게-구현했나요">꼬리질문 1. 낙관적 락에서 충돌이 발생했을 때 재시도 로직은 어떻게 구현했나요?</h3>
<p>낙관적 락은 <code>version</code> 컬럼을 기준으로 충돌을 감지합니다.</p>
<p>동시에 여러 요청이 같은 재고를 수정하면 먼저 커밋한 요청만 성공하고, 나중 요청은 버전 불일치로 예외가 발생합니다.</p>
<p>이때 재시도 로직을 둘 수 있습니다.</p>
<pre><code class="language-text">1. 재고 조회
2. 재고 차감 시도
3. OptimisticLockException 발생
4. 짧은 대기 후 재시도
5. 최대 재시도 횟수 초과 시 실패 처리</code></pre>
<p>재시도 횟수는 무한정으로 두면 안 되고, 예를 들어 3회처럼 제한을 두는 것이 안전합니다.</p>
<h3 id="꼬리질문-2-비관적-락-사용-시-데드락이-발생할-수-있는-시나리오를-설명해주세요">꼬리질문 2. 비관적 락 사용 시 데드락이 발생할 수 있는 시나리오를 설명해주세요.</h3>
<p>데드락은 두 트랜잭션이 서로가 가진 락을 기다리면서 멈추는 상황입니다.</p>
<p>예를 들어 주문 A는 상품 1번을 먼저 잠그고 상품 2번을 기다리고, 주문 B는 상품 2번을 먼저 잠그고 상품 1번을 기다리면 데드락이 발생할 수 있습니다.</p>
<p>이를 줄이려면 여러 row에 락을 걸 때 항상 같은 순서로 접근해야 합니다.</p>
<p>예를 들어 상품 ID 오름차순으로 정렬한 뒤 락을 획득하는 방식이 있습니다.</p>
<hr>
<h2 id="q8-redis-기반-분산-락을-구현할-때-락을-보유한-서버가-갑자기-죽으면-어떻게-되나요">Q8. Redis 기반 분산 락을 구현할 때, 락을 보유한 서버가 갑자기 죽으면 어떻게 되나요?</h2>
<p><strong>A.</strong></p>
<p>Redis 분산 락은 보통 TTL을 함께 설정합니다.</p>
<p>락을 획득한 서버가 갑자기 죽더라도 TTL이 지나면 락이 자동으로 만료되어 다른 서버가 다시 락을 획득할 수 있습니다.</p>
<p>만약 TTL 없이 락을 잡으면 서버 장애 시 락이 영원히 풀리지 않는 문제가 생길 수 있습니다.</p>
<p>그래서 분산 락에서는 다음이 중요합니다.</p>
<ul>
<li>락에 TTL 설정</li>
<li>락 소유자 식별값 저장</li>
<li>락 해제 시 내가 잡은 락인지 확인</li>
<li>비즈니스 로직보다 TTL이 너무 짧지 않게 설정</li>
</ul>
<h3 id="꼬리질문-1-lettuce의-스핀-락-방식과-redisson의-pubsub-방식-중-어떤-상황에서-redisson이-더-적합한가요">꼬리질문 1. Lettuce의 스핀 락 방식과 Redisson의 pub/sub 방식 중 어떤 상황에서 Redisson이 더 적합한가요?</h3>
<p>Lettuce로 직접 분산 락을 구현하면 보통 락을 얻을 때까지 Redis에 반복 요청하는 스핀 락 방식이 됩니다.</p>
<p>요청이 적으면 단순해서 사용할 수 있지만, 요청이 많으면 Redis에 부담이 커질 수 있습니다.</p>
<p>Redisson은 pub/sub 방식으로 락 해제 이벤트를 기다릴 수 있어 불필요한 반복 요청을 줄일 수 있습니다.</p>
<p>따라서 트래픽이 많고 락 경합이 자주 발생하는 상황에서는 Redisson이 더 적합할 수 있습니다.</p>
<h3 id="꼬리질문-2-분산-락을-aop로-분리한다면-어떤-장점이-있고-주의해야-할-점은-무엇인가요">꼬리질문 2. 분산 락을 AOP로 분리한다면 어떤 장점이 있고, 주의해야 할 점은 무엇인가요?</h3>
<p>분산 락을 AOP로 분리하면 비즈니스 로직에서 락 획득과 해제 코드를 제거할 수 있습니다.</p>
<p>장점은 다음과 같습니다.</p>
<ul>
<li>중복 코드 감소</li>
<li>락 처리 로직 일관성 유지</li>
<li>비즈니스 로직 가독성 향상</li>
<li>여러 메서드에 공통 적용 가능</li>
</ul>
<p>주의할 점은 다음과 같습니다.</p>
<ul>
<li>락 획득 후 트랜잭션이 시작되는 순서를 고려해야 함</li>
<li>락 해제가 반드시 <code>finally</code>에서 수행되어야 함</li>
<li>락 key를 잘못 설계하면 불필요하게 넓은 범위가 잠김</li>
<li>Self-invocation으로 AOP가 적용되지 않을 수 있음</li>
</ul>
<hr>
<h1 id="aop--테스트-영역">AOP / 테스트 영역</h1>
<h2 id="q9-aop를-통해-횡단-관심사를-분리했을-때-transactional과-커스텀-aop-어드바이스가-같은-메서드에-적용되면-실행-순서가-어떻게-되나요">Q9. AOP를 통해 횡단 관심사를 분리했을 때, @Transactional과 커스텀 AOP 어드바이스가 같은 메서드에 적용되면 실행 순서가 어떻게 되나요?</h2>
<p><strong>A.</strong></p>
<p><code>@Transactional</code>도 Spring AOP 기반으로 동작하고, 커스텀 AOP도 프록시 기반으로 동작합니다.</p>
<p>둘 다 같은 메서드에 적용되면 <code>@Order</code> 설정이나 우선순위에 따라 실행 순서가 달라질 수 있습니다.</p>
<p>예를 들어 분산 락 AOP와 트랜잭션이 함께 있다면 보통 다음 순서를 의도합니다.</p>
<pre><code class="language-text">락 획득
→ 트랜잭션 시작
→ 비즈니스 로직 실행
→ 트랜잭션 커밋
→ 락 해제</code></pre>
<p>이렇게 해야 트랜잭션이 끝나기 전에 락이 먼저 풀려서 다른 요청이 들어오는 문제를 줄일 수 있습니다.</p>
<h3 id="꼬리질문-1-self-invocation-문제가-발생하는-원인이-무엇인가요-transactional과-cacheable에서도-같은-문제가-생기나요">꼬리질문 1. Self-invocation 문제가 발생하는 원인이 무엇인가요? @Transactional과 @Cacheable에서도 같은 문제가 생기나요?</h3>
<p>Self-invocation은 같은 클래스 내부에서 자기 자신의 메서드를 직접 호출할 때 발생합니다.</p>
<p>Spring AOP는 프록시 객체를 거쳐야 동작하는데, 내부 호출은 프록시를 거치지 않고 <code>this.method()</code>로 호출됩니다.</p>
<p>그래서 <code>@Transactional</code>, <code>@Cacheable</code>, 커스텀 AOP 모두 같은 문제가 발생할 수 있습니다.</p>
<p>해결 방법은 다음과 같습니다.</p>
<ul>
<li>메서드를 다른 Bean으로 분리</li>
<li>외부에서 프록시를 통해 호출되도록 구조 변경</li>
<li>필요 시 AspectJ 방식 고려</li>
</ul>
<h3 id="꼬리질문-2-aop-어드바이스-내부에서-예외가-발생했을-때-트랜잭션-롤백은-어떻게-되나요">꼬리질문 2. AOP 어드바이스 내부에서 예외가 발생했을 때 트랜잭션 롤백은 어떻게 되나요?</h3>
<p>트랜잭션 안에서 예외가 발생하고 해당 예외가 밖으로 전파되면 롤백됩니다.</p>
<p>기본적으로 Spring은 RuntimeException과 Error에 대해 롤백합니다.</p>
<p>다만 AOP 어드바이스에서 예외를 잡고 삼켜버리면 트랜잭션은 정상 처리로 판단할 수 있습니다.</p>
<p>그래서 롤백이 필요한 예외는 반드시 다시 던지거나, <code>rollbackFor</code> 옵션을 명확히 설정해야 합니다.</p>
<hr>
<h2 id="q10-멀티-스레드-환경에서-동시성-이슈를-재현하는-테스트를-어떻게-작성했나요">Q10. 멀티 스레드 환경에서 동시성 이슈를 재현하는 테스트를 어떻게 작성했나요?</h2>
<p><strong>A.</strong></p>
<p>동시성 테스트는 <code>ExecutorService</code>와 <code>CountDownLatch</code>를 사용해 여러 스레드가 동시에 같은 로직을 실행하도록 작성할 수 있습니다.</p>
<p>흐름은 다음과 같습니다.</p>
<ol>
<li>테스트용 상품과 재고를 생성합니다.</li>
<li>여러 개의 스레드를 준비합니다.</li>
<li><code>CountDownLatch</code>로 동시에 시작하도록 맞춥니다.</li>
<li>각 스레드가 재고 차감 요청을 보냅니다.</li>
<li>모든 스레드가 끝날 때까지 기다립니다.</li>
<li>최종 재고와 성공한 요청 수를 검증합니다.</li>
</ol>
<pre><code class="language-java">ExecutorService executorService = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(10);

for (int i = 0; i &lt; 10; i++) {
    executorService.submit(() -&gt; {
        try {
            stockService.decreaseStock(productId);
        } finally {
            latch.countDown();
        }
    });
}

latch.await();</code></pre>
<p>이 테스트를 통해 동시에 요청이 들어와도 재고가 음수가 되지 않는지 확인할 수 있습니다.</p>
<h3 id="꼬리질문-1-countdownlatch--executorservice로-동시-요청을-시뮬레이션하는-방식의-한계가-무엇인가요">꼬리질문 1. CountDownLatch + ExecutorService로 동시 요청을 시뮬레이션하는 방식의 한계가 무엇인가요?</h3>
<p>이 방식은 동시성 문제를 재현하는 데 도움이 되지만 실제 운영 환경을 완전히 대체할 수는 없습니다.</p>
<p>한계는 다음과 같습니다.</p>
<ul>
<li>실제 네트워크 지연이 반영되지 않음</li>
<li>여러 서버 인스턴스 환경을 완전히 재현하기 어려움</li>
<li>DB 커넥션 풀, Redis, 외부 API 병목을 모두 반영하기 어려움</li>
<li>스레드 스케줄링에 따라 테스트 결과가 달라질 수 있음</li>
<li>테스트에서 통과해도 운영 트래픽에서는 문제가 생길 수 있음</li>
</ul>
<p>그래서 단위 테스트뿐만 아니라 통합 테스트, 부하 테스트도 함께 고려해야 합니다.</p>
<h3 id="꼬리질문-2-동시성-테스트가-로컬에서는-통과했는데-운영-환경에서-문제가-생기는-경우가-있다면-어떤-요인-때문일까요">꼬리질문 2. 동시성 테스트가 로컬에서는 통과했는데 운영 환경에서 문제가 생기는 경우가 있다면 어떤 요인 때문일까요?</h3>
<p>운영 환경은 로컬보다 훨씬 많은 변수가 있습니다.</p>
<p>대표적인 원인은 다음과 같습니다.</p>
<ul>
<li>서버 인스턴스가 여러 대로 늘어난 경우</li>
<li>DB 커넥션 풀이 부족한 경우</li>
<li>트랜잭션 시간이 길어진 경우</li>
<li>Redis Lock TTL이 너무 짧거나 긴 경우</li>
<li>네트워크 지연이 발생한 경우</li>
<li>실제 요청량이 테스트보다 많은 경우</li>
<li>락 획득 순서가 일정하지 않아 데드락이 발생한 경우</li>
<li>외부 API 응답 지연으로 커넥션이 오래 점유된 경우</li>
</ul>
<p>따라서 운영 환경을 고려한 동시성 제어는 단순히 로컬 테스트 통과만으로 끝내면 안 되고, 락 범위, 트랜잭션 범위, 커넥션 풀, 장애 복구까지 함께 검토해야 합니다.</p>
<hr>
<h1 id="마무리-정리">마무리 정리</h1>
<p>CH5 플러스 스프링 면접에서는 단순 CRUD보다 <strong>성능 최적화와 동시성 제어</strong>를 얼마나 이해하고 있는지 확인할 가능성이 높습니다.</p>
<p>특히 다음 키워드는 꼭 연결해서 설명할 수 있어야 합니다.</p>
<ul>
<li>QueryDSL 동적 쿼리</li>
<li>BooleanExpression과 null-safe 조건 처리</li>
<li>페이징 count 쿼리 최적화</li>
<li>Offset / No-Offset 페이징 차이</li>
<li>Redis Cache와 TTL</li>
<li>Cache Stampede</li>
<li>복합 인덱스 컬럼 순서</li>
<li>EXPLAIN 실행 계획 분석</li>
<li>낙관적 락과 비관적 락</li>
<li>Redis 분산 락</li>
<li>Lettuce와 Redisson 차이</li>
<li>AOP 실행 순서</li>
<li>Self-invocation 문제</li>
<li>동시성 테스트 한계</li>
</ul>
<p>면접에서는 “기능을 구현했다”보다
<strong>왜 이 방식을 선택했는지</strong>, <strong>어떤 문제가 생길 수 있는지</strong>, <strong>그 문제를 어떻게 완화했는지</strong>까지 함께 말하는 것이 중요합니다.</p>
<p>출처: 내일배움 스파르타 클럽<img src="https://velog.velcdn.com/images/loaded_diaper/post/d9a254e9-26fe-4425-b52b-efb75198128f/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CH4 클라우드·결제시스템 모의면접 예상질문 리스트]]></title>
            <link>https://velog.io/@loaded_diaper/CH4-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C%EA%B2%B0%EC%A0%9C%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%AA%A8%EC%9D%98%EB%A9%B4%EC%A0%91-%EC%98%88%EC%83%81%EC%A7%88%EB%AC%B8-%EB%A6%AC%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@loaded_diaper/CH4-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C%EA%B2%B0%EC%A0%9C%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%AA%A8%EC%9D%98%EB%A9%B4%EC%A0%91-%EC%98%88%EC%83%81%EC%A7%88%EB%AC%B8-%EB%A6%AC%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Mon, 29 Jun 2026 03:11:31 GMT</pubDate>
            <description><![CDATA[<h1 id="ch4-클라우드·결제-시스템-모의면접-예상질문-정리">CH4 클라우드·결제 시스템 모의면접 예상질문 정리</h1>
<blockquote>
<p>AWS 배포, Docker, HTTPS, PortOne V2 결제, Webhook, 포인트 결제, 환불, 재고 정합성, 정기 구독 결제까지 CH4 프로젝트에서 나올 수 있는 면접 질문과 핵심 답변을 정리했습니다.</p>
</blockquote>
<hr>
<h1 id="aws-·-배포-영역">AWS · 배포 영역</h1>
<h2 id="q1-ec2-rds-s3를-함께-사용하는-이유가-뭔가요-각각-어떤-역할을-담당했나요">Q1. EC2, RDS, S3를 함께 사용하는 이유가 뭔가요? 각각 어떤 역할을 담당했나요?</h2>
<p><strong>A.</strong></p>
<p>EC2, RDS, S3는 각각 역할이 다르기 때문에 함께 사용했습니다.</p>
<ul>
<li><strong>EC2</strong>: Spring Boot 애플리케이션을 실행하는 서버</li>
<li><strong>RDS</strong>: MySQL 데이터베이스를 관리하는 서비스</li>
<li><strong>S3</strong>: 이미지, 파일 같은 정적 리소스를 저장하는 스토리지</li>
</ul>
<p>EC2에 애플리케이션을 배포하고, 데이터는 RDS에 저장하며, 파일은 S3에 저장해서 서버와 저장소의 책임을 분리했습니다.</p>
<h3 id="꼬리질문-1-ec2와-rds를-같은-vpc-안에-두면서-보안-그룹은-어떻게-설정했나요">꼬리질문 1. EC2와 RDS를 같은 VPC 안에 두면서 보안 그룹은 어떻게 설정했나요?</h3>
<p>EC2와 RDS는 같은 VPC 안에 두고, RDS는 외부에서 직접 접근하지 못하도록 설정했습니다.</p>
<p>보안 그룹은 보통 다음과 같이 구성합니다.</p>
<ul>
<li>EC2: 80, 443, 필요 시 22번 포트 허용</li>
<li>RDS: 3306 포트를 EC2 보안 그룹에서 오는 요청만 허용</li>
</ul>
<p>즉, 사용자는 EC2에만 접근하고, DB는 EC2를 통해서만 접근할 수 있게 구성합니다.</p>
<h3 id="꼬리질문-2-rds-대신-ec2에-직접-mysql을-설치하는-방식과-비교했을-때-단점은-없나요">꼬리질문 2. RDS 대신 EC2에 직접 MySQL을 설치하는 방식과 비교했을 때 단점은 없나요?</h3>
<p>RDS는 관리형 서비스라 편리하지만 비용이 더 들 수 있고, 세부 설정을 완전히 직접 제어하기는 어렵습니다.</p>
<p>반대로 EC2에 직접 MySQL을 설치하면 비용이나 설정 자유도는 장점이 될 수 있지만, 백업, 장애 복구, 보안 패치, 모니터링을 직접 관리해야 해서 운영 부담이 커집니다.</p>
<hr>
<h2 id="q2-docker로-애플리케이션을-컨테이너화한-이유가-뭔가요">Q2. Docker로 애플리케이션을 컨테이너화한 이유가 뭔가요?</h2>
<p><strong>A.</strong></p>
<p>Docker를 사용하면 애플리케이션 실행 환경을 이미지로 고정할 수 있습니다.</p>
<p>개발 환경과 배포 환경의 Java 버전, 라이브러리, 실행 방식 차이를 줄일 수 있고, 어디서 실행하더라도 동일한 방식으로 애플리케이션을 띄울 수 있습니다.</p>
<p>즉, 배포 환경의 차이로 발생하는 문제를 줄이기 위해 사용했습니다.</p>
<h3 id="꼬리질문-1-dockerfile-작성-시-이미지-크기를-줄이기-위해-고려한-것이-있나요">꼬리질문 1. Dockerfile 작성 시 이미지 크기를 줄이기 위해 고려한 것이 있나요?</h3>
<p>이미지 크기를 줄이기 위해 다음을 고려할 수 있습니다.</p>
<ul>
<li>빌드 단계와 실행 단계를 분리하는 Multi-stage build 사용</li>
<li>실행에 필요 없는 Gradle 캐시, 테스트 파일, 소스 파일 제외</li>
<li>JDK 대신 실행만 가능한 JRE 기반 이미지 사용</li>
<li>불필요한 패키지 설치 최소화</li>
</ul>
<p>이렇게 하면 최종 이미지에는 애플리케이션 실행에 필요한 파일만 포함할 수 있습니다.</p>
<h3 id="꼬리질문-2-cicd-파이프라인에서-테스트-실패-시-배포가-어떻게-차단되나요">꼬리질문 2. CI/CD 파이프라인에서 테스트 실패 시 배포가 어떻게 차단되나요?</h3>
<p>CI/CD 파이프라인에서 보통 <code>test</code> 단계와 <code>deploy</code> 단계를 분리합니다.</p>
<p><code>./gradlew test</code>가 실패하면 해당 job이 실패 상태가 되고, 그 다음 배포 job이 실행되지 않도록 설정합니다.</p>
<p>즉, 테스트를 통과한 코드만 Docker 이미지 빌드와 배포 단계로 넘어가게 해서 안정성을 높입니다.</p>
<hr>
<h2 id="q3-https를-적용한-이유와-적용-방식을-설명해주세요">Q3. HTTPS를 적용한 이유와 적용 방식을 설명해주세요.</h2>
<p><strong>A.</strong></p>
<p>HTTPS는 클라이언트와 서버 사이의 통신을 암호화하기 위해 적용합니다.</p>
<p>로그인 정보, 결제 정보, 개인정보가 네트워크에서 평문으로 노출되는 것을 막을 수 있습니다.</p>
<p>적용 방식은 보통 다음과 같습니다.</p>
<ul>
<li>도메인 연결</li>
<li>SSL/TLS 인증서 발급</li>
<li>Nginx 또는 ALB에 인증서 적용</li>
<li>HTTP 요청을 HTTPS로 리다이렉트</li>
</ul>
<h3 id="꼬리질문-1-lets-encrypt를-쓰는-방식과-acm을-쓰는-방식의-차이가-뭔가요">꼬리질문 1. Let&#39;s Encrypt를 쓰는 방식과 ACM을 쓰는 방식의 차이가 뭔가요?</h3>
<p><strong>Let&#39;s Encrypt</strong>는 무료 인증서를 발급받아 EC2나 Nginx에 직접 적용하는 방식입니다.</p>
<p><strong>ACM</strong>은 AWS Certificate Manager에서 인증서를 발급받고, ALB나 CloudFront 같은 AWS 서비스에 연결하는 방식입니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>Let&#39;s Encrypt</th>
<th>ACM</th>
</tr>
</thead>
<tbody><tr>
<td>적용 위치</td>
<td>EC2, Nginx 등</td>
<td>ALB, CloudFront 등</td>
</tr>
<tr>
<td>관리 방식</td>
<td>직접 갱신 설정 필요</td>
<td>AWS 서비스와 연동 시 관리 편리</td>
</tr>
<tr>
<td>사용 상황</td>
<td>EC2 단독 구성</td>
<td>ALB, CloudFront 사용 구성</td>
</tr>
</tbody></table>
<h3 id="꼬리질문-2-alb-없이-ec2-한-대에-https를-적용하려면-어떻게-해야-하나요">꼬리질문 2. ALB 없이 EC2 한 대에 HTTPS를 적용하려면 어떻게 해야 하나요?</h3>
<p>EC2에 Nginx를 설치하고, Certbot으로 Let&#39;s Encrypt 인증서를 발급받아 Nginx에 적용할 수 있습니다.</p>
<p>흐름은 다음과 같습니다.</p>
<ol>
<li>도메인을 EC2 Public IP에 연결</li>
<li>EC2 보안 그룹에서 80, 443 포트 허용</li>
<li>Nginx 설치</li>
<li>Certbot으로 SSL 인증서 발급</li>
<li>Nginx에서 443 포트 SSL 설정</li>
<li>80번 HTTP 요청을 443번 HTTPS로 리다이렉트</li>
</ol>
<hr>
<h1 id="결제-시스템-영역">결제 시스템 영역</h1>
<h2 id="q4-portone-v2-기반-결제-흐름-client-confirm-방식을-처음부터-끝까지-설명해주세요">Q4. PortOne V2 기반 결제 흐름, Client Confirm 방식을 처음부터 끝까지 설명해주세요.</h2>
<p><strong>A.</strong></p>
<p>Client Confirm 방식은 클라이언트에서 결제창을 띄우고, 서버에서 최종 검증과 확정 처리를 하는 방식입니다.</p>
<p>전체 흐름은 다음과 같습니다.</p>
<ol>
<li>사용자가 주문을 요청합니다.</li>
<li>서버가 주문 금액, 할인 금액, 포인트 사용 금액을 계산합니다.</li>
<li>서버가 결제 대기 상태의 주문과 결제 정보를 생성합니다.</li>
<li>클라이언트가 PortOne SDK로 결제창을 호출합니다.</li>
<li>결제 성공 후 클라이언트가 <code>paymentId</code> 등을 서버로 전달합니다.</li>
<li>서버가 PortOne 결제 정보를 조회하거나 confirm API를 호출합니다.</li>
<li>서버가 주문 금액과 실제 결제 금액을 검증합니다.</li>
<li>검증 성공 시 결제 상태를 <code>PAID</code>로 변경합니다.</li>
<li>주문 상태, 포인트 내역, 재고 상태를 함께 확정합니다.</li>
</ol>
<p>핵심은 클라이언트 응답만 믿지 않고, 서버가 최종적으로 금액과 상태를 검증한 뒤 결제를 확정하는 것입니다.</p>
<h3 id="꼬리질문-1-사전-금액-검증을-서버에서-하는-이유가-뭔가요-클라이언트에서-처리하면-안-되나요">꼬리질문 1. 사전 금액 검증을 서버에서 하는 이유가 뭔가요? 클라이언트에서 처리하면 안 되나요?</h3>
<p>클라이언트 값은 사용자가 조작할 수 있기 때문입니다.</p>
<p>상품 금액, 할인 금액, 포인트 사용 금액을 클라이언트에서만 계산하면 결제 금액을 임의로 낮춰 보낼 위험이 있습니다.</p>
<p>따라서 최종 결제 금액은 반드시 서버가 DB 기준으로 다시 계산하고 검증해야 합니다.</p>
<h3 id="꼬리질문-2-결제-검증-후-확정-처리-중-서버가-다운되면-어떻게-되나요">꼬리질문 2. 결제 검증 후 확정 처리 중 서버가 다운되면 어떻게 되나요?</h3>
<p>결제는 PG에서는 성공했지만, 내부 DB에는 반영되지 않은 상태가 될 수 있습니다.</p>
<p>이를 대비해 결제 확정 로직은 멱등하게 설계해야 합니다.</p>
<p>예를 들어 같은 <code>paymentId</code>로 다시 요청이 들어와도 이미 처리된 결제라면 중복 처리하지 않고 기존 결과를 반환합니다.</p>
<p>또한 Webhook이나 결제 상태 재조회 배치를 통해 PG 결제 상태와 내부 결제 상태를 다시 맞출 수 있습니다.</p>
<hr>
<h2 id="q5-webhook-멱등성-처리를-어떻게-구현했나요">Q5. Webhook 멱등성 처리를 어떻게 구현했나요?</h2>
<p><strong>A.</strong></p>
<p>Webhook은 같은 이벤트가 여러 번 올 수 있다고 가정하고 멱등하게 처리해야 합니다.</p>
<p>보통 다음 기준으로 중복 처리를 막습니다.</p>
<ul>
<li><code>webhookEventId</code></li>
<li><code>paymentId</code></li>
<li><code>orderId</code></li>
<li>결제 상태값</li>
</ul>
<p>처리 흐름은 다음과 같습니다.</p>
<ol>
<li>Webhook 요청을 받습니다.</li>
<li>HMAC signature를 검증합니다.</li>
<li>이벤트 ID 또는 paymentId로 이미 처리된 이벤트인지 확인합니다.</li>
<li>이미 처리된 경우 바로 성공 응답을 반환합니다.</li>
<li>아직 처리되지 않았다면 결제 상태를 조회하고 내부 상태를 변경합니다.</li>
<li>처리 완료 이벤트를 저장합니다.</li>
</ol>
<p>핵심은 같은 Webhook이 여러 번 와도 결제, 포인트, 재고가 중복 변경되지 않게 하는 것입니다.</p>
<h3 id="꼬리질문-1-hmac-서명-검증은-어느-시점에-처리했나요">꼬리질문 1. HMAC 서명 검증은 어느 시점에 처리했나요?</h3>
<p>Webhook 요청을 받은 직후, 비즈니스 로직을 실행하기 전에 처리합니다.</p>
<p>서명 검증에 실패한 요청은 신뢰할 수 없는 요청이므로 결제 상태 변경, 포인트 환불, 재고 복구 같은 내부 로직을 실행하면 안 됩니다.</p>
<p>즉, HMAC 검증은 Webhook 처리의 가장 앞단에서 수행해야 합니다.</p>
<h3 id="꼬리질문-2-webhook-수신에-실패했을-때-portone이-재전송을-보장하나요">꼬리질문 2. Webhook 수신에 실패했을 때 PortOne이 재전송을 보장하나요?</h3>
<p>외부 결제사의 Webhook 재전송 정책에만 의존하면 위험합니다.</p>
<p>서버 장애, 네트워크 장애, 응답 지연 등으로 Webhook이 누락되거나 늦게 도착할 수 있기 때문입니다.</p>
<p>따라서 Webhook은 보조 수단으로 보고, 서버에서는 결제 단건 조회 API나 상태 동기화 배치를 통해 결제 상태를 다시 확인할 수 있게 설계하는 것이 안전합니다.</p>
<hr>
<h2 id="q6-포인트--pg-복합-결제에서-pg-실결제-금액을-어떻게-산정했나요">Q6. 포인트 + PG 복합 결제에서 PG 실결제 금액을 어떻게 산정했나요?</h2>
<p><strong>A.</strong></p>
<p>PG 실결제 금액은 최종 주문 금액에서 사용 포인트를 차감해서 계산했습니다.</p>
<pre><code class="language-text">PG 실결제 금액 = 최종 주문 금액 - 사용 포인트</code></pre>
<p>예를 들어 주문 금액이 50,000원이고 포인트를 10,000원 사용하면 PG 결제 금액은 40,000원입니다.</p>
<p>다만 포인트 사용 가능 여부는 서버에서 검증해야 합니다.</p>
<ul>
<li>사용자의 포인트 잔액이 충분한지</li>
<li>주문 금액보다 많은 포인트를 사용하지 않았는지</li>
<li>최소 결제 금액 정책을 만족하는지</li>
<li>이미 사용 처리된 포인트가 아닌지</li>
</ul>
<h3 id="꼬리질문-1-결제-취소-시-포인트-환불은-어떻게-처리했나요">꼬리질문 1. 결제 취소 시 포인트 환불은 어떻게 처리했나요?</h3>
<p>결제 취소나 환불이 성공하면 사용했던 포인트를 다시 복구합니다.</p>
<p>이때 단순히 잔액만 증가시키는 것이 아니라, 포인트 사용 취소 내역을 포인트 거래 원장에 남기는 방식이 좋습니다.</p>
<p>예를 들어 다음과 같이 기록합니다.</p>
<ul>
<li>결제 시: <code>USE</code></li>
<li>환불 시: <code>REFUND</code></li>
</ul>
<p>이렇게 하면 포인트가 왜 증가하거나 감소했는지 추적할 수 있습니다.</p>
<h3 id="꼬리질문-2-포인트-잔액보다-많은-포인트를-사용하려는-요청은-어떻게-처리했나요">꼬리질문 2. 포인트 잔액보다 많은 포인트를 사용하려는 요청은 어떻게 처리했나요?</h3>
<p>서버에서 사용자의 현재 포인트 잔액을 조회하고, 요청 포인트가 잔액보다 크면 예외를 발생시킵니다.</p>
<p>이 경우 결제창을 띄우기 전에 요청을 차단해야 합니다.</p>
<p>응답은 보통 <code>400 Bad Request</code>로 처리하고, 메시지는 “보유 포인트보다 많은 포인트를 사용할 수 없습니다.”처럼 반환할 수 있습니다.</p>
<hr>
<h2 id="q7-부분-환불-시-결제수단별-비율-분리-산정과-반올림-오차-보정을-어떻게-구현했나요">Q7. 부분 환불 시 결제수단별 비율 분리 산정과 반올림 오차 보정을 어떻게 구현했나요?</h2>
<p><strong>A.</strong></p>
<p>포인트와 PG가 함께 사용된 결제에서는 환불 금액을 결제수단별로 나눠야 합니다.</p>
<p>예를 들어 총 결제 금액이 100,000원이고, 포인트 20,000원, PG 80,000원으로 결제했다면 비율은 다음과 같습니다.</p>
<ul>
<li>포인트: 20%</li>
<li>PG: 80%</li>
</ul>
<p>부분 환불 금액이 30,000원이라면 비율대로 계산합니다.</p>
<ul>
<li>포인트 환불: 6,000원</li>
<li>PG 환불: 24,000원</li>
</ul>
<p>다만 나눗셈 과정에서 소수점이 생길 수 있기 때문에 반올림 오차를 보정해야 합니다.</p>
<pre><code class="language-text">PG 환불 금액 = 전체 환불 금액 - 포인트 환불 금액</code></pre>
<p>이렇게 계산하면 포인트와 PG 환불 금액의 합이 항상 전체 환불 금액과 일치합니다.</p>
<h3 id="꼬리질문-1-반올림-오차를-pg에-몰아주는-이유가-있나요">꼬리질문 1. 반올림 오차를 PG에 몰아주는 이유가 있나요?</h3>
<p>포인트는 내부 시스템에서 직접 관리할 수 있지만, PG 환불 금액은 외부 결제사에 정확한 금액으로 요청해야 합니다.</p>
<p>따라서 전체 환불 금액과 결제수단별 환불 합계가 어긋나면 안 됩니다.</p>
<p>한쪽을 기준으로 계산하고 나머지를 차액으로 보정하면 총액 불일치 문제를 줄일 수 있습니다.</p>
<p>실무에서는 회사 정책에 따라 오차를 포인트에 둘 수도 있고 PG에 둘 수도 있지만, 중요한 것은 합계가 정확히 맞도록 일관된 기준을 두는 것입니다.</p>
<h3 id="꼬리질문-2-여러-상품-중-일부만-환불하는-부분-환불과-전체-금액의-일부만-환불하는-경우를-구분해서-처리했나요">꼬리질문 2. 여러 상품 중 일부만 환불하는 부분 환불과, 전체 금액의 일부만 환불하는 경우를 구분해서 처리했나요?</h3>
<p>네, 구분해서 처리하는 것이 좋습니다.</p>
<ul>
<li><strong>상품 단위 부분 환불</strong>: 특정 주문 상품의 수량과 금액을 기준으로 환불</li>
<li><strong>금액 단위 부분 환불</strong>: 전체 결제 금액 중 일부 금액만 환불</li>
</ul>
<p>상품 단위 환불은 재고 복구, 주문 상품 상태 변경, 환불 가능 수량 검증이 함께 필요합니다.</p>
<p>반면 금액 단위 환불은 주로 결제 금액 정산과 환불 한도 검증이 중요합니다.</p>
<hr>
<h2 id="q8-재고-선차감-정책과-단일-트랜잭션-기반-정합성-보장을-어떻게-설계했나요">Q8. 재고 선차감 정책과 단일 트랜잭션 기반 정합성 보장을 어떻게 설계했나요?</h2>
<p><strong>A.</strong></p>
<p>재고 선차감 정책은 주문 생성 시점에 재고를 먼저 차감하는 방식입니다.</p>
<p>결제까지 기다렸다가 재고를 차감하면 동시에 많은 사용자가 결제할 때 초과 판매가 발생할 수 있습니다.</p>
<p>그래서 주문 생성 시점에 재고를 차감하고, 결제 실패나 주문 취소가 발생하면 재고를 복구하는 방식으로 설계합니다.</p>
<p>흐름은 다음과 같습니다.</p>
<ol>
<li>주문 요청을 받습니다.</li>
<li>상품 재고를 조회합니다.</li>
<li>재고가 충분한지 검증합니다.</li>
<li>재고를 차감합니다.</li>
<li>주문을 생성합니다.</li>
<li>결제 대기 상태로 저장합니다.</li>
<li>결제 실패 또는 취소 시 재고를 복구합니다.</li>
</ol>
<p>이 과정은 하나의 트랜잭션 안에서 처리해서 주문 생성과 재고 차감이 함께 성공하거나 함께 실패하도록 해야 합니다.</p>
<h3 id="꼬리질문-1-비관적-락-대신-낙관적-락을-사용하지-않은-이유가-있나요">꼬리질문 1. 비관적 락 대신 낙관적 락을 사용하지 않은 이유가 있나요?</h3>
<p>재고처럼 동시에 많은 요청이 몰리고, 초과 판매가 절대 발생하면 안 되는 경우에는 비관적 락이 더 단순하고 안전할 수 있습니다.</p>
<p>낙관적 락은 충돌이 적을 때 유리하지만, 충돌이 많으면 재시도가 많이 발생할 수 있습니다.</p>
<p>반면 비관적 락은 먼저 데이터에 락을 걸고 처리하므로 같은 재고를 동시에 수정하는 상황을 강하게 제어할 수 있습니다.</p>
<h3 id="꼬리질문-2-인스턴스가-n대로-늘어나도-지금의-재고-동시성-제어가-그대로-유효할까요">꼬리질문 2. 인스턴스가 N대로 늘어나도 지금의 재고 동시성 제어가 그대로 유효할까요?</h3>
<p>DB의 비관적 락을 사용한다면 애플리케이션 인스턴스가 여러 대로 늘어나도 같은 DB row에 대한 락이 걸리기 때문에 기본적인 정합성은 유지됩니다.</p>
<p>하지만 애플리케이션 내부의 <code>synchronized</code> 같은 JVM 락만 사용했다면 인스턴스가 여러 대일 때는 유효하지 않습니다.</p>
<p>인스턴스가 N대로 늘어나는 환경에서는 DB Lock, Redis Lock 같은 외부 공유 자원을 기준으로 동시성을 제어해야 합니다.</p>
<h3 id="꼬리질문-3-커넥션-점유가-늘어나면-어떤-일이-발생할까요">꼬리질문 3. 커넥션 점유가 늘어나면 어떤 일이 발생할까요?</h3>
<p>트랜잭션이 오래 걸리거나 락 대기가 길어지면 DB 커넥션이 오래 점유됩니다.</p>
<p>그 결과 커넥션 풀이 고갈되고, 다른 요청들이 DB 커넥션을 얻지 못해 지연되거나 실패할 수 있습니다.</p>
<p>그래서 트랜잭션 범위를 짧게 유지하고, 외부 API 호출을 트랜잭션 안에서 오래 수행하지 않도록 주의해야 합니다.</p>
<hr>
<h2 id="q9-빌링키-기반-정기-구독-결제와-스케줄러-멱등-처리를-어떻게-구현했나요">Q9. 빌링키 기반 정기 구독 결제와 스케줄러 멱등 처리를 어떻게 구현했나요?</h2>
<p><strong>A.</strong></p>
<p>빌링키 기반 정기 결제는 사용자가 최초 1회 결제수단을 등록하면, 이후에는 서버가 빌링키를 이용해 정해진 주기마다 자동 결제를 요청하는 방식입니다.</p>
<p>구현 흐름은 다음과 같습니다.</p>
<ol>
<li>사용자가 결제수단을 등록합니다.</li>
<li>PortOne을 통해 빌링키를 발급받습니다.</li>
<li>서버에 빌링키와 구독 정보를 저장합니다.</li>
<li>스케줄러가 결제 예정 구독을 조회합니다.</li>
<li>결제 요청을 보냅니다.</li>
<li>결제 성공 시 다음 결제일을 갱신합니다.</li>
<li>결제 실패 시 실패 상태와 재시도 정보를 저장합니다.</li>
</ol>
<p>스케줄러는 중복 실행될 수 있으므로 멱등하게 처리해야 합니다.</p>
<p>예를 들어 <code>subscriptionId + billingDate</code> 기준으로 결제 시도 이력을 저장하고, 이미 처리된 결제일이면 다시 결제하지 않도록 막습니다.</p>
<h3 id="꼬리질문-1-정기-결제-실패-시-재시도-정책은-어떻게-설정했나요">꼬리질문 1. 정기 결제 실패 시 재시도 정책은 어떻게 설정했나요?</h3>
<p>정기 결제 실패 시 바로 구독을 종료하기보다는 재시도 정책을 둘 수 있습니다.</p>
<p>예를 들어 다음과 같이 처리할 수 있습니다.</p>
<ul>
<li>1차 실패: 실패 이력 저장 후 다음 날 재시도</li>
<li>2차 실패: 사용자에게 결제 실패 알림</li>
<li>3차 실패: 구독 일시 중지 또는 만료 처리</li>
</ul>
<p>재시도 횟수와 주기는 서비스 정책에 따라 정하고, 중복 결제가 발생하지 않도록 결제 시도 이력을 반드시 저장해야 합니다.</p>
<h3 id="꼬리질문-2-등급별-적립률-차등-적용은-어떻게-관리했나요">꼬리질문 2. 등급별 적립률 차등 적용은 어떻게 관리했나요?</h3>
<p>회원 등급별 적립률은 코드에 하드코딩하기보다 정책 테이블이나 Enum으로 관리할 수 있습니다.</p>
<p>예를 들어 <code>BASIC</code>, <code>VIP</code>, <code>VVIP</code> 등급에 따라 적립률을 다르게 두고, 결제 완료 시점에 회원 등급을 조회해서 적립 포인트를 계산합니다.</p>
<p>중요한 점은 결제 당시 적용된 적립률을 이력으로 남기는 것입니다.</p>
<p>나중에 등급 정책이 바뀌더라도 과거 결제의 적립 내역은 그대로 추적할 수 있어야 합니다.</p>
<hr>
<h2 id="q10-결제-성공-응답과-webhook-이벤트가-동시에-들어오거나-서로-다른-시점에-지연-도착하는-상황을-고려해-결제-확정-로직을-어떻게-설계했나요">Q10. 결제 성공 응답과 Webhook 이벤트가 동시에 들어오거나, 서로 다른 시점에 지연 도착하는 상황을 고려해 결제 확정 로직을 어떻게 설계했나요?</h2>
<p><strong>A.</strong></p>
<p>결제 성공 응답과 Webhook은 도착 순서가 보장되지 않습니다.</p>
<p>클라이언트 성공 응답이 먼저 올 수도 있고, Webhook이 먼저 올 수도 있으며, 둘 다 동시에 들어올 수도 있습니다.</p>
<p>그래서 결제 확정 로직은 하나의 공통 메서드로 모으고, 멱등하게 처리해야 합니다.</p>
<p>예를 들어 다음과 같이 설계할 수 있습니다.</p>
<ol>
<li>클라이언트 성공 응답 또는 Webhook 수신</li>
<li><code>paymentId</code> 기준으로 결제 정보 조회</li>
<li>이미 <code>PAID</code> 상태라면 추가 처리 없이 종료</li>
<li>아직 대기 상태라면 PortOne 결제 상태 조회</li>
<li>금액, 주문번호, 상태 검증</li>
<li>검증 성공 시 내부 결제 상태를 <code>PAID</code>로 변경</li>
<li>주문, 포인트, 재고 상태를 확정</li>
<li>처리 이력 저장</li>
</ol>
<p>핵심은 어느 경로로 들어오든 같은 확정 로직을 타게 만드는 것입니다.</p>
<p>그래야 중복 요청, 지연 요청, 동시 요청 상황에서도 결제가 한 번만 확정됩니다.</p>
<hr>
<h2 id="q11-결제-실패가-발생했을-때-원인을-어떻게-추적할-수-있나요">Q11. 결제 실패가 발생했을 때 원인을 어떻게 추적할 수 있나요?</h2>
<p><strong>A.</strong></p>
<p>결제 실패 원인을 추적하려면 외부 PG 응답과 내부 서버 로그를 연결할 수 있어야 합니다.</p>
<p>이를 위해 다음 값을 로그와 DB에 남기는 것이 좋습니다.</p>
<ul>
<li><code>orderId</code></li>
<li><code>paymentId</code></li>
<li>PortOne transaction id</li>
<li>요청 금액</li>
<li>실제 결제 금액</li>
<li>결제 상태</li>
<li>실패 코드</li>
<li>실패 메시지</li>
<li>Webhook event id</li>
<li>요청 시각과 응답 시각</li>
</ul>
<p>이렇게 남겨두면 결제 실패가 외부 PG 문제인지, 서버 검증 실패인지, 내부 트랜잭션 문제인지 추적할 수 있습니다.</p>
<h3 id="꼬리질문-외부-pg-webhook-내부-트랜잭션-장애-발생-시-어떻게-대응할-수-있나요">꼬리질문. 외부 PG, Webhook, 내부 트랜잭션 장애 발생 시 어떻게 대응할 수 있나요?</h3>
<p>장애 위치에 따라 대응 방식이 달라집니다.</p>
<table>
<thead>
<tr>
<th>장애 위치</th>
<th>대응 방식</th>
</tr>
</thead>
<tbody><tr>
<td>외부 PG 장애</td>
<td>실패 응답 저장, 사용자에게 결제 실패 안내, 재시도 유도</td>
</tr>
<tr>
<td>Webhook 장애</td>
<td>결제 단건 조회 API나 배치로 상태 재동기화</td>
</tr>
<tr>
<td>내부 트랜잭션 장애</td>
<td>결제 상태와 내부 주문 상태 불일치 여부 확인 후 보정</td>
</tr>
<tr>
<td>DB 장애</td>
<td>트랜잭션 롤백, 실패 로그 저장, 재처리 대상 등록</td>
</tr>
<tr>
<td>네트워크 장애</td>
<td>timeout 설정, 재시도 정책, 중복 처리 방지</td>
</tr>
</tbody></table>
<p>중요한 것은 결제 성공 여부를 클라이언트 응답만으로 판단하지 않고, PG 결제 상태와 내부 DB 상태를 비교해서 불일치를 복구할 수 있게 만드는 것입니다.</p>
<hr>
<h1 id="마무리-정리">마무리 정리</h1>
<p>CH4 클라우드·결제 시스템 면접에서는 단순히 기능 구현 여부보다 <strong>장애 상황에서 정합성을 어떻게 보장했는지</strong>를 많이 물어볼 수 있습니다.</p>
<p>특히 다음 키워드는 꼭 연결해서 답변하는 것이 좋습니다.</p>
<ul>
<li>EC2, RDS, S3의 역할 분리</li>
<li>VPC와 보안 그룹 설정</li>
<li>Docker를 통한 실행 환경 통일</li>
<li>HTTPS 적용 이유</li>
<li>서버 기준 결제 금액 검증</li>
<li>Webhook 멱등성</li>
<li>포인트 원장 관리</li>
<li>부분 환불 금액 산정</li>
<li>재고 선차감과 복구</li>
<li>트랜잭션과 락</li>
<li>결제 실패 추적과 상태 보정</li>
</ul>
<p>면접에서는 “구현했습니다”에서 끝내기보다,
<strong>왜 그렇게 설계했는지</strong>, <strong>장애가 나면 어떻게 복구할 수 있는지</strong>, <strong>중복 요청이 와도 안전한지</strong>까지 함께 설명하는 것이 중요합니다.</p>
<p>출처: 내일배움 스파르타 클럽<img src="https://velog.velcdn.com/images/loaded_diaper/post/d41d4e81-946f-4727-960b-2c4904f3589f/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CH1-3 Java ~ 숙련 Spring 모의면접 예상질문 리스트]]></title>
            <link>https://velog.io/@loaded_diaper/CH1-3-Java-%EC%88%99%EB%A0%A8-Spring-%EB%AA%A8%EC%9D%98%EB%A9%B4%EC%A0%91-%EC%98%88%EC%83%81%EC%A7%88%EB%AC%B8-%EB%A6%AC%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@loaded_diaper/CH1-3-Java-%EC%88%99%EB%A0%A8-Spring-%EB%AA%A8%EC%9D%98%EB%A9%B4%EC%A0%91-%EC%98%88%EC%83%81%EC%A7%88%EB%AC%B8-%EB%A6%AC%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Mon, 29 Jun 2026 03:08:34 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/04916cb9-667d-48b7-95d0-1826f75313ad/image.png" alt=""></p>
<h1 id="ch1-3-java--숙련-spring-모의면접-예상질문-정리">CH1-3 Java ~ 숙련 Spring 모의면접 예상질문 정리</h1>
<blockquote>
<p>Java / OOP, HTTP / Web, Spring, JPA, 인증 영역에서 자주 나올 수 있는 면접 질문과 핵심 답변을 정리했습니다.</p>
</blockquote>
<hr>
<h1 id="java--oop-영역">Java / OOP 영역</h1>
<h2 id="q1-oop의-4가지-특징을-설명해주세요">Q1. OOP의 4가지 특징을 설명해주세요.</h2>
<p><strong>A.</strong></p>
<p>OOP의 4가지 특징은 <strong>캡슐화, 상속, 다형성, 추상화</strong>입니다.</p>
<ul>
<li><strong>캡슐화</strong>: 데이터와 기능을 하나의 객체로 묶고, 외부에서 직접 접근하지 못하게 보호하는 것</li>
<li><strong>상속</strong>: 부모 클래스의 필드와 메서드를 자식 클래스가 물려받아 재사용하는 것</li>
<li><strong>다형성</strong>: 같은 타입으로 여러 구현 객체를 다룰 수 있는 것</li>
<li><strong>추상화</strong>: 공통된 특징만 뽑아 복잡한 내부 구현을 숨기고 필요한 기능만 보여주는 것</li>
</ul>
<h3 id="꼬리질문-1-캡슐화와-은닉화는-같은-개념인가요">꼬리질문 1. 캡슐화와 은닉화는 같은 개념인가요?</h3>
<p>완전히 같은 개념은 아닙니다.</p>
<p><strong>은닉화</strong>는 필드를 <code>private</code>으로 막는 것처럼 내부 데이터를 숨기는 개념이고,
<strong>캡슐화</strong>는 데이터와 메서드를 객체 안에 묶고 필요한 기능만 외부에 제공하는 더 넓은 개념입니다.</p>
<h3 id="꼬리질문-2-다형성을-실제-코드에서-어떻게-활용했나요">꼬리질문 2. 다형성을 실제 코드에서 어떻게 활용했나요?</h3>
<p>예를 들어 <code>PaymentService</code>가 <code>KakaoPay</code>, <code>TossPay</code> 같은 구체 클래스가 아니라 <code>PaymentClient</code> 인터페이스에 의존하게 만들 수 있습니다.</p>
<p>그러면 결제 방식이 바뀌어도 서비스 코드는 크게 변경하지 않고 구현체만 교체할 수 있습니다.</p>
<hr>
<h2 id="q2-오버로딩과-오버라이딩의-차이를-설명해주세요">Q2. 오버로딩과 오버라이딩의 차이를 설명해주세요.</h2>
<p><strong>A.</strong></p>
<p><strong>오버로딩</strong>은 같은 이름의 메서드를 매개변수 개수나 타입을 다르게 해서 여러 개 정의하는 것입니다.</p>
<p><strong>오버라이딩</strong>은 부모 클래스나 인터페이스의 메서드를 자식 클래스에서 재정의하는 것입니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>오버로딩</th>
<th>오버라이딩</th>
</tr>
</thead>
<tbody><tr>
<td>기준</td>
<td>메서드 이름 같음</td>
<td>부모 메서드 재정의</td>
</tr>
<tr>
<td>매개변수</td>
<td>달라야 함</td>
<td>같아야 함</td>
</tr>
<tr>
<td>반환 타입</td>
<td>자유로움</td>
<td>같거나 하위 타입</td>
</tr>
<tr>
<td>시점</td>
<td>컴파일 시점</td>
<td>런타임 시점</td>
</tr>
</tbody></table>
<h3 id="꼬리질문-1-override를-붙이지-않아도-동작하는데-왜-붙이나요">꼬리질문 1. <code>@Override</code>를 붙이지 않아도 동작하는데 왜 붙이나요?</h3>
<p><code>@Override</code>를 붙이면 컴파일러가 실제로 오버라이딩이 맞는지 검사해줍니다.</p>
<p>메서드 이름이나 파라미터를 실수로 잘못 작성했을 때 컴파일 단계에서 오류를 잡을 수 있어서 안정성이 높아집니다.</p>
<h3 id="꼬리질문-2-오버라이딩된-메서드에서-부모-메서드를-호출하려면-어떻게-하나요">꼬리질문 2. 오버라이딩된 메서드에서 부모 메서드를 호출하려면 어떻게 하나요?</h3>
<p><code>super.메서드명()</code>을 사용합니다.</p>
<p>예를 들어 <code>super.print()</code>처럼 호출하면 부모 클래스의 원래 메서드를 실행할 수 있습니다.</p>
<hr>
<h2 id="q3-인터페이스와-추상-클래스의-차이를-설명하고-어떤-상황에서-각각-선택하나요">Q3. 인터페이스와 추상 클래스의 차이를 설명하고, 어떤 상황에서 각각 선택하나요?</h2>
<p><strong>A.</strong></p>
<p><strong>인터페이스</strong>는 객체가 반드시 구현해야 하는 기능의 규칙을 정의할 때 사용합니다.</p>
<p><strong>추상 클래스</strong>는 공통 필드나 공통 로직을 자식 클래스에 물려주면서 일부 메서드는 자식이 직접 구현하게 할 때 사용합니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>인터페이스</th>
<th>추상 클래스</th>
</tr>
</thead>
<tbody><tr>
<td>목적</td>
<td>기능 명세, 규칙 정의</td>
<td>공통 로직 재사용</td>
</tr>
<tr>
<td>다중 구현</td>
<td>가능</td>
<td>불가능</td>
</tr>
<tr>
<td>필드</td>
<td>상수 중심</td>
<td>인스턴스 필드 가능</td>
</tr>
<tr>
<td>생성자</td>
<td>없음</td>
<td>있음</td>
</tr>
</tbody></table>
<h3 id="꼬리질문-1-java-8-이후-default-메서드가-생겼는데-추상-클래스와-차이가-없어졌나요">꼬리질문 1. Java 8 이후 default 메서드가 생겼는데 추상 클래스와 차이가 없어졌나요?</h3>
<p>아닙니다.</p>
<p>인터페이스에 <code>default</code> 메서드가 추가되어 기본 구현은 가능해졌지만, 인터페이스는 여전히 상태를 가지는 인스턴스 필드나 생성자를 가질 수 없습니다.</p>
<p>추상 클래스는 공통 상태와 공통 로직을 함께 관리할 수 있다는 차이가 있습니다.</p>
<h3 id="꼬리질문-2-다중-상속이-필요한-상황에서-인터페이스를-선택하는-이유는-무엇인가요">꼬리질문 2. 다중 상속이 필요한 상황에서 인터페이스를 선택하는 이유는 무엇인가요?</h3>
<p>Java는 클래스 다중 상속을 지원하지 않지만, 인터페이스는 여러 개 구현할 수 있습니다.</p>
<p>그래서 여러 역할이나 기능을 동시에 부여해야 할 때 인터페이스를 사용합니다.</p>
<hr>
<h2 id="q4-java-컬렉션-프레임워크에서-list-set-map의-특징과-차이를-설명해주세요">Q4. Java 컬렉션 프레임워크에서 List, Set, Map의 특징과 차이를 설명해주세요.</h2>
<p><strong>A.</strong></p>
<ul>
<li><strong>List</strong>: 순서가 있고, 중복을 허용합니다.</li>
<li><strong>Set</strong>: 순서가 보장되지 않는 경우가 많고, 중복을 허용하지 않습니다.</li>
<li><strong>Map</strong>: Key-Value 구조로 데이터를 저장하고, Key는 중복될 수 없습니다.</li>
</ul>
<table>
<thead>
<tr>
<th>구분</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>List</td>
<td>순서 있음, 중복 허용</td>
</tr>
<tr>
<td>Set</td>
<td>중복 불가</td>
</tr>
<tr>
<td>Map</td>
<td>Key-Value 저장, Key 중복 불가</td>
</tr>
</tbody></table>
<h3 id="꼬리질문-1-arraylist와-linkedlist의-시간복잡도-차이는-무엇인가요">꼬리질문 1. ArrayList와 LinkedList의 시간복잡도 차이는 무엇인가요?</h3>
<p><code>ArrayList</code>는 배열 기반이라 인덱스 조회가 빠릅니다.</p>
<ul>
<li>조회: <code>O(1)</code></li>
<li>중간 삽입/삭제: <code>O(n)</code></li>
</ul>
<p><code>LinkedList</code>는 노드 연결 구조라 중간 삽입/삭제에 유리할 수 있지만, 특정 위치까지 이동해야 하므로 조회가 느립니다.</p>
<ul>
<li>조회: <code>O(n)</code></li>
<li>삽입/삭제: 위치를 알고 있으면 <code>O(1)</code></li>
</ul>
<p>일반적인 조회가 많으면 <code>ArrayList</code>를 주로 사용합니다.</p>
<h3 id="꼬리질문-2-hashmap의-키-중복-판단-기준은-무엇인가요">꼬리질문 2. HashMap의 키 중복 판단 기준은 무엇인가요?</h3>
<p><code>HashMap</code>은 먼저 <code>hashCode()</code>로 저장 위치를 찾고, 같은 해시 위치에 값이 있으면 <code>equals()</code>로 실제 같은 Key인지 비교합니다.</p>
<p>즉, Key 중복 판단에는 <code>hashCode()</code>와 <code>equals()</code>가 함께 사용됩니다.</p>
<hr>
<h1 id="http--web-기초-영역">HTTP / Web 기초 영역</h1>
<h2 id="q5-get과-post의-차이를-데이터-전달-방식과-사용-목적-관점에서-설명해주세요">Q5. GET과 POST의 차이를 데이터 전달 방식과 사용 목적 관점에서 설명해주세요.</h2>
<p><strong>A.</strong></p>
<p><strong>GET</strong>은 주로 데이터를 조회할 때 사용하고, 요청 데이터는 보통 URL의 Query Parameter로 전달합니다.</p>
<p><strong>POST</strong>는 데이터를 생성하거나 처리할 때 사용하고, 요청 데이터는 주로 Body에 담아 전달합니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>GET</th>
<th>POST</th>
</tr>
</thead>
<tbody><tr>
<td>목적</td>
<td>조회</td>
<td>생성, 처리</td>
</tr>
<tr>
<td>데이터 전달</td>
<td>Query Parameter</td>
<td>Body</td>
</tr>
<tr>
<td>멱등성</td>
<td>일반적으로 있음</td>
<td>일반적으로 없음</td>
</tr>
<tr>
<td>캐싱</td>
<td>가능</td>
<td>보통 캐싱하지 않음</td>
</tr>
</tbody></table>
<h3 id="꼬리질문-1-get-요청에-body를-담아-보내면-어떻게-되나요">꼬리질문 1. GET 요청에 Body를 담아 보내면 어떻게 되나요?</h3>
<p>기술적으로 Body를 담는 것이 완전히 불가능한 것은 아닙니다.</p>
<p>하지만 HTTP 표준 관점에서 일반적인 사용 방식이 아니고, 서버나 프록시, 라이브러리에서 Body를 무시할 수 있기 때문에 권장하지 않습니다.</p>
<h3 id="꼬리질문-2-조회-api인데-요청-파라미터가-많아-url-길이-제한에-걸리면-어떻게-설계하나요">꼬리질문 2. 조회 API인데 요청 파라미터가 많아 URL 길이 제한에 걸리면 어떻게 설계하나요?</h3>
<p>복잡한 검색 조건이라면 <code>POST /search</code>처럼 Body에 검색 조건을 담는 방식으로 설계할 수 있습니다.</p>
<p>단, 이 경우에도 실제 의미는 조회이므로 API 이름과 문서에서 검색용 요청이라는 것을 명확히 하는 것이 좋습니다.</p>
<hr>
<h2 id="q6-http-상태코드-200-201-400-404-500의-의미를-설명해주세요">Q6. HTTP 상태코드 200, 201, 400, 404, 500의 의미를 설명해주세요.</h2>
<p><strong>A.</strong></p>
<table>
<thead>
<tr>
<th>상태코드</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>200 OK</td>
<td>요청 성공</td>
</tr>
<tr>
<td>201 Created</td>
<td>리소스 생성 성공</td>
</tr>
<tr>
<td>400 Bad Request</td>
<td>잘못된 요청</td>
</tr>
<tr>
<td>404 Not Found</td>
<td>리소스를 찾을 수 없음</td>
</tr>
<tr>
<td>500 Internal Server Error</td>
<td>서버 내부 오류</td>
</tr>
</tbody></table>
<h3 id="꼬리질문-1-생성-api에서-200-대신-201을-반환해야-하는-이유가-있나요">꼬리질문 1. 생성 API에서 200 대신 201을 반환해야 하는 이유가 있나요?</h3>
<p><code>201 Created</code>는 새로운 리소스가 생성되었다는 의미를 더 명확하게 전달합니다.</p>
<p>따라서 생성 API에서는 <code>200 OK</code>보다 <code>201 Created</code>가 RESTful한 의미에 더 잘 맞습니다.</p>
<h3 id="꼬리질문-2-없는-리소스를-요청했을-때-404-대신-200과-빈-데이터를-반환하는-방식도-있는데-어떤-방식이-더-적절한가요">꼬리질문 2. 없는 리소스를 요청했을 때 404 대신 200과 빈 데이터를 반환하는 방식도 있는데, 어떤 방식이 더 적절한가요?</h3>
<p>단건 조회에서 특정 ID의 리소스가 없다면 <code>404 Not Found</code>가 더 적절합니다.</p>
<p>반면 목록 조회 결과가 비어 있는 경우에는 요청 자체는 성공한 것이므로 <code>200 OK</code>와 빈 배열을 반환하는 것이 자연스럽습니다.</p>
<hr>
<h2 id="q7-cookie와-session의-차이를-저장-위치와-보안-관점에서-설명해주세요">Q7. Cookie와 Session의 차이를 저장 위치와 보안 관점에서 설명해주세요.</h2>
<p><strong>A.</strong></p>
<p><strong>Cookie</strong>는 클라이언트 브라우저에 저장되는 데이터입니다.</p>
<p><strong>Session</strong>은 서버에 사용자 상태를 저장하고, 클라이언트는 Session ID만 Cookie로 들고 있습니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>Cookie</th>
<th>Session</th>
</tr>
</thead>
<tbody><tr>
<td>저장 위치</td>
<td>클라이언트</td>
<td>서버</td>
</tr>
<tr>
<td>보안</td>
<td>상대적으로 취약</td>
<td>상대적으로 안전</td>
</tr>
<tr>
<td>서버 부담</td>
<td>적음</td>
<td>있음</td>
</tr>
<tr>
<td>사용 예시</td>
<td>자동 로그인, 사용자 설정</td>
<td>로그인 상태 관리</td>
</tr>
</tbody></table>
<h3 id="꼬리질문-1-session-사용-중-서버가-여러-대로-확장되면-어떤-문제가-생기나요">꼬리질문 1. Session 사용 중 서버가 여러 대로 확장되면 어떤 문제가 생기나요?</h3>
<p>서버마다 세션 저장소가 다르면 사용자가 처음 로그인한 서버가 아닌 다른 서버로 요청했을 때 세션을 찾지 못할 수 있습니다.</p>
<p>이를 해결하기 위해 Redis 같은 중앙 세션 저장소를 사용하거나 Sticky Session을 사용할 수 있습니다.</p>
<h3 id="꼬리질문-2-cookie의-httponly와-secure-속성은-각각-어떤-보안-위협을-방지하나요">꼬리질문 2. Cookie의 HttpOnly와 Secure 속성은 각각 어떤 보안 위협을 방지하나요?</h3>
<p><code>HttpOnly</code>는 JavaScript에서 쿠키에 접근하지 못하게 해서 XSS 공격으로 쿠키가 탈취되는 위험을 줄입니다.</p>
<p><code>Secure</code>는 HTTPS 통신에서만 쿠키가 전송되도록 해서 네트워크 구간에서 쿠키가 노출되는 위험을 줄입니다.</p>
<hr>
<h1 id="spring-기초-영역">Spring 기초 영역</h1>
<h2 id="q8-3-layer-architecture를-사용하는-이유는-무엇인가요">Q8. 3 Layer Architecture를 사용하는 이유는 무엇인가요?</h2>
<p><strong>A.</strong></p>
<p>3 Layer Architecture는 역할을 분리해서 코드의 유지보수성과 테스트 편의성을 높이기 위해 사용합니다.</p>
<ul>
<li><strong>Controller</strong>: 요청과 응답 처리</li>
<li><strong>Service</strong>: 비즈니스 로직 처리</li>
<li><strong>Repository</strong>: 데이터베이스 접근</li>
</ul>
<p>각 계층의 책임이 분리되면 변경이 필요한 부분을 찾기 쉽고, 테스트하기도 쉬워집니다.</p>
<h3 id="꼬리질문-1-service-레이어에서-다른-service를-호출하는-것이-왜-문제가-될-수-있나요">꼬리질문 1. Service 레이어에서 다른 Service를 호출하는 것이 왜 문제가 될 수 있나요?</h3>
<p>Service끼리 무분별하게 호출하면 의존 관계가 복잡해지고 순환 참조가 발생할 수 있습니다.</p>
<p>또한 하나의 Service가 너무 많은 책임을 가지게 되어 유지보수가 어려워질 수 있습니다.</p>
<p>여러 도메인이 엮이는 로직은 Facade 계층을 두어 흐름을 조율하는 방식도 사용할 수 있습니다.</p>
<h3 id="꼬리질문-2-controller에-비즈니스-로직을-바로-작성하면-어떤-문제가-생기나요">꼬리질문 2. Controller에 비즈니스 로직을 바로 작성하면 어떤 문제가 생기나요?</h3>
<p>Controller가 요청 처리뿐만 아니라 비즈니스 로직까지 담당하게 되어 책임이 커집니다.</p>
<p>그 결과 코드 중복이 생기고, 테스트가 어려워지며, API 변경 시 비즈니스 로직까지 영향을 받을 수 있습니다.</p>
<hr>
<h2 id="q9-spring의-ioc와-di가-무엇인지-생성자-주입이-권장되는-이유를-설명해주세요">Q9. Spring의 IoC와 DI가 무엇인지, 생성자 주입이 권장되는 이유를 설명해주세요.</h2>
<p><strong>A.</strong></p>
<p><strong>IoC</strong>는 객체 생성과 의존성 관리의 제어권을 개발자가 아니라 Spring 컨테이너가 가지는 것입니다.</p>
<p><strong>DI</strong>는 필요한 의존 객체를 직접 생성하지 않고 외부에서 주입받는 방식입니다.</p>
<p>생성자 주입이 권장되는 이유는 다음과 같습니다.</p>
<ul>
<li>의존성을 <code>final</code>로 선언할 수 있음</li>
<li>객체 생성 시점에 필수 의존성을 보장할 수 있음</li>
<li>테스트 코드 작성이 쉬움</li>
<li>순환 의존성을 빠르게 발견할 수 있음</li>
</ul>
<h3 id="꼬리질문-1-필드-주입은-편한데-왜-권장하지-않나요">꼬리질문 1. 필드 주입은 편한데 왜 권장하지 않나요?</h3>
<p>필드 주입은 외부에서 의존성을 주입하기 어렵기 때문에 테스트가 불편합니다.</p>
<p>또한 의존성이 숨겨져 있어 객체가 어떤 의존성을 필요로 하는지 명확히 드러나지 않습니다.</p>
<h3 id="꼬리질문-2-순환-의존성이란-무엇이고-생성자-주입에서는-왜-더-빨리-감지되나요">꼬리질문 2. 순환 의존성이란 무엇이고, 생성자 주입에서는 왜 더 빨리 감지되나요?</h3>
<p>순환 의존성은 A가 B를 필요로 하고, B도 다시 A를 필요로 하는 구조입니다.</p>
<p>생성자 주입은 객체 생성 시점에 모든 의존성이 필요하기 때문에 순환 구조가 있으면 애플리케이션 시작 단계에서 바로 오류가 발생합니다.</p>
<hr>
<h2 id="q10-spring-bean이-무엇이고-싱글톤으로-관리되는-이유가-무엇인가요">Q10. Spring Bean이 무엇이고, 싱글톤으로 관리되는 이유가 무엇인가요?</h2>
<p><strong>A.</strong></p>
<p>Spring Bean은 Spring 컨테이너가 생성하고 관리하는 객체입니다.</p>
<p>Spring은 기본적으로 Bean을 싱글톤으로 관리합니다.</p>
<p>그 이유는 매 요청마다 객체를 새로 만들지 않고 하나의 객체를 재사용해서 메모리 사용을 줄이고 성능을 높이기 위해서입니다.</p>
<h3 id="꼬리질문-1-스프링-빈이-싱글톤이면-멀티스레드-환경에서-상태를-가지면-어떤-문제가-생기나요">꼬리질문 1. 스프링 빈이 싱글톤이면 멀티스레드 환경에서 상태를 가지면 어떤 문제가 생기나요?</h3>
<p>싱글톤 Bean은 여러 요청이 동시에 공유합니다.</p>
<p>따라서 인스턴스 필드에 사용자별 상태를 저장하면 다른 요청과 데이터가 섞이는 동시성 문제가 발생할 수 있습니다.</p>
<p>그래서 Service나 Controller Bean은 보통 상태를 가지지 않도록 설계합니다.</p>
<h3 id="꼬리질문-2-component와-bean의-차이는-무엇인가요">꼬리질문 2. <code>@Component</code>와 <code>@Bean</code>의 차이는 무엇인가요?</h3>
<p><code>@Component</code>는 클래스에 붙여서 Spring이 자동으로 스캔해 Bean으로 등록하게 합니다.</p>
<p><code>@Bean</code>은 설정 클래스에서 메서드에 붙여 직접 객체를 생성하고 Bean으로 등록할 때 사용합니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>사용 상황</th>
</tr>
</thead>
<tbody><tr>
<td><code>@Component</code></td>
<td>내가 만든 클래스를 자동 등록할 때</td>
</tr>
<tr>
<td><code>@Bean</code></td>
<td>외부 라이브러리 객체를 Bean으로 등록할 때</td>
</tr>
</tbody></table>
<hr>
<h2 id="q11-dto를-사용하는-이유가-무엇인가요-entity를-직접-반환하면-안-되는-이유를-설명해주세요">Q11. DTO를 사용하는 이유가 무엇인가요? Entity를 직접 반환하면 안 되는 이유를 설명해주세요.</h2>
<p><strong>A.</strong></p>
<p>DTO는 계층 간 데이터를 전달하기 위한 객체입니다.</p>
<p>Entity를 직접 반환하면 다음과 같은 문제가 생길 수 있습니다.</p>
<ul>
<li>DB 구조가 API 응답에 그대로 노출됨</li>
<li>민감한 필드가 노출될 수 있음</li>
<li>Entity 변경이 API 스펙 변경으로 이어짐</li>
<li>양방향 연관관계에서 무한 참조 문제가 발생할 수 있음</li>
</ul>
<p>따라서 API 요청과 응답에는 Entity 대신 DTO를 사용하는 것이 좋습니다.</p>
<h3 id="꼬리질문-1-requestdto와-responsedto를-분리하는-이유는-무엇인가요">꼬리질문 1. RequestDTO와 ResponseDTO를 분리하는 이유는 무엇인가요?</h3>
<p>요청 데이터와 응답 데이터의 목적이 다르기 때문입니다.</p>
<p>RequestDTO는 클라이언트가 보내는 값을 검증하는 데 집중하고,
ResponseDTO는 클라이언트에게 보여줄 데이터만 담는 데 집중합니다.</p>
<h3 id="꼬리질문-2-entity에-jsonignore를-붙여-민감한-필드를-숨기면-dto-없이도-되지-않나요">꼬리질문 2. Entity에 <code>@JsonIgnore</code>를 붙여 민감한 필드를 숨기면 DTO 없이도 되지 않나요?</h3>
<p>간단한 경우에는 가능할 수 있지만 권장되지는 않습니다.</p>
<p><code>@JsonIgnore</code>는 직렬화 문제만 일부 해결할 뿐, API 응답 구조와 Entity 구조가 강하게 묶이는 문제는 해결하지 못합니다.</p>
<p>DTO를 사용하면 API 스펙과 DB 모델을 분리할 수 있습니다.</p>
<hr>
<h1 id="jpa-영역">JPA 영역</h1>
<h2 id="q12-jpa의-영속성-컨텍스트란-무엇이고-어떤-이점이-있나요">Q12. JPA의 영속성 컨텍스트란 무엇이고, 어떤 이점이 있나요?</h2>
<p><strong>A.</strong></p>
<p>영속성 컨텍스트는 JPA가 Entity를 관리하는 1차 캐시 공간입니다.</p>
<p>EntityManager가 Entity를 저장하고 관리하는 영역이라고 볼 수 있습니다.</p>
<p>주요 이점은 다음과 같습니다.</p>
<ul>
<li>1차 캐시</li>
<li>동일성 보장</li>
<li>변경 감지</li>
<li>쓰기 지연</li>
<li>지연 로딩 지원</li>
</ul>
<h3 id="꼬리질문-1-같은-트랜잭션-내에서-같은-id로-두-번-조회하면-sql이-몇-번-실행되나요">꼬리질문 1. 같은 트랜잭션 내에서 같은 ID로 두 번 조회하면 SQL이 몇 번 실행되나요?</h3>
<p>처음 조회할 때만 SQL이 실행되고, 두 번째 조회는 영속성 컨텍스트의 1차 캐시에서 가져옵니다.</p>
<p>따라서 같은 트랜잭션 안에서는 같은 ID의 Entity에 대해 동일성이 보장됩니다.</p>
<h3 id="꼬리질문-2-준영속-상태란-무엇이고-언제-발생하나요">꼬리질문 2. 준영속 상태란 무엇이고, 언제 발생하나요?</h3>
<p>준영속 상태는 한 번 영속성 컨텍스트에서 관리되었지만 현재는 분리되어 관리되지 않는 상태입니다.</p>
<p>예를 들어 트랜잭션이 끝나거나, <code>detach()</code>, <code>clear()</code>, <code>close()</code>가 호출되면 Entity가 준영속 상태가 될 수 있습니다.</p>
<hr>
<h2 id="q13-더티-체킹이란-무엇인가요-save를-호출하지-않아도-update가-되는-이유를-설명해주세요">Q13. 더티 체킹이란 무엇인가요? save()를 호출하지 않아도 UPDATE가 되는 이유를 설명해주세요.</h2>
<p><strong>A.</strong></p>
<p>더티 체킹은 영속성 컨텍스트가 관리 중인 Entity의 변경 사항을 감지해서 트랜잭션 커밋 시점에 자동으로 UPDATE 쿼리를 실행하는 기능입니다.</p>
<p>JPA는 Entity를 처음 조회했을 때의 스냅샷을 가지고 있다가, 커밋 시점에 현재 Entity와 비교해 변경 사항이 있으면 UPDATE를 실행합니다.</p>
<h3 id="꼬리질문-1-transactional이-없는-메서드에서-entity를-수정하면-더티-체킹이-동작하나요">꼬리질문 1. <code>@Transactional</code>이 없는 메서드에서 Entity를 수정하면 더티 체킹이 동작하나요?</h3>
<p>일반적으로 동작하지 않습니다.</p>
<p>더티 체킹은 영속성 컨텍스트와 트랜잭션 안에서 의미가 있습니다.</p>
<p>트랜잭션 없이 Entity를 수정하면 변경 사항이 DB에 반영되지 않을 수 있습니다.</p>
<h3 id="꼬리질문-2-더티-체킹으로-발생하는-update-쿼리는-변경된-컬럼만-업데이트하나요-전체-컬럼을-업데이트하나요">꼬리질문 2. 더티 체킹으로 발생하는 UPDATE 쿼리는 변경된 컬럼만 업데이트하나요, 전체 컬럼을 업데이트하나요?</h3>
<p>기본적으로 JPA는 전체 컬럼을 업데이트하는 경우가 많습니다.</p>
<p>변경된 컬럼만 업데이트하고 싶다면 Hibernate의 <code>@DynamicUpdate</code>를 사용할 수 있습니다.</p>
<p>다만 무조건 사용하는 것이 좋은 것은 아니고, 상황에 따라 선택해야 합니다.</p>
<hr>
<h2 id="q14-즉시-로딩보다-지연-로딩을-권장하는-이유를-설명해주세요">Q14. 즉시 로딩보다 지연 로딩을 권장하는 이유를 설명해주세요.</h2>
<p><strong>A.</strong></p>
<p>즉시 로딩은 Entity를 조회할 때 연관된 Entity까지 바로 조회합니다.</p>
<p>이로 인해 예상하지 못한 SQL이 실행되거나 성능 문제가 발생할 수 있습니다.</p>
<p>반면 지연 로딩은 실제로 연관 Entity가 필요할 때 조회하기 때문에 불필요한 조회를 줄일 수 있습니다.</p>
<p>그래서 실무에서는 기본적으로 <code>LAZY</code>를 권장합니다.</p>
<h3 id="꼬리질문-1-지연-로딩-상태에서-연관-entity에-접근할-때-트랜잭션이-없으면-어떤-예외가-발생하나요">꼬리질문 1. 지연 로딩 상태에서 연관 Entity에 접근할 때 트랜잭션이 없으면 어떤 예외가 발생하나요?</h3>
<p><code>LazyInitializationException</code>이 발생할 수 있습니다.</p>
<p>이는 영속성 컨텍스트가 닫힌 상태에서 프록시 객체를 초기화하려고 할 때 발생합니다.</p>
<h3 id="꼬리질문-2-n1-문제가-무엇이고-fetch-join으로-어떻게-해결하나요">꼬리질문 2. N+1 문제가 무엇이고, Fetch Join으로 어떻게 해결하나요?</h3>
<p>N+1 문제는 처음에 1번의 쿼리로 목록을 조회한 뒤, 각 Entity의 연관 데이터를 조회하기 위해 추가 쿼리가 N번 발생하는 문제입니다.</p>
<p>Fetch Join을 사용하면 처음 조회할 때 연관 Entity를 함께 가져와 추가 쿼리 발생을 줄일 수 있습니다.</p>
<hr>
<h1 id="인증-영역">인증 영역</h1>
<h2 id="q15-세션-방식과-jwt-방식의-차이를-서버-상태-관리와-확장성-관점에서-설명해주세요">Q15. 세션 방식과 JWT 방식의 차이를 서버 상태 관리와 확장성 관점에서 설명해주세요.</h2>
<p><strong>A.</strong></p>
<p>세션 방식은 서버가 로그인 상태를 저장하고, 클라이언트는 Session ID를 쿠키로 전달합니다.</p>
<p>JWT 방식은 로그인 정보를 담은 토큰을 클라이언트가 가지고 있고, 서버는 토큰을 검증해서 인증합니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>세션</th>
<th>JWT</th>
</tr>
</thead>
<tbody><tr>
<td>상태 관리</td>
<td>서버가 상태 저장</td>
<td>서버가 상태 저장하지 않음</td>
</tr>
<tr>
<td>확장성</td>
<td>세션 저장소 필요</td>
<td>상대적으로 확장 쉬움</td>
</tr>
<tr>
<td>무효화</td>
<td>쉬움</td>
<td>상대적으로 어려움</td>
</tr>
<tr>
<td>저장 위치</td>
<td>서버</td>
<td>클라이언트</td>
</tr>
</tbody></table>
<h3 id="꼬리질문-1-jwt-토큰이-탈취됐을-때-즉시-무효화하기-어려운-이유는-무엇인가요">꼬리질문 1. JWT 토큰이 탈취됐을 때 즉시 무효화하기 어려운 이유는 무엇인가요?</h3>
<p>JWT는 서버가 상태를 저장하지 않고 토큰 자체를 검증하는 방식입니다.</p>
<p>그래서 한 번 발급된 Access Token은 만료 시간 전까지 유효합니다.</p>
<p>즉시 무효화하려면 블랙리스트를 관리해야 하는데, 그러면 서버가 별도의 상태를 관리해야 합니다.</p>
<h3 id="꼬리질문-2-access-token과-refresh-token을-분리해서-사용하는-이유는-무엇인가요">꼬리질문 2. Access Token과 Refresh Token을 분리해서 사용하는 이유는 무엇인가요?</h3>
<p>Access Token은 API 요청에 사용되기 때문에 탈취 위험을 줄이기 위해 만료 시간을 짧게 가져갑니다.</p>
<p>Refresh Token은 Access Token을 재발급받기 위해 사용하며, 상대적으로 긴 만료 시간을 가집니다.</p>
<p>이렇게 분리하면 보안성과 사용자 편의성을 함께 챙길 수 있습니다.</p>
<hr>
<h1 id="마무리-정리">마무리 정리</h1>
<p>이번 면접 질문들은 Java 기본기부터 Spring, JPA, 인증 방식까지 백엔드 개발자가 반드시 이해해야 하는 핵심 개념들입니다.</p>
<p>단순히 키워드를 외우기보다는 다음 흐름으로 답변하는 연습이 중요합니다.</p>
<ol>
<li>개념을 한 문장으로 설명한다.</li>
<li>왜 사용하는지 말한다.</li>
<li>실제 프로젝트에서 어떻게 적용했는지 예시를 든다.</li>
<li>장단점이나 주의점을 함께 말한다.</li>
</ol>
<p>면접에서는 완벽한 암기보다, 개념을 실제 코드와 연결해서 설명하는 것이 중요합니다.
<img src="https://velog.velcdn.com/images/loaded_diaper/post/904443bb-0393-4b53-a185-e7b4fb5f714d/image.png" alt=""></p>
<p>출처: 내일배움 스파르타 클럽</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] RedissonClient Bean 등록 후 테스트 빌드 실패 해결]]></title>
            <link>https://velog.io/@loaded_diaper/Troubleshooting-RedissonClient-Bean-%EB%93%B1%EB%A1%9D-%ED%9B%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%B9%8C%EB%93%9C-%EC%8B%A4%ED%8C%A8-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@loaded_diaper/Troubleshooting-RedissonClient-Bean-%EB%93%B1%EB%A1%9D-%ED%9B%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%B9%8C%EB%93%9C-%EC%8B%A4%ED%8C%A8-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Tue, 23 Jun 2026 10:50:06 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>선착순 이벤트 기능에서 Redis Lock을 사용하기 위해 <code>RedisLockManager</code>를 구현했다.</p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class RedisLockManager {

    private final RedissonClient redissonClient;
}</code></pre>
<p>하지만 이후 빌드를 실행하자 테스트 단계에서 실패했다.</p>
<pre><code class="language-bash">bash ./gradlew clean build</code></pre>
<p>처음 발생한 에러는 다음과 같았다.</p>
<pre><code class="language-text![](https://velog.velcdn.com/images/loaded_diaper/post/d38b840a-2703-4f59-9569-76d5b96884a2/image.png)">
Team8SaleCommerceApplicationTests &gt; contextLoads() FAILED
java.lang.IllegalStateException
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException</code></pre>
<h2 id="1차-원인">1차 원인</h2>
<p><code>RedisLockManager</code>는 <code>RedissonClient</code>를 생성자 주입으로 받고 있다.</p>
<p>그런데 프로젝트에 아직 <code>RedissonClient</code>를 Spring Bean으로 등록하는 설정 클래스가 없었다.</p>
<p>즉, Spring이 애플리케이션 컨텍스트를 로딩하면서 다음 흐름으로 실패했다.</p>
<pre><code class="language-text">RedisLockManager Bean 생성 시도
↓
RedissonClient 필요
↓
Spring Context 안에 RedissonClient Bean 없음
↓
NoSuchBeanDefinitionException 발생</code></pre>
<h2 id="1차-해결">1차 해결</h2>
<p><code>RedissonClient</code>를 Bean으로 등록하기 위해 <code>RedissonConfig</code>를 추가했다.</p>
<pre><code class="language-java">package com.example.team8salecommerce.global.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Redisson 설정 클래스
 *
 * Redis Lock을 사용하기 위해 RedissonClient를 Spring Bean으로 등록한다.
 */
@Configuration
public class RedissonConfig {

    @Value(&quot;${spring.data.redis.host:localhost}&quot;)
    private String redisHost;

    @Value(&quot;${spring.data.redis.port:6379}&quot;)
    private int redisPort;

    @Bean(destroyMethod = &quot;shutdown&quot;)
    public RedissonClient redissonClient() {
        Config config = new Config();

        config.useSingleServer()
                .setAddress(&quot;redis://&quot; + redisHost + &quot;:&quot; + redisPort);

        return Redisson.create(config);
    }
}</code></pre>
<p>이제 <code>RedissonClient</code> Bean은 정상적으로 등록되었다.</p>
<p>하지만 다시 빌드하자 또 다른 에러가 발생했다.</p>
<pre><code class="language-text">Caused by: org.redisson.client.RedisConnectionException
Caused by: java.net.ConnectException</code></pre>
<h2 id="2차-원인">2차 원인</h2>
<p>이번에는 Bean이 없어서 실패한 것이 아니라, 실제 Redis 서버 연결에 실패한 문제였다.</p>
<p><code>RedissonConfig</code>가 추가되면서 테스트 실행 시에도 실제 Redis 서버에 연결하려고 했다.</p>
<p>하지만 로컬에서 Redis가 실행 중이 아니었기 때문에 다음 흐름으로 실패했다.</p>
<pre><code class="language-text">Spring Context 로딩
↓
RedissonClient Bean 생성
↓
localhost:6379 Redis 연결 시도
↓
Redis 서버 실행 중 아님
↓
RedisConnectionException 발생</code></pre>
<p>즉, 문제의 핵심은 다음과 같았다.</p>
<pre><code class="language-text">실제 애플리케이션 실행에서는 Redis를 사용해야 한다.
하지만 contextLoads 테스트에서는 실제 Redis 서버 연결까지 필요하지 않다.</code></pre>
<p><code>contextLoads()</code> 테스트는 실제 Redis Lock 동작을 검증하는 테스트가 아니라, Spring 애플리케이션 컨텍스트가 정상적으로 로딩되는지만 확인하는 테스트이기 때문이다.</p>
<h2 id="최종-해결">최종 해결</h2>
<p>테스트 환경에서는 <code>RedissonClient</code>를 실제 Bean이 아니라 Mock Bean으로 대체했다.</p>
<pre><code class="language-java">package com.example.team8salecommerce;

import org.junit.jupiter.api.Test;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

/**
 * 애플리케이션 컨텍스트 로딩 테스트
 *
 * RedisLockManager는 RedissonClient를 필요로 한다.
 * 하지만 contextLoads 테스트에서 실제 Redis 서버까지 연결할 필요는 없으므로
 * RedissonClient를 MockitoBean으로 대체한다.
 */
@SpringBootTest
class Team8SaleCommerceApplicationTests {

    /**
     * 테스트용 RedissonClient Mock Bean
     *
     * 실제 Redis 서버가 켜져 있지 않아도
     * Spring Context가 정상적으로 로딩될 수 있게 한다.
     */
    @MockitoBean
    private RedissonClient redissonClient;

    @Test
    void contextLoads() {
    }
}</code></pre>
<p>수정 후 다시 빌드를 실행했다.</p>
<pre><code class="language-bash">bash ./gradlew clean build</code></pre>
<p>결과적으로 빌드가 성공했다.</p>
<pre><code class="language-text">BUILD SUCCESSFUL</code></pre>
<h2 id="실제-redis를-안-쓰는-건가">실제 Redis를 안 쓰는 건가?</h2>
<p>아니다.</p>
<p>실제 프로젝트에서는 Redis를 사용한다.</p>
<p>다만 테스트 중 <code>contextLoads()</code>에서는 실제 Redis 서버에 연결할 필요가 없기 때문에 Mock Bean으로 대체한 것이다.</p>
<p>정리하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>Redis 사용 여부</th>
</tr>
</thead>
<tbody><tr>
<td>실제 서버 실행</td>
<td>실제 Redis 사용</td>
</tr>
<tr>
<td>선착순 구매 Redis Lock</td>
<td>실제 Redis 사용</td>
</tr>
<tr>
<td>결제 실패 재고 복구 Lock</td>
<td>실제 Redis 사용</td>
</tr>
<tr>
<td>환불 재고 복구 Lock</td>
<td>실제 Redis 사용</td>
</tr>
<tr>
<td>contextLoads 테스트</td>
<td>Mock RedissonClient 사용</td>
</tr>
</tbody></table>
<p>실제 기능 테스트나 Postman 시연에서는 Redis 서버를 실행해야 한다.</p>
<pre><code class="language-bash">docker run -d --name team8-sale-redis -p 6379:6379 redis:7-alpine</code></pre>
<p>이미 Redis 컨테이너가 존재한다면 다음 명령어로 실행할 수 있다.</p>
<pre><code class="language-bash">docker start team8-sale-redis</code></pre>
<h2 id="정리">정리</h2>
<p>이번 문제는 두 단계로 나눠서 발생했다.</p>
<p>첫 번째 문제는 <code>RedissonClient</code> Bean이 등록되어 있지 않아 발생한 <code>NoSuchBeanDefinitionException</code>이었다.
이를 해결하기 위해 <code>RedissonConfig</code>를 추가하여 <code>RedissonClient</code>를 Spring Bean으로 등록했다.</p>
<p>두 번째 문제는 테스트 실행 시 실제 Redis 서버에 연결하려다가 실패한 <code>RedisConnectionException</code>이었다.
이를 해결하기 위해 <code>contextLoads()</code> 테스트에서는 <code>RedissonClient</code>를 Mock Bean으로 대체했다.</p>
<p>결과적으로 실제 애플리케이션에서는 Redis Lock을 정상적으로 사용할 수 있고, 기본 빌드 테스트는 Redis 서버 실행 여부에 의존하지 않도록 개선할 수 있었다.</p>
<h2 id="배운-점">배운 점</h2>
<p>외부 인프라를 사용하는 Bean을 추가할 때는 실제 실행 환경과 테스트 환경을 분리해서 생각해야 한다.</p>
<p>Redis, Kafka, S3, 외부 API처럼 별도 서버나 외부 서비스가 필요한 의존성은 테스트에서 실제 연결이 필요하지 않은 경우 Mock 처리하거나 테스트 전용 설정을 따로 두는 것이 좋다.</p>
<p>이번 경험을 통해 단순히 Bean을 등록하는 것뿐 아니라, 테스트 환경에서 해당 Bean이 어떻게 동작하는지도 함께 고려해야 한다는 점을 배웠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] CH 5 플러스 Spring 과제 초기 설정 트러블슈팅]]></title>
            <link>https://velog.io/@loaded_diaper/Spring-CH-5-%ED%94%8C%EB%9F%AC%EC%8A%A4-Spring-%EA%B3%BC%EC%A0%9C-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@loaded_diaper/Spring-CH-5-%ED%94%8C%EB%9F%AC%EC%8A%A4-Spring-%EA%B3%BC%EC%A0%9C-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</guid>
            <pubDate>Fri, 19 Jun 2026 01:31:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/466e12ef-298f-49f2-bc3d-f1dbf2fbd30c/image.png" alt=""></p>
<h2 id="들어가며">들어가며</h2>
<p>CH 5 플러스 Spring 과제를 진행하면서 본격적인 기능 구현에 들어가기 전, 프로젝트 실행 환경을 맞추는 과정에서 몇 가지 문제가 발생했다.</p>
<p>이번 과제는 기존 GitHub Repository를 fork한 뒤 local로 clone해서 진행하는 방식이었다.
프로젝트를 처음 실행하고 테스트를 돌리는 과정에서 Gradle Wrapper, Java 버전, JWT 설정값 관련 문제가 연속으로 발생했고, 이를 해결한 과정을 정리해보려고 한다.</p>
<hr>
<h2 id="1-gradle-wrapper-설정-파일-누락-문제">1. Gradle Wrapper 설정 파일 누락 문제</h2>
<h3 id="문제-상황">문제 상황</h3>
<p>프로젝트를 clone한 뒤 테스트를 실행했다.</p>
<pre><code class="language-bash">./gradlew test</code></pre>
<p>그런데 아래와 같은 에러가 발생했다.</p>
<pre><code class="language-text">Wrapper properties file &#39;.../gradle/wrapper/gradle-wrapper.properties&#39; does not exist.</code></pre>
<h3 id="원인">원인</h3>
<p>프로젝트에는 <code>gradlew</code> 파일과 <code>gradle-wrapper.jar</code> 파일은 존재했지만, Gradle Wrapper가 어떤 Gradle 버전을 사용할지 정의하는 <code>gradle-wrapper.properties</code> 파일이 없었다.</p>
<p>Gradle Wrapper가 정상적으로 동작하려면 보통 아래 구조가 필요하다.</p>
<pre><code class="language-text">gradle
└── wrapper
    ├── gradle-wrapper.jar
    └── gradle-wrapper.properties</code></pre>
<p>하지만 현재 프로젝트에는 <code>gradle-wrapper.properties</code>가 누락되어 있어서 <code>./gradlew</code> 명령어를 실행할 수 없었다.</p>
<h3 id="해결">해결</h3>
<p><code>gradle/wrapper/gradle-wrapper.properties</code> 파일을 직접 생성하고 Gradle 배포 URL을 설정했다.</p>
<pre><code class="language-properties">distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists</code></pre>
<p>이후 다시 Gradle 명령어를 실행하자 Wrapper 관련 에러는 해결되었다.</p>
<hr>
<h2 id="2-java-버전-불일치-문제">2. Java 버전 불일치 문제</h2>
<h3 id="문제-상황-1">문제 상황</h3>
<p>Gradle Wrapper 문제를 해결한 뒤 다시 테스트를 실행했다.</p>
<pre><code class="language-bash">./gradlew test</code></pre>
<p>이번에는 아래와 같은 에러가 발생했다.</p>
<pre><code class="language-text">Unsupported class file major version 67</code></pre>
<h3 id="원인-1">원인</h3>
<p><code>major version 67</code>은 Java 23에 해당한다.</p>
<p>현재 터미널에서 Java 23이 사용되고 있었고, 프로젝트와 Gradle 환경은 Java 17 기준으로 맞추는 것이 적절했다.
즉, 프로젝트에서 기대하는 Java 버전과 실제 터미널에서 사용하는 Java 버전이 달라서 문제가 발생한 것이다.</p>
<h3 id="해결-1">해결</h3>
<p>터미널과 IntelliJ 설정을 Java 17로 맞췄다.</p>
<p>터미널에서는 <code>JAVA_HOME</code>을 Java 17로 설정했다.</p>
<pre><code class="language-bash">export JAVA_HOME=$(/usr/libexec/java_home -v 17)
export PATH=&quot;$JAVA_HOME/bin:$PATH&quot;</code></pre>
<p>이후 확인했다.</p>
<pre><code class="language-bash">java -version</code></pre>
<p>또한 IntelliJ에서도 아래 설정을 Java 17로 맞췄다.</p>
<ul>
<li>Project SDK: Java 17</li>
<li>Gradle JVM: Java 17</li>
</ul>
<p>이후 Gradle daemon을 정리하고 다시 테스트를 실행했다.</p>
<pre><code class="language-bash">./gradlew --stop
./gradlew clean test</code></pre>
<p>Java 버전 문제는 해결되었다.</p>
<hr>
<h2 id="3-jwt-secret-key-누락-문제">3. JWT secret key 누락 문제</h2>
<h3 id="문제-상황-2">문제 상황</h3>
<p>Java 버전 문제를 해결한 뒤 테스트를 실행했지만, 이번에는 Spring Context 로딩 중 실패가 발생했다.</p>
<p>에러의 핵심은 다음과 같았다.</p>
<pre><code class="language-text">PropertyPlaceholderHelper</code></pre>
<p>프로젝트 코드를 확인해보니 <code>JwtUtil</code>에서 아래 설정값을 사용하고 있었다.</p>
<pre><code class="language-java">@Value(&quot;${jwt.secret.key}&quot;)
private String secretKey;</code></pre>
<h3 id="원인-2">원인</h3>
<p>테스트 환경에서 <code>jwt.secret.key</code> 값이 설정되어 있지 않았다.</p>
<p>Spring은 <code>${jwt.secret.key}</code> 값을 찾아 주입해야 하는데, 테스트 실행 시 해당 설정값을 찾지 못해 ApplicationContext 로딩에 실패했다.</p>
<h3 id="해결-2">해결</h3>
<p>테스트 전용 설정 파일을 생성했다.</p>
<pre><code class="language-text">src/test/resources/application.properties</code></pre>
<p>그리고 테스트용 JWT secret key를 추가했다.</p>
<p>처음에는 일반 문자열을 넣었지만, 이후 Base64 디코딩 과정에서 다시 오류가 발생했다.</p>
<pre><code class="language-text">IllegalArgumentException at Base64.java</code></pre>
<p><code>JwtUtil</code> 내부에서 secret key를 Base64로 디코딩하고 있었기 때문에, 일반 문자열이 아니라 Base64 형식의 값이 필요했다.</p>
<p>그래서 테스트용 값을 Base64 형식으로 수정했다.</p>
<pre><code class="language-properties">jwt.secret.key=c3BhcnRhLXNwcmluZy1wbHVzLXRlc3Qtand0LXNlY3JldC1rZXktMTIzNDU2Nzg5MA==</code></pre>
<p>이후 Spring Context가 정상적으로 로딩되었다.</p>
<hr>
<h2 id="4-controller-테스트-기대값-불일치-문제">4. Controller 테스트 기대값 불일치 문제</h2>
<h3 id="문제-상황-3">문제 상황</h3>
<p>초기 설정 문제를 해결한 뒤에도 아래 테스트가 실패했다.</p>
<pre><code class="language-text">todo_단건_조회_시_todo가_존재하지_않아_예외가_발생한다()</code></pre>
<p>테스트 코드를 확인해보니 서비스에서 예외를 던지도록 설정되어 있었다.</p>
<pre><code class="language-java">when(todoService.getTodo(todoId))
        .thenThrow(new InvalidRequestException(&quot;Todo not found&quot;));</code></pre>
<p>하지만 테스트의 기대 응답은 <code>200 OK</code>로 되어 있었다.</p>
<pre><code class="language-java">.andExpect(status().isOk())</code></pre>
<h3 id="원인-3">원인</h3>
<p><code>InvalidRequestException</code>은 <code>GlobalExceptionHandler</code>에서 <code>400 BAD_REQUEST</code>로 처리되고 있었다.</p>
<p>즉 실제 응답은 <code>400 BAD_REQUEST</code>인데, 테스트는 <code>200 OK</code>를 기대하고 있었기 때문에 실패한 것이다.</p>
<h3 id="해결-3">해결</h3>
<p>테스트 기대값을 실제 예외 처리 결과에 맞게 수정했다.</p>
<pre><code class="language-java">.andExpect(status().isBadRequest())
.andExpect(jsonPath(&quot;$.status&quot;).value(HttpStatus.BAD_REQUEST.name()))
.andExpect(jsonPath(&quot;$.code&quot;).value(HttpStatus.BAD_REQUEST.value()))
.andExpect(jsonPath(&quot;$.message&quot;).value(&quot;Todo not found&quot;));</code></pre>
<p>수정 후 테스트가 정상적으로 통과했다.</p>
<hr>
<h2 id="최종-결과">최종 결과</h2>
<p>위 문제들을 해결한 뒤 전체 테스트를 다시 실행했다.</p>
<pre><code class="language-bash">./gradlew clean test</code></pre>
<p>결과적으로 테스트가 정상적으로 통과했다.</p>
<pre><code class="language-text">BUILD SUCCESSFUL</code></pre>
<hr>
<h2 id="정리">정리</h2>
<p>이번 트러블슈팅을 통해 프로젝트를 시작할 때 기능 구현만큼이나 실행 환경을 정확히 맞추는 것이 중요하다는 것을 느꼈다.</p>
<p>특히 다음 내용을 다시 확인할 수 있었다.</p>
<ul>
<li>Gradle Wrapper는 <code>gradle-wrapper.properties</code> 파일이 필요하다.</li>
<li>프로젝트 Java 버전과 터미널/IDE Java 버전이 일치해야 한다.</li>
<li>테스트 환경에서도 필요한 설정값은 별도로 제공해야 한다.</li>
<li>JWT secret key처럼 내부에서 Base64 디코딩하는 값은 형식까지 맞춰야 한다.</li>
<li>테스트는 실제 예외 처리 정책과 기대값이 일치해야 한다.</li>
</ul>
<p>이번 과제에서는 초반 설정 문제를 해결한 뒤, 이후 필수 기능들을 하나씩 구현하며 전체 테스트를 통과시킬 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# 🛠️ [트러블슈팅] 전액 환불 시 `ORDER_004` 오류가 발생한 이유]]></title>
            <link>https://velog.io/@loaded_diaper/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EC%A0%84%EC%95%A1-%ED%99%98%EB%B6%88-%EC%8B%9C-ORDER004-%EC%98%A4%EB%A5%98%EA%B0%80-%EB%B0%9C%EC%83%9D%ED%95%9C-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@loaded_diaper/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EC%A0%84%EC%95%A1-%ED%99%98%EB%B6%88-%EC%8B%9C-ORDER004-%EC%98%A4%EB%A5%98%EA%B0%80-%EB%B0%9C%EC%83%9D%ED%95%9C-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Tue, 09 Jun 2026 07:21:08 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/8af95136-47e4-43a8-98f2-536be4d649ad/image.png" alt=""></p>
<h2 id="✅-문제-상황">✅ 문제 상황</h2>
<p>커머스 결제 시스템 프로젝트에서 <strong>주문 환불 API</strong>를 테스트하던 중, 요청 Body 형식은 올바르게 작성했는데 계속 아래와 같은 오류가 발생했다.</p>
<pre><code class="language-json">{
  &quot;code&quot;: &quot;ORDER_004&quot;,
  &quot;message&quot;: &quot;유효하지 않은 주문 상태입니다.&quot;
}</code></pre>
<p>Postman 요청은 다음과 같았다.</p>
<pre><code class="language-http">POST /api/orders/8/refunds</code></pre>
<pre><code class="language-json">{
  &quot;items&quot;: [
    {
      &quot;orderItemId&quot;: 8,
      &quot;quantity&quot;: 3
    }
  ],
  &quot;reason&quot;: &quot;단순 변심으로 인한 환불&quot;
}</code></pre>
<p>테스트 데이터도 정상적으로 준비되어 있었다.</p>
<pre><code class="language-text">orders.status = COMPLETED
payment.status = COMPLETED
order_item.quantity = 3</code></pre>
<p>즉, 이미 결제가 완료된 주문이었고, 전액 환불을 진행할 수 있는 상태라고 생각했다.</p>
<p>그런데 계속 <code>ORDER_004</code>가 발생했다.</p>
<hr>
<h2 id="🔍-원인-분석">🔍 원인 분석</h2>
<p>처음에는 Request Body 형식이 잘못된 줄 알았다.</p>
<p>하지만 Body 구조는 DTO와 일치했다.</p>
<pre><code class="language-json">{
  &quot;items&quot;: [
    {
      &quot;orderItemId&quot;: 8,
      &quot;quantity&quot;: 3
    }
  ],
  &quot;reason&quot;: &quot;단순 변심으로 인한 환불&quot;
}</code></pre>
<p>그래서 오류 코드를 기준으로 검색해보았다.</p>
<pre><code class="language-bash">grep -R &quot;INVALID_ORDER_STATUS&quot; -n src/main/java</code></pre>
<p>검색 결과, <code>Order.java</code> 안에서 <code>INVALID_ORDER_STATUS</code>가 발생하고 있었다.</p>
<pre><code class="language-text">src/main/java/com/paymentsystemproject/domain/order/entity/Order.java:92</code></pre>
<p>문제는 기존 <code>Order.cancel()</code> 메서드였다.</p>
<pre><code class="language-java">public void cancel() {
    if (this.status != OrderStatus.PENDING_PAYMENT) {
        throw new BusinessException(ErrorCode.INVALID_ORDER_STATUS);
    }

    this.status = OrderStatus.CANCELLED;
}</code></pre>
<p>기존 <code>cancel()</code> 메서드는 <strong>결제 대기 상태 주문 취소</strong>를 위한 메서드였다.</p>
<p>즉, 아래 흐름만 허용하고 있었다.</p>
<pre><code class="language-text">PENDING_PAYMENT → CANCELLED</code></pre>
<p>하지만 전액 환불은 이미 결제가 완료된 주문에서 발생한다.</p>
<p>전액 환불의 상태 전이는 아래와 같아야 한다.</p>
<pre><code class="language-text">COMPLETED → CANCELLED</code></pre>
<p>결국 문제는 이것이었다.</p>
<blockquote>
<p>주문 직접 취소와 전액 환불 취소가 같은 <code>order.cancel()</code> 메서드를 공유하고 있었다.</p>
</blockquote>
<hr>
<h2 id="💥-실제-오류-흐름">💥 실제 오류 흐름</h2>
<p>전액 환불 요청이 들어오면 내부 흐름은 다음과 같았다.</p>
<pre><code class="language-text">환불 요청
→ 전체 환불 여부 판단
→ Payment 상태를 REFUNDED로 변경
→ Order 상태를 CANCELLED로 변경하기 위해 order.cancel() 호출
→ order.cancel()은 PENDING_PAYMENT만 허용
→ 현재 주문 상태는 COMPLETED
→ INVALID_ORDER_STATUS 발생</code></pre>
<p>즉, 전액 환불 로직 자체가 틀렸다기보다
<strong>주문 상태를 변경하는 메서드의 책임이 너무 넓게 묶여 있었던 것</strong>이 문제였다.</p>
<hr>
<h2 id="✅-해결-방법">✅ 해결 방법</h2>
<p>취소 목적에 따라 메서드를 분리했다.</p>
<p>주문 취소는 크게 두 가지 상황이 있다.</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>시작 상태</th>
<th>변경 상태</th>
</tr>
</thead>
<tbody><tr>
<td>사용자가 결제 전 주문 취소</td>
<td><code>PENDING_PAYMENT</code></td>
<td><code>CANCELLED</code></td>
</tr>
<tr>
<td>결제 완료 주문을 전액 환불</td>
<td><code>COMPLETED</code></td>
<td><code>CANCELLED</code></td>
</tr>
</tbody></table>
<p>둘 다 결과는 <code>CANCELLED</code>지만, <strong>출발 상태가 다르기 때문에 하나의 메서드로 처리하면 안 된다.</strong></p>
<p>그래서 <code>Order</code> 엔티티에 메서드를 분리했다.</p>
<hr>
<h2 id="🔧-수정-코드">🔧 수정 코드</h2>
<h3 id="기존-코드">기존 코드</h3>
<pre><code class="language-java">public void cancel() {
    if (this.status != OrderStatus.PENDING_PAYMENT) {
        throw new BusinessException(ErrorCode.INVALID_ORDER_STATUS);
    }

    this.status = OrderStatus.CANCELLED;
}</code></pre>
<hr>
<h3 id="수정-후-코드">수정 후 코드</h3>
<pre><code class="language-java">/**
 * 결제 대기 상태의 주문을 회원이 직접 취소합니다.
 */
public void cancelPendingOrder() {
    if (this.status != OrderStatus.PENDING_PAYMENT) {
        throw new BusinessException(ErrorCode.INVALID_ORDER_STATUS);
    }

    this.status = OrderStatus.CANCELLED;
}

/**
 * 전액 환불로 인해 주문을 취소 상태로 변경합니다.
 */
public void cancelByFullRefund() {
    if (this.status != OrderStatus.COMPLETED) {
        throw new BusinessException(ErrorCode.INVALID_ORDER_STATUS);
    }

    this.status = OrderStatus.CANCELLED;
}</code></pre>
<hr>
<h2 id="🔁-호출부-수정">🔁 호출부 수정</h2>
<h3 id="주문-직접-취소-api">주문 직접 취소 API</h3>
<p>기존에는 주문 취소에서도 <code>order.cancel()</code>을 사용하고 있었다.</p>
<pre><code class="language-java">order.cancel();</code></pre>
<p>이를 주문 직접 취소 전용 메서드로 변경했다.</p>
<pre><code class="language-java">order.cancelPendingOrder();</code></pre>
<hr>
<h3 id="전액-환불-처리">전액 환불 처리</h3>
<p>전액 환불 시에는 아래 메서드를 사용하도록 수정했다.</p>
<pre><code class="language-java">order.cancelByFullRefund();</code></pre>
<p>최종적으로 전액 환불 흐름은 다음과 같이 정리되었다.</p>
<pre><code class="language-text">RefundService
→ 전체 환불 판단
→ Payment 상태를 REFUNDED로 변경
→ OrderService.cancelByFullRefund()
→ order.cancelByFullRefund()
→ COMPLETED 주문을 CANCELLED로 변경</code></pre>
<hr>
<h2 id="✅-테스트-결과">✅ 테스트 결과</h2>
<p>수정 후 다시 Postman으로 전액 환불 요청을 보냈다.</p>
<pre><code class="language-http">POST /api/orders/8/refunds</code></pre>
<pre><code class="language-json">{
  &quot;items&quot;: [
    {
      &quot;orderItemId&quot;: 8,
      &quot;quantity&quot;: 3
    }
  ],
  &quot;reason&quot;: &quot;단순 변심으로 인한 환불&quot;
}</code></pre>
<p>이후 DB를 확인했다.</p>
<pre><code class="language-sql">SELECT id, status
FROM orders
WHERE id = 8;

SELECT id, status
FROM payment
WHERE order_id = 8;</code></pre>
<p>결과는 정상적으로 변경되었다.</p>
<pre><code class="language-text">orders.status = CANCELLED
payment.status = REFUNDED</code></pre>
<p>상품 재고도 환불 수량만큼 정상 복구되었다.</p>
<p>또한 빌드도 성공했다.</p>
<pre><code class="language-bash">sh ./gradlew clean compileJava</code></pre>
<hr>
<h2 id="📌-배운-점">📌 배운 점</h2>
<p>이번 문제를 통해 <strong>같은 결과 상태를 만들더라도, 상태 전이의 출발점이 다르면 메서드를 분리해야 한다</strong>는 점을 배웠다.</p>
<p>처음에는 단순히 주문을 취소하는 행위라고 생각해서 <code>cancel()</code> 하나로 처리했다.</p>
<p>하지만 실제 도메인에서는 다음 두 흐름이 전혀 다르다.</p>
<pre><code class="language-text">결제 전 주문 취소
PENDING_PAYMENT → CANCELLED</code></pre>
<pre><code class="language-text">결제 완료 후 전액 환불
COMPLETED → CANCELLED</code></pre>
<p>둘 다 최종 상태는 <code>CANCELLED</code>지만, 의미와 검증 조건이 다르다.</p>
<p>그래서 도메인 메서드는 단순히 “결과 상태”만 기준으로 만들면 안 되고,
<strong>어떤 비즈니스 상황에서 호출되는 메서드인지 이름과 검증 조건에 드러나야 한다.</strong></p>
<hr>
<h2 id="✅-정리">✅ 정리</h2>
<p>이번 오류의 핵심은 Request Body 문제가 아니었다.</p>
<p>문제의 원인은 다음과 같았다.</p>
<blockquote>
<p>주문 직접 취소와 전액 환불 취소가 같은 <code>order.cancel()</code> 메서드를 공유하면서,
<code>COMPLETED</code> 주문의 전액 환불까지 <code>PENDING_PAYMENT</code> 검증에 걸려 실패한 문제</p>
</blockquote>
<p>해결은 다음과 같이 진행했다.</p>
<pre><code class="language-text">order.cancel()
→ cancelPendingOrder()
→ cancelByFullRefund()</code></pre>
<p>이를 통해 주문 직접 취소와 전액 환불의 상태 전이 책임을 명확히 분리할 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [Spring Boot + Docker MySQL] Access denied for user 'root'@'localhost' 해결 — 원인은 비밀번호가 아니라 포트 충돌이었다]]></title>
            <link>https://velog.io/@loaded_diaper/Spring-Boot-Docker-MySQL-Access-denied-for-user-rootlocalhost-%ED%95%B4%EA%B2%B0-%EC%9B%90%EC%9D%B8%EC%9D%80-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8%EA%B0%80-%EC%95%84%EB%8B%88%EB%9D%BC-%ED%8F%AC%ED%8A%B8-%EC%B6%A9%EB%8F%8C%EC%9D%B4%EC%97%88%EB%8B%A4</link>
            <guid>https://velog.io/@loaded_diaper/Spring-Boot-Docker-MySQL-Access-denied-for-user-rootlocalhost-%ED%95%B4%EA%B2%B0-%EC%9B%90%EC%9D%B8%EC%9D%80-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8%EA%B0%80-%EC%95%84%EB%8B%88%EB%9D%BC-%ED%8F%AC%ED%8A%B8-%EC%B6%A9%EB%8F%8C%EC%9D%B4%EC%97%88%EB%8B%A4</guid>
            <pubDate>Wed, 27 May 2026 11:35:11 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/803d7388-986a-4ba1-b708-ea426daba02c/image.png" alt="">
지피티랑 1시간 넘게 싸웠는데 알고 보니 포트 번호 문제;;</p>
<hr>
<h2 id="문제-상황">문제 상황</h2>
<p>Spring Boot 프로젝트에서 Docker로 MySQL을 띄운 뒤 애플리케이션을 실행하려고 했다.</p>
<p>처음 설정은 다음과 같았다.</p>
<h3 id="docker-composeyml">docker-compose.yml</h3>
<pre><code class="language-yml">services:
  mysql:
    image: mysql:8
    container_name: sparta-payment-mysql
    environment:
      MYSQL_ROOT_PASSWORD: root1234
      MYSQL_DATABASE: payment_db
    ports:
      - &quot;3306:3306&quot;
    volumes:
      - mysql_data:/var/lib/mysql
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

volumes:
  mysql_data:</code></pre>
<h3 id="applicationyaml">application.yaml</h3>
<pre><code class="language-yml">spring:
  datasource:
    url: jdbc:mysql://localhost:3306/payment_db?useSSL=false&amp;allowPublicKeyRetrieval=true&amp;serverTimezone=Asia/Seoul
    username: root
    password: root1234
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: create
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.MySQLDialect</code></pre>
<p>하지만 Spring Boot를 실행하면 계속 아래 에러가 발생했다.</p>
<pre><code class="language-text">Access denied for user &#39;root&#39;@&#39;localhost&#39; (using password: YES)</code></pre>
<p>최종적으로는 JPA가 DB 커넥션을 얻지 못해 <code>entityManagerFactory</code> 생성에 실패했고, 애플리케이션 실행이 중단됐다.</p>
<pre><code class="language-text">Failed to initialize JPA EntityManagerFactory
Unable to open JDBC Connection for DDL execution
Application run failed</code></pre>
<hr>
<h2 id="처음에-의심한-것-root-비밀번호-문제">처음에 의심한 것: root 비밀번호 문제</h2>
<p>에러 메시지만 보면 가장 먼저 의심되는 것은 MySQL 비밀번호였다.</p>
<pre><code class="language-text">Access denied for user &#39;root&#39;@&#39;localhost&#39;</code></pre>
<p>그래서 처음에는 <code>root</code> 계정의 비밀번호가 잘못되었거나, IntelliJ가 예전 비밀번호를 저장하고 있다고 생각했다.</p>
<p>내가 확인했던 비밀번호 후보는 다음과 같았다.</p>
<pre><code class="language-text">root1234
dlatjsrn1!</code></pre>
<p>먼저 Docker 컨테이너 안에서 root 계정으로 접속해봤다.</p>
<pre><code class="language-bash">docker exec -it sparta-payment-mysql mysql -uroot -p</code></pre>
<p>비밀번호로 <code>root1234</code>를 입력하니 정상 접속됐다.</p>
<pre><code class="language-text">Welcome to the MySQL monitor.</code></pre>
<p>그리고 DB도 정상적으로 선택됐다.</p>
<pre><code class="language-sql">USE payment_db;
SHOW TABLES;</code></pre>
<p>결과는 다음과 같았다.</p>
<pre><code class="language-text">Database changed
Empty set</code></pre>
<p>즉, Docker 컨테이너 내부의 MySQL에서는 <code>root / root1234</code>가 정상적으로 동작했다.</p>
<hr>
<h2 id="root-비밀번호-직접-확인">root 비밀번호 직접 확인</h2>
<p>그래도 확실히 하기 위해 명령어로 비밀번호를 직접 검증했다.</p>
<pre><code class="language-bash">docker exec -i sparta-payment-mysql mysql -uroot -proot1234 -e &quot;SELECT &#39;root1234 OK&#39;;&quot;</code></pre>
<p>결과는 성공이었다.</p>
<pre><code class="language-text">root1234 OK
root1234 OK</code></pre>
<p>반대로 예전에 다른 프로젝트에서 쓰던 비밀번호도 확인했다.</p>
<pre><code class="language-bash">docker exec -i sparta-payment-mysql mysql -uroot -p&#39;dlatjsrn1!&#39; -e &quot;SELECT &#39;old password OK&#39;;&quot;</code></pre>
<p>결과는 실패였다.</p>
<pre><code class="language-text">ERROR 1045 (28000): Access denied for user &#39;root&#39;@&#39;localhost&#39; (using password: YES)</code></pre>
<p>따라서 현재 Docker MySQL의 root 비밀번호는 <code>root1234</code>가 맞았다.</p>
<hr>
<h2 id="헷갈렸던-부분-터미널에서는-되는데-spring-boot는-안-됨">헷갈렸던 부분: 터미널에서는 되는데 Spring Boot는 안 됨</h2>
<p>이상했던 점은 터미널에서는 접속이 되는데 Spring Boot에서는 계속 실패한다는 점이었다.</p>
<p>컨테이너 내부에서는 아래 명령어가 정상 동작했다.</p>
<pre><code class="language-bash">docker exec -it sparta-payment-mysql mysql -uroot -p</code></pre>
<p>하지만 Spring Boot 실행 로그에서는 계속 아래 에러가 발생했다.</p>
<pre><code class="language-text">Access denied for user &#39;root&#39;@&#39;localhost&#39; (using password: YES)</code></pre>
<p>처음에는 이 차이를 이해하지 못해서 다음과 같은 것들을 의심했다.</p>
<pre><code class="language-text">1. application.yaml 비밀번호 오타
2. IntelliJ Database 탭의 비밀번호 캐시 문제
3. MySQL root 계정 권한 문제
4. Docker 볼륨에 예전 비밀번호가 남아있는 문제</code></pre>
<p>그래서 <code>payment_user</code>라는 새 계정도 만들어보고, 권한도 부여해봤다.</p>
<pre><code class="language-sql">CREATE USER &#39;payment_user&#39;@&#39;%&#39; IDENTIFIED BY &#39;payment1234&#39;;
GRANT ALL PRIVILEGES ON payment_db.* TO &#39;payment_user&#39;@&#39;%&#39;;
FLUSH PRIVILEGES;</code></pre>
<p>하지만 이것도 근본 해결이 아니었다.</p>
<p>나중에 확인해보니 문제는 계정이 아니었다.</p>
<hr>
<h2 id="intellij-database-연결도-실패">IntelliJ Database 연결도 실패</h2>
<p>IntelliJ 오른쪽 Database 탭에서도 MySQL 연결을 시도했다.</p>
<p>처음에는 다음과 같이 설정했다.</p>
<pre><code class="language-text">Host: localhost
Port: 3306
User: root
Password: root1234
Database: payment_db</code></pre>
<p>하지만 IntelliJ Database에서도 계속 연결이 실패했다.</p>
<pre><code class="language-text">Access denied for user &#39;root&#39;@&#39;localhost&#39; (using password: YES)</code></pre>
<p>이때까지만 해도 계속 비밀번호나 계정 문제라고 생각했다.</p>
<p>하지만 Docker 컨테이너 안에서는 <code>root / root1234</code>가 정상 접속되기 때문에 앞뒤가 맞지 않았다.</p>
<hr>
<h2 id="applicationyaml-확인">application.yaml 확인</h2>
<p>설정 파일이 실제로 반영됐는지도 확인했다.</p>
<p>처음에는 파일명을 <code>application.yml</code>로 착각해서 확인했다.</p>
<pre><code class="language-bash">cat src/main/resources/application.yml | grep &quot;jdbc:mysql&quot;</code></pre>
<p>하지만 실제 파일명은 <code>application.yaml</code>이었다.</p>
<pre><code class="language-bash">find . -name &quot;application.yml&quot; -o -name &quot;application.yaml&quot; -o -name &quot;application.properties&quot;</code></pre>
<p>결과는 다음과 같았다.</p>
<pre><code class="language-text">./build/resources/main/application.yaml
./src/main/resources/application.yaml</code></pre>
<p>그래서 실제 파일을 다시 확인했다.</p>
<pre><code class="language-bash">cat src/main/resources/application.yaml | grep &quot;jdbc:mysql&quot;
cat build/resources/main/application.yaml | grep &quot;jdbc:mysql&quot;</code></pre>
<p>둘 다 정상적으로 같은 설정을 가지고 있었다.</p>
<pre><code class="language-text">url: jdbc:mysql://127.0.0.1:3306/payment_db?useSSL=false&amp;allowPublicKeyRetrieval=true&amp;serverTimezone=Asia/Seoul</code></pre>
<p>즉, Spring Boot 설정 파일 문제도 아니었다.</p>
<hr>
<h2 id="root-계정-host-권한-확인">root 계정 host 권한 확인</h2>
<p>혹시 root 계정이 외부 접속을 허용하지 않는 문제일 수도 있어서 MySQL 계정의 host 값을 확인했다.</p>
<pre><code class="language-bash">docker exec -i sparta-payment-mysql mysql -uroot -proot1234 -e &quot;SELECT user, host FROM mysql.user WHERE user = &#39;root&#39;;&quot;</code></pre>
<p>결과는 다음과 같았다.</p>
<pre><code class="language-text">user    host
root    %
root    localhost</code></pre>
<p><code>root@%</code>가 있다는 것은 외부 접속도 허용된다는 뜻이다.</p>
<p>따라서 root 계정 권한 문제도 아니었다.</p>
<hr>
<h2 id="진짜-원인-확인-3306-포트-충돌">진짜 원인 확인: 3306 포트 충돌</h2>
<p>결국 3306 포트를 누가 사용하고 있는지 확인했다.</p>
<pre><code class="language-bash">lsof -iTCP:3306 -sTCP:LISTEN -n -P</code></pre>
<p>결과는 다음과 같았다.</p>
<pre><code class="language-text">COMMAND     PID    USER     NAME
mysqld      1529   imsungu  TCP 127.0.0.1:3306 (LISTEN)
com.docke   52511  imsungu  TCP *:3306 (LISTEN)</code></pre>
<p>여기서 핵심은 이 부분이었다.</p>
<pre><code class="language-text">mysqld ... TCP 127.0.0.1:3306 (LISTEN)</code></pre>
<p>내 Mac에 예전에 설치되어 있던 로컬 MySQL이 이미 <code>127.0.0.1:3306</code>을 사용하고 있었다.</p>
<p>즉, Spring Boot가 아래 주소로 접속할 때,</p>
<pre><code class="language-yml">jdbc:mysql://127.0.0.1:3306/payment_db</code></pre>
<p>내가 의도한 Docker MySQL이 아니라 Mac 로컬 MySQL로 접속하고 있었던 것이다.</p>
<p>그래서 Docker MySQL의 비밀번호인 <code>root1234</code>를 넣어도 Mac 로컬 MySQL에서는 맞지 않기 때문에 계속 로그인 실패가 발생했다.</p>
<pre><code class="language-text">Access denied for user &#39;root&#39;@&#39;localhost&#39;</code></pre>
<hr>
<h2 id="원인-정리">원인 정리</h2>
<p>문제의 흐름은 다음과 같았다.</p>
<pre><code class="language-text">1. Mac 로컬 MySQL이 127.0.0.1:3306 사용 중
2. Docker MySQL도 3306으로 열려고 함
3. Spring Boot는 127.0.0.1:3306으로 접속
4. 실제로는 Docker MySQL이 아니라 Mac 로컬 MySQL에 접속
5. Docker용 비밀번호 root1234를 넣음
6. Mac 로컬 MySQL 입장에서는 틀린 비밀번호
7. Access denied 발생</code></pre>
<p>즉, 원인은 비밀번호가 아니라 포트 충돌이었다.</p>
<hr>
<h2 id="해결-방법">해결 방법</h2>
<p>Docker MySQL의 외부 포트를 <code>3306</code>에서 <code>3307</code>로 변경했다.</p>
<h3 id="docker-composeyml-수정-전">docker-compose.yml 수정 전</h3>
<pre><code class="language-yml">ports:
  - &quot;3306:3306&quot;</code></pre>
<h3 id="docker-composeyml-수정-후">docker-compose.yml 수정 후</h3>
<pre><code class="language-yml">ports:
  - &quot;3307:3306&quot;</code></pre>
<p>최종 <code>docker-compose.yml</code>은 다음과 같다.</p>
<pre><code class="language-yml">services:
  mysql:
    image: mysql:8
    container_name: sparta-payment-mysql
    environment:
      MYSQL_ROOT_PASSWORD: root1234
      MYSQL_DATABASE: payment_db
      MYSQL_ROOT_HOST: &#39;%&#39;
    ports:
      - &quot;3307:3306&quot;
    volumes:
      - mysql_data:/var/lib/mysql
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

volumes:
  mysql_data:</code></pre>
<p>그리고 Spring Boot의 <code>application.yaml</code>도 <code>3307</code>로 변경했다.</p>
<pre><code class="language-yml">spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3307/payment_db?useSSL=false&amp;allowPublicKeyRetrieval=true&amp;serverTimezone=Asia/Seoul
    username: root
    password: root1234
    driver-class-name: com.mysql.cj.jdbc.Driver</code></pre>
<hr>
<h2 id="docker-재실행">Docker 재실행</h2>
<p>설정 변경 후 Docker 컨테이너를 재실행했다.</p>
<pre><code class="language-bash">docker compose down
docker compose up -d</code></pre>
<p>이미 데이터 초기화가 필요한 상황은 아니었기 때문에 <code>-v</code>는 붙이지 않았다.</p>
<pre><code class="language-bash">docker compose down -v</code></pre>
<p><code>-v</code>를 붙이면 볼륨까지 삭제되어 DB 데이터가 초기화된다.</p>
<hr>
<h2 id="intellij-database-연결도-다시-설정">IntelliJ Database 연결도 다시 설정</h2>
<p>IntelliJ Database 탭에서도 기존 <code>localhost:3306</code> 연결은 삭제하고 새로 만들었다.</p>
<p>새 연결 정보는 다음과 같다.</p>
<pre><code class="language-text">Host: 127.0.0.1
Port: 3307
User: root
Password: root1234
Database: payment_db</code></pre>
<p>URL은 다음과 같이 잡혔다.</p>
<pre><code class="language-text">jdbc:mysql://127.0.0.1:3307/payment_db</code></pre>
<p>그 결과 Test Connection이 성공했다.</p>
<pre><code class="language-text">Succeeded
DBMS: MySQL 8.4.9</code></pre>
<p>드디어 IntelliJ Database 탭에서도 <code>payment_db</code>가 정상적으로 보이기 시작했다.</p>
<hr>
<h2 id="최종-정리">최종 정리</h2>
<p>이번 문제는 단순한 비밀번호 문제가 아니었다.</p>
<p>처음에는 root 비밀번호, MySQL 계정 권한, IntelliJ 비밀번호 저장 문제라고 생각해서 한참을 헤맸다.</p>
<p>하지만 실제 원인은 Mac 로컬 MySQL과 Docker MySQL이 같은 <code>3306</code> 포트를 사용하면서 발생한 포트 충돌이었다.</p>
<pre><code class="language-text">Mac 로컬 MySQL  → 127.0.0.1:3306 사용 중
Docker MySQL    → 원래 3306으로 열려고 함
Spring Boot     → 127.0.0.1:3306으로 접속
결과             → Docker가 아니라 Mac 로컬 MySQL로 접속</code></pre>
<p>해결 후 구조는 다음과 같다.</p>
<pre><code class="language-text">Mac 로컬 MySQL       → 127.0.0.1:3306
Docker MySQL 외부접속 → 127.0.0.1:3307
Docker 내부 MySQL    → 3306</code></pre>
<p>즉, 내 Mac에서 Docker MySQL에 접속할 때는 <code>3307</code>을 사용해야 하고, Docker 컨테이너 내부에서는 여전히 <code>3306</code>을 사용한다.</p>
<hr>
<h2 id="배운-점">배운 점</h2>
<p><code>docker exec</code>로 MySQL 접속이 성공한다고 해서 Spring Boot도 같은 MySQL에 접속하고 있다고 단정하면 안 된다.</p>
<pre><code class="language-bash">docker exec -it sparta-payment-mysql mysql -uroot -p</code></pre>
<p>이 명령어는 컨테이너 내부에서 MySQL에 접속하는 것이다.</p>
<p>반면 Spring Boot는 Mac에서 Docker MySQL에 접속한다.</p>
<p>그래서 로컬 PC에 이미 MySQL이 설치되어 있고 <code>3306</code> 포트를 사용 중이라면, Spring Boot가 Docker MySQL이 아니라 로컬 MySQL로 접속할 수 있다.</p>
<p>포트 충돌이 의심될 때는 아래 명령어로 확인하자.</p>
<pre><code class="language-bash">lsof -iTCP:3306 -sTCP:LISTEN -n -P</code></pre>
<hr>
<h2 id="한-줄-요약">한 줄 요약</h2>
<blockquote>
<p>Docker MySQL 비밀번호 문제가 아니라, Mac 로컬 MySQL이 3306 포트를 이미 사용하고 있어서 Spring Boot와 IntelliJ Database가 Docker가 아닌 로컬 MySQL에 접속하던 문제였다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[CH4. 클라우드 아키택처 설계 & 배포 트러블 슈팅]]></title>
            <link>https://velog.io/@loaded_diaper/CH4.-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EC%95%84%ED%82%A4%ED%83%9D%EC%B2%98-%EC%84%A4%EA%B3%84-%EB%B0%B0%ED%8F%AC-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@loaded_diaper/CH4.-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EC%95%84%ED%82%A4%ED%83%9D%EC%B2%98-%EC%84%A4%EA%B3%84-%EB%B0%B0%ED%8F%AC-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</guid>
            <pubDate>Tue, 26 May 2026 01:16:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/c69bf062-3649-4326-bf89-8fad8b15989d/image.png" alt=""></p>
<h1 id="aws-rds-mysql-접속-중-unknown-database-3306-오류-해결">[AWS RDS] MySQL 접속 중 Unknown database &#39;3306&#39; 오류 해결</h1>
<h2 id="문제-상황">문제 상황</h2>
<p>AWS RDS에 MySQL 데이터베이스를 생성한 뒤, EC2에서 RDS에 접속 테스트를 진행했다.</p>
<p>RDS Endpoint는 정상적으로 복사했고, EC2에서 MySQL 클라이언트도 설치한 상태였다.</p>
<p>처음에는 아래 명령어로 접속을 시도했다.</p>
<pre><code class="language-bash">mysql -h team-profile-rds.c50wseaoq4uw.ap-northeast-2.rds.amazonaws.com -p 3306 -u admin -p</code></pre>
<p>그런데 다음과 같은 에러가 발생했다.</p>
<pre><code class="language-bash">ERROR 1049 (42000): Unknown database &#39;3306&#39;</code></pre>
<p>처음에는 RDS 보안 그룹 문제인지, 데이터베이스가 제대로 생성되지 않은 문제인지 헷갈렸다.</p>
<hr>
<h2 id="원인-분석">원인 분석</h2>
<p>에러 메시지를 보면 다음과 같이 나온다.</p>
<pre><code class="language-bash">Unknown database &#39;3306&#39;</code></pre>
<p>즉, MySQL은 <code>3306</code>을 포트 번호가 아니라 <strong>데이터베이스 이름</strong>으로 해석하고 있었다.</p>
<p>문제는 접속 명령어에서 사용한 옵션이었다.</p>
<p>내가 작성한 명령어는 다음과 같았다.</p>
<pre><code class="language-bash">mysql -h RDS_ENDPOINT -p 3306 -u admin -p</code></pre>
<p>여기서 실수한 부분은 이 옵션이다.</p>
<pre><code class="language-bash">-p 3306</code></pre>
<p>MySQL 명령어에서:</p>
<pre><code class="language-text">-p  → password 옵션
-P  → port 옵션</code></pre>
<p>즉, 포트 번호를 지정하려면 소문자 <code>-p</code>가 아니라 <strong>대문자 <code>-P</code></strong> 를 사용해야 한다.</p>
<p>나는 포트 번호 <code>3306</code>을 지정하려고 했지만, 소문자 <code>-p</code>를 사용해서 MySQL이 명령어를 잘못 해석한 것이다.</p>
<hr>
<h2 id="해결-방법">해결 방법</h2>
<p>포트 번호 옵션을 대문자 <code>-P</code>로 수정했다.</p>
<pre><code class="language-bash">mysql -h team-profile-rds.c50wseaoq4uw.ap-northeast-2.rds.amazonaws.com -P 3306 -u admin -p</code></pre>
<p>수정 후 비밀번호를 입력하자 정상적으로 접속되었다.</p>
<pre><code class="language-bash">Welcome to the MariaDB monitor.
Commands end with ; or \g.

MySQL [(none)]&gt;</code></pre>
<p>그다음 데이터베이스 목록을 확인했다.</p>
<pre><code class="language-sql">SHOW DATABASES;</code></pre>
<p>결과는 다음과 같았다.</p>
<pre><code class="language-text">+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| teamprofiledb      |
+--------------------+</code></pre>
<p><code>teamprofiledb</code>가 정상적으로 보이는 것을 확인했다.</p>
<hr>
<h2 id="최종-정리">최종 정리</h2>
<h3 id="잘못된-명령어">잘못된 명령어</h3>
<pre><code class="language-bash">mysql -h RDS_ENDPOINT -p 3306 -u admin -p</code></pre>
<h3 id="올바른-명령어">올바른 명령어</h3>
<pre><code class="language-bash">mysql -h RDS_ENDPOINT -P 3306 -u admin -p</code></pre>
<hr>
<h2 id="배운-점">배운 점</h2>
<p>이번 문제는 RDS 설정이나 보안 그룹 문제가 아니라, MySQL 접속 명령어 옵션을 잘못 사용해서 발생한 문제였다.</p>
<p>특히 MySQL CLI에서는 소문자와 대문자 옵션이 다른 의미를 가진다.</p>
<pre><code class="language-text">-p : 비밀번호 입력 옵션
-P : 포트 번호 지정 옵션</code></pre>
<p>앞으로 MySQL에 접속할 때는 포트 번호를 지정할 경우 반드시 대문자 <code>-P</code>를 사용해야 한다.</p>
<p>또한 에러 메시지에서 <code>Unknown database &#39;3306&#39;</code>이라고 나왔기 때문에, MySQL이 <code>3306</code>을 포트가 아닌 데이터베이스 이름으로 해석했다는 점을 통해 원인을 추적할 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 정리 3]]></title>
            <link>https://velog.io/@loaded_diaper/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%A0%95%EB%A6%AC-3</link>
            <guid>https://velog.io/@loaded_diaper/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%A0%95%EB%A6%AC-3</guid>
            <pubDate>Wed, 20 May 2026 10:33:16 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/8017e677-b9de-4fa9-890b-32350cca6cf2/image.png" alt=""></p>
<h1 id="7-조회전략과-n--1">7. 조회전략과 N + 1</h1>
<h2 id="01-⭐-jpa-조회-전략">01. ⭐ JPA 조회 전략</h2>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/d5b85bab-a612-46b2-8c04-e97feec488e6/image.png" alt=""></p>
<h2 id="a-eager-loading---즉시-조회">a. Eager Loading - 즉시 조회</h2>
<blockquote>
<p>단방향(<code>@MnayToOne</code>)  의 기본 조회 전략입니다. 엔티티를 가져올때 연관된 엔티티도 함께 조회합니다.</p>
</blockquote>
<p>i. 즉시 조회 설정 방법</p>
<pre><code class="language-java">@Entity
@Table(name = &quot;students&quot;)
public class Student {

        ...

    @ManyToOne(fetch = FetchType.EAGER) // ✅ 기본 조회 전략(생략 가능)
    @JoinColumn(name = &quot;course_id&quot;)
    private Course course;

        ...
}</code></pre>
<h3 id="ii-즉시조회-코드로-살펴보기---v1-v2">ii. 즉시조회 코드로 살펴보기 - V1, V2</h3>
<blockquote>
<p>학생 정보만  조회하더라도 연관된 수업의 정보를 함께 조회하는 모습을 확인할 수 있습니다.</p>
</blockquote>
<p>b. Lazy Loading -  지연 조회</p>
<blockquote>
<p>연관관계에 있는 엔티티의 정보가 필요할 때만 조회합니다. 양방향(@OneToMany, @ManyToMany)의 기본 조회 전략입니다.</p>
</blockquote>
<p>i. 지연 조회 설정 방법</p>
<pre><code class="language-java">@Entity
@Table(name = &quot;students&quot;)
public class Student {

        ...

    @ManyToOne(fetch = FetchType.LAZY) // ✅ 지연 조회 설정
    @JoinColumn(name = &quot;course_id&quot;)
    private Course course;

    ...
}</code></pre>
<h2 id="02-논의-포인트---지연-조회-전략">02. 논의 포인트 - 지연 조회 전략</h2>
<blockquote>
<p>Lazy 전략을 활용해 조회를 한다면 연관된 속성 안에는 무엇이 들어가 있을까요?</p>
</blockquote>
<h2 id="03-⭐-프록시-객체">03. ⭐ 프록시 객체</h2>
<blockquote>
<p>proxy == 대리인</p>
</blockquote>
<h3 id="a-정체와-구조">a. 정체와 구조</h3>
<blockquote>
<p>실제 객체 대신 먼저 앞에서 요청을 받아주는 객체입니다. 프록시 덕분에 지연로딩이 가능해집니다. 프록시 객체 덕분에 지연로딩이 가능해집니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/eed81ebc-0207-497f-bad9-a5def2d38a99/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/132445af-ce37-4533-9750-fac4c46c437e/image.png" alt="">
Eager Loading 일 경우</p>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/64cfdcf0-0feb-4a59-b70b-6f565f064579/image.png" alt="">
Lazy Loading 일 경우</p>
<h3 id="b-동작-원리">b. 동작 원리</h3>
<blockquote>
<p>JPA 는 지연로딩을 가능하게 만들기 위해 연관관계 조회시점에 진짜 객체(엔티티) 대신 프록시 객체를 주입해줍니다. 이 프록시는 식별자(id)만 가진 껍데기 객체입니다. 이후 실제 필드에 접근하는 순간 DB 쿼리가 실행되어서 진짜 데이터가 조회됩니다.</p>
</blockquote>
<h2 id="03-⭐-지연-로딩의-한계-→--n--1">03. ⭐ 지연 로딩의 한계 →  N + 1</h2>
<blockquote>
<p>ORM 을 활용하면서 데이터를 객체처럼 쉽게 다룰 수 있게 되었습니다! 하지만 조회전략을 잘 이해하지 못하고 활용한다면 연관된 데이터를 가져오기 위해 n 개의 추가 쿼리가 발생하는 성능 문제가 발생할  수 있습니다.</p>
</blockquote>
<p>a. 퀴즈: 아래 구조에서 쿼리가 몇번 발생할까요?
<img src="https://velog.velcdn.com/images/loaded_diaper/post/a9d4cbab-9d96-4823-a9b6-4cfd88a7c017/image.png" alt="">
4번의 쿼리 발생</p>
<p>b.  흔히 발생하는 n + 1 문제 유형 살펴보기</p>
<h3 id="i-반복문-내부">i. 반복문 내부</h3>
<blockquote>
<p>반복문 내부에서 연관된 엔티티 조회시</p>
</blockquote>
<pre><code class="language-java">  public List&lt;StudentDetailResponseDto&gt; getStudentListV1() {
        // 1. 학생 목록 조회
        List&lt;Student&gt; foundStudentList = studentRepository.findAll();

        // 2. Dto 만들기
        // 2-1. dto 리스트 준비
        List&lt;StudentDetailResponseDto&gt; studentDetailResponseDtoList = new ArrayList&lt;&gt;();

        for (Student student : foundStudentList) {
            // 2-1. dtoDetail 요소 재료 준비
            Long foundStudentId = student.getId();
            String foundStudentName = student.getName();
            Course foundCourse = student.getCourse();
            String foundCourseName = foundCourse.getName(); // n + 1 문제

            // 2-2. dto 요소 만들기
            StudentDetailResponseDto studentDetailResponseDto = new StudentDetailResponseDto(
                    foundStudentId,
                    foundStudentName,
                    foundCourseName
            );

            // 2-3. dto 리스트에 detailDto 추가
            studentDetailResponseDtoList.add(studentDetailResponseDto);
        }
        return studentDetailResponseDtoList;
    }</code></pre>
<h3 id="ii-stream-내부">ii. stream 내부</h3>
<blockquote>
<p>stream 내부에서 연관된 엔티티 조회시</p>
</blockquote>
<pre><code class="language-java">    public List&lt;StudentDetailResponseDto&gt; getStudentListV2() {
        // 1. 학생 목록 조회
        List&lt;Student&gt; foundStudentList = studentRepository.findAll();

        // 2. Dto 만들기
        // 2-1. dto 리스트 준비
        List&lt;StudentDetailResponseDto&gt; studentDetailResponseDtoList = foundStudentList.stream()
                .map(student -&gt; new StudentDetailResponseDto(
                                student.getId(),
                                student.getName(),
                                student.getCourse()
                                        .getName() // n + 1 문제
                        )
                )
                .collect(Collectors.toList());
        return studentDetailResponseDtoList;
    }</code></pre>
<h3 id="iii-dto-내부">iii. dto 내부</h3>
<blockquote>
<p>dto 내부에서 연관된 엔티티 조회시</p>
</blockquote>
<pre><code class="language-java">    public List&lt;StudentDetailResponseDto&gt; getStudentListV3() {
        // 1. 학생 목록 조회
        List&lt;Student&gt; foundStudentList = studentRepository.findAll();

        // 2. Dto 만들기
        // 2-1. dto 리스트 준비
        List&lt;StudentDetailResponseDto&gt; studentDetailResponseDtoList = foundStudentList.stream()
                .map(student -&gt; new StudentDetailResponseDto(student)
                )
                .collect(Collectors.toList());
        return studentDetailResponseDtoList;
    }</code></pre>
<pre><code class="language-java">class StudentDetailResponseDto {
        ...
        private String courseName;

        public StudentDetailResponseDto(Stduent student) {
                this.student.getCourse().getName(); // n + 1 문제 발생

        }
}</code></pre>
<h3 id="04-⭐-n--1-문제-해결-방법">04. ⭐ n + 1 문제 해결 방법</h3>
<p>a. join fetch</p>
<pre><code class="language-java">    @Query(&quot;SELECT s FROM Student s LEFT JOIN FETCH s.course&quot;)
    List&lt;Student&gt; findAllWithStudent();</code></pre>
<p>b. entityGraph</p>
<h3 id="i-기본-활용-방법">i. 기본 활용 방법</h3>
<blockquote>
<p>Student 엔티티에 연관된 객체들을 어떻게 조회할건지 미리 그래프로 정해놓고 repository 에서 사용하는 방법입니다.</p>
</blockquote>
<p>하나만 정의해놓기(student ↔ course):</p>
<pre><code class="language-java">@Entity
@Table(name = &quot;students&quot;)
@NamedEntityGraph(
        name = &quot;Student.withCourse&quot;,
        attributeNodes = @NamedAttributeNode(&quot;course&quot;)
)
public class Student {
    ...
    private Course course;
}</code></pre>
<p>여러개 정의해놓기(student ↔ course, student ↔ tutor, student:</p>
<pre><code class="language-java">@Entity
@Table(name = &quot;students&quot;)
@NamedEntityGraphs({
    @NamedEntityGraph(
        name = &quot;Student.withCourse&quot;,
        attributeNodes = @NamedAttributeNode(&quot;course&quot;)
    ),
    @NamedEntityGraph(
        name = &quot;Student.withTutor&quot;,
        attributeNodes = @NamedAttributeNode(&quot;tutor&quot;)
    ),
    @NamedEntityGraph(
        name = &quot;Student.withCourseAndTutor&quot;,
        attributeNodes = {
            @NamedAttributeNode(&quot;course&quot;),
            @NamedAttributeNode(&quot;tutor&quot;)
        }
    )
})
public class Student {
    ...
    private Course course;
    private Tutor tutor;
}</code></pre>
<pre><code class="language-java">public interface StudentRepository extends JpaRepository&lt;Student, Long&gt; {

    @EntityGraph(&quot;Student.withCourse&quot;)
        List&lt;Student&gt; findAll();

    @EntityGraph(&quot;Student.withTutor&quot;)
        List&lt;Student&gt; findAll();

    @EntityGraph(&quot;Student.withCourseAndTutor&quot;)
        List&lt;Student&gt; findAll();

        @EntityGraph(&quot;Student.withCourse&quot;)
        List&lt;Student&gt; findByName(String name);
}</code></pre>
<ul>
<li>queryMethod 방식, JPQL 방식 모두 활용 가능</li>
</ul>
<p>ii. 간단한 사용 방법</p>
<pre><code class="language-java">    @EntityGraph(attributePaths = {&quot;course&quot;})
    @Query(&quot;SELECT s FROM Student s&quot;)
    List&lt;Student&gt; findAllWithEntityGraph();

    @EntityGraph(attributePaths = {&quot;course&quot;})
    List&lt;Student&gt; findAll();</code></pre>
<hr>
<h1 id="8-jwt---인증인가">8. JWT - 인증인가</h1>
<h2 id="01-jwt-개념">01. JWT 개념</h2>
<blockquote>
<p>토큰 안에 담기는 데이터를 json 형식으로 표현하는 토큰인증 방식입니다.
<img src="https://velog.velcdn.com/images/loaded_diaper/post/196c1242-6d31-493a-ac40-841adb64084e/image.png" alt=""></p>
</blockquote>
<p>a. JWS 와 JWE 
<img src="https://velog.velcdn.com/images/loaded_diaper/post/486c2328-3f14-4c32-abf9-b96d8914c1dc/image.png" alt=""></p>
<ul>
<li>JWS: 어떻게 데이터를 보호할까? → 서명방식으로!</li>
<li>JWE: 어떻게 데이터를 보호할까? → 암호화방식으로!</li>
</ul>
<h2 id="c-jwt-살펴보기">c. JWT 살펴보기</h2>
<blockquote>
<p>우리가 쓰는 토큰은 정확히 말하면 ”JWS로 서명된 JWT” 입니다.</p>
</blockquote>
<p>i. jwt 예시
<img src="https://velog.velcdn.com/images/loaded_diaper/post/e6499485-4305-470a-839a-2b936ba1d8d2/image.png" alt=""></p>
<h3 id="a-⭐-header---어떻게-토큰을-만들었는지-설명서">a. ⭐ Header - 어떻게 토큰을 만들었는지 설명서</h3>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/7f24e64b-54c9-4105-9114-de430589ba41/image.png" alt=""></p>
<ul>
<li>어떤 알고리즘(HMAC, SHA256 등)을 사용해서 <code>signature(서명)</code> 를 만들지 명시하는 곳입니다.</li>
<li>Base64 로 인코딩 되어 있어서 누구나 간단하게 읽을 수 있습니다.</li>
<li>토큰 검증시 header 부분을 보고 어떤 알고리즘을 활용해서 서명을 검증할지 판단할 수 있습니다.</li>
</ul>
<h3 id="b-⭐-payload---토큰-내용">b. ⭐ Payload - 토큰 내용</h3>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/fe9d0ee5-7ac7-460e-9abe-0b7fcd4dcafe/image.png" alt=""></p>
<ul>
<li>토큰의 내용(payload)이 담기는 곳입니다.</li>
<li>Base64 로 인코딩 되어 있어서 누구나 간단하게 읽을 수 있습니다.</li>
<li>key-value 형식으로 저장되어 있습니다.</li>
<li>userId, role 등 다양한 정보를 담을 수 있습니다.</li>
</ul>
<h3 id="c-⭐-signature---서명">c. ⭐ Signature - 서명</h3>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/77c839e9-a3c4-4947-b8b6-e1d7ce72217f/image.png" alt=""></p>
<ul>
<li>공식: Base64(header) + Base64(payload) + secret = signature</li>
<li>검증시 서명을 확인해서 토큰이 위변조 되었는지 확인할 수 있습니다.</li>
</ul>
<h3 id="d-⭐-secret---비밀키">d. ⭐ Secret - 비밀키</h3>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/5850c8e6-c33f-4378-a33e-401336dbc658/image.png" alt=""></p>
<ul>
<li><code>Signature(서명)</code> 를 만들때 사용합니다.</li>
<li>외부로 유출될 경우 누구든지 유효한 서명을 가진 JWT 를 만들 수 있음으로 보안에 신경써야합니다.</li>
</ul>
<h3 id="e-정리">e. 정리</h3>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/89c17261-634d-499a-8d5a-aecf2f238987/image.png" alt=""></p>
<ul>
<li>Header: 서명을 만들 때 어떤 알고리즘을 사용할지 선언하는 곳</li>
<li>Payload: 토큰의 내용이 담기는 곳</li>
<li>Signature: <code>Header + Payload + Secret</code> 조합하고 서명 알고리즘을 활용해 만들어내는 서명</li>
<li>Secret: Signature 를 만들기위해 사용되는 비밀키</li>
</ul>
<h2 id="03-jwt-흐름">03. JWT 흐름</h2>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/579da6ac-a19b-4738-9d6b-ec47ef433b93/image.png" alt=""></p>
<h3 id="b-검증">b. 검증</h3>
<blockquote>
<p>서버는 Header 의 alg 값을 보고 어떤 알고리즘을 사용해서 서명을 만들지 판단합니다. 그렇게 생성한 서명을 토큰에 담겨온 서명과 비교해서 일치하지 않으면 위변조된 토큰으로 판단합니다.
<img src="https://velog.velcdn.com/images/loaded_diaper/post/5ac846e5-1003-4b91-bd71-2054ee7f44a8/image.png" alt="">
<img src="https://velog.velcdn.com/images/loaded_diaper/post/a7fbd4b1-1353-481f-b6ba-0449a7636b9f/image.png" alt="">
<img src="https://velog.velcdn.com/images/loaded_diaper/post/a347e71b-ad84-4339-a9cc-97bd20bca1f6/image.png" alt=""></p>
</blockquote>
<h2 id="⭐-06-인증인가-jwt-실습">⭐ 06. 인증인가 jwt 실습</h2>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/cb0f565a-080d-44e0-b8c9-67c1128a082d/image.png" alt=""></p>
<h2 id="정리">정리</h2>
<p>흔히 개발자들이 말하는 JWT 는 <code>Header, payload, Signature</code> 세 부분으로 구성됩니다.</p>
<p><code>JWS</code> 은 JWT 를 서명(Signature)으로 보호한 형태를 의미합니다.</p>
<p><code>JWE</code>은 JWT 를 암호화(Encryption)로 보호한 형태를 의미합니다.</p>
<p>모든 JWS 는 JWT 이지만 모든 JWT 는 JWS 가 아니다 (O)</p>
<p>JWS 의 서명(Signature)부분을 만들때 <code>Header, payload, secret</code> 를 조합하여 만들어집니다.</p>
<p> 서명(signature)을 만들때 사용되는 알고리즘은 <code>Header</code>부분에 선언되어 있습니다.</p>
<p> 토큰에서 실제 내용이 담기는 곳은 <code>payload</code>부분입니다.</p>
<hr>
<h1 id="9-aop-관점지향-프로그래밍">9. AOP 관점지향 프로그래밍</h1>
<h3 id="01-aop">01. AOP</h3>
<p>a. OOP(Object Oriented Programming)
<img src="https://velog.velcdn.com/images/loaded_diaper/post/393917d4-a422-4373-bd5a-56613dde57cb/image.png" alt=""></p>
<p>b. AOP(Aspect Oriented Programming)
<img src="https://velog.velcdn.com/images/loaded_diaper/post/e845d82f-0728-48a5-8125-adc7478691a3/image.png" alt=""></p>
<h3 id="aop-핵심-키워드---횡단-관심사">AOP 핵심 키워드 - 횡단 관심사</h3>
<blockquote>
<p>횡단관심사는 여러곳에서 공통적으로 발생하는 기능(부가기능)을 의미합니다.
<img src="https://velog.velcdn.com/images/loaded_diaper/post/86d562b0-7c22-4307-a831-2deab85beb9c/image.png" alt=""></p>
</blockquote>
<h3 id="aop---관점지향-프로그래밍이란-">AOP - 관점지향 프로그래밍이란 ?</h3>
<blockquote>
<p>비즈니스 로직에서 횡단관심사(부가기능) 을 분리해서 관리하는 것 - 관점을 가지고 설계를 하는것!
<img src="https://velog.velcdn.com/images/loaded_diaper/post/ac918c49-175d-4047-912e-a779376ed6e9/image.png" alt=""></p>
</blockquote>
<h3 id="aop-중요-키워드">AOP 중요 키워드</h3>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/f49987f2-5f4c-43c9-8748-f532b3a6ca94/image.png" alt=""></p>
<p>a. ⭐ 어드바이스(Advice)</p>
<blockquote>
<p>부가기능
<img src="https://velog.velcdn.com/images/loaded_diaper/post/7ebaba03-86a8-4941-a7e1-ed39f479b077/image.png" alt="">
@Around, @Before, @After, @AfterThrowing</p>
</blockquote>
<p>b. ⭐️포인트컷(PointCut)</p>
<blockquote>
<p>어드바이스(부가기능 / 횡단관심사) 이  적용될 구체적인 범위를 표현하는 규칙
<img src="https://velog.velcdn.com/images/loaded_diaper/post/95eeeba6-04b5-41ba-9594-3ad29649cfe9/image.png" alt=""></p>
</blockquote>
<p>c. ⭐️대상객체(Target)</p>
<blockquote>
<p>포인트컷 조건에 의해 어드바이스가 적용되는 실제 객체(클래스)
<img src="https://velog.velcdn.com/images/loaded_diaper/post/cc4b2241-a1f2-4afe-81fc-42d3436f4380/image.png" alt=""></p>
</blockquote>
<p>d. ⭐️조인포인트(JoinPoint)</p>
<blockquote>
<p>어드바이스가 적용되는 실행지점(메서드)
<img src="https://velog.velcdn.com/images/loaded_diaper/post/caef0c19-7ca8-438f-9e2d-466a99643969/image.png" alt=""></p>
</blockquote>
<pre><code class="language-java">// com.example.service 패키지에 포함되어 있는 CourseService 객체의 모든 메서드
execution(* com.example.service.CourseService..*(..))</code></pre>
<p>e. ⭐️에스팩트(Aspect)</p>
<blockquote>
<p>AOP 를 구성하는 하나의 단위(어드바이스 + 포인트컷)
<img src="https://velog.velcdn.com/images/loaded_diaper/post/b5d979af-2093-4668-8f97-d8043c138aec/image.png" alt=""></p>
</blockquote>
<h3 id="정리-1">정리</h3>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/7b58ebff-024f-4997-81d7-affdc8292b1a/image.png" alt=""></p>
<hr>
<h1 id="10-운영환경-배포와-클라우드">10. 운영환경 배포와 클라우드</h1>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/fa4df111-a754-475a-8b00-ad99f7255dc8/image.png" alt="">
<img src="https://velog.velcdn.com/images/loaded_diaper/post/2eee8b0d-e9a7-49d2-8731-60d3228c0105/image.png" alt="">
<img src="https://velog.velcdn.com/images/loaded_diaper/post/49125abe-1d3f-4bab-907a-86ad1272dca2/image.png" alt=""></p>
<p>a. 다양한 빌드 도구</p>
<blockquote>
<p>JVM 진영의 대표적인 빌드 도구들은 다음과 같습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/0d8f982f-230a-4456-a8a1-9c43fc024a4e/image.png" alt=""></p>
<ul>
<li><code>Ant</code> - 직접 빌드 스크립트를 작성 후 실행</li>
<li><code>Maven</code> - XML(pom.xml) 파일 기반</li>
<li><code>Gradle</code> - xml 의 한계 극복 → build.gradle</li>
</ul>
<p>b. 빌드 파일 종류와 용도</p>
<blockquote>
<p>SpringBoot 에서 jar 로 빌드하면 애플리케이션과
WAS(내장 톰캣)이 함께 패키징 되기 때문에 해당 jar 파일만으로 직접 실행할 수 있습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/f1ebce97-c877-4e55-8cc7-4a3adc1231aa/image.png" alt="">
<img src="https://velog.velcdn.com/images/loaded_diaper/post/d19265a5-e06e-4d9a-a8a3-cc482055f0e5/image.png" alt=""></p>
<h3 id="c-빌드파일-살펴보기-buildgradle">c. 빌드파일 살펴보기 build.gradle</h3>
<p>i. 어떤 빌드 규칙을 사용할지 결정</p>
<blockquote>
<ol>
<li>이건 자바 프로젝트야 그리고 2. 스프링부트도 쓸꺼야 3.. 의존성은 자동관리 시스템을 쓸꺼야</li>
</ol>
</blockquote>
<pre><code class="language-java">plugins {
    id &#39;java&#39;
    id &#39;org.springframework.boot&#39; version &#39;3.4.5&#39;
    id &#39;io.spring.dependency-management&#39; version &#39;1.1.7&#39;
}</code></pre>
<p>ii. 결과물의 이름 &amp; 버전 설정</p>
<blockquote>
<p>빌드하면 결과물(jar 파일) 이 만들어지는데 그 파일에 붙일 이름표를 결정하는 부분</p>
</blockquote>
<pre><code class="language-java">group = &#39;com.example&#39;
version = &#39;0.0.1-SNAPSHOT&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/ffa1c260-382b-4368-88fa-b586d3277b88/image.png" alt=""></p>
<h2 id="프로파일profile">프로파일(profile)</h2>
<blockquote>
<p>같은 애플리케이션을 환경별로 다르게 설정하기 위한 기능</p>
</blockquote>
<h3 id="a-다양한-환경">a. 다양한 환경</h3>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/52ebc6bf-d53c-472a-9b25-83985bdb5662/image.png" alt="">
<img src="https://velog.velcdn.com/images/loaded_diaper/post/3cfaf3be-3eff-4824-a97b-f139bccc889c/image.png" alt=""></p>
<h2 id="⭐-프로파일을-활용한-운영환경-배포-방법">⭐ 프로파일을 활용한 운영환경 배포 방법</h2>
<p>a. 클라우드 개념</p>
<blockquote>
<p>클라우드란 구름 - 구름이 하늘에 떠 있는 것처럼 인터넷 상에 떠있는 컴퓨터 자원을 의미합니다.
<img src="https://velog.velcdn.com/images/loaded_diaper/post/52be2dee-4373-4eed-bb04-4d2105f6d21a/image.png" alt=""></p>
</blockquote>
<h3 id="b-클라우드에-배포---운영환경에-배포-해보기">b. 클라우드에 배포 - 운영환경에 배포 해보기</h3>
<p>i. 프로젝트 빌드
<img src="https://velog.velcdn.com/images/loaded_diaper/post/2582b56a-2e97-41db-b8e4-c340aca55050/image.png" alt="">
ii. 클라우드에 떠있는 Ec2 로 빌드 파일 원격 전송
<img src="https://velog.velcdn.com/images/loaded_diaper/post/4a0d2a25-da35-4cae-a893-4584b9c7b15a/image.png" alt=""></p>
<pre><code class="language-java">scp -i &lt;보안키.pem&gt; &lt;빌드파일경로&gt; &lt;아이디&gt;@&lt;Ec2주소&gt;:&lt;설치경로</code></pre>
<pre><code class="language-java">scp -i ~/.ssh/id_rsa build/libs/basic-0.0.1-SNAPSHOT.jar gygimdev@34.64.152.58:~/app.jar</code></pre>
<p>c. 클라우드에서 서버 실행
<img src="https://velog.velcdn.com/images/loaded_diaper/post/cc37a24a-4134-4669-9c6e-fef57b800c62/image.png" alt=""></p>
<p>⚠️ 수동배포의 번거로움
<img src="https://velog.velcdn.com/images/loaded_diaper/post/564c32e2-48c7-47dd-ac22-e921b872c06c/image.png" alt="">
번거로운 환경설정 → JVM 설치과정</p>
<p>이래서 도커가 나옴</p>
<p>정리</p>
<p><strong>Q. 다음 중 스프링 프로젝트를 운영 서버에 배포하는 절차를 올바른 순서로 나열한 것은?</strong></p>
<ol>
<li>빌드 파일을 실행한다.</li>
<li>빌드 파일을 원격 서버로 전송한다.</li>
<li>프로젝트를 빌드해 실행 가능한 파일을 만든다.</li>
</ol>
<p>C. 3 -&gt; 2 -&gt; 1</p>
<p><strong>Q. ./gradlew build 명령어를 시작했을때 만들어지는 결과물은 무엇일까요?</strong></p>
<p>A. .class 파일만 생성된다.</p>
<pre><code>B. 실행 가능한 .jar 파일이 생성된다.
</code></pre><p>C. 소스 코드(.java) 복사된다.</p>
<p>D. 데이터베이스 스키마가 만들어진다.</p>
<p><strong>Q. Ec2 서버에 jar 파일을 성공적으로 옮겼지만 java -jar app.jar 실행시</strong></p>
<p><strong>&quot;command not found&quot; 에러가 발생합니다. 가장 가능성이 높은 원인은?</strong></p>
<p>A. jar 파일이 손상되었다.</p>
<pre><code>B. 서버에 자바 런타임(JRE, JDK) 가 설치되지 않았다.</code></pre><p>C. 포트가 열려있지 않다.</p>
<p>D. 데이터베이스 연결이 실패했다.</p>
<p><strong>Q. 다음 중 로컬에서 만든 빌드파일을 원격 서버(EC2) 에 전송할때 사용하는 명령어는 ?</strong></p>
<p>A. ssh</p>
<pre><code>B. scp</code></pre><p>C. git push</p>
<p>D. curl</p>
<p><strong>Q. 스프링 프로젝트에서 profile 을 사용하는 가장 큰 이유는 무엇일까요?</strong></p>
<p>A. 빌드 속도를 빠르게 하기 위해서</p>
<p>B. 코드 양을 줄이기 위해서</p>
<pre><code>C. 개발, 운영 등 환경별로 다른 설정을 쉽게 적용하기 위해서</code></pre><p>D. 자바 버전을 자동으로 맞추기 위해서</p>
<hr>
<h1 id="11-도커docker---컨테이너기술">11. 도커(docker) - 컨테이너기술</h1>
<h3 id="01-도커를-왜-배워야할까">01. 도커를 왜 배워야할까?</h3>
<blockquote>
<p>서버마다 환경을 설정해줘야하는 번거로움</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/c4d3554e-feb2-4fe9-a6d3-e8315b617cf5/image.png" alt=""></p>
<h3 id="02-도커docker">02. 도커(docker)</h3>
<blockquote>
<p>리눅스 커널 기반으로 만들어진 컨테이너 기술입니다. 
앱과 실행 환경을 통째로 묶어서 어디서든 똑같이 실행할 수 있게 도와주는 기술입니다.
<img src="https://velog.velcdn.com/images/loaded_diaper/post/801b78b1-9949-44ff-b9eb-343f731f1a75/image.png" alt=""></p>
</blockquote>
<h3 id="a-컨테이너의-3가지-철학">a. 컨테이너의 3가지 철학</h3>
<ul>
<li>재현성 - 어디서든 동일한 실행 결과 보장</li>
<li>격리 - 컨테이너마다 독립된 환경에서 실행</li>
<li>불변성 - 이미지는 수정없이 항상 새 버전으로 교체</li>
</ul>
<h3 id="b-다양한-컨테이너-기술들">b. 다양한 컨테이너 기술들</h3>
<ul>
<li>docker, podman, containerD, etc</li>
</ul>
<h2 id="03-가상머신vm-virtual-machine">03. 가상머신(VM: virtual machine)</h2>
<blockquote>
<p>컴퓨터 안에 새로운 컴퓨터를 만드는 기술
<img src="https://velog.velcdn.com/images/loaded_diaper/post/e072a587-6847-4562-ad39-26441cd63259/image.png" alt=""></p>
</blockquote>
<ul>
<li>vm 이 만들어질때 컴퓨터의 자원을 미리 나눠가집니다.(RAM, CPU)<ul>
<li>컴퓨터 자원: RAM 16GB, CPU 8 코어<ul>
<li>HOST: RAM 8GB, 4 코어</li>
<li>VM 1: RAM 4GB, 2 코어(나눠줌)</li>
<li>VM 2: RAM 4GB, 2 코어(나눠줌)</li>
</ul>
</li>
</ul>
</li>
<li>다른 곳에서 자원을 가져가지 못합니다.</li>
</ul>
<p><strong>OS Kernel</strong></p>
<p>운영체제의  핵심부분입니다. 하드웨어와 소프트웨어 사이에서 통역하고 관리해주는 총 관리자라고 생각하시면 충분합니다.</p>
<ul>
<li>window - NT Kernel (ms 독자 개발)</li>
<li>Mac -XNU (unix 계열)</li>
<li>Linux -Linux 커널 (도커가 그대로 도는 환경(네이티브 환경), unix 계열)</li>
</ul>
<p>출처: 내일배움 스파르타 클럽</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring심화 과제 트러블슈팅]]></title>
            <link>https://velog.io/@loaded_diaper/Spring%EC%8B%AC%ED%99%94-%EA%B3%BC%EC%A0%9C-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@loaded_diaper/Spring%EC%8B%AC%ED%99%94-%EA%B3%BC%EC%A0%9C-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</guid>
            <pubDate>Mon, 11 May 2026 01:17:34 GMT</pubDate>
            <description><![CDATA[<h1 id="🛠️-spring-boot-프로젝트-실행-실패-해결하기---jwt-secret-key와-datasource-설정">🛠️ Spring Boot 프로젝트 실행 실패 해결하기 - JWT Secret Key와 DataSource 설정</h1>
<h2 id="🚨-문제-상황">🚨 문제 상황</h2>
<p>새 과제 프로젝트를 <code>clone</code>한 뒤 애플리케이션을 실행했는데 서버가 정상적으로 실행되지 않았다.</p>
<p>처음 실행했을 때 아래와 같은 에러가 발생했다.</p>
<pre><code class="language-text">ApplicationContextException: Unable to start web server</code></pre>
<p>처음에는 단순히 서버 실행 문제라고 생각했지만, 에러 로그를 아래로 내려보니 진짜 원인은 따로 있었다.</p>
<pre><code class="language-text">Could not resolve placeholder &#39;jwt.secret.key&#39; in value &quot;${jwt.secret.key}&quot;</code></pre>
<p>즉, Spring Boot가 실행되는 과정에서 <code>jwt.secret.key</code> 값을 찾지 못해서 <code>JwtUtil</code> Bean 생성에 실패하고 있었다.</p>
<hr>
<h2 id="🔍-원인-분석">🔍 원인 분석</h2>
<p>프로젝트의 <code>JwtUtil</code> 클래스에서는 JWT Secret Key 값을 설정 파일에서 주입받고 있었다.</p>
<pre><code class="language-java">@Value(&quot;${jwt.secret.key}&quot;)
private String secretKey;</code></pre>
<p>하지만 프로젝트를 처음 열었을 때 <code>src/main/resources/application.properties</code> 파일이 존재하지 않았다.</p>
<p>즉, Spring Boot 입장에서는 아래 값을 읽어와야 하는데,</p>
<pre><code class="language-properties">jwt.secret.key=...</code></pre>
<p>해당 설정 자체가 없었기 때문에 애플리케이션 실행에 실패한 것이다.</p>
<hr>
<h2 id="🧩-1차-해결-시도">🧩 1차 해결 시도</h2>
<p>먼저 <code>src/main/resources</code> 폴더를 만들고, 그 안에 <code>application.properties</code> 파일을 생성했다.</p>
<p>그리고 아래처럼 JWT Secret Key 값을 추가했다.</p>
<pre><code class="language-properties">jwt.secret.key=sparta-spring-advanced-secret-key-2026-must-be-at-least-32-bytes</code></pre>
<p>하지만 다시 실행해보니 또 다른 에러가 발생했다.</p>
<pre><code class="language-text">Illegal base64 character 2d</code></pre>
<hr>
<h2 id="⚠️-추가-문제-발생">⚠️ 추가 문제 발생</h2>
<p>이번 에러는 <code>jwt.secret.key</code> 값이 없어서 발생한 문제가 아니라, Secret Key의 형식이 잘못되어 발생한 문제였다.</p>
<p><code>JwtUtil</code> 내부에서 Secret Key를 Base64로 디코딩하고 있었기 때문에 일반 문자열을 넣으면 안 됐다.</p>
<p>처음 넣은 값에는 <code>-</code> 하이픈이 포함되어 있었고, 이 문자는 Base64 디코딩 과정에서 문제가 되었다.</p>
<hr>
<h2 id="✅-2차-해결">✅ 2차 해결</h2>
<p>그래서 Secret Key 값을 Base64 형식의 문자열로 변경했다.</p>
<pre><code class="language-properties">jwt.secret.key=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=</code></pre>
<p>이후 JWT 관련 에러는 해결되었다.</p>
<p>하지만 이번에는 DB 설정 관련 에러가 발생했다.</p>
<pre><code class="language-text">Failed to configure a DataSource: &#39;url&#39; attribute is not specified</code></pre>
<hr>
<h2 id="🗄️-datasource-에러-원인">🗄️ DataSource 에러 원인</h2>
<p><code>build.gradle</code>을 확인해보니 MySQL 의존성이 추가되어 있었다.</p>
<pre><code class="language-gradle">runtimeOnly &#39;com.mysql:mysql-connector-j&#39;</code></pre>
<p>즉, 이 프로젝트는 MySQL DB 연결이 필요한 프로젝트였다.</p>
<p>하지만 <code>application.properties</code>에 DB 연결 정보가 없었기 때문에 Spring Boot가 어떤 DB에 연결해야 하는지 알 수 없었다.</p>
<hr>
<h2 id="✅-최종-해결">✅ 최종 해결</h2>
<p>MySQL에 사용할 데이터베이스를 생성했다.</p>
<pre><code class="language-sql">CREATE DATABASE spring_advanced;</code></pre>
<p>그리고 <code>application.properties</code>에 MySQL 연결 설정을 추가했다.</p>
<pre><code class="language-properties">spring.datasource.url=jdbc:mysql://localhost:3306/spring_advanced
spring.datasource.username=root
spring.datasource.password=내 MySQL 비밀번호
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true</code></pre>
<p>최종적으로 <code>application.properties</code>에는 JWT 설정과 DB 설정이 함께 들어가게 되었다.</p>
<pre><code class="language-properties">jwt.secret.key=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=

spring.datasource.url=jdbc:mysql://localhost:3306/spring_advanced
spring.datasource.username=root
spring.datasource.password=내 MySQL 비밀번호
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true</code></pre>
<hr>
<h2 id="🎉-결과">🎉 결과</h2>
<p>다시 애플리케이션을 실행하니 서버가 정상적으로 실행되었다.</p>
<pre><code class="language-text">Tomcat started on port 8080
Started ExpertApplication</code></pre>
<p>또한 Hibernate가 필요한 테이블들도 정상적으로 생성했다.</p>
<pre><code class="language-text">create table users
create table todos
create table comments
create table managers</code></pre>
<hr>
<h2 id="🔐-주의한-점">🔐 주의한 점</h2>
<p><code>application.properties</code>에는 MySQL 비밀번호 같은 민감한 정보가 들어간다.</p>
<p>그래서 GitHub에 올라가지 않도록 반드시 <code>.gitignore</code> 적용 여부를 확인했다.</p>
<pre><code class="language-bash">git check-ignore -v src/main/resources/application.properties</code></pre>
<p>확인 결과 아래처럼 <code>.gitignore</code>에 의해 무시되고 있었다.</p>
<pre><code class="language-text">.gitignore:40:*.properties src/main/resources/application.properties</code></pre>
<p>즉, DB 비밀번호가 GitHub에 올라가지 않는 안전한 상태였다.</p>
<hr>
<h2 id="🧠-알게-된-점">🧠 알게 된 점</h2>
<p>이번 트러블 슈팅을 통해 Spring Boot 프로젝트 실행 실패 로그를 볼 때는 가장 위의 에러만 볼 것이 아니라, 아래쪽의 <code>Caused by</code>를 따라가며 진짜 원인을 찾아야 한다는 것을 알게 되었다.</p>
<p>또한 <code>@Value(&quot;${...}&quot;)</code>로 주입받는 설정값은 반드시 <code>application.properties</code> 또는 <code>application.yml</code>에 존재해야 한다.</p>
<p>JWT Secret Key처럼 내부에서 Base64 디코딩을 수행하는 값은 단순 문자열이 아니라 Base64 형식으로 넣어야 한다는 점도 알게 되었다.</p>
<p>마지막으로 MySQL 의존성이 있는 프로젝트에서는 DataSource 설정이 없으면 애플리케이션 실행 단계에서 실패할 수 있다는 것도 확인했다.</p>
<hr>
<h2 id="📌-정리">📌 정리</h2>
<p>이번 문제의 흐름은 다음과 같았다.</p>
<pre><code class="language-text">1. 프로젝트 실행 실패
2. jwt.secret.key 설정 누락 확인
3. application.properties 생성
4. JWT Secret Key 추가
5. Base64 형식 문제 해결
6. DataSource 설정 누락 확인
7. MySQL DB 생성 및 연결 설정 추가
8. 서버 정상 실행</code></pre>
<p>단순히 에러 메시지만 보고 해결하려고 하기보다, 로그의 원인을 단계별로 따라가면서 해결하는 것이 중요하다는 것을 배웠다.<img src="https://velog.velcdn.com/images/loaded_diaper/post/7415ea84-03c6-4c74-a88f-5892cebaeeb4/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 정리 2]]></title>
            <link>https://velog.io/@loaded_diaper/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%A0%95%EB%A6%AC-2</link>
            <guid>https://velog.io/@loaded_diaper/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%A0%95%EB%A6%AC-2</guid>
            <pubDate>Wed, 06 May 2026 11:47:16 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/c422c2ca-581c-4b89-83ea-7c974995d2dc/image.png" alt=""></p>
<h1 id="3데이터베이스-접근-방법jpa">3.데이터베이스 접근 방법(JPA)</h1>
<h2 id="01-jpajava-persistence-api">01. JPA(Java Persistence API)</h2>
<blockquote>
<p>JPA(Java Persistence API) 는 자바에서 ORM을 위한 표준 명세 입니다.</p>
</blockquote>
<h3 id="a-ormobject-relational-mapping">a. ORM(Object Relational Mapping)</h3>
<blockquote>
<p>데이터베이스를 Object로 다루는 기술입니다. 자바에서는 Object 는 객체를 의미합니다.</p>
</blockquote>
<h3 id="b-다양한-jpa-구현체">b. 다양한 JPA 구현체</h3>
<blockquote>
<p>JPA 를 실제로 동작하게 하는 여러 구현체가 있습니다. 스프링부트에서 어떤 구현체를 사용할지 개발자가 결정할 수 있지만 기본적으로 <code>Hibernate</code> 구현체를 사용하고 있습니다.</p>
</blockquote>
<h2 id="02-jpa-가-sql-을-실행하는-과정">02. JPA 가 SQL 을 실행하는 과정</h2>
<blockquote>
<p>JPA 로 편하게 DB 를 조작하는 것은
DB 에 SQL 을 보내는 과정을 편하게 만든 것입니다.
→ 실제로 DB 에 SQL 을 보내는 것은 JDBC 가 담당하게 됩니다.</p>
</blockquote>
<p><strong>JDBC(Java Database Connectivity)</strong></p>
<ul>
<li>자바와 DB 사이의 통신 규약(인터페이스)입니다.</li>
<li>구현체는 DB-driver(mysql, mariadb, 등) 가 담당합니다.</li>
</ul>
<h2 id="03--jpa-핵심-키워드--동작원리">03.  JPA 핵심 키워드 &amp; 동작원리</h2>
<h2 id="⭐-a-entitymanagerfactory">⭐ a. EntityManagerFactory</h2>
<hr>
<blockquote>
<p><code>EntityManagerFactory</code> 는 DB 연결에 필요한 모든 세팅을 미리 해놓습니다.
그리고 요청이 들어오면 <code>EntityManager</code> 생성에 사용됩니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/58a1601a-fa5f-4d5c-a381-d2de036651f9/image.png" alt="">
<img src="https://velog.velcdn.com/images/loaded_diaper/post/99ce96e2-cd9c-4562-9bee-e36273bf28ae/image.png" alt=""></p>
<h2 id="⭐-b-entitymanager">⭐ b. EntityManager</h2>
<hr>
<blockquote>
<p>EntityManager 는 DB 에 저장, 조회, 수정, 삭제하는 작업자 입니다.</p>
</blockquote>
<ul>
<li>*Transaction 단위로 생성됩니다.
<img src="https://velog.velcdn.com/images/loaded_diaper/post/84f2373e-092b-47f1-b34b-2a2c6b5d910f/image.png" alt="">
<img src="https://velog.velcdn.com/images/loaded_diaper/post/b7b75d70-7b9f-4ffe-ac58-afd43d56d0a6/image.png" alt=""></li>
</ul>
<h2 id="⭐-c-엔티티">⭐ c. 엔티티</h2>
<blockquote>
<p>데이터베이스의 테이블 구조를 그대로 표현하는 역할을 가진 객체입니다.</p>
</blockquote>
<pre><code class="language-java">@Entity
public class Book {

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

    private String name;

    private String author;
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/d705007f-8434-4f28-a75d-16311d0971dd/image.png" alt=""></p>
<h2 id="⭐-d-영속성-컨텍스트">⭐ d. 영속성 컨텍스트</h2>
<blockquote>
<p>엔티티의 정보를 메모리에서 관리하는 임시 저장소. 데이터를 반영하기전 변경사항을 모두 기록합니다. 트랜잭션이 끝나면 모든 변경사항을 DB 에 반영합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/e921ea7d-33cf-472c-bf22-e04f98a375fd/image.png" alt="">
<img src="https://velog.velcdn.com/images/loaded_diaper/post/0d50bf3d-5429-41a8-94e8-848cd494620e/image.png" alt=""></p>
<pre><code class="language-java">class Service {

        // 트랜잭션 시작
        @Transactional 
        public void doService() {

                // 1. 데이터 조회, 생성, 수정, 삭제 
                // ...
                // ...
        }
    // 트랜잭션 끝
}</code></pre>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/e0fd24fc-cb5f-406b-aa97-2cb6f65af165/image.png" alt=""></p>
<p><strong>i. 엔티티의 상태</strong>
<img src="https://velog.velcdn.com/images/loaded_diaper/post/38fa810f-d995-48ae-acc7-e525aeaf4bf6/image.png" alt="">
<strong>ii. 영속성 컨텍스트의 4가지 특징</strong></p>
<ol>
<li><p>1차 캐싱</p>
<pre><code class="language-java">Book a = em.find(Book.class, &quot;의문의책A&quot;); // SELECT * FROM books WHERE id = &#39;의문의책A&#39;
Book b = em.find(Book.class, &quot;의문의책A&quot;); // SELECT 쿼리 발생 X (1차 캐시 사용)</code></pre>
</li>
<li><p>동일성 보장</p>
<pre><code class="language-java">Book a = em.find(Book.class, &quot;의문의책A&quot;);
Book b = em.find(Book.class, &quot;의문의책A&quot;);
</code></pre>
</li>
</ol>
<p>// 동일성 비교 true (같은 객체 반환)
System.out.println(a == b);</p>
<pre><code>
3. 쓰기 지연
```java
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();

//데이터 변경시 트랜잭션을 시작
transaction.begin(); // 트랜잭션 시작

em.persist(bookA); // (1) INSERT ...
em.persist(bookB); // (2) INSERT ...

// 쓰기지연은 persist 호출시 SQL 이 바로 실행되지 않습니다.
// (1, 2 의 sql 을 모았다가 보낸다.)
trasnaction.commit(); 트랜젝션 커밋</code></pre><ol start="4">
<li>변경감지
```java</li>
</ol>
<p>--&gt; 트랜잭션 시작</p>
<p>// 영속
Book findBook = em.find(Book.class, &quot;채식주의자&quot;);
findBook.setName(&quot;소년이온다&quot;);</p>
<p>// persist 호출 불필요합니다.
// 변경된 속성은 트랜잭션 커밋 시 자동 반영 됩니다.
// em.persist(findBook);</p>
<p>--&gt; 트랜잭션 끝</p>
<pre><code>
## ⭐ 요약

`EntityManager`  는 실제로 엔티티를 DB 에 저장, 조회, 수정, 삭제하는 작업자입니다. 


`EntityManagerFactory` 는 DB 에 연결에 필요한 모든 세팅을 미리 해놓고 `EntityManager` 생성에 사용됩니다.


`EntityManagerFactory` 는 애플리케이션 시작시 딱 한번 만들어지고 애플리케이션이 종료될때까지 유지됩니다.


 EntityManagerFactory 를 만들려면 `DataSource` 가 정상적으로 생성되어야합니다.


 EntityManager 는 EntityManagerFactory 로부터 생성되며 `Transaction`  단위로 생성되고 종료됩니다.

 `영속성컨텍스트`  는 엔티티를 메모리에서 관리하는 임시 저장소입니다.


 ---

 # 4. 인증인가 기초(쿠키, 세션, 토큰)

 ## ⭐ 01. 인증인가
 ![](https://velog.velcdn.com/images/loaded_diaper/post/213bb1b1-8eda-4e35-84de-b5b8bd9e908d/image.png)

a. 인증(Authentication) 
&gt;누구인가 확인하는 단계 
![](https://velog.velcdn.com/images/loaded_diaper/post/93cdeb09-9471-4ca8-844c-edbea2b180d0/image.png)

b. 인가(Authorization) 
&gt;자격을 확인하는 단계
![](https://velog.velcdn.com/images/loaded_diaper/post/2ecaf2a2-d044-461f-9858-df10d0902c1b/image.png)

## ⭐ 02. 쿠키
&gt; 브라우저의 저장공간

![](https://velog.velcdn.com/images/loaded_diaper/post/c28b9781-7b70-4e09-9021-56727ab9bd7d/image.png)

### 쿠키인증의 한계
![](https://velog.velcdn.com/images/loaded_diaper/post/8df66b3d-1a61-4b64-b226-9b351f3ef1bf/image.png)


## ⭐ 03. 세션
&gt;클라이언트와 서버의 연결을 나타내는 하나의 상태
![](https://velog.velcdn.com/images/loaded_diaper/post/16485669-8c1e-4cce-93e2-45c55398a43e/image.png)

 ### 세션의 한계
![](https://velog.velcdn.com/images/loaded_diaper/post/0e8eb433-7978-48f3-9185-6a51e033e01f/image.png)


## ⭐ 04. 토큰
&gt;의미 있는 불규칙한 문자열
 ![](https://velog.velcdn.com/images/loaded_diaper/post/4fe7aeb1-f7c0-4bc2-a822-b61bca157a9b/image.png)
![](https://velog.velcdn.com/images/loaded_diaper/post/218d217c-dafd-436e-bc5e-b199c7169330/image.png)

### 토큰인증방식의 한계
암호화가 아닙니다 - 단순한 문자열(Base64 적용)


## ⭐ 요약
![](https://velog.velcdn.com/images/loaded_diaper/post/a95face2-5a8a-4b14-9234-118d815e2fb6/image.png)


---

# 5. 연관관계

## ⭐ 01. 데이터베이스 모델링
&gt;어떤 서비스를 만들 때 필요한 데이터들을 정리하고 구조화하는 작업(테이블 명세 + ERD)
![](https://velog.velcdn.com/images/loaded_diaper/post/2d0db63a-5efa-46ab-a435-581a4a9a61f5/image.png)
![](https://velog.velcdn.com/images/loaded_diaper/post/1a0cfcec-7f8f-4e7d-949b-29e9f40545dd/image.png)


### 연관관계
&gt;객체(엔티티) 가 서로 연결되어 있는 모습을 연관관계 라고 합니다. (JPA 와 다른 ORM 에서 사용하는 용어).

a. 연관관계 주인
b. 부모-자식 관계
c. 단방향과 양방향 연관관계


## ⭐ a. 연관관계 주인
&gt;연관관계가 있는 테이블 사이에서
 물리적으로 다른테이블의 외래키를 소유하고 있는 테이블/엔티티를 연관관계의 주인이라고 표현합니다. 

![](https://velog.velcdn.com/images/loaded_diaper/post/05fe0921-f2b3-4dca-bec9-35885602cfed/image.png)
![](https://velog.velcdn.com/images/loaded_diaper/post/d6d40e6e-c21b-4532-b038-2ff360e8bf98/image.png)

## ⭐ b. 부모 - 자식 관계
&gt;비즈니스 관점에서 한 도메인이 다른 도메인을 포함하는 논리적인 관계를 의미합니다.

- 부모: 포함하고 있는 도메인
- 자식: 포함되는 도메인

![](https://velog.velcdn.com/images/loaded_diaper/post/7679cb3a-b662-402b-913c-5ac4ce9286c6/image.png)

수강생은 연관관계 부모-자식 관계에서 `자식` 이다. 

수업은 연관관계 부모-자식 관계에서 `부모` 이다. 

### 연관관계 정리
![](https://velog.velcdn.com/images/loaded_diaper/post/0af24e5d-fe6a-4804-ab1a-0d9d61e96d61/image.png)

## ⭐ i. 단방향

&gt; 한쪽에서만 다른 엔티티를 참조하는 상황 (`@ManyToOne`)

## ⭐ ii. 양방향

&gt; 엔티티(객체) 들이 서로 참조하는 상황(`@OneToMany`)


## cascadeType 그리고 orphanRemoval
&gt;양방향 연관관계를 더 멋지게? 사용하는 방법

## a. cascadeType

&gt; 양방향 연관관계(@OneToMany) 에서 부모 엔티티를 PERSIT 하거나 REMOVE할때 연관된 자식 엔티티에게도 그 작업을 자동으로 적용할지 결정하는 설정
&gt;![](https://velog.velcdn.com/images/loaded_diaper/post/97b91768-8577-461f-aed1-fcce42360720/image.png)

## b. orphanRemoval 
&gt;부모와의 연관관계가 끊겨 있는 엔티티를 자동으로 삭제할지 결정하는 기능 
→ 부모 없는 엔티티 → 고아(orphan) 이라고 표현합니다. 
![](https://velog.velcdn.com/images/loaded_diaper/post/e389eb60-5bcd-4a9d-8de7-319b7d96b1bb/image.png)

## c. 조합 정리(cascadeType + orphanRemoval) 
 ![](https://velog.velcdn.com/images/loaded_diaper/post/4263d8c5-180e-416e-bb3c-a7385d51e847/image.png)


## ⭐ 05. 양방향 연관관계의 위험성
&gt;양방향 연관관계 사용시 객체의 상태가 양쪽에 존재하기 때문에 어떤 값을 DB 에 반영해야하는지 JPA 가 자동으로 판단할 수 없는 모호한 상황이 발생할 수 있습니다.
![](https://velog.velcdn.com/images/loaded_diaper/post/1534765b-9456-488a-8ddb-d39deb9edcc7/image.png)

실무는 대부분 단방향


---

# 6. 스프링의 예외처리 원리
![](https://velog.velcdn.com/images/loaded_diaper/post/b0ce93a8-8e92-49aa-856d-dcfa64429430/image.png)

## 01. 예외 처리 복습 
&gt;예외 처리 라고 하는 것은 크게 두가지로 나누어져 있습니다. 

예외 처리 
1. 직접 처리
2. 전파 후 처리(책임전가)

## 예외 종류
a. 체크예외(Exception) 
- `RuntimeException` 을 제외한 Exception 클래스를 상속받은 모든 하위 클래스
- 반드시 명시적으로 예외를 처리해야합니다.
- 컴파일러가 컴파일 단계에서 검사해줍니다. 

b. 언체크예외(UncheckedException) 
- `RuntimeException` 클래스를 상속 받은 모든 하위 예외 클래스
- 처리방식은 똑같지만 예외처리를 강제하지 않습니다.
- 컴파일 단계에서 검사해주지 않습니다. 

### 정리
 ![](https://velog.velcdn.com/images/loaded_diaper/post/137d12fa-01a5-4b11-b074-74ec6e320b61/image.png)


## ⭐ 4. 논의 - 언제 check / unchecked 를 사용해야할까?
&gt;프로그램 실행 흐름안에서 직접 처리시 복구가능성이 있는 상황이라면 `checked` 그렇지 않다면 `unchecked` 를 사용합니다. 
 ![](https://velog.velcdn.com/images/loaded_diaper/post/f31748a9-4186-4573-ab3d-8dde808dca36/image.png)

![](https://velog.velcdn.com/images/loaded_diaper/post/3c8e2b45-c83b-4f13-8000-2c731e5d8f00/image.png)
![](https://velog.velcdn.com/images/loaded_diaper/post/63d8c003-9e10-4a9f-9c51-92932a3858eb/image.png)

## ⭐ 스프링 예외처리 흐름
&gt;예외가 발생하면 /error 경로로 BasicErrorController 를 호출해서 에러 응답 본문을 만들어줍니다.
![](https://velog.velcdn.com/images/loaded_diaper/post/4e9130b9-f26d-42e3-a0a5-392499a592d7/image.png)
![](https://velog.velcdn.com/images/loaded_diaper/post/990075e3-0bbc-489d-8359-78b231b20a76/image.png)

## 예외처리 흐름 살펴보기 - 필터  
a. 필터 
&gt;스프링필터는 WAS 와 DispatcherServlet 사이에 위치합니다. 이 위치 덕분에 모든 요청 응답에 대한 공통작업(보안검사, 요청/응답 기록 등) 처리에 적합한 역할을 합니다. 
![](https://velog.velcdn.com/images/loaded_diaper/post/f5a33055-f025-4b96-88a5-3d5f05ae8a0a/image.png)

## ⭐ 05. ExceptionResolver
&gt;스프링은 3가지 resolver 를 활용해 예외처리시 사용할 상태코드, 에러메시지 등을 미리 설정합니다.
![](https://velog.velcdn.com/images/loaded_diaper/post/c5edd8f4-ebcb-4d7b-9a8e-4ef7841aaf85/image.png)

스프링에서 예외가 발생했을때  **c → b → a**  순차적으로 예외를 처리하려고 시도합니다.

1. DefaultHandlerExceptionResolver
2. ResponseStatusExceptionResolver
3. *(현업사용) ExceptionHandlerExceptionResolver

### 1. DefaultHandlerExceptionResolver

- 스프링에서 기본적으로 정의해놓은 표준 예외(메시지, 상태코드)를 처리하는 resolver 입니다.
- BasicErrorController 통해서 예외를 처리합니다.
- 파일참고: DefaultHandlerExceptionResolver.java 참고

### 2. ResponseStatusExceptionResolver

- @ResponseStatus  로 설정한 예외를 처리하는 resolver 입니다.
- BasicErrorController 통해서 예외를 처리합니다.

### ⭐ c. ExceptionHandlerExceptionResolver
커스텀하게 특정 예외에 상태코드, 메시지, 응답 본문을 만들어 줄 수 있습니다.
![](https://velog.velcdn.com/images/loaded_diaper/post/20ce57e7-206f-4e91-a78a-264862fa36d1/image.png)
![](https://velog.velcdn.com/images/loaded_diaper/post/be9da2d4-e6a7-4403-b9eb-9d861d037780/image.png)



















































































** 출처: 내일배움 스파르타 코딩 클럽**

































































</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot 트러블 슈팅] `@ModelAttribute` DTO에 값이 안 들어와서 상태 필터 검증이 동작하지 않았던 문제]]></title>
            <link>https://velog.io/@loaded_diaper/Spring-Boot-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-ModelAttribute-DTO%EC%97%90-%EA%B0%92%EC%9D%B4-%EC%95%88-%EB%93%A4%EC%96%B4%EC%99%80%EC%84%9C-%EC%83%81%ED%83%9C-%ED%95%84%ED%84%B0-%EA%B2%80%EC%A6%9D%EC%9D%B4-%EB%8F%99%EC%9E%91%ED%95%98%EC%A7%80-%EC%95%8A%EC%95%98%EB%8D%98-%EB%AC%B8%EC%A0%9C-qupc12am</link>
            <guid>https://velog.io/@loaded_diaper/Spring-Boot-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-ModelAttribute-DTO%EC%97%90-%EA%B0%92%EC%9D%B4-%EC%95%88-%EB%93%A4%EC%96%B4%EC%99%80%EC%84%9C-%EC%83%81%ED%83%9C-%ED%95%84%ED%84%B0-%EA%B2%80%EC%A6%9D%EC%9D%B4-%EB%8F%99%EC%9E%91%ED%95%98%EC%A7%80-%EC%95%8A%EC%95%98%EB%8D%98-%EB%AC%B8%EC%A0%9C-qupc12am</guid>
            <pubDate>Tue, 28 Apr 2026 04:01:39 GMT</pubDate>
            <description><![CDATA[<p>주문 목록 조회 API를 구현하던 중,
분명히 잘못된 상태값을 보냈는데도 <code>400 Bad Request</code>가 아니라 <code>200 OK</code>가 반환되는 이상한 문제를 겪었다.</p>
<p>처음에는 상태 검증 로직이 잘못된 줄 알았는데,
원인은 전혀 다른 곳에 있었다.</p>
<hr>
<h2 id="문제-상황">문제 상황</h2>
<p>주문 목록 조회 API는 아래처럼 구현되어 있었다.</p>
<pre><code class="language-java">@GetMapping
public ResponseEntity&lt;OrderApiResponse&lt;OrderListResponseDto&gt;&gt; getOrders(
        @SessionAttribute(name = SessionConst.ADMIN_ID) Long adminId,
        @ModelAttribute OrderListSearchRequestDto requestDto
) {
    OrderListResponseDto responseDto = orderService.getOrders(adminId, requestDto);

    OrderApiResponse&lt;OrderListResponseDto&gt; response = OrderApiResponse.success(
            HttpStatus.OK.value(),
            &quot;주문 목록 조회 성공&quot;,
            responseDto
    );

    return ResponseEntity.ok(response);
}</code></pre>
<p>서비스에서는 <code>status</code> 값이 들어오면 enum 변환을 통해 유효한 상태값인지 검증하도록 작성해두었다.</p>
<pre><code class="language-java">OrderStatus orderStatus = null;

if (requestDto.getStatus() != null &amp;&amp; !requestDto.getStatus().isBlank()) {
    try {
        orderStatus = OrderStatus.valueOf(requestDto.getStatus());
    } catch (IllegalArgumentException exception) {
        throw new ApiException(ErrorCode.INVALID_ORDER_STATUS);
    }
}</code></pre>
<p>이 상태에서 아래 요청을 보냈다.</p>
<pre><code class="language-text">GET /api/orders?status=REA</code></pre>
<p><code>REA</code>는 유효한 주문 상태값이 아니므로,
당연히 <code>INVALID_ORDER_STATUS</code> 예외가 발생해서 <code>400 Bad Request</code>가 나와야 한다고 생각했다.</p>
<p>그런데 실제 결과는 <strong>200 OK</strong>였다.</p>
<hr>
<h2 id="처음에-의심했던-부분">처음에 의심했던 부분</h2>
<p>처음에는 서비스 로직이 이상하다고 생각했다.</p>
<ul>
<li><code>OrderStatus.valueOf(...)</code>가 잘못된 걸까?</li>
<li>예외 처리가 이상한 걸까?</li>
<li>혹시 <code>status</code> 검증이 호출되지 않는 걸까?</li>
</ul>
<p>코드만 계속 보고 있으면 원인을 찾기 어려웠다.
그래서 실제로 서비스에 어떤 값이 들어오는지 확인해보기로 했다.</p>
<hr>
<h2 id="원인-확인-방법">원인 확인 방법</h2>
<p><code>OrderService</code>의 <code>getOrders()</code> 메서드 맨 위에 아래 로그를 찍었다.</p>
<pre><code class="language-java">System.out.println(&quot;status = &quot; + requestDto.getStatus());
System.out.println(&quot;sortBy = &quot; + requestDto.getSortBy());
System.out.println(&quot;direction = &quot; + requestDto.getDirection());</code></pre>
<p>그리고 다시 같은 요청을 보냈다.</p>
<pre><code class="language-text">GET /api/orders?status=REA</code></pre>
<p>콘솔 결과는 이렇게 나왔다.</p>
<pre><code class="language-text">status = null
sortBy = orderedAt
direction = desc</code></pre>
<p>여기서 원인이 확실해졌다.</p>
<p><strong>쿼리파라미터로 <code>status=REA</code>를 보냈는데도 DTO 안에는 <code>null</code>로 들어오고 있었다.</strong></p>
<p>즉, 문제는 상태 검증 로직이 아니라
<strong>아예 <code>status</code> 값이 DTO에 바인딩되지 않고 있었다</strong>는 점이었다.</p>
<hr>
<h2 id="왜-이런-일이-발생했나">왜 이런 일이 발생했나</h2>
<p>문제의 DTO는 아래처럼 작성되어 있었다.</p>
<pre><code class="language-java">@Getter
@NoArgsConstructor
public class OrderListSearchRequestDto {

    private String keyword;
    private String status;

    @Min(value = 1, message = &quot;페이지 번호는 1 이상이어야 합니다.&quot;)
    private Integer page = 1;

    @Min(value = 1, message = &quot;페이지 크기는 1 이상이어야 합니다.&quot;)
    private Integer size = 10;

    private String sortBy = &quot;orderedAt&quot;;
    private String direction = &quot;desc&quot;;
}</code></pre>
<p>겉보기에는 문제가 없어 보였지만,
여기엔 <strong>setter가 없었다.</strong></p>
<p>이번 API는 <code>GET</code> 요청이고,
컨트롤러에서는 <code>@ModelAttribute</code>를 사용해서 쿼리파라미터를 DTO에 바인딩하고 있었다.</p>
<p>그런데 <code>@ModelAttribute</code> 방식으로 값을 채울 때는
현재 구조에서는 <strong>setter가 있어야 필드에 값이 정상적으로 들어갔다.</strong></p>
<p>즉,</p>
<ul>
<li><code>status=REA</code>를 요청으로 보냄</li>
<li>하지만 DTO에 setter가 없음</li>
<li>그래서 <code>status</code> 필드에 값이 안 들어감</li>
<li>결국 <code>requestDto.getStatus()</code>는 <code>null</code></li>
<li>상태 검증 로직이 실행되지 않음</li>
<li>전체 주문 목록이 그대로 조회됨</li>
</ul>
<p>이 흐름이었다.</p>
<hr>
<h2 id="왜-sortby-direction은-값이-있는-것처럼-보였을까">왜 <code>sortBy</code>, <code>direction</code>은 값이 있는 것처럼 보였을까</h2>
<p>처음에 <code>sortBy = orderedAt</code>, <code>direction = desc</code>가 찍혀서
바인딩이 되고 있는 줄 착각할 수 있었다.</p>
<p>하지만 이 값들은 요청에서 들어온 게 아니라,
DTO에 미리 설정해둔 <strong>기본값</strong>이었다.</p>
<pre><code class="language-java">private Integer page = 1;
private Integer size = 10;
private String sortBy = &quot;orderedAt&quot;;
private String direction = &quot;desc&quot;;</code></pre>
<p>즉,</p>
<ul>
<li><code>status</code>는 바인딩 실패 → <code>null</code></li>
<li><code>sortBy</code>, <code>direction</code>은 바인딩 성공이 아니라 <strong>기본값 출력</strong></li>
</ul>
<p>이었던 것이다.</p>
<p>이 부분 때문에 문제를 처음에 더 늦게 파악했다.</p>
<hr>
<h2 id="해결-방법">해결 방법</h2>
<p>해결은 간단했다.
DTO에 <code>@Setter</code>를 추가했다.</p>
<pre><code class="language-java">@Getter
@Setter
@NoArgsConstructor
public class OrderListSearchRequestDto {

    private String keyword;
    private String status;

    @Min(value = 1, message = &quot;페이지 번호는 1 이상이어야 합니다.&quot;)
    private Integer page = 1;

    @Min(value = 1, message = &quot;페이지 크기는 1 이상이어야 합니다.&quot;)
    private Integer size = 10;

    private String sortBy = &quot;orderedAt&quot;;
    private String direction = &quot;desc&quot;;
}</code></pre>
<p>그리고 다시 같은 요청을 보냈다.</p>
<pre><code class="language-text">GET /api/orders?status=REA</code></pre>
<p>이번에는 콘솔에 이렇게 찍혔다.</p>
<pre><code class="language-text">status = REA
sortBy = orderedAt
direction = desc</code></pre>
<p>이제는 <code>status</code> 값이 DTO에 정상적으로 들어왔고,
서비스의 상태값 검증 로직도 제대로 동작했다.</p>
<p>최종 응답도 기대한 대로 바뀌었다.</p>
<ul>
<li><code>400 Bad Request</code></li>
<li><code>errorCode = INVALID_ORDER_STATUS</code></li>
</ul>
<hr>
<h2 id="해결-전--해결-후">해결 전 / 해결 후</h2>
<h3 id="해결-전">해결 전</h3>
<ul>
<li><code>status=REA</code> 요청</li>
<li>DTO의 <code>status</code> 값이 <code>null</code></li>
<li>상태 검증 로직이 실행되지 않음</li>
<li>전체 주문 목록이 <code>200 OK</code>로 조회됨</li>
</ul>
<h3 id="해결-후">해결 후</h3>
<ul>
<li><code>status=REA</code> 요청</li>
<li>DTO의 <code>status</code> 값이 <code>&quot;REA&quot;</code>로 정상 바인딩됨</li>
<li><code>OrderStatus.valueOf(&quot;REA&quot;)</code>에서 예외 발생</li>
<li><code>INVALID_ORDER_STATUS</code>와 함께 <code>400 Bad Request</code> 반환</li>
</ul>
<hr>
<h2 id="이번-문제에서-배운-점">이번 문제에서 배운 점</h2>
<h3 id="1-로직보다-먼저-실제-입력값을-확인해야-한다">1. 로직보다 먼저 “실제 입력값”을 확인해야 한다</h3>
<p>처음에는 검증 코드가 틀린 줄 알았다.
하지만 실제 문제는 <strong>DTO에 값이 안 들어오는 것</strong>이었다.</p>
<p>코드를 오래 붙잡고 있기보다,
서비스에 어떤 값이 실제로 들어오는지 먼저 확인하는 게 훨씬 빠르다는 걸 배웠다.</p>
<hr>
<h3 id="2-requestbody와-modelattribute는-바인딩-방식이-다르다">2. <code>@RequestBody</code>와 <code>@ModelAttribute</code>는 바인딩 방식이 다르다</h3>
<p>이전에는 <code>POST</code> 요청에서 <code>@RequestBody</code>를 많이 다뤄서
DTO 바인딩을 비슷하게 생각하고 있었다.</p>
<p>하지만 이번 경험으로 차이를 더 분명히 이해했다.</p>
<ul>
<li><code>@RequestBody</code> → JSON 본문 바인딩</li>
<li><code>@ModelAttribute</code> → 쿼리파라미터 바인딩</li>
</ul>
<p>겉보기엔 둘 다 DTO를 받는 것 같지만,
실제로는 동작 방식이 다르다는 점을 체감했다.</p>
<hr>
<h3 id="3-기본값이-있으면-바인딩-문제를-놓치기-쉽다">3. 기본값이 있으면 바인딩 문제를 놓치기 쉽다</h3>
<p><code>sortBy</code>, <code>direction</code>이 계속 값이 찍히니까
처음에는 바인딩이 잘 되고 있다고 착각하기 쉬웠다.</p>
<p>하지만 그 값은 요청에서 들어온 값이 아니라 DTO 기본값이었다.</p>
<p>즉, 기본값이 들어간 DTO를 디버깅할 때는
<strong>“이 값이 진짜 요청에서 들어온 값인지, 그냥 기본값인지”를 꼭 구분해야 한다</strong>는 걸 배웠다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>이번 트러블 슈팅의 핵심은 이 한 문장으로 정리할 수 있다.</p>
<blockquote>
<p><code>@ModelAttribute</code>로 받는 DTO에 setter가 없어서 쿼리파라미터가 바인딩되지 않았고,
그 결과 상태값 검증 로직이 동작하지 않았다.</p>
</blockquote>
<p>처음에는 서비스 로직 문제처럼 보였지만,
실제로는 <strong>DTO 바인딩 문제</strong>였다.</p>
<p>작은 로그 출력 하나로 원인을 바로 확인할 수 있었고,
DTO에 <code>@Setter</code>를 추가해서 해결할 수 있었다.</p>
<hr>
<h2 id="최종-코드">최종 코드</h2>
<h3 id="controller">Controller</h3>
<pre><code class="language-java">@GetMapping
public ResponseEntity&lt;OrderApiResponse&lt;OrderListResponseDto&gt;&gt; getOrders(
        @SessionAttribute(name = SessionConst.ADMIN_ID) Long adminId,
        @ModelAttribute OrderListSearchRequestDto requestDto
) {
    OrderListResponseDto responseDto = orderService.getOrders(adminId, requestDto);

    OrderApiResponse&lt;OrderListResponseDto&gt; response = OrderApiResponse.success(
            HttpStatus.OK.value(),
            &quot;주문 목록 조회 성공&quot;,
            responseDto
    );

    return ResponseEntity.ok(response);
}</code></pre>
<h3 id="dto">DTO</h3>
<pre><code class="language-java">@Getter
@Setter
@NoArgsConstructor
public class OrderListSearchRequestDto {

    private String keyword;
    private String status;

    @Min(value = 1, message = &quot;페이지 번호는 1 이상이어야 합니다.&quot;)
    private Integer page = 1;

    @Min(value = 1, message = &quot;페이지 크기는 1 이상이어야 합니다.&quot;)
    private Integer size = 10;

    private String sortBy = &quot;orderedAt&quot;;
    private String direction = &quot;desc&quot;;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/9bc40de1-574a-4caf-8502-96bf08c420ff/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 에러 로그 읽는 법]]></title>
            <link>https://velog.io/@loaded_diaper/Spring-%EC%97%90%EB%9F%AC-%EB%A1%9C%EA%B7%B8-%EC%9D%BD%EB%8A%94-%EB%B2%95</link>
            <guid>https://velog.io/@loaded_diaper/Spring-%EC%97%90%EB%9F%AC-%EB%A1%9C%EA%B7%B8-%EC%9D%BD%EB%8A%94-%EB%B2%95</guid>
            <pubDate>Wed, 22 Apr 2026 06:25:33 GMT</pubDate>
            <description><![CDATA[<h1 id="trouble-shooting---스프링-에러-로그-읽는-법과-not-a-managed-type-문제">Trouble Shooting - 스프링 에러 로그 읽는 법과 <code>Not a managed type</code> 문제</h1>
<p>유저 생성 기능을 추가한 뒤 서버 실행 중 아래와 같은 에러가 발생했다.</p>
<pre><code class="language-text">Error starting ApplicationContext
Application run failed
...
Not a managed type: class com.schedule2.entity.User</code></pre>
<p>처음에는 맨 위의 <code>Application run failed</code> 만 보고 원인을 찾으려고 했는데,
이 문장은 단순히 <strong>“애플리케이션 실행이 실패했다”</strong> 는 요약일 뿐이었다.</p>
<p>실제 원인은 에러 로그를 아래로 내려가며 확인한 <code>Caused by</code> 부분에 있었다.</p>
<p>에러 흐름은 다음과 같았다.</p>
<ul>
<li><code>UserController</code> 생성 실패</li>
<li><code>UserService</code> 생성 실패</li>
<li><code>UserRepository</code> 생성 실패</li>
<li>최종 원인: <code>Not a managed type: class com.schedule2.entity.User</code></li>
</ul>
<p>즉, JPA가 <code>User</code> 클래스를 <strong>엔티티로 관리되는 타입</strong>으로 인식하지 못해서
<code>UserRepository</code>를 생성할 수 없었고, 그 결과로 서비스와 컨트롤러도 연쇄적으로 생성되지 못한 것이었다.</p>
<h3 id="원인">원인</h3>
<p><code>User</code> 엔티티가 JPA 엔티티로 제대로 등록되지 않은 상태였다.</p>
<h3 id="해결-방법">해결 방법</h3>
<p><code>User</code> 클래스에 엔티티 관련 설정이 제대로 들어가 있는지 확인했다.</p>
<pre><code class="language-java">@Entity
@Table(name = &quot;users&quot;)
public class User extends BaseEntity {</code></pre>
<p>그리고 저장/빌드 상태까지 다시 확인한 뒤 서버를 재실행하니 문제가 해결되었다.</p>
<h3 id="이번에-배운-점">이번에 배운 점</h3>
<p>스프링 에러 로그를 볼 때는 맨 위 요약 문장만 보면 안 되고,
반드시 아래쪽의 <code>Caused by</code> 와 <strong>가장 마지막의 구체적인 에러 문장</strong>을 확인해야 한다.</p>
<p>즉,</p>
<ul>
<li>위쪽: 어떤 작업이 실패했는지</li>
<li>아래쪽: 왜 실패했는지</li>
</ul>
<p>이 순서로 읽는 습관이 중요하다.</p>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/fc2d485e-e75b-4108-aa50-016c250f3f79/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 숙련 과제 트러블 슈팅]]></title>
            <link>https://velog.io/@loaded_diaper/Spring-%EC%88%99%EB%A0%A8-%EA%B3%BC%EC%A0%9C-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@loaded_diaper/Spring-%EC%88%99%EB%A0%A8-%EA%B3%BC%EC%A0%9C-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</guid>
            <pubDate>Wed, 22 Apr 2026 05:59:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/77af9889-6ed0-4fca-a874-3d8ea67b7495/image.png" alt=""></p>
<h1 id="trouble-shooting---getcreatedat--getupdatedat-메서드가-인식되지-않은-문제">Trouble Shooting - <code>getCreatedAt()</code> / <code>getUpdatedAt()</code> 메서드가 인식되지 않은 문제</h1>
<p>유저 생성 기능을 구현하면서 <code>UserService</code>에서 아래 코드를 작성했을 때 에러가 발생했다.</p>
<pre><code class="language-java">savedUser.getCreatedAt()
savedUser.getUpdatedAt()</code></pre>
<p>처음에는 <code>BaseEntity</code>에 <code>createdAt</code>, <code>updatedAt</code> 필드와 getter를 만들어 두었기 때문에
<code>User</code> 엔티티에서도 당연히 사용할 수 있을 것이라고 생각했다.</p>
<p>하지만 원인은 <code>User</code> 엔티티가 <code>BaseEntity</code>를 상속받고 있지 않았기 때문이었다.</p>
<p>기존 코드는 다음과 같았다.</p>
<pre><code class="language-java">public class User {</code></pre>
<p>이 상태에서는 <code>User</code> 클래스가 <code>BaseEntity</code>의 필드와 메서드를 물려받지 못하므로
<code>getCreatedAt()</code>과 <code>getUpdatedAt()</code>을 사용할 수 없다.</p>
<p>그래서 클래스 선언부를 아래처럼 수정했다.</p>
<pre><code class="language-java">public class User extends BaseEntity {</code></pre>
<p>수정 후에는 <code>User</code> 엔티티가 <code>BaseEntity</code>의 공통 필드와 메서드를 상속받게 되었고,
<code>createdAt</code>, <code>updatedAt</code>, <code>getCreatedAt()</code>, <code>getUpdatedAt()</code>을 정상적으로 사용할 수 있었다.</p>
<h3 id="정리">정리</h3>
<ul>
<li><code>getCreatedAt()</code>이 안 잡힌 이유: <code>User</code>가 <code>BaseEntity</code>를 상속하지 않았기 때문</li>
<li>해결 방법: <code>User extends BaseEntity</code>로 수정</li>
<li>배운 점: 공통 필드/공통 메서드를 부모 클래스에 만들었더라도, 자식 클래스가 <strong>상속</strong>하지 않으면 사용할 수 없다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/9370b2ba-602f-48b4-857f-c99b691b93cb/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[숙련 스프링 과제하면서 헷갈렸던 것들 ]]></title>
            <link>https://velog.io/@loaded_diaper/%EC%88%99%EB%A0%A8-%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B3%BC%EC%A0%9C%ED%95%98%EB%A9%B4%EC%84%9C-%ED%97%B7%EA%B0%88%EB%A0%B8%EB%8D%98-%EA%B2%83%EB%93%A4</link>
            <guid>https://velog.io/@loaded_diaper/%EC%88%99%EB%A0%A8-%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B3%BC%EC%A0%9C%ED%95%98%EB%A9%B4%EC%84%9C-%ED%97%B7%EA%B0%88%EB%A0%B8%EB%8D%98-%EA%B2%83%EB%93%A4</guid>
            <pubDate>Tue, 21 Apr 2026 11:35:40 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-java">package com.schedule2.service;

import com.schedule2.dto.ScheduleCreateRequestDto;
import com.schedule2.dto.ScheduleResponseDto;
import com.schedule2.entity.Schedule;
import com.schedule2.repository.ScheduleRepository;
import org.springframework.stereotype.Service;

// 일정 관련 비즈니스 로직을 처리하는 서비스 클래스
@Service
public class ScheduleService {

    // ==================== 속성 ====================

    // 일정 데이터를 데이터베이스에 저장하고 조회할 레포지토리
    private final ScheduleRepository scheduleRepository;


    // ==================== 생성자 ====================

    // 서비스가 레포지토리 객체를 전달받는 생성자
    public ScheduleService(ScheduleRepository scheduleRepository) {
        this.scheduleRepository = scheduleRepository;
    }


    // ==================== 기능 ====================

    // 일정 생성 기능
    public ScheduleResponseDto createSchedule(ScheduleCreateRequestDto requestDto) {

        // 1. 요청값 꺼내기
        String username = requestDto.getUsername();
        String title = requestDto.getTitle();
        String contents = requestDto.getContents();

        // 2. 요청값으로 Schedule 엔티티 만들기
        Schedule schedule = new Schedule(username, title, contents);

        // 3. DB에 저장하기
        Schedule savedSchedule = scheduleRepository.save(schedule);

        // 4. 저장된 결과를 응답 DTO로 만들기
        ScheduleResponseDto responseDto = new ScheduleResponseDto(
                savedSchedule.getId(),
                savedSchedule.getUsername(),
                savedSchedule.getTitle(),
                savedSchedule.getContents(),
                savedSchedule.getCreatedAt(),
                savedSchedule.getUpdatedAt()
        );

        // 5. 응답 DTO 반환하기
        return responseDto;
    }
}</code></pre>
<pre><code class="language-md">### `save()` 결과를 다시 변수에 담는 이유

```java
Schedule savedSchedule = scheduleRepository.save(schedule);</code></pre>
<p>처음 만든 <code>schedule</code> 객체는 아직 DB에 저장되기 전 상태라서
<code>id</code>, <code>createdAt</code>, <code>updatedAt</code> 같은 값이 비어 있을 수 있다.</p>
<p>하지만 <code>save()</code>로 DB에 저장하고 나면</p>
<ul>
<li><code>id</code>가 생성되고</li>
<li><code>createdAt</code>, <code>updatedAt</code> 같은 값도 채워질 수 있다.</li>
</ul>
<p>그래서 저장 결과를 <code>savedSchedule</code>에 다시 담아서
<strong>저장 후 최종 상태의 객체</strong>를 사용한다.</p>
<p>즉,</p>
<ul>
<li><code>schedule</code> = 저장 전 객체</li>
<li><code>savedSchedule</code> = 저장 후 값이 반영된 객체</li>
</ul>
<p>라고 이해하면 된다.</p>
<hr>
<pre><code class="language-java">// 일정 전체 조회 기능
public List&lt;ScheduleResponseDto&gt; getAllSchedules() {

    // 1. DB에서 일정 전체 조회하기
    List&lt;Schedule&gt; scheduleList = scheduleRepository.findAll();

    // 2. 응답 DTO들을 담을 빈 리스트 만들기
    List&lt;ScheduleResponseDto&gt; responseDtoList = new ArrayList&lt;&gt;();

    // 3. 조회한 일정들을 하나씩 꺼내서 응답 DTO로 바꾸기
    for (Schedule schedule : scheduleList) {

        ScheduleResponseDto responseDto = new ScheduleResponseDto(
                schedule.getId(),
                schedule.getUsername(),
                schedule.getTitle(),
                schedule.getContents(),
                schedule.getCreatedAt(),
                schedule.getUpdatedAt()
        );

        // 4. 만들어진 응답 DTO를 리스트에 담기
        responseDtoList.add(responseDto);
    }

    // 5. 최종 응답 리스트 반환하기
    return responseDtoList;
}</code></pre>
<h3 id="일정-전체-조회-로직-정리">일정 전체 조회 로직 정리</h3>
<p><code>getAllSchedules()</code> 메서드는 DB에서 일정 전체를 조회한 뒤,<br>그 결과를 그대로 반환하지 않고 <code>ScheduleResponseDto</code> 리스트로 바꿔서 반환한다.</p>
<p>흐름은 다음과 같다.</p>
<ol>
<li><code>scheduleRepository.findAll()</code>로 DB에 저장된 일정들을 모두 조회한다.</li>
<li>조회한 결과는 <code>List&lt;Schedule&gt;</code> 형태로 받아온다.</li>
<li>응답용 리스트(<code>List&lt;ScheduleResponseDto&gt;</code>)를 새로 만든다.</li>
<li>반복문을 사용해서 <code>Schedule</code> 객체를 하나씩 꺼낸다.</li>
<li>각 <code>Schedule</code> 객체를 <code>ScheduleResponseDto</code>로 변환한다.</li>
<li>변환한 DTO를 응답용 리스트에 담는다.</li>
<li>최종적으로 DTO 리스트를 반환한다.</li>
</ol>
<p>즉,<br><strong>엔티티 리스트를 그대로 주는 것이 아니라, 응답용 DTO 리스트로 변환해서 반환하는 과정</strong>이라고 이해하면 된다.</p>
<hr>
<h2 id="db-세팅">DB 세팅</h2>
<h3 id="ddl-auto-옵션-간단-정리"><code>ddl-auto</code> 옵션 간단 정리</h3>
<p>Spring JPA에서 <code>ddl-auto</code> 옵션은 애플리케이션 실행 시<br>테이블을 어떻게 다룰지 정하는 설정이다.</p>
<ul>
<li><p><code>create</code><br>실행할 때 테이블을 새로 생성한다.  <img src="https://velog.velcdn.com/images/loaded_diaper/post/494eb2b1-2a50-45ba-8cde-83ad2432bebe/image.png" alt=""></p>
<p>기존 데이터가 사라질 수 있어서 개발 중 테스트용으로만 주로 사용한다.</p>
</li>
<li><p><code>update</code><br>엔티티 변경 사항을 기준으로 테이블 구조를 맞춰준다.<br>기존 데이터는 유지되기 때문에 개발 중 가장 많이 사용한다.</p>
</li>
<li><p><code>none</code><br>JPA가 테이블 생성이나 수정에 관여하지 않는다.<br>DB를 직접 준비한 경우에 사용한다.</p>
</li>
<li><p><code>validate</code><br>엔티티와 테이블 구조가 맞는지만 검사한다.<br>테이블을 새로 만들거나 수정하지 않기 때문에 제출용이나 최종 점검용으로 많이 사용한다.</p>
</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 기본 활용]]></title>
            <link>https://velog.io/@loaded_diaper/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B8%B0%EB%B3%B8-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@loaded_diaper/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B8%B0%EB%B3%B8-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Thu, 16 Apr 2026 11:56:38 GMT</pubDate>
            <description><![CDATA[<h1 id="베이직-1회차">베이직 1회차</h1>
<h2 id="스프링이란-→-ioc-컨테이너">스프링이란? → IoC 컨테이너</h2>
<blockquote>
<p>스프링은 <code>IoC 컨테이너</code> 를 제공해주는 프레임워크입니다. 
→ 핵심 기능은 IoC 기반의 DI 을 제공하는 ApplicationContext 라는 IoC 컨테이너입니다.
<img src="https://velog.velcdn.com/images/loaded_diaper/post/0b77e2d1-8f8f-4cd5-8f1d-06756eca41eb/image.png" alt=""></p>
</blockquote>
<hr>
<h2 id="⭐-개념1---스프링컨테이너ioc-컨테이너">⭐ 개념1 - 스프링컨테이너(IoC 컨테이너)</h2>
<blockquote>
<p>애플리케이션이 시작될때 스프링컨테이너(IoC 컨테이너) 는 애플리케이션 실행에 필요한 객체를 생성하고 각 객체에 필요한 의존성을 주입합니다. 스프링컨테이너는 이렇게 생성한 객체를 미리 생성해두고 관리하기 때문에 요청이 들어오면 요청처리에 적합한 객체를 활용해 요청을 처리합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/dcdc7b14-3ce4-432d-aa1e-2cc9c5d25aae/image.png" alt="">
<img src="https://velog.velcdn.com/images/loaded_diaper/post/1f6d6334-9c71-4883-8568-602479025ca6/image.png" alt=""></p>
<hr>
<h2 id="⭐-개념2---스프링빈springbean">⭐ 개념2 - 스프링빈(SpringBean)</h2>
<blockquote>
<p>IoC 컨테이너에 의해서 생성되고 관리되는 객체를 스프링빈(SpringBean) 이라고 합니다.
<img src="https://velog.velcdn.com/images/loaded_diaper/post/ad12caf6-50e5-445b-b13e-dda0f507ccbb/image.png" alt=""></p>
</blockquote>
<h2 id="⭐-스프링기술---컴포넌트-스캔component-scan">⭐ 스프링기술 - 컴포넌트 스캔(Component Scan)</h2>
<blockquote>
<p>스프링의 자체 기능 - 자바 객체를 스프링 빈으로 등록하는 기술
컴포넌트스캔 을 활용해서 자바 클래스를 스프링 빈 등록 대상으로 지정할 수 있습니다. 
<img src="https://velog.velcdn.com/images/loaded_diaper/post/55d11edd-95a4-4bd8-a9cb-fc9e1fd11704/image.png" alt=""></p>
</blockquote>
<p>다양한 어노테이션 종류들</p>
<ul>
<li><code>@Component</code></li>
<li><code>@RestController</code></li>
<li><code>@Controller</code></li>
<li><code>@Service</code></li>
<li><code>@Repository</code></li>
<li>etc</li>
</ul>
<h2 id="⭐-web-기술---요청-연결-매핑request-mapping">⭐ web 기술 - 요청 연결 매핑(Request Mapping)</h2>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/a1eed98a-3b74-480f-a632-69cea8ea03a9/image.png" alt=""></p>
<ul>
<li><strong><code>@RequestMapping()</code></strong> - 보통 클래스 레벨에서 공통 경로 잡을 때 사용<ul>
<li><strong><code>@GetMapping()</code></strong> - <strong>조회</strong>. 데이터 가져올 때 (목록 조회, 상세 조회)</li>
<li><strong><code>@PostMapping()</code></strong> - <strong>생성</strong>. 새 데이터 만들 때 (회원가입, 글 작성)</li>
<li><strong><code>@PatchMapping()</code></strong> - <strong>부분 수정</strong>. 기존 데이터 일부만 바꿀 때 (이름만 변경)</li>
<li><strong><code>@DeleteMapping()</code></strong> - <strong>삭제</strong>. 데이터 지울 때 (글 삭제, 회원 탈퇴)</li>
</ul>
</li>
</ul>
<h2 id="⭐-web-기술---클라이언트에서-데이터를-받아오는-방법">⭐ web 기술 - 클라이언트에서 데이터를 받아오는 방법</h2>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/3eace1a3-ee44-445d-ae68-e833d0181e43/image.png" alt=""></p>
<ol>
<li>@RequestBody<blockquote>
<p>클라이언트가 HTTP 요청 바디로 데이터를 전송할때 사용해야합니다.</p>
</blockquote>
</li>
</ol>
<pre><code class="language-java">POST example.com/schedule</code></pre>
<pre><code class="language-java">&quot;title&quot;: &quot;코딩&quot;</code></pre>
<pre><code class="language-java"> @PostMapping(&quot;/requestbody&quot;)
    public String useRequestBody(@RequestBody DataTransferObject dto) {
        System.out.println(&quot;message = &quot; + dto.getMessage());
        return &quot;response 문자열&quot;;
    }</code></pre>
<ol start="2">
<li>@PathVariablle<blockquote>
<p>클라이언트가 URL 경로로 데이터를 전송할 때 사용해야합니다.</p>
</blockquote>
</li>
</ol>
<pre><code class="language-java">GET example.com/schedule/1</code></pre>
<pre><code class="language-java">@GetMapping(&quot;/schedule/{id}&quot;)
public String find(@PathVariable Long id) {
    return &quot;조회 ID: &quot; + id;
}</code></pre>
<ol start="3">
<li>@RequestParm<blockquote>
<p>클라이언트가 URL 쿼리 파라미터로 데이터를 전송할때 사용해야합니다.</p>
</blockquote>
<pre><code class="language-java">GET example.com/schedule?title=코딩</code></pre>
<pre><code class="language-java">@GetMapping(&quot;/schedule&quot;)
public String search(@RequestParam String title) {
 return &quot;검색어: &quot; + title;
}</code></pre>
</li>
</ol>
<h2 id="⭐-jpa-기술---데이터베이스를-다루는-방법">⭐ JPA 기술 - 데이터베이스를 다루는 방법</h2>
<p><strong>a. 엔티티</strong></p>
<blockquote>
<p>엔티티는 자바 클래스와 DB 테이블을 연결해주는 개념입니다.
<img src="https://velog.velcdn.com/images/loaded_diaper/post/1c60416e-440a-407d-8b10-14c6d6031680/image.png" alt=""></p>
</blockquote>
<pre><code class="language-java">@Entity</code></pre>
<p>b. repository</p>
<blockquote>
<p><code>JpaRepository</code> 를 활용해서 인터페이스를 만들면 스프링이 알아서 <code>ScheduleRepository</code>클래스를 구현해줍니다!</p>
</blockquote>
<pre><code class="language-java">public interface ScheduleRepository extends JpaRepository&lt;Schedule, Long&gt; {}</code></pre>
<ul>
<li>자동으로 구현된 기능:<ul>
<li>전체 조회 - <code>findAll()</code></li>
<li>단건조회 - <code>findById(…)</code></li>
<li>저장 - <code>save()</code></li>
<li>삭제 - <code>delete()</code></li>
</ul>
</li>
</ul>
<h2 id="⭐-3-계층">⭐ 3 계층</h2>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/e1719c90-23e4-4b39-b41e-ed0db416dcff/image.png" alt=""></p>
<h2 id="⭐-요약">⭐ 요약</h2>
<ul>
<li><p>스프링이 관리하는 객체를 <code>스프링 빈</code>이라고 한다.</p>
</li>
<li><p>스프링 빈을 보관하고 관리하는 곳을 <code>Ioc컨테이너</code>라고 한다.</p>
</li>
<li><p>@Component, @Service, @Repository 등이 붙은 클래스를 자동으로 찾아서 스프링 빈으로 등록하는 기술을 <code>컴포넌트스캔</code> 이라고 합니다.</p>
</li>
<li><p>클라이언트가 URL 경로에 데이터를 보낼 때 사용하는 어노테이션은 <code>@PathVariable</code> 이다. 
요청예시:  <code>GET /schedule/1</code></p>
</li>
<li><p>클라이언트가 HTTP 요청 바디로 데이터를 보낼 때 사용하는 어노테이션은 <code>@RequestBody</code> 이다. 
요청예시: <code>POST /schedule {&quot;title&quot;: &quot;코딩&quot;}</code></p>
</li>
<li><p>클라이언트가 쿼리 파라미터로 데이터를 보낼 때 사용하는 어노테이션은 <code>@RequestParam</code> 이다.
요청예시: <code>GET /schedule?title=&quot;코딩&quot;</code></p>
</li>
<li><p>자바 클래스와 DB 테이블을 연결해주는 설계도 역할을 하는 것을 <code>엔티티(entity)</code> 라고 합니다.</p>
</li>
</ul>
<hr>
<h1 id="베이직-2회차">베이직 2회차</h1>
<h2 id="객체간의-결합도를-통한-3계층-이해di-ioc">객체간의 결합도를 통한 3계층 이해(DI, IoC)</h2>
<h2 id="의존dependency">의존(Dependency)</h2>
<blockquote>
<p>객체지향 프로그래밍에서는 객체들이 서로 상호작용합니다. 이 과정에서 한 객체가 다른 객체를 필요하게 되는데 이 관계를 의존(Dependency) 라고 표현합니다.
<img src="https://velog.velcdn.com/images/loaded_diaper/post/0c39eb72-aacb-429a-8d4f-a6bfb7b4a08d/image.png" alt=""></p>
</blockquote>
<h2 id="⭐-의존성-주입dependency-injection">⭐ 의존성 주입(Dependency Injection)</h2>
<blockquote>
<p>Car 라는 객체가 필요로 하는 객체(의존객체: Engine) 를 외부에서 생성해서 Car 에 주입해주는 것을 의존성 주입이라고 합니다.
<img src="https://velog.velcdn.com/images/loaded_diaper/post/e04418df-5b34-43d0-a733-d711c4aef934/image.png" alt=""></p>
</blockquote>
<h2 id="⭐-객체간의-결합도">⭐ 객체간의 결합도</h2>
<blockquote>
<p>객체간의 의존성을 어떻게 설정해주느냐에 따라서 객체간의 결합도가 달라집니다.</p>
</blockquote>
<h3 id="1단계---직접-의존--스스로-생성">1단계 - 직접 의존 + 스스로 생성</h3>
<blockquote>
<p>필요한 의존 객체를 직접 생성하는 구조
<img src="https://velog.velcdn.com/images/loaded_diaper/post/3b345b7a-aace-4c95-903d-aca3fdb2a199/image.png" alt=""></p>
</blockquote>
<ul>
<li>강한 결합</li>
<li>변경에 취약</li>
</ul>
<h3 id="2단계---직접-의존--외부에서-생성">2단계 - 직접 의존 + 외부에서 생성</h3>
<blockquote>
<p>의존 객체를 외부에서 생성해서 전달받는 구조
<img src="https://velog.velcdn.com/images/loaded_diaper/post/9e36608a-4780-4c65-9a6f-0c86e6c18a63/image.png" alt=""></p>
</blockquote>
<ul>
<li>생성 책임이 외부로 분리됨</li>
<li>하지만 여전히 구체적인 클래스에 의존</li>
</ul>
<h3 id="3단계---추상화에-의존">3단계 - 추상화에 의존</h3>
<blockquote>
<p>구현체가 아니라 추상화(인터페이스)에 의존하는 구조
<img src="https://velog.velcdn.com/images/loaded_diaper/post/f52b4bfb-8448-4321-827a-8a9acc7c9bdc/image.png" alt=""></p>
</blockquote>
<ul>
<li>구현체 자유롭게 변경 가능</li>
<li>느슨한 결합</li>
</ul>
<h2 id="⭐-다형성">⭐ 다형성</h2>
<blockquote>
<p>하나의 형태(인터페이스/부모클래스)로 여러가지 형태(구현체) 를 다룰 수 있는 성질
<img src="https://velog.velcdn.com/images/loaded_diaper/post/056d607c-a407-4dc0-98a2-54e9ade527ec/image.png" alt=""></p>
</blockquote>
<h2 id="⭐-ioc제어-역전-inversion-of-control">⭐ IoC(제어 역전: Inversion of Control)</h2>
<blockquote>
<p>프로그램 제어흐름을 외부(제 3의 주체) 로 넘기는 설계원칙
→ 스프링에서는 IoC 개념을 컨테이너 형태로 제공해줍니다.</p>
</blockquote>
<h2 id="⭐-스프링의-의존성-주입-방법">⭐ 스프링의 의존성 주입 방법</h2>
<ol>
<li><p>생성자 주입</p>
<pre><code class="language-java">@Service
public class MemberService {...}</code></pre>
<pre><code class="language-java">@RestController
public class MemberController {
 private final MemberService memberService;

     @Autowired // 생략 가능
 public UserController(MemberService memberService) {
     this.memberService = memberService;
 }
}</code></pre>
<p>@Autowired생략가능</p>
</li>
<li><p>필드주입</p>
<pre><code class="language-java">@Service
public class MemberService {...}</code></pre>
<pre><code class="language-java">@RestController
public class MemberController {

     @Autowired
 private MemberService memberService;

 ...
}</code></pre>
</li>
<li><p>세터주입</p>
<pre><code class="language-java">@Service
public class MemberService {...}</code></pre>
<pre><code class="language-java">@RestController
public class MemberController {
 private MemberController memberService;

     @Autowired
 public void setMemberService(MemberController memberService) {
     this.memberService = memberService;
 }

     ...
}</code></pre>
</li>
</ol>
<h2 id="⭐-정리">⭐ 정리</h2>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/ba5752ad-558a-4c0f-bd6d-6fe647a716d4/image.png" alt=""></p>
<h2 id="⭐-요약-1">⭐ 요약</h2>
<ul>
<li><p>하나의 데이터타입으로 다양한 구현체를 다룰 수 있는 성질을 <code>다형성</code> 이라고 합니다.</p>
</li>
<li><p>프로그램 제어흐름을 외부로 넘기는 설계 원칙을 <code>제어 역전(IoC: Inversion of Control)</code>  라고 합니다.</p>
</li>
<li><p>구현체가 아닌 인터페이스에 의존하면 Car 코드를 수정하지 않고도 Engine 을 자유롭게 교체할 수 있었습니다. 이를 <code>느슨한 결합</code>  결합이라고 합니다.</p>
</li>
<li><p>객체가 필요로 하는 B객체(의존객체) 를 외부에서 생성해서 주입해주는 것을 <code>의존성주입(DI: dependency Injection)</code> 이라고 합니다.</p>
</li>
<li><p><code>private Engine engine = new ElectricEngine();</code> 처럼 의존 객체를 직접 생성하면 엔진을 교체하려고 할때 코드를 수정해야합니다. 이를 <code>강한 결합</code>  결합이라고 합니다.</p>
</li>
</ul>
<p>출처: 내일배움스파르타</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스파르타 15일차]]></title>
            <link>https://velog.io/@loaded_diaper/%EC%8A%A4%ED%8C%8C%EB%A5%B4%ED%83%80-15%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@loaded_diaper/%EC%8A%A4%ED%8C%8C%EB%A5%B4%ED%83%80-15%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 30 Mar 2026 08:38:41 GMT</pubDate>
            <description><![CDATA[<h1 id="객체지향-설계에서-무분별한-setter를-지양해야-하는-이유">객체지향 설계에서 무분별한 Setter를 지양해야 하는 이유</h1>
<p>오늘은 자바 객체지향 과제를 진행하면서 <code>Setter</code>를 무조건 만드는 것이 꼭 좋은 것은 아니라는 점을 다시 정리하게 되었다.
처음에는 클래스 필드를 만들면 자연스럽게 <code>Getter</code>, <code>Setter</code>를 전부 작성하는 습관이 있었는데, 과제를 진행하고 피드백을 들으면서 <strong>중요한 값은 함부로 바뀌지 않도록 설계하는 것이 더 객체지향적</strong>이라는 점을 이해하게 되었다.</p>
<hr>
<h2 id="1-처음에는-왜-setter를-다-만들게-될까">1. 처음에는 왜 Setter를 다 만들게 될까?</h2>
<p>자바를 처음 배우면 보통 이렇게 배운다.</p>
<ul>
<li>필드는 <code>private</code>으로 숨긴다</li>
<li><code>Getter</code>로 값을 조회한다</li>
<li><code>Setter</code>로 값을 수정한다</li>
</ul>
<p>그래서 자연스럽게 모든 필드에 대해 <code>Getter</code>, <code>Setter</code>를 전부 만드는 습관이 생긴다.</p>
<p>예를 들어 <code>Product</code> 클래스가 있으면 이렇게 작성하기 쉽다.</p>
<pre><code class="language-java">public class Product {

    private String productName;
    private long productPrice;
    private String productDescription;
    private int productStock;

    public Product(String productName, long productPrice, String productDescription, int productStock) {
        this.productName = productName;
        this.productPrice = productPrice;
        this.productDescription = productDescription;
        this.productStock = productStock;
    }

    public String getProductName() {
        return productName;
    }

    public void setProductName(String productName) {
        this.productName = productName;
    }

    public long getProductPrice() {
        return productPrice;
    }

    public void setProductPrice(long productPrice) {
        this.productPrice = productPrice;
    }

    public String getProductDescription() {
        return productDescription;
    }

    public void setProductDescription(String productDescription) {
        this.productDescription = productDescription;
    }

    public int getProductStock() {
        return productStock;
    }

    public void setProductStock(int productStock) {
        this.productStock = productStock;
    }
}</code></pre>
<p>처음에는 이 방식이 정석처럼 보인다.
하지만 이렇게 하면 <strong>객체의 중요한 정보가 외부에서 너무 쉽게 변경될 수 있다.</strong></p>
<hr>
<h2 id="2-무분별한-setter가-왜-문제일까">2. 무분별한 Setter가 왜 문제일까?</h2>
<p>예를 들어 상품 가격, 상품명, 재고는 중요한 데이터다.
그런데 Setter를 전부 열어두면 밖에서 이렇게 바꿀 수 있다.</p>
<pre><code class="language-java">product.setProductPrice(-10000);
product.setProductName(&quot;이상한 상품명&quot;);
product.setProductStock(-5);</code></pre>
<p>이렇게 되면 객체가 스스로 자신의 상태를 지키지 못한다.
즉, <strong>객체가 안전하지 않다.</strong></p>
<p>객체지향에서는 단순히 필드를 숨기는 것만 중요한 것이 아니라,
<strong>그 값을 누가, 언제, 어떤 방식으로 바꿀 수 있는지까지 통제하는 것</strong>이 중요하다.</p>
<hr>
<h2 id="3-getter는-조회-setter는-수정이다">3. Getter는 조회, Setter는 수정이다</h2>
<p>오늘 다시 정리한 중요한 포인트는 이것이다.</p>
<ul>
<li><code>Getter</code>는 객체의 값을 읽기 위해 사용한다</li>
<li><code>Setter</code>는 객체의 값을 바꾸기 위해 사용한다</li>
</ul>
<p>문제는 “조회”보다 “수정”이 훨씬 위험하다는 점이다.</p>
<p>값을 읽는 것은 비교적 안전하지만,
값을 수정하는 순간 객체 상태가 달라진다.
그래서 수정 메서드는 정말 필요한 경우에만 열어두는 것이 좋다.</p>
<hr>
<h2 id="4-현재-단계에서는-setter가-꼭-필요하지-않았다">4. 현재 단계에서는 Setter가 꼭 필요하지 않았다</h2>
<p>이번 커머스 과제에서 만든 클래스들은 대략 이런 역할을 가진다.</p>
<ul>
<li><code>Product</code> : 상품 1개 정보</li>
<li><code>Category</code> : 카테고리 이름 + 상품 목록</li>
<li><code>Customer</code> : 고객 정보</li>
<li><code>CommerceSystem</code> : 프로그램 흐름 제어</li>
</ul>
<p>이 단계에서 대부분의 객체는 <strong>조회</strong>가 중심이었다.</p>
<p>예를 들면:</p>
<ul>
<li>상품명을 출력한다</li>
<li>가격을 출력한다</li>
<li>설명을 출력한다</li>
<li>카테고리 이름을 출력한다</li>
<li>고객 정보를 확인한다</li>
</ul>
<p>즉, 값을 읽는 것은 많이 하지만
중간에 값을 계속 수정할 필요는 거의 없었다.</p>
<p>그래서 <code>Setter</code>를 모두 둘 필요가 없다고 판단했다.</p>
<hr>
<h2 id="5-불필요한-setter를-제거한-방향">5. 불필요한 Setter를 제거한 방향</h2>
<p>예를 들어 <code>Category</code>는 생성될 때 이름과 상품 목록이 정해지면,
실행 중에 굳이 계속 이름을 바꿀 필요가 없다.</p>
<p>그래서 이런 식으로 최소한의 구조로 가져가는 것이 더 자연스럽다.</p>
<pre><code class="language-java">public class Category {

    private String categoryName;
    private List&lt;Product&gt; products;

    public Category(String categoryName, List&lt;Product&gt; products) {
        this.categoryName = categoryName;
        this.products = products;
    }

    public String getCategoryName() {
        return categoryName;
    }

    public List&lt;Product&gt; getProducts() {
        return products;
    }
}</code></pre>
<p><code>Customer</code>도 마찬가지다.</p>
<pre><code class="language-java">public class Customer {

    private String customerName;
    private String email;
    private String grade;

    public Customer(String customerName, String email, String grade) {
        this.customerName = customerName;
        this.email = email;
        this.grade = grade;
    }

    public String getCustomerName() {
        return customerName;
    }

    public String getEmail() {
        return email;
    }

    public String getGrade() {
        return grade;
    }
}</code></pre>
<p>이렇게 하면 객체를 만들 때 필요한 값을 넣고,
그 이후에는 <strong>필요한 정보만 조회</strong>하는 구조가 된다.</p>
<hr>
<h2 id="6-setter-대신-더-의미-있는-메서드를-만드는-것이-좋다">6. Setter 대신 더 의미 있는 메서드를 만드는 것이 좋다</h2>
<p>오늘 공부하면서 가장 인상 깊었던 부분은
<strong>무조건 <code>setXXX()</code>를 만드는 것보다, 의도가 드러나는 메서드를 만드는 것이 더 좋다</strong>는 점이다.</p>
<p>예를 들어 상품 재고는 나중에 주문 기능이 생기면 바뀔 수 있다.
그런데 이렇게 작성하는 것보다:</p>
<pre><code class="language-java">product.setProductStock(29);</code></pre>
<p>이렇게 작성하는 것이 훨씬 자연스럽다.</p>
<pre><code class="language-java">product.decreaseStock(1);</code></pre>
<p>왜냐하면 <code>setProductStock(29)</code>는
그 값이 왜 29가 되었는지 문맥이 잘 드러나지 않는다.</p>
<p>반면 <code>decreaseStock(1)</code>은
<strong>“재고를 1개 줄인다”</strong>는 의도가 명확하다.</p>
<p>즉, Setter는 단순 수정이고
의미 있는 메서드는 <strong>행동 중심 설계</strong>에 더 가깝다.</p>
<hr>
<h2 id="7-캡슐화와도-연결된다">7. 캡슐화와도 연결된다</h2>
<p>이번 과제 Step 4가 캡슐화였는데,
Setter를 줄이는 방향은 캡슐화와도 연결된다.</p>
<p>캡슐화는 단순히 <code>private</code>을 붙이는 것만이 아니라
<strong>객체 내부 상태를 안전하게 관리하는 것</strong>이다.</p>
<p>즉:</p>
<ul>
<li>필드는 <code>private</code>으로 숨기고</li>
<li>필요한 값은 <code>Getter</code>로 조회하고</li>
<li>수정은 꼭 필요한 경우에만 허용하고</li>
<li>가능하면 의미 있는 행동 메서드로 표현한다</li>
</ul>
<p>이런 방식이 더 좋은 캡슐화라고 느꼈다.</p>
<hr>
<h2 id="8-오늘-정리한-기준">8. 오늘 정리한 기준</h2>
<p>앞으로는 필드를 만들 때 무조건 Setter를 만들지 않고,
아래 기준으로 생각해보려고 한다.</p>
<h3 id="setter가-필요-없는-경우">Setter가 필요 없는 경우</h3>
<ul>
<li>생성 시 값이 정해지고 이후 잘 바뀌지 않는 경우</li>
<li>외부에서 함부로 수정되면 안 되는 중요한 정보</li>
<li>현재 단계에서 조회만 필요한 경우</li>
</ul>
<h3 id="setter-대신-다른-메서드가-더-좋은-경우">Setter 대신 다른 메서드가 더 좋은 경우</h3>
<ul>
<li>값 수정에 명확한 의도가 있는 경우</li>
<li>단순 대입이 아니라 “행동”을 표현해야 하는 경우</li>
</ul>
<p>예:</p>
<ul>
<li><code>setProductStock()</code> 보다 <code>decreaseStock()</code></li>
<li><code>setOrderStatus()</code> 보다 <code>completeOrder()</code></li>
</ul>
<hr>
<h2 id="9-오늘-느낀-점">9. 오늘 느낀 점</h2>
<p>예전에는 <code>Getter</code>, <code>Setter</code>를 모두 만드는 것이 좋은 습관이라고만 생각했다.
하지만 오늘은 <strong>“모든 Setter가 좋은 것은 아니다”</strong>라는 점을 분명히 이해하게 되었다.</p>
<p>객체지향에서 중요한 것은 단순히 클래스를 나누는 것이 아니라,
<strong>객체가 자신의 데이터를 어떻게 지키는지 설계하는 것</strong>이었다.</p>
<p>특히 이번 커머스 과제를 하면서
<code>Product</code>, <code>Category</code>, <code>Customer</code> 같은 클래스는 지금 단계에서 조회가 중심이기 때문에
불필요한 Setter를 열어두지 않는 것이 더 적절하다고 느꼈다.</p>
<hr>
<h2 id="10-한-줄-요약">10. 한 줄 요약</h2>
<p><strong>Setter는 무조건 만드는 것이 아니라, 정말 필요한 경우에만 열어두고, 가능하면 객체의 의도가 드러나는 메서드로 설계하는 것이 더 객체지향적이다.</strong></p>
<hr>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/d05666f4-9fec-4957-b336-8be87fd81578/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스파르타 14일차]]></title>
            <link>https://velog.io/@loaded_diaper/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%A0%81-%EC%BD%94%EB%94%A9%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@loaded_diaper/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%A0%81-%EC%BD%94%EB%94%A9%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Fri, 27 Mar 2026 11:49:45 GMT</pubDate>
            <description><![CDATA[<p>객체지향적 코딩에 대해서 생각해보았다
코딩을 하면서 처음으로 생각해 본 듯?
<img src="https://velog.velcdn.com/images/loaded_diaper/post/10f4692b-d27f-4538-a590-aa7192e152fd/image.png" alt=""></p>
<hr>
<h2 id="생성자-주입이-왜-좋은가">생성자 주입이 왜 좋은가?</h2>
<p>이전에는 <code>Scanner</code>를 <code>CommerceSystem</code> 안에서 직접 생성할지,
아니면 <code>Main</code>에서 만들어서 넘길지 고민했다.</p>
<p>예를 들면 이런 두 방식이 있다.</p>
<h3 id="1-클래스-내부에서-직접-생성하는-방식">1. 클래스 내부에서 직접 생성하는 방식</h3>
<pre><code class="language-java">public class CommerceSystem {

    private Scanner scanner = new Scanner(System.in);

}</code></pre>
<h3 id="2-생성자로-전달받는-방식">2. 생성자로 전달받는 방식</h3>
<pre><code class="language-java">public class CommerceSystem {

    private Scanner scanner;

    public CommerceSystem(Scanner scanner) {
        this.scanner = scanner;
    }
}</code></pre>
<p>이 두 방식 중에서 객체지향적으로 더 깔끔한 방법은
<strong>생성자 주입 방식</strong>이다.</p>
<hr>
<h2 id="1-생성자-주입이란">1. 생성자 주입이란?</h2>
<p>생성자 주입은
<strong>클래스가 필요한 객체를 스스로 만들지 않고, 밖에서 전달받는 방식</strong>이다.</p>
<p>예를 들면:</p>
<pre><code class="language-java">Scanner scanner = new Scanner(System.in);
CommerceSystem commerceSystem = new CommerceSystem(scanner);</code></pre>
<p>이 구조는 이런 흐름이다.</p>
<ol>
<li><code>Main</code>이 필요한 객체를 생성한다.</li>
<li><code>CommerceSystem</code>은 생성자를 통해 그 객체를 전달받는다.</li>
<li>전달받은 객체를 필드에 저장해서 사용한다.</li>
</ol>
<p>즉, <code>CommerceSystem</code>이 <code>Scanner</code>를 직접 만들지 않고
<strong>이미 준비된 <code>Scanner</code>를 받는 구조</strong>다.</p>
<hr>
<h2 id="2-왜-직접-생성하지-않고-전달받는-것이-좋을까">2. 왜 직접 생성하지 않고 전달받는 것이 좋을까?</h2>
<h3 id="이유-1-역할이-더-분명해진다">이유 1. 역할이 더 분명해진다</h3>
<p><code>Main</code>은 프로그램 시작점이고,
필요한 객체를 준비하는 역할을 맡는다.</p>
<p><code>CommerceSystem</code>은
준비된 객체를 받아서 실제 기능을 수행하는 역할을 맡는다.</p>
<p>즉 역할이 이렇게 나뉜다.</p>
<ul>
<li><code>Main</code> : 준비 담당</li>
<li><code>CommerceSystem</code> : 실행 담당</li>
</ul>
<p>이 구조가 더 자연스럽다.</p>
<p>만약 <code>CommerceSystem</code> 안에서 <code>Scanner</code>를 직접 만든다면,
<code>CommerceSystem</code>은 기능 수행뿐 아니라
객체 생성까지 같이 하게 된다.</p>
<p>그러면 역할이 조금 섞인다.</p>
<hr>
<h3 id="이유-2-코드가-더-일관적이다">이유 2. 코드가 더 일관적이다</h3>
<p>이미 <code>CommerceSystem</code>은 <code>products</code>도 밖에서 받고 있다.</p>
<pre><code class="language-java">public CommerceSystem(List&lt;Product&gt; products, Scanner scanner) {
    this.products = products;
    this.scanner = scanner;
}</code></pre>
<p>즉 <code>products</code>는 밖에서 받고, <code>scanner</code>만 내부에서 직접 만들면
설계가 통일되지 않는다.</p>
<p>반면 둘 다 밖에서 전달받으면 흐름이 깔끔하다.</p>
<ul>
<li>필요한 객체는 모두 <code>Main</code>이 준비한다.</li>
<li><code>CommerceSystem</code>은 전달받은 것만 사용한다.</li>
</ul>
<p>이런 구조가 더 읽기 쉽고 설계 의도가 분명하다.</p>
<hr>
<h3 id="이유-3-나중에-변경하기-쉬워진다">이유 3. 나중에 변경하기 쉬워진다</h3>
<p>처음에는 <code>Scanner(System.in)</code>을 쓸 수 있다.
그런데 나중에는 입력 방식이 달라질 수도 있다.</p>
<p>예를 들어:</p>
<ul>
<li>파일에서 입력받기</li>
<li>테스트용 입력값 사용하기</li>
<li>다른 입력 객체로 변경하기</li>
</ul>
<p>만약 <code>CommerceSystem</code> 안에서 직접 <code>new Scanner(System.in)</code>을 써버리면,
입력 방식을 바꾸려면 <code>CommerceSystem</code> 코드를 직접 수정해야 한다.</p>
<p>하지만 생성자 주입 구조라면
<code>CommerceSystem</code>은 그대로 두고,
<code>Main</code>에서 어떤 <code>Scanner</code>를 넘길지만 바꾸면 된다.</p>
<p>즉 <strong>변경에 더 유연하다.</strong></p>
<hr>
<h3 id="이유-4-테스트하기-쉬워진다">이유 4. 테스트하기 쉬워진다</h3>
<p>지금 단계에서는 테스트 코드까지 깊게 안 가도 되지만,
구조적으로는 이것도 큰 장점이다.</p>
<p>직접 생성하는 방식은:</p>
<pre><code class="language-java">private Scanner scanner = new Scanner(System.in);</code></pre>
<p>이렇게 고정돼 있어서 테스트할 때 다루기 불편하다.</p>
<p>반면 생성자 주입은:</p>
<pre><code class="language-java">public CommerceSystem(Scanner scanner) {
    this.scanner = scanner;
}</code></pre>
<p>이렇게 되어 있으니,
테스트할 때 원하는 입력 객체를 넣어볼 수 있다.</p>
<p>즉 <strong>외부에서 조립 가능하다</strong>는 점이 장점이다.</p>
<hr>
<h2 id="3-직접-생성-방식과-생성자-주입-방식-비교">3. 직접 생성 방식과 생성자 주입 방식 비교</h2>
<h3 id="직접-생성-방식">직접 생성 방식</h3>
<pre><code class="language-java">public class CommerceSystem {

    private Scanner scanner = new Scanner(System.in);

}</code></pre>
<p>이 방식의 특징:</p>
<ul>
<li>편해 보인다</li>
<li>바로 쓸 수 있다</li>
<li>하지만 클래스가 도구를 직접 만들고 있다</li>
<li>변경과 테스트에 불리하다</li>
</ul>
<hr>
<h3 id="생성자-주입-방식">생성자 주입 방식</h3>
<pre><code class="language-java">public class CommerceSystem {

    private Scanner scanner;

    public CommerceSystem(Scanner scanner) {
        this.scanner = scanner;
    }
}</code></pre>
<p>이 방식의 특징:</p>
<ul>
<li>처음엔 조금 더 길어 보인다</li>
<li>하지만 구조가 더 깔끔하다</li>
<li>필요한 객체를 외부에서 조립할 수 있다</li>
<li>역할 분리가 명확하다</li>
</ul>
<hr>
<h2 id="4-현재-프로젝트에-적용하면">4. 현재 프로젝트에 적용하면</h2>
<h3 id="mainjava">Main.java</h3>
<pre><code class="language-java">Scanner scanner = new Scanner(System.in);

CommerceSystem commerceSystem = new CommerceSystem(products, scanner);
commerceSystem.start();

scanner.close();</code></pre>
<h3 id="commercesystemjava">CommerceSystem.java</h3>
<pre><code class="language-java">private List&lt;Product&gt; products;
private Scanner scanner;

public CommerceSystem(List&lt;Product&gt; products, Scanner scanner) {
    this.products = products;
    this.scanner = scanner;
}</code></pre>
<p>이 구조의 장점은 명확하다.</p>
<ul>
<li><code>Main</code>이 필요한 객체를 준비한다.</li>
<li><code>CommerceSystem</code>은 전달받은 객체를 사용한다.</li>
<li>설계가 일관적이다.</li>
</ul>
<hr>
<h2 id="5-초보자-눈높이로-이해하면">5. 초보자 눈높이로 이해하면</h2>
<p>이걸 아주 쉽게 생각하면:</p>
<h3 id="직접-생성-방식-1">직접 생성 방식</h3>
<p><code>CommerceSystem</code>이 말한다.</p>
<blockquote>
<p>“상품도 내가 받고, Scanner는 내가 직접 만들게.”</p>
</blockquote>
<h3 id="생성자-주입-방식-1">생성자 주입 방식</h3>
<p><code>Main</code>이 말한다.</p>
<blockquote>
<p>“필요한 재료는 내가 다 준비해줄게.
너는 그걸 받아서 일만 하면 돼.”</p>
</blockquote>
<p>두 번째가 더 깔끔하지 않나?</p>
<p>즉 생성자 주입은
<strong>필요한 재료를 밖에서 넣어주는 방식</strong>이라고 생각하면 된다.</p>
<hr>
<h2 id="6-생성자-주입의-핵심-정리">6. 생성자 주입의 핵심 정리</h2>
<p>생성자 주입이 좋은 이유는 다음과 같다.</p>
<ul>
<li>역할 분리가 더 명확하다</li>
<li>객체 생성 책임이 <code>Main</code>에 모인다</li>
<li>설계가 일관적이다</li>
<li>변경에 유연하다</li>
<li>테스트하기 쉽다</li>
</ul>
<hr>
<h2 id="7-내가-이번에-이해한-점">7. 내가 이번에 이해한 점</h2>
<p>이번 과제를 하면서 느낀 점은,
단순히 “동작만 하면 된다”가 아니라
<strong>누가 만들고, 누가 받고, 누가 사용하는지</strong>를 나누는 것이 중요하다는 점이었다.</p>
<p>특히 <code>Scanner</code>처럼 작은 객체 하나도
어디서 생성하는지가 설계 차이를 만든다는 걸 알게 됐다.</p>
<p>처음에는 <code>CommerceSystem</code> 안에서 직접 <code>new Scanner(System.in)</code>을 해도 된다고 생각했지만,
생성자 주입 방식으로 바꾸고 나니
코드의 역할이 더 분명해지고 구조도 더 자연스러워졌다.</p>
<hr>
<h2 id="한-줄-요약">한 줄 요약</h2>
<p><strong>생성자 주입은 클래스가 필요한 객체를 직접 만들지 않고, 밖에서 전달받아 사용하는 방식이며, 역할 분리와 유지보수 측면에서 더 좋은 설계이다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스파르타 12일차]]></title>
            <link>https://velog.io/@loaded_diaper/%EC%8A%A4%ED%8C%8C%EB%A5%B4%ED%83%80-12%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@loaded_diaper/%EC%8A%A4%ED%8C%8C%EB%A5%B4%ED%83%80-12%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 25 Mar 2026 06:33:10 GMT</pubDate>
            <description><![CDATA[<h1 id="java-계산기-과제에서-제네릭과-number-이해하기">Java 계산기 과제에서 제네릭과 Number 이해하기</h1>
<h2 id="정수-전용-계산기에서-실수까지-처리하도록-확장해보기">정수 전용 계산기에서 실수까지 처리하도록 확장해보기</h2>
<p>계산기 과제를 진행하면서 처음에는 <code>int</code>만 사용하는 계산기를 만들었다.
즉, 정수끼리만 계산할 수 있는 구조였다.</p>
<p>예를 들어 이런 입력은 가능했다.</p>
<ul>
<li><code>1 + 2</code></li>
<li><code>10 - 3</code></li>
<li><code>5 * 4</code></li>
</ul>
<p>하지만 이런 입력은 처리할 수 없었다.</p>
<ul>
<li><code>1.5 + 2.3</code></li>
<li><code>5 / 2 = 2.5</code></li>
</ul>
<p>즉, 기존 계산기는 <strong>정수 전용 계산기</strong>였다.</p>
<p>그런데 과제를 진행하면서
<strong>실수도 받을 수 있게 확장하라</strong>,
그리고 <strong>제네릭을 활용하라</strong>는 요구사항이 나왔다.</p>
<p>처음에는
“그냥 <code>int</code>를 <code>double</code>로 바꾸면 되는 거 아닌가?”
라고 생각했지만, 과제의 핵심은 단순히 실수 계산이 아니라
<strong>여러 숫자 타입을 더 유연하게 받을 수 있는 구조를 이해하는 것</strong>이었다.</p>
<p>이번 글에서는 계산기 과제를 통해 배운
<strong>제네릭과 Number의 역할</strong>,
그리고 <strong>왜 <code>doubleValue()</code>를 사용했는지</strong>를 정리해보려고 한다.</p>
<hr>
<h2 id="1-처음-계산기는-왜-한계가-있었을까">1. 처음 계산기는 왜 한계가 있었을까?</h2>
<p>기존 계산 메서드는 이런 느낌이었다.</p>
<pre><code class="language-java">public int calculate(int a, int b, OperatorType operatorType) {
    int result = 0;

    if (operatorType == OperatorType.ADD) {
        result = a + b;
    } else if (operatorType == OperatorType.SUBTRACT) {
        result = a - b;
    } else if (operatorType == OperatorType.MULTIPLY) {
        result = a * b;
    } else if (operatorType == OperatorType.DIVIDE) {
        result = a / b;
    }

    return result;
}</code></pre>
<p>이 구조의 문제는 명확했다.</p>
<ul>
<li><code>a</code>, <code>b</code>가 <code>int</code>라서 정수만 받을 수 있다</li>
<li>나눗셈 결과도 정수로 잘릴 수 있다</li>
<li>실수 입력인 <code>1.2</code>, <code>2.5</code>를 받을 수 없다</li>
</ul>
<p>예를 들어:</p>
<pre><code class="language-java">5 / 2</code></pre>
<p>를 계산하면 수학적으로는 <code>2.5</code>여야 하지만,
정수 계산에서는 <code>2</code>가 나와버린다.</p>
<p>즉, <strong>정수 타입에 너무 묶여 있는 구조</strong>였다.</p>
<hr>
<h2 id="2-그럼-그냥-double로-바꾸면-되지-않을까">2. 그럼 그냥 double로 바꾸면 되지 않을까?</h2>
<p>처음에는 이런 생각을 했다.</p>
<pre><code class="language-java">public double calculate(double a, double b, OperatorType operatorType)</code></pre>
<p>이렇게 바꾸면 정수도 받을 수 있고, 실수도 받을 수 있다.</p>
<p>실제로 이 방식도 동작은 한다.
하지만 과제에서는 여기서 한 걸음 더 나아가
<strong>제네릭을 사용해서 여러 숫자 타입을 받을 수 있는 구조</strong>를 이해하는 것이 중요했다.</p>
<p>즉 핵심은</p>
<blockquote>
<p>단순히 <code>double 계산기</code>를 만드는 것이 아니라,
<strong>숫자 타입에 유연한 계산기 구조</strong>를 만드는 것</p>
</blockquote>
<p>이었다.</p>
<hr>
<h2 id="3-제네릭generic이란">3. 제네릭(Generic)이란?</h2>
<p>제네릭은 쉽게 말하면</p>
<blockquote>
<p><strong>타입을 나중에 정하는 문법</strong></p>
</blockquote>
<p>이다.</p>
<p>예를 들어 아래 코드를 보자.</p>
<pre><code class="language-java">public class Box&lt;T&gt; {
    private T item;
}</code></pre>
<p>여기서 <code>T</code>는 아직 정해지지 않은 타입이다.</p>
<p>나중에 사용할 때</p>
<ul>
<li><code>Box&lt;String&gt;</code></li>
<li><code>Box&lt;Integer&gt;</code></li>
</ul>
<p>처럼 실제 타입을 넣어서 사용할 수 있다.</p>
<p>즉 <code>T</code>는 일종의 <strong>임시 타입 이름</strong>이다.</p>
<hr>
<h2 id="4-계산기에서-왜-제네릭이-필요했을까">4. 계산기에서 왜 제네릭이 필요했을까?</h2>
<p>계산기의 입력값은 이제</p>
<ul>
<li>정수일 수도 있고</li>
<li>실수일 수도 있다</li>
</ul>
<p>즉 어떤 때는 <code>Integer</code>, 어떤 때는 <code>Double</code>이 들어올 수 있다.</p>
<p>그런데 이런 서로 다른 숫자 타입을
하나의 계산기 메서드가 받을 수 있으면 좋겠다고 생각했다.</p>
<p>그래서 계산기 클래스를 이렇게 바꿨다.</p>
<pre><code class="language-java">public class ArithmeticCalculator&lt;T extends Number&gt; {
}</code></pre>
<hr>
<h2 id="5-t-extends-number는-무슨-뜻일까">5. <code>T extends Number</code>는 무슨 뜻일까?</h2>
<p>이 문장은 처음 보면 조금 어렵게 느껴진다.</p>
<pre><code class="language-java">public class ArithmeticCalculator&lt;T extends Number&gt;</code></pre>
<p>하지만 뜻은 생각보다 단순하다.</p>
<ul>
<li><code>T</code> = 나중에 정해질 타입</li>
<li><code>extends Number</code> = 그런데 아무 타입이나 안 되고, 숫자 타입만 허용</li>
</ul>
<p>즉 이 계산기는</p>
<ul>
<li><code>Integer</code></li>
<li><code>Double</code></li>
<li><code>Long</code></li>
<li><code>Float</code></li>
</ul>
<p>같은 <strong>숫자 타입만 받을 수 있다</strong>는 의미다.</p>
<p>반대로 이런 건 받을 수 없다.</p>
<ul>
<li><code>String</code></li>
<li><code>Student</code></li>
</ul>
<p>즉 이 코드는 한 줄로 이렇게 해석할 수 있다.</p>
<blockquote>
<p><strong>숫자 타입만 받을 수 있는 계산기 클래스를 만들겠다</strong></p>
</blockquote>
<hr>
<h2 id="6-number는-무엇인가">6. Number는 무엇인가?</h2>
<p><code>Number</code>는 자바에서 숫자 타입들의 부모 클래스 같은 존재다.</p>
<p>대표적으로 이런 클래스들이 <code>Number</code> 계열이다.</p>
<ul>
<li><code>Integer</code></li>
<li><code>Double</code></li>
<li><code>Long</code></li>
<li><code>Float</code></li>
</ul>
<p>즉 <code>Number</code>는
<strong>“숫자 타입들의 공통 부모”</strong>
라고 이해하면 편하다.</p>
<p>그래서 <code>T extends Number</code>라고 쓰면,
정수도 가능하고 실수도 가능한 구조를 만들 수 있다.</p>
<hr>
<h2 id="7-실제-계산기-코드는-어떻게-바뀌었을까">7. 실제 계산기 코드는 어떻게 바뀌었을까?</h2>
<p>기존 <code>Calculator</code> 클래스를
<code>ArithmeticCalculator</code>로 바꾸고,
calculate 메서드를 이렇게 수정했다.</p>
<pre><code class="language-java">import java.util.ArrayList;
import java.util.List;

public class ArithmeticCalculator&lt;T extends Number&gt; {

    private List&lt;Double&gt; results = new ArrayList&lt;&gt;();

    public double calculate(T num1, T num2, OperatorType operatorType) {

        double num1Double = num1.doubleValue();
        double num2Double = num2.doubleValue();

        double result = 0;

        if (operatorType == OperatorType.ADD) {
            result = num1Double + num2Double;
        } else if (operatorType == OperatorType.SUBTRACT) {
            result = num1Double - num2Double;
        } else if (operatorType == OperatorType.MULTIPLY) {
            result = num1Double * num2Double;
        } else if (operatorType == OperatorType.DIVIDE) {
            result = num1Double / num2Double;
        }

        results.add(result);
        return result;
    }
}</code></pre>
<hr>
<h2 id="8-여기서-왜-doublevalue를-썼을까">8. 여기서 왜 <code>doubleValue()</code>를 썼을까?</h2>
<p>이 부분이 가장 중요했다.</p>
<pre><code class="language-java">double num1Double = num1.doubleValue();
double num2Double = num2.doubleValue();</code></pre>
<p><code>num1</code>, <code>num2</code>는 <code>T extends Number</code>이기 때문에
실제로는 <code>Integer</code>일 수도 있고 <code>Double</code>일 수도 있다.</p>
<p>예를 들면:</p>
<ul>
<li><code>num1 = Integer 3</code></li>
<li><code>num2 = Double 2.5</code></li>
</ul>
<p>이런 식이다.</p>
<p>그런데 계산을 하려면 타입을 하나로 통일하는 게 편하다.
그래서 <strong>계산 전에 모두 <code>double</code>로 바꾼 것</strong>이다.</p>
<p><code>Number</code> 계열은 공통적으로 <code>doubleValue()</code> 메서드를 가지고 있기 때문에,
어떤 숫자 타입이 들어와도 <code>double</code>로 바꿀 수 있다.</p>
<p>예를 들어:</p>
<pre><code class="language-java">Integer a = 3;
Double b = 2.5;

System.out.println(a.doubleValue()); // 3.0
System.out.println(b.doubleValue()); // 2.5</code></pre>
<p>즉 <code>doubleValue()</code>는</p>
<blockquote>
<p><strong>어떤 숫자 타입이 오든 계산하기 편한 double로 통일하는 역할</strong></p>
</blockquote>
<p>을 한다고 이해하면 된다.</p>
<hr>
<h2 id="9-왜-결과는-listt가-아니라-listdouble로-저장했을까">9. 왜 결과는 <code>List&lt;T&gt;</code>가 아니라 <code>List&lt;Double&gt;</code>로 저장했을까?</h2>
<p>처음에는 나도 이런 생각을 했다.</p>
<blockquote>
<p>입력 타입이 <code>T</code>니까 결과도 <code>List&lt;T&gt;</code>로 저장하면 되는 거 아닌가?</p>
</blockquote>
<p>그런데 곰곰이 생각해보면 문제가 있다.</p>
<p>예를 들어 계산기가 <code>Integer</code>를 받는다고 해도
나눗셈 결과는 실수가 될 수 있다.</p>
<pre><code class="language-java">5 / 2 = 2.5</code></pre>
<p>즉 입력 타입이 <code>Integer</code>라고 해서
결과도 항상 <code>Integer</code>가 되는 건 아니다.</p>
<p>그래서 결과 저장은 아예</p>
<pre><code class="language-java">private List&lt;Double&gt; results = new ArrayList&lt;&gt;();</code></pre>
<p>이렇게 <strong>Double 타입으로 통일</strong>했다.</p>
<p>이렇게 하면</p>
<ul>
<li>정수 결과도 저장 가능 (<code>3 → 3.0</code>)</li>
<li>실수 결과도 저장 가능 (<code>2.5 → 2.5</code>)</li>
</ul>
<p>즉 결과를 더 유연하게 저장할 수 있다.</p>
<hr>
<h2 id="10-app에서는-어떻게-입력을-처리했을까">10. App에서는 어떻게 입력을 처리했을까?</h2>
<p>입력값은 더 이상 <code>nextInt()</code>로 바로 받을 수 없었다.
왜냐하면 <code>1.2</code> 같은 실수 입력도 받아야 하기 때문이다.</p>
<p>그래서 입력은 먼저 문자열로 받고,
그 문자열이 정수인지 실수인지 판단해서 숫자로 바꾸는 방식을 사용했다.</p>
<pre><code class="language-java">System.out.print(&quot;첫 번째 숫자를 입력하세요: &quot;);
String input1 = scanner.next();

System.out.print(&quot;두 번째 숫자를 입력하세요: &quot;);
String input2 = scanner.next();

Number num1 = parseNumber(input1);
Number num2 = parseNumber(input2);</code></pre>
<p>그리고 <code>parseNumber()</code> 메서드는 이렇게 만들었다.</p>
<pre><code class="language-java">private static Number parseNumber(String number) {
    if (number.contains(&quot;.&quot;)) {
        return Double.parseDouble(number);
    } else {
        return Integer.parseInt(number);
    }
}</code></pre>
<p>즉</p>
<ul>
<li><code>&quot;3&quot;</code> 입력 → <code>Integer</code></li>
<li><code>&quot;1.5&quot;</code> 입력 → <code>Double</code></li>
</ul>
<p>로 처리하도록 만든 것이다.</p>
<p>이렇게 하면 App 입장에서는
정수와 실수를 구분해서 적절한 숫자 타입으로 바꾸고,
계산기에는 <code>Number</code>로 넘겨줄 수 있게 된다.</p>
<hr>
<h2 id="11-정리하면-이번-과제에서-배운-핵심은">11. 정리하면 이번 과제에서 배운 핵심은?</h2>
<p>이번 제네릭 과제를 하면서 정리된 핵심은 이렇다.</p>
<h3 id="1-제네릭은-타입을-유연하게-만든다">1) 제네릭은 타입을 유연하게 만든다</h3>
<pre><code class="language-java">ArithmeticCalculator&lt;T extends Number&gt;</code></pre>
<p>이렇게 하면 정수와 실수 같은 여러 숫자 타입을 받을 수 있다.</p>
<h3 id="2-number는-숫자-타입들의-공통-부모다">2) <code>Number</code>는 숫자 타입들의 공통 부모다</h3>
<p><code>Integer</code>, <code>Double</code> 같은 숫자 타입을 하나의 공통 타입으로 다룰 수 있게 해준다.</p>
<h3 id="3-doublevalue는-계산-전에-타입을-통일하는-역할을-한다">3) <code>doubleValue()</code>는 계산 전에 타입을 통일하는 역할을 한다</h3>
<p>정수든 실수든 상관없이 <code>double</code>로 바꿔 계산하면 훨씬 편하다.</p>
<h3 id="4-결과는-double로-저장하는-것이-자연스럽다">4) 결과는 <code>Double</code>로 저장하는 것이 자연스럽다</h3>
<p>입력은 정수일 수도 있지만, 결과는 실수가 될 수 있기 때문이다.</p>
<hr>
<h2 id="12-느낀-점">12. 느낀 점</h2>
<p>처음에는 <code>T extends Number</code>가 굉장히 추상적으로 느껴졌다.
하지만 계산기처럼 “정수도 받고 실수도 받고 싶다”는 상황에 적용해보니,
왜 이런 문법이 필요한지 조금 이해가 됐다.</p>
<p>이번 과제를 통해 느낀 건,</p>
<blockquote>
<p>제네릭은 단순히 어려운 문법이 아니라,
<strong>타입을 더 유연하고 재사용 가능하게 만드는 도구</strong>라는 점이었다.</p>
</blockquote>
<p>그리고 <code>Number</code>와 <code>doubleValue()</code>를 함께 사용하면
정수와 실수를 하나의 계산 흐름 안에서 자연스럽게 다룰 수 있다는 것도 알게 되었다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>이번 계산기 과제에서 제네릭과 Number를 적용해보면서
기존의 정수 전용 계산기를
<strong>정수와 실수 모두 처리 가능한 계산기</strong>로 확장할 수 있었다.</p>
<p>한 줄로 정리하면:</p>
<blockquote>
<p><code>T extends Number</code>는 숫자 타입만 받게 제한하는 것이고,
<code>doubleValue()</code>는 그 숫자들을 계산하기 쉬운 형태로 통일하는 역할을 한다.</p>
</blockquote>
<p>처음에는 조금 어렵게 느껴졌지만,
직접 계산기에 붙여보면서 제네릭이 “왜 필요한지”를 조금은 이해할 수 있었던 시간이었다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/loaded_diaper/post/5de1d17b-11bc-47fa-b3b1-4a41476a1cdc/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스파르타 11일차]]></title>
            <link>https://velog.io/@loaded_diaper/%EC%8A%A4%ED%8C%8C%EB%A5%B4%ED%83%80-11%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@loaded_diaper/%EC%8A%A4%ED%8C%8C%EB%A5%B4%ED%83%80-11%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Tue, 24 Mar 2026 08:22:52 GMT</pubDate>
            <description><![CDATA[<hr>
<h1 id="java-계산기-과제에서-enum-이해하기">Java 계산기 과제에서 Enum 이해하기</h1>
<h2 id="char-대신-왜-enum을-쓰는지-처음부터-정리"><code>char</code> 대신 왜 <code>enum</code>을 쓰는지 처음부터 정리</h2>
<p>계산기 과제를 진행하면서 STEP 3에서 <code>enum</code>을 활용하라는 요구사항을 만났다.
처음에는 솔직히 이렇게 생각했다.</p>
<blockquote>
<p><code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>는 그냥 문자니까 <code>char</code>로 하면 되는 거 아닌가?</p>
</blockquote>
<p>실제로 STEP 1, STEP 2까지는 <code>char</code> 타입으로도 계산기를 충분히 만들 수 있었다.
하지만 STEP 3에서는 단순히 “동작하는 계산기”를 넘어서,
<strong>연산자 타입을 더 명확하고 안전하게 관리하는 방식</strong>으로 코드를 개선하는 것이 핵심이었다.</p>
<p>이번 글에서는 내가 계산기 과제를 하면서 이해한
<strong>enum이 무엇인지</strong>,
<strong>왜 계산기에서 enum을 쓰는지</strong>,
<strong>기존 코드에 어떻게 연결했는지</strong>를 정리해보려고 한다.</p>
<hr>
<h2 id="1-enum이란-무엇인가">1. enum이란 무엇인가?</h2>
<p><code>enum</code>은 <strong>정해진 값들만 가질 수 있도록 만든 특별한 타입</strong>이다.</p>
<p>예를 들어 이런 것들이 있다.</p>
<ul>
<li>요일: 월, 화, 수, 목, 금, 토, 일</li>
<li>계절: 봄, 여름, 가을, 겨울</li>
<li>상태: 대기중, 진행중, 완료</li>
<li>연산자: 더하기, 빼기, 곱하기, 나누기</li>
</ul>
<p>이런 값들은 공통점이 있다.</p>
<blockquote>
<p>값의 종류가 미리 정해져 있다.</p>
</blockquote>
<p>자바에서는 이렇게 <strong>정해진 종류만 있는 값들</strong>을 표현할 때 <code>enum</code>을 사용한다.</p>
<p>예를 들어 계산기의 연산자를 enum으로 만들면 이렇게 표현할 수 있다.</p>
<pre><code class="language-java">public enum OperatorType {
    ADD,
    SUBTRACT,
    MULTIPLY,
    DIVIDE
}</code></pre>
<p>이제 <code>OperatorType</code>은 새로운 타입이 되고,
이 타입은 오직 아래 네 값만 가질 수 있다.</p>
<ul>
<li><code>OperatorType.ADD</code></li>
<li><code>OperatorType.SUBTRACT</code></li>
<li><code>OperatorType.MULTIPLY</code></li>
<li><code>OperatorType.DIVIDE</code></li>
</ul>
<p>즉, <code>enum</code>의 핵심은</p>
<blockquote>
<p><strong>아무 값이나 들어오지 못하게 하고, 정해진 값만 다루게 만드는 것</strong></p>
</blockquote>
<p>이라고 이해하면 된다.</p>
<hr>
<h2 id="2-기존-계산기에서는-왜-char를-썼을까">2. 기존 계산기에서는 왜 <code>char</code>를 썼을까?</h2>
<p>처음 만들었던 계산기에서는 사용자가 사칙연산 기호를 직접 입력했다.</p>
<p>예를 들면 이런 식이다.</p>
<pre><code class="language-java">char operator = scanner.next().charAt(0);</code></pre>
<p>그리고 계산할 때는 이렇게 비교했다.</p>
<pre><code class="language-java">if (operator == &#39;+&#39;) {
    result = a + b;
} else if (operator == &#39;-&#39;) {
    result = a - b;
} else if (operator == &#39;*&#39;) {
    result = a * b;
} else if (operator == &#39;/&#39;) {
    result = a / b;
}</code></pre>
<p>이 방식은 동작은 잘 한다.
하지만 문제는 <code>char</code> 타입은 연산자만 넣는 타입이 아니라는 점이다.</p>
<p>즉 아래 같은 값도 모두 들어갈 수 있다.</p>
<ul>
<li><code>&#39;a&#39;</code></li>
<li><code>&#39;?&#39;</code></li>
<li><code>&#39;1&#39;</code></li>
</ul>
<p>즉 <code>char</code>는 그저 “문자 한 글자”를 저장하는 타입일 뿐이고,
<strong>“연산자 전용 타입”은 아니다.</strong></p>
<p>여기서 enum을 쓰는 이유가 생긴다.</p>
<hr>
<h2 id="3-계산기에서-enum을-쓰는-이유">3. 계산기에서 enum을 쓰는 이유</h2>
<p>계산기에서는 사실 연산자의 종류가 딱 4개로 정해져 있다.</p>
<ul>
<li>더하기</li>
<li>빼기</li>
<li>곱하기</li>
<li>나누기</li>
</ul>
<p>즉 아무 문자나 들어오는 게 아니라
<strong>정해진 연산자만 관리하고 싶다</strong>는 요구가 있다.</p>
<p>이럴 때 enum을 사용하면 코드가 훨씬 명확해진다.</p>
<h3 id="기존-방식">기존 방식</h3>
<pre><code class="language-java">char operator = &#39;+&#39;;</code></pre>
<h3 id="enum-방식">enum 방식</h3>
<pre><code class="language-java">OperatorType operatorType = OperatorType.ADD;</code></pre>
<p>둘 다 결국 더하기를 의미하지만,
<code>OperatorType.ADD</code> 쪽이 훨씬 의미가 분명하다.</p>
<p><code>&#39;+&#39;</code>는 그냥 문자 하나처럼 보이지만,
<code>ADD</code>는 누가 봐도 “더하기 연산자”라는 걸 알 수 있다.</p>
<p>즉 enum을 쓰면</p>
<ul>
<li>의미가 더 명확해지고</li>
<li>잘못된 값을 관리하기 쉬워지고</li>
<li>연산자의 종류를 타입 차원에서 제한할 수 있다</li>
</ul>
<p>는 장점이 있다.</p>
<hr>
<h2 id="4-계산기에-enum을-어떻게-붙였는가">4. 계산기에 enum을 어떻게 붙였는가?</h2>
<p>사용자는 여전히 <code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>를 입력한다.
하지만 프로그램 내부에서는 이 문자를 바로 계산에 쓰지 않고,
먼저 enum 값으로 바꾸도록 했다.</p>
<p>즉 흐름은 이렇게 바뀐다.</p>
<h3 id="예전-방식">예전 방식</h3>
<pre><code class="language-java">&#39;+&#39; -&gt; 바로 Calculator에 전달</code></pre>
<h3 id="enum-적용-후-방식">enum 적용 후 방식</h3>
<pre><code class="language-java">&#39;+&#39; -&gt; OperatorType.ADD 로 변환 -&gt; Calculator에 전달</code></pre>
<p>즉 사용자 입력은 여전히 문자지만,
프로그램 내부 계산 로직은 enum 기반으로 동작하게 바꾼 것이다.</p>
<hr>
<h2 id="5-operatortype-enum-만들기">5. OperatorType enum 만들기</h2>
<p>먼저 연산자의 종류를 enum으로 정의했다.</p>
<pre><code class="language-java">package com.example.calculator;

public enum OperatorType {
    ADD,
    SUBTRACT,
    MULTIPLY,
    DIVIDE
}</code></pre>
<p>이제 <code>OperatorType</code>은 계산기에서 사용하는 연산자 타입이 되었다.</p>
<hr>
<h2 id="6-문자-입력을-enum으로-바꾸는-메서드-추가">6. 문자 입력을 enum으로 바꾸는 메서드 추가</h2>
<p>문제는 사용자가 입력하는 값은 <code>+</code>, <code>-</code>, <code>*</code>, <code>/</code> 같은 문자라는 점이다.
그래서 이 문자를 enum 값으로 변환해주는 메서드가 필요했다.</p>
<pre><code class="language-java">package com.example.calculator;

public enum OperatorType {
    ADD,
    SUBTRACT,
    MULTIPLY,
    DIVIDE;

    public static OperatorType fromChar(char operator) {
        if (operator == &#39;+&#39;) {
            return ADD;
        } else if (operator == &#39;-&#39;) {
            return SUBTRACT;
        } else if (operator == &#39;*&#39;) {
            return MULTIPLY;
        } else if (operator == &#39;/&#39;) {
            return DIVIDE;
        } else {
            throw new IllegalArgumentException(&quot;잘못된 연산자입니다.&quot;);
        }
    }
}</code></pre>
<p>이 메서드는 말 그대로
<strong>문자(char)를 받아서 OperatorType으로 바꾸는 역할</strong>을 한다.</p>
<p>예를 들어:</p>
<pre><code class="language-java">OperatorType type = OperatorType.fromChar(&#39;+&#39;);</code></pre>
<p>이렇게 하면 결과는:</p>
<pre><code class="language-java">OperatorType.ADD</code></pre>
<p>가 된다.</p>
<p>즉, 사용자 입력을 프로그램 내부에서 안전하게 사용할 수 있는 enum 값으로 바꾸는 과정이라고 보면 된다.</p>
<hr>
<h2 id="7-기존-calculator-클래스-수정하기">7. 기존 Calculator 클래스 수정하기</h2>
<p>원래 Calculator 클래스는 <code>char</code>를 매개변수로 받았었다.</p>
<h3 id="기존-코드">기존 코드</h3>
<pre><code class="language-java">public int calculate(int a, int b, char operator) {
    int result = 0;

    if (operator == &#39;+&#39;) {
        result = a + b;
    } else if (operator == &#39;-&#39;) {
        result = a - b;
    } else if (operator == &#39;*&#39;) {
        result = a * b;
    } else if (operator == &#39;/&#39;) {
        result = a / b;
    }

    results.add(result);
    return result;
}</code></pre>
<p>이제는 <code>char</code> 대신 <code>OperatorType</code>을 받도록 바꾸었다.</p>
<h3 id="수정-후-코드">수정 후 코드</h3>
<pre><code class="language-java">public int calculate(int a, int b, OperatorType operatorType) {
    int result = 0;

    if (operatorType == OperatorType.ADD) {
        result = a + b;
    } else if (operatorType == OperatorType.SUBTRACT) {
        result = a - b;
    } else if (operatorType == OperatorType.MULTIPLY) {
        result = a * b;
    } else if (operatorType == OperatorType.DIVIDE) {
        result = a / b;
    }

    results.add(result);
    return result;
}</code></pre>
<p>여기서 핵심 변화는 두 가지였다.</p>
<ol>
<li>매개변수가 <code>char</code>에서 <code>OperatorType</code>으로 바뀜</li>
<li>비교 대상이 <code>&#39;+&#39;</code> 같은 문자에서 <code>OperatorType.ADD</code> 같은 enum 값으로 바뀜</li>
</ol>
<p>이렇게 바꾸니 계산 로직이 훨씬 의미 중심으로 읽히기 시작했다.</p>
<hr>
<h2 id="8-app-클래스에서는-어떻게-연결했는가">8. App 클래스에서는 어떻게 연결했는가?</h2>
<p>사용자 입력은 그대로 문자로 받았다.</p>
<pre><code class="language-java">char operator![](https://velog.velcdn.com/images/loaded_diaper/post/3c15d636-f7a5-4d3c-af62-619e293eb424/image.png)
![](https://velog.velcdn.com/images/loaded_diaper/post/2b29aaa4-8fab-4ea9-9c49-699f0e312eb9/image.png)
![](https://velog.velcdn.com/images/loaded_diaper/post/f04d90c3-ea63-4161-88c9-4f473e001d2c/image.png)
 = scanner.next().charAt(0);</code></pre>
<p>그다음 바로 Calculator에 넘기는 대신,
먼저 enum으로 바꿨다.</p>
<pre><code class="language-java">OperatorType operatorType = OperatorType.fromChar(c);</code></pre>
<p>그리고 이 enum 값을 Calculator에 전달했다.</p>
<pre><code class="language-java">int result = calculator.calculate(a, b, operatorType);</code></pre>
<p>즉 App 클래스의 역할은</p>
<ul>
<li>입력 받기</li>
<li>입력받은 문자를 enum으로 변환하기</li>
<li>Calculator에 넘기기</li>
</ul>
<p>가 되었다.</p>
<hr>
<h2 id="9-enum을-적용하면서-느낀-점">9. enum을 적용하면서 느낀 점</h2>
<p>처음에는 솔직히
“문자 하나 받아서 if문으로 계산하면 되는데 굳이 enum까지 써야 하나?”
라는 생각이 들었다.</p>
<p>하지만 직접 적용해보니 enum의 장점이 조금씩 보이기 시작했다.</p>
<h3 id="1-코드-의미가-더-분명해진다">1) 코드 의미가 더 분명해진다</h3>
<p><code>&#39;+&#39;</code>보다는 <code>OperatorType.ADD</code>가 훨씬 읽기 쉽다.</p>
<h3 id="2-정해진-값만-다룬다는-것이-코드에서-드러난다">2) 정해진 값만 다룬다는 것이 코드에서 드러난다</h3>
<p>연산자는 아무 문자나 들어오는 값이 아니라,
정해진 4가지 중 하나라는 사실이 코드 구조에 반영된다.</p>
<h3 id="3-나중에-확장하기-좋다">3) 나중에 확장하기 좋다</h3>
<p>지금은 단순히 연산 종류만 관리하지만,
나중에는 각 enum에 기호, 설명, 계산 로직까지 붙일 수도 있다.</p>
<p>즉 enum은 단순히 “문법 하나 더 배운 것”이 아니라
<strong>정해진 종류를 더 안전하게 표현하는 방식</strong>이라는 걸 느꼈다.</p>
<hr>
<h2 id="10-정리">10. 정리</h2>
<p>이번 계산기 과제에서 enum을 적용하면서 가장 중요하게 느낀 점은 다음과 같다.</p>
<ul>
<li><code>enum</code>은 <strong>정해진 값들만 가지는 타입</strong>이다.</li>
<li>계산기의 연산자처럼 종류가 미리 정해져 있는 값에 잘 어울린다.</li>
<li>기존에는 <code>char</code>로 직접 비교했지만,</li>
<li>enum을 사용하면 <strong>의미가 더 분명하고 안전한 코드</strong>가 된다.</li>
<li>사용자 입력은 문자로 받더라도, 프로그램 내부에서는 enum으로 변환해서 처리할 수 있다.</li>
</ul>
<p>한 줄로 정리하면:</p>
<blockquote>
<p>계산기에서 enum을 사용한다는 것은
연산자를 단순한 문자로 다루는 것이 아니라,
<strong>정해진 연산 타입으로 명확하게 관리하겠다는 의미</strong>였다.</p>
</blockquote>
<hr>
<h2 id="마무리">마무리</h2>
<p>처음에는 enum이 조금 추상적으로 느껴졌지만,
계산기처럼 연산자 종류가 명확한 예제에 직접 붙여보니 왜 필요한지 이해가 됐다.</p>
<p>이번 과제를 통해
단순히 “계산이 되게 만드는 것”에서 한 걸음 더 나아가,
<strong>코드를 더 명확하고 구조적으로 표현하는 방법</strong>을 조금씩 배우고 있다는 느낌이 들었다.</p>
<p>다음에는 enum뿐만 아니라
제네릭, 람다, 스트림도 계산기 과제에 어떻게 연결되는지 차근차근 정리해보고 싶다.</p>
<hr>
]]></description>
        </item>
    </channel>
</rss>