<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Rockernun.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Fri, 26 Jun 2026 08:55:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Rockernun.log</title>
            <url>https://velog.velcdn.com/images/rocker_nun/profile/f0c0bd50-74e7-4ccf-a1cf-9f8278565deb/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Rockernun.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/rocker_nun" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Spring Batch 개념 정리]]></title>
            <link>https://velog.io/@rocker_nun/Spring-Batch-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@rocker_nun/Spring-Batch-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 26 Jun 2026 08:55:03 GMT</pubDate>
            <description><![CDATA[<h1 id="👨🏻🔧-배치batch란">👨🏻‍🔧 배치(Batch)란?</h1>
<p><strong>배치 처리(Batch Processing)</strong>는 대용량 데이터를 사용자 개입 없이 일괄로 처리하는 방식이다.</p>
<p>조금 더 풀어서 말하면, 배치 처리는 다음과 같은 특징을 가진다.</p>
<ul>
<li>대용량 데이터를 처리한다.</li>
<li>사용자가 응답을 기다리지 않는다.</li>
<li>보통 정해진 시간에 실행된다.</li>
<li>처리 상태를 기록할 수 있어야 한다.</li>
<li>실패 시 재시작할 수 있어야 한다.</li>
<li>데이터를 일정 단위로 나누어 안정적으로 처리해야 한다.</li>
</ul>
<p>배치 처리는 단순히 <code>for</code>문으로 많은 데이터를 반복 처리하는 것이 아니라, 운영 환경에서 대용량 데이터를 안정적으로 처리하기 위한 실행 방식이라고 볼 수 있다.</p>
<p>배치 처리는 일반적인 웹 API 처리와 다르다. 웹 API는 사용자의 요청에 즉시 응답해야 한다. 예를 들어 사용자가 상품 상세 페이지에 들어오면 서버는 상품 정보를 조회해서 빠르게 응답해야 한다. 반면, 배치 처리는 사용자가 직접 기다리는 작업이 아니라 정산, 통계 집계, 만료 쿠폰 삭제, 대량 알림 발송, 데이터 마이그레이션처럼 정해진 시점에 대량의 데이터를 한 번에 처리하는 경우에 이용되는 방식이다.</p>
<p>정리하자면, 웹 API는 <strong><em>“빠른 응답”</em></strong> 이 중요하고, 배치는 <strong><em>“안정적인 대량 처리”</em></strong> 가 중요하다.</p>
<p>&nbsp;</p>
<h1 id="😁-spring-batch">😁 Spring Batch</h1>
<p><strong>Spring Batch</strong>는 스프링 프레임워크 기반의 배치 처리 프레임워크다. 대용량 데이터를 안정적으로 처리하기 위해 필요한 기능들을 제공한다. Spring Batch가 제공하는 대표적인 기능은 다음과 같다.</p>
<ul>
<li><p><code>재시작 기능</code>: 배치 작업은 오래 걸릴 수 있다. 작업 도중 서버가 중단되거나 예외가 발생할 수 있다. Spring Batch는 작업이 어디까지 진행되었는지 데이터베이스에 기록한다. 그래서 작업이 실패하더라도 실행 이력을 바탕으로 다시 실행하거나, 실패 지점을 추적할 수 있다.</p>
</li>
<li><p><code>Chunk 단위 처리</code>: 100만 건의 데이터를 한 번에 메모리에 올리면 위험하다. Spring Batch는 데이터를 일정 개수씩 나누어 읽고 처리할 수 있다. 예를 들어 200만 건을 한 번에 처리하는 것이 아니라, 1000건씩 읽고, 처리하고, 저장하는 식으로 작업할 수 있다. 이렇게 일정 단위로 나누어 처리하는 방식을 Chunk 지향 처리라고 한다.</p>
</li>
<li><p><code>Skip/Retry 처리</code>: 운영 데이터는 항상 완전하지 않다. 특정 데이터 하나 때문에 예외가 발생할 수 있다. Spring Batch는 특정 예외가 발생했을 때 해당 데이터만 건너뛰거나, 일정 횟수만큼 재시도하는 기능을 제공한다. 이를 통해 일부 데이터 오류 때문에 전체 작업이 중단되는 상황을 줄일 수 있다.</p>
</li>
<li><p><code>메타 데이터 관리</code>: Spring Batch는 Job과 Step의 실행 이력을 데이터베이스에 기록한다. 예를 들어, 어떤 Job이 실행되었는지, 어떤 파라미터로 실행됐는지, 언제 시작되고 언제 종료됐는지, 성공했는지 실패했는지, 몇 건을 읽고 쓰고, 몇 건을 건너뛰었는지 등과 같은 정보들을 저장한다.</p>
</li>
</ul>
<p>&nbsp;</p>
<h2 id="🏛️-spring-batch의-구조">🏛️ Spring Batch의 구조</h2>
<p>Spring Batch는 크게 3개의 레이어(Application, Batch Core, Batch Infrastructure)로 구성된다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/2ddbf493-660d-446c-b9d0-a6dd81ddcfa7/image.png" alt=""></p>
<ul>
<li><p><code>Application Layer</code>: 업무 로직이나 서비스, 혹은 잡 구조화와 관련된 내용이 포함된다. 월말 정산 배치를 만든다고 가정해보면 아래와 같은 내용들이 포함될 것이다.</p>
<ul>
<li>정산 Job은 어떤 Step들로 구성되는가?</li>
<li>판매자 데이터를 어디서 읽을 것인가?</li>
<li>정산 금액은 어떻게 계산할 것인가?</li>
<li>계산 결과를 어디에 저장할 것인가?</li>
<li>오류가 발생하면 어떻게 처리할 것인가?</li>
</ul>
<p>즉, 개발자가 주로 집중해야 하는 영역이 Application Layer라고 할 수 있다.</p>
</li>
</ul>
<ul>
<li><p><code>Batch Core Layer</code>: 배치 작업을 실행하고 제어하는 핵심 기능을 제공한다. 대표적으로 JobLauncher, Job, Step, JobRepository가 이 레이어에 존재한다. Application Layer에서 개발자가 Job과 Step을 정의하면, Batch Core Layer가 그 Job과 Step을 실행하고 제어한다.</p>
</li>
<li><p><code>Batch Infrastructure Layer</code>: 배치 처리에 필요한 공통 기반 기능을 제공한다. 예를 들어 CSV 파일을 읽는 FlatFileItemReader, 데이터베이스에서 데이터를 읽는 JdbcPagingItemReader, 데이터를 파일이나 DB에 쓰는 ItemWriter, 재시도 처리를 위한 RetryTemplate, 실행 이력을 저장하는 메타데이터 저장소 등이 이 레이어에 존재한다. 개발자는 Infrastructure Layer에서 제공하는 구현체를 가져와 Application Layer에서 조합하고, Batch Core Layer가 이를 실행하는 구조로 이해할 수 있다.</p>
</li>
</ul>
<h2 id="🔀-spring-batch-작업-흐름">🔀 Spring Batch 작업 흐름</h2>
<p>Spring Batch의 전체 작업 흐름에 대해 살펴보면 아래 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/acc44305-9761-4234-baf1-a3e66acd0089/image.png" alt=""></p>
<h3 id="🍔-triggering-system">🍔 Triggering System</h3>
<p><code>Triggering System</code>은 Batch Job 실행을 시작시키는 외부 시스템이다. Spring Batch 자체가 특정 시간에 Job을 자동으로 실행해주는 스케줄러는 아니다. 실제로 Job을 언제 실행할지는 외부 트리거가 결정한다.</p>
<p>예를 들면 다음과 같은 것들이 Triggering System이 될 수 있다.</p>
<ul>
<li>Spring Scheduler</li>
<li>Linux Cron</li>
<li>Jenkins</li>
<li>Kubernetes CronJob</li>
<li>운영자가 직접 실행하는 CLI</li>
<li>특정 API 요청</li>
</ul>
<p>예를 들어 매월 말 정산 배치를 실행한다면, 스케줄러가 정해진 시간에 Batch Job을 실행하도록 트리거할 수 있다.</p>
<pre><code>매월 말 00:00
    ↓
정산 Batch Job 실행</code></pre><p>즉, Triggering System은 <strong><em>“언제 배치를 실행할 것인가?”</em></strong> 를 담당한다.</p>
<p>&nbsp;</p>
<h3 id="🧀-joblauncher">🧀 JobLauncher</h3>
<p><code>JobLauncher</code>는 Job을 실행하는 진입점이다. Job을 정의했다고 해서 자동으로 실행되는 것은 아니다. 정의된 Job을 실제로 실행하려면 <code>JobLauncher</code>를 통해 실행 요청을 보내야 한다.</p>
<pre><code class="language-java">jobLauncher.run(job, jobParameters);</code></pre>
<p><code>JobLauncher</code>는 실행할 <code>Job</code>과 해당 Job에 전달할 <code>JobParameters</code>를 받아서 배치 작업을 시작한다. 쉽게 말하면 <code>JobLauncher</code>는 <em><strong>“이 Job을 이 파라미터로 실행해줘”</strong></em> 와 같은 요청을 처리한다.</p>
<p>&nbsp;</p>
<h3 id="🍿-jobparameters">🍿 JobParameters</h3>
<p><code>JobParameters</code>는 Job을 실행할 때 함께 전달하는 파라미터다. 단순히 Job 내부에서 사용할 값이라고만 생각하면 안 된다. Spring Batch에서 <code>JobParameters</code>는 <code>JobInstance</code>를 식별하는 중요한 기준이 된다.</p>
<p>Spring Batch는 <em>“Job 이름 + JobParameters”</em> 조합으로 하나의 <code>JobInstance</code>를 구분한다. 예를 들어 다음과 같은 Job이 있다고 해보자.</p>
<pre><code>Job 이름: settlementJob
JobParameters: settlementMonth=2026-06</code></pre><p>이 경우 Spring Batch는 이것을 <code>2026년 6월 정산 작업</code>이라는 하나의 논리적인 실행 단위로 보는 것이다.</p>
<p>&nbsp;</p>
<h3 id="🍖-job">🍖 Job</h3>
<p><code>Job</code>은 하나의 배치 작업 전체를 의미한다. 예를 들어 다음과 같은 작업은 각각 하나의 Job이 될 수 있다.</p>
<ul>
<li>월말 정산 Job</li>
<li>일별 매출 통계 생성 Job</li>
<li>만료 쿠폰 삭제 Job</li>
<li>휴면 회원 전환 Job</li>
<li>대량 알림 발송 Job</li>
</ul>
<p>즉, Job은 <strong><em>“무엇을 처리할 것인가?”</em></strong> 에 대한 가장 큰 단위다.</p>
<p>&nbsp;</p>
<h3 id="🍗-jobinstance">🍗 JobInstance</h3>
<p><code>JobInstance</code>는 <code>Job</code> 이름과 <code>JobParameters</code> 조합으로 식별되는 논리적인 실행 단위다. 예를 들어 다음과 같이 실행했다고 해보자.</p>
<pre><code>Job 이름: settlementJob
JobParameters: settlementMonth=2026-06</code></pre><p>그러면 Spring Batch는 이 조합을 하나의 <code>JobInstance</code>로 본다.</p>
<pre><code>settlementJob + settlementMonth=2026-06
= 하나의 JobInstance</code></pre><p>이 <code>JobInstance</code>는 <code>2026년 6월 정산 작업</code>이라는 논리적인 배치 작업을 의미한다. 중요한 점은 <strong>이미 성공적으로 완료된 <code>JobInstance</code>는 같은 파라미터로 다시 실행할 수 없다는 것</strong>이다. 왜냐하면 같은 데이터를 같은 조건으로 다시 처리하면 중복 정산, 중복 지급, 중복 알림 같은 문제가 발생할 수 있기 때문이다.</p>
<p>따라서 Spring Batch는 완료된 <code>JobInstance</code>의 동일 파라미터 재실행을 막는다. 새로운 <code>JobInstance</code>를 만들고 싶다면 새로운 파라미터를 사용해야 한다.</p>
<p>&nbsp;</p>
<h3 id="🍕-jobexecution">🍕 JobExecution</h3>
<p><code>JobExecution</code>은 <code>JobInstance</code>를 실제로 실행한 한 번의 시도다. <code>JobInstance</code>가 <strong><em>“어떤 작업인가?”</em></strong> 를 나타낸다면, <code>JobExecution</code>은 <strong><em>“그 작업을 실제로 실행한 한 번의 기록”</em></strong> 이라고 볼 수 있다.</p>
<p>예를 들어 <code>2026년 6월 정산 JobInstance</code>가 있다고 해보자. 이 작업을 실행했는데 중간에 실패했다면, <code>JobInstance</code>는 그대로 유지된다. 하지만 실행 시도인 <code>JobExecution</code>은 하나 생성된다.</p>
<p>이후 같은 <code>JobInstance</code>를 다시 실행하면 어떻게 될까? 논리적인 작업은 여전히 <code>2026년 6월 정산 작업</code>이므로 <code>JobInstance</code>는 기존 것을 재사용한다.</p>
<p>하지만 실행 시도는 새롭게 발생한 것이므로 <code>JobExecution</code>은 새로 생성된다. 정리하면 다음과 같다.</p>
<pre><code>JobInstance: 2026년 6월 정산 작업
JobExecution 1: 첫 번째 실행 시도 - 실패
JobExecution 2: 두 번째 실행 시도 - 성공</code></pre><p>즉, 하나의 <code>JobInstance</code>는 여러 개의 <code>JobExecution</code>을 가질 수 있다. <code>JobExecution</code>에는 보통 다음과 같은 정보가 기록된다.</p>
<ul>
<li>실행 상태</li>
<li>시작 시간</li>
<li>종료 시간</li>
<li>종료 코드</li>
<li>실패 예외</li>
<li>ExecutionContext</li>
</ul>
<p>&nbsp;</p>
<h3 id="🥗-step">🥗 Step</h3>
<p><code>Step</code>은 <code>Job</code>을 구성하는 독립적인 실행 단계다. 하나의 <code>Job</code>은 하나 이상의 <code>Step</code>으로 구성되어 있다.</p>
<p>예를 들어 월말 정산 <code>Job</code>은 다음과 같이 여러 <code>Step</code>으로 나눌 수 있다.</p>
<pre><code>월말 정산 Job
 ├─ Step 1. 정산 대상 판매자 조회
 ├─ Step 2. 판매자별 정산 금액 계산
 ├─ Step 3. 정산 결과 저장
 └─ Step 4. 정산 완료 알림 발송</code></pre><p>각 <code>Step</code>은 독립적인 실행 단위다. 앞의 <code>Step</code>이 성공해야 다음 <code>Step</code>이 실행되도록 구성할 수도 있고, 조건에 따라 다른 <code>Step</code>으로 분기할 수도 있다.</p>
<p>&nbsp;</p>
<h3 id="🍤-stepexecution">🍤 StepExecution</h3>
<p><code>StepExecution</code>은 <code>JobExecution</code> 안에서 각각의 <code>Step</code>이 실행된 정보를 의미한다.</p>
<p><code>JobExecution</code>이 <code>Job</code>의 실행 시도라면, <code>StepExecution</code>은 <code>Step</code>의 실행 시도다. 하나의 <code>Job</code> 안에 <code>Step</code>이 여러 개 있으면, 각 <code>Step</code>마다 <code>StepExecution</code>이 생성된다.</p>
<p>예를 들어 월말 정산 <code>Job</code>에 <code>Step</code>이 3개 있다면 다음과 같이 볼 수 있다.</p>
<pre><code>JobExecution
 ├─ StepExecution 1. 정산 대상 조회 Step 실행 정보
 ├─ StepExecution 2. 정산 금액 계산 Step 실행 정보
 └─ StepExecution 3. 정산 결과 저장 Step 실행 정보</code></pre><p><code>StepExecution</code>에는 해당 <code>Step</code>의 상태와 처리 건수 등이 기록된다.</p>
<p>예를 들면 다음과 같은 정보가 저장될 수 있다.</p>
<ul>
<li>Step 시작 시간</li>
<li>Step 종료 시간</li>
<li>Step 실행 상태</li>
<li>읽은 데이터 수</li>
<li>저장한 데이터 수</li>
<li>커밋 횟수</li>
<li>롤백 횟수</li>
<li>Skip 횟수</li>
</ul>
<p>&nbsp;</p>
<h3 id="🥟-executioncontext">🥟 ExecutionContext</h3>
<p><code>ExecutionContext</code>는 <code>JobExecution</code> 또는 <code>StepExecution</code> 범위에서 사용할 수 있는 key-value 형태의 상태 저장 공간이다.</p>
<p>쉽게 말하면 배치 실행 중 유지해야 하는 상태를 저장하는 공간이다. 예를 들어 다음과 같은 정보를 저장할 수 있다.</p>
<ul>
<li>마지막으로 읽은 파일 위치</li>
<li>마지막으로 처리한 ID</li>
<li>Step 간 공유해야 하는 값</li>
<li>재시작 시 복원해야 하는 상태</li>
</ul>
<p>Spring Batch는 <code>ExecutionContext</code>를 메타데이터 테이블에 저장한다. 그래서 배치 작업이 중간에 실패했다가 재시작될 때, 이전 실행에서 저장해둔 <code>ExecutionContext</code> 값을 다시 읽어와 이어서 처리할 수 있다.</p>
<p>즉, <code>ExecutionContext</code>는 재시작 기능과 밀접하게 연결된 개념이다.</p>
<p>&nbsp;</p>
<h3 id="🍟-itemreader">🍟 ItemReader</h3>
<p><code>ItemReader</code>는 데이터를 읽는 역할을 한다. 읽어오는 대상은 다양할 수 있다.</p>
<ul>
<li>파일</li>
<li>데이터베이스</li>
<li>메시지 큐</li>
<li>외부 API</li>
<li>다른 시스템의 데이터</li>
</ul>
<p><code>ItemReader</code>는 데이터를 한 건씩 읽어서 다음 단계로 넘긴다. 예를 들어 CSV 파일에서 데이터를 읽는다면 <code>FlatFileItemReader</code>를 사용할 수 있고, 데이터베이스에서 페이징 방식으로 데이터를 읽는다면 <code>JdbcPagingItemReader</code>를 사용할 수 있다.</p>
<p><code>ItemReader</code>의 중요한 특징은 읽을 데이터가 더 이상 없을 때 <code>null</code>을 반환한다는 것이다. Spring Batch는 이 <code>null</code> 반환을 보고 더 이상 처리할 데이터가 없다고 판단한다.</p>
<p>&nbsp;</p>
<h3 id="🍭-itemprocessor">🍭 ItemProcessor</h3>
<p><code>ItemProcessor</code>는 <code>ItemReader</code>가 읽은 데이터를 가공하거나 검증하는 역할을 한다. 예를 들어 정산 배치라면 <code>ItemProcessor</code>에서 다음과 같은 작업을 할 수 있다.</p>
<ul>
<li>판매 금액 계산</li>
<li>수수료 계산</li>
<li>환불 금액 차감</li>
<li>정산 대상 검증</li>
<li>잘못된 데이터 필터링</li>
<li>Entity를 저장용 DTO로 변환</li>
</ul>
<p><code>ItemProcessor</code>는 필수는 아니다. 데이터를 읽은 그대로 저장해도 된다면 <code>ItemProcessor</code> 없이 <code>ItemReader</code>와 <code>ItemWriter</code>만으로 <code>Step</code>을 구성할 수 있다.</p>
<p>하지만 실제 비즈니스 로직에서는 데이터를 가공하거나 검증해야 하는 경우가 많기 때문에 자주 사용된다. 또한 <code>ItemProcessor</code>에서 <code>null</code>을 반환하면 해당 아이템은 이후 <code>Writer</code>로 전달되지 않는다.</p>
<p>즉, 특정 조건에 맞지 않는 데이터를 필터링할 때 사용할 수 있다.</p>
<p>&nbsp;</p>
<h3 id="🍫-itemwriter">🍫 ItemWriter</h3>
<p><code>ItemWriter</code>는 처리된 데이터를 저장하는 역할을 한다. <code>ItemProcessor</code>를 거친 데이터는 Chunk 단위로 모이고, 정해진 개수만큼 쌓이면 <code>ItemWriter</code>가 한 번에 저장한다.</p>
<p>저장 대상은 다양할 수 있다.</p>
<ul>
<li>데이터베이스</li>
<li>파일</li>
<li>메시지 큐</li>
<li>외부 시스템</li>
</ul>
<p>예를 들어 정산 배치라면 <code>ItemWriter</code>는 계산된 정산 결과를 정산 테이블에 저장할 수 있다.</p>
<p><code>ItemWriter</code>는 Chunk 단위로 데이터를 전달받기 때문에, 한 건씩 저장하기보다는 여러 건을 모아서 저장하는 구조에 가깝다.</p>
<p>&nbsp;</p>
<h3 id="🍰-data-source">🍰 Data Source</h3>
<p><code>Data Source</code>는 배치 작업이 읽거나 쓰는 외부 데이터 저장소를 의미한다. 대표적으로 다음과 같은 것들이 있다.</p>
<ul>
<li>File System</li>
<li>JMS Queue</li>
<li>Database</li>
<li>외부 API</li>
<li>다른 애플리케이션 시스템</li>
</ul>
<p>Spring Batch의 <code>ItemReader</code>는 Data Source에서 데이터를 읽고, <code>ItemWriter</code>는 처리 결과를 Data Source에 쓴다.</p>
<p>정산 배치를 예로 들면 다음과 같이 볼 수 있다.</p>
<pre><code>Database에서 주문 데이터를 읽는다.
    ↓
정산 금액을 계산한다.
    ↓
Database에 정산 결과를 저장한다.</code></pre><p>즉, Data Source는 배치 작업의 입력이자 출력 대상이 될 수 있다.</p>
<p>&nbsp;</p>
<h3 id="🍜-jobrepository">🍜 JobRepository</h3>
<p><code>JobRepository</code>는 Spring Batch의 실행 메타데이터를 저장하고 관리하는 저장소다. </p>
<p>여기서 중요한 점은 <code>JobRepository</code>가 비즈니스 데이터를 저장하는 저장소가 아니라는 것이다. 예를 들어 정산 결과 자체는 정산 테이블에 저장된다. 반면 <code>JobRepository</code>는 다음과 같은 배치 실행 이력을 저장한다.</p>
<ul>
<li>어떤 Job이 실행되었는지</li>
<li>어떤 파라미터로 실행되었는지</li>
<li>Job이 언제 시작되고 끝났는지</li>
<li>Job이 성공했는지 실패했는지</li>
<li>어떤 Step이 실행되었는지</li>
<li>Step에서 몇 건을 읽었는지</li>
<li>Step에서 몇 건을 썼는지</li>
<li>Step에서 몇 건을 건너뛰었는지</li>
</ul>
<p>Spring Batch는 이 정보를 메타데이터 테이블에 저장한다. 대표적인 테이블은 다음과 같다.</p>
<table>
<thead>
<tr>
<th>테이블</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>BATCH_JOB_INSTANCE</td>
<td>JobInstance 정보 저장</td>
</tr>
<tr>
<td>BATCH_JOB_EXECUTION</td>
<td>JobExecution 정보 저장</td>
</tr>
<tr>
<td>BATCH_JOB_EXECUTION_PARAMS</td>
<td>JobParameters 정보 저장</td>
</tr>
<tr>
<td>BATCH_STEP_EXECUTION</td>
<td>StepExecution 정보 저장</td>
</tr>
<tr>
<td>BATCH_JOB_EXECUTION_CONTEXT</td>
<td>JobExecutionContext 저장</td>
</tr>
<tr>
<td>BATCH_STEP_EXECUTION_CONTEXT</td>
<td>StepExecutionContext 저장</td>
</tr>
</tbody></table>
<p><code>JobRepository</code>가 중요한 이유는 재시작과 추적 때문이다. 배치 작업이 중간에 실패해도, Spring Batch는 <code>JobRepository</code>에 저장된 실행 이력을 바탕으로 어떤 <code>Job</code>이 어떤 상태였는지 확인할 수 있다.</p>
<p><code>JobRepository</code>는 크게 3가지 시점에서 동작한다.</p>
<ol>
<li><p>Job이 시작되기 전: 같은 이름과 파라미터로 조합으로 이전에 실행된 이력이 존재하는지 조회한다.</p>
</li>
<li><p>Job이 실행되는 중: Step이 처리한 건수, 현재 실행 상태 같은 Spring Batch에서 참조할 수 있는 진행 정보를 주기적으로 업데이트한다.</p>
</li>
<li><p>Job이 종료: 최종 상태를 기록한다. (COMPLETED, FAILED 등)</p>
</li>
</ol>
<p>&nbsp;</p>
<h2 id="📝-전체-흐름-요약">📝 전체 흐름 요약</h2>
<p>마지막으로 지금까지 나온 용어를 하나의 흐름으로 정리하면 다음과 같다.</p>
<pre><code>1. Triggering System이 배치 실행을 시작한다.

2. JobLauncher가 Job과 JobParameters를 받아 Job을 실행한다.

3. Spring Batch는 Job 이름과 JobParameters로 JobInstance를 식별한다.

4. JobInstance의 실제 실행 시도로 JobExecution이 생성된다.

5. Job은 하나 이상의 Step을 실행한다.

6. 각 Step 실행 정보는 StepExecution으로 기록된다.

7. Step은 Tasklet 또는 Chunk 방식으로 실행된다.

8. Chunk 방식에서는 ItemReader, ItemProcessor, ItemWriter가 Read → Process → Write 흐름으로 데이터를 처리한다.

9. ItemReader와 ItemWriter는 File, Queue, Database 같은 Data Source와 데이터를 주고받는다.

10. JobRepository는 이 모든 실행 이력을 메타데이터 테이블에 저장한다.</code></pre><hr>
<p><strong>&lt;참고 자료&gt;</strong>
<a href="https://www.toptal.com/developers/spring/spring-batch-tutorial">Spring Batch Tutorial: Batch Processing Made Easy with Spring</a>
<a href="https://gngsn.tistory.com/177#google_vignette">Spring Batch, 제대로 이해하기 (1) - 개념이해</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[eGovFrame VSCode Initializr 오픈소스 기여: txAdvisor 직접 호출 안티패턴 수정]]></title>
            <link>https://velog.io/@rocker_nun/eGovFrame-VSCode-Initializr-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EA%B8%B0%EC%97%AC-txAdvisor-%EC%A7%81%EC%A0%91-%ED%98%B8%EC%B6%9C-%EC%95%88%ED%8B%B0%ED%8C%A8%ED%84%B4-%EC%88%98%EC%A0%95</link>
            <guid>https://velog.io/@rocker_nun/eGovFrame-VSCode-Initializr-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EA%B8%B0%EC%97%AC-txAdvisor-%EC%A7%81%EC%A0%91-%ED%98%B8%EC%B6%9C-%EC%95%88%ED%8B%B0%ED%8C%A8%ED%84%B4-%EC%88%98%EC%A0%95</guid>
            <pubDate>Fri, 12 Jun 2026 08:45:01 GMT</pubDate>
            <description><![CDATA[<h1 id="🔍-문제의-발견">🔍 문제의 발견</h1>
<p>eGovFrame VSCode Initializr는 전자정부 표준프레임워크 기반 프로젝트를 VS Code 환경에서 쉽게 생성할 수 있도록 도와주는 도구다. 프로젝트 생성뿐만 아니라 CRUD 코드 생성, 설정 파일 생성 기능도 제공한다.</p>
<p>그중 설정 파일 생성 기능은 Spring Framework 설정 파일을 XML, Java Config, YAML, Properties 형식으로 생성할 수 있게 해준다. 트랜잭션 설정의 경우 <code>templates/templates-context-xml.json</code> 파일에 다음 세 가지 항목으로 등록되어 있었다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/a9b546a8-d430-4920-b886-27f45cbebd88/image.png" alt=""></p>
<p>즉, 사용자가 설정 파일 생성 탭에서 트랜잭션 관련 설정을 만들 때 실제로 연결되는 템플릿 위치는 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/c36acd98-7f5a-490c-b625-295727e9dbf5/image.png" alt=""></p>
<p>여기서 <code>datasource.hbs</code>, <code>jpa.hbs</code>, <code>jta.hbs</code>는 XML 설정 템플릿이고, <code>datasource-java.hbs</code>, <code>jpa-java.hbs</code>, <code>jta-java.hbs</code>는 Java Config 설정 템플릿이다.</p>
<p>문제는 Java Config 템플릿에서 발견됐다. AOP 기반 트랜잭션 설정을 활성화하고 Java Config 파일을 생성하면, <code>txAdvisor</code> Bean 메서드 내부에서 <code>txAdvice(txManager)</code>를 직접 호출하는 코드가 생성되고 있었다.</p>
<p>&nbsp;</p>
<p>수정 전 코드는 다음과 같은 형태였다.</p>
<pre><code class="language-java">@Bean 
public Advisor txAdvisor(@Qualifier(&quot;txManager&quot;) DataSourceTransactionManager txManager) { 
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); 
    pointcut.setExpression( &quot;execution(* egovframework.example..*Impl.*(..)) &quot; + &quot;|| execution(* org.egovframe.rte.fdl.excel.impl.*Impl.*(..))&quot; ); 
    return new DefaultPointcutAdvisor(pointcut, txAdvice(txManager)); 
}</code></pre>
<p>처음에는 이 코드가 크게 이상해 보이지 않을 수 있다. <code>txAdvisor</code>를 만들 때 필요한 <code>txAdvice</code>를 호출해서 넣어주는 것처럼 보이기 때문이다.</p>
<p>하지만 이 코드는 Spring 설정 클래스에서 다른 <code>@Bean</code> 메서드를 직접 호출하는 패턴이다. 그리고 이 패턴은 <code>@Configuration</code>의 동작 방식, 특히 <code>proxyBeanMethods</code> 설정에 따라 미묘하거나 실제적인 문제를 만들 수 있다.</p>
<p>&nbsp;</p>
<h2 id="❓-aop-config-transaction-옵션은-무엇인가">❓ AOP Config Transaction 옵션은 무엇인가?</h2>
<p>먼저 이 문제가 언제 발생하는지부터 정리해봤다.</p>
<p>설정 파일 생성 화면에는 <code>AOP Config Transaction</code>이라는 체크박스가 있다. 이 체크박스를 선택하면 내부 폼 데이터의 <code>chkAopConfigTransaction</code> 값이 <code>true</code>가 된다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/9223e972-d130-41f2-bd3d-2c72ec00ae57/image.png" alt=""></p>
<p>그리고 템플릿 파일에서는 이 값을 조건문으로 사용한다. 즉, 사용자가 AOP 기반 트랜잭션 설정을 체크하면 <code>{{#if chkAopConfigTransaction}} ... {{/if}}</code> 블록 안에 있는 코드가 실제 생성 파일에 포함된다.</p>
<p>Java Config 템플릿에서는 다음과 같은 코드가 조건부로 생성된다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/daf74151-4c80-4aa7-93d6-0b15f1bdf82e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/8c6fc783-4721-4982-bb50-71dc935342ef/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/26e4733c-974e-4fba-894e-b86cd5988520/image.png" alt=""></p>
<p>정리하면, 아래와 같은 흐름이다. </p>
<ol>
<li>사용자가 AOP Config Transaction 체크 </li>
<li><code>chkAopConfigTransaction</code> 값이 true가 됨</li>
<li>템플릿의 <code>{{#if chkAopConfigTransaction}}</code> 블록 활성화</li>
<li><code>@EnableAspectJAutoProxy(proxyTargetClass = true)</code>, <code>txAdvice</code>, <code>txAdvisor</code> 코드 생성</li>
</ol>
<p>&nbsp;</p>
<p>여기서 중요한 점은 <code>chkAopConfigTransaction</code>이 단순히 <code>@EnableAspectJAutoProxy</code>만 생성하는 옵션은 아니라는 것이다. 이 옵션이 켜지면 AOP 기반 트랜잭션 설정에 필요한 코드가 함께 생성된다. 대표적으로 다음 코드들이 포함된다.</p>
<pre><code class="language-java">@Bean(name = &quot;{{txtAdviceName}}&quot;)
public TransactionInterceptor txAdvice(PlatformTransactionManager txManager) {
    TransactionInterceptor txAdvice = new TransactionInterceptor();
    txAdvice.setTransactionManager(txManager);
    txAdvice.setTransactionAttributeSource(getNameMatchTransactionAttributeSource());
    return txAdvice;
}

@Bean
public Advisor txAdvisor(...) {
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression(&quot;{{txtPointCutExpression}}&quot;);
    return new DefaultPointcutAdvisor(pointcut, txAdvice);
}</code></pre>
<p>즉, <code>chkAopConfigTransaction</code>은 AOP 방식으로 트랜잭션을 적용하는 설정 코드를 생성할지 여부를 나타내는 옵션이다.</p>
<p>&nbsp;</p>
<h3 id="🤔-enableaspectjautoproxy-애노테이션">🤔 @EnableAspectJAutoProxy 애노테이션?</h3>
<p>AOP Config Transaction을 체크하면 Java Config 파일에 다음 애노테이션이 생성된다.</p>
<pre><code class="language-java">@EnableAspectJAutoProxy(proxyTargetClass = true)</code></pre>
<p>이 애노테이션을 한 문장으로 정리하면 다음과 같다.</p>
<blockquote>
<p><strong>Spring에게 AOP 프록시를 자동으로 만들어서, 특정 메서드 호출 앞뒤에 공통 기능을 끼워 넣을 수 있게 해달라고 요청하는 설정이다.</strong></p>
</blockquote>
<p>Spring 공식 문서에서는 <code>@EnableAspectJAutoProxy</code>를 AspectJ의 <code>@Aspect</code> 애노테이션이 붙은 컴포넌트를 처리할 수 있도록 지원을 활성화하는 애노테이션이라고 설명한다. XML 설정의 <code>&lt;aop:aspectj-autoproxy&gt;</code>와 비슷한 기능이며, 보통 <code>@Configuration</code> 클래스에 붙여 사용한다.</p>
<p>&nbsp;</p>
<p>예를 들면 다음과 같은 구조다.</p>
<pre><code class="language-java">@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

    @Bean
    public FooService fooService() {
        return new FooService();
    }

    @Bean
    public MyAspect myAspect() {
        return new MyAspect();
    }
}</code></pre>
<pre><code class="language-java">@Aspect
public class MyAspect {

    @Before(&quot;execution(* FooService+.*(..))&quot;)
    public void advice() {
        // advise FooService methods as appropriate
    }
}</code></pre>
<p>이 경우 Spring은 <code>MyAspect</code>를 AOP 설정으로 인식하고, <code>FooService</code>에 Advice를 적용하기 위해 필요한 프록시를 생성한다.</p>
<p>흐름을 간단히 정리하면 다음과 같다.</p>
<ol>
<li><code>@EnableAspectJAutoProxy</code> 애노테이션을 붙인다.</li>
<li>Spring이 AOP 설정을 읽을 수 있게 된다.</li>
<li>어드바이스를 적용해야 하는 Bean을 찾는다.</li>
<li>그 Bean 앞에 프록시 객체를 세운다.</li>
<li>메서드 호출을 프록시가 가로챈다.</li>
<li>공통 기능 실행 후 실제 메서드를 호출한다.</li>
</ol>
<p>즉, <code>@EnableAspectJAutoProxy</code>는 Spring AOP 프록시 기능을 활성화하는 역할을 한다.</p>
<p>&nbsp;</p>
<p>다만 eGovFrame 템플릿에서는 <code>@Aspect</code> 클래스를 직접 만드는 방식이 아니라, <code>Advisor</code>, <code>Pointcut</code>, <code>TransactionInterceptor</code>를 직접 Bean으로 등록하는 방식이다.</p>
<pre><code class="language-java">@Bean 
public Advisor txAdvisor(@Qualifier(&quot;txAdvice&quot;) TransactionInterceptor txAdvice) { 
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); 
    pointcut.setExpression(&quot;execution(* egovframework.example..*Impl.*(..))&quot;); 
    return new DefaultPointcutAdvisor(pointcut, txAdvice); 
}</code></pre>
<p>이 구조는 다음과 같이 이해할 수 있다.</p>
<ul>
<li><code>@AspectJExpressionPointcut</code>: 어떤 메서드에 트랜잭션을 적용할지 결정한다.</li>
<li><code>@TransactionInterceptor</code>: 트랜잭션 시작, 커밋, 롤백을 처리한다.</li>
<li><code>@DefaultPointcutAdvisor</code>: Pointcut과 Advice를 묶는다.</li>
<li><code>@EnableAspectJAutoProxy</code>: Advisor를 보고 대상 Bean에 AOP 프록시를 적용할 수 있게 한다.</li>
</ul>
<p>결과적으로 포인트컷 표현식에 매칭되는 메서드가 호출되면, Spring AOP 프록시가 호출을 가로채고 <code>TransactionInterceptor</code>가 트랜잭션 처리를 수행한다.</p>
<p>&nbsp;</p>
<p>실행 흐름은 간단히 정리하면 다음과 같다.</p>
<ol>
<li><code>ServiceImpl</code> 메서드 호출</li>
<li>Spring AOP 프록시가 호출을 가로챔</li>
<li><code>txAdvisor</code>의 포인트컷에 매칭되는지 확인</li>
<li>매칭되면 <code>txAdvice(TransactionInterceptor)</code> 실행</li>
<li><code>PlatformTransactionManager</code>로 트랜잭션 시작</li>
<li>실제 <code>ServiceImpl</code> 메서드 실행</li>
<li>정상 종료면 커밋</li>
<li>예외 발생하면 롤백</li>
</ol>
<p>&nbsp;</p>
<h3 id="🎭--proxytargetclass--true는-무엇인가">🎭  proxyTargetClass = true는 무엇인가?</h3>
<p><code>@EnableAspectJAutoProxy</code>에는 <code>proxyTargetClass</code>라는 속성이 있다. </p>
<p>공식 문서 기준으로 <code>proxyTargetClass</code>의 기본값은 <code>false</code>다. 기본값인 <code>false</code>에서는 가능한 경우 인터페이스 기반 JDK 동적 프록시를 사용한다. 반면 <code>true</code>로 설정하면 CGLIB 스타일의 클래스 기반 프록시를 사용한다. 이처럼 Spring AOP의 프록시 방식은 크게 두 가지로 나눌 수 있다.</p>
<p>&nbsp;</p>
<p>첫 번째는 JDK 동적 프록시다.</p>
<pre><code class="language-java">public interface OrderService { 
    void saveOrder(); 
} 

public class OrderServiceImpl implements OrderService { 
    @Override 
    public void saveOrder() { 
        // 주문 저장 
    } 
}</code></pre>
<p>JDK 동적 프록시는 인터페이스를 기반으로 프록시를 만든다. 즉, <code>OrderService</code> 인터페이스를 구현한 프록시 객체가 생성되고, 실제 호출은 <code>OrderServiceImpl</code>로 위임된다.</p>
<p>&nbsp;</p>
<p>두 번째는 CGLIB 프록시다.</p>
<pre><code class="language-java">public class OrderServiceImpl { 
    public void saveOrder() { 
        // 주문 저장 
    } 
}</code></pre>
<p>CGLIB는 클래스를 상속해서 프록시를 만든다. 개념적으로는 아래 코드와 유사하다.</p>
<pre><code class="language-java">public class OrderServiceImplProxy extends OrderServiceImpl { 
    @Override 
    public void saveOrder() { 
        // 공통 기능 실행 
        super.saveOrder(); 
        // 공통 기능 실행 
    } 
}</code></pre>
<p>물론 실제 코드는 Spring과 CGLIB가 런타임에 생성하지만, 이해는 이런 식으로 하면 된다. eGovFrame 템플릿의 기본 포인트컷 표현식에는 다음과 같이 <code>*Impl.*(..)</code> 패턴이 들어 있다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/b7076ef1-4480-4c9e-a5a9-81731772c2e6/image.png" alt=""></p>
<p>즉, 기본적으로 <code>*Impl</code>로 끝나는 구현 클래스의 메서드를 트랜잭션 적용 대상으로 삼으려는 의도로 볼 수 있다. 그래서 템플릿에서 <code>proxyTargetClass = true</code>를 사용한 것은 구현 클래스 중심의 전통적인 eGovFrame 서비스 구조에서 AOP 적용을 더 명확하게 하기 위한 설정으로 볼 수 있다.</p>
<p>&nbsp;</p>
<p>다만 여기서 주의할 점이 있다. <code>@EnableAspectJAutoProxy(proxyTargetClass = true)</code>는 트랜잭션을 직접 처리하는 애노테이션이 아니다. 이 애노테이션은 AOP 프록시 생성을 활성화하는 역할을 한다. 실제 트랜잭션 처리는 <code>TransactionInterceptor</code>가 담당한다.</p>
<p>정리하면 다음과 같다.</p>
<ol>
<li><code>@EnableAspectJAutoProxy(proxyTargetClass = true)</code></li>
<li>클래스 기반 AOP 프록시 생성 기능 활성화 <code>AspectJExpressionPointcut</code> </li>
<li>어떤 메서드에 적용할지 결정 <code>TransactionInterceptor</code> </li>
<li>트랜잭션 시작, 커밋, 롤백 처리 <code>DefaultPointcutAdvisor</code> </li>
<li>포인트컷과 어드바이스를 하나로 묶음</li>
</ol>
<p>&nbsp;</p>
<h2 id="💥-문제-코드-txadvicetxmanager-직접-호출">💥 문제 코드: txAdvice(txManager) 직접 호출</h2>
<p>이제 다시 문제 코드로 돌아가보자.</p>
<p>AOP 기반 트랜잭션 설정을 Java Config로 생성하면 기존 템플릿에서는 다음과 같은 코드가 만들어졌다.</p>
<pre><code class="language-java">@Bean
public Advisor txAdvisor(@Qualifier(&quot;txManager&quot;) DataSourceTransactionManager txManager) {
    ...
    return new DefaultPointcutAdvisor(pointcut, txAdvice(txManager));
}</code></pre>
<p>&nbsp;</p>
<p>문제는 마지막 줄이다. <code>txAdvice(txManager)</code>와 같이 설정 클래스 안에 있는 다른 <code>@Bean</code> 메서드를 직접 호출하고 있었다.</p>
<pre><code class="language-java">@Bean(name = &quot;{{txtAdviceName}}&quot;)
public TransactionInterceptor txAdvice(@Qualifier(&quot;{{txtTransactionName}}&quot;) DataSourceTransactionManager transactionManager) {
    TransactionInterceptor txAdvice = new TransactionInterceptor();
    txAdvice.setTransactionManager(transactionManager);
    txAdvice.setTransactionAttributeSource(getNameMatchTransactionAttributeSource());
    return txAdvice;
}</code></pre>
<p>겉으로 보면 단순히 <code>txAdvisor</code>를 만들기 위해 필요한 <code>txAdvice</code>를 생성해서 넣는 것처럼 보인다. 하지만 Spring 설정 클래스에서 <code>@Bean</code> 메서드를 직접 호출하는 것은 일반 Java 메서드 호출과 다르게 해석될 수 있다.</p>
<p>이 문제를 이해하려면 <code>@Configuration</code>과 <code>@Bean</code>의 동작 방식을 다시 살펴봐야 한다.</p>
<p>&nbsp;</p>
<h3 id="🏭-configuration과-bean-메서드-호출">🏭 @Configuration과 @Bean 메서드 호출</h3>
<p>보통 Spring 설정 클래스는 다음과 같이 작성한다.</p>
<pre><code class="language-java">@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(
            memberRepository(),
            discountPolicy());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();  
    }
}</code></pre>
<p>여기서 <code>@Configuration</code>은 이 클래스가 Spring 설정 클래스라는 뜻이고, <code>@Bean</code>은 이 메서드가 반환하는 객체를 Spring 컨테이너에 Bean으로 등록하겠다는 뜻이다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/bc67a069-8d0e-4864-993f-840716ae1eab/image.png" alt=""></p>
<p>그런데 중요한 부분은 다음과 같은 코드다.</p>
<pre><code class="language-java">@Bean
public MemberService memberService() {
    return new MemberServiceImpl(memberRepository());
}</code></pre>
<p><code>memberService()</code> 메서드 안에서 <code>memberRepository()</code> 메서드를 직접 호출하고 있다. 또 <code>orderService()</code> 메서드 안에서도 <code>memberRepository()</code>와 <code>discountPolicy()</code>를 직접 호출한다.</p>
<p>순수 Java 코드라면 <code>memberRepository()</code>를 호출할 때마다 매번 새로운 <code>MemoryMemberRepository</code> 인스턴스가 생성될 것이다. 이러면 <code>MemberServiceImpl</code>이 사용하는 <code>MemoryMemberRepository</code>와 <code>OrderServiceImpl</code>이 사용하는 <code>MemoryMemberRepository</code>가 서로 다른 객체가 될 수 있다.</p>
<p>&nbsp;</p>
<p>하지만 <code>@Configuration</code>이 붙은 Spring 설정 클래스에서는 기본적으로 그렇게 동작하지 않는다. Spring은 <code>@Configuration</code> 클래스를 그냥 그대로 사용하지 않고, CGLIB를 이용해 설정 클래스를 상속한 프록시 객체를 만든다. 개념적으로는 다음과 같은 느낌이다.</p>
<pre><code class="language-java">class AppConfigProxy extends AppConfig {

    private final SimpleSpringContainer springContainer = new SimpleSpringContainer();

    @Override
    public MemberService memberService() {
        return springContainer.getBean(&quot;memberService&quot;, () -&gt; super.memberService());
    }

    @Override
    public OrderService orderService() {
        return springContainer.getBean(&quot;orderService&quot;, () -&gt; super.orderService());
    }

    @Override
    public MemberRepository memberRepository() {
        return springContainer.getBean(&quot;memberRepository&quot;, () -&gt; super.memberRepository());
    }

    @Override
    public DiscountPolicy discountPolicy() {
        return springContainer.getBean(&quot;discountPolicy&quot;, () -&gt; super.discountPolicy());
    }
}</code></pre>
<p><code>@Bean</code> 메서드가 호출되면 곧바로 새로운 인스턴스를 생성하는 것이 아니라, 먼저 Spring 컨테이너에 해당 Bean이 이미 존재하는지 확인한다. 이미 존재하면 기존 Bean을 반환하고, 아직 없다면 원본 메서드를 실행해 Bean을 생성한 뒤 컨테이너에 등록한다.</p>
<p>&nbsp;</p>
<h3 id="🎏-full-mode와-lite-mode">🎏 Full mode와 Lite mode</h3>
<p>여기서 중요한 개념이 <strong>full mode</strong>와 <strong>lite mode</strong>다.</p>
<p>일반적으로 다음처럼 <code>@Configuration</code> 애노테이션을 붙여서 작성하면,</p>
<pre><code class="language-java">@Configuration
public class AppConfig {}</code></pre>
<p><code>@Configuration(proxyBeanMethods = true)</code>로 설정된다. 이 경우 Spring은 설정 클래스를 CGLIB 프록시로 감싸고, <code>@Bean</code> 메서드 간 직접 호출을 가로챈다. 이를 보통 full mode라고 이해할 수 있다. </p>
<p>반면 <code>@Configuration(proxyBeanMethods = false)</code> 로 설정하면 Spring은 설정 클래스를 CGLIB 프록시로 감싸지 않는다. 이 경우 <code>@Bean</code> 메서드는 일반 Java 메서드처럼 동작한다. 이를 lite mode라고 볼 수 있다. lite mode에서는 다음 코드가 진짜 일반 Java 메서드 호출이 된다.</p>
<pre><code class="language-java">@Bean 
public MemberService memberService() { 
    return new MemberServiceImpl(memberRepository()); 
}</code></pre>
<p>즉, <code>memberRepository()</code>를 호출할 때마다 새로운 <code>MemoryMemberRepository</code> 인스턴스가 만들어질 수 있다.</p>
<p>그래서 <code>proxyBeanMethods = false</code>를 사용할 때는 보통 다음처럼 작성하는 것이 좋다.</p>
<pre><code class="language-java">@Configuration(proxyBeanMethods = false)
public class AppConfig {

    @Bean
    public MemberService memberService(MemberRepository memberRepository) {
        return new MemberServiceImpl(memberRepository);
    }

    @Bean
    public OrderService orderService(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        return new OrderServiceImpl(memberRepository, discountPolicy);
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();  
    }
}</code></pre>
<p>이 방식은 <code>memberRepository()</code>를 직접 호출하지 않는다. 대신 Spring 컨테이너가 관리하는 <code>MemberRepository</code> Bean을 메서드 파라미터로 주입받는다.</p>
<p>Spring 공식 문서에서도 런타임 프록시가 없는 설정 클래스에서는 <code>@Bean</code> 메서드가 다른 <code>@Bean</code> 메서드를 직접 호출할 필요가 없고, 그런 호출은 팩토리 메서드 인자로 표현할 수 있다고 설명한다.</p>
<p>즉, 핵심을 간단하게 다음과 같다.</p>
<ul>
<li><code>@Bean</code> 메서드 직접 호출<ul>
<li><code>proxyBeanMethods = true</code>에서는 Spring이 가로채므로 겉으로는 동작 가능</li>
<li><code>proxyBeanMethods = false</code>에서는 일반 Java 메서드 호출이 되어 새로운 인스턴스가 생성</li>
</ul>
</li>
<li>파라미터 주입<ul>
<li><code>proxyBeanMethods</code> 설정과 무관하게 Spring 컨테이너가 관리하는 Bean을 사용</li>
</ul>
</li>
</ul>
<p>&nbsp;</p>
<h2 id="⏮️-다시-txadvisor-문제로-돌아가보자">⏮️ 다시 <code>txAdvisor</code> 문제로 돌아가보자</h2>
<p>이제 기존 코드가 왜 문제인지 더 명확해진다. 수정 전 코드는 다음과 같았다.</p>
<pre><code class="language-java">@Bean
public Advisor txAdvisor(@Qualifier(&quot;txManager&quot;) DataSourceTransactionManager txManager) {
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression(&quot;execution(* egovframework.example..*Impl.*(..)) || execution(* org.egovframe.rte.fdl.excel.impl.*Impl.*(..))&quot;);
    return new DefaultPointcutAdvisor(pointcut, txAdvice(txManager));  // 직접 호출
}</code></pre>
<p>여기서 <code>txAdvice(txManager)</code>는 다른 <code>@Bean</code> 메서드를 직접 호출하는 코드다.</p>
<p>기본 <code>@Configuration(proxyBeanMethods = true)</code> 환경에서는 Spring이 이 호출을 가로챈다. 그래서 겉으로는 문제가 없어 보일 수 있다. Spring은 <code>txAdvice(txManager)</code>를 일반 Java 메서드 호출로 두지 않고, 컨테이너가 관리하는 <code>txAdvice</code> Bean을 반환한다.</p>
<p>&nbsp;</p>
<p>하지만 이 상황에서도 코드의 의미는 어색하다.</p>
<pre><code class="language-java">txAdvice(txManager)</code></pre>
<p>이 코드는 마치 <code>txManager</code>를 직접 넘겨서 <code>txAdvice</code>를 새로 만들거나 구성하는 것처럼 보인다. 하지만 실제 런타임에서는 Spring 컨테이너가 이미 관리 중인 <code>txAdvice</code> Bean을 반환한다. 이때 <code>txAdvice(txManager)</code>에 전달한 <code>txManager</code> 인자는 개발자가 예상하는 방식으로 사용되지 않을 수 있다.</p>
<p>즉, 코드의 겉모습은 <code>txAdvisor</code>를 만들 때 <code>txManager</code>를 넘겨서 <code>txAdvice</code>를 구성하는 것처럼 보이지만, 실제 동작은 Spring 컨테이너에서 이미 관리 중인 <code>txAdvice</code> Bean을 가져오는 것이다. 이렇게 코드의 표현과 실제 동작이 다르면, 나중에 이 코드를 읽는 개발자가 의도를 오해할 수 있다.</p>
<p>&nbsp;</p>
<p>더 큰 문제는 <code>proxyBeanMethods = false</code> 환경에서 발생한다.</p>
<pre><code class="language-java">@Configuration(proxyBeanMethods = false) 
public class TransactionConfig { ... }</code></pre>
<p>이 경우 Spring은 설정 클래스를 CGLIB 프록시로 감싸지 않는다. 따라서 <code>txAdvisor()</code> 안에서 호출한 <code>txAdvice(txManager)</code>는 진짜 일반 Java 메서드 호출이 된다.</p>
<p>&nbsp;</p>
<p>실제로 <code>proxyBeanMethods = false</code>로 설정 후 인스턴스 참조값을 비교해보면 다른 것을 확인할 수 있다.</p>
<pre><code>Spring 관리 txAdvice@1028908791
txAdvisor 내부 advice@ 395686760
→ 동일 인스턴스: false
→ [BUG] 별개 인스턴스 생성됨 — Spring 생명주기 미적용, 트랜잭션 미적용 위험</code></pre><p>그 후, <code>TransactionInterceptor</code>가 새로 생성되어 Spring 컨테이너가 관리하는 <code>txAdvice</code> Bean이 TransactionInterceptor 객체 A를 만들고, <code>txAdvisor</code> 내부에서 직접 호출로 만들어진 <code>txAdvice</code> Bean이 새로운 TransactionInterceptor 객체 B를 만드는 상황이 된다. </p>
<p>&nbsp;</p>
<h2 id="👀-근데-이게-무슨-문제가-될까">👀 근데 이게 무슨 문제가 될까?</h2>
<p><code>txAdvice</code>는 트랜잭션 AOP에서 실제 트랜잭션 처리 로직을 담고 있는 Advice다. 구체적으로는 <code>TransactionInterceptor</code> 객체다. <code>txAdvisor</code>는 어떤 메서드에 이 Advice를 적용할지 결정하는 객체고, 내부적으로 포인트컷과 어드바이스를 함께 가지고 있다.</p>
<p>여기서 중요한 것은 <code>txAdvisor</code>가 참조하는 <code>txAdvice</code>가 Spring 컨테이너가 관리하는 Bean이어야 한다는 점이다. <code>proxyBeanMethods = false</code> 환경에서 직접 호출이 발생하면, Spring 컨테이너가 관리하는 <code>txAdvice</code> Bean과 실제 <code>txAdvisor</code> 내부에 들어간 <code>txAdvice</code> 인스턴스가 달라진다. 겉으로는 트랜잭션이 동작하는 것처럼 보일 수도 있다. 하지만 구조적으로는 위험하다. </p>
<p>왜냐하면 설정, 생명주기, 후처리, 프록시 적용, Bean 관리 관점에서 Spring이 관리하는 객체와 실제 사용 객체가 달라질 수 있기 때문이다. 특히 나중에 <code>txAdvice</code> Bean에 추가 설정이 붙거나, Bean 후처리기가 개입하거나, 특정 스코프나 프록시가 적용되면 더 미묘한 문제가 생길 수 있다.</p>
<p>&nbsp;</p>
<h1 id="👍-해결-직접-호출-대신-파라미터-주입으로-변경">👍 해결: 직접 호출 대신 파라미터 주입으로 변경</h1>
<p>공식 문서가 권장하는 방식도 다른 <code>@Bean</code> 메서드를 직접 호출하지 말고, 필요한 Bean을 메서드 파라미터로 받으라는 것이다.</p>
<p>&nbsp;</p>
<p>수정 전 코드는 다음과 같았다.</p>
<pre><code class="language-java">@Bean 
public Advisor txAdvisor(@Qualifier(&quot;txManager&quot;) DataSourceTransactionManager txManager) { 
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); 
    pointcut.setExpression( &quot;execution(* egovframework.example..*Impl.*(..)) &quot; + &quot;|| execution(* org.egovframe.rte.fdl.excel.impl.*Impl.*(..))&quot; ); 
    return new DefaultPointcutAdvisor(pointcut, txAdvice(txManager)); 
}</code></pre>
<p>&nbsp;</p>
<p>수정 후에는 다음과 같이 변경했다.</p>
<pre><code class="language-java">@Bean 
public Advisor txAdvisor(@Qualifier(&quot;txAdvice&quot;) TransactionInterceptor txAdvice) { 
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); 
    pointcut.setExpression( &quot;execution(* egovframework.example..*Impl.*(..)) &quot; + &quot;|| execution(* org.egovframe.rte.fdl.excel.impl.*Impl.*(..))&quot; ); 
    return new DefaultPointcutAdvisor(pointcut, txAdvice); 
}</code></pre>
<p>&nbsp;</p>
<p>첫 번째 변경점은 <code>txAdvisor</code>가 더 이상 <code>txManager</code>를 파라미터로 받지 않도록 한 것이다. 대신 <code>TransactionInterceptor</code> 타입의 <code>txAdvice</code> Bean을 직접 주입받는다.</p>
<pre><code class="language-java">@Qualifier(&quot;txAdvice&quot;) TransactionInterceptor txAdvice</code></pre>
<p>&nbsp;</p>
<p>두 번째 변경점은 <code>txAdvice(txManager)</code> 메서드 직접 호출을 제거한 부분이다. </p>
<pre><code class="language-java">// 수정 전
return new DefaultPointcutAdvisor(pointcut, txAdvice(txManager));

// 수정 후
return new DefaultPointcutAdvisor(pointcut, txAdvice);</code></pre>
<p>이제 <code>txAdvisor()</code>는 <code>txAdvice()</code> 메서드를 직접 호출하지 않는다. Spring에게 <code>txAdvisor</code> Bean을 만들 때 <code>TransactionInterceptor</code> 타입의 <code>txAdvice</code> Bean을 주입을 요청하는 구조가 된다. 이 방식은 훨씬 명확하다. 결과적으로 <code>proxyBeanMethods</code> 설정과 무관하게 <code>txAdvisor</code>는 항상 Spring 컨테이너가 관리하는 <code>txAdvice</code> Bean을 참조한다.</p>
<p>&nbsp;</p>
<h1 id="🎉-pull-request-결과">🎉 Pull Request 결과</h1>
<p>PR의 핵심 내용은 다음과 같다.</p>
<blockquote>
<p><strong>AOP 기반 트랜잭션 Java Config 템플릿에서 <code>txAdvisor</code> Bean이 <code>txAdvice()</code>  Bean 메서드를 직접 호출하던 패턴을 제거하고, <code>@Qualifier</code>를 사용한 파라미터 주입 방식으로 변경했다.</strong></p>
</blockquote>
<p>변경 전에는 다음 구조였다.</p>
<pre><code class="language-java">return new DefaultPointcutAdvisor(pointcut, txAdvice(txManager));</code></pre>
<p>&nbsp;</p>
<p>변경 후에는 다음 구조가 되었다.</p>
<pre><code class="language-java">return new DefaultPointcutAdvisor(pointcut, txAdvice);</code></pre>
<p>이 변경으로 <code>proxyBeanMethods = false</code> 환경에서도 <code>txAdvisor</code>가 Spring 컨테이너가 관리하는 <code>txAdvice</code> Bean을 안정적으로 참조하게 되었다. 그리고 해당 PR은 최종적으로 main 브랜치에 merge되었다.</p>
<p>&nbsp;</p>
<h2 id="😂-이럴거면-왜-proxybeanmethods--false-설정을-제공하는거지">😂 이럴거면 왜 proxyBeanMethods = false 설정을 제공하는거지?</h2>
<p>이렇게 의도치 않은 동작이 발생할 수도 있으니 그냥 항상 true로 두면 안 되는 건지 의문이 들었다. 하지만 proxyBeanMethods = true 는 CGLIB 프록시를 만들어야 한다. 즉, 런타임에 설정 클래스를 상속한 프록시 클래스를 만들고, 메서드 호출을 가로채야 하는데 이건 약간의 비용과 제약이 있다.</p>
<p>특히 Spring Boot의 자동 설정 클래스들에서는 보통 <code>proxyBeanMethods = false</code> 를 많이 사용한다. 왜냐하면 자동 설정 클래스는 대체로 <code>@Bean</code> 메서드끼리 직접 호출하지 않고, 필요한 의존성을 파라미터로 받도록 작성하기 때문이다. 실제로 Spring 5.2 이후부터는 자동 구성 클래스에 <code>proxyBeanMethods = false</code>가 기본값으로 사용되는 경우가 많아졌으며, 프록시를 생성하지 않아도 충분히 동작할 수 있는 경우 이를 활용하도록 공식 문서에서도 권장하고 있다.</p>
<hr>
<p><em><strong>&lt;참고 자료&gt;</strong></em>
<a href="https://github.com/eGovFramework/egovframe-vscode-initializr/issues/11">[Bug] AOP 기반 트랜잭션 설정 생성 시 txAdvisor Bean의 잘못된 직접 메서드 호출 패턴</a>
<a href="https://github.com/eGovFramework/egovframe-vscode-initializr/pull/12">fix: AOP 트랜잭션 설정 템플릿에서 txAdvisor의 잘못된 직접 메서드 호출 수정</a>
<a href="https://velog.io/@rocker_nun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%B4-%EC%A7%80%EC%9B%90%ED%95%98%EB%8A%94-%ED%94%84%EB%A1%9D%EC%8B%9C">스프링이 지원하는 프록시</a>
<a href="https://docs.spring.io/spring-framework/reference/core/beans/java/basic-concepts.html">Basic Concepts: @Bean and @Configuration</a>
<a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/EnableAspectJAutoProxy.html">Annotation Interface EnableAspectJAutoProxy</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA(Java Persistence API) 들어가기]]></title>
            <link>https://velog.io/@rocker_nun/JPAJava-Persistence-API-%EB%93%A4%EC%96%B4%EA%B0%80%EA%B8%B0</link>
            <guid>https://velog.io/@rocker_nun/JPAJava-Persistence-API-%EB%93%A4%EC%96%B4%EA%B0%80%EA%B8%B0</guid>
            <pubDate>Fri, 12 Jun 2026 00:25:02 GMT</pubDate>
            <description><![CDATA[<h1 id="💥-sql을-직접-다룰-때의-문제점">💥 SQL을 직접 다룰 때의 문제점</h1>
<p>관계형 데이터베이스에서 데이터를 다루고 관리하려면 SQL을 작성해야 한다. JPA를 사용하기 전의 자바 애플리케이션에서는 보통 JDBC API를 사용해서 SQL을 데이터베이스에 전달하는 코드를 직접 작성했다.</p>
<p>이해를 위해 상품을 CRUD하는 기능을 개발한다고 해보자. 먼저 상품 정보를 표현하기 위한 <code>Product</code> 객체를 만들어야 한다.</p>
<pre><code class="language-java">@Getter 
@Setter 
@NoArgsConstructor 
@AllArgsConstructor 
@Builder 
@ToString
public class Product {
    private int productId;        // 상품 번호, PK
    private String productName;   // 상품명
    private int categoryId;       // 카테고리 번호, FK
    private int price;            // 가격
    private int stock;            // 재고
    private String manufacturer;  // 제조사
    private LocalDate createdAt;  // 등록일
}</code></pre>
<p>&nbsp;</p>
<p>이제 상품 데이터를 데이터베이스에 저장하거나 조회하려면 DAO 객체를 만들어야 한다. 일반적으로는 먼저 SQL 문을 정의하고, JDBC API를 사용해서 SQL을 실행한 뒤, 조회 결과를 다시 자바 객체에 매핑하는 방식으로 작업을 진행한다.</p>
<pre><code class="language-java">public class ProductDao {

    // 상품 삽입 기능
    public int insert(Product product) {
        String sql = &quot;&quot;&quot; 
                INSERT product (product_name, category_id, price, stock, manufacturer, created_at) 
                VALUES (?, ?, ?, ?, ?, ?) 
                &quot;&quot;&quot;;

        try (
                Connection connection = DBUtil.getConnection();
                PreparedStatement pstmt = connection.prepareStatement(sql);
                )
        {
            pstmt.setString(1, product.getProductName());
            pstmt.setInt(2, product.getCategoryId());
            pstmt.setInt(3, product.getPrice());
            pstmt.setInt(4, product.getStock());
            pstmt.setString(5, product.getManufacturer());
            pstmt.setDate(6, Date.valueOf(product.getCreatedAt()));

            return pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return 0;
    }

    // 상품 리스트 조회 기능
    public List&lt;Product&gt; selectList(int count) {
        List&lt;Product&gt; products = new ArrayList&lt;&gt;();
        String sql = &quot;&quot;&quot; 
                SELECT product_id, product_name, category_id, price, stock, manufacturer, created_at 
                FROM product ORDER BY price DESC LIMIT ? 
                &quot;&quot;&quot;;

        try (
                Connection connection = DBUtil.getConnection();
                PreparedStatement pstmt = connection.prepareStatement(sql);
                )
        {
            pstmt.setInt(1, count);
            try (ResultSet rs = pstmt.executeQuery();) {
                while (rs.next()) {
                    products.add(mpaProduct(rs));
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return products;
    }

    // 상품 단건 조회 기능
    public Product selectOne(int productId) {
        String sql = &quot;&quot;&quot; 
                SELECT product_id, product_name, category_id, price, stock, manufacturer, created_at 
                FROM product WHERE product_id = ? 
                &quot;&quot;&quot;;

        try (
                Connection connection = DBUtil.getConnection();
                PreparedStatement pstmt = connection.prepareStatement(sql);
                )
        {
            pstmt.setInt(1, productId);
            try (ResultSet rs = pstmt.executeQuery();) {
                if (rs.next()) {
                    return mpaProduct(rs);
                }
            } 
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return null;
    }

    // 상품 삭제 기능
    public int delete(int productId) {
        String sql = &quot;&quot;&quot;
            DELETE FROM product
            WHERE product_id = ?
            &quot;&quot;&quot;;

        try (
                Connection connection = DBUtil.getConnection();
                PreparedStatement pstmt = connection.prepareStatement(sql);
        ) {
            pstmt.setInt(1, productId);

            return pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return 0;
    }

    private Product mpaProduct(ResultSet rs) throws SQLException {
        return Product.builder()
                .productId(rs.getInt(&quot;product_id&quot;))
                .productName(rs.getString(&quot;product_name&quot;))
                .categoryId(rs.getInt(&quot;category_id&quot;))
                .price(rs.getInt(&quot;price&quot;)) .stock(rs.getInt(&quot;stock&quot;))
                .manufacturer(rs.getString(&quot;manufacturer&quot;))
                .createdAt(rs.getDate(&quot;created_at&quot;).toLocalDate())
                .build();
    }
}</code></pre>
<p>&nbsp;</p>
<p>보다시피 고작 상품 CRUD를 구현하는데도 코드량이 상당하다. 그리고 자세히 보면 반복되는 코드도 많다. 예를 들어 다음과 같은 작업이 거의 모든 메서드에서 반복된다.</p>
<pre><code class="language-java">Connection connection = DBUtil.getConnection(); 
PreparedStatement pstmt = connection.prepareStatement(sql);</code></pre>
<p>&nbsp;</p>
<p>그리고 조회 기능에서는 <code>ResultSet</code>에서 값을 하나씩 꺼내 객체에 직접 넣어주는 작업도 필요하다.</p>
<pre><code class="language-java">.productId(rs.getInt(&quot;product_id&quot;)) 
.productName(rs.getString(&quot;product_name&quot;)) 
.categoryId(rs.getInt(&quot;category_id&quot;)) 
.price(rs.getInt(&quot;price&quot;)) 
.stock(rs.getInt(&quot;stock&quot;)) 
.manufacturer(rs.getString(&quot;manufacturer&quot;)) 
.createdAt(rs.getDate(&quot;created_at&quot;).toLocalDate())</code></pre>
<p>&nbsp;</p>
<p>상품 객체를 데이터베이스가 아니라 아래와 같이 자바 컬렉션에 간편하게 처리할 수 있다면 얼마나 좋을까?</p>
<pre><code class="language-java">list.add(product); 
Product product = list.get(productId);</code></pre>
<p>하지만 현실적으로는 이렇게 할 수 없다. 자바 객체와 관계형 데이터베이스는 서로 비슷해 보이지만, 실제로는 데이터를 표현하는 방식이 다르다. 객체는 필드와 메서드를 가지고 있고, 객체끼리 참조를 통해 연결된다. 반면 관계형 데이터베이스는 데이터를 테이블, 행, 열, 외래 키 중심으로 관리한다. 그래서 JDBC를 사용할 때는 중간에서 SQL과 JDBC API를 이용해 객체와 테이블 사이의 변환 작업을 개발자가 직접 처리해야 한다. </p>
<p>&nbsp;</p>
<p>문제는 테이블이 하나만 있는 것이 아니라는 점이다. 상품 테이블뿐만 아니라 회원, 주문, 카테고리, 재고, 결제 같은 테이블이 계속 늘어난다면 각 테이블마다 비슷한 DAO 코드를 반복해서 작성해야 한다. </p>
<p>더 큰 문제는 변경에 취약하다는 것인데, 예를 들어 <code>Product</code>에 필드 하나가 추가되기만 해도 INSERT 쿼리, SELECT 쿼리, <code>PreparedStatement</code> 파라미터 바인딩, <code>ResultSet</code> 매핑 코드를 모두 수정해야 한다.</p>
<p>또한 JDBC 기반 개발에서는 객체만 보고 해당 객체가 어떤 데이터를 온전히 가지고 있는지 판단하기 어렵다. 어떤 DAO 메서드는 상품 기본 정보만 조회하고, 어떤 DAO 메서드는 카테고리 정보까지 조인해서 조회할 수 있다. 결국 개발자는 객체를 신뢰하기보다 DAO 메서드와 SQL을 직접 열어보고 <em>“이 객체가 어디까지 조회된 객체인지”</em> 확인해야 한다.</p>
<p>&nbsp;</p>
<p>따라서 SQL과 JDBC를 직접 사용해서 코드를 작성했을 때의 문제점은 다음과 같이 정리할 수 있다.</p>
<ul>
<li>반복되는 JDBC 코드가 많다.</li>
<li>SQL 중심으로 개발하게 된다.</li>
<li>필드가 추가되거나 테이블 구조가 바뀌면 수정할 코드가 많다.</li>
<li>객체만 보고 어떤 데이터가 조회되었는지 신뢰하기 어렵다.</li>
<li>진정한 의미에서 객체 중심의 개발을 하기 어렵다.</li>
<li>계층을 분리해도 결국 DAO와 SQL에 강하게 의존하게 된다.</li>
</ul>
<p>결국 JDBC를 직접 사용하는 방식에서는 개발자가 비즈니스 로직에 집중하기보다 SQL 작성, 파라미터 바인딩, 결과 매핑 같은 반복 작업에 많은 시간을 사용하게 된다.</p>
<p>&nbsp;</p>
<h2 id="⚠️-패러다임의-불일치">⚠️ 패러다임의 불일치</h2>
<p>애플리케이션이 발전할수록 내부 복잡성은 점점 증가한다. 이런 복잡성을 관리하기 위해 자바 같은 객체지향 언어를 사용하는 것은 매우 자연스러운 선택이다.</p>
<p>객체지향 언어를 사용하면 비즈니스 요구사항을 객체로 모델링할 수 있다. 예를 들어 상품, 카테고리, 주문, 회원 같은 개념을 각각 객체로 표현하고, 객체들이 서로 협력하도록 만들 수 있다. 문제는 이렇게 만든 객체를 데이터베이스에 저장할 때 발생한다.</p>
<p>&nbsp;</p>
<p>알다시피 객체는 속성과 기능을 가진다. 그리고 하나의 객체는 다른 객체를 참조할 수도 있다. 따라서 객체지향적으로 상품과 카테고리를 모델링한다면 아래와 같은 형태가 더 자연스러울 것이다.</p>
<pre><code class="language-java">public class Product { 
    private int productId; 
    private String productName; 
    private Category category; 
    private int price; 
    private int stock; 
}</code></pre>
<p>상품 입장에서는 <code>categoryId</code>라는 숫자 값만 가지고 있는 것보다 <code>Category</code> 객체를 직접 참조하는 것이 더 객체지향적이다.</p>
<pre><code class="language-java">String categoryName = product.getCategory().getCategoryName();</code></pre>
<p>&nbsp;</p>
<p>하지만 관계형 데이터베이스에서는 객체 참조라는 개념이 없고, 대신 외래 키를 사용한다.</p>
<pre><code class="language-sql">SELECT * FROM product p 
JOIN category c ON p.category_id = c.category_id;</code></pre>
<p>즉, 객체는 참조를 통해 연관된 객체를 탐색하지만, 관계형 데이터베이스는 외래 키와 조인을 통해 연관된 데이터를 조회한다. 이 차이는 단순한 문법 차이가 아니다. 객체지향과 관계형 데이터베이스가 데이터를 바라보는 관점 자체가 다르기 때문에 발생하는 문제다.</p>
<p>&nbsp;</p>
<p>객체지향에서는 추상화, 캡슐화, 상속, 다형성, 참조, 객체 그래프 탐색 같은 개념을 중요하게 생각한다. 반면 관계형 데이터베이스는 데이터를 테이블에 정규화해서 저장하고, SQL을 통해 필요한 데이터를 집합적으로 조회하는 방식에 더 가깝다.</p>
<p>대표적인 차이를 정리하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th>객체지향</th>
<th>관계형 데이터베이스</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>필요한 데이터는 SQL로 명시적으로 조회해야 한다</td>
</tr>
<tr>
<td>객체는 상태와 행위를 함께 가진다</td>
<td>테이블은 주로 데이터 중심으로 구성된다</td>
</tr>
</tbody></table>
<p>이처럼 객체지향 프로그래밍과 관계형 데이터베이스는 지향하는 방향이 다르다. 이 차이를 흔히 <strong>패러다임의 불일치(Impedance Mismatch)</strong>라고 한다.</p>
<p>결국 개발자는 객체지향적으로 코드를 작성하고 싶지만, 데이터를 저장하고 조회하는 순간에는 결국 관계형 데이터베이스 구조에 맞춰 SQL을 작성해야 한다. 이 과정에서 객체 중심의 설계가 테이블 중심의 코드로 흔들리기 쉽다.</p>
<p>&nbsp;</p>
<h1 id="🛠️-jpa의-등장">🛠️ JPA의 등장</h1>
<p>이러한 문제를 해결하기 위해 등장한 기술이 JPA다. <strong>JPA</strong>(<strong>Java Persistence API)</strong>는 자바 진영의 ORM 기술 표준이다. 여기서 <strong>ORM(Object-Relational Mapping)은</strong> 말 그대로 객체와 관계형 데이터베이스를 매핑한다는 뜻이다.</p>
<p>즉, ORM은 객체와 테이블 사이의 차이를 중간에서 해결해주는 기술이다. JPA를 사용하면 개발자는 SQL과 JDBC API를 직접 다루는 대신, 객체를 중심으로 데이터를 저장하고 조회할 수 있다. 마치 자바 컬렉션에 객체를 저장하듯이 엔티티를 데이터베이스에 저장하고, 식별자를 이용해 객체를 조회할 수 있다.</p>
<p>JPA는 애플리케이션과 JDBC 사이에서 동작한다. 개발자가 JPA를 통해 엔티티를 저장하거나 조회하면, JPA가 내부적으로 적절한 SQL을 생성하고 JDBC API를 사용해서 데이터베이스와 통신한다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/235f43d8-6e54-4244-a480-0628f7e9e4f5/image.png" alt=""></p>
<p>&nbsp;</p>
<pre><code class="language-java">em.persist(product); 
Product product = em.find(Product.class, productId);</code></pre>
<p>여기서 <strong>em(엔티티 매니저)</strong>는 JPA에서 <strong>엔티티를 저장, 조회, 수정, 삭제할 때 사용하는 핵심 객체</strong>다.</p>
<p>&nbsp;</p>
<p>다만 여기서 한 가지 짚고 넘어갈 부분이 있다. JPA는 구현체가 아니라 자바 진영의 ORM 표준 인터페이스라는 점이다. 실제로 JPA를 구현한 대표적인 구현체로는 Hibernate가 있다. 스프링 부트에서 JPA를 사용한다고 할 때 대부분 내부적으로 Hibernate를 함께 사용한다.</p>
<p>따라서 JPA를 배운다는 것은 단순히 특정 라이브러리 사용법을 배우는 것이 아니라, 자바 객체와 관계형 데이터베이스를 어떻게 매핑하고 관리할 것인지에 대한 표준 방식을 배우는 것이라고 볼 수 있다.</p>
<p>&nbsp;</p>
<h2 id="🤔-왜-jpa를-사용해야-하는가">🤔 왜 JPA를 사용해야 하는가?</h2>
<h3 id="🏭-생산성">🏭 생산성</h3>
<p>첫 번째 이유는 생산성이다.</p>
<p>JDBC를 직접 사용할 때는 SQL 작성, 파라미터 바인딩, <code>ResultSet</code> 처리, 객체 매핑 코드를 반복해서 작성해야 했다. 반면 JPA를 사용하면 객체를 마치 자바 컬렉션에 저장하듯이 다룰 수 있다.</p>
<pre><code class="language-java">em.persist(product);  // 저장
Product product = em.find(Product.class, productId);  // 조회</code></pre>
<p>상품을 저장할 때 직접 INSERT 쿼리를 작성하지 않아도 된다. 상품을 조회할 때도 직접 SELECT 쿼리를 작성하고 <code>ResultSet</code>을 하나씩 꺼내 객체에 담는 작업을 하지 않아도 된다.</p>
<p>물론 JPA가 SQL을 완전히 몰라도 되게 만들어주는 것은 아니다. 기본적인 CRUD SQL과 반복적인 JDBC 코드를 직접 작성하는 부담을 크게 줄여주는 것이다.</p>
<p>복잡한 조회가 필요하거나 성능 최적화가 필요한 경우에는 JPQL, QueryDSL, Native SQL 등을 사용해야 할 수 있다. 또한 JPA가 실제로 어떤 SQL을 실행하는지 이해하는 능력도 중요하다.</p>
<p>즉, JPA는 SQL 지식을 대체하는 기술이라기보다 반복적인 SQL 작성과 객체 매핑 작업을 줄여주고, 개발자가 객체 중심으로 비즈니스 로직을 작성할 수 있도록 도와주는 기술이다.</p>
<p>&nbsp;</p>
<h3 id="👨🏻🔧-유지보수성">👨🏻‍🔧 유지보수성</h3>
<p>두 번째 이유는 유지보수성이다.</p>
<p>JDBC 방식에서는 필드 하나가 추가되어도 여러 부분을 수정해야 한다. 예를 들어 상품에 <code>description</code>이라는 설명 필드가 추가되었다고 해보자.</p>
<p>그러면 다음과 같은 코드를 모두 수정해야 한다.</p>
<ul>
<li>INSERT 쿼리</li>
<li>SELECT 쿼리</li>
<li><code>PreparedStatement</code> 파라미터 바인딩 코드</li>
<li><code>ResultSet</code> 매핑 코드</li>
<li>관련 DTO 또는 화면 출력 코드</li>
</ul>
<p>물론 JPA를 사용한다고 해서 모든 변경이 자동으로 해결되는 것은 아니다. 엔티티 필드를 추가하고, 필요한 경우 테이블 컬럼도 추가해야 한다. 하지만 JDBC를 직접 사용할 때처럼 모든 CRUD SQL과 매핑 코드를 일일이 수정하는 부담은 크게 줄어든다. 특히 단순한 CRUD에서는 JPA가 변경된 엔티티 구조를 기반으로 SQL을 생성해주기 때문에 반복적인 수정 작업이 줄어든다.</p>
<p>&nbsp;</p>
<h3 id="🎏-패러다임-불일치-해결">🎏 패러다임 불일치 해결</h3>
<p>세 번째 이유는 객체와 관계형 데이터베이스 사이의 패러다임 불일치를 줄여준다는 점이다.</p>
<p>객체는 객체답게 참조를 사용할 수 있고, 데이터베이스는 데이터베이스답게 외래 키를 사용할 수 있다. JPA는 이 둘 사이를 매핑해주기 때문에 개발자는 테이블 구조에만 맞춘 코드가 아니라 객체 관계를 중심으로 코드를 작성할 수 있다. </p>
<p>물론 JPA가 모든 패러다임 불일치를 완벽하게 없애주는 것은 아니다. 객체와 관계형 데이터베이스는 여전히 다른 모델이다. 하지만 JPA는 이 차이를 상당 부분 완화해주고, 개발자가 더 객체지향적인 방식으로 애플리케이션을 설계할 수 있도록 도와준다.</p>
<p>&nbsp;</p>
<h3 id="⚙️-성능-최적화">⚙️ 성능 최적화</h3>
<p>네 번째 이유는 성능 최적화 기능이다.</p>
<p>JPA는 애플리케이션과 데이터베이스 사이에서 다양한 최적화 기능을 제공한다. 대표적인 예가 <strong>1차 캐시</strong>다.</p>
<p>JPA는 엔티티를 영속성 컨텍스트라는 공간에서 관리한다. 같은 영속성 컨텍스트 안에서 동일한 식별자로 엔티티를 다시 조회하면, 데이터베이스를 다시 조회하지 않고 이미 관리 중인 엔티티를 반환한다.</p>
<pre><code class="language-java">int productId = 1; 
Product productA = em.find(Product.class, productId); 
Product productB = em.find(Product.class, productId);</code></pre>
<p>JDBC를 직접 사용했다면 같은 상품을 조회하는 쿼리가 두 번 실행될 수 있다. 하지만 JPA는 첫 번째 조회 결과를 영속성 컨텍스트에 보관하고, 두 번째 조회에서는 이미 관리 중인 객체를 반환할 수 있다. 스프링 환경에서는 같은 트랜잭션 범위 안에서 이러한 효과를 볼 수 있다.</p>
<p>또한 JPA는 변경 감지 기능도 제공한다. 엔티티를 조회한 뒤 값을 변경하면, 트랜잭션이 끝나는 시점에 JPA가 변경된 내용을 감지해서 필요한 UPDATE 쿼리를 실행한다.</p>
<pre><code class="language-java">Product product = em.find(Product.class, productId); 
product.changePrice(30000);</code></pre>
<p>위 코드에서 별도로 UPDATE 쿼리를 직접 작성하지 않아도, JPA는 엔티티의 변경 사항을 감지해서 데이터베이스에 반영할 수 있다.</p>
<p>이 외에도 JPA는 쓰기 지연, 지연 로딩, 즉시 로딩, 플러시 같은 다양한 기능을 제공한다. 이러한 기능들은 편리하지만 잘못 사용하면 성능 문제가 발생할 수도 있기 때문에 JPA의 동작 원리를 이해하는 것이 중요하다.</p>
<p>&nbsp;</p>
<h3 id="👤-데이터-접근-추상화와-벤더-독립성">👤 데이터 접근 추상화와 벤더 독립성</h3>
<p>다섯 번째 이유는 데이터 접근 추상화와 벤더 독립성이다.</p>
<p>관계형 데이터베이스는 MySQL, Oracle, PostgreSQL 등 여러 종류가 있다. 각 데이터베이스는 SQL 문법, 자료형, 함수, 페이징 방식 등이 조금씩 다르다. JDBC를 직접 사용하는 방식에서는 애플리케이션 코드가 특정 데이터베이스에 강하게 의존할 수 있다. 예를 들어 MySQL에 맞춰 작성한 SQL이 Oracle에서는 그대로 동작하지 않을 수 있다. </p>
<p>JPA는 이러한 차이를 줄이기 위해 <strong>Dialect</strong>라는 개념을 사용한다. Dialect는 데이터베이스 방언이라는 뜻으로, JPA 구현체가 특정 데이터베이스에 맞는 SQL을 생성할 수 있도록 도와준다.</p>
<p>예를 들어 사용하는 데이터베이스가 MySQL인지, PostgreSQL인지, Oracle인지 설정하면 JPA는 해당 데이터베이스에 맞는 SQL을 생성하려고 한다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/fb364d4d-7de2-4f00-b8c3-ea4992943f59/image.png" alt=""></p>
<p>물론 데이터베이스를 변경한다고 해서 모든 문제가 자동으로 해결되는 것은 아니다. 데이터베이스마다 지원하는 함수, 자료형, 락 동작, 인덱스 전략 등이 다를 수 있기 때문이다. 하지만 JPA는 데이터베이스별 차이를 상당 부분 추상화해주기 때문에, 순수 JDBC로 모든 SQL을 직접 작성하는 방식보다 특정 벤더에 대한 의존을 줄일 수 있다.</p>
<p>&nbsp;</p>
<h1 id="❓-jpa를-사용한다고-sql을-몰라도-될까">❓ JPA를 사용한다고 SQL을 몰라도 될까?</h1>
<p>JPA를 처음 접하면 <em>“이제 SQL을 몰라도 되는 것 아닌가?”</em> 라고 생각할 수 있다. 하지만 그렇지는 않은 것 같다. 학습하면서 JPA는 SQL을 직접 작성하는 부담을 줄여주는 기술이지, SQL과 데이터베이스 지식을 완전히 대체하는 기술은 아니라는 느낌이 들었다. </p>
<p>따라서 JPA를 제대로 사용하기 위해서는 다음과 같은 내용에 대한 이해가 필수인 것 같다.</p>
<ul>
<li>엔티티와 테이블이 어떻게 매핑되는지</li>
<li>연관관계를 어떻게 매핑하는지</li>
<li>영속성 컨텍스트가 무엇인지</li>
<li>지연 로딩과 즉시 로딩이 어떻게 동작하는지</li>
<li>JPQL이 어떤 SQL로 변환되는지</li>
<li>실제 실행되는 SQL이 성능상 문제가 없는지</li>
<li>트랜잭션 안에서 엔티티가 어떻게 관리되는지</li>
</ul>
<p>특히 성능 문제를 해결하려면 JPA가 내부적으로 실행하는 SQL을 반드시 확인할 수 있어야 한다. JPA를 사용하더라도 최종적으로 데이터베이스와 통신할 때는 SQL이 실행되기 때문이다. JPA를 잘 사용하기 위해 객체지향과 관계형 데이터베이스에 대한 학습도 게을리해서는 안 되겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링이 지원하는 프록시]]></title>
            <link>https://velog.io/@rocker_nun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%B4-%EC%A7%80%EC%9B%90%ED%95%98%EB%8A%94-%ED%94%84%EB%A1%9D%EC%8B%9C</link>
            <guid>https://velog.io/@rocker_nun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%B4-%EC%A7%80%EC%9B%90%ED%95%98%EB%8A%94-%ED%94%84%EB%A1%9D%EC%8B%9C</guid>
            <pubDate>Wed, 03 Jun 2026 16:05:28 GMT</pubDate>
            <description><![CDATA[<h1 id="💥-동적-프록시의-문제점">💥 동적 프록시의 문제점</h1>
<p>아래 3가지 질문에 대한 해결책을 생각해보자.</p>
<blockquote>
<p><strong>1번. 인터페이스가</strong> <strong>있는</strong> <strong>경우에는</strong> JDK <strong>동적</strong> <strong>프록시를</strong> <strong>적용하고</strong>, <strong>그렇지</strong> <strong>않은</strong> <strong>경우에는</strong> CGLIB<strong>를</strong> <strong>적용하려면</strong> <strong>어떻게</strong> <strong>해야할까</strong>?</p>
</blockquote>
<p>이전에는 개발자가 상황에 따라서 직접 JDK 동적 프록시를 사용할지 CGLIB를 사용할지 선택해야 했다. 하지만 스프링이 제공하는 프록시 팩토리(<code>ProxyFactory</code>)라는 기능을 사용하면 인터페이스가 있으면 JDK 동적 프록시를, 구체 클래스만 있다면 알아서 CGLIB를 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/b1181d62-7acc-44dc-8a76-8f7ae2fec239/image.png" alt=""></p>
<p>클라이언트는 이제 더 이상 JDK 동적 프록시나 CGLIB에 직접 프록시 생성을 요청할 필요가 없다. 그냥 프록시 팩토리에 요청하면 된다. </p>
<p>&nbsp;</p>
<blockquote>
<p><strong>2번. 두 기술을 함께 사용할 때 부가 기능을 적용하기 위해 JDK 동적 프록시가 제공하는 <code>InvocationHandler</code>와 CGLIB가 제공하는 <code>MethodInterceptor</code>를 각각 중복으로 따로 만들어야 할까?</strong></p>
</blockquote>
<p>느낌상 그래도 각각 만들어야 할 것 같지만 스프링은 이 문제를 해결하기 위해 부가기능을 적용할 때 어드바이스(<code>Advice</code>)라는 추상화된 새로운 개념을 도입했다. 이제 개발자는 <code>InvocationHandler</code>든 <code>MethodInterceptor</code>든 신경쓰지 않고 <code>Advice</code>만 만들면 된다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/e51d4c3d-1028-4b46-adb3-03818ab22725/image.png" alt=""></p>
<p>프록시 팩토리에서 생성된 프록시는 JDK 동적 프록시 같은 경우, 내부에 <code>adviceInvocationHandler</code>를 만들어 놓고, CGLIB도 <code>adviceMethodInterceptor</code>를 만들어 놓았다. 클라이언트가 JDK 동적 프록시나 CGLIB로 만들어진 프록시를 호출하면 각 프록시들은 각각 <code>adviceInvocationHandler</code>와 <code>adviceMethodInterceptor</code>를 호출하고, 이 둘은 <code>Advice</code>를 호출하는 것이다. 전체 흐름을 보자면 아래 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/fff0123a-ca02-46c1-ab72-75967c11b253/image.png" alt=""></p>
<p>&nbsp;</p>
<blockquote>
<p><strong>3번. 특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공되었으면?</strong></p>
</blockquote>
<p>앞서 패턴을 정의해서 메서드 이름이 매칭될 경우에만 부가기능을 사용할 수 있도록 처리했다. 하지만 스프링의 <code>PointCut</code>이라는 개념을 이용한다면 이 문제를 일관성있게 처리할 수 있다.</p>
<p>&nbsp;</p>
<h2 id="📝-예제-코드---advice">📝 예제 코드 - Advice</h2>
<p>일단 <code>Advice</code>를 만들어보자. 만드는 방법에는 여러가지가 있지만 가장 기본적인 방법은 <code>MethodInterceptor</code> 인터페이스를 구현하는 것이다. 기존의 CGLIB를 사용할 때의 <code>MethodInterceptor</code>와는 별개의 패키지에 있는 완전 다른 인터페이스다.</p>
<pre><code class="language-java">package org.aopalliance.intercept;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
    @Nullable
    Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}</code></pre>
<p>해당 인터페이스는 <code>Interceptor</code>를 상속 받고 있고, <code>Interceptor</code>는 다시 <code>Advice</code>를 상속 받고 있다. 파라미터로 <code>MethodInvocation</code>만 넘어가고 있는데 이 내부에는 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, <code>args</code> , 메서드 정보 등이 포함되어 있다. 기존에 파라미터로 제공되는 부분들이 이 안으로 모두 들어갔다고 생각하면 된다.</p>
<p>&nbsp;</p>
<p>이제 실제 <code>Advice</code>를 이용해서 테스트 코드를 작성해보자.</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.common.advice;

import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

@Slf4j
public class TimeAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info(&quot;TimeProxy 실행...&quot;);
        long startTime = System.currentTimeMillis();

        Object result = invocation.proceed();

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info(&quot;TimeProxy 종료 resultTime={}&quot;, resultTime);
        return result;
    }
}</code></pre>
<p>위 코드에서 <code>invocation.proceed()</code> 메서드를 호출하면 <code>target</code> 클래스를 호출하고 그 결과를 받을 수 있다. 그런데 <code>MethodInterceptor</code>는 타깃 객체를 직접 필드로 보관하지 않는다. 대신 <code>MethodInvocation</code> 객체 안에 메서드 정보, 인자 정보, 프록시 정보, 타깃 호출 체인 정보가 포함되어 있으며, <code>proceed()</code>를 호출하여 다음 호출 대상으로 위임한다. <code>MethodInterceptor</code>으로 메서드 정보와 함께 타깃 객체가 담긴 <code>MethodInvocation</code> 객체가 전달된다.</p>
<p><code>MethodInvocation</code>은 일종의 콜백 객체로, <code>MethodInvocation.proceed()</code>는 다음 인터셉터(Advice)가 존재하면 해당 인터셉터를 호출하고, 더 이상 인터셉터가 없으면 최종적으로 타깃 메서드를 호출한다. 그러면 <code>MethodInvocation</code>을 구현한 클래스는 일종의 공유 가능한 템플릿처럼 동작한다. 이 부분이 동적 프록시를 직접 만드는 방식보다 프록시 팩토리를 사용하는 방식의 가장 큰 차이점이자 장점이라고 할 수 있다. </p>
<p>&nbsp;</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.proxyfactory;

import com.example.advanced_spring.proxy.common.advice.TimeAdvice;
import com.example.advanced_spring.proxy.common.service.ConcreteService;
import com.example.advanced_spring.proxy.common.service.ServiceImpl;
import com.example.advanced_spring.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;

@Slf4j
public class ProxyFactoryTest {

    @DisplayName(&quot;인터페이스가 있으면 JDK 동적 프록시 사용&quot;)
    @Test
    void interfaceProxy() {
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvice(new TimeAdvice());
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

        log.info(&quot;targetClass={}&quot;, target.getClass());
        log.info(&quot;proxyClass={}&quot;, proxy.getClass());

        proxy.save();

        Assertions.assertThat(AopUtils.isAopProxy(proxy)).isTrue();
        Assertions.assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
        Assertions.assertThat(AopUtils.isCglibProxy(proxy)).isFalse();

        /**
         * 13:35:31.170 [Test worker] INFO com.example.advanced_spring.proxy.proxyfactory.ProxyFactoryTest -- targetClass=class com.example.advanced_spring.proxy.common.service.ServiceImpl
         * 13:35:31.172 [Test worker] INFO com.example.advanced_spring.proxy.proxyfactory.ProxyFactoryTest -- proxyClass=class jdk.proxy3.$Proxy13
         * 13:35:31.174 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 실행...
         * 13:35:31.174 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- save 호출...
         * 13:35:31.174 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
         */
    }
}
</code></pre>
<p><code>new ProxyFactory(target)</code>에서 프록시 팩토리를 생성할 때, 생성자에 프록시의 호출 대상을 함께 넘겨주고 있는 것을 볼 수 있다. 프록시 팩토리는 이 인스턴스 정보를 기반으로 프록시를 만들어 내고, 만약 이 인스턴스에 인터페이스가 있다면 JDK 동적 프록시를 기본으로 사용하고 인터페이스가 없고 구체 클래스만 있다면 CGLIB를 통해서 동적 프록시를 생성하는 것이다. </p>
<p>여기서 호출 대상이 <code>ServiceImpl</code>이고 해당 구현체는 <code>ServiceInterface</code> 인터페이스를 구현하고 있으므로 해당 인터페이스를 기반으로 한 JDK 동적 프록시를 생성한다.</p>
<p>그 후, <code>proxyFactory.addAdvice(new TimeAdvice())</code>를 통해 프록시 팩토리를 통해서 만든 프록시가 사용할 부가기능 로직을 설정한다. 프록시 팩토리에서 <code>MethodInterceptor</code>를 설정해줄 때는 일반적인 DI처럼 세터를 사용하는 대신, 이렇게 <code>addAdvice()</code>라는 메서드를 사용한다. 프록시 팩토리에는 여러 개의 <code>MethodInterceptor</code>를 추가가 가능하고, 이는 프록시 팩토리 하나만으로 여러 개의 부가기능을 제공하는 프록시를 생성할 수 있다는 말이다.</p>
<p>&nbsp;</p>
<p>쉽게 생각하면, JDK 동적 프록시가 제공하는 <code>InvocationHandler</code> 와 CGLIB가 제공하는
<code>MethodInterceptor</code> 의 개념과 유사하다. 이렇게 프록시가 제공하는 부가 기능 로직을 어드바이스(<code>Advice</code>)라 한다. </p>
<p>&nbsp;</p>
<p>이번에는 CGLIB를 적용한 테스트 코드를 작성해보자.</p>
<pre><code class="language-java">@DisplayName(&quot;구체 클래스가 있으면 CGLIB 사용&quot;)
@Test
void concreteProxy() {
    ConcreteService target = new ConcreteService();
    ProxyFactory proxyFactory = new ProxyFactory(target);
    proxyFactory.addAdvice(new TimeAdvice());
    ConcreteService proxy = (ConcreteService) proxyFactory.getProxy();

    log.info(&quot;targetClass={}&quot;, target.getClass());
    log.info(&quot;proxyClass={}&quot;, proxy.getClass());

    proxy.call();

    Assertions.assertThat(AopUtils.isAopProxy(proxy)).isTrue();
    Assertions.assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
    Assertions.assertThat(AopUtils.isCglibProxy(proxy)).isTrue();

    /**
     * 13:35:45.647 [Test worker] INFO com.example.advanced_spring.proxy.proxyfactory.ProxyFactoryTest -- targetClass=class com.example.advanced_spring.proxy.common.service.ConcreteService
     * 13:35:45.648 [Test worker] INFO com.example.advanced_spring.proxy.proxyfactory.ProxyFactoryTest -- proxyClass=class com.example.advanced_spring.proxy.common.service.ConcreteService$$SpringCGLIB$$0
     * 13:35:45.649 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 실행...
     * 13:35:45.649 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ConcreteService -- ConcreteService 호출...
     * 13:35:45.649 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
     */
}</code></pre>
<p><code>ConcreteService$$SpringCGLIB$$0</code>를 보면 CGLIB 프록시가 정상적으로 적용된 것을 확인할 수 있다.</p>
<p>&nbsp;</p>
<p>이제 <code>ProxyTargetClass</code> 옵션을 사용해서 인터페이스가 있어도 CGLIB 프록시를 생성하도록 처리해보자. 해당 옵션에 <code>true</code> 값을 넣으면 인터페이스가 있어도 강제로 CGLIB를 사용한다. 그리고 인터페이스가 아닌 클래스 기반의 프록시를 만들어준다.</p>
<pre><code class="language-java">@DisplayName(&quot;ProxyTargetClass 옵션을 이용하면 인터페이스가 있어도 CGLIB를 사용&quot;)
@Test
void proxyTargetClass() {
    ServiceInterface target = new ServiceImpl();
    ProxyFactory proxyFactory = new ProxyFactory(target);
    proxyFactory.setProxyTargetClass(true);  // 옵션 설정
    proxyFactory.addAdvice(new TimeAdvice());
    ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

    log.info(&quot;targetClass={}&quot;, target.getClass());
    log.info(&quot;proxyClass={}&quot;, proxy.getClass());

    proxy.save();

    Assertions.assertThat(AopUtils.isAopProxy(proxy)).isTrue();
    Assertions.assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
    Assertions.assertThat(AopUtils.isCglibProxy(proxy)).isTrue();

    /**
     * 13:36:21.783 [Test worker] INFO com.example.advanced_spring.proxy.proxyfactory.ProxyFactoryTest -- targetClass=class com.example.advanced_spring.proxy.common.service.ServiceImpl
     * 13:36:21.785 [Test worker] INFO com.example.advanced_spring.proxy.proxyfactory.ProxyFactoryTest -- proxyClass=class com.example.advanced_spring.proxy.common.service.ServiceImpl$$SpringCGLIB$$0
     * 13:36:21.786 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 실행...
     * 13:36:21.786 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- save 호출...
     * 13:36:21.786 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
     */
}</code></pre>
<p>&nbsp;</p>
<h1 id="🧑🧑🧒-pointcut-advice-advisor">🧑‍🧑‍🧒 PointCut, Advice, Advisor</h1>
<p><strong>어드바이스(Advice)</strong>: 타깃 객체에 적용하는 부가기능을 담은 객체다. 단순하게 프록시 로직이라 생각하면 된다. </p>
<p><strong>포인트컷(PointCut)</strong>: 어디에 부가 기능을 적용할지, 어디에 부가 기능을 적용하지 않을지 판단하는 필터링 로직이다. 주로 클래스와 메서드 이름으로 필터링 한다. 이름 그대로 어떤 포인트에 기능을 적용할지 하지 않을지 잘라서 구분하는 것이다.</p>
<p><strong>어드바이저(Advisor)</strong>: 포인트컷 1개와 어드바이스 1개를 합쳐 놓은 것이다. <del>왜 합친거지…?</del></p>
<p>&nbsp;</p>
<p>간단히 정리하면 부가 기능 로직을 적용해야 하는데, 포인트컷으로 어디에 적용할지 선택하고, 어드바이스로 어떤 로직을 적용할지 선택해야 한다. 이 모든 것을 알고 있는 것이 바로 <strong>어드바이저(Advisor)</strong>이다. 조언자(<code>Advisor</code>)는 어디(<code>Pointcut</code>)에 조언(<code>Advice</code>)을 해야할지 알고 있는 것이다.</p>
<p>&nbsp;</p>
<p>이렇게 구분한 것은 역할과 책임을 명확하게 분리한 것이다. 포인트컷은 대상 여부를 확인하는 필터 역할만 담당한다. 어드바이스는 깔끔하게 부가 기능 로직만 담당한다. 둘을 합치면 어드바이저가 된다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/fa4c4991-70bd-4a52-a95b-9366201e6e07/image.png" alt=""></p>
<p>호출 흐름을 간단하게 살펴보자면, 일단 클라이언트가 프록시를 호출하게 되면 프록시는 어드바이저 내부의 포인트컷을 호출한다. 메서드를 호출해도 된다는 확인을 받으면 그때 부가기능을 호출하고 필터링에 실패하면 부가기능을 호출하지 않고 바로 타겟 객체를 호출한다.</p>
<p>&nbsp;</p>
<h2 id="📝-예제-코드---advisor">📝 예제 코드 - Advisor</h2>
<p>어드바이저는 내부에 하나의 포인트컷과 하나의 어드바이스를 가지고 있다고 했다. 프록시 팩토리를 통해 프록시를 생성할 때 어드바이저를 제공하면 어디에 어떤 기능을 제공할지 알 수 있다.</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.advisor;

import com.example.advanced_spring.proxy.common.advice.TimeAdvice;
import com.example.advanced_spring.proxy.common.service.ServiceImpl;
import com.example.advanced_spring.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

@Slf4j
public class AdvisorTest {

    @Test
    void advisorTest1() {
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
        proxyFactory.addAdvisor(advisor);
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

        proxy.save();
        proxy.find();

        /**
         * 16:41:50.024 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 실행...
         * 16:41:50.026 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- save 호출...
         * 16:41:50.026 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
         *
         * 16:41:50.027 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 실행...
         * 16:41:50.027 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- find 호출...
         * 16:41:50.027 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
         */
    }
}
</code></pre>
<p>지금은 <code>Pointcut.TRUE</code>를 넘겨줌으로써 포인트컷의 필터링을 무조건 통과하도록 처리했다. 따라서 실행 시간 측정 부가기능을 수행하는 <code>TimeAdvice</code>가 정상적으로 동작하는 것을 확인할 수 있다.</p>
<p>&nbsp;</p>
<h2 id="📝-예제-코드---pointcut">📝 예제 코드 - <strong>PointCut</strong></h2>
<p>이번에는 <code>save()</code> 메서드에는 어드바이스를 호출하고, <code>find()</code> 메서드에는 호출하지 않도록 필터링 하는 포인트컷을 직접 만들어보도록 하자. 다행히도 스프링이 아래와 같이 <code>Pointcut</code> 인터페이스를 제공하고 있어서 깡구현은 하지 않아도 된다. </p>
<pre><code class="language-java">package org.springframework.aop;

public interface Pointcut {
    Pointcut TRUE = TruePointcut.INSTANCE;

    ClassFilter getClassFilter();

    MethodMatcher getMethodMatcher();
}</code></pre>
<p><code>Pointcut</code> 인터페이스는 내부에 <code>ClassFilter</code>와 <code>MethodMatcher</code>로 이루어져 있는데 클래스가 맞는지, 특정 메서드와 매칭이 되는지 확인할 때 사용한다. 이 2개의 관문을 모두 통과해야 어드바이스를 적용할 수 있게 되는 것이다. </p>
<p>&nbsp;</p>
<p><code>Pointcut</code> 인터페이스를 구현한 구현체를 스프링이 제공하지만 연습을 위해 이번만 직접 포인트컷을 만들어보자.</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.advisor;

import com.example.advanced_spring.proxy.common.advice.TimeAdvice;
import com.example.advanced_spring.proxy.common.service.ServiceImpl;
import com.example.advanced_spring.proxy.common.service.ServiceInterface;
import java.lang.reflect.Method;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

@Slf4j
public class AdvisorTest {

    ...

    @Test
    void advisorTest2() {
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(new MyPointcut(), new TimeAdvice());
        proxyFactory.addAdvisor(advisor);
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

        proxy.save();
        proxy.find();

        /**
         * 17:01:55.248 [Test worker] INFO com.example.advanced_spring.proxy.advisor.AdvisorTest -- 포인트컷 호출 method=save, targetClass=class com.example.advanced_spring.proxy.common.service.ServiceImpl
         * 17:01:55.251 [Test worker] INFO com.example.advanced_spring.proxy.advisor.AdvisorTest -- 포인트컷 결과 result=true
         * 17:01:55.252 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 실행...
         * 17:01:55.253 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- save 호출...
         * 17:01:55.253 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
         * 
         * 17:01:55.253 [Test worker] INFO com.example.advanced_spring.proxy.advisor.AdvisorTest -- 포인트컷 호출 method=find, targetClass=class com.example.advanced_spring.proxy.common.service.ServiceImpl
         * 17:01:55.253 [Test worker] INFO com.example.advanced_spring.proxy.advisor.AdvisorTest -- 포인트컷 결과 result=false
         * 17:01:55.253 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- find 호출...
         */
    }

    static class MyPointcut implements Pointcut {
        @Override
        public ClassFilter getClassFilter() {
            return ClassFilter.TRUE;
        }

        @Override
        public MethodMatcher getMethodMatcher() {
            return new MyMethodMatcher();
        }
    }

    static class MyMethodMatcher implements MethodMatcher {

        private String matchName = &quot;save&quot;;

        @Override
        public boolean matches(Method method, Class&lt;?&gt; targetClass) {
            boolean result = method.getName().equals(matchName);
            log.info(&quot;포인트컷 호출 method={}, targetClass={}&quot;, method.getName(), targetClass);
            log.info(&quot;포인트컷 결과 result={}&quot;, result);
            return result;
        }

        @Override
        public boolean isRuntime() {
            return false;
        }

        @Override
        public boolean matches(Method method, Class&lt;?&gt; targetClass, Object... args) {
            return false;
        }
    }
}</code></pre>
<p>&nbsp;</p>
<h2 id="📝-예제-코드---스프링이-제공하는-포인트컷">📝 예제 코드 - 스프링이 제공하는 포인트컷</h2>
<p>이번에는 스프링이 제공하는 <code>NameMatchMethodPointcut</code> 를 사용해서 구현해보자.</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.advisor;

import com.example.advanced_spring.proxy.common.advice.TimeAdvice;
import com.example.advanced_spring.proxy.common.service.ServiceImpl;
import com.example.advanced_spring.proxy.common.service.ServiceInterface;
import java.lang.reflect.Method;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;

@Slf4j
public class AdvisorTest {

    ...

    @Test
    void advisorTest3() {
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames(&quot;save&quot;);

        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
        proxyFactory.addAdvisor(advisor);
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

        proxy.save();
        proxy.find();

        /**
         * 17:15:55.486 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 실행...
         * 17:15:55.489 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- save 호출...
         * 17:15:55.489 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
         * 
         * 17:15:55.490 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- find 호출...
         */
    }

    ...
}
</code></pre>
<p>보다시피 <code>find()</code> 메서드일 경우에는 어드바이스가 호출되지 않는 것을 확인할 수 있다. 이처럼 스프링은 <code>NameMatchMethodPointcut</code>뿐만 아니라 여러 포인트컷 구현체를 제공하고 있다. 대표적인 구현체는 다음과 같다.</p>
<ul>
<li><p><code>NameMatchMethodPointcut</code> : 메서드 이름을 기반으로 매칭한다. 내부에서는 <code>PatternMatchUtils</code> 를 사용한다. (<code>*xxx*</code> 허용)</p>
</li>
<li><p><code>JdkRegexpMethodPointcut</code> : JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.</p>
</li>
<li><p><code>TruePointcut</code> : 항상 참을 반환한다.</p>
</li>
<li><p><code>AnnotationMatchingPointcut</code> : 애노테이션으로 매칭한다.</p>
</li>
<li><p><code>AspectJExpressionPointcut</code> : aspectJ 표현식으로 매칭한다.</p>
</li>
</ul>
<p>실무에서는 사용하기도 편리하고 기능도 가장 많은 aspectJ 표현식을 기반으로 사용하는 <code>AspectJExpressionPointcut</code>을 사용하게 된다.</p>
<p>&nbsp;</p>
<h2 id="📝-예제-코드---여러-어드바이저-적용">📝 예제 코드 - 여러 어드바이저 적용</h2>
<p>이번에는 처리 흐름을 아래 그림과 같이 타깃 객체에 여러 어드바이저를 적용하는 코드를 작성해보자. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/84640caf-f25a-479a-b2a8-60c90fa69d68/image.png" alt=""></p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.advisor;

import com.example.advanced_spring.proxy.common.service.ServiceImpl;
import com.example.advanced_spring.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

public class MultiAdvisorTest {

    @Test
    void multiAdvisorTest1() {
        // 프록시1 생성
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory1 = new ProxyFactory(target);
        DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
        proxyFactory1.addAdvisor(advisor1);
        ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();

        // 프록시2 생성
        ProxyFactory proxyFactory2 = new ProxyFactory(proxy1);
        DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
        proxyFactory1.addAdvisor(advisor2);
        ServiceInterface proxy2 = (ServiceInterface) proxyFactory2.getProxy();

        // 실행
        proxy2.save();

        /**
         * 23:33:12.266 [Test worker] INFO com.example.advanced_spring.proxy.advisor.MultiAdvisorTest$Advice1 -- advice1 호출...
         * 23:33:12.267 [Test worker] INFO com.example.advanced_spring.proxy.advisor.MultiAdvisorTest$Advice2 -- advice2 호출...
         * 23:33:12.268 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- save 호출...
         */
    }

    @Slf4j
    static class Advice1 implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            log.info(&quot;advice1 호출...&quot;);
            return invocation.proceed();
        }
    }

    @Slf4j
    static class Advice2 implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            log.info(&quot;advice2 호출...&quot;);
            return invocation.proceed();
        }
    }

}
</code></pre>
<p>&nbsp;</p>
<p>근데 위와 같이 여러 부가기능을 수행하기 위해서는 그 부가기능만큼 프록시를 생성하는 코드를 작성해야 하는 문제점이 있다. 다행히도 스프링은 하나의 프록시에 여러 어드바이저를 적용할 수 있도록 되어 있다. 그냥 적용할 어드바이저를 <code>addAdvisor()</code> 메서드를 이용해서 추가하기만 하면 된다.</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.advisor;

import com.example.advanced_spring.proxy.common.service.ServiceImpl;
import com.example.advanced_spring.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

public class MultiAdvisorTest {

    ...

    @Test
    void multiAdvisorTest2() {
        DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
        DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());

        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory1 = new ProxyFactory(target);

        proxyFactory1.addAdvisor(advisor1);
        proxyFactory1.addAdvisor(advisor2);
        ServiceInterface proxy = (ServiceInterface) proxyFactory1.getProxy();

        // 실행
        proxy.save();

        /**
         * 23:40:21.378 [Test worker] INFO com.example.advanced_spring.proxy.advisor.MultiAdvisorTest$Advice1 -- advice1 호출...
         * 23:40:21.380 [Test worker] INFO com.example.advanced_spring.proxy.advisor.MultiAdvisorTest$Advice2 -- advice2 호출...
         * 23:40:21.380 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- save 호출...
         */
    }

    ...
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/7cb95b6a-8b3a-463f-9df6-d2bf14554ab0/image.png" alt=""></p>
<p>보다시피 <code>multiAdvisorTest1()</code>, <code>multiAdvisorTest2()</code> 모두 여러 개의 어드바이저를 적용할 수 있지만, <code>multiAdvisorTest2()</code>처럼 <code>addAdvisor()</code>를 사용해서 적용할 어드바이저를 추가하는 것이 코드량도 훨씬 적고 성능도 더 좋다.</p>
<p>&nbsp;</p>
<h1 id="🏭-프록시-팩토리-적용">🏭 프록시 팩토리 적용</h1>
<p>이제 프록시 팩토리를 사용해서 애플리케이션에 프록시를 생성해보자.</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.config.v3_proxyfactory.advice;

import com.example.advanced_spring.trace.TraceStatus;
import com.example.advanced_spring.trace.logtrace.LogTrace;
import java.lang.reflect.Method;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class LogTraceAdvice implements MethodInterceptor {

    private final LogTrace logTrace;

    public LogTraceAdvice(LogTrace logTrace) {
        this.logTrace = logTrace;
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        TraceStatus status = null;

        try {
            Method method = invocation.getMethod();
            String message = method.getDeclaringClass().getName() + &quot;.&quot; + method.getName() + &quot;()&quot;;
            status = logTrace.begin(message);

            // 로직 호출
            Object result = invocation.proceed();

            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}
</code></pre>
<p>&nbsp;</p>
<h3 id="👤-인터페이스가-존재하는-경우">👤 인터페이스가 존재하는 경우</h3>
<pre><code class="language-java">@Slf4j
@Configuration
public class ProxyFactoryConfigV1 {

    @Bean
    public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
        OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
        ProxyFactory factory = new ProxyFactory(orderController);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderControllerV1 proxy = (OrderControllerV1) factory.getProxy();
        log.info(&quot;ProxyFactory proxy={}, target={}&quot;, proxy.getClass(), orderController.getClass());
        return proxy;
    }

    @Bean
    public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
        OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
        ProxyFactory factory = new ProxyFactory(orderService);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderServiceV1 proxy = (OrderServiceV1) factory.getProxy();
        log.info(&quot;ProxyFactory proxy={}, target={}&quot;, proxy.getClass(), orderService.getClass());
        return proxy;
    }
    @Bean
    public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
        OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();
        ProxyFactory factory = new ProxyFactory(orderRepository);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderRepositoryV1 proxy = (OrderRepositoryV1) factory.getProxy();
        log.info(&quot;ProxyFactory proxy={}, target={}&quot;, proxy.getClass(), orderRepository.getClass());
        return proxy;
    }

    private Advisor getAdvisor(LogTrace logTrace) {
        // 포인트컷
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames(&quot;request*&quot;, &quot;order*&quot;, &quot;save*&quot;);

        // 어드바이스
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);

        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}
</code></pre>
<p>현재 V1은 인터페이스만 존재하고 있기 때문에 애플리케이션을 실행해보면 프록시 팩토리가 JDK 동적 프록시가 적용되는 것을 확인할 수 있다. </p>
<p>&nbsp;</p>
<h3 id="💎-인터페이스가-없고-구현체만-존재하는-경우">💎 인터페이스가 없고, 구현체만 존재하는 경우</h3>
<pre><code class="language-java">@Slf4j
@Configuration
public class ProxyFactoryConfigV2 {

    @Bean
    public OrderControllerV2 orderControllerV2(LogTrace logTrace) {
        OrderControllerV2 orderController = new OrderControllerV2(orderServiceV2(logTrace));
        ProxyFactory factory = new ProxyFactory(orderController);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderControllerV2 proxy = (OrderControllerV2) factory.getProxy();
        log.info(&quot;ProxyFactory proxy={}, target={}&quot;, proxy.getClass(), orderController.getClass());
        return proxy;
    }

    @Bean
    public OrderServiceV2 orderServiceV2(LogTrace logTrace) {
        OrderServiceV2 orderService = new OrderServiceV2(orderRepositoryV2(logTrace));
        ProxyFactory factory = new ProxyFactory(orderService);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderServiceV2 proxy = (OrderServiceV2) factory.getProxy();
        log.info(&quot;ProxyFactory proxy={}, target={}&quot;, proxy.getClass(), orderService.getClass());
        return proxy;
    }
    @Bean
    public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {
        OrderRepositoryV2 orderRepository = new OrderRepositoryV2();
        ProxyFactory factory = new ProxyFactory(orderRepository);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderRepositoryV2 proxy = (OrderRepositoryV2) factory.getProxy();
        log.info(&quot;ProxyFactory proxy={}, target={}&quot;, proxy.getClass(), orderRepository.getClass());
        return proxy;
    }

    private Advisor getAdvisor(LogTrace logTrace) {
        // 포인트컷
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames(&quot;request*&quot;, &quot;order*&quot;, &quot;save*&quot;);

        // 어드바이스
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);

        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}
</code></pre>
<p>V2는 인터페이스가 없고 구현체만 존재하고 있기 때문에 프록시 팩토리가 CGLIB을 적용하는 것을 확인할 수 있다. 이로써 번거롭게 프록시를 일일이 생성해주지 않고 프록시 팩토리가 상황에 맞게 알맞은 방법으로 프록시를 생성해주도록 위임할 수 있게 되었고, 어드바이저, 어드바이스, 포인트컷 이라는 개념 덕분에 어떤 부가기능을 어디에 적용해야 할지 아주 명확하게 이해할 수 있게 되었다. </p>
<p>하지만 아직 남은 문제들이 몇 가지 남았는데, 위의 코드만 보더라도 각 방식을 적용하기 위해서는 설정 파일들을 일일이 만들어줘야 한다는 것이다. 스프링 빈으로 직접 등록하는 것도 모자라서, 프록시를 적용하는 코드까지 빈 생성 코드에 넣어줘야 한다. 그리고 컴포넌트 스캔을 사용하는 경우 지금까지 알고 있는 방법으로는 프록시 적용이 불가능하다. 왜냐하면 실제 객체를 컴포넌트 스캔으로 스프링 컨테이너에 스프링 빈으로 등록을 다 해버린 상태이기 때문이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Maven과 Gradle 이해하기]]></title>
            <link>https://velog.io/@rocker_nun/Maven%EA%B3%BC-Gradle-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@rocker_nun/Maven%EA%B3%BC-Gradle-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 16 May 2026 03:53:45 GMT</pubDate>
            <description><![CDATA[<h1 id="🎏-빌드-도구의-개념과-차이">🎏 빌드 도구의 개념과 차이</h1>
<p>eGovFrame VSCode Initializr로 Web 템플릿과 Boot 템플릿을 생성해보면서 자연스럽게 Maven과 Gradle도 함께 살펴보게 되었다.</p>
<p>처음에는 Maven과 Gradle을 단순히 <em>“프로젝트 빌드할 때 쓰는 도구”</em> 정도로만 생각했지만, 학습을 진행하며 둘은 같은 목적을 가지지만 접근 방식이 꽤 다르다는 것을 알 수 있었다.</p>
<p>또 eGovFrame 5.0 기반 프로젝트를 실행하려면 Tomcat 버전도 중요했다. 기존 Java EE의 <code>javax.*</code> 패키지가 Jakarta EE의 <code>jakarta.*</code> 패키지로 바뀌면서, Tomcat 10 이상이 필요해졌기 때문이다.</p>
<p>&nbsp;</p>
<h2 id="❓-빌드-도구가-왜-필요할까">❓ 빌드 도구가 왜 필요할까?</h2>
<p>Java 프로젝트를 만든다고 해서 단순히 <code>.java</code> 파일만 작성하면 끝나는 것은 아니다.</p>
<p>실제 프로젝트에서는 다음과 같은 작업이 필요하다.</p>
<ul>
<li>필요한 외부 라이브러리 다운로드</li>
<li>Java 소스 코드 컴파일</li>
<li>테스트 실행</li>
<li>정적 리소스 포함</li>
<li>JAR 또는 WAR 파일로 패키징</li>
<li>로컬 저장소 또는 원격 저장소에 배포</li>
<li>실행 환경에 맞는 설정 적용</li>
</ul>
<p>이런 많은 작업을 매번 개발자가 직접 하면 매우 번거로울 뿐만 아니라 실수도 많이 발생할 것이다. 그래서 등장한 것이 <strong>빌드 자동화 도구</strong>다.</p>
<p>빌드 자동화 도구는 프로젝트 설정 파일을 읽고, 필요한 라이브러리를 가져오고, 소스 코드를 컴파일하고, 최종 결과물인 JAR 또는 WAR 파일을 만들어준다. 그 대표적인 빌드 도구가 바로 Java 생태계에서는 <strong>Maven</strong>과 <strong>Gradle</strong>인 것이다.</p>
<p>&nbsp;</p>
<h2 id="🤔-maven이란">🤔 Maven이란?</h2>
<p>Maven은 Java 프로젝트를 빌드하고 관리하기 위한 대표적인 빌드 자동화 도구다. Maven은 <code>pom.xml</code> 파일을 중심으로 동작한다.</p>
<p><code>pom.xml</code>은 <strong>Project Object Model</strong>의 약자로, 프로젝트의 정보를 담고 있는 설정 파일이다. 여기에는 다음과 같은 정보가 들어간다.</p>
<ul>
<li>프로젝트 이름</li>
<li>프로젝트 버전</li>
<li>패키징 방식</li>
<li>의존성 목록</li>
<li>저장소 정보</li>
<li>빌드 플러그인</li>
<li>테스트 설정</li>
</ul>
<p>예를 들어 eGovFrame Web 템플릿의 <code>pom.xml</code>에는 다음과 같은 설정이 들어 있다.</p>
<pre><code class="language-xml">&lt;groupId&gt;com.egovframework&lt;/groupId&gt;
&lt;artifactId&gt;egovweb&lt;/artifactId&gt;
&lt;packaging&gt;war&lt;/packaging&gt;
&lt;version&gt;1.0.0&lt;/version&gt;</code></pre>
<p>&nbsp;</p>
<h3 id="🔄️-maven의-라이프사이클">🔄️ Maven의 라이프사이클</h3>
<p>Maven의 핵심은 <strong>정해진 규칙과 라이프사이클</strong>이다. Maven은 프로젝트가 어떤 흐름으로 빌드되어야 하는지 미리 정해두었다. </p>
<p>&nbsp;</p>
<p>대표적인 라이프사이클 단계는 다음과 같다.</p>
<ol>
<li><p><code>validate</code>: 프로젝트가 올바른지 확인한다.</p>
</li>
<li><p><code>compile</code>: Java 소스 코드를 컴파일한다.</p>
</li>
<li><p><code>test</code>: 테스트 코드를 실행한다.</p>
</li>
<li><p><code>package</code>: 컴파일된 결과를 JAR이나 WAR로 패키징한다.</p>
</li>
<li><p><code>verify</code>: 패키징 결과를 검증한다.</p>
</li>
<li><p><code>install</code>: 로컬 Maven 저장소에 설치한다.</p>
</li>
<li><p><code>deploy</code>: 원격 저장소에 배포한다.</p>
</li>
</ol>
<p>&nbsp;</p>
<p>예를 들어 <code>mvn clean package</code> 명령어를 실행하면 Maven은 아래와 같은 작업들을 순서대로 수행한다.</p>
<ol>
<li>clean 단계에서 기존 target 폴더 삭제</li>
<li>필요한 의존성 다운로드</li>
<li>Java 소스 코드 컴파일</li>
<li>테스트 처리</li>
<li>프로젝트를 JAR 또는 WAR로 패키징</li>
</ol>
<p>eGovFrame Web 템플릿에서는 <code>packaging</code>이 <code>war</code>이므로 최종 결과물로 <code>.war</code> 파일이 생성되고, Spring Boot Boot 템플릿에서는 실행 가능한 <code>.jar</code> 파일이 생성된다.</p>
<p>&nbsp;</p>
<h3 id="👪-maven의-부모-pom-이해하기">👪 Maven의 부모 POM 이해하기</h3>
<p>eGovFrame Web 템플릿의 <code>pom.xml</code>에서 중요한 부분은 부모 POM이다.</p>
<pre><code class="language-xml">&lt;parent&gt;
    &lt;groupId&gt;org.egovframe.web&lt;/groupId&gt;
    &lt;artifactId&gt;egovframe-web-config-parent&lt;/artifactId&gt;
    &lt;version&gt;5.0.0&lt;/version&gt;
&lt;/parent&gt;</code></pre>
<p>부모 POM은 여러 라이브러리의 버전을 한 곳에서 관리해준다.</p>
<p>&nbsp;</p>
<p>예를 들어 아래와 같은 의존성이 있다고 해보자.</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.egovframe.rte&lt;/groupId&gt;
    &lt;artifactId&gt;egovframe-rte-ptl-mvc&lt;/artifactId&gt;
&lt;/dependency&gt;</code></pre>
<p>여기에는 <code>&lt;version&gt;</code>이 없다. 그런데도 빌드가 된다. 그 이유는 부모 POM에서 해당 라이브러리의 버전을 관리해주기 때문이다.</p>
<p>이 방식의 장점은 명확하다. 여러 라이브러리의 버전을 프로젝트마다 직접 맞추지 않아도 된다는 것이다. 부모 POM에서 검증된 버전 조합을 관리하면, 하위 프로젝트는 그 조합을 그대로 사용할 수 있다. 이런 방식은 eGovFrame처럼 여러 실행환경 라이브러리와 Spring, MyBatis, Jakarta 관련 라이브러리를 함께 사용하는 프로젝트에서 매우 중요하다.</p>
<p>&nbsp;</p>
<h3 id="⛓️-maven의-scope-이해하기">⛓️ Maven의 scope 이해하기</h3>
<p>Maven 의존성에는 <code>scope</code>라는 개념이 있다. 예를 들어 Web 템플릿의 <code>pom.xml</code>을 보면 다음과 같은 설정이 나온다.</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;jakarta.servlet.jsp&lt;/groupId&gt;
    &lt;artifactId&gt;jakarta.servlet.jsp-api&lt;/artifactId&gt;
    &lt;scope&gt;provided&lt;/scope&gt;
&lt;/dependency&gt;</code></pre>
<p>여기서 <code>provided</code>는 <strong>컴파일할 때는 필요하지만, 실행할 때는 외부 환경이 제공한다</strong>는 뜻이다.</p>
<p>&nbsp;</p>
<p>예를 들어 Servlet API나 JSP API는 코드를 컴파일할 때 필요하다. 하지만 실제 실행 시점에는 Tomcat이 이미 Servlet/JSP 관련 기능을 제공한다. 따라서 이런 라이브러리를 WAR 파일 안에 또 넣으면 충돌이 발생할 수 있다.</p>
<p>정리하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th>scope</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>compile</code></td>
<td>컴파일과 실행 모두 필요. 기본값</td>
</tr>
<tr>
<td><code>provided</code></td>
<td>컴파일할 때만 필요. 실행 환경이 제공</td>
</tr>
<tr>
<td><code>test</code></td>
<td>테스트할 때만 필요</td>
</tr>
</tbody></table>
<p>&nbsp;</p>
<h3 id="🎭-maven의-장점과-단점">🎭 Maven의 장점과 단점</h3>
<ul>
<li><p><strong>장점</strong>: 규칙이 명확하다는 것이다. 프로젝트 구조, 빌드 단계, 의존성 관리 방식이 어느 정도 정해져 있다. 그래서 처음 보는 Maven 프로젝트라도 <code>pom.xml</code>을 보면 프로젝트가 어떤 라이브러리를 사용하고, 어떤 방식으로 빌드되는지 파악하기 쉽다. 또한 eGovFrame처럼 여러 라이브러리 버전을 안정적으로 맞춰야 하는 프로젝트에서는 부모 POM을 통한 버전 관리가 큰 장점이 된다.</p>
</li>
<li><p><strong>단점</strong>: XML 기반이라 설정이 길고 장황해질 수 있다. 또한 정해진 라이프사이클에 맞춰 동작하기 때문에 복잡한 커스텀 빌드 로직을 작성하기에는 유연성이 부족할 수 있다. 즉, Maven은 안정적이고 예측 가능하지만, 복잡한 빌드 자동화를 세밀하게 제어하고 싶을 때는 답답하게 느껴질 수 있다.</p>
</li>
</ul>
<p>&nbsp;</p>
<h2 id="🤔-gradle이란">🤔 Gradle이란?</h2>
<p>Gradle도 Maven처럼 Java 프로젝트를 빌드하고 의존성을 관리하는 빌드 자동화 도구다. 하지만 Maven과는 접근 방식이 다르다.</p>
<p>Maven이 <code>pom.xml</code>이라는 XML 파일에 정해진 구조로 빌드를 선언한다면, Gradle은 <code>build.gradle</code> 또는 <code>build.gradle.kts</code> 파일에 Groovy나 Kotlin DSL로 빌드 로직을 작성한다.</p>
<p>&nbsp;</p>
<p>예를 들어 Gradle에서는 의존성을 다음처럼 작성한다.</p>
<pre><code class="language-gradle">dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
    testImplementation &#39;org.junit.jupiter:junit-jupiter-api&#39;
}</code></pre>
<p>보다시피 Maven보다 문법이 짧고, 조건문이나 반복문 같은 프로그래밍적 표현도 사용할 수 있다. Gradle의 핵심은 <strong>태스크(Task)</strong>다. Gradle은 빌드를 여러 태스크의 조합으로 바라본다.</p>
<p>&nbsp;</p>
<p>예를 들어 다음과 같은 태스크가 있을 수 있다.</p>
<pre><code class="language-text">compileJava
processResources
test
jar
bootRun
build</code></pre>
<p>Gradle은 이 태스크들 사이의 의존 관계를 분석해서 필요한 작업만 실행한다.</p>
<p>&nbsp;</p>
<h3 id="🗃️-gradle-wrapper란">🗃️ Gradle Wrapper란?</h3>
<p>Gradle 프로젝트를 보면 다음과 같은 파일들을 <strong>Gradle Wrapper</strong>라고 한다.</p>
<pre><code class="language-text">gradlew
gradlew.bat
gradle/wrapper/gradle-wrapper.properties</code></pre>
<p>Gradle Wrapper가 있으면 로컬 PC에 Gradle이 설치되어 있지 않아도 프로젝트에서 지정한 Gradle 버전으로 빌드할 수 있다.</p>
<p>&nbsp;</p>
<p>예를 들어 <code>./gradlew build</code> 명령어를 실행하면 Wrapper가 필요한 Gradle 버전을 확인하고, 없으면 자동으로 다운로드한 뒤 빌드를 수행한다.</p>
<p>이 방식은 팀 프로젝트에서 매우 편리한데, 팀원마다 로컬에 설치된 Gradle 버전이 다르면 빌드 결과가 달라질 수 있는 부분들을 Wrapper를 사용하면 모든 팀원이 프로젝트에서 지정한 동일한 Gradle 버전으로 빌드할 수 있다.</p>
<p>CI/CD 환경에서도 마찬가지다. 빌드 서버에 Gradle을 따로 설치하지 않아도 <code>./gradlew build</code>만 실행하면 프로젝트가 요구하는 버전으로 빌드된다.</p>
<p>&nbsp;</p>
<h3 id="🏃♂️-gradle이-빠르다고-하는-이유">🏃‍♂️ Gradle이 빠르다고 하는 이유</h3>
<p>Gradle은 Maven보다 빌드 성능 측면에서 유리한 기능을 많이 제공한다.</p>
<p>대표적으로 다음과 같은 기능이 있다.</p>
<ul>
<li><p><code>Incremental Build</code>: 이전 빌드 이후 변경된 부분만 다시 빌드하는 방식이다. 예를 들어 소스 코드가 바뀌지 않았다면 굳이 같은 컴파일 작업을 다시 하지 않을 수 있다.</p>
</li>
<li><p><code>Build Cache</code>: 이미 수행한 빌드 결과를 저장해두었다가 같은 입력에 대해 다시 사용할 수 있게 해준다. 동일한 입력으로 동일한 결과가 나오는 작업이라면, Gradle은 매번 새로 작업하지 않고 캐시된 결과를 재사용할 수 있다.</p>
</li>
<li><p><code>Gradle Daemon</code>: Gradle 실행 프로세스를 백그라운드에 유지해서 매번 새로 JVM을 띄우는 비용을 줄여준다. 이런 기능 덕분에 규모가 큰 프로젝트에서는 Gradle이 Maven보다 빠르게 느껴질 수 있다. 다만 Gradle은 자유도가 높은 만큼 처음 배우는 사람에게는 Maven보다 어렵게 느껴질 수 있다. 빌드가 어떻게 동작하는지 이해하려면 태스크, 플러그인, DSL, Wrapper 구조를 함께 이해해야 하기 때문이다.</p>
</li>
</ul>
<p>&nbsp;</p>
<h2 id="📝-maven과-gradle의-차이">📝 Maven과 Gradle의 차이</h2>
<p>Maven과 Gradle은 모두 Java 프로젝트를 빌드하고 의존성을 관리하는 도구다. 둘의 목표는 비슷하지만 철학이 다르다.</p>
<p>Maven은 <strong>정해진 규칙을 따르는 선언형 빌드 도구</strong>에 가깝고, Gradle은 <strong>필요한 빌드 흐름을 코드처럼 구성할 수 있는 유연한 빌드 도구</strong>에 가깝다.</p>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>Maven</th>
<th>Gradle</th>
</tr>
</thead>
<tbody><tr>
<td>설정 파일</td>
<td><code>pom.xml</code></td>
<td><code>build.gradle</code>, <code>build.gradle.kts</code></td>
</tr>
<tr>
<td>설정 언어</td>
<td>XML</td>
<td>Groovy DSL 또는 Kotlin DSL</td>
</tr>
<tr>
<td>빌드 철학</td>
<td>정해진 라이프사이클 중심</td>
<td>태스크 그래프 중심</td>
</tr>
<tr>
<td>구조</td>
<td>규칙이 강하고 예측 가능</td>
<td>자유도가 높고 유연함</td>
</tr>
<tr>
<td>의존성 관리</td>
<td>안정적이고 명확함</td>
<td>Maven 저장소 사용 가능, 표현이 간결함</td>
</tr>
<tr>
<td>성능</td>
<td>단순하고 안정적이지만 반복 빌드가 느릴 수 있음</td>
<td>캐시, 증분 빌드, 데몬으로 빠른 빌드 가능</td>
</tr>
<tr>
<td>커스텀 빌드</td>
<td>XML 플러그인 설정이 장황할 수 있음</td>
<td>코드처럼 작성 가능</td>
</tr>
<tr>
<td>학습 난이도</td>
<td>초보자에게 상대적으로 이해하기 쉬움</td>
<td>DSL과 태스크 개념을 익혀야 함</td>
</tr>
<tr>
<td>공공/기업 레거시</td>
<td>많이 사용됨</td>
<td>점점 증가 중</td>
</tr>
<tr>
<td>eGovFrame 공식 구조</td>
<td>Maven 중심으로 이해하기 쉬움</td>
<td>별도 BOM/플러그인 설정 이해 필요</td>
</tr>
</tbody></table>
<p>&nbsp;</p>
<h3 id="👍-maven을-선택하면-좋은-경우">👍 Maven을 선택하면 좋은 경우</h3>
<ul>
<li>eGovFrame 프로젝트를 처음 학습하는 경우</li>
<li>공공/기업 프로젝트의 전통적인 구조를 익히고 싶은 경우</li>
<li>정해진 라이프사이클과 표준 구조를 따르고 싶은 경우</li>
<li><code>pom.xml</code> 기반 의존성 관리와 부모 POM 구조를 이해하고 싶은 경우</li>
</ul>
<p>&nbsp;</p>
<h3 id="👍-gradle을-선택하면-좋은-경우">👍 Gradle을 선택하면 좋은 경우</h3>
<ul>
<li>빌드 속도가 중요한 대규모 프로젝트인 경우</li>
<li>멀티 모듈 프로젝트를 효율적으로 관리하고 싶은 경우</li>
<li>커스텀 빌드 로직이 많은 경우</li>
<li>Spring Boot, Android, Kotlin 기반 프로젝트에 익숙한 경우</li>
<li>Gradle Wrapper로 팀의 빌드 환경을 통일하고 싶은 경우</li>
</ul>
<p>&nbsp;</p>
<h2 id="👀-egovframe-프로젝트에서-maven과-gradle을-어떻게-바라보면-좋을까">👀 eGovFrame 프로젝트에서 Maven과 Gradle을 어떻게 바라보면 좋을까?</h2>
<p>이번 실습을 통해 느낀 점은, eGovFrame 프로젝트를 처음 공부한다면 Maven부터 이해하는 것이 좋다는 것이다.</p>
<p>이유는 간단하다. eGovFrame Web 템플릿의 <code>pom.xml</code>은 부모 POM을 통해 버전을 관리하고, 전자정부 저장소와 실행환경 의존성을 명확하게 보여준다. 따라서 Maven 구조를 이해하면 eGovFrame 프로젝트가 어떤 라이브러리에 의존하고, 어떤 방식으로 빌드되는지 파악하기 쉽다.</p>
<p>Gradle도 충분히 사용할 수 있다. 하지만 Maven의 parent POM 상속 구조를 Gradle에서는 BOM 형태로 따로 설정해야 할 수 있다.</p>
<p>예를 들어 Gradle에서는 다음과 같이 플랫폼 의존성을 명시하는 방식이 필요할 수 있다.</p>
<pre><code class="language-gradle">dependencies {
    implementation platform(&#39;org.egovframe.boot:egovframe-dependencies:5.0.0&#39;)
    implementation &#39;org.egovframe.rte:egovframe-rte-ptl-mvc&#39;
}</code></pre>
<p>즉, Gradle은 유용하지만 eGovFrame 공식 구조를 이해하지 않은 상태에서 바로 변환하면 오히려 더 헷갈릴 수 있다.</p>
<p>&nbsp;</p>
<h2 id="🔥-tomcat-10과-jakarta-전환-이해하기">🔥 Tomcat 10과 Jakarta 전환 이해하기</h2>
<p>eGovFrame 5.0 기반 프로젝트를 실행할 때는 Tomcat 버전을 반드시 확인해야 한다. 결론부터 말하면 Tomcat 10 버전을 사용해야 한다. 왜 그럴까? 그냥 최신 버전이니까? 이유는 Tomcat 10부터 Java 웹 표준 API의 패키지 이름이 <code>javax.*</code>에서 <code>jakarta.*</code>로 바뀌었기 때문이다. </p>
<p>&nbsp;</p>
<p>예전 Java EE 기반 프로젝트에서는 다음과 같은 패키지를 사용했다.</p>
<pre><code class="language-java">import javax.servlet.http.HttpServletRequest;</code></pre>
<p>&nbsp;</p>
<p>하지만 Jakarta EE 기반 프로젝트에서는 패키지명이 다음처럼 바뀌었다.</p>
<pre><code class="language-java">import jakarta.servlet.http.HttpServletRequest;</code></pre>
<p>&nbsp;</p>
<p>즉, 단순히 이름만 바뀐 것이 아니라 Java 웹 생태계의 표준 패키지 네임스페이스가 <code>javax.*</code>에서 <code>jakarta.*</code>로 이동한 것이다. 이 차이 때문에 Tomcat 버전도 중요하다.</p>
<p>Tomcat 9 이하 버전은 기본적으로 <code>javax.*</code> 기반이고, Tomcat 10 이상부터 Jakarta 네임스페이스를 지원한다. eGovFrame 5.0은 Spring Framework 6 / Spring Boot 3 계열 흐름에 맞춰 Jakarta EE 기반으로 전환된 프로젝트이기 때문에 Tomcat 9 이하에서 실행하면 관련 클래스를 찾지 못하는 문제가 발생할 수 있는 것이다.</p>
<p>정리하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>주요 네임스페이스</th>
<th>대표 Tomcat 버전</th>
</tr>
</thead>
<tbody><tr>
<td>Java EE 기반</td>
<td><code>javax.*</code></td>
<td>Tomcat 9 이하</td>
</tr>
<tr>
<td>Jakarta EE 기반</td>
<td><code>jakarta.*</code></td>
<td>Tomcat 10 이상</td>
</tr>
</tbody></table>
<p>&nbsp;</p>
<p>근데 왜 굳이 <code>javax.*</code>에서 <code>jakarta.*</code>로 이동시킨걸까? 이 부분은 프로그래밍 얘기는 아니므로 그냥 그러려니 하고 넘어가자. </p>
<p>원래 처음 Java EE는 Sun Microsystems가 주도했고, 이후 Sun을 Oracle이 인수하면서 Oracle이 관리하게 됐는데 시간이 지나면서 Java EE의 발전 속도가 느려졌고, 오픈소스 커뮤니티 중심으로 더 빠르게 발전시키기 위해 Oracle이 Java EE를 Eclipse Foundation으로 이관했다. 이때 Java EE가 Jakarta EE로 이름이 바뀐 것이다. </p>
<p>자세히 말하자면, Eclipse 재단이 Java EE 기술을 넘겨받았지만, Java라는 이름과 <code>javax.*</code> 네임스페이스에 대한 권리는 Oracle이 가지고 있었기 때문에 Eclipse 재단은 Jakarta EE를 발전시키면서도 기존의 <code>javax.*</code> 패키지 이름을 계속 자유롭게 변경하거나 확장할 수 없었던 것이다. </p>
<p>쉽게 말해, 새 표준은 Eclipse 재단이 관리하지만, 패키지 이름은 Oracle의 Java 상표권과 얽혀 있는 약간 애매한 상황이 돼 버린건데, 그래서 결국 Jakarta EE는 기존 <code>javax.*</code>를 버리고 새로운 네임스페이스를 선택한 것이다.</p>
<p>Spring Framework 6, Spring Boot 3부터는 Jakarta EE 기반으로 전환돼서 Spring Boot 3 기반 프로젝트에서는 더 이상 예전 <code>javax.servlet.*</code>, <code>javax.validation.*</code>, <code>javax.persistence.*</code>를 쓰지 않고, <code>jakarta.*</code>를 사용한다. </p>
<p>eGovFrame 5.0도 이 흐름을 따라가면서 Jakarta 기반으로 전환된 것이다. 그래서 앞서 생성한 eGovFrame 5.0 Web 템플릿에서도 <code>jakarta.servlet</code>, <code>jakarta.validation</code> 같은 의존성이 등장하고, 외부 Tomcat도 Jakarta를 지원하는 Tomcat 10 이상이 필요해진 것이다.</p>
<p>&nbsp;</p>
<h2 id="▶️-tomcat-10-실행하기참고">▶️ Tomcat 10 실행하기(참고)</h2>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/090f48c9-11d9-455c-abd4-c5a8da22c2cf/image.png" alt=""></p>
<p>Tomcat을 실행할 때는 Tomcat 폴더의 <code>bin</code> 디렉터리로 이동한 뒤 <code>startup.sh</code>를 실행하면 된다.</p>
<pre><code class="language-bash">cd ~/dev/apache-tomcat-10.1.47/bin
chmod +x startup.sh
./startup.sh</code></pre>
<p>&nbsp;</p>
<p>정상 실행되면 아래와 같은 로그가 출력된다.</p>
<pre><code class="language-text">byeonguk@bagbyeong-ug-ui-MacBookPro bin % ./startup.sh
Using CATALINA_BASE:   /Users/byeonguk/dev/apache-tomcat-10.1.47
Using CATALINA_HOME:   /Users/byeonguk/dev/apache-tomcat-10.1.47
Using CATALINA_TMPDIR: /Users/byeonguk/dev/apache-tomcat-10.1.47/temp
Using JRE_HOME:        /Users/byeonguk/Library/Java/JavaVirtualMachines/temurin-24.0.2/Contents/Home
Using CLASSPATH:       /Users/byeonguk/dev/apache-tomcat-10.1.47/bin/bootstrap.jar:/Users/byeonguk/dev/apache-tomcat-10.1.47/bin/tomcat-juli.jar
Using CATALINA_OPTS:
Tomcat started.</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[eGovFrame Web 템플릿과 Boot 템플릿의 차이(WAR 방식 & JAR 방식)]]></title>
            <link>https://velog.io/@rocker_nun/Standard-WAR-vs-Spring-Boot-Executable-JAR-%EB%B9%8C%EB%93%9C-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@rocker_nun/Standard-WAR-vs-Spring-Boot-Executable-JAR-%EB%B9%8C%EB%93%9C-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Fri, 15 May 2026 04:18:46 GMT</pubDate>
            <description><![CDATA[<h1 id="🆚-war-vs-jar">🆚 WAR vs JAR</h1>
<p>eGovFrame VSCode Initializr를 사용해보면 프로젝트 생성 단계에서 여러 템플릿을 선택할 수 있다. 그중 가장 먼저 눈에 들어오는 차이는 <strong>Web 템플릿</strong>과 <strong>Boot 템플릿</strong>이다.</p>
<p>Web 템플릿은 전통적인 <strong>WAR 방식</strong>으로 배포하는 프로젝트이고, Boot 템플릿은 Spring Boot 기반의 <strong>JAR 방식</strong>으로 실행하는 프로젝트다.</p>
<p>처음에는 단순히 <code>.war</code>, <code>.jar</code> 와 같이 확장자만 다른 줄 알았는데, 직접 생성하고 실행해보니 두 방식은 단순히 파일 확장자만 다른 것이 아니었다. 결론적으로 핵심적인 차이는 다음 질문으로 정리할 수 있었다.</p>
<blockquote>
<p><strong>1. 애플리케이션을 누가 실행하는가?</strong>
<strong>2. 외부 Tomcat이 실행하는가, 아니면 애플리케이션이 스스로 서버를 포함해서 실행하는가?</strong></p>
</blockquote>
<p>&nbsp;</p>
<h2 id="🤔-war-방식이란">🤔 WAR 방식이란?</h2>
<p>위키백과에서 WAR의 정의는 다음과 같이 설명되어 있다. </p>
<blockquote>
<p><strong>웹 애플리케이션을 배포하기 위해 설계된 특수한 JAR 파일(.war 확장자)이다. 소프트웨어 공학에서 웹 애플리케이션(서블릿, 자바 클래스, JSP, HTML, CSS, JS, XML, 태그 라이브러리 및 기타 자원 등)을 하나의 파일로 패키징해 웹 서버에 배포하는 압축 파일 형식이다. 톰캣, 제티같은 웹 서버나 서블릿 컨테이너에서 실행할 수 있다.</strong></p>
</blockquote>
<p>쉽게 말해, <strong>WAR(Web Application Archive)</strong>는 Java 웹 애플리케이션을 배포하기 위해 필요한 파일들을 하나로 묶은 압축 파일이라고 생각하면 된다. 위의 설명대로 WAR 파일 안에는 보통 다음과 같은 것들이 들어간다.</p>
<ul>
<li>컴파일된 Java 클래스</li>
<li>JSP 파일</li>
<li>CSS, JavaScript, 이미지 같은 정적 리소스</li>
<li><code>web.xml</code> 같은 배포 설정 파일</li>
<li>애플리케이션에서 사용하는 라이브러리 JAR 파일</li>
</ul>
<p>&nbsp;</p>
<p>하지만 여기서 중요하게 생각해야 할 부분은 <strong>WAR 파일은 일반적으로 혼자 실행되지 않는다</strong>는 점이다. WAR 파일은 Tomcat, Jetty, JBoss 같은 <strong>외부 WAS(Web Application Server)</strong> 또는 서블릿 컨테이너에 배포되어야 실행된다. 즉, WAR 방식에서는 애플리케이션이 직접 서버를 실행하는 것이 아니라, 예를 들어 Tomcat이 먼저 실행되고 그 위에 애플리케이션이 올라가는 구조라는 것이다.</p>
<p>&nbsp;</p>
<p>흐름을 간단히 표현하면 다음과 같다.</p>
<ol>
<li>Tomcat 실행</li>
<li><code>webapps</code> 폴더에 WAR 배포</li>
<li>Tomcat이 WAR 압축 해제</li>
<li>web.xml 읽기</li>
<li>DispatcherServlet 등록</li>
<li>Spring MVC 애플리케이션 실행</li>
</ol>
<p>비유하자면 <strong>Tomcat은 건물을 관리하는 운영 시설</strong>이고, <strong>WAR 파일은 그 건물 안에 입주하는 하나의 서비스 공간</strong>이라고 할 수 있다. 서비스 공간은 혼자 존재할 수는 있지만, 실제로 손님을 받으려면 건물의 전기, 수도, 출입구, 관리 시스템이 필요하다. WAR 방식에서 그 역할을 하는 것이 Tomcat 같은 WAS라는 것이다.</p>
<p>&nbsp;</p>
<h2 id="🤔-jar-방식이란">🤔 JAR 방식이란?</h2>
<p>JAR는 다음과 같이 설명되어 있다.</p>
<blockquote>
<p><strong>여러개의 자바 클래스 파일과, 클래스들이 이용하는 관련 리소스(텍스트, 그림 등) 및 메타데이터를 하나의 파일로 모아서 자바 플랫폼에 응용 소프트웨어나 라이브러리를 배포하기 위한 소프트웨어 패키지 파일 포맷이다.</strong></p>
</blockquote>
<p>쉽게 말해, <strong>JAR(Java Archive)</strong>는 Java 클래스와 리소스를 하나로 묶는 일반적인 Java 압축 파일 형식이라는 것이다. 그런데 Spring Boot에서는 이 JAR 파일을 조금 특별하게 사용한다.</p>
<p>Spring Boot의 JAR는 단순한 라이브러리 묶음이 아니라, <strong>실행 가능한 JAR(Executable JAR)</strong>이다. 즉, 다음 명령어 하나로 애플리케이션을 바로 실행할 수 있다는 말이다.</p>
<pre><code class="language-bash">java -jar app.jar</code></pre>
<p>&nbsp;</p>
<p>이게 가능한 이유는 Spring Boot JAR 안에 애플리케이션 코드뿐만 아니라 <strong>내장 Tomcat</strong>도 함께 포함되기 때문이다. WAR 방식이 외부 Tomcat을 실행하고 그 위에서 WAR를 배포하면 애플리케이션이 실행되는 흐름이었다면, JAR 방식은 위의 명령어로 Spring Boot 애플리케이션을 시작하면 내장 Tomcat이 실행되고 애플리케이션이 실행되는 흐름인 것이다.</p>
<p>즉, JAR 방식에서는 실행의 주도권이 외부 Tomcat이 아니라 <strong>애플리케이션 자신</strong>에게 있다는 말이다. 이해를 위해 비유를 하자면 WAR 방식은 <strong><em>“건물에 입주하는 가게”</em></strong> 이고, JAR 방식은 <em><strong>“푸드트럭”</strong></em> 에 가깝다. 푸드트럭은 주방, 전기, 판매 공간을 어느 정도 스스로 갖고 있기 때문에 특정 건물에 입주하지 않아도 바로 영업할 수 있다. </p>
<p>이 구조는 클라우드, Docker, Kubernetes 같은 환경과 잘 어울린다. 서버에 별도로 Tomcat을 설치하고 설정할 필요 없이, JAR 파일 하나와 Java 실행 환경만 있으면 애플리케이션을 실행할 수 있기 때문이다.</p>
<p>&nbsp;</p>
<h2 id="🗂️-egovframe-initializr에서의-web-템플릿과-boot-템플릿">🗂️ eGovFrame Initializr에서의 Web 템플릿과 Boot 템플릿</h2>
<p>eGovFrame VSCode Initializr에서 직접 생성해본 프로젝트는 두 가지다.</p>
<h3 id="🏹-web-템플릿">🏹 Web 템플릿</h3>
<p>Web 템플릿은 전통적인 Java 웹 애플리케이션 방식이다. 빌드 결과물이 <code>.war</code> 파일로 만들어지고, 이 파일을 외부 Tomcat 같은 WAS에 배포해서 실행한다.</p>
<p>Web 템플릿은 다음과 같은 특징을 가진다.</p>
<ul>
<li>외부 Tomcat에 배포한다.</li>
<li><code>src/main/webapp</code> 폴더가 중요하다.</li>
<li><code>WEB-INF/web.xml</code>이 존재한다.</li>
<li><code>dispatcher-servlet.xml</code> 같은 Spring MVC XML 설정이 있다.</li>
<li>화면은 JSP 기반이다.</li>
<li>Maven으로 빌드하면 WAR 파일이 생성된다.</li>
</ul>
<p>&nbsp;</p>
<h3 id="✨-boot-템플릿">✨ Boot 템플릿</h3>
<p>Boot 템플릿은 Spring Boot 기반 프로젝트다. 빌드 결과물이 <code>.jar</code> 파일로 만들어지고, 애플리케이션 내부에 내장 Tomcat이 포함되어 있어 <code>java -jar</code> 명령어만으로 실행할 수 있다.</p>
<p>Boot 템플릿은 다음과 같은 특징을 가진다.</p>
<ul>
<li>외부 Tomcat에 배포하지 않아도 된다.</li>
<li>애플리케이션 내부에 내장 Tomcat이 포함된다.</li>
<li><code>main()</code> 메서드가 애플리케이션의 시작점이다.</li>
<li>XML 설정보다 Java Config와 <code>application.properties</code> 중심이다.</li>
<li>화면은 Thymeleaf 기반이다.</li>
<li>Maven 또는 Gradle로 빌드하면 실행 가능한 JAR 파일이 생성된다.</li>
</ul>
<p>&nbsp;</p>
<h2 id="🔍-egovframe-web-템플릿-구조">🔍 eGovFrame Web 템플릿 구조</h2>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/938faf24-b551-46ed-81f5-f1fa8d3821b4/image.png" alt=""></p>
<p>eGovFrame VSCode Initializr에서 Web 템플릿을 선택해 프로젝트를 생성하면 대략 다음과 같은 구조가 만들어진다.</p>
<pre><code class="language-text">egov-web/
├── pom.xml
├── src/main/
│   ├── java/egovframework/example/
│   │   ├── cmmn/
│   │   └── sample/
│   │       ├── service/
│   │       │   ├── EgovSampleService.java
│   │       │   ├── SampleVO.java
│   │       │   ├── SampleDefaultVO.java
│   │       │   └── impl/
│   │       │       ├── EgovSampleServiceImpl.java
│   │       │       └── SampleMapper.java
│   │       └── web/
│   │           └── EgovSampleController.java
│   │
│   ├── resources/
│   │   └── egovframework/
│   │       ├── spring/
│   │       │   ├── context-common.xml
│   │       │   ├── context-datasource.xml
│   │       │   ├── context-mapper.xml
│   │       │   ├── context-transaction.xml
│   │       │   └── context-properties.xml
│   │       └── sqlmap/example/
│   │           └── mappers/
│   │               └── EgovSample_Sample_SQL.xml
│   │
│   └── webapp/
│       ├── WEB-INF/
│       │   ├── web.xml
│       │   ├── config/egovframework/springmvc/
│       │   │   └── dispatcher-servlet.xml
│       │   └── jsp/egovframework/example/sample/
│       │       ├── egovSampleList.jsp
│       │       └── egovSampleRegister.jsp
│       ├── index.jsp
│       ├── css/
│       ├── js/
│       ├── fonts/
│       └── img/
└── target/</code></pre>
<p>&nbsp;</p>
<h3 id="🔀-web-템플릿의-요청-처리-흐름">🔀 Web 템플릿의 요청 처리 흐름</h3>
<p>Web 템플릿에서 사용자가 브라우저로 접근하면 요청은 다음 흐름으로 처리된다.</p>
<ol>
<li>사용자가 브라우저에서 URL 요청</li>
<li>Tomcat이 요청 수신</li>
<li>web.xml의 설정에 따라 DispatcherServlet으로 요청 전달</li>
<li>DispatcherServlet이 Controller 탐색</li>
<li>EgovSampleController 실행</li>
<li>EgovSampleService 호출</li>
<li>EgovSampleServiceImpl 실행</li>
<li>SampleMapper 호출</li>
<li>MyBatis가 EgovSample_Sample_SQL.xml의 SQL 실행</li>
<li>HSQLDB에서 데이터 조회</li>
<li>결과를 Model에 담아 JSP로 전달</li>
<li>JSP가 HTML을 렌더링</li>
<li>브라우저에 응답</li>
</ol>
<p>&nbsp;</p>
<p>이 흐름에서 중요한 점은 <strong>Tomcat이 먼저 요청을 받고, 애플리케이션은 Tomcat 내부에서 실행된다</strong>는 것이다.</p>
<p>&nbsp;</p>
<h3 id="🔨-web-템플릿-빌드와-배포">🔨 Web 템플릿 빌드와 배포</h3>
<p>WAR 방식 프로젝트는 Maven으로 빌드할 수 있다. 프로젝트 루트에서 다음 명령어를 실행한다.</p>
<pre><code class="language-bash">mvn clean package</code></pre>
<p>여기서 <code>clean</code>은 이전 빌드 결과물인 <code>target</code> 폴더를 정리하는 단계이고, <code>package</code>는 프로젝트를 컴파일하고 배포 가능한 파일로 묶는 단계다.</p>
<p>&nbsp;</p>
<p>빌드가 성공하면 <code>target</code> 폴더 아래에 다음과 같은 WAR 파일이 생성된다.</p>
<pre><code class="language-text">target/egovweb-1.0.0.war</code></pre>
<p>이 파일이 바로 Tomcat에 배포할 수 있는 결과물이다.</p>
<p>&nbsp;</p>
<p>Tomcat이 실행 중이라면, 생성된 WAR 파일을 Tomcat의 <code>webapps</code> 폴더에 복사한다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/97fdec8b-b95a-4bb7-ac2e-e6375432350b/image.png" alt=""></p>
<p>Tomcat은 <code>webapps</code> 폴더에 들어온 WAR 파일을 감지하고 자동으로 압축을 해제한다. 그리고 WAR 파일 이름을 기준으로 컨텍스트 경로를 만든다.</p>
<p>예를 들어 파일 이름이 <code>egovweb-1.0.0.war</code>이라면 <code>http://localhost:8080/egovweb-1.0.0/</code> 경로로 접속해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/2800efdd-cc8e-4995-9afc-ac8a150b92e3/image.png" alt=""></p>
<p>여기서 <code>egovweb-1.0.0</code> 부분을 <strong>컨텍스트 패스(Context Path)</strong>라고 한다. 만약 운영 환경에서 <code>/</code> 경로로 바로 접근하고 싶다면 WAR 파일 이름을 <code>ROOT.war</code>로 변경하거나 Tomcat 설정을 수정할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/888e2bf7-9642-4fe0-bbe6-db269368ea2c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/2b32fc80-683c-4ca8-9474-1de2907f5078/image.png" alt=""></p>
<p>&nbsp;</p>
<h2 id="🔍-egovframe-boot-템플릿-구조">🔍 eGovFrame Boot 템플릿 구조</h2>
<p>Boot 템플릿은 Spring Boot 기반 프로젝트다. 위에서도 말했지만 가장 큰 차이는 <strong>외부 Tomcat에 배포하지 않아도 된다</strong>는 점이다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/c1a4eb08-d947-4d22-990d-38e60da8d893/image.png" alt=""></p>
<p>Boot 템플릿을 생성하면 대략 다음과 같은 구조를 볼 수 있다.</p>
<pre><code class="language-text">boot-web/
├── pom.xml
├── src/main/
│   ├── java/egovframework/example/
│   │   ├── EgovBootApplication.java
│   │   ├── config/
│   │   │   ├── EgovConfigDatasource.java
│   │   │   ├── EgovConfigMapper.java
│   │   │   ├── EgovConfigTransaction.java
│   │   │   └── EgovConfigWeb.java
│   │   └── sample/
│   │       ├── service/
│   │       └── web/
│   │
│   └── resources/
│       ├── application.properties
│       ├── static/
│       │   ├── css/
│       │   ├── js/
│       │   ├── fonts/
│       │   └── img/
│       └── templates/
│           └── egovframework/example/sample/
│               ├── egovSampleList.html
│               └── egovSampleRegister.html
└── target/</code></pre>
<p>&nbsp;</p>
<p>Web 템플릿과 비교해보니 몇 가지 차이가 바로 보였다.</p>
<ol>
<li><code>src/main/webapp</code>이 중심이 아니다.</li>
<li>정적 리소스는 <code>src/main/resources/static</code>에 있다.</li>
<li>동적 화면 템플릿은 <code>src/main/resources/templates</code>에 있다.</li>
<li><code>web.xml</code>이 없다.</li>
<li>XML 설정보다 Java Config가 많다.</li>
<li>JSP 대신 Thymeleaf를 사용한다.</li>
</ol>
<p>즉, Web 템플릿이 XML 설정 중심이라면, Boot 템플릿은 Java 클래스와 <code>application.properties</code> 중심이다.</p>
<p>&nbsp;</p>
<h3 id="❓-egovbootapplicationjava의-의미">❓ EgovBootApplication.java의 의미</h3>
<p>Boot 템플릿에서 가장 중요한 파일은 <code>EgovBootApplication.java</code>다.</p>
<pre><code class="language-java">@SpringBootApplication
public class EgovBootApplication {

    public static void main(String[] args) {
        SpringApplication.run(EgovBootApplication.class, args);
    }
}</code></pre>
<p>이 파일은 애플리케이션의 시작점이다.</p>
<p>WAR 방식에서는 Tomcat이 먼저 실행되고, Tomcat이 <code>web.xml</code>을 읽어 애플리케이션을 로딩했다. 하지만 Boot 방식에서는 <code>main()</code> 메서드가 직접 실행된다. 그리고 <code>SpringApplication.run()</code>이 Spring 컨텍스트를 만들고 내장 Tomcat을 실행한다.</p>
<p>즉, 실행 주도권이 외부 Tomcat에서 애플리케이션 내부로 이동한다. 이 차이가 WAR와 JAR 방식의 가장 핵심적인 차이다.</p>
<p>&nbsp;</p>
<h3 id="🔀-boot-템플릿의-요청-처리-흐름">🔀 Boot 템플릿의 요청 처리 흐름</h3>
<p>Boot 템플릿에서는 외부 Tomcat이 먼저 요청을 받지 않는다. 애플리케이션 내부에서 내장 Tomcat이 실행되고, 그 내장 Tomcat이 요청을 처리한다.</p>
<p>흐름을 그림처럼 표현하면 다음과 같다.</p>
<pre><code class="language-text">[사용자 브라우저]
      ↓
[Spring Boot JAR 실행 프로세스]
      ↓
[내장 Tomcat]
     ↓
[DispatcherServlet]
      ↓
[Controller]
    ↓
[Service]
    ↓
[Mapper]
    ↓
[Database]
     ↓
[Thymeleaf View]
       ↓
[사용자 브라우저]</code></pre>
<p>WAR 방식과 비교하면 외부 Tomcat이 Spring Boot JAR 실행 프로세스 내부의 내장 Tomcat으로 바뀐 것이라고 생각하면 된다.</p>
<p>&nbsp;</p>
<h3 id="▶️-boot-템플릿-실행하기">▶️ Boot 템플릿 실행하기</h3>
<p>앞서 말했다시피 Boot 템플릿은 외부 Tomcat에 배포할 필요가 없다. 개발 중에는 다음 명령어로 실행할 수 있다.</p>
<pre><code class="language-bash">mvn spring-boot:run</code></pre>
<p>또는 Gradle 프로젝트라면 다음 명령어를 사용할 수 있다.</p>
<pre><code class="language-bash">./gradlew bootRun</code></pre>
<p>&nbsp;</p>
<p>빌드 후 실행하려면 다음 흐름을 사용한다.</p>
<pre><code class="language-bash">mvn clean package
java -jar target/boot-web-1.0.0.jar</code></pre>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/ca91c649-a022-409b-9951-7c065b714791/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/cd099eeb-d99b-4c13-bcc3-cea254cb75ce/image.png" alt=""></p>
<p>이 방식은 배포가 단순하다. 서버에 JAR 파일 하나를 올리고 그냥 <code>java -jar</code>로 실행하면 된다. 외부 Tomcat을 따로 설치하고 WAR를 복사하는 등과 같은 복잡한 과정이 필요 없다.</p>
<p>&nbsp;</p>
<h2 id="💥-war-방식과-jar-방식의-핵심-차이">💥 WAR 방식과 JAR 방식의 핵심 차이</h2>
<p>WAR 방식과 JAR 방식의 차이는 단순히 확장자가 다르다는 정도로 이해하면 부족하다.</p>
<p>가장 중요한 차이는 <strong>실행 주체</strong>인데, WAR 방식은 외부 Tomcat이 애플리케이션을 실행하고, JAR 방식은 애플리케이션이 내장 Tomcat을 포함하고 직접 실행된다는 점을 반드시 기억하자.</p>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>WAR 방식</th>
<th>JAR 방식</th>
</tr>
</thead>
<tbody><tr>
<td>파일 의미</td>
<td>Web Application Archive</td>
<td>Java Archive</td>
</tr>
<tr>
<td>주로 사용되는 형태</td>
<td>전통적인 Java 웹 애플리케이션</td>
<td>Spring Boot 실행 애플리케이션</td>
</tr>
<tr>
<td>실행 주체</td>
<td>외부 WAS</td>
<td>애플리케이션 자체</td>
</tr>
<tr>
<td>서버 포함 여부</td>
<td>포함하지 않음</td>
<td>내장 Tomcat 포함</td>
</tr>
<tr>
<td>실행 명령</td>
<td>Tomcat에 배포 후 실행</td>
<td><code>java -jar app.jar</code></td>
</tr>
<tr>
<td>배포 방식</td>
<td>WAR 파일을 <code>webapps</code>에 복사</td>
<td>JAR 파일을 서버에서 직접 실행</td>
</tr>
<tr>
<td>설정 중심</td>
<td><code>web.xml</code>, XML 설정</td>
<td><code>application.properties</code>, Java Config</td>
</tr>
<tr>
<td>화면 기술 예시</td>
<td>JSP</td>
<td>Thymeleaf</td>
</tr>
<tr>
<td>환경 의존성</td>
<td>외부 WAS 설정에 영향 받음</td>
<td>애플리케이션이 실행 환경을 많이 포함</td>
</tr>
<tr>
<td>운영 방식</td>
<td>하나의 WAS에 여러 WAR 배포 가능</td>
<td>하나의 프로세스가 하나의 앱을 실행하는 구조에 적합</td>
</tr>
<tr>
<td>어울리는 환경</td>
<td>전통적인 공공/기업 WAS 운영 환경</td>
<td>클라우드, 컨테이너, MSA, CI/CD 환경</td>
</tr>
</tbody></table>
<p>&nbsp;</p>
<h3 id="👍-war-방식이-어울리는-경우">👍 WAR 방식이 어울리는 경우</h3>
<p>WAR 방식은 다음과 같은 상황에서 자연스럽다.</p>
<ul>
<li>이미 조직에서 표준 Tomcat/WAS를 운영하고 있는 경우</li>
<li>여러 웹 애플리케이션을 하나의 WAS에서 함께 관리하는 경우</li>
<li>기존 공공 SI 또는 레거시 Spring MVC 구조를 유지해야 하는 경우</li>
<li><code>web.xml</code>, JSP, 외부 WAS 배포 방식에 익숙한 조직인 경우</li>
</ul>
<p>&nbsp;</p>
<h3 id="👍-jar-방식이-어울리는-경우">👍 JAR 방식이 어울리는 경우</h3>
<p>JAR 방식은 다음과 같은 상황에서 유리하다.</p>
<ul>
<li>애플리케이션을 독립 실행하고 싶은 경우</li>
<li>Docker 컨테이너로 배포하고 싶은 경우</li>
<li>서버마다 Tomcat을 따로 설치하고 관리하고 싶지 않은 경우</li>
<li>CI/CD 파이프라인에서 빌드 산출물을 단순하게 관리하고 싶은 경우</li>
<li>Spring Boot 자동 설정과 내장 서버 구조를 적극 활용하고 싶은 경우</li>
</ul>
<p>&nbsp;</p>
<h3 id="🎏-jsp와-thymeleaf의-차이참고">🎏 JSP와 Thymeleaf의 차이(참고)</h3>
<p>Web 템플릿과 Boot 템플릿은 화면 기술에서도 차이가 있다. Web 템플릿은 JSP를 사용하고, Boot 템플릿은 Thymeleaf를 사용한다.</p>
<ol>
<li><code>JSP</code>: 오래된 Java 웹 화면 기술이다. JSP 파일은 바로 HTML로 실행되는 것이 아니라, Tomcat 내부에서 Java Servlet 코드로 변환되고 컴파일된 뒤 실행된다. JSP는 전통적인 Java 웹 프로젝트에서 많이 사용되었고, JSTL 같은 태그 라이브러리를 통해 반복문, 조건문, 메시지 출력 등을 처리할 수 있다. 하지만 HTML 안에 서버 전용 문법이 많이 들어가면 프론트엔드 개발자가 단독으로 화면을 확인하기 어려울 수 있다.</li>
</ol>
<p>&nbsp;</p>
<ol start="2">
<li><code>Thymeleaf</code>: Spring Boot에서 많이 사용하는 서버 사이드 템플릿 엔진이다. 가장 큰 특징은 HTML 파일 자체가 자연스럽게 유지된다는 점이다. 예를 들어 <code>&lt;td th:text=&quot;${sample.name}&quot;&gt;샘플 이름&lt;/td&gt;</code>와 같이 작성할 수 있다. 브라우저는 <code>th:text</code>라는 속성을 몰라도 무시하고, 안에 있는 “샘플 이름” 텍스트를 보여준다. 반면 Spring Boot가 실행될 때는 Thymeleaf가 <code>th:text</code>를 해석해서 실제 데이터로 치환한다. 이런 방식을 <strong>Natural Template</strong>이라고 한다. 덕분에 Thymeleaf는 서버를 실행하지 않아도 HTML 구조를 어느 정도 확인할 수 있고, 디자이너나 프론트엔드 담당자와 협업하기 편하다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Bean 순환 참조(Circular Dependency) 문제 해결하기]]></title>
            <link>https://velog.io/@rocker_nun/Spring-Bean-%EC%88%9C%ED%99%98-%EC%B0%B8%EC%A1%B0Circular-Dependency</link>
            <guid>https://velog.io/@rocker_nun/Spring-Bean-%EC%88%9C%ED%99%98-%EC%B0%B8%EC%A1%B0Circular-Dependency</guid>
            <pubDate>Wed, 06 May 2026 02:37:53 GMT</pubDate>
            <description><![CDATA[<h1 id="🤔-순환-참조circular-dependency란">🤔 순환 참조(Circular Dependency)란?</h1>
<p>Spring을 공부하다 보면 한 번쯤 이런 에러를 만나게 된다.</p>
<pre><code class="language-text">***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  AService defined in file [/Users/byeonguk/Desktop/egov-vscode2026/circular-reference-demo/target/classes/com/example/circular/bad/AService.class]
↑     ↓
|  BService defined in file [/Users/byeonguk/Desktop/egov-vscode2026/circular-reference-demo/target/classes/com/example/circular/bad/BService.class]
└─────┘

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name &#39;AService&#39;: Requested bean is currently in creation: Is there an unresolvable circular reference?</code></pre>
<p>또는 Spring Boot 2.6 이상 환경에서는 애플리케이션 실행 중 다음과 비슷한 메시지를 볼 수 있다.</p>
<pre><code class="language-text">The dependencies of some of the beans in the application context form a cycle</code></pre>
<p>처음 이 에러를 보면 단순히 <em>“Bean 주입이 꼬였나?”</em> 정도로 생각할 수 있다. 하지만 Spring Bean 순환 참조는 단순한 설정 오류가 아니다. 오히려 이 에러는 애플리케이션 내부의 객체들이 서로 너무 강하게 얽혀 있고, 책임 분리가 제대로 되어 있지 않다는 신호에 가깝다.</p>
<p>이번 글에서는 Spring Bean 순환 참조가 무엇인지, 왜 발생하는지, Spring 컨테이너 내부에서는 어떤 일이 일어나는지, 그리고 실무에서는 어떻게 해결해야 하는지 정리해보려고 한다.</p>
<p>&nbsp;</p>
<h2 id="🫛-spring-bean-이해하기">🫛 Spring Bean 이해하기</h2>
<p>순환 참조를 이해하려면 먼저 <strong>Spring Bean</strong>이 무엇인지부터 정리해야 한다. 알디시피 Spring에서는 개발자가 직접 객체를 생성하고 관리하기보다, Spring 컨테이너가 객체의 생성과 의존성 주입을 대신 관리한다.</p>
<p>예를 들어 다음과 같은 클래스가 있다고 하자.</p>
<pre><code class="language-java">@Service
public class OrderService {
    private final PaymentService paymentService;

    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}</code></pre>
<p><code>OrderService</code>는 직접 <code>new PaymentService()</code>를 호출하지 않는다. 대신 생성자를 통해 <code>PaymentService</code>를 주입받는다. 이때 <code>OrderService</code>와 <code>PaymentService</code> 같은 객체를 Spring 컨테이너가 관리하면 이를 <strong>Bean</strong>이라고 부른다.</p>
<p>Spring 컨테이너는 애플리케이션이 시작될 때 <code>@Component</code>, <code>@Service</code>, <code>@Repository</code>, <code>@Controller</code>, <code>@Configuration</code> 같은 어노테이션이 붙은 클래스를 스캔한다. 그리고 이 클래스들을 기반으로 Bean을 생성하고, 필요한 의존성을 자동으로 연결해준다. </p>
<p>이것이 바로 Spring의 핵심 개념인 <strong>IoC(Inversion of Control, 제어의 역전)</strong>와 <strong>DI(Dependency Injection, 의존성 주입)</strong>이다. 개발자가 객체 생성과 연결을 직접 제어하는 것이 아니라, Spring 컨테이너가 객체의 생명주기와 의존 관계를 관리하는 것이다.</p>
<p>&nbsp;</p>
<h2 id="🔄-순환-참조란-무엇인가">🔄 순환 참조란 무엇인가?</h2>
<p>순환 참조는 이름 그대로 <strong>의존성의 흐름이 원처럼 순환하는 구조</strong>를 말한다.</p>
<p>가장 단순한 예시는 다음과 같다.</p>
<pre><code class="language-text">A → B → A</code></pre>
<p>A는 B가 필요하고, B는 다시 A가 필요한 구조다.</p>
<p>&nbsp;</p>
<p>코드로 보면 다음과 같다.</p>
<pre><code class="language-java">@Service
public class AService {
    private final BService bService;

    public AService(BService bService) {
        this.bService = bService;
    }
}</code></pre>
<pre><code class="language-java">@Service
public class BService {
    private final AService aService;

    public BService(AService aService) {
        this.aService = aService;
    }
}</code></pre>
<p><code>AService</code>를 만들려면 <code>BService</code>가 필요하다. 그런데 <code>BService</code>를 만들려면 다시 <code>AService</code>가 필요하다.</p>
<p>&nbsp;</p>
<p>Spring 컨테이너 입장에서는 이런 상황이 된다.</p>
<ol>
<li>_&quot;AService를 만들려고 했더니 BService가 필요하네?&quot; _ </li>
<li><em>&quot;그럼 BService를 먼저 만들자.&quot;</em></li>
<li>_&quot;그런데 BService를 만들려고 했더니 AService가 필요하네?&quot;  _</li>
<li>_&quot;다시 AService를 만들어야 하나?&quot;  _</li>
<li><em>&quot;그런데 AService를 만들려면 BService가 필요한데?&quot;</em></li>
</ol>
<p>결국 누구를 먼저 만들어야 하는지 결정할 수 없는 상황, 이것이 바로 <strong>Spring Bean 순환 참조</strong>다. <em>“닭이 먼저냐, 달걀이 먼저냐”</em> 문제와 아주 비슷하다. A를 만들려면 B가 필요하고, B를 만들려면 A가 필요하다. 둘 중 하나가 먼저 완성되어야 다른 하나를 만들 수 있는데, 둘 다 서로가 먼저 필요하다고 말하는 상황이다.</p>
<p>&nbsp;</p>
<h2 id="💣-생성자-주입에서-순환-참조가-더-치명적인-이유">💣 생성자 주입에서 순환 참조가 더 치명적인 이유</h2>
<p>Spring에서는 의존성을 주입하는 방식이 여러 가지 있다.</p>
<p>대표적으로 <strong>생성자 주입(Constructor Injection)</strong>, <strong>세터 주입(Setter Injection)</strong>, <strong>필드 주입(Field Injection)</strong>가 있다. 이 중 현대 Spring 개발에서는 생성자 주입이 가장 권장된다. 생성자 주입은 필수 의존성을 객체 생성 시점에 반드시 전달받도록 강제할 수 있다. 또한 필드를 <code>final</code>로 선언할 수 있어 객체의 불변성을 지키기 좋고, 테스트 코드 작성도 쉽다.</p>
<p>&nbsp;</p>
<p>하지만 순환 참조 상황에서는 생성자 주입이 가장 빠르게 문제를 드러낸다.</p>
<pre><code class="language-java">@Service
public class OrderService {
    private final UserService userService;

    public OrderService(UserService userService) {
        this.userService = userService;
    }
}</code></pre>
<pre><code class="language-java">@Service
public class UserService {
    private final OrderService orderService;

    public UserService(OrderService orderService) {
        this.orderService = orderService;
    }
}</code></pre>
<p><code>OrderService</code> 객체를 생성하려면 생성자 인자로 <code>UserService</code>가 필요하다. 그런데 <code>UserService</code> 객체를 생성하려면 생성자 인자로 <code>OrderService</code>가 필요하다.</p>
<p>생성자 주입에서는 객체가 완성되기 전에 필요한 의존성이 모두 준비되어 있어야 한다. 따라서 서로가 서로의 생성자 인자로 필요한 상황에서는 객체를 하나도 완성할 수 없다.</p>
<p>그래서 생성자 주입 기반의 순환 참조는 Spring 컨테이너가 해결하기 어렵고, 애플리케이션 시작 단계에서 바로 예외가 발생한다. 불편해 보일 수 있지만, 사실 이건 좋은 신호이기도 하다. 생성자 주입은 잘못된 의존 관계를 애플리케이션 실행 초기에 명확히 드러내기 때문이다.</p>
<p>&nbsp;</p>
<h2 id="💉-세터-주입과-필드-주입">💉 세터 주입과 필드 주입</h2>
<p>과거 Spring Boot 2.5 이하 환경에서는 세터 주입이나 필드 주입을 사용하면 순환 참조가 동작하는 경우가 있었다.</p>
<p>예를 들어 다음과 같은 코드다.</p>
<pre><code class="language-java">@Service
public class AService {
    @Autowired
    private BService bService;
}</code></pre>
<pre><code class="language-java">@Service
public class BService {
    @Autowired
    private AService aService;
}</code></pre>
<p>생성자 주입과 달리 필드 주입은 객체를 먼저 만든 뒤 나중에 필드를 채운다.</p>
<p>&nbsp;</p>
<p>즉, Spring 컨테이너는 다음과 같은 방식으로 처리할 수 있다.</p>
<ol>
<li>AService 객체를 일단 만든다.</li>
<li>아직 bService 필드는 비어 있다.</li>
<li>BService 객체를 만든다.</li>
<li>BService에 AService의 미완성 참조를 넣는다.</li>
<li>BService 생성이 끝난다.</li>
<li>다시 AService로 돌아와 bService 필드를 채운다.</li>
</ol>
<p>이런 방식이 가능했던 이유는 Spring 내부에 <strong>3단계 캐시 메커니즘</strong>이 있었기 때문이다.</p>
<p>&nbsp;</p>
<h3 id="🏰-spring의-3단계-캐시-메커니즘">🏰 Spring의 3단계 캐시 메커니즘</h3>
<p>Spring은 싱글톤 Bean을 생성하고 관리하기 위해 내부적으로 여러 캐시를 사용한다. 순환 참조와 관련해서 자주 언급되는 것이 바로 <strong>3단계 캐시</strong>다.</p>
<table>
<thead>
<tr>
<th>캐시 단계</th>
<th>내부 변수명</th>
<th>저장 대상</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>1단계 캐시</td>
<td><code>singletonObjects</code></td>
<td>완전히 생성된 Bean</td>
<td>인스턴스화, 의존성 주입, 초기화까지 끝난 정상 Bean</td>
</tr>
<tr>
<td>2단계 캐시</td>
<td><code>earlySingletonObjects</code></td>
<td>조기 참조 Bean</td>
<td>아직 완전히 초기화되지는 않았지만 참조 가능한 미완성 Bean</td>
</tr>
<tr>
<td>3단계 캐시</td>
<td><code>singletonFactories</code></td>
<td>ObjectFactory</td>
<td>필요할 때 조기 참조 객체를 만들어 반환할 수 있는 팩토리</td>
</tr>
</tbody></table>
<p>이 구조를 아주 단순하게 설명하면 다음과 같다.</p>
<p>Spring은 A Bean을 만들기 시작하면, 아직 완전히 완성되지 않았더라도 “A라는 객체의 참조를 나중에 꺼낼 수 있는 통로”를 3단계 캐시에 등록해둔다.</p>
<p>이후 B Bean을 만들다가 A Bean이 필요해지면, Spring은 완성된 A Bean이 없더라도 3단계 캐시에서 A의 조기 참조를 꺼내 B에 주입한다.</p>
<p>그 후 B가 완성되면 다시 A로 돌아와 B를 주입하고 A도 완성한다.</p>
<p>즉, Spring은 Java 객체가 참조 타입이라는 특성을 이용해서 미완성 객체의 참조를 임시로 넘겨주는 방식으로 일부 순환 참조를 해결해왔다. 하지만 이 방식은 어디까지나 제한적인 우회 방식이다.</p>
<p>&nbsp;</p>
<h3 id="❓-aop가-끼어들면-왜-더-복잡해질까">❓ AOP가 끼어들면 왜 더 복잡해질까?</h3>
<p>Spring 애플리케이션에서는 <code>@Transactional</code>, <code>@Async</code> 같은 기능을 자주 사용한다. 이런 기능은 Spring AOP를 통해 동작한다. Spring은 원본 객체를 그대로 사용하는 것이 아니라, 원본 객체를 감싼 <strong>프록시 객체</strong>를 만들어 사용한다.</p>
<p>예를 들어 <code>OrderService</code>에 <code>@Transactional</code>이 붙어 있다면, Spring 컨테이너가 관리하는 실제 Bean은 순수한 <code>OrderService</code> 객체가 아니라 트랜잭션 기능이 적용된 프록시 객체일 수 있다.</p>
<p>문제는 순환 참조 상황에서 조기 참조로 노출된 객체와 최종적으로 컨테이너에 등록되는 객체가 달라질 수 있다는 점이다.</p>
<p>예를 들어 B Bean이 A Bean의 조기 참조를 주입받았는데, 그 시점의 A는 아직 프록시가 적용되기 전의 원본 객체라고 해보자. 이후 A의 초기화가 끝나면서 Spring이 A를 프록시 객체로 감싼다. 그러면 컨테이너가 관리하는 공식 A Bean은 프록시 객체인데, B 내부에 들어 있는 A는 원본 객체일 수 있다.</p>
<p>이렇게 되면 트랜잭션, 비동기 처리, 보안 같은 AOP 기능이 기대한 대로 동작하지 않을 수 있다. Spring이 내부적으로 이런 문제를 줄이기 위한 복잡한 방어 로직을 갖고 있지만, 모든 상황을 완벽히 안전하게 처리하기는 어렵다. 그래서 순환 참조는 단순히 Spring이 알아서 해결해주면 되는 문제로 보면 안 된다.</p>
<p>&nbsp;</p>
<h2 id="🚫-spring-boot-26부터-순환-참조가-기본적으로-금지된-이유">🚫 Spring Boot 2.6부터 순환 참조가 기본적으로 금지된 이유</h2>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/854b5848-9e6f-4084-9559-6691e6cbb289/image.png" alt=""></p>
<p>Spring Boot 2.6부터는 순환 참조가 기본적으로 금지되었다. 이전에는 Spring이 일부 순환 참조를 내부 캐시를 통해 해결해주었지만, 이제는 애플리케이션 시작 시점에 순환 참조를 발견하면 기본적으로 실패하도록 바뀌었다. 이 정책 변화는 매우 중요하다.</p>
<p>Spring이 순환 참조를 막는 이유는 개발자를 불편하게 만들기 위해서가 아니라 오히려 애플리케이션 구조의 문제를 더 빨리 발견하게 하기 위해서다. 순환 참조는 대부분 다음과 같은 설계 문제를 의미한다.</p>
<ul>
<li>두 객체의 책임이 명확히 분리되어 있지 않다.</li>
<li>서로 다른 도메인이 너무 강하게 결합되어 있다.</li>
<li>한 클래스가 너무 많은 일을 하고 있다.</li>
<li>의존성 방향이 정리되어 있지 않다.</li>
</ul>
<p>&nbsp;</p>
<p>즉, 순환 참조는 단순 에러라기보다 <strong>코드 스멜(Code Smell)</strong>에 가깝다.</p>
<p>Spring Boot 2.6의 기본 차단 정책은 이런 구조적 문제를 애플리케이션 실행 초기에 드러내는 <strong>Fail-fast 전략</strong>이라고 볼 수 있다. Fail-fast 전략이란 문제가 있다면 가능한 한 빨리 실패하게 해서, 더 큰 장애로 번지기 전에 개발자가 문제를 수정하도록 하는 방식을 말한다.</p>
<p>&nbsp;</p>
<h2 id="👀-allow-circular-referencestrue는-해결책일까">👀 allow-circular-references=true는 해결책일까?</h2>
<p>릴리즈 노트를 보면 알 수 있듯이 Spring Boot 2.6 이상에서도 설정을 통해 순환 참조를 다시 허용할 수 있다.</p>
<pre><code class="language-yaml">spring:
  main:
    allow-circular-references: true</code></pre>
<p>또는 <code>application.properties</code>에서는 다음처럼 설정할 수 있다.</p>
<pre><code class="language-properties">spring.main.allow-circular-references=true</code></pre>
<p>이 설정을 켜면 Spring은 예전처럼 순환 참조를 해결하려고 시도한다.</p>
<p>&nbsp;</p>
<p>하지만 이 설정은 근본 해결책이 아니다. 해당 설정은 일단 애플리케이션을 실행시키기 위한 임시방편이다. 레거시 프로젝트를 Spring Boot 2.6 이상으로 마이그레이션하는 상황이라면 일시적으로 사용할 수는 있다. 하지만 장기적으로는 순환 참조 구조를 제거하는 방향으로 리팩토링해야 한다.</p>
<p>이 설정에 계속 의존하면 시스템 내부의 잘못된 의존 관계가 숨겨진 채로 남게 된다. 시간이 지날수록 구조는 더 복잡해지고, 나중에는 작은 변경 하나에도 여러 서비스가 연쇄적으로 영향을 받는 상황이 될 수 있다.</p>
<p>따라서 <code>allow-circular-references=true</code>는 문제를 해결하는 스위치가 아니라, 리팩토링 시간을 벌기 위한 임시 안전장치로만 사용하는 것이 바람직하다.</p>
<p>&nbsp;</p>
<h2 id="📝-순환-참조가-발생하는-대표적인-설계-문제">📝 순환 참조가 발생하는 대표적인 설계 문제</h2>
<p>순환 참조는 우연히 발생하지 않는다. 대부분 객체 간 책임이 명확하지 않거나, 도메인 경계가 흐릿할 때 발생한다.</p>
<p>대표적인 예시를 보자.</p>
<pre><code class="language-java">@Service
public class UserService {
    private final OrderService orderService;

    public UserService(OrderService orderService) {
        this.orderService = orderService;
    }

    public void withdraw(Long userId) {
        if (orderService.hasActiveOrders(userId)) {
            throw new IllegalStateException(&quot;진행 중인 주문이 있어 탈퇴할 수 없습니다.&quot;);
        }
        // 회원 탈퇴 처리
    }
}</code></pre>
<pre><code class="language-java">@Service
public class OrderService {
    private final UserService userService;

    public OrderService(UserService userService) {
        this.userService = userService;
    }

    public void createOrder(Long userId) {
        userService.validateUserStatus(userId);
        // 주문 생성 처리
    }
}</code></pre>
<p>위 구조에서 <code>UserService</code>는 회원 탈퇴를 위해 주문 정보를 확인해야 한다. 반대로 <code>OrderService</code>는 주문 생성을 위해 회원 상태를 검증해야 한다.</p>
<p>말만 들었을 때는 굉장히 자연스러워 보인다. 하지만 구조적으로 들여다 보면 두 서비스가 서로의 도메인 로직에 깊게 관여하고 있다. <code>UserService</code>는 회원 관리에 집중해야 하고, <code>OrderService</code>는 주문 관리에 집중해야 한다. 그런데 서로의 내부 기능을 직접 호출하면서 양방향 의존성이 생긴다. 이런 구조는 시간이 지날수록 유지보수가 어려워진다.</p>
<p>&nbsp;</p>
<h3 id="🚥-해결책-1-lazy-애노테이션-사용">🚥 해결책 1: @Lazy 애노테이션 사용</h3>
<p>가장 간단한 우회 방법 중 하나는 <code>@Lazy</code>를 사용하는 것이다.</p>
<pre><code class="language-java">@Service
public class OrderService {
    private final UserService userService;

    public OrderService(@Lazy UserService userService) {
        this.userService = userService;
    }
}</code></pre>
<p><code>@Lazy</code>를 사용하면 Spring은 실제 <code>UserService</code> 객체를 즉시 주입하지 않고, 프록시 객체를 대신 주입한다. 실제 <code>UserService</code>는 해당 메서드가 처음 호출되는 시점에 생성된다. 이렇게 하면 애플리케이션 시작 시점의 순환 참조 문제를 우회할 수 있다.</p>
<p>하지만 <code>@Lazy</code> 애노테이션 역시 근본 해결책은 아니다. 문제 발생 시점이 애플리케이션 시작 단계에서 런타임으로 미뤄질 수 있다. 또한 실제 메서드가 처음 호출되는 순간 객체 초기화가 발생하면서 예상치 못한 지연이 생길 수도 있다.</p>
<p>따라서 <code>@Lazy</code>는 리팩토링이 당장 어려운 레거시 코드에서 제한적으로 사용하는 것이 좋다.</p>
<p>&nbsp;</p>
<h3 id="🚚-해결책-2-objectprovider-사용">🚚 해결책 2: ObjectProvider 사용</h3>
<p><code>ObjectProvider</code>를 사용하면 필요한 Bean을 즉시 주입받는 대신, 필요한 시점에 직접 조회할 수 있다.</p>
<pre><code class="language-java">@Service
public class OrderService {
    private final ObjectProvider&lt;UserService&gt; userServiceProvider;

    public OrderService(ObjectProvider&lt;UserService&gt; userServiceProvider) {
        this.userServiceProvider = userServiceProvider;
    }

    public void createOrder(Long userId) {
        UserService userService = userServiceProvider.getIfAvailable();
        if (userService == null) {
            throw new IllegalStateException(&quot;UserService를 사용할 수 없습니다.&quot;);
        }

        userService.validateUserStatus(userId);
        // 주문 생성 처리
    }
}</code></pre>
<p>이 방식도 결국 의존성 주입 시점을 늦추는 것이다. <code>OrderService</code>를 만들 때 <code>UserService</code> 자체를 주입받는 것이 아니라, 나중에 <code>UserService</code>를 찾아올 수 있는 공급자 객체를 주입받는다.</p>
<p><code>@Lazy</code>보다 의존성을 가져오는 시점이 코드에 명시적으로 드러난다는 장점이 있지만 이 방식 역시 Spring의 <code>ObjectProvider</code>라는 프레임워크 API에 도메인 코드가 의존하게 된다. 따라서 근본적인 설계 개선보다는 전술적 우회 방법으로 보는 것이 좋다.</p>
<p>&nbsp;</p>
<h3 id="📡-해결책-3-세터-주입-또는-필드-주입으로-변경">📡 해결책 3: 세터 주입 또는 필드 주입으로 변경</h3>
<p>순환 참조를 우회하기 위해 생성자 주입을 세터 주입이나 필드 주입으로 바꾸는 방법도 있다.</p>
<pre><code class="language-java">@Service
public class UserService {
    private OrderService orderService;

    @Autowired
    public void setOrderService(OrderService orderService) {
        this.orderService = orderService;
    }
}</code></pre>
<p>이 방식은 객체를 먼저 생성한 뒤 나중에 의존성을 주입할 수 있기 때문에 일부 상황에서는 순환 참조를 우회할 수 있다.</p>
<p>하지만 이 방법도 권장하기 어렵다. 생성자 주입을 사용하면 필수 의존성을 객체 생성 시점에 강제할 수 있고, 필드를 <code>final</code>로 선언할 수 있다. 반면 세터 주입이나 필드 주입은 객체가 불완전한 상태로 존재할 가능성을 만든다.</p>
<p>또한 테스트 코드에서도 의존성이 명확하게 드러나지 않는다. 따라서 세터 주입이나 필드 주입은 순환 참조를 해결하기 위한 근본적인 방법이 아니라, 오히려 문제를 숨기는 방식에 가깝다.</p>
<p>&nbsp;</p>
<h3 id="👨🏻🔧-해결책-4-공통-책임을-제3의-서비스로-분리권장">👨🏻‍🔧 해결책 4: 공통 책임을 제3의 서비스로 분리(권장)</h3>
<p>순환 참조를 가장 건강하게 해결하는 방법은 <strong>책임을 다시 나누는 것</strong>이다. 앞에서 본 예시에서 <code>UserService</code>와 <code>OrderService</code>가 서로를 참조하는 이유는 아래와 같다.</p>
<ul>
<li>주문 생성 시 회원 상태 검증이 필요하다.</li>
<li>회원 탈퇴 시 진행 중인 주문 여부 확인이 필요하다.</li>
</ul>
<p>이때 두 서비스가 서로를 직접 호출하게 두는 대신, 공통으로 필요한 책임을 별도 서비스로 분리할 수 있다.</p>
<p>&nbsp;</p>
<p>예를 들어 회원 검증 로직을 <code>UserValidator</code>로 분리한다.</p>
<pre><code class="language-java">@Component
public class UserValidator {
    public void validateUserStatus(Long userId) {
        // 회원 상태 검증
    }
}</code></pre>
<p>&nbsp;</p>
<p>그리고 <code>OrderService</code>는 <code>UserService</code>가 아니라 <code>UserValidator</code>에 의존한다.</p>
<pre><code class="language-java">@Service
public class OrderService {
    private final UserValidator userValidator;

    public OrderService(UserValidator userValidator) {
        this.userValidator = userValidator;
    }

    public void createOrder(Long userId) {
        userValidator.validateUserStatus(userId);
        // 주문 생성 처리
    }
}</code></pre>
<p>또는 주문 상태 확인 로직을 별도의 <code>OrderReader</code>, <code>OrderValidator</code> 같은 컴포넌트로 분리할 수도 있다.</p>
<pre><code class="language-java">@Component
public class OrderValidator {
    public boolean hasActiveOrders(Long userId) {
        // 진행 중인 주문 여부 확인
        return false;
    }
}</code></pre>
<pre><code class="language-java">@Service
public class UserService {
    private final OrderValidator orderValidator;

    public UserService(OrderValidator orderValidator) {
        this.orderValidator = orderValidator;
    }

    public void withdraw(Long userId) {
        if (orderValidator.hasActiveOrders(userId)) {
            throw new IllegalStateException(&quot;진행 중인 주문이 있어 탈퇴할 수 없습니다.&quot;);
        }
        // 회원 탈퇴 처리
    }
}</code></pre>
<p>&nbsp;</p>
<p>이렇게 하면 <code>UserService</code>와 <code>OrderService</code>가 서로 직접 의존하지 않아도 된다.</p>
<pre><code class="language-text">Before
UserService → OrderService
OrderService → UserService

After
UserService → OrderValidator
OrderService → UserValidator</code></pre>
<p>순환 구조가 단방향 구조로 바뀐다.</p>
<p>&nbsp;</p>
<h3 id="📨-해결책-5-이벤트-기반으로-분리권장">📨 해결책 5: 이벤트 기반으로 분리(권장)</h3>
<p>두 서비스가 반드시 즉시 서로를 호출할 필요가 없다면, 이벤트 기반 구조를 사용할 수도 있다. 예를 들어 주문이 생성된 뒤 회원 관련 후처리가 필요하다고 하자.</p>
<p>이때 <code>OrderService</code>가 <code>UserService</code>를 직접 호출하는 대신, “주문이 생성되었다”는 이벤트를 발행할 수 있다.</p>
<pre><code class="language-java">public record OrderCreatedEvent(Long orderId, Long userId) {
}</code></pre>
<pre><code class="language-java">@Service
public class OrderService {
    private final ApplicationEventPublisher eventPublisher;

    public OrderService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void createOrder(Long userId) {
        // 주문 생성 처리
        Long orderId = 1L;

        eventPublisher.publishEvent(new OrderCreatedEvent(orderId, userId));
    }
}</code></pre>
<p>&nbsp;</p>
<p>그리고 <code>UserService</code> 또는 별도의 이벤트 핸들러가 해당 이벤트를 구독한다.</p>
<pre><code class="language-java">@Component
public class OrderEventHandler {

    @EventListener
    public void handle(OrderCreatedEvent event) {
        // 주문 생성 이후 필요한 후처리
    }
}</code></pre>
<p>이 구조에서는 <code>OrderService</code>가 <code>UserService</code>를 직접 알 필요가 없다. 단지 이벤트를 발행할 뿐이다. 이벤트를 처리하는 쪽도 <code>OrderService</code>를 직접 호출하지 않는다. 특정 이벤트가 발생하면 반응할 뿐이다.</p>
<p>따라서 두 객체 간의 결합도를 크게 낮출 수 있다. 다만 이벤트 기반 구조는 흐름이 눈에 바로 보이지 않을 수 있다. 메서드 호출처럼 <em>“어디서 어디로 호출되는지”</em> 가 명확하지 않기 때문에, 이벤트 이름과 핸들러 위치를 잘 관리해야 한다.</p>
<p>&nbsp;</p>
<h3 id="🔨-해결책-6-facade-또는-애플리케이션-서비스-도입권장">🔨 해결책 6: Facade 또는 애플리케이션 서비스 도입(권장)</h3>
<p>여러 서비스의 로직을 조합해야 하는 경우, 각 서비스가 서로를 직접 호출하게 만들기보다 상위 계층의 조합 서비스를 둘 수 있다. 예를 들어 회원 탈퇴를 처리할 때 회원 정보도 확인해야 하고, 주문 상태도 확인해야 한다고 하자.</p>
<p>이때 <code>UserService</code>와 <code>OrderService</code>가 서로를 직접 호출하게 만들지 않고, <code>UserWithdrawalFacade</code> 같은 클래스를 둘 수 있다.</p>
<pre><code class="language-java">@Service
public class UserWithdrawalFacade {
    private final UserService userService;
    private final OrderService orderService;

    public UserWithdrawalFacade(UserService userService, OrderService orderService) {
        this.userService = userService;
        this.orderService = orderService;
    }

    public void withdraw(Long userId) {
        if (orderService.hasActiveOrders(userId)) {
            throw new IllegalStateException(&quot;진행 중인 주문이 있어 탈퇴할 수 없습니다.&quot;);
        }

        userService.withdraw(userId);
    }
}</code></pre>
<p>파사드가 여러 서비스를 조합하고, 각 도메인 서비스는 자신의 책임에만 집중한다. 이 방식은 계층 간 의존성 방향을 정리하는 데 특히 유용하다.</p>
<p>&nbsp;</p>
<h2 id="👤-자기-자신을-주입받는-경우도-순환-참조일까">👤 자기 자신을 주입받는 경우도 순환 참조일까?</h2>
<p>특이한 경우로, Bean이 자기 자신을 주입받는 경우도 있다.</p>
<pre><code class="language-java">@Service
public class MyService {
    @Autowired
    private MyService self;

    public void methodA() {
        self.methodB();
    }

    @Transactional
    public void methodB() {
        // 트랜잭션 처리
    }
}</code></pre>
<p>이런 코드는 보통 같은 클래스 내부에서 <code>@Transactional</code> 같은 AOP 기능을 적용하기 위해 사용된다.</p>
<p>Spring AOP는 프록시 기반으로 동작하기 때문에, 같은 클래스 내부에서 <code>this.methodB()</code>를 호출하면 프록시를 거치지 않는다. 그래서 트랜잭션이 적용되지 않을 수 있다. 이를 피하려고 자기 자신의 프록시를 주입받는 방식이 사용되기도 한다.</p>
<p>하지만 이것 역시 좋은 설계라고 보기는 어렵다. 트랜잭션 경계가 달라야 하는 메서드라면 별도의 서비스로 분리하는 것이 더 명확하다.</p>
<pre><code class="language-java">@Service
public class MyService {
    private final MyTransactionService myTransactionService;

    public MyService(MyTransactionService myTransactionService) {
        this.myTransactionService = myTransactionService;
    }

    public void methodA() {
        myTransactionService.methodB();
    }
}</code></pre>
<pre><code class="language-java">@Service
public class MyTransactionService {

    @Transactional
    public void methodB() {
        // 트랜잭션 처리
    }
}</code></pre>
<p>이렇게 분리하면 자기 자신을 주입받지 않아도 되고, 트랜잭션 경계도 더 명확해진다.</p>
<hr>
<p><em><strong>&lt;참고 자료&gt;</strong></em></p>
<p><a href="https://www.alibabacloud.com/blog/spring-circular-dependency_600191">Spring Circular Dependency - Alibaba Cloud Community</a>
<a href="https://oneuptime.com/blog/post/2025-12-22-spring-circular-reference-errors/view">How to Handle Circular Reference Errors in Spring</a>
<a href="https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.6-Release-Notes">Spring Boot 2.6 Release Notes</a>
<a href="https://www.geeksforgeeks.org/java/circular-dependencies-in-spring/">Circular Dependencies in Spring - GeeksforGeeks</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[eGovFrame VSCode Initializr 프로젝트]]></title>
            <link>https://velog.io/@rocker_nun/eGovFrame-VSCode-Initializr-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</link>
            <guid>https://velog.io/@rocker_nun/eGovFrame-VSCode-Initializr-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</guid>
            <pubDate>Mon, 04 May 2026 07:29:32 GMT</pubDate>
            <description><![CDATA[<h2 id="🤔-egovframe-vscode-initializr란">🤔 eGovFrame VSCode Initializr란?</h2>
<p>eGovFrame VSCode Initializr는 <strong>VS Code 환경에서 전자정부 표준 프레임워크 기반 프로젝트를 쉽게 생성하고, 필요한 코드와 설정 파일을 자동으로 만들어주는 개발 도구</strong>다. 조금 더 쉽게 말하면, 전자정부 표준 프레임워크 프로젝트를 시작할 때 필요한 초기 작업을 도와주는 도구다.</p>
<p>개발자는 VSCode 안에서 몇 번의 선택만으로 프로젝트를 생성할 수 있고, 데이터베이스 테이블 정의를 기반으로 CRUD 코드도 자동으로 만들 수 있다. 여기서 <strong><em>“Initializr”</em></strong> 라는 이름이 중요한데, 말 그대로 <strong><em>“초기화 도구”</em></strong> 라는 의미에 가깝다.</p>
<p>Spring 진영에 Spring Initializr가 있는 것처럼, eGovFrame VSCode Initializr는 전자정부 표준 프레임워크 프로젝트를 더 쉽게 시작할 수 있도록 도와주는 역할을 한다.</p>
<p>즉, 이 도구는 개발자가 처음부터 모든 설정과 코드를 직접 작성하지 않고, 표준화된 프로젝트 뼈대를 빠르게 만들 수 있도록 도와준다.</p>
<p>&nbsp;</p>
<h2 id="❓-왜-vscode-initializr가-필요할까">❓ 왜 VSCode Initializr가 필요할까?</h2>
<p>전자정부 표준 프레임워크는 오랫동안 공공 정보시스템 개발에서 중요한 역할을 해왔다. 하지만 과거에는 개발 환경이 주로 Eclipse 중심이었다. Eclipse는 오랜 기간 Java 개발 환경의 대표적인 IDE였고, 전자정부 표준 프레임워크 개발환경도 Eclipse 기반으로 제공되는 경우가 많았다.</p>
<p>하지만 최근 개발 환경은 많이 바뀌었다. 프론트엔드와 백엔드가 분리되고, React, Vue, Node.js, Spring Boot, MSA, 클라우드 네이티브 같은 기술이 함께 사용되면서 개발자는 더 가볍고 유연한 도구를 선호하게 되었다.</p>
<p>그 흐름 속에서 많은 개발자가 VSCode를 사용한다. VSCode는 가볍고 빠르며, 확장 프로그램 생태계가 풍부하다. 프론트엔드 개발뿐만 아니라 백엔드, DevOps, 문서 작성, Git 관리까지 폭넓게 사용할 수 있다. 전자정부 표준 프레임워크도 이런 변화에 맞춰 VSCode 기반 개발 도구가 필요해졌다.</p>
<p>그 결과 등장한 것이 바로 <strong>eGovFrame VSCode Initializr</strong>이다. 이 도구는 전자정부 표준 프레임워크가 과거의 무거운 개발 환경에만 머무르지 않고, 현대적인 개발자 경험으로 확장되고 있다는 것을 보여주는 중요한 프로젝트다.</p>
<p>&nbsp;</p>
<h2 id="📝-이-도구가-해결하는-문제">📝 이 도구가 해결하는 문제</h2>
<p>eGovFrame VSCode Initializr가 해결하려는 문제는 크게 세 가지로 볼 수 있다.</p>
<ol>
<li><p><strong>초기 프로젝트 생성의 복잡함:</strong> 전자정부 표준 프레임워크 기반 프로젝트를 만들려면 정해진 구조와 설정을 따라야 한다. 하지만 초보 개발자나 처음 프로젝트에 참여한 개발자 입장에서는 어떤 구조가 표준인지, 어떤 파일이 필요한지, 어떤 의존성을 추가해야 하는지 헷갈릴 수 있다. VSCode Initializr는 검증된 템플릿을 기반으로 프로젝트 구조를 자동 생성해준다. 덕분에 개발자는 처음부터 구조를 고민하는 데 시간을 많이 쓰지 않아도 된다.</p>
</li>
<li><p><strong>반복적인 코드 작성</strong>: CRUD 기능을 만든다고 생각해보자. 게시글을 등록하고, 조회하고, 수정하고, 삭제하는 기능은 거의 모든 웹 서비스에서 필요하다. 그런데 이 기능을 만들려면 보통 여러 파일을 함께 작성해야 한다. VO, Service, ServiceImpl, Controller, Mapper Interface, Mapper XML, 목록 화면, 등록 화면 등이 필요하다. 이런 파일들은 구조가 반복되는 경우가 많다. 그래서 사람이 직접 작성하면 시간이 오래 걸리고, 오타나 경로 실수도 발생할 수 있다. VSCode Initializr는 DDL을 기반으로 이런 파일들을 자동 생성해준다.</p>
</li>
<li><p><strong>설정 파일 작성의 어려움</strong>: Spring 기반 프로젝트에서는 데이터베이스 설정, 트랜잭션 설정, 로깅 설정, 프로퍼티 설정 등 다양한 설정 파일이 필요하다. 특히 레거시 XML 방식과 최신 Java Config, YAML, Properties 방식이 함께 존재하면 초보자 입장에서는 더 헷갈릴 수 있다. VSCode Initializr는 이런 설정 파일 생성도 도와준다. </p>
</li>
</ol>
<p>즉, 이 도구의 핵심은 단순히 코드를 빨리 만들어주는 것이 아니라 <strong>표준화된 방식으로 프로젝트를 시작하게 해주고, 반복 작업을 줄이며, 사람의 실수를 줄여주는 도구</strong>라고 볼 수 있다.</p>
<p>&nbsp;</p>
<h2 id="🛠️-주요-기능-살펴보기">🛠️ 주요 기능 살펴보기</h2>
<p>eGovFrame VSCode Initializr의 핵심 기능은 크게 세 가지로 정리할 수 있다.</p>
<ol>
<li><p><strong>프로젝트 자동 생성:</strong> 개발자는 VS Code 안에서 원하는 프로젝트 유형과 빌드 도구를 선택할 수 있다. 예를 들어 Maven을 사용할지, Gradle을 사용할지 선택할 수 있고, 프로젝트 템플릿도 선택할 수 있다. </p>
<p> 이 과정은 개발자가 직접 압축 파일을 내려받고, 폴더 구조를 만들고, 설정 파일을 수정하는 과정을 줄여준다. 쉽게 비유하면, 빈 땅에 건물을 지으려고 할 때 기초 공사부터 직접 하는 것이 아니라, 기본 골조가 잡힌 상태에서 시작하는 것과 같다. </p>
<p> 프로젝트 생성 기능은 특히 처음 전자정부 표준 프레임워크를 접하는 개발자에게 도움이 된다. 프로젝트 구조를 잘못 잡으면 이후 개발 과정에서 계속 문제가 생길 수 있다. 하지만 Initializr가 표준 템플릿을 기반으로 프로젝트를 만들어주면, 처음부터 어느 정도 검증된 구조에서 출발할 수 있다.</p>
</li>
</ol>
<ol start="2">
<li><p><strong>DDL 기반 CRUD 코드 자동 생성:</strong>  DDL(Data Definition Language)은 데이터베이스 테이블 구조를 정의하는 SQL 문이다. 예를 들어 다음과 같은 테이블 생성 구문이 있다고 해보자.</p>
<pre><code class="language-sql"> CREATETABLE USER_INFO (
     USER_IDVARCHAR(20) NOTNULL,
     USER_NAMEVARCHAR(50),
     EMAILVARCHAR(100),
 PRIMARYKEY (USER_ID)
 );</code></pre>
<p> 보통 개발자는 이 테이블을 기반으로 여러 계층의 코드를 작성해야 한다. 데이터를 담을 VO 클래스가 필요하고, 비즈니스 로직을 정의할 Service 인터페이스가 필요하고, 실제 로직을 구현할 ServiceImpl 클래스가 필요하고, HTTP 요청을 받을 Controller가 필요하고, DB 접근을 위한 Mapper Interface와 Mapper XML이 필요하고, 화면을 위한 JSP나 Thymeleaf 파일도 필요하다.</p>
<p> 이 작업은 구조가 반복적이지만, 작성해야 할 파일 수가 많다. VSCode Initializr는 DDL을 입력하면 이를 분석해서 필요한 코드를 자동으로 생성해준다.</p>
<p> 자동 생성 대상은 대표적으로 다음과 같다.</p>
<table>
<thead>
<tr>
<th>계층</th>
<th>생성 파일</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 객체</td>
<td>VO, DefaultVO</td>
</tr>
<tr>
<td>비즈니스 로직</td>
<td>Service, ServiceImpl</td>
</tr>
<tr>
<td>웹 요청 처리</td>
<td>Controller</td>
</tr>
<tr>
<td>데이터 접근</td>
<td>Mapper Interface, Mapper XML</td>
</tr>
<tr>
<td>화면</td>
<td>JSP List, JSP Register</td>
</tr>
<tr>
<td>모던 뷰</td>
<td>Thymeleaf List, Thymeleaf Register</td>
</tr>
</tbody></table>
<p> 즉, 하나의 테이블 정의를 기반으로 백엔드 계층과 화면 계층의 기본 코드를 한 번에 만들 수 있다. 이 기능의 장점은 단순히 코드 작성 시간을 줄이는 것만이 아니다. 반복적인 코드에서 자주 발생하는 변수명 오타, 패키지 경로 실수, 파일명 불일치, Mapper 연결 오류 같은 문제를 줄일 수 있다. 개발자는 자동 생성된 기본 코드를 바탕으로 실제 비즈니스 요구사항에 맞는 로직을 추가하면 된다.</p>
</li>
<li><p><strong>설정 파일 자동 생성:</strong> Spring 기반 프로젝트에서는 설정 파일이 매우 중요하다. 데이터베이스 연결 정보, 트랜잭션 설정, 로깅 설정, 스케줄링 설정, 프로퍼티 설정 등이 잘못되면 애플리케이션이 제대로 실행되지 않는다.</p>
<p> 특히 전자정부 표준 프레임워크는 레거시 프로젝트와 최신 프로젝트가 함께 존재할 수 있기 때문에, XML 설정, Java Config, YAML, Properties 등 다양한 설정 방식에 대한 이해가 필요하다. VSCode Initializr는 이런 설정 파일 생성을 도와준다.</p>
<p> 개발자는 복잡한 설정 문법을 처음부터 모두 외우지 않아도 된다. 도구가 제공하는 흐름에 따라 필요한 값을 입력하면, 설정 파일의 기본 구조를 생성할 수 있다. 이 기능은 초보 개발자에게 특히 유용하다. 설정 파일은 비즈니스 로직보다 눈에 잘 보이지 않지만, 한 글자만 잘못 작성해도 실행 오류가 발생할 수 있다. 따라서 설정 파일 자동 생성 기능은 프로젝트 초기 오류를 줄이는 데 큰 도움이 된다.</p>
</li>
</ol>
<p>&nbsp;    </p>
<h2 id="🔍-내부적으로는-어떻게-동작할까">🔍 내부적으로는 어떻게 동작할까?</h2>
<p>이 프로젝트는 단순히 템플릿 파일을 복사하는 수준의 도구가 아니다. eGovFrame VSCode Initializr는 <strong>TypeScript 기반으로 작성된 VSCode 확장 프로그램</strong>이며, 사용자 인터페이스는 <strong>React와 WebView API</strong>를 활용해 구성되어 있다.</p>
<p>여기서 WebView는 VSCode 확장 프로그램 안에서 웹 화면을 띄울 수 있게 해주는 기능이다. 즉, VSCode 안에 작은 웹 애플리케이션이 들어가 있는 것처럼 동작한다고 이해하면 쉽다.</p>
<p>개발자가 버튼을 클릭하거나 값을 입력하면 React 기반 UI가 상태를 관리하고, VSCode Extension API를 통해 로컬 파일 시스템에 프로젝트 파일이나 코드 파일을 생성한다. 또한 코드 생성에는 Handlebars라는 템플릿 엔진이 사용된다.</p>
<p>Handlebars는 미리 만들어둔 코드 템플릿에 테이블명, 컬럼명, 타입 같은 값을 끼워 넣어 실제 코드 파일을 만들어주는 역할을 한다. 예를 들어 템플릿에 다음과 같은 부분이 있다고 생각해보자.</p>
<pre><code>privateString {{fieldName}};</code></pre><p>DDL에서 <code>USER_NAME</code>이라는 컬럼을 읽어 <code>userName</code>이라는 필드명으로 변환하면, 최종 코드는 다음처럼 생성된다.</p>
<pre><code>privateStringuserName;</code></pre><p>이런 방식으로 DDL에서 추출한 메타데이터를 여러 코드 템플릿에 바인딩하여 VO, Service, Controller, Mapper 등을 생성한다. </p>
<p>또한 SQL 문법 분석에는 <code>monaco-sql-languages</code>가 활용된다. 이 라이브러리는 DDL 입력 내용을 분석하고, SQL 문법 오류를 감지하는 역할을 한다. 덕분에 잘못된 DDL을 기반으로 잘못된 코드가 생성되는 문제를 줄일 수 있다.</p>
<p>&nbsp;</p>
<h2 id="🖼️-왜-react와-webview를-사용할까">🖼️ 왜 React와 WebView를 사용할까?</h2>
<p>VSCode 확장 프로그램은 단순한 명령어 기반 도구로 만들 수도 있다. 하지만 eGovFrame VSCode Initializr는 프로젝트 생성, 코드 생성, 설정 생성처럼 사용자가 선택하고 입력해야 하는 값이 많다. 만약 모든 기능을 명령어 입력 방식으로만 제공한다면 사용성이 떨어질 수 있다.</p>
<p>그래서 React 기반 UI와 WebView를 사용한다. React를 사용하면 복잡한 화면 상태를 컴포넌트 단위로 관리할 수 있다. 사용자가 선택한 빌드 도구, 프로젝트 유형, DDL 입력값, 생성 결과 미리보기 등을 UI에서 효율적으로 다룰 수 있다. 또한 VSCode의 테마와 자연스럽게 어울리는 UI를 제공할 수도 있다.</p>
<p>예를 들어 사용자가 VSCode를 다크 모드로 사용하면 확장 프로그램 화면도 다크 모드에 맞춰 보이고, 라이트 모드로 바꾸면 그에 맞춰 자연스럽게 전환된다. 이런 부분은 단순한 기능 구현을 넘어 개발자 경험을 좋게 만드는 요소다. 개발 도구는 기능만 많다고 좋은 것이 아니다. 사용자가 불편함 없이 자연스럽게 사용할 수 있어야 한다.</p>
<p>&nbsp;</p>
<h2 id="⚙️-코드-생성-성능을-위해-어떤-최적화가-필요할까">⚙️ 코드 생성 성능을 위해 어떤 최적화가 필요할까?</h2>
<p>DDL 기반 CRUD 코드 생성 기능은 매우 편리하지만, 내부적으로는 처리해야 할 일이 많다. DDL을 분석해야 하고, 테이블명과 컬럼명을 추출해야 하고, 타입을 변환해야 하고, 여러 템플릿에 값을 바인딩해야 하고, 생성 결과를 미리보기 화면에 보여줘야 한다.</p>
<p>문제는 생성해야 하는 파일이 하나가 아니라는 점이다. VO, DefaultVO, Service, ServiceImpl, Controller, Mapper Interface, Mapper XML, JSP List, JSP Register, Thymeleaf List, Thymeleaf Register 등 여러 파일이 동시에 생성될 수 있다.</p>
<p>만약 사용자가 DDL을 한 글자 입력할 때마다 모든 템플릿을 순차적으로 다시 렌더링한다면 어떻게 될까? VSCode 화면이 버벅일 수 있다. 사용자는 입력할 때마다 지연을 느끼게 되고, 도구를 사용하는 경험이 나빠진다. 그래서 이 프로젝트에서는 성능 최적화가 중요하다.</p>
<p>자료에서는 선택적 자동 업데이트와 비동기 병렬 렌더링 같은 최적화 전략이 언급된다. 쉽게 말하면, 사용자가 입력할 때마다 무조건 전체 코드를 다시 생성하지 않고, 필요한 시점에 효율적으로 생성하도록 제어하는 것이다. 또한 여러 템플릿은 서로 독립적이기 때문에 순서대로 하나씩 생성하기보다 병렬로 처리할 수 있다.</p>
<p>예를 들어 VO 생성과 Controller 생성은 서로 직접 의존하지 않는다. 그렇다면 두 작업을 동시에 처리하는 것이 더 효율적이다. 이런 방식으로 사용자는 더 빠른 미리보기와 코드 생성 경험을 얻을 수 있다.</p>
<hr>
<p><em><strong>&lt;참고 자료&gt;</strong></em>
<a href="https://github.com/eGovFramework/egovframe-vscode-initializr">egovframe-vscode-initializr 레포지토리</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[전자정부 표준 프레임워크(eGovFrame) 이해하기]]></title>
            <link>https://velog.io/@rocker_nun/eGovFrame-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@rocker_nun/eGovFrame-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 04 May 2026 06:34:02 GMT</pubDate>
            <description><![CDATA[<h1 id="🔍-전자정부-표준-프레임워크egovframe">🔍 전자정부 표준 프레임워크(eGovFrame)</h1>
<p>이번 오픈소스 컨트리뷰션 아카데미에서 내가 맡은 프로젝트는 <code>egovframe-vscode-initializr</code>이다. 처음 이 프로젝트를 접했을 때 가장 먼저 든 생각은 이것이었다.</p>
<blockquote>
<p><strong><em>“전자정부 표준 프레임워크가 정확히 뭐지?”</em></strong></p>
</blockquote>
<p>이름만 보면 조금 딱딱하고 어렵게 느껴진다. 전자정부, 표준, 프레임워크라는 단어가 한 번에 붙어 있기 때문이다.</p>
<p>하지만 핵심은 생각보다 단순하다. 전자정부 표준 프레임워크는 <strong>공공기관 정보시스템을 더 빠르고, 안정적이고, 표준화된 방식으로 만들기 위해 제공되는 Java 기반 개발 표준 환경이다.</strong></p>
<p>조금 더 쉽게 말하면, 공공기관 웹 서비스를 만들 때 매번 로그인, 권한 관리, 게시판, 데이터베이스 연결, 예외 처리, 로그 처리 같은 공통 기능을 처음부터 다시 만들지 않도록 미리 준비해둔 개발 기반이다. 집을 지을 때마다 벽돌, 창문, 문, 전기 배선 규격을 매번 새로 만들지 않는 것처럼, 정보시스템을 만들 때도 반복적으로 필요한 기능들을 표준화해서 제공하는 것이다.</p>
<p>&nbsp;</p>
<h2 id="❓-프레임워크는-왜-등장했을까">❓ 프레임워크는 왜 등장했을까?</h2>
<p>전자정부 표준 프레임워크를 이해하기 전에 먼저 프레임워크가 왜 필요한지 이해해야 한다.</p>
<p>소프트웨어 개발 환경은 시대에 따라 계속 변해왔다. 1970년대에는 메인프레임 중심의 중앙 집중형 컴퓨팅 환경이 주로 사용되었다. 하나의 큰 컴퓨터가 데이터 처리, 비즈니스 로직, 화면 처리까지 대부분의 일을 담당했다. 이 방식은 관리와 배포가 편리했지만, 장비 가격이 비싸고 시스템 확장이 어렵다는 단점이 있었다.</p>
<p>이후 1980년대에는 클라이언트-서버 환경이 등장했다. 서버는 데이터베이스를 관리하고, 사용자의 PC가 일부 비즈니스 로직과 화면 처리를 담당하는 방식이었다. 이전보다 비용은 낮아졌고 사용자 인터페이스도 좋아졌지만, 클라이언트 프로그램을 배포하고 관리하는 일이 어려웠다.</p>
<p>1990년대 이후 인터넷과 웹이 보편화되면서 시스템 구조는 다시 크게 바뀌었다. 사용자는 브라우저를 통해 서비스에 접속하고, 서버는 웹 서버, WAS, 데이터베이스 등으로 역할을 나누어 처리하게 되었다. 이 방식은 시스템 간 연동과 확장, 유지보수 측면에서 훨씬 유리했다.</p>
<p>하지만 웹 기반 시스템이 많아지면서 또 다른 문제가 생겼다. 만들어야 할 소프트웨어는 많아졌는데, 매번 비슷한 기능을 반복해서 개발해야 했던 것이다. 예를 들어 어떤 공공기관 시스템을 만든다고 생각해보자. 민원 시스템이든, 복지 시스템이든, 행정 정보 시스템이든 대부분 다음과 같은 기능이 필요하다.</p>
<ul>
<li>회원가입 및 로그인</li>
<li>회원 관리</li>
<li>권한 처리</li>
<li>게시판</li>
<li>파일 업로드</li>
<li>데이터베이스 연결</li>
<li>트랜잭션 처리</li>
<li>로그 기록</li>
<li>예외 처리</li>
<li>보안 처리</li>
</ul>
<p>이 기능들은 프로젝트마다 조금씩 다를 수는 있지만 기본 구조는 비슷하다. 그런데 매번 새 프로젝트를 할 때마다 이 기능을 처음부터 다시 만든다면 개발 비용이 커지고, 품질도 프로젝트마다 달라질 수밖에 없다. 그래서 소프트웨어 개발은 점점 <strong>재사용성</strong>을 높이는 방향으로 발전해왔다.</p>
<p>&nbsp;</p>
<h2 id="🔄-소프트웨어-재사용-방식의-발전">🔄 소프트웨어 재사용 방식의 발전</h2>
<p>소프트웨어를 재사용하는 방식은 여러 단계를 거쳐 발전했다.</p>
<h3 id="🖋️-1단계-소스-코드-재사용">🖋️ 1단계: 소스 코드 재사용</h3>
<p>가장 단순한 방식은 과거에 작성했던 코드를 복사해서 붙여넣는 것이다. </p>
<p>예를 들어 날짜를 문자열로 변환하는 코드가 필요할 때, 이전 프로젝트에서 사용했던 코드를 그대로 복사해오는 식이다.</p>
<p>처음에는 빠르고 간단해 보인다. 하지만 문제가 있다. 같은 코드가 여러 곳에 복사되어 있으면, 나중에 수정이 필요할 때 모든 복사본을 찾아서 고쳐야 한다. 즉, 코드 중복이 늘어나고 유지보수가 어려워진다.</p>
<p>&nbsp;</p>
<h3 id="📖-2단계-메서드-라이브러리-재사용">📖 2단계: 메서드 라이브러리 재사용</h3>
<p>복사 붙여넣기의 문제를 줄이기 위해 자주 사용하는 기능을 메소드나 라이브러리로 분리하기 시작했다. 예를 들어 날짜 변환 기능을 <code>DateUtil</code> 같은 유틸리티 클래스로 만들고, 여러 곳에서 해당 메소드를 호출하는 방식이다.</p>
<p>이렇게 하면 같은 코드를 여러 번 복사하지 않아도 된다. 기능을 수정해야 할 때도 라이브러리 내부만 고치면 된다.</p>
<p>하지만 이 방식도 한계가 있다. 특정 기능 단위의 재사용에는 좋지만, 전체 애플리케이션 구조를 잡아주지는 못한다.</p>
<p>&nbsp;</p>
<h3 id="🧩-3단계-객체지향-재사용">🧩 3단계: 객체지향 재사용</h3>
<p>객체지향 프로그래밍에서는 클래스를 통해 재사용을 할 수 있다.</p>
<p>예를 들어 <code>Person</code>이라는 부모 클래스에 이름, 생년월일, 출력 기능을 정의하고, <code>Client</code>, <code>Employee</code> 같은 자식 클래스가 이를 상속받아 사용할 수 있다. 상속을 활용하면 공통 속성과 기능을 재사용할 수 있다.</p>
<p>하지만 객체지향 재사용도 주로 수직적인 관계에서 효과적이다. 현실의 시스템은 단순히 부모-자식 구조만으로 해결되지 않는다. 데이터 저장 방식, 외부 시스템 연동, 화면 처리 방식처럼 상황에 따라 유연하게 바뀌어야 하는 문제들이 많다.</p>
<p>&nbsp;</p>
<h3 id="🎏-4단계-디자인-패턴-재사용">🎏 4단계: 디자인 패턴 재사용</h3>
<p>디자인 패턴은 자주 발생하는 설계 문제에 대한 검증된 해결 방법이다.</p>
<p>예를 들어 데이터 저장 방식이 관계형 데이터베이스일 수도 있고, 파일일 수도 있고, 외부 API일 수도 있다고 해보자. 이때 Adapter 패턴을 사용하면 서로 다른 저장 방식의 차이를 감추고 일관된 방식으로 사용할 수 있다.</p>
<p>디자인 패턴은 특정 코드 자체를 재사용한다기보다, 문제를 해결하는 구조와 아이디어를 재사용하는 방식이다.</p>
<p>하지만 디자인 패턴도 전체 애플리케이션을 구성하는 완성된 틀은 아니다. 시스템의 부분적인 문제를 해결하는 데는 좋지만, 애플리케이션 전체 구조를 표준화해주지는 못한다.</p>
<p>&nbsp;</p>
<h3 id="🏰-5단계-프레임워크-재사용">🏰 5단계: 프레임워크 재사용</h3>
<p>프레임워크는 앞선 재사용 방식들을 더 큰 단위로 통합한 것이다. 프레임워크는 단순한 코드 모음이 아니다. 애플리케이션을 어떤 구조로 만들지, 각 계층은 어떤 역할을 해야 하는지, 공통 기능은 어떻게 처리해야 하는지에 대한 기본 틀을 제공한다. 쉽게 말해 프레임워크는 <strong>반제품 형태의 소프트웨어</strong>다.</p>
<p>완제품은 아니지만, 기본 골격과 핵심 기능이 이미 준비되어 있다. 개발자는 그 위에 자신이 만들고자 하는 업무 기능을 추가하면 된다. 건축으로 비유하면, 프레임워크는 건물의 기본 설계도와 골조에 가깝다. 개발자는 그 골조 위에 각 건물의 목적에 맞는 방, 인테리어, 세부 기능을 추가하는 것이다.</p>
<p>&nbsp;</p>
<h2 id="⚙️-전자정부-표준-프레임워크가-만들어진-이유">⚙️ 전자정부 표준 프레임워크가 만들어진 이유</h2>
<p>그렇다면 왜 정부는 별도로 전자정부 표준 프레임워크를 만들었을까? 이유는 공공 정보화 사업의 구조적인 문제 때문이다. 과거 공공기관 정보시스템은 특정 대형 SI 업체의 자체 프레임워크를 기반으로 구축되는 경우가 많았다. 예를 들어 삼성 SDS, LG CNS, SK C&amp;C 같은 대형 업체들이 각자 자체 프레임워크를 가지고 있었고, 많은 공공 시스템이 이런 특정 업체의 기술에 의존했다. 이 방식에는 몇 가지 문제가 있었다.</p>
<ol>
<li><p><strong>특정 업체에 대한 종속성</strong>: 어떤 시스템이 특정 업체의 자체 프레임워크로 만들어지면, 나중에 유지보수나 고도화 사업을 할 때 다른 업체가 참여하기 어려워진다. 내부 구조와 기술을 기존 업체만 잘 알고 있기 때문이다.</p>
</li>
<li><p><strong>중소기업의 참여가 어려움</strong>: 공공사업에 참여하려면 해당 업체의 프레임워크를 알아야 하는데, 중소기업 입장에서는 이런 폐쇄적인 프레임워크에 접근하기 어렵다. 결국 대형 업체 중심의 구조가 강화될 수밖에 없다.</p>
</li>
<li><p><strong>비슷한 기능이 계속 중복 개발</strong>: A 기관도 로그인 기능을 만들고, B 기관도 게시판을 만들고, C 기관도 권한 관리 기능을 새로 만든다. 이미 다른 프로젝트에서 만든 기능인데도 매번 다시 개발하는 일이 반복되었다.</p>
</li>
<li><p><strong>시스템 간 연계와 유지보수가 어려움:</strong> 각 기관의 시스템이 서로 다른 프레임워크와 구조로 만들어지면, 나중에 시스템을 통합하거나 연계할 때 많은 비용이 든다.</p>
</li>
</ol>
<p>위와 같은 문제를 해결하기 위해 정부는 2008년부터 전자정부 표준 프레임워크 개발과 보급을 추진했다. 목적은 분명했다. <strong>공공 정보시스템 개발 기반을 표준화하고, 특정 업체 종속을 줄이며, 중복 개발을 방지하고, 중소기업도 공정하게 참여할 수 있는 개발 생태계를 만드는 것</strong>이다.</p>
<p>즉, 전자정부 표준 프레임워크는 단순히 개발자를 편하게 해주는 도구가 아닌, 공공 소프트웨어 생태계의 구조를 개선하기 위해 만들어진 표준 개발 기반이다.</p>
<p>&nbsp;</p>
<h2 id="🤔-전자정부-표준-프레임워크란-무엇인가">🤔 전자정부 표준 프레임워크란 무엇인가?</h2>
<p>전자정부 표준 프레임워크는 공공 정보시스템 개발에 필요한 공통 기반을 표준화해서 제공하는 프레임워크다. 조금 더 쉽게 말하면, 공공기관 웹 서비스를 만들 때 자주 필요한 기능과 구조를 미리 준비해둔 개발 플랫폼이다.</p>
<p>공공기관 시스템에는 보통 아래와 같은 기능이 필요하다고 위에서 언급했다.</p>
<ul>
<li>회원가입 및 로그인</li>
<li>회원 관리</li>
<li>권한 처리</li>
<li>게시판</li>
<li>파일 업로드</li>
<li>데이터베이스 연결</li>
<li>트랜잭션 처리</li>
<li>로그 기록</li>
<li>예외 처리</li>
<li>보안 처리</li>
</ul>
<p>전자정부 표준 프레임워크는 이런 기능들을 공통적으로 사용할 수 있도록 제공한다. 개발자는 매번 바닥부터 구현하지 않고, 표준 프레임워크가 제공하는 구조와 기능을 활용해 실제 업무 로직에 집중할 수 있다. 여기서 중요한 점은 전자정부 표준 프레임워크가 단순히 하나의 라이브러리가 아니라는 것이다.</p>
<p>전자정부 표준 프레임워크는 다음과 같은 요소들을 포함하는 개발 생태계에 가깝다.</p>
<ul>
<li>실행환경</li>
<li>개발환경</li>
<li>관리환경</li>
<li>운영환경</li>
<li>공통컴포넌트</li>
<li>개발 가이드</li>
<li>템플릿</li>
<li>예제 코드</li>
</ul>
<p>즉, 프로젝트를 만들고, 개발하고, 테스트하고, 운영하고, 유지보수하는 전체 과정을 지원하는 표준 기반이라고 볼 수 있다.</p>
<p>&nbsp;</p>
<h2 id="🎭-전자정부-표준-프레임워크와-spring의-관계">🎭 전자정부 표준 프레임워크와 Spring의 관계</h2>
<p>전자정부 표준 프레임워크를 처음 접하면 이런 의문이 들 수 있다.</p>
<blockquote>
<p><em><strong>“전자정부 표준 프레임워크는 Spring이랑 같은 건가?”</strong></em></p>
</blockquote>
<p>95% 정도만 같다. 전자정부 표준 프레임워크는 Spring을 기반으로 하지만, Spring 그 자체는 아니다. Spring은 Java 애플리케이션 개발을 도와주는 대표적인 프레임워크다. 전자정부 표준 프레임워크는 Spring을 포함한 여러 오픈소스 기술을 공공 정보시스템 개발에 맞게 선별하고, 검증하고, 표준화해서 제공한다. 즉, Spring이 핵심 엔진이라면, 전자정부 표준 프레임워크는 그 엔진 위에 공공 시스템 개발에 필요한 표준 구조, 공통 기능, 개발 도구, 운영 도구, 가이드까지 함께 묶어 제공하는 패키지라고 볼 수 있다.</p>
<p>비유하자면 Spring은 자동차의 엔진이고, 전자정부 표준 프레임워크는 그 엔진을 기반으로 실제 도로에서 안전하게 달릴 수 있도록 차체, 운전석, 안전장치, 내비게이션, 정비 매뉴얼까지 갖춘 완성형 플랫폼에 가깝다.</p>
<p>&nbsp;</p>
<h2 id="🗂️-전자정부-표준-프레임워크의-구성">🗂️ 전자정부 표준 프레임워크의 구성</h2>
<p>전자정부 표준 프레임워크는 크게 실행환경, 개발환경, 관리환경, 운영환경, 공통 컴포넌트로 구성된다.</p>
<h2 id="▶️-실행환경">▶️ 실행환경</h2>
<p>실행환경은 애플리케이션이 실제 서버에서 동작할 때 필요한 기반 기능을 제공한다. 쉽게 말해 시스템이 돌아가기 위한 엔진룸이다. 실행환경에는 다음과 같은 기능들이 포함된다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/daa901ff-aa00-4f2c-8ac3-83e170be2670/image.png" alt=""></p>
<ul>
<li><p><strong>화면 처리 영역</strong>: 사용자가 브라우저에서 보는 화면과 서버 간의 요청과 응답을 담당한다.</p>
</li>
<li><p><strong>업무 처리 영역</strong>: 실제 비즈니스 로직이 실행되는 부분이다.</p>
</li>
<li><p><strong>데이터 처리 영역</strong>: 데이터베이스와 연결하고 데이터를 조회하거나 저장하는 역할을 한다.</p>
</li>
<li><p><strong>연계 및 통합 영역</strong>: 외부 시스템이나 다른 기관 시스템과 통신하는 기능을 담당한다.</p>
</li>
<li><p><strong>공통 기반 영역</strong>: 로그, 예외 처리, 메시지 처리 같은 공통 기능을 제공한다.</p>
</li>
<li><p><strong>배치 처리 영역</strong>: 대량의 데이터를 정해진 시간에 일괄 처리하는 기능을 담당한다.</p>
</li>
</ul>
<p>예를 들어 매일 밤 전 국민의 특정 데이터를 정산하거나, 대량의 통계 데이터를 생성하는 작업은 배치 처리 영역에서 담당할 수 있다.</p>
<p>&nbsp;</p>
<h2 id="🛠️-개발환경">🛠️ 개발환경</h2>
<p>개발환경은 개발자가 표준 프레임워크 기반 프로젝트를 더 쉽게 만들고 개발할 수 있도록 돕는 도구 모음이다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/5eb92b6e-8619-4143-afb7-770e9f85f430/image.png" alt=""></p>
<p>과거에는 전자정부 표준 프레임워크 개발환경이 Eclipse 중심으로 제공되었다. 하지만 최근 개발 환경은 많이 바뀌었다. 많은 개발자가 VS Code, IntelliJ, 다양한 프론트엔드 도구를 함께 사용한다.</p>
<p>이 흐름에 맞춰 전자정부 표준 프레임워크도 VS Code 기반 개발 도구를 제공하고 있다. <del>여기서 내가 맡은 <code>egovframe-vscode-initializr</code> 프로젝트가 등장한다.</del></p>
<p>이 도구는 VS Code 안에서 전자정부 표준 프레임워크 기반 프로젝트를 쉽게 생성하고, 설정 파일을 만들고, CRUD 코드까지 자동 생성할 수 있도록 돕는 확장 프로그램이다. 즉, 개발자가 표준 프레임워크 프로젝트를 시작할 때 겪는 복잡한 초기 설정 과정을 줄여주는 도구다.</p>
<p>&nbsp;</p>
<h2 id="👨🏻🔧-관리환경">👨🏻‍🔧 관리환경</h2>
<p>관리환경은 표준 프레임워크 자체를 유지하고 개선하기 위한 체계다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/d7d22b2e-4763-4b0f-9b36-25306381a3c3/image.png" alt=""></p>
<p>프레임워크도 한 번 만들고 끝나는 것이 아니다. 오픈소스 라이브러리 버전이 올라가고, 보안 취약점이 발견되고, 새로운 기술이 등장하면 계속 개선되어야 한다. 관리환경은 이런 변경 요청, 오류 수정, 기능 개선, 버전 관리, 표준 관리 등을 처리하는 역할을 한다. 쉽게 말하면 표준 프레임워크가 계속 안정적으로 발전할 수 있도록 관리하는 내부 운영 체계다.</p>
<p>&nbsp;</p>
<h2 id="🏭-운영환경">🏭 운영환경</h2>
<p>운영환경은 실제 서비스가 운영되는 동안 시스템 상태를 확인하고 관리하기 위한 환경이다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/cfd2ee57-2ef3-44e1-8856-eaceb1ffd073/image.png" alt=""></p>
<p>서비스가 정상적으로 동작하는지, 서버 자원은 충분한지, 응답 시간이 너무 느리지는 않은지, 에러는 발생하지 않는지 등을 모니터링한다. 쉽게 말해 운영환경은 시스템의 관제실이다.</p>
<p>공공기관 서비스는 많은 사용자가 이용하고, 장애가 발생하면 사회적 영향이 클 수 있다. 따라서 시스템이 잘 동작하는지 지속적으로 확인하는 운영환경이 중요하다.</p>
<p>&nbsp;</p>
<h2 id="📦-공통-컴포넌트">📦 공통 컴포넌트</h2>
<p>전자정부 표준 프레임워크에서 가장 실질적으로 개발 생산성을 높여주는 요소가 공통컴포넌트다. 공통 컴포넌트는 여러 정보시스템에서 반복적으로 사용되는 기능을 미리 만들어둔 소프트웨어 부품이다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/755b5823-14eb-4c5f-87ca-3d3593efca12/image.png" alt=""></p>
<p>예를 들어 다음과 같은 기능들이 공통 컴포넌트로 제공된다고 한다면,</p>
<ul>
<li>로그인</li>
<li>사용자 관리</li>
<li>권한 관리</li>
<li>게시판</li>
<li>공지사항</li>
<li>주소록</li>
<li>일정 관리</li>
<li>파일 업로드</li>
<li>공통 코드 관리</li>
<li>메뉴 관리</li>
<li>로그 관리</li>
<li>통계 관리</li>
<li>암호화</li>
<li>유효성 검사</li>
</ul>
<p>개발자는 이런 기능을 처음부터 새로 만들 필요 없이, 필요한 공통컴포넌트를 가져와 프로젝트에 적용할 수 있다. 비유하자면 공통 컴포넌트는 레고 블록과 비슷하다. 필요한 블록을 선택해서 조립하면 원하는 구조물을 더 빠르게 만들 수 있다.</p>
<p>물론 공통 컴포넌트를 그대로 사용하는 것만이 정답은 아니다. 기관이나 프로젝트의 요구사항에 맞게 수정해서 사용할 수도 있다. 중요한 것은 완전히 빈 상태에서 시작하지 않아도 된다는 점이다.</p>
<p>&nbsp;</p>
<h2 id="👍-전자정부-표준-프레임워크를-사용하면-좋은-점">👍 전자정부 표준 프레임워크를 사용하면 좋은 점</h2>
<p>전자정부 표준 프레임워크를 사용하면 여러 장점이 있다.</p>
<ol>
<li><p><strong>개발 생산성 향상</strong>: 반복적으로 필요한 기능을 매번 새로 만들 필요가 줄어든다. 개발자는 로그인, 권한, DB 연결, 로그 처리 같은 기반 기능보다 실제 업무 로직에 더 집중할 수 있다.</p>
</li>
<li><p><strong>품질 향상</strong>: 프로젝트마다 제각각 구현하던 공통 기능을 검증된 표준 방식으로 구현할 수 있다. 이는 코드 품질과 시스템 안정성을 높이는 데 도움이 된다.</p>
</li>
<li><p><strong>유지보수성 향상</strong>: 표준화된 구조를 사용하면 새로운 개발자가 프로젝트에 참여했을 때 구조를 이해하기 쉽다. 공공 시스템은 한 번 만들고 끝나는 것이 아니라 오랜 기간 운영되기 때문에 유지보수성이 매우 중요하다.</p>
</li>
<li><p><strong>특정 업체 종속성 완화</strong>: 표준 프레임워크는 공개된 표준과 오픈소스 기반 기술을 활용한다. 따라서 특정 업체의 폐쇄적인 프레임워크에 의존하는 문제를 줄일 수 있다.</p>
</li>
<li><p><strong>중소기업 참여 기회 확대</strong>: 공통된 표준 기반이 있으면 특정 대기업만 시스템을 이해하고 유지보수할 수 있는 구조를 줄일 수 있다. 이는 더 많은 기업이 공정하게 경쟁할 수 있는 환경을 만드는 데 도움이 된다.</p>
</li>
</ol>
<p>&nbsp;</p>
<h2 id="🌊-전자정부-표준-프레임워크의-최신-흐름">🌊 전자정부 표준 프레임워크의 최신 흐름</h2>
<p>전자정부 표준 프레임워크는 과거의 공공 SI 개발 도구에 머무르지 않고 계속 변화하고 있다.</p>
<p>초기 버전은 주로 공공 웹 시스템의 표준 개발 기반을 제공하는 데 초점이 있었다. 이후 모바일 환경 지원, 보안 강화, 오픈소스 라이브러리 업그레이드, Spring 버전 업그레이드 등이 꾸준히 이루어졌다. 최근에는 Spring Boot, MSA, 클라우드 네이티브, OpenSearch, AI 기반 검색, VS Code 개발환경 같은 현대적인 흐름도 반영되고 있다. </p>
<p>특히 Spring Boot 기반으로 전환되면서 과거보다 설정이 간소화되고, 독립 실행 가능한 애플리케이션 구성이 쉬워졌다. 기존에는 복잡한 XML 설정을 많이 작성해야 했지만, Spring Boot 기반에서는 자동 설정을 활용해 초기 설정 부담을 크게 줄일 수 있다.</p>
<p>또한 MSA 환경도 중요해지고 있다. 과거에는 하나의 큰 애플리케이션 안에 모든 기능이 들어 있는 모놀리식 구조가 일반적이었다. 하지만 시스템 규모가 커지고 트래픽이 늘어나면 하나의 거대한 애플리케이션을 관리하기 어려워진다. MSA는 사용자, 게시판, 예약, 인증 같은 기능을 작고 독립적인 서비스로 나누어 운영하는 방식이다. 특정 서비스에 트래픽이 몰리면 해당 서비스만 확장할 수 있고, 일부 서비스에 문제가 생겨도 전체 시스템 장애로 번지는 것을 줄일 수 있다. 전자정부 표준 프레임워크도 이런 클라우드 네이티브 환경에 맞춰 계속 확장되고 있다.</p>
<hr>
<p><em><strong>&lt;참고 자료&gt;</strong></em>
<a href="https://www.egovframe.go.kr/home/main.do">표준 프레임워크 포털 eGovFrame</a>
오픈소스 컨트리뷰션 아카데미 배포 자료</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[애그리거트(Aggregate)]]></title>
            <link>https://velog.io/@rocker_nun/%EC%95%A0%EA%B7%B8%EB%A6%AC%EA%B1%B0%ED%8A%B8Aggregate</link>
            <guid>https://velog.io/@rocker_nun/%EC%95%A0%EA%B7%B8%EB%A6%AC%EA%B1%B0%ED%8A%B8Aggregate</guid>
            <pubDate>Sun, 03 May 2026 14:43:16 GMT</pubDate>
            <description><![CDATA[<h1 id="🏰-애그리거트">🏰 애그리거트</h1>
<p>온라인 쇼핑몰 시스템을 상위 수준 개념을 이용해서 바라보면 아래와 같이 전체 모델들 간의 관계를 이해할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/07a7f2b9-3e3f-4d8a-a76d-589cd2aaa7f8/image.png" alt=""></p>
<p>위와 같은 상위 수준 모델 간의 관계를 제대로 이해하지 않고 개별 객체들 간의 관계만을 보고 전체 모델의 관계를 파악하기는 매우 어렵고, 코드를 변경하고 확장하는 것이 매우 힘들어진다. 이를 위해 상위 수준에서 모델을 관찰할 수 있게 해주는 <strong>애그리거트(Aggregate)</strong>가 등장한다. 도메인 규칙에 따라 함께 일관성을 유지해야 하는 객체들을 하나의 애그리거트 경계로 묶어서 상위 수준에서 도메인 모델 간의 관계를 파악하는 것이다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/7c08746d-bca9-495e-8ed3-44747c5d2e64/image.png" alt=""></p>
<p>위의 그림처럼 애그리거트는 전체 모델 간의 관계를 이해할 수 있도록 도와줄 뿐만 아니라, 일관성을 관리하는 기준도 될 수 있다. 그리고 관련된 모델을 하나로 모았기 때문에 한 애그리거트에 속하는 객체는 유사하거나 동일한 라이프 사이클을 갖는다. 그리고 그림을 보면 알 수 있듯이 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다. 경계를 명확히 해서 복잡한 도메인을 단순한 구조로 만드는 것이 애그리거트의 임무이기 때문에 어찌 보면 당연하다. </p>
<p>이 경계를 어떻게 결정하는지에 대한 답은 도메인 규칙과 요구사항으로부터 찾아야 한다. 일단 도메인 규칙에 따라 함께 생성되고 변경되는 구성요소는 한 애그리거트에 속할 가능성이 높다. 그리고 주의할 점은 <em>“A가 B를 갖는다”</em> 와 같은 요구사항을 보고 A와 B는 무조건 하나의 애그리거트에 속할 것이라고 속단하는 것이다. 아래 상품과 리뷰 예시를 보자.</p>
<p>상품 상세 페이지에 들어가면 보통 리뷰가 달려 있으니 상품과 리뷰는 하나의 애그리거트에 포함시키는 것이 이상하다고 느껴지지 않는다. 하지만 분명 상품과 리뷰는 함께 생성되지도, 변경되지도 않는다. 게다가 변경 주체도 상품은 관리자가 변경하고, 리뷰는 고객이 변경 주체이기 때문에 하나의 애그리거트에 포함시키는 것은 무리가 있다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/78460e31-38da-4795-9ba6-a770c6445d0f/image.png" alt=""></p>
<p>&nbsp;</p>
<h1 id="👨🏻🔧-애그리거트-루트">👨🏻‍🔧 애그리거트 루트</h1>
<p>도메인 규칙을 지키기 위해서는 하나의 애그리거트에 속한 여러 객체들이 모두 정상 상태를 유지해야 한다. 따라서 애그리거트 전체를 컨트롤할 수 있는 관리자가 필요한데, 이 책임을 지는 것이 바로 애그리거트의 <strong>루트 엔티티(Root Entity)</strong>다. </p>
<p>루트 엔티티의 임무는 애그리거트의 일관성이 깨지지 않도록 하는 것이다. 이를 위해 루트 엔티티는 애그리거트가 제공해야 할 도메인 기능을 구현한다. 불필요한 중복을 피하고 루트 엔티티를 통해서만 도메인 로직을 구현하게 만들기 위해서는 <code>setter</code> 메서드를 외부에서 접근할 수 없도록 만들고, 밸류 타입은 불변으로 구현해야 한다. </p>
<p>그리고 루트 엔티티는 애그리거트 내부의 다른 객체를 조합해서 기능을 완성한다. 아래 <code>Order</code> 루트 엔티티 코드와 <code>Member</code> 루트 엔티티 코드를 살펴보자.</p>
<pre><code class="language-java">public class Order {
    private Money totalAmount;
    private List&lt;OrderLine&gt; orderLines;

    private void calculateTotalAmount() {
        int sum = orderLines.stream()
                .mapToInt(ol -&gt; ol.getPrice() * ol.getQuantity())
                .sum();
        this.totalAmount = new Money(sum);
    }
}</code></pre>
<pre><code class="language-java">public class Member {
    private Password password;

    public void changePassword(Password currentPassword, Password newPassword) {
        if (!password.match(currentPassword)) {
            throw new PasswordNotMatchException();
        }
        this.password = new Password(newPassword);
    }
}</code></pre>
<p><code>Order</code>는 총 주문 금액을 구하기 위해 <code>OrderLine</code> 목록을 사용하고, <code>Member</code>는 비밀번호를 변경하기 위해 <code>Password</code> 밸류 타입에 비밀번호가 일치하는지 확인하고 있다. 추가로 루트 엔티티는 구성요소의 필드만 참조하는 것이 아니라 기능 실행을 위임하기도 한다.</p>
<p>&nbsp;</p>
<h2 id="🚧-트랜잭션-범위">🚧 트랜잭션 범위</h2>
<p>트랜잭션의 범위는 작을수록 좋다. 예를 들어 1개의 테이블을 수정하면 락의 대상이 그 테이블의 1개 행일 뿐이지만, 3개의 테이블을 수정한다면 락의 대상이 많아질 수밖에 없다. 이는 동시에 처리할 수 있는 트랜잭션 개수가 줄어든다는 것을 의미하고 전체적인 성능을 떨어뜨린다. </p>
<p>이와 마찬가지로 한 번에 수정하는 애그리거트 개수가 많으면 많아질수록 전체 처리량이 떨어지기 때문에 한 트랜잭션에서는 한 개의 애그리거트만 수정해야 한다. 다시 말하자면, 애그리거트 내부에서 다른 애그리거트의 상태를 변경하는 기능을 되도록 두지 말자는 것이다. 아래 코드를 보자.</p>
<pre><code class="language-java">public class Order {
    private Orderer orderer;

    public void shipTo(ShippingInfo newShippingInfo, boolean useNewShippingAddressAsMemberAddress) {
        verifyNotYetShipped();
        setShippingInfo(newShippingInfo);  // 주문 애그리거트의 배송지 정보를 변경
        if (useNewShippingAddressAsMemberAddress) {
                // 회원의 주소를 변경된 배송지로 설정...
            orderer.getMember().changeAddress(newShippingInfo.getAddress());
        }
    }

    ...
}</code></pre>
<p>위의 코드는 주문 애그리거트에서 회원 애그리거트 내부 상태까지 변경하고 있는 상태다. <code>Order</code> 루트 엔티티나 <code>Member</code> 루트 엔티티는 각각 주문, 회원 애그리거트 내부의 일관성을 책임져야 하는데 <code>Order</code> 루트 엔티티가 회원 애그리거트 내부 일관성을 해치고 있는 것이다. 하지만 상황에 따라 한 트랜잭션으로 2개 이상의 애그리거트를 수정해야 한다면 위의 코드처럼 직접 수정하는 것보다 응용 서비스에서 두 애그리거트를 수정하도록 구현해야 한다.</p>
<pre><code class="language-java">public class ChangeOrderService {
    private OrderRepository orderRepository;
    private MemberRepository memberRepository;

    @Transactional
    public void changeShippingInfo(
            OrderId orderId,
            ShippingInfo newShippingInfo,
            boolean useNewShippingAddressAsMemberAddress
    ) {
        Order order = orderRepository.findById(orderId);
        if (order == null) {
            throw new OrderNotFoundException();
        }
        order.shipTo(newShippingInfo);

        if (useNewShippingAddressAsMemberAddress) {
            Member member = findMember(order.getOrderer());
            member.changeAddress(newShippingInfo.getAddress());
        }
    }

    ...
}</code></pre>
<p>정리하면, 한 트랜잭션에서 한 개의 애그리거트를 변경하는 것이 가장 좋지만, 아래의 경우에는 한 트랜잭션에서 2개 이상의 애그리거트를 변경하는 것을 고려할 수도 있다.</p>
<ul>
<li><strong>팀 표준</strong>: 팀이나 조직의 표준에 따라 사용자 유스케이스와 관련된 응용 서비스의 기능을 한 트랜잭션으로 실행해야 하는 경우가 있다.</li>
<li><strong>기술 제약</strong>: 기술적으로 이벤트 방식을 도입할 수 없는 경우 한 트랜잭션에서 다수의 애그리거트를 수정해서 일관성을 처리해야 한다.</li>
<li><strong>UI 구현의 편리</strong>: 운영자의 편리함을 위해 주문 목록 화면에서 여러 주문의 상태를 한 번에 변경하고 싶을 것이다. 이 경우 한 트랜잭션에서 여러 주문 애그리거트의 상태를 변경해야 한다.</li>
</ul>
<p>&nbsp;</p>
<h1 id="💾-리포지토리와-애그리거트">💾 리포지토리와 애그리거트</h1>
<p>객체의 영속성을 처리하는 리포지토리도 애그리거트 단위로 존재한다. 새로운 애그리거트를 만들면 저장소에 애그리거트를 영속화하고 애그리거트를 사용하려면 저장소에서 애그리거트를 읽어 와야 하기 때문에 리포지토리는 기본적으로 아래 2개의 메서드를 제공한다.</p>
<ul>
<li><code>save()</code>: 애그리거트를 저장</li>
<li><code>findById()</code>: ID로 애그리거트를 조회</li>
</ul>
<p>알다시피 리포지토리를 구현하는 기술들은 정말 다양하다. 어떤 기술을 채택하느냐에 따라 리포지토리 구현 방식도 달라진다. 그리고 리포지토리 구현체는 애그리거트 루트뿐만 아니라 애그리거트에 속한 구성요소까지 함께 저장하고 조회해야 한다. 만약 <code>Order</code> 애그리거트 루트와 그와 관련된 테이블이 여러 개가 있다면 루트 엔티티뿐만 아니라 나머지 애그리거트에 속한 모든 구성요소에 매핑된 테이블에 데이터를 저장해야 한다. </p>
<pre><code class="language-java">// 애그리거트 전체를 영속화
orderRepository.save(order);

// 완전한 주문 애그리거트를 조회
Order order = orderRepository.findById(orderId);</code></pre>
<p>&nbsp;</p>
<h1 id="🆔-id를-이용한-애그리거트-참조">🆔 ID를 이용한 애그리거트 참조</h1>
<p>아까 봤다시피 하나의 애그리거트도 다른 애그리거트를 참조할 수 있다. 더 정확히 말하면, 다른 애그리거트의 루트 엔티티를 참조하는 것이다. 앞에서 본 예시 코드처럼 애그리거트 간의 참조는 필드를 통해 쉽게 구현할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/9266b8b1-1081-4cb8-b6c0-98b3771d206d/image.png" alt=""></p>
<p>이처럼 애그리거트 간의 참조를 구현하는 것은 쉽지만 몇 가지 문제들이 발생할 수 있다는 것을 유념해야 한다.</p>
<ul>
<li>편한 탐색 오용</li>
<li>성능에 대한 고민</li>
<li>확장 어려움</li>
</ul>
<p>&nbsp;</p>
<p>일단 첫 번째로, 한 애그리거트가 관리하는 범위는 자기 자신으로 한정하는 것이 가장 좋다고 했다. 하지만 구현이 워낙 편리하기 때문에 다른 애그리거트를 수정하고자 하는 유혹에 빠지기 쉽다. 또한 애그리거트 간의 결합도가 높아져 애그리거트의 변경을 어렵게 만든다. </p>
<p>두 번째 문제는 성능이다. 애그리거트를 객체로 직접 참조하면 JPA 같은 ORM을 사용할 때 지연 로딩과 즉시 로딩 중 무엇을 선택할지 고민해야 한다. 잘못 선택하면 불필요한 쿼리가 많이 실행되거나, 반대로 필요하지 않은 객체까지 한 번에 조회하는 문제가 생길 수 있다.</p>
<p>마지막으로, 확장에 대한 문제다. 서비스가 성장하고 사용자 수가 늘면 자연스럽게 부하를 분산하기 위해 하위 도메인별로 시스템을 분리해야 할 것이다. 이 과정에서 하위 도메인마다 각기 다른 DBMS를 사용하거나 아예 다른 데이터 저장소를 사용할 수도 있다. 이렇게 되면 다른 애그리거트 루트를 참조하기 위해 JPA와 같은 단일 기술은 사용할 수 없게 된다.</p>
<p>&nbsp;</p>
<p>위와 같은 문제점들을 한 번에 해결할 수 있는 방법이 바로 ID를 이용해서 다른 애그리거트를 참조하는 방법이다. 아래 다이어그램을 보자.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/7f7987b6-0296-4be0-96a3-164604474bed/image.png" alt=""></p>
<p>보다시피 ID 참조를 사용하면 모든 객체가 참조로 연결되지 않고 한 애그리거트에 속한 객체들만 참조로 연결되기 때문에 모델의 복잡도를 낮추고 각 애그리거트의 응집도도 높여줄 수 있다. 추가로 구현 복잡도도 낮아진다. 이제 직접적으로 참조하지 않기 때문에 JPA를 예로 들면 애그리거트 간의 참조를 지연 로딩으로 할지 즉시 로딩으로 할지 더 이상 고민하지 않아도 된다. 참조하는 애그리거트가 필요하다면 그냥 응용 서비스에서 ID로 로딩하면 된다.</p>
<pre><code class="language-java">public class ChangeOrderService {

    ...

    @Transactional
    public void changeShippingInfo(
            OrderId orderId,
            ShippingInfo newShippingInfo,
            boolean useNewShippingAddressAsMemberAddress
    ) {
        Order order = orderRepository.findById(orderId);
        if (order == null) {
            throw new OrderNotFoundException();
        }
        order.changeShippingInfo(newShippingInfo);

        if (useNewShippingAddressAsMemberAddress) {
            // ID를 이용해서 참조하는 애그리거트를 구한다.
            Member member = memberRepository.findById(
                    order.getOrderer().getMemberId()
            );
            member.changeAddress(newShippingInfo.getAddress());
        }
    }

    ...
}</code></pre>
<p>&nbsp;</p>
<h2 id="⚙️-조회-성능">⚙️ 조회 성능</h2>
<p>하지만 다른 애그리거트를 ID로 참조하게 되면 여러 애그리거트를 읽을 때 조회 속도가 저하될 수도 있다. 예를 들어 주문 목록을 보여주려면 상품 애그리거트와 회원 애그리거트를 같이 읽어 와야 할 것이다. 아래 코드를 보자.</p>
<pre><code class="language-java">...

Member member = memberRepository.findById(ordererId);

List&lt;Order&gt; orders = orderRepository.findByOrderer(ordererId);
List&lt;OrderView&gt; dtos = orders.stream()
        .map(order -&gt; {
            ProductId productId = order.getOrderLines().get(0).getProductId();

            // 각 주문마다 첫 번째 주문 상품 정보 로딩을 위한 쿼리 실행
            Product product = productRepository.findById(productId);

            return new OrderView(order, member, product);
        })
        .collect(Collectors.toList());

 ...</code></pre>
<p>만약 주문이 10개면 주문을 읽어오기 위한 1번의 쿼리와 주문별로 각 상품을 읽어오기 위한 10번의 쿼리를 실행해야 한다. 이게 그 유명한 <strong>N + 1 조회 문제</strong>다. 이는 더 많은 쿼리를 날리기 때문에 당연히 성능을 저하시킬 수밖에 없다. </p>
<p>이 문제를 해결하려면 조회 목적에 맞는 전용 쿼리를 사용하는 것이 좋다. 객체 참조와 즉시 로딩으로 해결할 수도 있지만, 이는 애그리거트 간 결합도를 높이고 필요하지 않은 데이터까지 함께 조회할 위험이 있다. </p>
<p>따라서 목록 화면처럼 여러 애그리거트의 데이터를 한 번에 보여줘야 하는 경우에는 조회 전용 DAO나 조회 전용 모델을 두고 조인 쿼리로 필요한 데이터만 읽어오는 방식이 더 적합하다. 예를 들어 데이터 조회를 위한 별도 DAO를 만들고 DAO의 조회 메서드에서 조인을 이용해 1번의 쿼리로 필요한 데이터를 로딩하면 된다. 아래 특정 사용자의 주문 내역을 보여주는 코드를 보자.</p>
<pre><code class="language-java">@Repository
public class JpaOrderViewDao implements OrderViewDao {
    @PersistenceContext
    private EntityManager em;

    @Override
    public List&lt;OrderView&gt; selectByOrderer(String ordererId) {
        String selectQuery =
                &quot;select new com.myshop.order.application.dto.OrderView(o, m, p) &quot; + 
                        &quot;from Order o join o.orderLines ol, Member m, Product p &quot; +
                        &quot;where o.orderer.memberId.id = :ordererId &quot; +
                        &quot;and o.orderer.memberId = m.id &quot; +
                        &quot;and index(ol) = 0 &quot; +
                        &quot;and ol.productId = p.id &quot; +
                        &quot;order by o.number.number desc&quot;;
        TypedQuery&lt;OrderView&gt; query =
                em.createQuery(selectQuery, OrderView.class);
        query.setParameter(&quot;ordererId&quot;, ordererId);
        return query.getResultList();
    }
}</code></pre>
<p>이 JPQL은 <code>Order</code> 애그리거트와 <code>Member</code> 애그리거트, <code>Product</code> 애그리거트를 조인으로 조회해서 1번의 쿼리로 로딩한다. 따라서 즉시 로딩이든 지연 로딩이든 상관없이 조회 화면에서 필요한 애그리거트 데이터를 1번의 쿼리로 로딩할 수 있는 것이다. </p>
<p>&nbsp;</p>
<h1 id="⛓️-애그리거트-간-집합-연관">⛓️ 애그리거트 간 집합 연관</h1>
<p>이제 애그리거트 간의 일대다 관계, 다대다 관계에 대해 알아보자. 이 두 연관은 <strong>컬렉션(Collection)</strong>을 이용한 연관이다. 일단 애그리거트 간의 일대다 관계로, 하나의 카테고리와 그에 연관된 상품을 값으로 갖는 컬렉션을 필드로 아래와 같이 정의할 수 있다.</p>
<pre><code class="language-java">public class Category {

    private Set&lt;Product&gt; products;

    ...
}</code></pre>
<p>&nbsp;</p>
<p>근데 개념적으로 존재하는 애그리거트 간의 일대다 관계를 실제 구현에 반영하는 것이 요구사항을 충족하는 것과는 상관없을 때가 있다. 특정 카테고리에 속한 상품 목록을 보여주는 요구사항을 생각해보자. 보통 목록과 관련된 요구사항은 한 번에 모든 정보를 보여주기보다는 페이징 기법을 이용해 요소들을 나눠서 보여준다. 이 기능을 카테고리 입장에서 일대다 관계를 이용해서 구현하면 아래와 같이 코드를 작성할 수 있다. </p>
<pre><code class="language-java">public class Category {

    private Set&lt;Product&gt; products;

    public List&lt;Product&gt; getProducts(int page, int size) {
        List&lt;Product&gt; sortedProducts = sortById(products);
        return sortedProducts.subList((page - 1) * size, page * size);
    }

    ...
}</code></pre>
<p>하지만 이 코드를 실제 DB와 연동한다면 카테고리에 있는 모든 상품들이 다 딸려 나온다. 그러면 성능에 아주 심각한 문제가 생긴다. 따라서 개념적으로는 애그리거트 간에 일대다 연관이 있더라도 실제 구현에 반영하지는 않는다. 카테고리에 속한 상품 목록이 필요하다면, 카테고리 애그리거트가 상품 컬렉션을 직접 들고 있기보다 상품 조회용 리포지토리나 조회 전용 DAO를 통해 <code>categoryId</code> 조건으로 페이징 조회하는 편이 더 적절하다. </p>
<p>다대다 연관도 일대다 연관과 마찬가지로, 실제 요구사항을 고려해서 구현에 포함할지를 결정해야 한다. 카테고리와 상품을 예로 들면, 개념적으로는 상품과 카테고리 사이에 양방향 다대다 관계가 존재할 수 있다. 하지만 실제 구현에서는 요구사항에 필요한 방향만 반영하면 된다.</p>
<p>예를 들어 상품 상세 화면에서 상품이 속한 카테고리 정보만 필요하다면, 상품에서 카테고리 ID 목록을 참조하거나 별도 조회 쿼리로 상품과 카테고리 관계를 읽어오는 방식으로 충분할 수 있다. 반대로 특정 카테고리에 속한 상품 목록이 필요하다면, 카테고리가 상품 컬렉션을 직접 들고 있기보다 상품 조회용 DAO나 조회 전용 쿼리를 통해 페이징해서 조회하는 편이 더 적절하다.</p>
<p>&nbsp;</p>
<h1 id="🏭-애그리거트를-팩토리로-사용하기">🏭 애그리거트를 팩토리로 사용하기</h1>
<p>고객이 특정 상점을 신고해서 해당 상점이 더 이상 물건을 등록하지 못하는 상황을 생각해보자. 상품 등록 기능을 구현한 응용 서비스는 상점 계정이 차단 상태가 아닌 경우에만 상품을 등록할 수 있도록 로직을 구현해야 할 것이다. 아래 코드를 보자.</p>
<pre><code class="language-java">public class RegisterProductService {

    ...

    public ProductId registerNewProduct(NewProductRequest request) {
        Store store = storeRepository.findById(request.getStoreId());
        checkNull(store);
        if (store.isBlocked()) {
            throw new StoreBlockedException();
        }
        ProductId productId = productRepository.nextId();
        Product product = new Product(productId, store.getId(), ...);
        productRepository.save(product);
        return productId;
    }

    ...
}</code></pre>
<p>겉으로 보기에는 문제가 없어 보이지만, <code>Store</code>가 상품을 생성할 수 있는지 여부를 검사하고 상품을 생성하는 로직은 분명 논리적으로 하나의 도메인 기능인데 현재 응용 서비스에서 구현하고 있는 것이다. 해당 도메인 기능을 별도의 도메인 서비스나 팩토리 클래스를 만들 수도 있지만 아래와 같이 <code>Store</code> 애그리거트에 구현하는 방법도 있다. </p>
<pre><code class="language-java">public class Store {
    ...

    public Product createProduct(ProductId newProductId, ...) {
        if (isBlocked()) {
            throw new StoreBlockedException();
        }
        return new Product(newProductId, getId(), ...);
    }
}</code></pre>
<p><code>Store</code> 애그리거트의 <code>createProduct()</code> 메서드는 <code>Product</code> 애그리거트를 생성하는 팩토리 역할을 하면서도 중요한 도메인 로직을 구현하고 있다. 이제 응용 서비스에서 해당 팩토리 기능을 사용하기만 하면 된다.</p>
<pre><code class="language-java">public class RegisterProductService {
    ...

    public ProductId registerNewProduct(NewProductRequest request) {
        Store store = storeRepository.findById(request.getStoreId());
        checkNull(store);
        ProductId productId = productRepository.nextId();
        Product product = store.createProduct(productId, ...);
        productRepository.save(product);
        return productId;
    }

    ...
}</code></pre>
<p>이제 상품을 생성할 수 있는지 여부를 검사하는 도메인 로직에 변경이 일어나더라도 응용 서비스는 전혀 영향을 받지 않기 때문에 도메인의 응집도가 높아진 것이다. 이게 바로 애그리거트를 팩토리로 사용할 때의 장점이다. 따라서 앞으로 애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면 이런 식으로 애그리거트에 팩토리 메서드를 구현하는 것을 고려하도록 하자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java Stream 이해하기]]></title>
            <link>https://velog.io/@rocker_nun/Java-Stream-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-vm4fqdrr</link>
            <guid>https://velog.io/@rocker_nun/Java-Stream-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-vm4fqdrr</guid>
            <pubDate>Fri, 01 May 2026 08:51:13 GMT</pubDate>
            <description><![CDATA[<h1 id="↩️-외부-반복자에서-내부-반복자로">↩️ 외부 반복자에서 내부 반복자로</h1>
<p>자바에서 컬렉션을 다룰 때 우리는 보통 <code>for</code>, <code>while</code>, <code>Iterator</code>를 사용해왔다. 예를 들어 숫자 리스트에서 짝수만 출력한다고 해보자.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = List.of(1, 2, 3, 4, 5);

for (int number : numbers) {
    if (number % 2 == 0) {
        System.out.println(number);
    }
}</code></pre>
<p>이 코드는 굉장히 익숙하다. 리스트에서 값을 하나씩 꺼내고, 조건을 검사하고, 조건에 맞으면 출력한다.</p>
<pre><code class="language-text">1 꺼냄 → 짝수인지 검사
2 꺼냄 → 짝수인지 검사 → 출력
3 꺼냄 → 짝수인지 검사
4 꺼냄 → 짝수인지 검사 → 출력
5 꺼냄 → 짝수인지 검사</code></pre>
<p>이처럼 개발자가 컬렉션 바깥에서 직접 요소를 꺼내며 반복을 제어하는 방식을 <strong>외부 반복자</strong>라고 한다.</p>
<p>&nbsp;</p>
<p>반면 Java 8부터 자주 사용하게 된 <code>Stream</code>은 조금 다른 방식으로 컬렉션을 처리한다.</p>
<pre><code class="language-java">numbers.stream()
        .filter(number -&gt; number % 2 == 0)
        .forEach(number -&gt; System.out.println(number));</code></pre>
<p>이 코드에는 <code>for</code>, <code>while</code>, <code>Iterator</code>가 보이지 않는다. 그렇다고 반복이 사라진 것이 아니라 반복은 여전히 일어난다. 다만 개발자가 직접 반복을 제어하지 않을 뿐이다. 스트림 내부에서 반복이 일어나고, 개발자는 각 요소에 적용할 처리 규칙만 람다식으로 전달한다. 이것이 바로 <strong>내부 반복자</strong> 방식이다.</p>
<p>&nbsp;</p>
<h1 id="💉-스트림을-비유로-이해하기">💉 스트림을 비유로 이해하기</h1>
<p>스트림을 처음 접하면 이런 코드가 낯설게 느껴진다.</p>
<pre><code class="language-java">List&lt;Integer&gt; result = numbers.stream()
        .filter(number -&gt; number % 2 == 0)
        .map(number -&gt; number * 10)
        .toList();</code></pre>
<p><code>stream()</code>은 무엇이고, <code>filter()</code>는 무엇이며, <code>map()</code>은 무엇이고, 마지막에 <code>toList()</code>는 왜 필요한 걸까? 이걸 다음과 같이 비유해서 이해해보자.</p>
<pre><code class="language-text">내 몸 = 컬렉션
주사기 = 스트림
주사하는 것 = 람다식으로 전달하는 처리 규칙
중간 처리기 = filter, map, sorted
추출기 = forEach, sum, collect, toList</code></pre>
<p>여기서 중요한 점은 <strong>스트림이 원본 컬렉션을 직접 바꾸는 도구가 아니라는 것</strong>이다. 스트림은 컬렉션 안의 데이터를 하나씩 흘려보내면서, 중간중간 람다식으로 전달한 처리 규칙을 적용하고, 마지막에 원하는 결과를 꺼내 쓰는 방식이다. 즉, 스트림을 정리하면 다음과 같다.</p>
<blockquote>
<p><em><strong>&quot;스트림은 컬렉션의 원본 데이터를 직접 수정하지 않고, 요소들을 하나씩 흘려보내며 처리한 뒤, 최종 연산을 통해 원하는 결과를 꺼내 쓰는 데이터 처리 방식이다.&quot;</strong></em></p>
</blockquote>
<p>&nbsp;</p>
<h1 id="🎭-컬렉션은-원본-데이터다">🎭 컬렉션은 원본 데이터다</h1>
<p>먼저 컬렉션이 있다.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = List.of(1, 2, 3, 4, 5);</code></pre>
<p>위 <code>numbers</code>는 원본 데이터다. 비유하자면 내 몸 안에 있는 데이터라고 볼 수 있다.</p>
<pre><code class="language-text">내 몸 = 컬렉션
몸 안의 요소들 = 컬렉션 안의 데이터</code></pre>
<p>&nbsp;</p>
<p><code>numbers</code> 안에는 1, 2, 3, 4, 5라는 값이 들어 있다. 이제 이 컬렉션을 스트림으로 바꿔보면...</p>
<pre><code class="language-java">Stream&lt;Integer&gt; stream = numbers.stream();</code></pre>
<p>위와 같이 <code>stream()</code>을 호출했다고 해서 원본 리스트가 바뀌는 것은 아니다. <code>numbers</code>가 사라지는 것도 아니고, 새로운 리스트가 바로 만들어지는 것도 아니다. 단지 컬렉션 안의 요소들을 하나씩 처리할 수 있는 <strong>흐름</strong>을 만든 것이다.</p>
<blockquote>
<p><em><strong>&quot;numbers.stream(): numbers 안의 요소들을 하나씩 흘려보낼 수 있는 통로를 만든다 == 빨대를 꽂는다(강사님 피셜)&quot;</strong></em></p>
</blockquote>
<p>&nbsp;</p>
<h1 id="🌊-스트림은-결과물이-아니라-흐름이다">🌊 스트림은 결과물이 아니라 흐름이다</h1>
<p>스트림을 처음 배울 때 가장 헷갈리는 부분이 있었는데, 바로 스트림 자체를 결과물처럼 생각하는 것이다. 예를 들어 다음 코드를 보자.</p>
<pre><code class="language-java">numbers.stream().filter(number -&gt; number % 2 == 0);</code></pre>
<p>이 코드는 짝수만 골라낸 것처럼 보인다. 하지만 이 시점에서 아직 <code>List&lt;Integer&gt;</code> 결과가 만들어진 것은 아니다. 찍어보면 위 코드의 타입은 여전히 <code>Stream&lt;Integer&gt;</code>인 것을 확인할 수 있다. 즉, 결과 리스트가 아니라 스트림이다.</p>
<p>주사기 비유로 마저 설명해보자면 대략 아래와 느낌이다.</p>
<ol>
<li>주사기로 데이터를 뽑았는데,</li>
<li>아직 결과물을 꺼낸 것이 아니라</li>
<li>처리 규칙이 연결된 주사기 상태로 들고 있는 것이다.</li>
</ol>
<p>따라서 <em>&quot;<code>filter()</code> 처리했으니까 필터링된 결과가 리스트로 담겼겠네?&quot;</em> 라는 이해는 완전히 잘못된 것이다. <code>filter</code>, <code>map</code>, <code>sorted</code> 같은 중간 연산은 결과물을 바로 꺼내주지 않는다. 대부분 다시 <code>Stream</code>을 반환한다.</p>
<pre><code class="language-java">numbers.stream()
        .filter(number -&gt; number % 2 == 0)
        .map(number -&gt; number * 10);</code></pre>
<p>위 코드도 아직 최종 결과가 아니라 <code>numbers</code>에서 요소를 흘려보내는데, 짝수만 통과시키고, 통과한 값을 10배한 값으로 바꾼 처리 규칙이 명시된 스트림일 뿐이다. 따라서 <strong>결과물을 실제로 꺼내려면 마지막에 최종 연산이 필요</strong>하다.</p>
<p>&nbsp;</p>
<pre><code class="language-java">List&lt;Integer&gt; result = numbers.stream()
        .filter(number -&gt; number % 2 == 0)
        .map(number -&gt; number * 10)
        .toList();</code></pre>
<p>위와 같이 작성해야 결과가 리스트로 나온다. 정리하면 중간 연산만 연결했다면 여전히 <code>Stream</code> 타입이고 최종 연산을 해야 비로소 실제 결과물을 얻을 수 있다는 것이다.</p>
<p>&nbsp;</p>
<h1 id="📝-람다식은-스트림에-전달하는-처리-규칙이다">📝 람다식은 스트림에 전달하는 처리 규칙이다</h1>
<p>스트림에서는 람다식이 자주 등장한다. 예를 들어 다음 코드를 보자.</p>
<pre><code class="language-java">.filter(number -&gt; number % 2 == 0)</code></pre>
<p>위 코드에서 람다식은 <code>number -&gt; number % 2 == 0</code> 부분인데, 이 람다식은 <em>&quot;숫자 하나를 받아서 짝수인지 검사하라&quot;</em> 와 같은 의미다.</p>
<p>스트림은 컬렉션의 요소를 하나씩 흘려보내면서 이 람다식을 적용하는 것이다.</p>
<ul>
<li>1 들어옴 → 1 % 2 == 0 → false → 버림</li>
<li>2 들어옴 → 2 % 2 == 0 → true  → 통과</li>
<li>3 들어옴 → 3 % 2 == 0 → false → 버림</li>
<li>4 들어옴 → 4 % 2 == 0 → true  → 통과</li>
<li>5 들어옴 → 5 % 2 == 0 → false → 버림</li>
</ul>
<p>결국 2와 4만 통과하게 되는 것이다.</p>
<p>&nbsp;</p>
<p>이번에는 <code>map</code>을 보자.</p>
<pre><code class="language-java">.map(number -&gt; number * 10)</code></pre>
<p>여기서 람다식은 <code>number -&gt; number * 10</code>인데 숫자 하나를 받아서 10배로 바꾸라는 같은 의미를 갖는다. 따라서 앞에서 <code>filter</code>를 통과한 2, 4는 <code>map</code>을 지나면서 각각 20과 40으로 바뀌는 것이다. 즉, 람다식은 스트림에게 전달하는 처리 규칙이다.</p>
<p>&nbsp;</p>
<ul>
<li><code>filter()</code>에 전달하는 람다식: 어떤 요소를 통과시킬지 판단하는 규칙</li>
<li><code>map()</code>에 전달하는 람다식: 요소를 어떤 값으로 바꿀지 정하는 규칙</li>
<li><code>forEach()</code>에 전달하는 람다식: 요소를 어떻게 소비할지 정하는 규칙</li>
</ul>
<p>&nbsp;</p>
<h1 id="🧩-중간-연산-filter-map-sorted">🧩 중간 연산: filter, map, sorted</h1>
<p>스트림에는 크게 중간 연산, 최종 연산이 있다. 먼저 중간 연산부터 보자.</p>
<p>중간 연산은 스트림을 받아서 다시 스트림을 반환하는 연산이다. 대표적으로 다음과 같은 것들이 있다.</p>
<ul>
<li><code>filter()</code></li>
<li><code>map()</code></li>
<li><code>sorted()</code></li>
<li><code>distinct()</code></li>
<li><code>limit()</code></li>
<li><code>skip()</code></li>
</ul>
<p>&nbsp;</p>
<p>중간 연산의 특징은 다음과 같다.</p>
<ol>
<li>최종 결과를 바로 만들지 않는다.</li>
<li>대부분 <code>Stream</code>을 다시 반환한다.</li>
<li>여러 개를 연결해서 사용할 수 있다.</li>
<li>최종 연산이 호출되기 전까지 실제로 실행되지 않는다.</li>
</ol>
<p>&nbsp;</p>
<h2 id="📌-filter-조건에-맞는-요소만-통과시킨다">📌 filter(): 조건에 맞는 요소만 통과시킨다</h2>
<p><code>filter</code>는 조건에 맞는 요소만 통과시킨다.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = List.of(1, 2, 3, 4, 5);

List&lt;Integer&gt; result = numbers.stream()
        .filter(number -&gt; number % 2 == 0)
        .toList();

System.out.println(result);

/**
 * [2, 4]
 */</code></pre>
<p><code>filter</code>에 전달된 람다식은 <code>number -&gt; number % 2 == 0</code>로 각 요소에 대해 <code>true</code> 또는 <code>false</code>를 반환하고 <code>true</code>는 통과, <code>false</code>는 제외시킨다. 따라서 <code>filter</code>는 조건문과 비슷한 역할을 한다.</p>
<p>외부 반복자로 작성하면 다음과 같다.</p>
<pre><code class="language-java">List&lt;Integer&gt; result = new ArrayList&lt;&gt;();

for (Integer number : numbers) {
    if (number % 2 == 0) {
        result.add(number);
    }
}</code></pre>
<p>스트림으로 작성하면 다음과 같다.</p>
<pre><code class="language-java">List&lt;Integer&gt; result = numbers.stream()
        .filter(number -&gt; number % 2 == 0)
        .toList();</code></pre>
<p>&nbsp;</p>
<h2 id="⛏️-map-요소를-다른-값으로-변환한다">⛏️ map(): 요소를 다른 값으로 변환한다</h2>
<p><code>map</code>은 스트림을 지나가는 요소를 다른 값으로 변환한다.</p>
<pre><code class="language-java">List&lt;String&gt; names = List.of(&quot;kim&quot;, &quot;lee&quot;, &quot;park&quot;);

List&lt;String&gt; result = names.stream()
        .map(name -&gt; name.toUpperCase())
        .toList();

System.out.println(result);

/**
 * [KIM, LEE, PARK]
 */</code></pre>
<p>&nbsp;</p>
<p>원본 리스트는 바뀌지 않는다.</p>
<pre><code class="language-java">System.out.println(names);  // [kim, lee, park]</code></pre>
<p>즉, <code>map</code>은 원본 데이터를 직접 수정하는 것이 아니다. 스트림을 지나가는 값을 새로운 값으로 변환해서 다음 단계로 넘긴다.</p>
<pre><code class="language-text">&quot;kim&quot;  → &quot;KIM&quot;
&quot;lee&quot;  → &quot;LEE&quot;
&quot;park&quot; → &quot;PARK&quot;</code></pre>
<p>&nbsp;</p>
<p>객체 리스트에서도 자주 사용한다.</p>
<pre><code class="language-java">List&lt;Student&gt; students = List.of(
        new Student(&quot;Kim&quot;, 80),
        new Student(&quot;Lee&quot;, 90),
        new Student(&quot;Park&quot;, 70)
);

List&lt;String&gt; names = students.stream()
        .map(student -&gt; student.getName())
        .toList();

/**
 * Student(&quot;Kim&quot;, 80)  → &quot;Kim&quot;
 * Student(&quot;Lee&quot;, 90)  → &quot;Lee&quot;
 * Student(&quot;Park&quot;, 70) → &quot;Park&quot;
 */</code></pre>
<p>위와 같이 <code>Student</code> 객체에서 이름만 뽑아 새로운 리스트를 만든다.</p>
<p>&nbsp;</p>
<h2 id="⛓️-sorted-요소를-정렬한다">⛓️ sorted(): 요소를 정렬한다</h2>
<p><code>sorted</code>는 스트림의 요소를 정렬한다.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = List.of(5, 3, 1, 4, 2);

List&lt;Integer&gt; result = numbers.stream()
        .sorted()
        .toList();

System.out.println(numbers);
System.out.println(result);

/**
 * [5, 3, 1, 4, 2]
 * [1, 2, 3, 4, 5]
 */</code></pre>
<p>여기도 마찬가지로 원본 리스트가 바뀌지 않는다. <code>sorted()</code>는 원본 컬렉션을 직접 정렬하는 것이 아니라 스트림을 지나가는 요소들을 정렬된 흐름으로 만들어주고, 최종 연산을 통해 새로운 결과를 꺼내는 것이다.</p>
<p>객체를 정렬할 때는 <code>Comparator</code>를 함께 사용할 수 있다.</p>
<pre><code class="language-java">List&lt;Student&gt; students = List.of(
        new Student(&quot;Kim&quot;, 80),
        new Student(&quot;Lee&quot;, 90),
        new Student(&quot;Park&quot;, 70)
);

List&lt;Student&gt; result = students.stream()
        .sorted(Comparator.comparing(Student::getScore))
        .toList();</code></pre>
<p>이 코드는 학생들을 점수 기준으로 오름차순 정렬한 결과를 새 리스트로 만든다.</p>
<p>&nbsp;</p>
<h1 id="🎊-최종-연산-foreach-sum-collect-tolist">🎊 최종 연산: forEach, sum, collect, toList</h1>
<p>중간 연산만으로는 결과를 꺼낼 수 없다. 스트림에서 결과를 꺼내려면 반드시 최종 연산이 필요하다. 대표적인 최종 연산은 다음과 같다.</p>
<ul>
<li><code>forEach()</code></li>
<li><code>count()</code></li>
<li><code>sum()</code></li>
<li><code>average()</code></li>
<li><code>collect()</code></li>
<li><code>toList()</code></li>
<li><code>anyMatch()</code></li>
<li><code>allMatch()</code></li>
<li><code>findFirst()</code></li>
</ul>
<p>최종 연산의 특징은 다음과 같다.</p>
<ol>
<li>스트림을 실제로 실행시킨다.</li>
<li>결과를 반환하거나 소비한다.</li>
<li>최종 연산 이후 스트림은 다시 사용할 수 없다.</li>
</ol>
<p>&nbsp;</p>
<h2 id="🥪-foreach-요소를-하나씩-소비한다">🥪 forEach(): 요소를 하나씩 소비한다</h2>
<p><code>forEach</code>는 스트림의 요소를 하나씩 꺼내서 소비한다.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = List.of(1, 2, 3, 4, 5);

numbers.stream()
        .filter(number -&gt; number % 2 == 0)
        .forEach(number -&gt; System.out.println(number));

/**
 * 2
 * 4
 */</code></pre>
<p><code>forEach</code>는 최종 연산이다. 하지만 결과를 리스트로 모아서 반환하지는 않는다. 각 요소를 하나씩 소비하고 끝낸다. 그래서 <code>forEach</code>는 보통 출력, 로그 확인, 특정 동작 수행 등에 사용된다.</p>
<p>&nbsp;</p>
<h2 id="🗃️-tolist-결과를-리스트로-모은다">🗃️ toList(): 결과를 리스트로 모은다</h2>
<p><code>toList</code>는 스트림의 결과를 리스트로 모은다.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = List.of(1, 2, 3, 4, 5);

List&lt;Integer&gt; result = numbers.stream()
        .filter(number -&gt; number % 2 == 0)
        .map(number -&gt; number * 10)
        .toList();

System.out.println(result);

// [20, 40]</code></pre>
<p>&nbsp;</p>
<p>흐름을 풀어보면 다음과 같다.</p>
<pre><code class="language-text">원본 리스트: [1, 2, 3, 4, 5]

stream()
→ 요소를 하나씩 흘려보낼 준비

filter(number -&gt; number % 2 == 0)
→ 짝수만 통과
→ 2, 4

map(number -&gt; number * 10)
→ 통과한 값을 10배로 변환
→ 20, 40

toList()
→ 결과를 List로 추출
→ [20, 40]</code></pre>
<p>&nbsp;</p>
<h2 id="🔍-collect-원하는-형태로-결과를-수집한다">🔍 collect(): 원하는 형태로 결과를 수집한다</h2>
<p><code>collect</code>는 스트림의 결과를 원하는 자료구조나 형태로 모을 때 사용한다.</p>
<pre><code class="language-java">List&lt;Integer&gt; result = numbers.stream()
        .filter(number -&gt; number % 2 == 0)
        .collect(Collectors.toList());</code></pre>
<p>&nbsp;</p>
<p>Java 16 이후에는 단순히 리스트로 모을 때 <code>toList()</code>를 자주 사용할 수 있다.</p>
<pre><code class="language-java">List&lt;Integer&gt; result = numbers.stream()
        .filter(number -&gt; number % 2 == 0)
        .toList();</code></pre>
<p>&nbsp; </p>
<p>하지만 <code>collect</code>는 더 다양한 수집 작업을 할 수 있다. 예를 들어 이름들을 하나의 문자열로 합칠 수 있다.</p>
<pre><code class="language-java">List&lt;String&gt; names = List.of(&quot;Kim&quot;, &quot;Lee&quot;, &quot;Park&quot;);

String result = names.stream()
        .collect(Collectors.joining(&quot;, &quot;));

System.out.println(result);  // Kim, Lee, Park</code></pre>
<p>또는 그룹화도 할 수 있다.</p>
<pre><code class="language-java">Map&lt;Integer, List&lt;Student&gt;&gt; result = students.stream()
        .collect(Collectors.groupingBy(Student::getGrade));</code></pre>
<p>즉, <code>collect</code>는 스트림의 결과를 원하는 방식으로 수집하는 강력한 최종 연산이다.</p>
<p>&nbsp;</p>
<h2 id="✚-sum-숫자-값을-합산한다">✚ sum(): 숫자 값을 합산한다</h2>
<p><code>sum</code>은 숫자 스트림에서 합계를 구할 때 사용한다. 일반 <code>Stream&lt;Integer&gt;</code>에서는 바로 <code>sum()</code>을 사용할 수 없다. 보통 <code>mapToInt</code>를 사용해서 <code>IntStream</code>으로 바꾼 뒤 사용한다.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = List.of(1, 2, 3, 4, 5);

int sum = numbers.stream()
        .mapToInt(number -&gt; number)
        .sum();

System.out.println(sum);  // 15</code></pre>
<p>&nbsp;</p>
<p>객체 리스트에서도 자주 사용한다.</p>
<pre><code class="language-java">List&lt;Student&gt; students = List.of(
        new Student(&quot;Kim&quot;, 80),
        new Student(&quot;Lee&quot;, 90),
        new Student(&quot;Park&quot;, 70)
);

int totalScore = students.stream()
        .mapToInt(student -&gt; student.getScore())
        .sum();

System.out.println(totalScore);</code></pre>
<p>흐름은 다음과 같다.</p>
<pre><code class="language-text">Student 객체들이 지나간다.
→ 각 Student에서 score만 뽑는다.
→ int 값들의 흐름이 된다.
→ sum()으로 합산한다.</code></pre>
<p>&nbsp;</p>
<h1 id="🚫-중간-연산은-최종-연산이-호출되기-전까지-실행되지-않는다">🚫 중간 연산은 최종 연산이 호출되기 전까지 실행되지 않는다</h1>
<p>스트림에서 중요한 특징 중 하나는 <strong>지연 실행(Lazy Evaluation)</strong>이다. 중간 연산은 최종 연산이 호출되기 전까지 실제로 실행되지 않는다. 다음 코드를 보자.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = List.of(1, 2, 3, 4, 5);

Stream&lt;Integer&gt; stream = numbers.stream()
        .filter(number -&gt; {
            System.out.println(&quot;filter 실행: &quot; + number);
            return number % 2 == 0;
        });</code></pre>
<p>이 코드를 실행해도 아무것도 출력되지 않는다. 왜냐하면 아직 최종 연산이 없기 때문이다.</p>
<p>&nbsp;</p>
<pre><code class="language-java">stream.toList();</code></pre>
<p>이렇게 최종 연산을 호출해야 그때 실제로 실행된다.</p>
<pre><code class="language-text">filter 실행: 1
filter 실행: 2
filter 실행: 3
filter 실행: 4
filter 실행: 5</code></pre>
<p>&nbsp;</p>
<p>즉, 스트림의 중간 연산은 바로 실행되는 것이 아니라, 최종 연산이 호출될 때 한 번에 동작한다. 비유하자면 다음과 같다.</p>
<pre><code class="language-text">filter, map, sorted를 연결하는 것은
주사기에 처리 장치를 연결해두는 것과 비슷하다.

하지만 아직 실제로 뽑아낸 것은 아니다.

toList, collect, forEach 같은 최종 연산을 해야
비로소 데이터가 흐르면서 처리되고 결과가 나온다.</code></pre>
<p>&nbsp;</p>
<h1 id="😢-스트림은-한-번-사용하면-다시-사용할-수-없다">😢 스트림은 한 번 사용하면 다시 사용할 수 없다</h1>
<p>스트림은 한 번 최종 연산을 수행하면 다시 사용할 수 없다.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = List.of(1, 2, 3, 4, 5);

Stream&lt;Integer&gt; stream = numbers.stream();

stream.forEach(System.out::println);

stream.forEach(System.out::println);  // 예외 발생</code></pre>
<p>두 번째 <code>forEach</code>에서는 예외가 발생한다. 스트림은 일회용이다. 한 번 최종 연산으로 소비되면 다시 사용할 수 없다. 다시 사용하고 싶다면 원본 컬렉션에서 스트림을 새로 만들어야 한다.</p>
<pre><code class="language-java">numbers.stream().forEach(System.out::println);
numbers.stream().forEach(System.out::println);</code></pre>
<p>&nbsp;</p>
<p>비유하면 다음과 같다.</p>
<pre><code class="language-text">한 번 사용한 주사기는 버리고 새로운 주사기를 사용해야 한다.
다시 처리하고 싶다면 원본 컬렉션에서 새로운 스트림을 만들어야 한다는 말이다.</code></pre>
<p>&nbsp;</p>
<h1 id="🤔-외부-반복자란-무엇인가">🤔 외부 반복자란 무엇인가?</h1>
<p>이제 스트림을 외부 반복자와 내부 반복자 관점에서 다시 살펴보자. 그동안 컬렉션을 다룰 때 <code>for</code>, <code>while</code>, <code>Iterator</code>를 사용해왔다. 이 방식은 외부 반복자 방식이다. 외부 반복자는 개발자가 직접 컬렉션의 요소를 하나씩 꺼내며 반복을 제어하는 방식이다.</p>
<p>예를 들어 다음 코드를 보자.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = List.of(1, 2, 3, 4, 5);

List&lt;Integer&gt; result = new ArrayList&lt;&gt;();

for (Integer number : numbers) {
    if (number % 2 == 0) {
        result.add(number * 10);
    }
}

System.out.println(result);</code></pre>
<p>&nbsp;</p>
<p>위 코드에서 개발자는 다음 작업을 직접 수행한다.</p>
<ol>
<li>새로운 리스트를 만든다.</li>
<li>numbers에서 숫자를 하나씩 꺼낸다.</li>
<li>짝수인지 검사한다.</li>
<li>짝수라면 10을 곱한다.</li>
<li>결과 리스트에 추가한다.</li>
<li>반복이 끝나면 결과 리스트를 사용한다.</li>
</ol>
<p>즉, 반복의 흐름을 개발자가 직접 제어한다. <code>Iterator</code>를 사용해도 마찬가지다.</p>
<pre><code class="language-java">Iterator&lt;Integer&gt; iterator = numbers.iterator();

while (iterator.hasNext()) {
    Integer number = iterator.next();

    if (number % 2 == 0) {
        result.add(number * 10);
    }
}</code></pre>
<p>여기서도 개발자는 직접 다음 요소가 있는지 확인하고, 직접 요소를 꺼낸다.</p>
<pre><code class="language-java">iterator.hasNext()
iterator.next()</code></pre>
<p>이처럼 컬렉션 바깥에서 개발자가 반복을 직접 제어하는 방식이 외부 반복자다.</p>
<p>&nbsp;</p>
<h1 id="🤔-내부-반복자란-무엇인가">🤔 내부 반복자란 무엇인가?</h1>
<p>내부 반복자는 반복의 제어권을 개발자가 직접 가지지 않고, 스트림 내부에 맡기는 방식이다. 앞의 코드를 스트림으로 바꿔보자.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = List.of(1, 2, 3, 4, 5);

List&lt;Integer&gt; result = numbers.stream()
        .filter(number -&gt; number % 2 == 0)
        .map(number -&gt; number * 10)
        .toList();

System.out.println(result);</code></pre>
<p>&nbsp;</p>
<p>이 코드에서 개발자는 직접 요소를 꺼내지 않는다. 대신 스트림에게 다음과 같이 요청한다.</p>
<ol>
<li>numbers에서 스트림을 만들어줘.</li>
<li>짝수만 통과시켜줘.</li>
<li>통과한 값을 10배로 바꿔줘.</li>
<li>리스트로 모아줘.</li>
</ol>
<p>반복은 스트림 내부에서 일어난다. 개발자는 각 요소에 적용할 처리 규칙만 람다식으로 전달한다. 즉, 내부 반복자는 반복은 스트림 내부에 맡기고, 개발자는 처리 규칙만 전달하는 방식이라고 이해할 수 있다.</p>
<p>&nbsp;</p>
<p>외부 반복자와 내부 반복자의 차이점은 집중하는 관점이 다르다. </p>
<p><em><strong>&lt;외부 반복자&gt;</strong></em></p>
<ul>
<li>어떻게 반복할 것인가?</li>
<li>어떻게 요소를 꺼낼 것인가?</li>
<li>언제 결과 리스트에 추가할 것인가?</li>
</ul>
<p><em><strong>&lt;내부 반복자&gt;</strong></em></p>
<ul>
<li>무엇을 골라낼 것인가?</li>
<li>무엇으로 변환할 것인가?</li>
<li>어떤 결과로 모을 것인가?</li>
</ul>
<p>&nbsp;</p>
<p>따라서 스트림은 단순히 반복문을 짧게 쓰는 문법이 아니다. 스트림은 데이터 처리 과정을 더 선언적으로 표현하게 해주는 도구라고 할 수 있다.</p>
<p>&nbsp;</p>
<h1 id="👍-스트림의-장점">👍 스트림의 장점</h1>
<p>외부 반복자 방식은 반복 로직 때문에 핵심 의도가 묻힐 때가 있다.</p>
<pre><code class="language-java">List&lt;String&gt; result = new ArrayList&lt;&gt;();

for (Student student : students) {
    if (student.getScore() &gt;= 60) {
        result.add(student.getName());
    }
}</code></pre>
<p>이 코드는 다음 과정을 직접 따라가야 한다.</p>
<pre><code class="language-text">학생을 하나씩 꺼낸다.
점수가 60점 이상인지 검사한다.
조건에 맞으면 이름을 결과 리스트에 추가한다.</code></pre>
<p>&nbsp;</p>
<p>반면, 위와 코드를 똑같이 스트림으로 작성하면 의도가 더 직접적으로 드러난다.</p>
<pre><code class="language-java">List&lt;String&gt; result = students.stream()
        .filter(student -&gt; student.getScore() &gt;= 60)
        .map(student -&gt; student.getName())
        .toList();</code></pre>
<p>이 코드는 다음처럼 읽힌다.</p>
<pre><code class="language-text">60점 이상인 학생만 골라서
이름만 뽑고
리스트로 만든다.</code></pre>
<p>&nbsp;</p>
<p>두 번째 장점으로, 스트림은 여러 중간 연산을 연결해서 사용할 수 있다.</p>
<pre><code class="language-java">List&lt;String&gt; result = students.stream()
        .filter(student -&gt; student.getScore() &gt;= 60)
        .map(student -&gt; student.getName())
        .sorted()
        .toList();</code></pre>
<p>이 코드는 다음처럼 읽을 수 있다.</p>
<pre><code class="language-text">60점 이상인 학생만 고른다.
학생 객체에서 이름만 뽑는다.
이름을 정렬한다.
리스트로 만든다.</code></pre>
<p>각 단계가 파이프라인처럼 이어지기 때문에 데이터 처리 흐름을 파악하기 좋다.</p>
<p>&nbsp;</p>
<p>세 번째 장점으로는 스트림은 원본 데이터를 직접 변경하지 않는다는 것이다.</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = List.of(5, 3, 1, 4, 2);

List&lt;Integer&gt; result = numbers.stream()
        .sorted()
        .toList();

System.out.println(numbers);
System.out.println(result);

/*
 * [5, 3, 1, 4, 2]
 * [1, 2, 3, 4, 5]
 */</code></pre>
<p>원본 리스트는 그대로 유지되고, 정렬된 결과가 새롭게 만들어진다. 이 점은 데이터를 안전하게 다루는 데 도움이 된다.</p>
<p>&nbsp;</p>
<p>그리고 스트림은 내부 반복자 방식이라고 했다. 이 말은 반복의 제어권을 개발자가 직접 가지는 것이 아니라 스트림이 가진다는 말이고, 이 구조 덕분에 경우에 따라 병렬 처리를 적용하기 쉽다.</p>
<pre><code class="language-java">List&lt;Integer&gt; result = numbers.parallelStream()
        .filter(number -&gt; number % 2 == 0)
        .map(number -&gt; number * 10)
        .toList();</code></pre>
<p>물론 <code>parallelStream()</code>이 항상 좋은 것은 아니다. 데이터 양이 적거나, 처리 비용이 작거나, 순서가 중요하거나, 공유 자원을 변경하는 작업이 있다면 오히려 문제가 될 수 있다. 하지만 구조적으로 보면 내부 반복자는 반복 제어권을 라이브러리가 가지고 있기 때문에, 최적화나 병렬 처리에 유리한 여지가 생긴다.</p>
<p>&nbsp;</p>
<h1 id="🫣-스트림이-항상-정답은-아니다">🫣 스트림이 항상 정답은 아니다</h1>
<p>스트림이 편리하다고 해서 항상 스트림을 써야 하는 것은 아니다. 단순한 반복은 오히려 <code>for</code>문이 더 읽기 쉬울 수 있다.</p>
<pre><code class="language-java">for (int i = 0; i &lt; 5; i++) {
    System.out.println(i);
}</code></pre>
<p>이런 코드를 굳이 스트림으로 바꾸면 오히려 어색해질 수 있다.</p>
<p>또한 반복 중간에 복잡한 상태 변경이 필요하거나, <code>break</code>, <code>continue</code>처럼 흐름 제어가 중요한 경우에는 일반 반복문이 더 적합할 수 있다. 예를 들어 특정 학생을 찾으면 즉시 반복을 멈추고 싶다고 해보자.</p>
<pre><code class="language-java">for (Student student : students) {
    if (student.getName().equals(&quot;Kim&quot;)) {
        System.out.println(&quot;찾았다!&quot;);
        break;
    }
}</code></pre>
<p>스트림에도 <code>findFirst</code>, <code>anyMatch</code> 같은 최종 연산이 있지만, 복잡한 흐름 제어가 들어가면 오히려 코드가 읽기 어려워질 수 있다.</p>
<p>&nbsp;</p>
<p>따라서 기준은 다음과 같이 잡으면 좋다.</p>
<ul>
<li>단순 반복, 복잡한 흐름 제어가 필요하다 → for문이 나을 수 있다.</li>
<li>필터링, 변환, 정렬, 집계가 중심이다 → 스트림이 잘 어울린다.</li>
</ul>
<p>&nbsp;</p>
<h1 id="🖋️-정리">🖋️ 정리</h1>
<p>스트림을 처음 배울 때는 다음 세 가지를 반드시 기억해야 한다.</p>
<p>첫 번째, 스트림은 원본 컬렉션을 직접 변경하지 않는다.</p>
<pre><code class="language-text">원본 데이터는 그대로 두고,
처리 결과를 새롭게 만들어서 사용한다.</code></pre>
<p>&nbsp;</p>
<p>두 번째, 중간 연산만으로는 결과가 나오지 않는다.</p>
<pre><code class="language-text">filter, map, sorted는 중간 처리기다.
이 연산들은 대부분 다시 Stream을 반환한다.
따라서 최종 연산을 해야 결과를 꺼낼 수 있다.</code></pre>
<p>&nbsp;</p>
<p>세 번째, 스트림은 내부 반복자를 사용한다.</p>
<pre><code class="language-text">for, while, Iterator는 외부 반복자다.
개발자가 직접 요소를 꺼내며 반복을 제어한다.

Stream은 내부 반복자다.
반복은 스트림 내부에서 일어나고,
개발자는 람다식으로 처리 규칙만 전달한다.</code></pre>
<p>&nbsp;</p>
<p>결국 스트림은 다음과 같이 이해할 수 있다.</p>
<pre><code class="language-text">스트림은 컬렉션의 요소를 직접 꺼내던 외부 반복 방식에서 벗어나,
컬렉션 내부에서 요소를 흘려보내며
람다식으로 전달한 처리 규칙을 적용하고,
최종 연산으로 결과를 꺼내는 내부 반복 기반의 데이터 처리 방식이다.</code></pre>
<p>&nbsp;</p>
<p>처음 비유로 다시 돌아가면 다음과 같다.</p>
<pre><code class="language-text">내 몸 = 컬렉션
주사기 = 스트림
주사하는 것 = 람다식으로 전달하는 처리 규칙
중간 처리기 = filter, map, sorted
추출기 = forEach, sum, collect, toList</code></pre>
<p>&nbsp;</p>
<p>단, 스트림은 결과물이 담긴 주사기 자체가 아니다. 스트림은 데이터를 흘려보내며 처리하는 흐름이다. 따라서 중간 처리기만 연결해두면 아직 결과물이 아니라 <code>Stream</code> 타입일 뿐이다. 결과물을 사용하려면 반드시 <code>forEach</code>, <code>sum</code>, <code>collect</code>, <code>toList</code> 같은 최종 연산으로 꺼내야 한다. 이 관점을 잡으면 스트림 코드는 훨씬 자연스럽게 읽힌다.</p>
<p>스트림은 단순히 반복문을 짧게 쓰기 위한 문법이 아니다. 스트림은 컬렉션 데이터를 더 선언적으로, 더 조합하기 쉽게, 그리고 원본 데이터를 직접 변경하지 않는 방식으로 처리하기 위한 도구다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[아키텍처 개요]]></title>
            <link>https://velog.io/@rocker_nun/%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EA%B0%9C%EC%9A%94</link>
            <guid>https://velog.io/@rocker_nun/%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EA%B0%9C%EC%9A%94</guid>
            <pubDate>Fri, 01 May 2026 06:58:57 GMT</pubDate>
            <description><![CDATA[<h1 id="4️⃣-네-개의-영역">4️⃣ 네 개의 영역</h1>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/33c3f699-cac6-4e61-99be-65e76c1de4fe/image.png" alt=""></p>
<p>위 4개의 영역 중 <strong>표현(Presentation)</strong> 영역은 사용자의 요청을 받아 <strong>응용(Application)</strong> 영역에 전달하고 응용 영역의 결과를 다시 사용자에게 보여주는 역할을 한다. 표현 영역을 통해 사용자의 요청을 전달받는 응용 영역은 시스템이 사용자에게 제공해야 할 기능을 구현하는데 <em>“주문 등록”</em>, <em>“주문 취소”</em>, <em>“상품 상세 조회”</em> 와 같은 기능 구현을 예로 들 수 있다. 응용 영역 역시 기능을 구현하기 위해 도메인 영역의 도메인 모델을 사용한다. 아래 코드를 보자.</p>
<pre><code class="language-java">public class CancelOrderService {

    @Transactional
    public void cancelOrder(String orderId) {
        Order order = findOrderById(orderId);
        if (order == null) {
            throw new OrderNotFoundException(orderId);          
        }
        order.cancel();
    }

    ...
}</code></pre>
<p>주문 취소 기능을 제공하는 응용 서비스 코드다. 이처럼 응용 서비스는 로직을 직접 수행하기보다는 <code>Order</code>라는 도메인 모델에 책임을 위임해서 <code>Order</code> 클래스가 <code>cancel()</code> 메서드가 실행하도록 처리했다. 이처럼 응용 영역은 도메인 모델을 이용해서 사용자에게 제공할 기능을 구현한다. 실제 도메인 로직 구현은 도메인 모델에 위임한다. </p>
<p><strong>인프라스트럭처(Infrastructure)</strong> 영역은 DB 연동, 메시징 큐에 메시지를 전송하거나 수신하는 기능을 구현하는 등 논리적인 개념을 표현하기보다는 실제 구현을 다루는 영역이다. 표현 영역은 HTTP, JSON, 웹 프레임워크 같은 기술을 사용해 외부 요청을 처리한다. 응용 영역도 트랜잭션 처리와 같은 프레임워크 기능을 일부 사용할 수 있다. 다만 핵심 도메인 규칙은 특정 구현 기술에 의존하지 않도록 도메인 영역에 위치시키는 것이 중요하다. 인프라스트럭처 영역은 DB, 메시징, 외부 API, 룰 엔진 같은 구체 기술을 담당한다.</p>
<p>&nbsp;</p>
<h1 id="↕️-계층-구조-아키텍처">↕️ 계층 구조 아키텍처</h1>
<p>위의 4개의 영역으로 나눠놓은 계층 구조에서는 상위 계층에서 하위 계층으로의 의존만 존재할 뿐, 그 반대 방향의 의존은 존재하지 않는다. 계층 구조를 엄격하게 지킨다고 한다면, 바로 아래의 하위 계층에만 의존해야 하지만, 상황에 따라 더 아래 계층에도 의존할 수도 있다.</p>
<img src="https://velog.velcdn.com/images/rocker_nun/post/114bd3dd-71a0-4bb9-9a51-c946db8589b6/image.png" width=500 />


<p>결국 응용 계층과 도메인 계층은 인프라스트럭처 계층의 기능을 사용하기 때문에 위와 같은 계층 구조가 직관적으로 이해하기 쉬울 것이다. 다만, 각 계층이 구체적인 구현 기술이 담겨 있는 인프라스트럭처 계층에 종속된다는 점은 명백한 사실이다. </p>
<p>&nbsp;</p>
<p>예시로 도메인의 가격 계산 규칙 코드를 보자.</p>
<pre><code class="language-java">public class DroolsRuleEngine {
    private KieContainer kContainer;

    public DroolsRuleEngine() {
        KieServices ks = KieServices.Factory.get();
        kContainer = ks.getKieClasspathContainer();
    }

    public void evaluate(String sessionName, List&lt;?&gt; facts) {
        KieSession kSession = kContainer.newKieSession(sessionName);
        try {
            facts.forEach(x -&gt; kSession.insert(x));
            kSession.fireAllRules();
        } finally {
            kSession.dispose();
        }
    }
}</code></pre>
<p>&nbsp;</p>
<p><code>evaluate()</code> 메서드에 파라미터를 넘기면 별도 파일로 작성한 규칙을 이용해서 연산을 수행하는 간단한 코드다. 이제 응용 영역은 가격 계산을 위해 인프라스트럭처 영역의 <code>DroolsRuleEngine</code>을 사용할 것이다. </p>
<pre><code class="language-java">// 응용 계층의 서비스 코드
public class CalculateDiscountService {
    private DroolsRuleEngine ruleEngine;

    public CalculateDiscountService() {
        ruleEngine = new DroolsRuleEngine();
    }

    public Money calculateDiscount(List&lt;OrderLine&gt; orderLines, String customerId) {
        Customer customer = findCustomer(customerId);

        MutableMoney money = new MutableMoney(0);

        List&lt;?&gt; facts = new ArrayList&lt;&gt;();
        facts.add(customer);
                facts.add(money);
        facts.addAll(orderLines);

        ruleEngine.evaluate(&quot;discountCalculation&quot;, facts);
        return money.toImmutableMoney();
    }

    ...
}</code></pre>
<p>별다른 문제가 없어 보이지만, 2가지 치명적인 문제가 있다. 먼저 <code>CalculateDiscountService</code> 클래스만 <strong>테스트하기 어렵다</strong>는 것이다. 왜냐하면 해당 클래스를 테스트하기 위해서는 그 전에 <code>ruleEngine</code>이 완벽하게 동작해야 한다. 두 번째 <strong>문제는 구현 방식을 변경하기 어렵다</strong>는 것이다. <code>evaluate()</code> 메서드로 연산을 수행하기 위해 <em>“discountCalculation”</em> 라는 이름의 세션을 넘겨야 하는데 이는 <code>Drools</code>의 세션 이름이다. 따라서 그 세션 이름이 바뀐다? <code>CalculateDiscountService</code> 코드까지 찾아와서 추가로 수정해줘야 한다. <code>Drools</code>가 아닌 다른 구현 기술로 변경하기로 결정했다면 문제는 더욱 커진다. </p>
<p>&nbsp;</p>
<h1 id="👍-dip로-문제-해결">👍 DIP로 문제 해결</h1>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/24f7f334-3781-4bf5-93f7-58543cd685ef/image.png" alt=""></p>
<p>가격 할인 계산 기능을 구현하는 <code>CalculateDiscountService</code> 고수준 모듈은 고객 정보도 구해야 하고, 룰을 실행하는 등 여러 하위 기능이 필요한 상황이다. 앞선 문제점들을 해결하기 위해서는 저수준 모듈이 고수준 모듈에 의존하도록 설계를 변경해야 할 필요가 있다. 바로 이때 <strong>인터페이스(Interface)</strong>를 사용하면 된다.</p>
<pre><code class="language-java">// 고객 정보와 룰을 적용해서 할인 금액을 구하는 역할을 하는 인터페이스
public interface RuleDiscounter {
    Money applyRules(Customer customer, List&lt;OrderLine&gt; orderLines);
}

// 인터페이스에 의존하도록 리팩토링 한 서비스 코드
public class CalculateDiscountService {
    private RuleDiscounter ruleDiscounter;

    public CalculateDiscountService(RuleDiscounter ruleDiscounter) {
        this.ruleDiscounter = ruleDiscounter;
    }

    public Money calculateDiscount(List&lt;OrderLine&gt; orderLines, String customerId) {
        Customer customer = findCustomer(customerId);
        return ruleDiscounter.applyRules(customer, orderLines);
    }

    ...
}</code></pre>
<p>이제 더 이상 <code>Drools</code>에 의존하는 코드는 전혀 찾아볼 수 없다. 그냥 <code>RuleDiscounter</code>가 룰을 적용한다는 사실만 알 뿐이다. 이제 상황에 맞게 해당 인터페이스를 구현한 구현체가 생성자를 통해 필드에 꽂힐 것이다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/e1d60000-f1a5-445a-86b7-8095d9eabc70/image.png" alt=""></p>
<p>위 그림이 바로 최종 DIP를 적용한 다이어그램이다. 고수준 모듈이 저수준 모듈을 사용하려면 고수준 모듈이 저수준 모듈에 의존해야 하는데, 반대로 저수준 모듈이 고수준 모듈에 의존한다고 해서 <strong>DIP(Dependency Inversion Principle) 의존 역전 원칙</strong>이라고 한다.</p>
<p>&nbsp;</p>
<p>이제 테스트를 위해 고객을 찾는 기능도 고수준 인터페이스로 추가로 설계해서 서비스 코드를 리팩토링 해보자.</p>
<pre><code class="language-java">public class CalculateDiscountService {
    private CustomerRepository customerRepository;  // 고객 조회 고수준 인터페이스에 의존
    private RuleDiscounter ruleDiscounter;  // 규칙 인터페이스에 의존

    public CalculateDiscountService(CustomerRepository customerRepository, RuleDiscounter ruleDiscounter) {
        this.customerRepository = customerRepository;
        this.ruleDiscounter = ruleDiscounter;
    }

    public Money calculateDiscount(List&lt;OrderLine&gt; orderLines, String customerId) {
        Customer customer = findCustomer(customerId);
        return ruleDiscounter.applyRules(customer, orderLines);
    }

    private Customer findCustomer(String customerId) {
        Customer customer = customerRepository.findById(customerId);
        if (customer == null) {
            throw new NoCustomerException();
        }
        return customer;
    }

    ...
}</code></pre>
<p>이제 룰에 대한 로직과 고객을 조회하는 구체 클래스가 구현되어 있지 않더라도 대역 객체를 사용해서 테스트를 충분히 진행할 수 있게 됐다. </p>
<pre><code class="language-java">public class CalculateDiscountServiceTest {

    @Test
    public void noCustomer_thenExceptionShouldBeThrown() {
        // 대역 객체 생성
        CustomerRepository stubRepo = mock(CustomerRepository.class);
        when(stubRepo.findById(&quot;noCusId&quot;)).thenReturn(null);

        RuleDiscounter stubRule = (cust, lines) -&gt; null;

        // 대역 객체들을 주입 받아서 테스트 진행 가능
        CalculateDiscountService service = new CalculateDiscountService(stubRepo, stubRule);
        assertThrows(NoCustomerException.class, () -&gt; service.calculateDiscount(someLines, &quot;noCusId&quot;));
    }
}</code></pre>
<p>&nbsp;</p>
<h3 id="💥-dip-주의사항">💥 DIP 주의사항</h3>
<p>DIP는 단순히 인터페이스와 구체 클래스로 쪼개는 것이 아니라 고수준 모듈이 저수준 모듈에 의존하지 않도록 하는 것이 핵심이다. 따라서 아래와 같이 저수준 모듈에서 인터페이스를 추출하면 안 된다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/b6658408-6867-4a24-834a-a772aef38b5d/image.png" alt=""></p>
<p>이건 그냥 도메인 영역인 <code>CalculateDiscountService</code>가 구현 기술을 다루는 인프라스트럭처 계층에 직접적으로 의존하고 있는 상황이다. 고수준 모듈이 아닌 그냥 <code>RuleEngine</code>이라는 저수준 모듈에서 인터페이스를 뽑아낸 것이다. DIP를 적용할 때는 아래와 같이 <strong>하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출해야 한다.</strong> </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/4a256018-c293-4e0e-b559-909b304c8aed/image.png" alt=""></p>
<p>이처럼 인프라스트럭처 영역은 저수준, 응용 영역과 도메인 영역은 고수준 모듈이다. 근데 4개의 영역 구조를 보면 인프라스트럭처 영역이 가장 하단에 위치했는데 DIP를 적용하면 인프라스트럭처 영역이 응용 영역과 도메인 영역에 의존하는 구조가 된다. 하지만 고수준 모듈에서 정의한 인터페이스를 상속 받아서 구현하기 때문에 상위 계층에 영향을 주지 않는다. 아래 다이어그램을 보자.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/a23ec057-2361-40ee-8818-a4d769f1ace2/image.png" alt=""></p>
<p>다이어그램을 보면 인프라스트럭처 영역의 <code>CompositeNotifier</code> 클래스는 응용 영역의 <code>Notifier</code> 인터페이스를 구현하고, <code>JpaRepository</code>는 도메인 영역의 <code>OrderRepository</code> 인터페이스, <code>DroolsRuleDiscounter</code> 는 <code>RuleDiscounter</code> 인터페이스를 구현하고 있기 때문에 향후 새로운 요구사항이 들어왔을 때도 응용 영역의 <code>OrderService</code> 코드 변경 없이 기능을 교체하거나 확장할 수 있다. </p>
<p>&nbsp;</p>
<h1 id="🦾-도메인-영역의-주요-구성요소">🦾 도메인 영역의 주요 구성요소</h1>
<p>도메인 영역의 모델은 도메인의 주요 개념을 표현하며 핵심 로직을 구현한다. 도메인 영역의 주요 구성요소는 엔티티와 밸류 타입이라고 했는데 사실 다른 요소들이 더 존재한다. 전체적으로 정리해보자.</p>
<ul>
<li><p><strong>엔티티(Entity)</strong>: 고유의 식별자를 갖는 객체로 자신의 생명주기를 갖는다. 주문, 회원, 상품과 같이 도메인의 고유한 개념을 표현한다. 도메인 모델의 데이터를 포함하며 해당 데이터와 관련된 기능을 함께 제공한다.</p>
</li>
<li><p><strong>밸류(Value)</strong>: 고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나인 값을 표현할 때 사용된다. 배송지 주소를 표현하기 위한 주소나 구매 금액을 위한 금액과 같은 타입이 밸류 타입이다. 엔티티의 속성으로 사용할 뿐만 아니라 다른 밸류 타입의 속성으로도 사용할 수 있다.</p>
</li>
<li><p><strong>애그리거트(Aggregate)</strong>: 애그리거트는 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것이다. 예를 들어, 주문과 관련된 <code>Order</code> 엔티티, <code>OrderLine</code> 밸류, <code>Orderer</code> 밸류 객체를 주문 애그리거트로 묶을 수 있다.</p>
</li>
<li><p><strong>리포지토리(Repository)</strong>: 도메인 모델의 영속성을 처리한다. 예를 들어, DBMS 테이블에서 엔티티 객체를 로딩하거나 저장하는 기능을 제공한다.</p>
</li>
<li><p><strong>도메인 서비스(Domain Service)</strong>: 특정 엔티티에 속하지 않은 도메인 로직을 제공한다. 할인 금액 계산은 상품, 회원 등급, 구매 금액 등 다양한 조건을 이용해서 구현하게 되는데, 이렇게 도메인 로직이 여러 엔티티와 밸류를 필요로 하면 도메인 서비스에서 로직을 구현한다.</p>
</li>
</ul>
<p>&nbsp;</p>
<p>근데 엔티티를 볼 때마다 든 생각인데, <em>“DB 테이블을 설계할 때의 그 엔티티랑 같은 건가?”</em> 라는 의문이 들었다. 결론부터 말하면 도메인 모델의 엔티티와 DB 관계형 모델의 엔티티는 같은 것이 아니다. 차이점은 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공한다는 점이다. 뭔 소리지? 아래 코드를 보자.</p>
<pre><code class="language-java">public class Order {
    private OrderNo number;
    private Orderer orderer;
    private ShippingInfo shippingInfo;

    ...

    // 도메인 모델 엔티티는 도메인 기능도 함께 제공
    public void changeShippingInfo(ShippingInfo newShippingInfo) {
        ...
    }
}</code></pre>
<p>이처럼 그냥 데이터만 담고 있는 객체가 아니라 구체적인 기능을 제공할 수 있다는 말이다. DB 테이블은 그냥 필드만 작성돼 있지, 메서드(기능)가 적혀 있지 않으니까… </p>
<p>그리고 또 다른 차이점은 도메인 모델의 엔티티는 2개 이상의 데이터가 개념적으로 하나인 경우에 밸류 타입으로 뽑아낼 수 있다는 점이다. 위의 <code>Orderer</code>만 보더라도 아래와 같이 주문자 이름과 이메일 데이터를 포함할 수 있다. </p>
<pre><code class="language-java">public class Orderer {
    private String name;
    private String email;

    ...
}</code></pre>
<p>DB 테이블에서 밸류 타입을 표현하기 위해서는 해당 테이블에 데이터를 넣거나 별도 테이블로 분리해야 한다. </p>
<p>&nbsp;</p>
<h2 id="📦-애그리거트aggregate">📦 애그리거트(Aggregate)</h2>
<p>도메인의 규모가 커질수록 도메인 모델의 구성요소 또한 많아지고 복잡해진다. 이때 도메인 모델 개별 객체뿐만 아니라 상위 수준에서 모델을 볼 수 있어야 전체 모델의 관계와 개별 모델을 이해할 수 있다. 도메인 모델에서 전체 구조를 이해하는 데 도움이 되는 것이 바로 <strong>애그리거트(Aggregate)</strong>다.</p>
<p>애그리거트는 단순히 관련 있는 객체를 모아둔 묶음이 아니라, 함께 변경되어야 하고 같은 규칙 안에서 일관성을 지켜야 하는 객체들의 경계다. 따라서 애그리거트를 나눌 때의 기준은 <strong><em>“관련이 있는가?”</em></strong> 보다 <strong><em>“같은 트랜잭션 안에서 반드시 일관성을 유지해야 하는가?”</em></strong> 에 가깝다. 예를 들어 주문도 애그리거트인데, 주문은 <em>“주문”, “배송지 정보”, “주문자”, “주문 목록”, “총 결제 금액”</em> 의 하위 모델로 이루어져 있다. 애그리거트를 사용하면 각각의 객체가 아닌 관련 객체를 묶어서 객체 군집 단위로 도메인을 바라볼 수 있다. 따라서 애그리거트 간의 관계로 확장해서 도메인 모델을 이해할 수 있다.</p>
<p>애그리거트는 군집에 속한 객체를 관리하는 <strong>루트 엔티티(Root Entity)</strong>를 갖는다. 이 엔티티는 애그리거트에 속해 있는 엔티티와 밸류 객체를 이용해서 애그리거트가 구현해야 할 기능을 제공한다. 애그리거트를 사용하는 코드는 루트 엔티티가 제공하는 기능을 실행하고, 루트 엔티티를 통해 애그리거트 내의 다른 엔티티나 밸류 객체에 접근하여 내부 구현을 숨겨서 애그리거트 단위로 구현을 캡슐화할 수 있도록 돕는다. 이해를 위해 아래 다이어그램을 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/c536d602-3d3c-4458-bcbd-f73ca27c2496/image.png" alt=""></p>
<p><code>Order</code> 애그리거트 루트는 주문 도메인 로직에 맞게 애그리거트의 상태를 관리한다. <code>Order</code>의 배송지 정보를 변경하기 위해서는 일단 배송지를 변경할 수 있는지 확인한 뒤에 배송지를 변경하는 것처럼 말이다. 아래 코드를 보자.</p>
<pre><code class="language-java">public class Order {
    ...

    public void changeShippingInfo(ShippingInfo newShippingInfo) {
        checkShippingInfoChangeable();  // 배송지 변경 가능 여부 확인
        this.shippingInfo = newShippingInfo;
    }

    private void checkShippingInfoChangeable() {
        // 배송지 변경이 가능한지 여부를 검사하는 도메인 규칙
    }
}</code></pre>
<p>쉽게 말해, 주문 애그리거트는 루트 엔티티인 <code>Order</code>를 통하지 않고서는 <code>ShippingInfo</code>를 변경할 수 있는 방법을 제공하지 않는다는 것이다. 애그리거트를 어떻게 구현해야 하는지는 이후에 살펴보도록 하자.</p>
<p>&nbsp;</p>
<h3 id="🤔-도메인-모델-vs-애그리거트">🤔 도메인 모델 vs 애그리거트</h3>
<p><strong>도메인 모델(Domain Model)</strong>은 특정 도메인을 이해하고 표현하기 위한 개념 모델이다. 예를 들어 주문 도메인을 모델링한다면 <code>Order</code>, <code>OrderLine</code>, <code>ShippingInfo</code>, <code>Orderer</code>, <code>Money</code>, <code>OrderState</code> 같은 개념들이 등장할 수 있다. 이처럼 도메인 모델은 해당 도메인을 설명하기 위해 필요한 개념, 규칙, 상태, 관계를 포함하는 넓은 범위의 모델이다. </p>
<p>반면, <strong>애그리거트(Aggregate)</strong>는 도메인 모델 안에서 관련 객체들을 하나의 일관성 단위로 묶은 군집이다. 단순히 객체들이 서로 관련 있다고 해서 모두 하나의 애그리거트가 되는 것은 아니다. 애그리거트는 함께 변경되어야 하고, 반드시 같은 규칙 안에서 일관성을 유지해야 하는 객체들을 하나의 경계로 묶은 것이다.</p>
<p>예를 들어 주문은 <code>Order</code>, <code>OrderLine</code>, <code>ShippingInfo</code>, <code>Orderer</code>, <code>Money</code> 같은 여러 객체로 구성될 수 있다. 이때 <code>OrderLine</code>은 주문 없이 독립적으로 존재하기 어렵고, 총 주문 금액은 주문 항목들의 금액 합과 일치해야 하며, 배송지 변경이나 주문 취소 같은 규칙도 주문 상태에 따라 통제되어야 한다. 따라서 이 객체들은 <code>Order</code>를 중심으로 하나의 애그리거트로 묶을 수 있다.</p>
<pre><code>Order 애그리거트
 ├── Order    // 애그리거트 루트
 ├── OrderLine
 ├── ShippingInfo
 ├── Orderer
 ├── Money
 └── OrderState</code></pre><p>여기서 <code>Order</code>는 애그리거트 루트다. 애그리거트 루트는 애그리거트 내부 객체에 접근하고 변경하는 진입점 역할을 한다. 외부에서는 <code>OrderLine</code>이나 <code>ShippingInfo</code>를 직접 수정하는 것이 아니라, <code>Order.changeShippingInfo()</code>, <code>Order.cancel()</code> 같은 메서드를 통해 변경해야 한다. 그래야 애그리거트 내부의 규칙과 일관성을 안전하게 지킬 수 있다.</p>
<p>따라서 애그리거트는 도메인 모델이라는 큰 범주 안에 포함된다. 모든 도메인 모델이 애그리거트인 것은 아니지만, 애그리거트는 도메인 모델을 구성하는 중요한 설계 단위다. 한 문장으로 정리하면, <strong>도메인 모델(Domain Model)은 도메인을 이해하기 위한 전체 지도이고, 애그리거트(Aggregate)는 그 지도 안에서 함께 일관성을 지켜야 하는 객체들의 경계</strong>라고 볼 수 있다.</p>
<p>&nbsp;</p>
<h2 id="💾-리포지토리repository">💾 리포지토리(Repository)</h2>
<p>도메인 객체를 지속적으로 사용하기 위해서는 DB에 안전하게 보관해야 한다. 이를 위해 사용하는 도메인 영역의 구성요소가 바로 <strong>리포지토리(Repository)</strong>다.</p>
<p>리포지토리는 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다. 예시로 주문 애그리거트를 위한 리포지토리는 아래와 같이 구현할 수 있다. </p>
<pre><code class="language-java">public interface OrderRepository {
    Order findByNumber(OrderNumber number);
    void save(Order order);
    void delete(Order order);
}</code></pre>
<p>보다시피 찾고 저장하는 대상이 <code>Order</code> 루트 애그리거트다. 도메인 모델을 사용해야 하는 코드는 일단 리포지토리를 통해 도메인 객체를 꺼내와서 기능을 수행하도록 해야 한다. </p>
<pre><code class="language-java">public class CancelOrderService {
    private OrderRepository orderRepository;

    public void cancel(OrderNumber number) {
        Order order = orderRepository.findByNumber(number);  // 리포지토리에서 도메인 객체를 꺼냄
        if (order == null) {
            throw new NoOrderException(number);
        }
        order.cancel();  // 그 다음 기능 수행
    }
}</code></pre>
<p>위 코드를 보면 알겠지만 리포지토리 도메인 모델은 도메인 객체를 영속화하는 데 필요한 기능들을 인터페이스로 추상화했기 때문에 고수준 모듈에 속한다. <code>OrderRepository</code>를 구현한 클래스들은 저수준 모듈로 인프라스트럭처 영역에 속하는 것이다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/9bb4b0c1-76b0-40ff-9cd9-50405689a96b/image.png" alt=""></p>
<p>위의 코드와 다이어그램을 보면 응용 서비스는 의존 관계 주입을 통해 실제 리포지토리 구현 객체(<code>JpaOrderRepository</code>)에 접근하게 되는 것이다. 이처럼 응용 서비스는 리포지토리를 통해 애그리거트를 조회하고, 도메인 기능을 실행한 뒤, 변경된 상태가 저장소에 일관되게 반영되도록 트랜잭션 경계를 관리한다.</p>
<p>&nbsp;</p>
<h1 id="🌊-요청-처리-흐름">🌊 요청 처리 흐름</h1>
<p>알다시피 사용자의 요청을 처음 받는 영역은 <strong>표현(Presentation)</strong> 영역이다. 표현 영역은 사용자가 전송한 데이터 형식이 올바른지 검사하고 문제가 없다면 데이터를 이용해서 응용 서비스에 기능 실행을 위임한다. 이때 표현 영역은 데이터를 응용 서비스가 요구하는 형식으로 변환해서 전달하게 된다. 아래는 대략적인 요청 처리 흐름이다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/26f233e9-181b-4e4f-87e7-b195f2340a1e/image.png" alt=""></p>
<p>보다시피 컨트롤러(표현 영역)가 사용자의 요청을 받아서 응용 서비스가 요구하는 형식으로 데이터를 변환해서 응용 서비스에 전달한다. 응용 서비스는 도메인의 기능을 사용하기 위해 리포지토리로부터 도메인 객체를 꺼내서 실행하거나 신규 도메인 객체를 생성해서 리포지토리에 저장한다. 추가로 도메인의 상태가 변경되는 기능은 리포지토리에 일관되게 반영되도록 트랜잭션을 관리해야 한다. </p>
<pre><code class="language-java">public class CancelOrderService {
    private OrderRepository orderRepository;

    // 응용 서비스는 트랜잭션을 관리해야 한다.
    @Transactional
    public void cancel(OrderNumber number) {
        Order order = orderRepository.findByNumber(number);
        if (order == null) {
            throw new NoOrderException(number);
        }
        order.cancel();
    }
    ...
}</code></pre>
<p>&nbsp;</p>
<h1 id="🏗️-인프라스트럭처-개요">🏗️ 인프라스트럭처 개요</h1>
<p><strong>인프라스트럭처(Infrastructure)</strong>는 다른 영역에서 필요로 하는 프레임워크, 구현 기술, 보조 기능을 지원한다. 여기서 명심해야 할 점은 위에서도 말했지만 고수준 모듈에서 정의한 인터페이스를 저수준 모듈인 인프라스트럭처 영역에서 구현하는 것이 시스템을 더 유연하게 만들고 테스트하기 쉽게 만들어준다. 하지만 상황에 따라 인프라스트럭처에 대한 의존을 일부 도메인에 넣을 수도 있다.</p>
<p>&nbsp;</p>
<h1 id="🗂️-모듈-구성">🗂️ 모듈 구성</h1>
<p>위에서 봤던 표현, 응용, 도메인, 인프라스트럭처 계층이 있었듯이 각 영역별로 모듈이 위치할 패키지를 구성해주는 것이 기본이다. 여기서 도메인에 따라 알맞은 패키지로 대체할 수 있다. 도메인 하나가 너무 크다면 여러 개의 하위 도메인으로 나누고 그 도메인마다 패키지를 구성하면 된다. 여기서 도메인 모듈은 도메인에 속한 애그리거트를 기준으로 다시 패키지를 구성한다.</p>
<pre><code>com.myshop.order
 ├── ui
 │   └── OrderController
 ├── application
 │   ├── CancelOrderService
 │   └── PlaceOrderService
 ├── domain
 │   ├── Order
 │   ├── OrderLine
 │   ├── OrderNo
 │   ├── OrderRepository
 │   └── ShippingInfo
 └── infrastructure
     └── JpaOrderRepository</code></pre><p>애그리거트, 모델, 리포지토리는 같은 패키지에 위치시킨다. 도메인이 너무 복잡하면 도메인 모델과 도메인 서비스를 별도 패키지에 위치시킬 수도 있다. 이처럼 모듈 구조를 어디까지 나눠야 하는지에 대한 정답은 없다. 하나의 패키지에 몇 개 정도의 타입을 구성해야 하는지에 대한 본인만의 기준을 세우는 것이 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[도메인 모델 시작하기]]></title>
            <link>https://velog.io/@rocker_nun/%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%AA%A8%EB%8D%B8-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@rocker_nun/%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%AA%A8%EB%8D%B8-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 30 Apr 2026 00:59:01 GMT</pubDate>
            <description><![CDATA[<h1 id="📝-도메인domain이란">📝 도메인(Domain)이란?</h1>
<p>온라인 서점을 한번 떠올려보면 장바구니 기능, 쿠폰 적용 기능, 결제 기능, 배송 추적 기능 등 상당히 많은 기능들을 가지고 있다. 이때 <strong>온라인 서점</strong>이 바로 소프트웨어로 해결하고자 하는 문제 영역, 즉 <strong>도메인(Domain)</strong>에 해당한다.</p>
<p>한 도메인은 다시 하위의 여러 도메인으로 나눌 수 있다. 위의 온라인 서점만 생각하더라도 카탈로그 도메인, 주문 도메인, 혜택 도메인, 배송 도메인 등 여러 도메인으로 쪼개질 수 있다. 이 하위 도메인들이 서로 연동하여 완전한 기능을 제공하는 것이다. 여기서 하나의 도메인을 맡았다고 그 도메인이 제공해야 할 모든 기능을 직접 구현하는 것은 아니다. 예를 들어, 배송 시스템은 다른 업체의 제품을 채택하고 그 제품을 사용하기 위해 필요한 기능만 일부 연동하는 것이다. </p>
<p>알다시피 개발자는 고객의 요구사항을 분석하고 설계하여 코드를 작성하며 테스트하고 배포한다. 따라서 요구사항을 명확하게 이해하는 것이 가장 중요한데, 이를 위해 그 해당 도메인의 전문가와 직접 대화해서 그 전문가만큼은 아니어도 해당 도메인 지식을 갖춰야 할 필요가 있다. 도메인 주도 설계는 단순히 클래스를 잘 나누는 설계 기법이 아니다. 복잡한 비즈니스 문제를 이해하고, 도메인 전문가와 개발자가 같은 언어로 소통하며, 그 이해를 모델과 코드에 반영해 나가는 개발 방식이다.</p>
<p>&nbsp;</p>
<h1 id="🩻-도메인-모델domain-model">🩻 도메인 모델(Domain Model)</h1>
<p>도메인 모델은 특정 도메인을 개념적으로 표현한 것이다. 예시로 온라인 서점의 주문 모델을 객체 모델로 구성하면 아래 그림과 같이 만들 수 있다. </p>
<h3 id="🍔-클래스-다이어그램">🍔 클래스 다이어그램</h3>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/3fbe6033-94b3-452b-a54b-b80d8214f597/image.png" alt=""></p>
<p>위 도메인 모델은 <strong>객체를 이용한 도메인 모델</strong>이다. 도메인을 이해하기 위해서는 해당 <strong>도메인이 제공하는 기능</strong>과 <strong>필요한 주요 데이터</strong>를 알아야 하는데, 이런 면에서 객체 모델이 도메인을 모델링하기에 안성맞춤이다. 주문(<code>Order</code>)은 주문 번호(<code>orderNumber</code>), 총 주문 금액(<code>totalAmounts</code>)이 있고, 배송 정보(<code>ShippingInfo</code>)를 변경(<code>changeShipping</code>)할 수 있고, 주문을 취소(<code>cancel</code>)할 수 있다는 사실을 바로 알 수 있다. 이처럼 개발자가 아니더라도 해당 도메인에 대한 이해가 필요한 모든 사람들이 지식을 이해하고 공유하는데 많은 도움이 된다. </p>
<p>하지만 도메인을 모델링하기 위해 꼭 객체만 사용할 수 있는 것은 아니다. 아래와 같이 상태 다이어그램을 이용해서 주문의 상태 전이를 모델링할 수도 있다. </p>
<p>&nbsp;</p>
<h3 id="🔁-상태-다이어그램">🔁 상태 다이어그램</h3>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/d453bf53-57a1-4b0f-a086-23f3a4aa22b9/image.png" alt=""></p>
<p>이외에도 다양한 방법으로 도메인을 모델링할 수 있다. 관계가 중요한 도메인이라면 그래프를 이용할 수 있고, 계산 규칙이 중요하다면 수학 공식을 활용해서 모델링 할 수도 있다. 하여간 도메인을 이해하는데 도움이 된다면 그 어떤 것이든 가능하다. </p>
<p>정리하자면, 도메인 모델은 도메인 자체를 이해하기 위한 개념 모델이다. 이 개념 모델만으로는 바로 기능을 구현할 수 있는 것은 아니기에 구현 기술에 맞는 별도의 구현 모델도 필요하다. <del>그나마 위 클래스 다이어그램은 객체지향 언어로 개념 모델에 가깝게 구현할 수는 있다…</del> 처음부터 완벽한 개념 모델을 만들겠다는 마음보다는 전반적인 개요를 알 수 있는 수준으로 개념 모델을 작성해야 한다. 이후에 코드로 구현하는 과정에서 개념 모델을 구현 모델로 점진적으로 발전시켜 나가야 한다. </p>
<p>&nbsp;</p>
<h3 id="💥-주의-사항">💥 주의 사항</h3>
<p>따라서 여러 하위 도메인의 개념을 하나의 모델에 억지로 합치려고 하면 의미가 뒤섞일 수 있다. 같은 “상품”이라는 용어도 카탈로그 컨텍스트에서는 가격과 설명을 가진 판매 정보일 수 있고, 배송 컨텍스트에서는 실제 고객에게 전달되는 물리적인 물품을 의미할 수 있다.</p>
<p>그래서 도메인 모델은 특정 문맥 안에서 일관된 의미를 가져야 한다. DDD에서는 이런 모델의 의미가 유지되는 경계를 <strong>바운디드 컨텍스트(Bounded Context)</strong>라고 부른다. 하위 도메인은 비즈니스 문제 영역을 나눈 것이고, 바운디드 컨텍스트는 특정 모델이 일관된 의미를 갖는 경계다.</p>
<p>&nbsp;</p>
<h1 id="📦-도메인-모델-패턴">📦 도메인 모델 패턴</h1>
<p>일반적인 애플리케이션의 아키텍처는 아래와 같다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/4587fae4-97a6-4b0d-945b-90847aa14edd/image.png" alt=""></p>
<ul>
<li><p><code>사용자 인터페이스(UI) or 표현(Presentation)</code>: 사용자의 요청을 처리하고 사용자에게 정보를 보여준다. 여기서 사용자는 소프트웨어를 사용하는 사람뿐만 아니라 외부 시스템일 수도 있다.</p>
</li>
<li><p><code>응용(Application)</code>: 사용자가 요청한 기능을 실행한다. 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다.</p>
</li>
<li><p><code>도메인(Domain)</code>: 시스템이 제공할 도메인 규칙을 구현한다.</p>
</li>
<li><p><code>인프라스트럭처(Infrastructure)</code>: 데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동을 처리한다.</p>
</li>
</ul>
<p>&nbsp;</p>
<p>위에서 살펴봤던 도메인 모델은 진짜 도메인에 대해 이해하기 위해 필요한 개념 모델을 의미했다면, 지금부터 살펴볼 도메인 모델은 일종의 패턴을 의미한다. 좀 더 자세히 말하자면, 아키텍처 상의 도메인 계층에서는 도메인의 핵심 규칙을 구현하는데, 이런 도메인의 규칙을 객체지향 기법으로 구현하는 패턴을 말한다. 직접 코드를 보면서 이해해보자.</p>
<pre><code class="language-java">// 주문 상태와 배송 정보를 담은 Order 클래스
public class Order {
    private OrderState state;
    private ShippingInfo shippingInfo;

    public void changeShippingInfo(ShippingInfo newShippingInfo) {
        if (!state.isShippingChangeable()) {
            throw new IllegalStateException(&quot;can&#39;t change shipping in &quot; + state);
        }
        this.shippingInfo = newShippingInfo;
    }

    ...
}

// 주문 상태에 대한 정보를 담은 OrderState 클래스
public enum OrderState {
    PAYMENT_WAITING {
        public boolean isShippingChangeable() {
            return true;
        }
    },
    PREPARING {
        public boolean isShippingChangeable() {
            return true;
        }
    },
    SHIPPED, DELIVERING, DELIVERY_COMPLETED;

    public boolean isShippingChangeable() {
        return false;
    }
}</code></pre>
<p>주문 도메인의 일부 기능을 도메인 모델 패턴으로 구현한 것이다. 주문 상태를 표현하는 <code>OrderState</code>는 현재 주문 상태에서 배송지 정보를 변경할 수 있는지 판단하는 <code>isShippingChangeable()</code> 메서드를 제공한다. <code>PAYMENT_WAITING</code> 또는 <code>PREPARING</code> 상태에서는 아직 출고 전이므로 배송지 변경이 가능하고, <code>SHIPPED</code>, <code>DELIVERING</code>, <code>DELIVERY_COMPLETED</code> 상태에서는 배송지 변경이 불가능하다는 도메인 규칙을 코드로 표현한 것이다.</p>
<p>근데 여기서 <code>OrderState</code>는 <code>Order</code>에 속한 데이터라고 판단할 수도 있지 않나? 주문 상태를 변경할 수 있는지에 대한 판단 정도는 그냥 <code>Order</code>에 넣어도 될거 같다. 아래 코드를 보자.</p>
<pre><code class="language-java">public class Order {
    private OrderState state;
    private ShippingInfo shippingInfo;

    public void changeShippingInfo(ShippingInfo newShippingInfo) {
        if (!isShippingChangeable()) {
            throw new IllegalStateException(&quot;can&#39;t change shipping in &quot; + state);
        }
        this.shippingInfo = newShippingInfo;
    }

    // 주문 상태를 변경할 수 있는지 여부를 검사하는 메서드를 Order 클래스 내부로 이동
    private boolean isShippingChangeable() {
        return state == OrderState.PAYMENT_WAITING || state == OrderState.PREPARING;
    }

    ...

}

public enum OrderState {
    PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED;
}</code></pre>
<p>큰 무리는 없어 보인다. 만약 해당 메서드가 주문 상태뿐만 아니라 다른 정보를 함께 사용하고 있다면 <code>OrderState</code>만으로는 배송지를 변경할 수 있는지 판단할 수 없으므로 <code>Order</code>에서 로직을 구현해야 할 것이다. 아무튼 여기서 중요한 점은 해당 메서드가 <code>Order</code>에 있든 <code>OrderState</code>에 있든 결국 주문과 관련된 중요 업무 규칙을 주문 도메인 모델인 <code>Order</code>나 <code>OrderState</code>에서 구현해야 한다는 것이다.</p>
<p>&nbsp;</p>
<h1 id="🔍-도메인-모델-도출">🔍 도메인 모델 도출</h1>
<blockquote>
<p><strong><em>“그래서 도메인 모델을 어떻게 뽑아내는데?”</em></strong></p>
</blockquote>
<p>개인적으로 그동안 가장 많이 고민하고 판단 기준이 모호하다고 느낀 부분이다. 위에서 언급되었듯이 답은 요구사항에 있다. 요구사항으로부터 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것이다. 주문 도메인에 아래와 같은 요구사항이 있다고 해보자.</p>
<ul>
<li>최소 한 종류 이상의 상품을 주문해야 한다.</li>
<li>한 상품을 1개 이상 주문할 수 있다.</li>
<li>총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.</li>
<li>각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.</li>
<li>주문할 때 배송지 정보를 반드시 지정해야 한다.</li>
<li>배송지 정보는 받는 사람 이름, 전화번호, 주소로 구성된다.</li>
<li>출고를 하면 배송지를 변경할 수 없다.</li>
<li>출고 전에 주문을 취소할 수 있다.</li>
<li>고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.</li>
</ul>
<p>&nbsp;</p>
<p>위 요구사항에서 뽑아낼 수 있는 것은 주문은 배송지를 변경하는 기능, 결제를 완료하는 기능, 출고 상태로 변경하는 기능, 주문을 취소하는 기능을 제공한다는 것이다. </p>
<pre><code class="language-java">public class Order {
    public void changeShippingInfo(ShippingInfo shippingInfo) {...}
    public void completePayment() {...}
    public void changeShipped() {...}
    public void cancel() {...}
}</code></pre>
<p>&nbsp;</p>
<p>그리고 <em>“한 상품을 1개 이상 주문할 수 있다”</em>, <em>“각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다”</em> 라는 요구사항에서는 주문 항목의 데이터를 뽑아낼 수 있다. 주문할 상품, 상품의 가격, 구매 수량, 각 구매 항목의 구매 가격을 포함해야 한다. 이를 바탕으로 <code>OrderLine</code>을 구현해보자.</p>
<pre><code class="language-java">public class OrderLine {
    private Product product;
    private int price;
    private int quantity;
    private int amount;

    public OrderLine(Product product, int price, int quantity) {
        this.product = product;
        this.price = price;
        this.quantity = quantity;
        this.amount = calculateAmount();
    }

    public int getAmount() {
        ...
    }

    private int calculateAmount() {
        return price * quantity;
    }
}</code></pre>
<p>보다시피 <code>OrderLine</code>은 한 상품을 얼마에, 몇 개 살지를 담고 있고 <code>calculateAmount()</code> 메서드로 구매 가격을 구하는 로직을 담고 있다. 그리고 나서 아래 요구사항으로 <code>Order</code>와의 관계를 알 수 있다.</p>
<ul>
<li>최소 한 종류 이상의 상품을 주문해야 한다.</li>
<li>총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.</li>
</ul>
<p>&nbsp;</p>
<p><code>Order</code> 클래스를 리팩토링 해보자.</p>
<pre><code class="language-java">public class Order {
    private List&lt;OrderLine&gt; orderLines;
    private Money totalAmount;

    public Order(List&lt;OrderLine&gt; orderLines) {
        setOrderLines(orderLines);
    }

    private void setOrderLines(List&lt;OrderLine&gt; orderLines) {
        verifyAtLeastOneOrMoreOrderLines(orderLines);
        this.orderLines = orderLines;
        calculateTotalAmount();
    }

    private void verifyAtLeastOneOrMoreOrderLines(List&lt;OrderLine&gt; orderLines) {
        if (orderLines == null || orderLines.isEmpty()) {
            throw new IllegalArgumentException(&quot;No OrderLines...&quot;);
        }
    }

    private void calculateTotalAmount() {
        int sum = orderLines.stream()
                .mapToInt(x -&gt; x.getAmount())
                .sum();
        this.totalAmount = new Money(sum);
    }

    ...  // 다른 메서드
}</code></pre>
<p>최소 한 종류 이상의 상품을 주문해야 한다는 규칙은 실제 <code>Order</code>를 생성할 때 <code>setOrderLines()</code> 메서드에 <code>OrderLine</code> 리스트를 전달해서 <code>verifyAtLeastOneOrMoreOrderLines()</code> 메서드로 검사한다. 검사를 통과하면 <code>calculateTotalAmount()</code> 메서드로 총 주문 금액을 계산하도록 처리했다. </p>
<p>&nbsp;</p>
<p>이제 받는 사람 이름, 전화번호, 주소 데이터를 가지고 있는 배송지 정보 클래스를 아래와 같이 정의해보자.</p>
<pre><code class="language-java">public class ShippingInfo {
    private String receiverName;
    private String receiverPhoneNumber;
    private String shippingAddress1;
    private String shippingAddress2;
    private String shippingZipcode;

    ... 생성자, getter
}</code></pre>
<p>&nbsp;</p>
<p>위의 요구사항 중 <em>“주문할 때 배송지 정보를 반드시 지정해야 한다”</em> 라는 규칙이 있었다. 이 규칙으로 <code>Order</code>를 새로 생성할 때 <code>OrderLine</code> 뿐만 아니라 <code>ShippingInfo</code>도 전달해야 한다는 것을 알 수 있다. 다시 <code>Order</code> 클래스를 리팩토링 해보자.</p>
<pre><code class="language-java">public class Order {
    private List&lt;OrderLine&gt; orderLines;
    private ShippingInfo shippingInfo;  // 배송 정보 필드 추가

    ...

    public Order(List&lt;OrderLine&gt; orderLines, ShippingInfo shippingInfo) {
        setOrderLines(orderLines);
        setShippingInfo(shippingInfo);
    }

    private void setOrderLines(List&lt;OrderLine&gt; orderLines) {
        verifyAtLeastOneOrMoreOrderLines(orderLines);
        this.orderLines = orderLines;
        calculateTotalAmount();
    }

    // 생성 시 배송 정보를 설정하는 메서드
    private void setShippingInfo(ShippingInfo shippingInfo) {
        if (shippingInfo == null) {
            throw new IllegalArgumentException(&quot;No ShippingInfo...&quot;);
        }
        this.shippingInfo = shippingInfo;
    }

    ... 
}</code></pre>
<p>&nbsp;</p>
<p>이외에도 여러가지 제약과 규칙이 존재한다. <em>“출고를 하면 배송지 정보를 변경할 수 없다”</em> 와 <em>“출고 전에 주문을 취소할 수 있다”</em> 와 같은 요구사항은 출고 상태가 되기 전과 후의 제약사항이다. 그럼 <code>Order</code>는 최소한 출고 상태에 대해서는 알고 있어야 한다. 이제 기존의 <code>changeShippingInfo()</code> 메서드와 <code>cancel()</code>은 출고 전에 실행되도록 리팩토링하자.</p>
<pre><code class="language-java">public class Order {
    private OrderState state;

    ...

    // 출고 전일 때만 배송지를 변경할 수 있는 제약 추가
    public void changeShippingInfo(ShippingInfo newShippingInfo) {
        verifyNotYetShipped();
        setShippingInfo(newShippingInfo);
    }

    // 출고 전일 때만 주문을 취소할 수 있는 제약 추가
    public void cancel() {
        verifyNotYetShipped();
        this.state = OrderState.CANCELED;
    }

    // 이미 출고 되었는지 검증하는 메서드
    private void verifyNotYetShipped() {
        if (state != OrderState.PAYMENT_WAITING &amp;&amp; state != OrderState.PREPARING) {
            throw new IllegalArgumentException(&quot;Already shipped...&quot;);
        }
    }
}</code></pre>
<p>이런 식으로 도메인 모델을 점진적으로 만들어나가면 된다.</p>
<p>&nbsp;</p>
<h1 id="🎏-엔티티entity와-밸류value">🎏 엔티티(Entity)와 밸류(Value)</h1>
<p>위에서 도출한 모델은 크게 <strong>엔티티(Entity)</strong>와 <strong>밸류(Value)</strong>로 구분할 수 있다. 아래 클래스 다이어그램을 보자.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/f4de6c4e-4b52-4cf8-958c-c317a0469ab5/image.png" alt=""></p>
<p>엔티티는 <strong>식별자(Identifier)</strong>를 가진다. 각 엔티티는 구분할 수 있다는 말이다. 위의 <code>Order</code> 클래스가 바로 엔티티이며, <em>“주문번호”</em> 라는 식별자를 속성으로 가져야 한다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/7bea5f12-f26a-40e0-8935-d5d28d76ab6b/image.png" alt=""></p>
<p>엔티티의 식별자는 바뀌지 않고 고유하기 때문에 두 엔티티 객체의 식별자가 같으면 두 엔티티는 같다고 말할 수 있다. 엔티티를 구현한 클래스는 식별자를 이용해서 <code>equals()</code> 메서드와 <code>hashCode()</code> 메서드를 구현해야 한다. 아래는 리팩토링한 <code>Order</code> 클래스다.</p>
<pre><code class="language-java">public class Order {
    private String orderNumber;

    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }
        if (object == null || object.getClass() != Order.class) {
            return false;
        }
        Order other = (Order) object;
        if (this.orderNumber == null) {
            return false;
        }
        return this.orderNumber.equals(other.orderNumber);
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((this.orderNumber == null) ? 0 : this.orderNumber.hashCode());
        return result;
    }
}</code></pre>
<p>&nbsp;</p>
<h3 id="🌱-식별자-생성">🌱 식별자 생성</h3>
<p>근데 식별자를 저렇게 항상 필드에 박아 넣어야 할까? 엔티티의 식별자를 생성하는 시점은 도메인의 특징과 사용 기술에 따라 달라지게 된다. 보통 아래 방법 중 하나를 채택하게 된다.</p>
<ul>
<li>특정 규칙에 따라 생성한다.</li>
<li>UUID나 Nano ID와 같은 고유 식별자 생성기를 사용한다.</li>
<li>값을 직접 입력한다.</li>
<li>시퀀스나 DB의 자동 증가 컬럼과 같은 일련번호를 사용한다.</li>
</ul>
<p>주문번호처럼 사용자가 직접 확인하는 식별자는 현재 시각, 일련번호, 난수 등을 조합해서 의미 있는 형식으로 만들기도 한다. 반면 내부 기술 식별자는 UUID, Nano ID, DB 시퀀스, 자동 증가 컬럼 등을 사용할 수 있다. 어떤 방식을 선택할지는 도메인의 요구사항과 사용하는 기술에 따라 달라진다. </p>
<p>추가로, 식별자를 DB가 생성하는 경우에는 객체 생성 시점에는 식별자가 없을 수 있다. 이 상태에서 <code>equals()</code>와 <code>hashCode()</code>를 식별자만으로 구현하면 주의가 필요하다. 그래서 도메인에서 중요한 식별자는 가능하면 객체 생성 시점에 함께 생성하거나, 별도의 식별자 타입으로 다루는 방법도 고려할 수 있다.</p>
<p>&nbsp;</p>
<p>이제 <code>ShippingInfo</code> 클래스를 살펴보자.</p>
<pre><code class="language-java">public class ShippingInfo {
    // 받는 사람
    private String receiverName;
    private String receiverPhoneNumber;

    // 주소
    private String shippingAddress1;
    private String shippingAddress2;
    private String shippingZipcode;

    ... 
}</code></pre>
<p>보다시피 받는 사람과 주소에 대한 데이터가 존재한다. 주석처럼 나누어 놓은 이유는 <code>receiverName</code>와 <code>receiverPhoneNumber</code>는 분명 다른 데이터지만 개념적으로는 <em>“받는 사람”</em> 을 의미하고, 밑에 <code>shippingAddress</code>와 <code>shippingZipcode</code> 필드도 개념적으로는 <em>“주소”</em> 를 의미하기 때문이다. </p>
<p>&nbsp;</p>
<p>이처럼 밸류 타입은 개념적으로 완전한 하나를 표현할 때 사용한다. 따라서 <em>“받는 사람”</em> 과 <em>“주소”</em> 를 각각 도메인 개념으로 표현하기 위해 <code>Receiver</code> 클래스와 <code>Address</code> 클래스로 나눠서 설계해줄 필요가 있다. 아래 코드를 보자.</p>
<pre><code class="language-java">// 받는 사람
public class Receiver {
    private String name;
    private String phoneNumber;

    public Receiver(String name, String phoneNumber) {
        this.name = name;
        this.phoneNumber = phoneNumber;
    }

    public String getName() {
        return name;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }
}

// 주소
public class Address {
    private String address1;
    private String address2;
    private String zipcode;

    public Address(String address1, String address2, String zipcode) {
        this.address1 = address1;
        this.address2 = address2;
        this.zipcode = zipcode;
    }

    public String getAddress1() {
        return address1;
    }

    public String getAddress2() {
        return address2;
    }

    public String getZipcode() {
        return zipcode;
    }
}</code></pre>
<p>&nbsp;</p>
<p>이제 기존의 <code>ShippingInfo</code> 클래스에서의 받는 사람에 관한 데이터를 <code>Receiver</code>, 주소에 관한 데이터는 <code>Address</code> 밸류 타입을 사용해서 보다 명확하게 표현할 수 있게 된 것이다. 밸류 타입을 도입함으로써  <code>ShippingInfo</code> 클래스는 아래와 같이 리팩토링 된다.</p>
<pre><code class="language-java">public class ShippingInfo {
    private Receiver receiver;
    private Address address;

    ... 
}</code></pre>
<p>&nbsp;</p>
<p>이처럼 밸류 타입은 의미를 명확하게 표현하기 위해 사용한다. 다음으로 <code>OrderLine</code>을 살펴보도록 하자. <code>price</code>와 <code>amount</code>는 개념적으로는 금액을 의미하기 때문에 별도로 <code>Money</code> 라는 밸류 타입을 설계해서 추가했다.</p>
<pre><code class="language-java">public class OrderLine {
    private Product product;
    private Money price;
    private int quantity;
    private Money amount;

        ... 
}</code></pre>
<p>&nbsp;</p>
<p>밸류 타입의 또 다른 장점은 해당 밸류 타입만의 기능을 추가할 수 있다는 점이다. <code>Money</code> 타입에서 금액 계산 같은 로직을 추가할 수 있다.</p>
<pre><code class="language-java">public class Money {
    private final int value;

    public Money(int value) {
        this.value = value;
    }

    ... 

    // 금액 추가 기능
    public Money add(Money money) {
        return new Money(this.value + money.value);
    }

    // 금액 곱셈 기능
    public Money multiply(int multiplier) {
        return new Money(this.value * multiplier);
    }
}</code></pre>
<p>&nbsp;</p>
<p>이제 아래의 리팩토링된 <code>OrderLine</code> 클래스를 보면 금액 계산이라는 의미가 첨가되어 코드 가독성이 향상된 것을 볼 수 있다.</p>
<pre><code class="language-java">public class OrderLine {
    private final Product product;
    private final Money price;
    private final int quantity;
    private final Money amount;

    public OrderLine(Product product, Money price, int quantity) {
        this.product = product;
        this.price = price;
        this.quantity = quantity;
        this.amount = calculateAmount();
    }

    private Money calculateAmount() {
        return price.multiply(quantity);
    }


    public Money getAmount() {
        ...
    }

    ...
}</code></pre>
<p>&nbsp;</p>
<h3 id="🤔-왜-객체를-새로-생성하지">🤔 왜 객체를 새로 생성하지?</h3>
<p>근데 <code>Money</code> 밸류 타입에서 연산을 수행한 후에 그 연산 결과를 바탕으로 새로운 인스턴스를 생성하고 있다. 금액이라는 개념은 바뀔 수 없기 때문에 그런가? 생각해보면 내가 들고 있는 만원이 값을 다르게 세팅한다고 5만원이 되지는 않는다. 아무튼 <code>Money</code> 처럼 데이터 변경 기능을 제공하지 않는 타입을 <strong>불변(Immutable)</strong>하다고 표현한다.</p>
<p>이렇게 불변으로 처리하는 이유 중 가장 중요한 것은 바로 안전한 코드를 작성하기 위함이다. 막말로 <code>setter</code>를 도입해서 기존 객체의 값을 변경했다고 치자. 이미 해당 금액이 반영된 <code>OrderLine</code>이 생성됐는데 이후에 값이 변경되면 기존 <code>OrderLine</code>에 있는 금액도 변경되는 참사가 발생할 것이다. </p>
<p>따라서 밸류 타입에는 <code>setter</code>를 두지 않는 것이 좋다. 값이 바뀌어야 한다면 기존 객체를 수정하는 대신 새로운 밸류 객체를 생성하는 방식이 안전하다. 엔티티 역시 무분별한 <code>setter</code>보다는 <code>cancel()</code>, <code>changeShippingInfo()</code>처럼 의미 있는 도메인 메서드를 통해 상태를 변경하는 것이 좋다.</p>
<p>&nbsp;</p>
<h1 id="🤪-도메인-용어와-유비쿼터스-언어">🤪 도메인 용어와 유비쿼터스 언어</h1>
<p>코드를 작성할 때 도메인에서 사용하는 용어는 아주 중요하다. 아래 주문 상태 코드를 보자.</p>
<pre><code class="language-java">public enum OrderState {
    STEP1, STEP2, STEP3, STEP4, STEP5, STEP6
}</code></pre>
<p>아니, STEP이 뭔데? 몇 단계가 어떤 상태라는거지? 상태에 대해 명확하게 알지 못 하니까 아래와 같은 의도를 알기 어려운 코드를 작성할 가능성이 높다.</p>
<pre><code class="language-java">public class Order {
    public void changeShippingInfo(ShippingInfo newShippingInfo) {
        verifyStep1OrStep2();
        setShippingInfo(newShippingInfo);
    }

    private void verifyStep1OrStep2() {
        if (state != OrderState.STEP1 &amp;&amp; state != OrderState.STEP2) {
            throw new IllegalArgumentException(&quot;Already shipped...&quot;);
        }
    }
}</code></pre>
<p>&nbsp;</p>
<p>일단 그냥 봐도 <code>verifyStep1OrStep2()</code> 메서드가 무슨 검증을 하는지 감이 안 온다. <code>STEP1</code>과 <code>STEP2</code>가 각각 <em>“결제 대기 중”</em>, <em>“상품 준비 중”</em> 상태를 의미하는 것을 알아야 한다. 따라서 아래 코드처럼 도메인 용어를 사용해서 주문 상태를 구현하면 위와 같은 불필요한 변환 과정이 필요없다. </p>
<pre><code class="language-java">public enum OrderState {
    PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED;
}</code></pre>
<p>&nbsp;</p>
<p>보다시피 각 상태가 어떤 의미인지 직관적으로 파악이 가능해서 코드를 분석하고 이해하는 시간을 대폭 줄여준다. 도메인 주도 설계에서 언어의 중요성은 몇번을 강조해도 모자르다. 해당 도메인에 관련된 모든 이해관계자들은 공통의 언어를 만들고 이를 대화, 문서, 도메인 모델, 코드, 테스트 등 모든 곳에서 그 언어를 사용한다. 시간이 지나면서 도메인에 대한 이해가 깊어지면 해당 내용을 더 잘 표현할 수 있는 용어를 찾아내서 다시 공통의 언어로 만들어 다 같이 사용하기도 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리눅스 기본 명령어와 개념 정리]]></title>
            <link>https://velog.io/@rocker_nun/%EB%A6%AC%EB%88%85%EC%8A%A4-%EA%B8%B0%EB%B3%B8-%EB%AA%85%EB%A0%B9%EC%96%B4%EC%99%80-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@rocker_nun/%EB%A6%AC%EB%88%85%EC%8A%A4-%EA%B8%B0%EB%B3%B8-%EB%AA%85%EB%A0%B9%EC%96%B4%EC%99%80-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 16 Apr 2026 02:38:14 GMT</pubDate>
            <description><![CDATA[<h1 id="🗂️-파일과-디렉터리-구조">🗂️ 파일과 디렉터리 구조</h1>
<p>리눅스를 처음 배우기 시작하면 낯선 명령어가 한꺼번에 쏟아진다. <code>ls</code>, <code>cd</code>, <code>rm</code>, <code>chmod</code>, <code>ps</code>, <code>kill</code> 같은 명령어를 보다 보면, 뭔가 외워야 할 것이 너무 많아 보여서 막막해지기도 한다. 그런데 막상 리눅스를 조금 사용하다보니, 중요한 건 명령어를 많이 아는 것보다 <strong>리눅스가 파일, 사용자, 권한, 프로세스, 네트워크를 어떤 방식으로 다루는지 흐름을 이해하는 것</strong>이라는 생각이 들었다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/023594eb-8229-4e51-b7f2-f035ed095a54/image.png" alt=""></p>
<p>리눅스를 이해할 때 가장 먼저 익혀야 하는 것은 <strong>파일과 디렉터리 구조</strong>다. 리눅스는 모든 것을 파일처럼 다루는 운영체제라고 할 만큼 파일 중심적인 성격이 강하다. 사용자는 보통 <code>/home/{사용자명}</code> 아래에 자신만의 작업 공간을 가지는데, 이 경로는 편의상 <code>~</code> 기호로 표시된다. 예를 들어 <code>/home/ubuntu</code>는 <code>~</code>로 줄여 쓸 수 있다. 그래서 터미널을 사용할 때는 먼저 <strong>내가 지금 어디에 있는지</strong>를 아는 것이 중요하다. <code>pwd</code>는 현재 위치를 확인하는 명령어이고, <code>cd</code>는 디렉터리를 이동하는 명령어다. 여기에 <code>cd ..</code>는 상위 디렉터리로, <code>cd -</code>는 이전 디렉터리로 이동하는 명령어라는 점까지 익혀두면 기본적인 이동은 거의 해결된다. 경로를 볼 때 중요한 부분은 절대 경로와 상대 경로도 함께 이해해야 한다는 것이다. 절대 경로는 <code>/home/ubuntu/app</code>처럼 루트(<code>/</code>)부터 시작하는 전체 경로이고, 상대 경로는 현재 위치를 기준으로 <code>./app</code>, <code>../app</code>처럼 표현하는 방식이다. </p>
<p>기본적인 파일 조작 명령어도 리눅스 사용의 시작점이다. 파일은 <code>touch 파일명</code>으로 만들고, 디렉터리는 <code>mkdir 디렉터리명</code>으로 생성할 수 있다. 파일 삭제는 <code>rm 파일명</code>, 비어 있는 디렉터리 삭제는 <code>rmdir 디렉터리명</code>, 내부 내용까지 함께 삭제하려면 <code>rm -r 디렉터리명</code>을 사용한다. 파일을 복사할 때는 <code>cp</code>, 이동하거나 이름을 바꿀 때는 <code>mv</code>를 사용한다. 여기서 특히 조심해야 하는 명령어가 <code>rm -rf</code>다. 처음에는 이 명령어 하나로 파일과 디렉터리를 한꺼번에 지울 수 있어서 편해 보이지만, 실제로는 매우 위험하다. 경로를 잘못 입력하면 정말 지우면 안 되는 파일까지 한 번에 삭제할 수 있기 때문이다. 그래서 <code>rm -rf</code>는 편하니까 습관처럼 쓰는 명령어가 아니라, <strong>지금 내가 무엇을 지우는지 충분히 확인한 뒤 정말 필요할 때만 쓰는 명령어</strong>라고 생각하도록 하자.</p>
<p>&nbsp;</p>
<ul>
<li>파일 생성: <code>touch [파일명]</code></li>
<li>디렉터리 생성: <code>mkdir [디렉터리명]</code></li>
<li>파일 제거: <code>rm [파일명]</code></li>
<li>디렉터리 제거: <code>rm -r [디렉터리명]</code>(디렉터리 내부 파일까지 싸그리 삭제), <code>rmdir [디렉터리명]</code>(얘는 내부에 뭔가 있으면 삭제 못함)</li>
<li>파일 복사: <code>cp [복사할 파일][복사할 위치/파일명을 뭐라고 하고 복사할건지(선택)]</code></li>
<li>디렉터리 복사: <code>cp -r [복사할디렉터리][디렉터리명을 뭐라고 하고 복사할건지]</code></li>
<li>파일/디렉터리 이동: <code>mv [이동시킬 파일/이동시킬 디렉터리][이동시킬 디렉터리]</code></li>
<li>파일명 변경: 그냥 그 파일명이 있는 경로에서 <code>mv [이름을 바꿀 파일명][뭐라고 바꿀건지]</code></li>
</ul>
<p>&nbsp;</p>
<h1 id="🖋️-vim-편집기">🖋️ Vim 편집기</h1>
<p>리눅스를 배우다 보면 자연스럽게 터미널 기반 편집기인 <code>vim</code>도 만나게 된다. 리눅스에서 IntelliJ나 VSCode 같은 GUI 편집기를 전혀 사용할 수 없는 것은 아니지만, 서버에 SSH로 접속해서 작업하는 환경에서는 보통 <code>vim</code> 같은 편집기를 많이 사용한다. <code>vi 파일명</code>으로 파일을 열 수 있고, vim은 기본적으로 명령 모드에서 시작한다. 여기서 <code>i</code>를 누르면 입력 모드로 전환되어 내용을 작성할 수 있고, <code>Esc</code>를 누르면 다시 명령 모드로 돌아온다. 저장하고 종료하려면 <code>:wq</code>, 저장하지 않고 나가려면 <code>:q!</code>를 입력하면 된다. 또 <code>gg</code>는 파일 맨 앞으로, <code>G</code>는 파일 맨 뒤로 이동하는 명령이다. 처음에는 이런 조작 방식이 낯설었지만, 막상 몇 번 써보니 생각보다 금방 익숙해졌다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/8d65f82f-830a-4a2f-bcec-6cf9f56df148/image.png" alt=""></p>
<p>편집 중에는 <code>.swp</code> 파일이 생성될 수 있는데, 이는 비정상 종료 상황에서 복구를 돕기 위한 임시 파일이다. 따라서 swap 관련 메시지가 보이더라도 에러가 났다기보다는 복구할 파일이 남아 있구나 정도로 이해하면 된다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/a6a6408f-aa43-4b33-8d41-4dc846437576/image.png" alt=""></p>
<p>실제 저 <code>app.txt</code>를 열게 되면 아래와 같이 편집 중이던 작업을 어떻게 할건지 초록색 텍스트로 물어본다. 몇 가지만 살펴보면 아래와 같다.</p>
<ul>
<li>R: 편집 중이던 내용을 복구하고 계속 작성</li>
<li>D: 편집 중이던 내용 무시하고 계속 작성</li>
<li>Q: 아무 작업도 안 하고 그냥 vim 나가기</li>
</ul>
<p>&nbsp;</p>
<h1 id="👤-권한permission">👤 권한(Permission)</h1>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/aa4333fb-d9eb-4c1e-b592-f7cb058f0488/image.png" alt=""></p>
<p>리눅스를 사용하다 보면 자주 마주치는 문구 중 하나가 <code>Permission denied</code>다. 처음에는 이 메시지만 보면 습관적으로 <code>sudo</code>부터 붙이고 싶어진다. 하지만 오히려 이 시점이 리눅스의 사용자와 권한 구조를 이해할 좋은 기회다. 리눅스에는 <strong>일반 사용자</strong>와 <strong>슈퍼 사용자(root)</strong>, 그리고 <strong>그룹</strong>이라는 개념이 존재한다. </p>
<p>일반 사용자는 허용된 범위 안에서만 파일을 다루고 명령을 실행할 수 있지만, root 사용자는 시스템 전체에 강력한 권한을 가진다. 그룹은 여러 사용자에게 공통 권한을 부여하기 위한 단위다. 현재 내가 어떤 사용자로 로그인했는지는 <code>whoami</code>로 확인할 수 있고, <code>groups 사용자명</code>이나 <code>id 사용자명</code>을 사용하면 그룹 정보까지 함께 볼 수 있다. 또한 프롬프트에서 <code>$</code>가 보이면 <em>일반 사용자</em>, <code>#</code>가 보이면 <em>root 사용자</em> 라고 이해하면 된다. 중요한 점은, 평소에는 일반 사용자로 작업하다가 정말 필요한 순간에만 <code>sudo</code>를 사용하는 습관이 훨씬 안전하다는 것이다. 무조건 <code>sudo</code>를 붙이는 방식은 당장은 편할 수 있어도, 나중에는 권한 문제의 원인을 이해하지 못한 채 넘어가게 될 수도 있다.</p>
<p>&nbsp;</p>
<p>다시 한번 정리하자.</p>
<ul>
<li><code>사용자(User)</code>: 컴퓨터에 접근하는 계정이다. 권한이 허용된 명령어만 실행시킬 수 있고, 권한이 허용된 파일만 조작할 수 있다.</li>
<li><code>슈퍼 사용자(Super user)</code>: 시스템의 모든 권한을 가진 계정을 말한다. 모든 명령어를 실행할 수 있고, 모든 파일을 조작할 수 있다. 일반적으로 root 계정이 슈퍼 사용자로 설정되어 있다.</li>
<li><code>그룹(Group)</code>: 사용자 계정을 묶어서 관리하기 위한 단위이다. 여러 사용자에게 공통된 권한을 한 번에 부여하고 관리할 때 유용하게 사용된다. 한 사용자는 무조건 하나의 그룹에 속해야 하고, 한 사용자는 여러 그룹에 속할 수 있다.</li>
</ul>
<p>&nbsp;</p>
<p>권한 구조를 조금 더 자세히 보려면 <code>ls -l</code> 명령어도 알아야 한다. 이 명령어를 실행하면 위의 이미지와 같이 좌측에 <code>-rw-r--r--</code> 같은 문자열이 보이는데…</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/10c51d42-661b-4299-bccf-4c194423c04c/image.png" alt=""></p>
<p>맨 앞의 <code>-</code>는 일반 파일이라는 뜻이고, <code>d</code>가 나오면 디렉터리를 의미한다. 그 뒤의 아홉 글자는 소유자, 그룹, 기타 사용자 권한을 세 글자씩 나눠서 보여준다. 여기서 <code>r</code>은 읽기, <code>w</code>는 쓰기, <code>x</code>는 실행 권한이다. 권한은 숫자로도 표현할 수 있는데, <code>r=4</code>, <code>w=2</code>, <code>x=1</code>로 계산해서 <code>chmod 755 파일명</code>처럼 설정할 수 있다. 예를 들어 <code>755</code>는 소유자에게는 읽기/쓰기/실행을 모두 허용하고, 그룹과 기타 사용자에게는 읽기와 실행만 허용한다는 뜻이다. </p>
<p>권한뿐 아니라 소유자도 중요하다. 파일 소유자를 변경할 때는 <code>sudo chown 사용자명:그룹명 파일명</code>을 사용한다. 결국 권한 문제는 단순히 실행 가능 유무의 문제가 아니라, <strong>누가 그 파일의 주인이고, 누가 어디까지 접근할 수 있느냐</strong>의 문제라고 보는 것이 더 정확하다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/9c14b87c-5bb8-4236-95f4-cb8b92429caf/image.png" alt=""></p>
<p>참고로 <code>/etc/passwd</code> 파일을 까보면 기본적으로 생성되어 있는 사용자들의 목록(root, daemon, bin, sys 등등)을 확인할 수 있다. 일단 지금은 <strong>root(슈퍼 사용자)</strong>와 <strong>ubuntu(일반 사용자)</strong>만 기억해두도록 하자.</p>
<p>&nbsp;</p>
<h1 id="🤖-패키지-매니저package-manager">🤖 패키지 매니저(Package Manager)</h1>
<p>Ubuntu 환경에서 프로그램을 설치하거나 삭제할 때는 보통 <code>apt</code>를 사용한다. 자바 진영에서 Maven이나 Gradle을 쓰고, Node.js에서 npm을 쓰듯이, 리눅스에는 패키지 매니저가 있는 셈이다. </p>
<p><code>sudo apt update</code>는 설치 가능한 패키지 목록 정보를 최신화하는 명령어이고, <code>sudo apt upgrade</code>는 현재 설치된 패키지들을 실제로 업그레이드한다. 새로운 프로그램을 설치할 때는 <code>sudo apt install 패키지명</code>, 설치된 패키지 목록을 보고 싶을 때는 <code>sudo apt list --installed</code>, 특정 패키지가 설치되어 있는지 찾고 싶을 때는 <code>sudo apt list --installed | grep 패키지명</code>을 사용할 수 있다. 삭제할 때는 <code>sudo apt purge --auto-remove 패키지명</code>처럼 관련 설정까지 함께 정리하는 방식도 자주 쓴다. </p>
<p>여기서 왜 <code>sudo</code>가 필요한지를 생각해보면, 설치와 삭제 과정이 결국 시스템 디렉터리와 root 소유 파일을 건드리는 작업이기 때문이다. 예를 들어, 그냥 <code>apt install nginx</code>로 실행해보면 <code>/var/lib/dpkg/lock-frontend</code> 폴더에 접근 권한이 없다는 이유로 설치가 안 된다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/4e3b666a-72c6-4e4a-b680-ff2288ee50a1/image.png" alt=""></p>
<p>위와 같이 실제 들어가보면 <code>lock-frontend</code> 파일의 소유자가 root, 소유 그룹도 root다. root이외의 사용자한테는 아무 권한도 없는 것을 확인할 수 있다. 그래서 <code>sudo</code>를 붙이지 않으면 에러가 발생한 것이다. 이런 맥락까지 함께 이해하고 나면, <code>apt</code> 명령어가 단순한 설치 도구가 아니라 시스템 수준의 변경을 수행하는 관리자 도구라는 것을 알 수 있다.</p>
<p>&nbsp;</p>
<ul>
<li>패키지 설치 명령어: <code>sudo apt install [패키지명]</code></li>
<li>패키지 목록 최신화: <code>sudo apt update</code></li>
<li>현재 컴퓨터에 설치된 모든 패키지 목록 출력: <code>sudo apt list —installed</code></li>
<li>현재 컴퓨터에 설치된 특정 패키지 확인: <code>sudo apt list —installed | grep [패키지명]</code></li>
<li>설치된 패키지에 관련된 모든 파일을 삭제: <code>sudo apt purge —auto-remove [패키지명]</code></li>
</ul>
<p>&nbsp;</p>
<h1 id="🖨️-표준-출력stdout--표준-에러stderr">🖨️ 표준 출력(stdout) &amp; 표준 에러(stderr)</h1>
<p>터미널을 조금 더 잘 이해하려면 <strong>표준 출력</strong>과 <strong>표준 에러</strong> 개념도 꼭 알아야 한다. 명령어를 실행했을 때 결과가 그냥 화면에 찍히는 것처럼 느껴지지만, 실제로는 결과가 먼저 <strong>표준 출력(stdout)</strong> 이라는 통로로 전달되고, 그 통로의 기본 목적지가 터미널 화면으로 연결되어 있는 것이다. 반면 에러 메시지는 <strong>표준 에러(stderr)</strong> 라는 별도 통로를 사용한다. 이 개념을 이해하면 <code>&gt;</code>, <code>&gt;&gt;</code>, <code>2&gt;</code>, <code>2&gt;&gt;</code> 같은 기호들이 훨씬 자연스럽게 받아들여진다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/79f34b7d-40d6-4c76-9ae0-59cafa58ca53/image.png" alt=""></p>
<p>예를 들어 <code>명령어 &gt; 파일</code>은 표준 출력을 파일에 덮어쓰는 것이고, <code>명령어 2&gt; 파일</code>은 에러 메시지를 파일에 저장하는 것이다. 즉, 리눅스에서는 출력 결과와 에러 메시지를 서로 다른 곳으로 보낼 수 있다. 처음에는 단순한 기호처럼 보이지만, 실제로 로그를 남기고 에러를 분리해서 확인할 때 이 기능은 생각보다 강력하다.</p>
<p>&nbsp;</p>
<ul>
<li>기존 파일에 출력 결과 덮어쓰기: <code>명령어 &gt; [그 결괏값을 저장할 파일]</code></li>
<li>기존 파일에 출력 결과 이어서 쓰기: <code>명령어 &gt;&gt; [그 결괏값을 저장할 파일]</code></li>
<li>기존 파일에 에러 출력 덮어쓰기: <code>명령어 2&gt; [그 결괏값을 저장할 파일]</code></li>
<li>기존 파일에 에러 출력 이어서 쓰기: <code>명령어 2&gt;&gt; [그 결괏값을 저장할 파일]</code></li>
</ul>
<p>&nbsp;</p>
<h1 id="🔍-특정-키워드가-들어간-문장-찾기grep">🔍 특정 키워드가 들어간 문장 찾기(grep)</h1>
<p>특정 키워드가 포함된 줄만 찾고 싶을 때는 <code>grep</code>이 아주 유용하다. 예를 들어 <code>ls -al | grep media</code>는 <code>ls -al</code>의 결과 중에서 <code>media</code>가 포함된 줄만 보여준다. <code>cat README | grep you</code>처럼 다른 명령어의 출력 결과와 함께 사용할 수도 있고, <code>grep you README</code>처럼 파일을 직접 검색할 수도 있다. </p>
<p><code>grep -n</code>은 줄 번호까지 같이 보여주고, <code>grep -i</code>는 대소문자를 구분하지 않고 검색한다. grep은 단독으로도 자주 쓰이지만, 파이프(<code>|</code>)와 함께 사용할 때 진짜 빛을 발한다. 예를 들어 <code>ps aux | grep java</code>처럼 실행 중인 프로세스 목록에서 특정 프로그램만 추려볼 수 있기 때문이다. 리눅스에서 결과가 너무 많아서 원하는 정보만 보고 싶은 상황이 오면, <code>grep</code>은 거의 항상 떠올려야 하는 도구다.</p>
<p>&nbsp;</p>
<h1 id="❤️🔥-실행-중인-프로세스-조회--종료">❤️‍🔥 실행 중인 프로세스 조회 &amp; 종료</h1>
<p>프로세스는 현재 실행 중인 프로그램을 의미한다. 윈도우의 작업 관리자처럼 생각하면 이해하기 쉽다. <code>ps aux</code>를 실행하면 현재 돌아가고 있는 프로세스 목록을 볼 수 있는데…</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/e58f5377-cba2-4768-bef7-cee23bfc2d67/image.png" alt=""></p>
<p>여기서 <code>USER</code>는 누가 실행했는지, <code>PID</code>는 프로세스 ID, <code>%CPU</code>와 <code>%MEM</code>은 자원 사용량, <code>COMMAND</code>는 어떤 명령으로 실행되었는지를 보여준다. 프로세스를 종료하려면 <code>kill [PID]</code>를 사용하고, 정상 종료가 되지 않을 때는 <code>kill -9 [PID]</code>로 강제 종료할 수 있다. 다만 <code>kill -9</code>는 프로그램이 스스로 정리할 기회도 주지 않고 바로 종료시키는 방식이므로, 가능하면 일반 <code>kill</code>을 먼저 시도하고 정말 필요할 때만 사용해야 한다. 리눅스에서는 단순히 프로그램을 실행하는 것만큼이나, <strong>지금 무엇이 돌아가고 있는지 확인하고 필요한 것을 종료하는 능력</strong>도 중요하다.</p>
<p>실행 중인 프로그램을 다룰 때는 <strong>포그라운드(Foreground)</strong>와 <strong>백그라운드(Background)</strong>의 차이도 꼭 이해해야 한다. 포그라운드는 프로그램이 현재 터미널을 점유한 상태이고, 백그라운드는 화면을 점유하지 않고 뒤에서 계속 실행되는 상태다. 예를 들어 스프링 부트 서버를 포그라운드로 실행하면 그 터미널은 사실상 서버 로그를 보는 창이 되어버려서 다른 명령을 입력하기 어렵다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/89cf6b8f-052b-4d66-9a40-be6800bd3309/image.png" alt=""></p>
<p><del>명령어를 입력해도 반응이 없다…</del></p>
<p>이럴 때 <code>nohup 명령어 &amp;</code> 형태로 실행하면 프로그램을 백그라운드로 돌릴 수 있다. <code>nohup</code>은 터미널이 닫혀도 프로그램이 계속 실행되게 해주고, 기본적으로 출력은 <code>nohup.out</code> 파일에 저장된다. 조금 더 실무적으로는 <code>nohup java -jar app.jar &gt; app.log 2&gt;&amp;1 &amp;</code>처럼 로그 파일을 직접 지정해서 표준 출력과 에러를 함께 저장하는 방식이 더 자주 쓰인다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/c4b64370-2153-49fe-b7ce-7642afe2d989/image.png" alt=""></p>
<p>이후 <code>tail -f app.log</code>로 로그를 실시간으로 확인하면 서버 상태를 훨씬 편하게 추적할 수 있다. 서버를 띄워놓고 다른 작업을 해야 하는 상황에서는 이 흐름을 기억하도록 하자.</p>
<p>&nbsp;</p>
<ul>
<li>실행 중인 모든 프로세스 조회: <code>ps aux</code></li>
<li>실행 중인 프로세스 종료: <code>kill [PID 값]</code></li>
<li>실행 중인 프로세스 강제 종료: <code>kill -9 [PID 값]</code></li>
<li>백그라운드에서 실행: <code>nohup + 명령어 + &amp;</code></li>
</ul>
<p>&nbsp;</p>
<h1 id="📲-특정-포트에서-실행되고-있는-프로세스-조회">📲 특정 포트에서 실행되고 있는 프로세스 조회</h1>
<p>네트워크와 관련해서는 포트와 IP 주소도 기본 개념으로 꼭 정리해둘 필요가 있다. 포트는 한 컴퓨터 안에서 특정 프로그램이 네트워크 통신을 위해 사용하는 번호라고 생각하면 된다. 그래서 같은 서버에서 같은 포트를 두 프로그램이 동시에 사용하려 하면 충돌이 난다. </p>
<p>예를 들어 이미 8080번 포트에서 스프링 부트가 실행 중인데, 또 다른 프로그램이 8080번 포트를 쓰려고 하면 에러가 발생한다. 이럴 때는 <code>sudo lsof -i:8080</code> 또는 <code>sudo ss -ltnp | grep 8080</code>을 사용해서 어떤 프로세스가 그 포트를 점유하고 있는지 확인할 수 있다. 보통은 결과에서 <code>COMMAND</code>와 <code>PID</code>를 중심으로 보면 된다. IP 주소는 네트워크에서 특정 컴퓨터를 식별하기 위한 주소인데, <strong>외부 인터넷에서 보이는 주소는 Public IP, 내부 네트워크 안에서만 쓰이는 주소는 Private IP</strong>라고 한다. Public IP는 <code>curl ifconfig.me</code>, Private IP는 <code>ip a</code> 또는 <code>hostname -I</code>로 확인할 수 있다. </p>
<p>&nbsp;</p>
<ul>
<li>특정 포트 번호에 실행되고 있는 프로세스 조회: <code>sudo lsof -i:[포트번호]</code></li>
<li>Public IP 주소 확인 명령어: <code>curl [ifconfig.me](http://ifconfig.me/)</code></li>
<li>Private IP 주소 확인 명령어: <code>ip a</code></li>
</ul>
<p>&nbsp;</p>
<h1 id="📝-쉘-스크립트-파일-작성--실행">📝 쉘 스크립트 파일 작성 &amp; 실행</h1>
<p>마지막으로 쉘 스크립트는 반복 작업을 자동화할 수 있게 해주는 아주 강력한 도구다. 여러 리눅스 명령어를 순서대로 실행해야 할 때, 매번 직접 입력하는 대신 <code>.sh</code> 파일에 적어두고 한 번에 실행할 수 있다. </p>
<p>보통 파일 맨 위에는 <code>#!/bin/bash</code>를 적어서 이 스크립트를 어떤 쉘로 실행할지 명시한다. 그 아래에는 <code>echo</code>, <code>pwd</code>, <code>ls -al</code> 같은 작업을 자동적으로 수행할 명령어를 순서대로 작성하면 된다. 실행할 때는 <code>chmod +x script.sh</code>로 실행 권한을 부여한 뒤 <code>./script.sh</code>로 실행할 수 있고, 또는 <code>bash script.sh</code>처럼 직접 bash로 실행할 수도 있다. 처음에는 단순히 명령어를 모아둔 파일처럼 보이지만, 실제로는 반복 작업, 배포, 로그 정리, 서버 실행 같은 복잡한 작업을 자동화하는 출발점이 된다.</p>
<p>아래는 스프링 부트 서버 실행과 종료, 서버 재시작을 자동화 처리하는 예시 쉘 스크립트 파일이다.</p>
<pre><code class="language-bash">#!/bin/bash

APP_NAME=&quot;my-spring-app&quot;
APP_HOME=&quot;/home/ubuntu/app&quot;
JAR_FILE=&quot;$APP_HOME/app.jar&quot;
LOG_DIR=&quot;$APP_HOME/logs&quot;
LOG_FILE=&quot;$LOG_DIR/app.log&quot;
PID_FILE=&quot;$APP_HOME/app.pid&quot;

echo &quot;[$APP_NAME] 시작합니다.&quot;

# JAR 파일 존재 확인
if [ ! -f &quot;$JAR_FILE&quot; ]; then
  echo &quot;[$APP_NAME] JAR 파일이 없습니다: $JAR_FILE&quot;
  exit 1
fi

# 로그 디렉터리 생성
mkdir -p &quot;$LOG_DIR&quot;

# 이미 실행 중인지 확인
if [ -f &quot;$PID_FILE&quot; ]; then
  PID=$(cat &quot;$PID_FILE&quot;)
  if ps -p &quot;$PID&quot; &gt; /dev/null 2&gt;&amp;1; then
    echo &quot;[$APP_NAME] 이미 실행 중입니다. PID=$PID&quot;
    exit 1
  else
    echo &quot;[$APP_NAME] 오래된 PID 파일을 삭제합니다.&quot;
    rm -f &quot;$PID_FILE&quot;
  fi
fi

# 애플리케이션 실행
nohup java -jar &quot;$JAR_FILE&quot; &gt; &quot;$LOG_FILE&quot; 2&gt;&amp;1 &amp;

PID=$!
echo &quot;$PID&quot; &gt; &quot;$PID_FILE&quot;

sleep 2

if ps -p &quot;$PID&quot; &gt; /dev/null 2&gt;&amp;1; then
  echo &quot;[$APP_NAME] 실행 성공&quot;
  echo &quot;[$APP_NAME] PID=$PID&quot;
  echo &quot;[$APP_NAME] LOG=$LOG_FILE&quot;
else
  echo &quot;[$APP_NAME] 실행 실패&quot;
  rm -f &quot;$PID_FILE&quot;
  exit 1
fi</code></pre>
<pre><code class="language-bash">#!/bin/bash

APP_NAME=&quot;my-spring-app&quot;
APP_HOME=&quot;/home/ubuntu/app&quot;
PID_FILE=&quot;$APP_HOME/app.pid&quot;

echo &quot;[$APP_NAME] 종료합니다.&quot;

if [ ! -f &quot;$PID_FILE&quot; ]; then
  echo &quot;[$APP_NAME] PID 파일이 없습니다. 이미 종료되었을 수 있습니다.&quot;
  exit 1
fi

PID=$(cat &quot;$PID_FILE&quot;)

if ps -p &quot;$PID&quot; &gt; /dev/null 2&gt;&amp;1; then
  kill &quot;$PID&quot;
  echo &quot;[$APP_NAME] 종료 요청을 보냈습니다. PID=$PID&quot;

  # 최대 10초 대기
  for i in {1..10}
  do
    if ps -p &quot;$PID&quot; &gt; /dev/null 2&gt;&amp;1; then
      sleep 1
    else
      break
    fi
  done

  if ps -p &quot;$PID&quot; &gt; /dev/null 2&gt;&amp;1; then
    echo &quot;[$APP_NAME] 정상 종료되지 않아 강제 종료합니다. PID=$PID&quot;
    kill -9 &quot;$PID&quot;
  fi

  rm -f &quot;$PID_FILE&quot;
  echo &quot;[$APP_NAME] 종료 완료&quot;
else
  echo &quot;[$APP_NAME] 해당 PID의 프로세스가 없습니다. PID 파일만 삭제합니다.&quot;
  rm -f &quot;$PID_FILE&quot;
fi</code></pre>
<pre><code class="language-bash">#!/bin/bash

APP_NAME=&quot;my-spring-app&quot;
APP_HOME=&quot;/home/ubuntu/app&quot;

echo &quot;[$APP_NAME] 재시작합니다.&quot;

&quot;$APP_HOME/stop.sh&quot;
sleep 2
&quot;$APP_HOME/start.sh&quot;</code></pre>
<hr>
<p><em><strong>&lt;참고 자료&gt;</strong></em>
<a href="https://www.inflearn.com/course/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90%EB%8F%84-%EC%9D%B4%ED%95%B4%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EB%A6%AC%EB%88%85%EC%8A%A4-%EC%9E%85/dashboard?cid=337817">비전공자도 이해할 수 있는 리눅스 입문/실전</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vue3 템플릿]]></title>
            <link>https://velog.io/@rocker_nun/Vue3-%ED%85%9C%ED%94%8C%EB%A6%BF</link>
            <guid>https://velog.io/@rocker_nun/Vue3-%ED%85%9C%ED%94%8C%EB%A6%BF</guid>
            <pubDate>Wed, 25 Mar 2026 02:44:04 GMT</pubDate>
            <description><![CDATA[<p>Vue에서의 템플릿은 Vue로 화면을 조작하는 방법을 말한다. <strong>템플릿 문법은 크게 데이터 바인딩과 디렉티브로 나뉜다.</strong> 하나씩 알아보도록 하자.</p>
<h1 id="🔗-data-binding">🔗 Data Binding</h1>
<p><strong>데이터 바인딩(Data Binding)은 Vue 인스턴스에서 정의한 속성들을 화면에 표시하는 방법</strong>이다. 보통 머시태시(<code>{{}}</code>) 문법을 사용한다.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;title&gt;Document&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id=&quot;app&quot;&gt;{{ message }}&lt;/div&gt;
    &lt;script src=&quot;https://unpkg.com/vue&quot;&gt;&lt;/script&gt;
    &lt;script&gt;
      let vm = Vue.createApp({
        name: &quot;App&quot;,
        data() {
          return {
            message: &quot;Hello Vue3!&quot;,
          };
        },
      }).mount(&quot;#app&quot;);
    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>예제 코드를 보면 <code>&lt;div&gt;</code> 안에 머스태시 괄호를 이용해서 Vue 인스턴스의 <code>message</code> 속성을 연결한 것을 볼 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/ceb27948-435d-4c01-a6ab-4cc1346cc906/image.png" alt=""></p>
<p>&nbsp;</p>
<h2 id="🛎️-vue-인스턴스">🛎️ Vue 인스턴스?</h2>
<p><strong>인스턴스는 Vue로 개발할 때 필수로 생성해야 하는 코드</strong>를 말한다. 위의 예제 코드에서 <code>Vue.createApp()</code>을 이용해서 인스턴스를 생성할 수 있다. 인스턴스 안에는 미리 정의되어 있는 속성과 메서드(API)들이 있기 때문에 해당 기능들을 이용해서 빠르게 화면을 개발할 수 있다. 인스턴스에서 사용할 수 있는 주요 속성들에 대해 간단히 말하자면…</p>
<pre><code class="language-jsx">Vue.createApp({
    template: ,  // 화면에 렌더링되는 녀석들
    data: ,  // 반응형 데이터 속성
    methods: ,  // 이벤트 로직을 제어하는 메서드
    created: ,  // 뷰의 라이프사이클에서의 부가 작업
    watch: ,  // data에서 정의한 속성에 변화가 일어났을 때 추가 동작을 수행할 수 있게 정의하는 속성
});</code></pre>
<p>참고로 Vue2에서는 <code>el</code>이라는 속성이 있었지만, Vue3로 넘어오면서 해당 속성을 사용하지 않고 <code>mount()</code> API만 사용한다.</p>
<p>&nbsp;</p>
<h1 id="🪝-directives">🪝 Directives</h1>
<p><strong>디렉티브(Directives)는 Vue로 화면의 요소를 더 쉽게 조작할 수 있게 하는 문법</strong>이다. <code>v-</code> 접두사가 붙은 특수 속성으로 직역하면 말 그대로 <em>“지시”</em> 를 뜻한다. 쉽게 말해, 디렉티브는 컴포넌트나 DOM 요소에게 <em>“~ 하게 작동할 것”</em> 을 지시해주는 지시문인 셈이다. 디렉티브는 아래와 같이 구성되어 있다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/7dc345c8-ed49-4d5c-9407-586570906b09/image.png" alt=""></p>
<p>어떤 디렉티브들이 있는지 직접 보면서 알아보도록 하자.</p>
<p>&nbsp;</p>
<h2 id="💬-v-text--v-html-디렉티브">💬 v-text &amp; v-html 디렉티브</h2>
<p>둘 다 텍스트를 출력하는 디렉티브지만, 둘의 차이는 <strong>문자열을</strong> <em>“그대로 텍스트로 넣느냐”, HTML로 해석해서 넣느냐”</em> 다. </p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;title&gt;Document&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id=&quot;app&quot;&gt;
      &lt;span v-text=&quot;message&quot;&gt;&lt;/span&gt;
      &lt;span v-html=&quot;message&quot;&gt;&lt;/span&gt;
      &lt;span&gt;{{ message }}&lt;/span&gt;
    &lt;/div&gt;
    &lt;script src=&quot;https://unpkg.com/vue&quot;&gt;&lt;/script&gt;
    &lt;script&gt;
      let vm = Vue.createApp({
        name: &quot;App&quot;,
        data() {
          return {
            message: &quot;&lt;b&gt;Hello Vue3!&lt;/b&gt;&quot;,
          };
        },
      }).mount(&quot;#app&quot;);
    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/3d8a4cff-06ac-4aa1-bb48-2a1551c04fd9/image.png" alt=""></p>
<p>보다시피 <code>v-text</code>는 텍스트를 그대로 뱉어내고, <code>v-html</code>은 HTML 요소는 HTML로 해석해서 뱉어낸다. 보통 <code>v-text</code> 디렉티브를 사용할 것을 권장한다. 왜냐하면 사용자가 입력한 값을 그대로 <code>v-html</code>에 넣으면 악성 스크립트가 섞일 수도 있기 때문이다. 이걸 보통 XSS(Cross-Site Scripting) 문제라고도 한다.</p>
<p>&nbsp;</p>
<h2 id="⛓️-v-bind">⛓️ v-bind</h2>
<p>이 디렉티브는 Vue 데이터와 HTML 속성을 연결해주는 디렉티브다. 바인딩은 말 그대로 <em>“연결한다”</em> 는 의미인데, Vue에서의 바인딩은 보통 <strong>data에 있는 값</strong>과 <strong>화면(HTML 요소)</strong>을 연결하는 것을 말한다.</p>
<pre><code class="language-html">&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot;&gt;
    &lt;head&gt;
        &lt;meta charset=&quot;UTF-8&quot; /&gt;
        &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
        &lt;title&gt;Document&lt;/title&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;div id=&quot;app&quot;&gt;
            &lt;input type=&quot;text&quot; v-bind:value=&quot;message&quot; /&gt;
            &lt;br /&gt;
            &lt;img v-bind:src=&quot;imagePath&quot; /&gt;
        &lt;/div&gt;
        &lt;script src=&quot;https://unpkg.com/vue&quot;&gt;&lt;/script&gt;
        &lt;script&gt;
            let viewModel = Vue.createApp({
                name: &#39;App&#39;,
                data() {
                    return {
                        message: &#39;v-bind 디렉티브&#39;,
                        imagePath:
                            &#39;https://img.freepik.com/free-photo/shot-tiger-laying-ground-while-watching-his-territory_181624-44049.jpg&#39;,
                    };
                },
            }).mount(&#39;#app&#39;);
        &lt;/script&gt;
    &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>일반적으로 HTML은 정적인 문서지만, 동적인 UI를 만들고 싶어서 Vue를 사용하는 것이다. 그래서 속성값도 고정된 값이 아니라 데이터의 값에 따라 달라지길 원하기 때문에 <code>v-bind</code>가 필요한 것이다. 여기서 짚고 넘어가야 할 점은 <code>v-bind</code> 디렉티브는 <strong>단방향으로만 데이터를 바인딩</strong>한다는 점이다. 즉, Vue 인스턴스의 데이터나 속성이 바뀌면 UI를 갱신한다는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/9e28ea1c-592c-4159-abcc-f0de635fce01/image.png" alt=""></p>
<p>렌더링된 화면을 보면 알 수 있듯이 화면에 바인딩된 요소에서 값을 변경하더라도 데이터가 바뀌지 않는 것을 볼 수 있다. 콘솔에서 <code>viewModel.message=&quot;단방향임...&quot;</code> 으로 속성에 직접 값을 할당해서 데이터를 변경해야 UI도 변경된다.</p>
<p>&nbsp;</p>
<h2 id="↔️-v-model">↔️ v-model</h2>
<p>앞서 살펴봤던 디렉티브들과는 다르게 <code>v-model</code>은 폼 입력 요소의 값과 Vue 데이터를 양방향으로 연결하는 디렉티브다. 쉽게 말해 데이터가 바뀌면 입력창 화면도 바뀌고, 사용자가 입력창 값을 바꾸면 데이터도 같이 바뀐다는 것이다.</p>
<pre><code class="language-html">&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot;&gt;
    &lt;head&gt;
        &lt;meta charset=&quot;UTF-8&quot; /&gt;
        &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
        &lt;title&gt;Document&lt;/title&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;div id=&quot;app&quot;&gt;
            &lt;input type=&quot;text&quot; v-model=&quot;name&quot; /&gt;
            &lt;br /&gt;
            입력한 값: &lt;span&gt;{{name}}&lt;/span&gt;
        &lt;/div&gt;
        &lt;script src=&quot;https://unpkg.com/vue&quot;&gt;&lt;/script&gt;
        &lt;script&gt;
            let viewModel = Vue.createApp({
                name: &#39;App&#39;,
                data() {
                    return {
                        name: &#39;&#39;,
                    };
                },
            }).mount(&#39;#app&#39;);
        &lt;/script&gt;
    &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/81936a57-3bc8-4489-8eb6-3248bc54e7d8/image.gif" alt=""></p>
<p>그리고 <code>checkbox</code>와 <code>select</code> 요소에서의 <code>v-model</code>은 다중 선택인지 단일 선택을 하는지에 따라 양방향 데이터 바인딩으로 값을 받아내기 위한 데이터의 형식이 달라진다. 다중 선택은 배열을 사용해야 하고, 단일 선택은 문자열로 값을 받아내야 한다. 그리고 <code>checkbox</code>, <code>radio</code>와 같이 <code>input</code> 요소를 사용하는 경우는 각각의 <code>input</code> 요소마다 <code>v-model</code> 디렉티브를 적용해야 하지만, <code>select</code>와 같이 값을 선택하는 요소를 감싸는 부모 요소가 있다면 부모 요소인 <code>select</code>에 <code>v-model</code> 디렉티브를 한 번만 적용해야 한다는 점도 알아두도록 하자.</p>
<p>&nbsp;</p>
<h2 id="↪️-v-if--v-for">↪️ v-if &amp; v-for</h2>
<p><code>v-if</code> 디렉티브는 느낌 그대로 화면 렌더링에서의 <code>if문</code>이라고 생각하면 된다. 조건이 참이면 화면에 렌더링되고, 거짓이면 아예 렌더링되지 않는다. 그냥 조건이 거짓이면 DOM에 아예 만들지 않는다는 점만 기억해두도록 하자.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;title&gt;Document&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id=&quot;app&quot;&gt;
      잔고: &lt;input type=&quot;text&quot; v-model=&quot;deposit&quot; /&gt;
      &lt;br /&gt;
      회원 등급:
      &lt;span v-if=&quot;deposit &gt; 1000000&quot;&gt;Gold&lt;/span&gt;
      &lt;span v-else-if=&quot;deposit &gt;= 500000&quot;&gt;Silver&lt;/span&gt;
      &lt;span v-else-if=&quot;deposit &gt;= 200000&quot;&gt;Bronze&lt;/span&gt;
      &lt;span v-else=&quot;deposit&quot;&gt;Basic&lt;/span&gt;
    &lt;/div&gt;
    &lt;script src=&quot;https://unpkg.com/vue&quot;&gt;&lt;/script&gt;
    &lt;script&gt;
      let viewModel = Vue.createApp({
        name: &quot;App&quot;,
        data() {
          return {
            deposit: 0,
          };
        },
      }).mount(&quot;#app&quot;);
    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>&nbsp;</p>
<p><code>v-for</code> 디렉티브도 느낌 그대로다. 배열이나 객체 데이터를 반복해서 화면에 출력하는 디렉티브다.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;title&gt;Document&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id=&quot;app&quot;&gt;
      &lt;ul&gt;
        &lt;li v-for=&quot;item in items&quot;&gt;{{item}}&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
    &lt;script src=&quot;https://unpkg.com/vue&quot;&gt;&lt;/script&gt;
    &lt;script&gt;
      let vm = Vue.createApp({
        name: &quot;App&quot;,
        data() {
          return {
            items: [&quot;맥북 프로&quot;, &quot;아이패드 프로&quot;, &quot;에어팟 프로&quot;],
          };
        },
      }).mount(&quot;#app&quot;);
    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p><code>v-for=&quot;(item, index) in items&quot;</code>처럼 인덱스 정보도 추가로 뿌려줄 수 있고 객체도 각 속성들을 뽑아서 뿌려줄 수도 있다. 그리고 나중에 요소가 많아지는 상황에서 반복 렌더링할 때는 보통 <code>:key</code>를 같이 사용한다. 아래 예제 코드를 살펴보고 넘어가도록 하자.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;title&gt;Document&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id=&quot;app&quot;&gt;
      &lt;ul&gt;
        &lt;li v-for=&quot;item in items&quot; :key=&quot;item.id&quot;&gt;{{ item.text }}&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
    &lt;script src=&quot;https://unpkg.com/vue&quot;&gt;&lt;/script&gt;
    &lt;script&gt;
      let vm = Vue.createApp({
        name: &quot;App&quot;,
        data() {
          return {
            items: [
              {
                id: 1,
                text: &quot;맥북 프로&quot;,
              },
              {
                id: 2,
                text: &quot;아이패드 프로&quot;,
              },
              {
                id: 3,
                text: &quot;에어팟 프로&quot;,
              },
            ],
          };
        },
      }).mount(&quot;#app&quot;);
    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Node에서의 비동기 처리 방식]]></title>
            <link>https://velog.io/@rocker_nun/Node%EC%97%90%EC%84%9C%EC%9D%98-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@rocker_nun/Node%EC%97%90%EC%84%9C%EC%9D%98-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Fri, 13 Mar 2026 23:54:24 GMT</pubDate>
            <description><![CDATA[<h1 id="🤔-자바스크립트는-싱글-스레드인데-어떻게-비동기-처리를">🤔 자바스크립트는 싱글 스레드인데 어떻게 비동기 처리를?</h1>
<p>자바스크립트는 싱글 스레드 기반으로, 한번에 하나의 명령만 수행 가능하지만 여러 라이브러리를 통해 비동기 방식으로 여러 가지 일을 처리할 수 있다. 자바스크립트를 비동기 방식으로 처리하는 방법은 크게 3가지가 있다.</p>
<ul>
<li><code>콜백(Callback)</code>: 요청이 끝난 후 실행할 함수를 매개변수로 추가하는 방식</li>
<li><code>프로미스(Promise)</code>: Promise 객체를 반환하는 방식</li>
<li><code>Async/Await</code>: Promise를 간단하게 사용하도록 변경한 방식</li>
</ul>
<p>&nbsp;</p>
<h2 id="↪️-callback-함수-사용">↪️ Callback 함수 사용</h2>
<p><strong>콜백 함수는 내가 원하는 순서로 비동기 코드를 실행할 수 있게 하는 함수</strong>를 말한다. 함수를 인자로 넘기고 작업 완료 시 실행한다(함수의 인자로 함수가 넘어감). 커피숍에서 점원에게 주문 후에 완료되면 점원이 손님을 호출하는 상황을 떠올려보면 이해가 편하다.</p>
<p>아래 회원 가입을 하는 과정을 예로 들어보자. 먼저 회원 가입 API가 호출되면 DB에 사용자 정보를 저장하고, 회원 가입 성공 시 사용자에게 이메일 전송 후, 가입 성공 메시지를 출력해주는 것이다. </p>
<pre><code class="language-jsx">const database = []; // 회원 DB

// 가입 요청한 회원의 정보를 저장하는 API 작성
function saveDb(user, callback) {
  database.push(user);
  console.log(`Save ${user.name} in Database...`);
  return callback(user);
}

// 회원에게 이메일 전송하는 API 작성
function sendEmail(user, callback) {
  console.log(`Send Email to ${user.email}`);
  return callback(user);
}

// 회원에게 가입 성공 메시지를 출력하는 API 작성
function getResult(user) {
  return `${user.name} is successfully registered...`;
}

// 콜백 함수로 API 작성
function register(user) {
  return saveDb(user, function (user) {
    return sendEmail(user, function (user) {
      return getResult(user);
    });
  });
}

const result = register({
  email: &#39;byeonguk@gmail.com&#39;,
  password: &#39;1234&#39;,
  name: &#39;ByeongUk&#39;,
});

console.log(result);
// Save ByeongUk in Database...
// Send Email to byeonguk@gmail.com
// ByeongUk is successfully registered...

// register() 함수는 순서대로 아래 절차대로 실행된다.
// 1. saveDb()
// 2. sendEmail()
// 3. getResult()
</code></pre>
<p>보다시피 콜백은 가장 기본적인 비동기 처리 방식이지만, 작업이 많아질수록 함수 안에 함수가 계속 중첩되어 코드가 복잡해진다. 이런 형태를 흔히 <strong>콜백 지옥(Callback Hell)</strong>이라고 부른다. 그래서 가독성과 에러 처리를 개선하기 위해 Promise가 등장했다.</p>
<p>&nbsp;</p>
<h2 id="👌-promise-객체-사용">👌 Promise 객체 사용</h2>
<p><strong>프로미스(Promise)</strong>는 비동기 작업의 최종 결과를 담는 객체를 말한다. 작업이 아직 끝나지 않았을 수도 있고, 성공했을 수도 있고, 실패했을 수도 있다. Promise는 처음 생성 시 대기 상태(PENDING)였다가 이행이 되면 <code>resolve()</code> 함수를 호출, 거절되면 <code>reject()</code> 함수가 호출된다.</p>
<p>&nbsp;</p>
<p>쉽게 비유를 하면, 현실에서 약속이란 의미는 미래의 어떤 것을 할 거라고 정하는 약속은 이행, 거절, 대기 3가지의 상태를 갖는다는 컨셉인 것이다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/c3c52870-c7ee-4daa-b346-48d67d290e44/image.png" alt=""></p>
<p><em><strong>&lt;<code>then()</code> 메서드 체이닝 사용&gt;</strong></em></p>
<pre><code class="language-jsx">// 회원 DB
const database = [];

// 회원 정보
const user = {
  email: &#39;byeonguk@gmail.com&#39;,
  name: &#39;ByeongUk&#39;,
  password: &#39;1234&#39;,
};

function saveDb(user) {
  const oldDbSize = database.length;
  database.push(user);
  console.log(`Save ${user.name} in Database...`);

  return new Promise(
    // Promise는 처음 대기 상태 정보를 가짐
    // resolve, reject 함수를 가지고 있음
    (resolve, reject) =&gt; {
      // 콜백 대신 Promise 객체 반환
      if (database.length &gt; oldDbSize) {
        resolve(user); // 성공 시 유저 정보 반환
      } else {
        reject(new Error(&#39;Save Failed...&#39;));
      }
    },
  );
}

function sendEmail(user) {
  console.log(`Send Email to ${user.email}`);
  return new Promise((resolve) =&gt; resolve(user));
}

function getResult(user) {
  return new Promise((resolve, reject) =&gt; {
    resolve(`${user.name} is successfully registered...`);
  });
}

function registerByPromise(user) {
  // 비동기 호출이지만, 순서를 지켜서 실행
  const result = saveDb(user).then(sendEmail).then(getResult);

  // 아직 완료되지 않았으므로 지연(PENDING) 상태
  // 실행이 완료되지 않았는데 result를 출력해버림
  console.log(result);
  return result;
}

const result = registerByPromise(user);

// 결과가 Promise 객체이므로 then() 메서드에 함수를 넣어서 결과를 출력
result.then(console.log);

// Save ByeongUk in Database...
// Promise { &lt;pending&gt; } -&gt; 이 메시지 출력으로 Promise가 아직 실행 중임을 알 수 있음
// Send Email to byeonguk@gmail.com
// ByeongUk is successfully registered...
</code></pre>
<p>&nbsp;</p>
<p><em><strong>&lt;<code>Promise.all()</code> 사용&gt;</strong></em></p>
<pre><code class="language-jsx">const database = [];

const user = {
  email: &#39;byeonguk@gmail.com&#39;,
  name: &#39;ByeongUk&#39;,
  password: &#39;1234&#39;,
};

function saveDb(user) {
  const oldDbSize = database.length;
  database.push(user);
  console.log(`Save ${user.name} in Database...`);

  return new Promise(
    (resolve, reject) =&gt; {
      if (database.length &gt; oldDbSize) {
        resolve(user); 
      } else {
        reject(new Error(&#39;Save Failed...&#39;));
      }
    },
  );
}

function sendEmail(user) {
  console.log(`Send Email to ${user.email}`);
  return new Promise((resolve) =&gt; resolve(user));
}

function getResult(user) {
  return new Promise((resolve, reject) =&gt; {
    resolve(`${user.name} is successfully registered...`);
  });
}

function registerByPromise(user) {
  const result = saveDb(user).then(sendEmail).then(getResult);
  console.log(result);
  return result;
}

allResult = Promise.all([saveDb(user), sendEmail(user), getResult(user)]);
allResult.then(console.log);

// Save ByeongUk in Database...
// Send Email to byeonguk@gmail.com
// [
//   { email: &#39;byeonguk@gmail.com&#39;, name: &#39;ByeongUk&#39;, password: &#39;1234&#39; },
//   { email: &#39;byeonguk@gmail.com&#39;, name: &#39;ByeongUk&#39;, password: &#39;1234&#39; },
//   &#39;ByeongUk is successfully registered...&#39;
// ]

// 출력된 배열이 getResult()임
</code></pre>
<p>&nbsp;</p>
<p><em><strong>&lt;예외 처리 로직까지 추가한 코드&gt;</strong></em></p>
<pre><code class="language-jsx">const database = [];

const user = {
  email: &#39;byeonguk@gmail.com&#39;,
  name: &#39;ByeongUk&#39;,
  password: &#39;1234&#39;,
};

function saveDb(user) {
  const oldDbSize = database.length + 1;
  database.push(user);
  console.log(`Save ${user.name} in Database...`);

  return new Promise((resolve, reject) =&gt; {
    if (database.length &gt; oldDbSize) {
      resolve(user);
    } else {
      reject(new Error(&#39;Save Failed...&#39;));
    }
  });
}

function sendEmail(user) {
  console.log(`Send Email to ${user.email}`);
  return new Promise((resolve) =&gt; resolve(user));
}

function getResult(user) {
  return new Promise((resolve, reject) =&gt; {
    resolve(`${user.name} is successfully registered...`);
  });
}

function registerByPromise(user) {
  const result = saveDb(user)
    .then(sendEmail)
    .then(getResult)
    .catch((error) =&gt; new Error(error))
    .finally(() =&gt; console.log(&#39;완료&#39;));

  console.log(result);
  return result;
}

const result = registerByPromise(user);
result.then(console.log);

// Save ByeongUk in Database...
// Promise { &lt;pending&gt; }
// 완료
// Error: Error: Save Failed...
//     at C:\Users\student\Desktop\node\chapter04\promiseCatch_test.js:40:23
//     at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
</code></pre>
<p>&nbsp;</p>
<p><em><strong>&lt;Promise 객체로 영화 Top 20 불러오기&gt;</strong></em></p>
<pre><code class="language-jsx">const axios = require(&#39;axios&#39;);
const url =
  &#39;https://raw.githubusercontent.com/wapj/musthavenodejs/main/movieinfo.json&#39;;

axios
  .get(url)
  .then((result) =&gt; {
    if (result.status != 200) {
      throw new Error(&#39;요청 정보를 가져오는데 실패했습니다.&#39;);
    }

    if (result.data) {
      return result.data;
    }

    throw new Error(&#39;전송 받은 데이터가 없습니다...&#39;);
  })
  .then((data) =&gt; {
    if (!data.articleList &amp;&amp; data.articleList.size == 0) {
      throw new Error(&#39;데이터가 없습니다.&#39;);
    }
    return data.articleList;
  })
  .then((articles) =&gt; {
    return articles.map((article, idx) =&gt; {
      return { title: article.title, rank: idx + 1 };
    });
  })
  .then((results) =&gt; {
    for (let movieInfo of results) {
      console.log(`${movieInfo.rank}위 - ${movieInfo.title}`);
    }
  })
  .catch((err) =&gt; {
    console.log(&#39;에러 발생!&#39;);
    console.error(err);
  });

// 1위 - 처음부터 잘했으면 얼마나 좋니
// 2위 - &lt;본즈 앤 올&gt; 궁지로 내몰린 10대를 보는 시선
// 3위 - 경이로운 생生의 의지로 창조해낸 ‘페르시아어’
// 4위 - 뻔하지 않은 사랑 영화
// 5위 - 우린 아무것도 모른다, 틀렸다는것만 증명할 뿐…에올
// 6위 - [영화리뷰] &lt;더 메뉴&gt;를 보고
// 7위 - 진실한 삶의 태도를 제시하다
// 8위 - 중요한 건 꺾이지 않는 마음
// 9위 - 치즈버거 세트의 행복
// 10위 - 즐기거나 놀리거나,
// 11위 - [영화감상]오늘 밤, 세계에서 이 사랑이 사라진다 해도
// 12위 - 아들을 구하고 싶다면 달려라
// 13위 - 인생은 아름다워(2022)
// 14위 - 영화 &lt;사도&gt;, 죽음의 문턱에 와서야 닿는 마음에 대해
// 15위 - 스타워즈: 안도르
// 16위 - 죽음의 문턱에서 거짓말로 살아남은 자의 고백
// 17위 - 닫힌 마음 - 영화 &#39;체리향기&#39;
// 18위 - &lt;오늘 밤, 세계에서 이 사랑이 사라진다 해도&gt; 리뷰
// 19위 - &lt;더 메뉴&gt; 180만 원짜리 먹으러 와서 사레 걸린기분
// 20위 - 닭장을 나온 백호
</code></pre>
<p>&nbsp;</p>
<h2 id="🦾-asyncawait-사용">🦾 Async/Await 사용</h2>
<p>Async/Await는 Promise를 더 쉽게 읽고 쓸 수 있도록 만든 문법이다. 겉보기에는 동기 코드처럼 보이지만, 실제로는 Promise 기반으로 동작한다.</p>
<pre><code class="language-jsx">async function myName() {
  // async는 Promise 객체를 반환
  return &#39;ByeongUk&#39;;
}

console.log(myName());
// Promise { &#39;ByeongUk&#39; }</code></pre>
<p>&nbsp;</p>
<p>함수 앞에 <code>async</code>를 붙이면 그 함수는 항상 반환값을 <code>Promise</code>로 감싸서 반환한다. <code>await</code>는 Promise 객체의 실행이 성공 또는 실패로 완료되기를 기다린다. 성공하면 그 결과값을 꺼내온다.</p>
<pre><code class="language-jsx">async function myName() {
  // async는 Promise 객체를 반환
  return &#39;ByeongUk&#39;;
}

// 이름을 출력하는 함수
async function showName() {
  const name = await myName();
  console.log(name);
}

console.log(showName());

// Promise { &lt;pending&gt; } &lt;- console.log(showName())의 결괏값임
// ByeongUk</code></pre>
<p>&nbsp;</p>
<p>여기서 <code>await myName()</code>은 <code>myName()</code>이 반환한 Promise가 끝날 때까지 기다린 뒤(<code>Promise { &lt;pending&gt; }</code>), 실제 결과인 <em>&#39;ByeongUk&#39;</em> 을 <code>name</code>에 넣어주는 것이다. 그리고 <code>showName()</code> 함수가 종료된다.</p>
<pre><code class="language-jsx">// 1초 대기하고 전달 받은 메시지 출력
function waitOneSecond(message) {
  return new Promise((resolve, _) =&gt; {
    setTimeout(() =&gt; {
      resolve(`전달 받은 메시지: ${message}`);
    }, 1000);
  });
}

// 10초 동안 1초마다 숫자 메시지를 출력
// Promise 객체를 처리해야 하기 때문에 async를 붙임
async function countOneToTen() {
  for (let number of [...Array(10).keys()]) {
    let result = await waitOneSecond(`${number + 1}초 대기 중...`);
    console.log(result);
  }

  console.log(&#39;실행 완료!&#39;);
}

countOneToTen();

// 전달 받은 메시지: 1초 대기 중...
// 전달 받은 메시지: 2초 대기 중...
// 전달 받은 메시지: 3초 대기 중...
// 전달 받은 메시지: 4초 대기 중...
// 전달 받은 메시지: 5초 대기 중...
// 전달 받은 메시지: 6초 대기 중...
// 전달 받은 메시지: 7초 대기 중...
// 전달 받은 메시지: 8초 대기 중...
// 전달 받은 메시지: 9초 대기 중...
// 전달 받은 메시지: 10초 대기 중...
// 실행 완료!</code></pre>
<p>위 코드는 반복문 안에서 <code>await</code>를 사용하고 있기 때문에 각 작업이 순차적으로 1초씩 기다린 뒤 실행되는 것을 볼 수 있다.</p>
<p>&nbsp;</p>
<p><em><strong>&lt;영화 Top 20 불러오는 코드 Async/Await를 사용해서 다시 작성&gt;</strong></em></p>
<pre><code class="language-jsx">const axios = require(&#39;axios&#39;);

async function getTop20Movies() {
  // await를 사용하여 동기화 시키기 위해 async 붙임
  const url =
    &#39;https://raw.githubusercontent.com/wapj/musthavenodejs/main/movieinfo.json&#39;;

  // http 네트워크에서 데이터를 받아 오기 때문에 await로 기다려야 함
  try {
    const result = await axios.get(url);
    const { data } = result;

    if (!data.articleList || data.articleList.size == 0) {
      throw new Error(&#39;데이터가 없습니다.&#39;);
    }

    const movieInfos = data.articleList.map((article, index) =&gt; {
      return { title: article.title, rank: index + 1 };
    });

    // 데이터 영화 정보 출력
    for (let movieInfo of movieInfos) {
      console.log(`[${movieInfo.rank}위] - ${movieInfo.title}`);
    }
  } catch (error) {
    throw new Error(error);
  }
}

getTop20Movies();
</code></pre>
<p>&nbsp;</p>
<h1 id="💥-node에서-꼭-알아야-하는-비동기-실행-구조">💥 Node에서 꼭 알아야 하는 비동기 실행 구조</h1>
<p>Node는 서버 환경에서의 자바스크립트 런타임이고, 자바스크립트는 기본적으로 싱글 스레드 방식으로 동작한다. 근데 다양한 작업이나 오래 걸리는 작업은 어떻게 처리하는지가 의문이었는데, 그 답은 바로 <strong>이벤트 루프 기반 실행 구조</strong>에 있었다. </p>
<p>&nbsp;</p>
<p>일단 간단하게 Node의 비동기 실행 구조를 이해하자면…</p>
<ol>
<li><p>자바스크립트 코드가 콜 스택(Call Stack)에서 실행된다.</p>
</li>
<li><p>오래 걸리는 작업은 Node 런타임(libuv, Node API) 쪽에 맡긴다.</p>
</li>
<li><p>그 작업이 끝나면, 실행해야 할 콜백 함수가 콜백 큐에 들어간다.</p>
</li>
<li><p>이벤트 루프가 콜 스택이 비었는지 확인한다.</p>
</li>
<li><p>비어 있으면 큐에서 콜백을 꺼내 다시 콜 스택에 넣어 실행한다.</p>
</li>
</ol>
<p>&nbsp;</p>
<p>그럼 먼저 콜 스택(Call Stack)에 대해 알아보자. 콜 스택은 동기 함수 호출 정보가 쌓이는 구조다. 다시 말해, 지금 실행 중인 함수들이 차곡차곡 쌓이는 공간이라는 소리다. 예를 들어, 아래와 같은 간단한 코드가 있으면…</p>
<pre><code class="language-jsx">console.log(&#39;첫 번째 작업&#39;);
console.log(&#39;두 번째 작업&#39;);
console.log(&#39;세 번째 작업&#39;);</code></pre>
<p>전부 동기 코드라서 한 줄이 끝나야 다음 줄로 간다. 따라서 실행 순서는 무조건 위에서 아래로 간다. </p>
<p>&nbsp;</p>
<p>자바스크립트 엔진은 코드 실행 자체를 담당하기는 하지만, 오래 걸리는 작업을 직접 다 처리하지는 않고 <strong>Node API</strong>와 <strong>libuv</strong> 쪽으로 넘긴다. 이 녀석들이 비동기 함수를 관리한다. 이해를 위해 동기 함수와 비동기 함수가 섞여 있는 아래 코드를 살펴보자.</p>
<pre><code class="language-jsx">console.log(&#39;첫 번째 작업&#39;);
setTimeout(() =&gt; {
    console.log(&#39;두 번째 작업&#39;);
}, 3000);
console.log(&#39;세 번째 작업&#39;);</code></pre>
<p>&nbsp;</p>
<p>먼저 <em>‘첫 번째 작업’</em> 이 콜 스택에 들어가고 출력될 것이다. 그 다음 <code>setTimeout()</code> 함수도 호출 자체는 콜 스택에서 들어가는데, 내부 콜백 함수는 Node API, libuv에 등록되는 것이다. 그래서 3초가 지나면 그 콜백이 바로 실행되는 것이 아니라 콜백 큐로 이동한다. 이후에 이벤트 루프가 <em>‘세 번째 작업’</em> 까지 출력이 끝나고 콜 스택이 비었을 때 콜백 큐에 있는 콜백 함수를 실행하는 것이다. 아래 그림을 보면 이해가 빠를 것이다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/cac50771-5f8a-4e2e-a478-6fefafe6aa5a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/fcade0bd-df4c-4914-9b7f-6ed77668cbcd/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/8a7eefa6-2cae-4f88-b975-a04526415014/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/453e2e0b-3452-403c-a253-11d6698d1b2c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/1c771322-5101-44c6-aa76-0c89fd6e02ac/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/68ce2db3-18a3-49e3-bcb0-4b2abd0deec2/image.png" alt=""></p>
<p>이 모든 흐름을 이어주는 녀석이 바로 이벤트 루프인 것이다. 이벤트 루프가 하는 가장 중요한 일은 딱 하나라고 생각하면 된다. </p>
<blockquote>
<p><strong><em>“콜 스택이 비었는지 계속 확인하고, 비어 있으면 콜백 큐에서 콜백 함수를 꺼내서 콜 스택에 넣는다.”</em></strong></p>
</blockquote>
<p>즉, 이벤트 루프가 직접 함수를 실행한다기보다 실행 순서를 조율하는 관리자에 가깝다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DOM이란?]]></title>
            <link>https://velog.io/@rocker_nun/DOM%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@rocker_nun/DOM%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Thu, 12 Mar 2026 02:19:33 GMT</pubDate>
            <description><![CDATA[<h1 id="🤔-dom의-정의와-필요성">🤔 DOM의 정의와 필요성</h1>
<p>HTML의 목적은 오직 정보의 구조를 명시적으로 표현하는 것 뿐이다. 따라서 HTML만으로는 사용자와 상호작용하는 것은 불가능하다. 그래서 생겨난 것이 자바스크립트다. </p>
<p>하지만 가장 중요한 점을 짚고 넘어갈 필요가 있다. 바로 <strong><em>“자바스크립트는 HTML을 직접 수정할 수 있는가?”</em></strong> 이다. 결론부터 말하면 직접 수정할 수는 없고, 브라우저가 HTML을 해석해서 메모리에 올려 놓은 객체 구조를 조작하는 것이다. 이 객체 구조를 바로 <strong>DOM(Document Object Model)</strong>이라고 하는 것이다.</p>
<p>DOM은 브라우저가 HTML을 파싱해서 구성한 트리 형태의 객체 집합이다. 말로는 이해가 힘드니 예시를 들어보자.</p>
<pre><code class="language-html">&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;title&gt;Document&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;h1&gt;Hello World&lt;/h1&gt;
    &lt;ul&gt;
      &lt;li&gt;HTML&lt;/li&gt;
      &lt;li&gt;JavaScript&lt;/li&gt;
      &lt;li&gt;DOM&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>위와 같은 HTML 파일이 있다고 했을 때, 브라우저는 아래와 같이 DOM 트리를 생성한다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/fbcd5de6-dfea-43ee-96ed-9ba7e4d0d5b9/image.jpg" alt=""></p>
<p>위와 같이 구조화하는 과정을 자세히 뜯어보면,</p>
<ol>
<li><p>HTML 문자열을 토큰으로 분할한다. (ex. <code>&lt;h1&gt;</code>, <code>&lt;/h1&gt;</code>, <code>&lt;body&gt;</code>와 같은 단위로 자름)</p>
</li>
<li><p>토큰을 기반으로 노드라는 객체를 생성한다.</p>
</li>
<li><p>각 노드를 계층 구조의 트리로 연결한다.</p>
</li>
</ol>
<p>이렇게 생성된 DOM 트리를 통해 자바스크립트는 원하는 DOM 요소를 가져와서 이벤트 리스너를 등록하고 내용을 조작할 수 있는 것이다. 명심하자. 이 모든 과정은 HTML 파일을 수정하는 게 아니라, 브라우저 메모리 상의 DOM 객체를 실시간으로 조작하는 것이다.</p>
<p>&nbsp;</p>
<h1 id="🛠️-dom-구조와-조작-도구">🛠️ DOM 구조와 조작 도구</h1>
<p>이제 더 나아가보자. <em>“DOM이란 무엇이냐”</em> 라는 질문에 제대로 대답하기 위해서는 구조와 API로 나눠서 생각하는 과정이 필요하다. 이를 건물과 공구로 생각해보자.</p>
<ul>
<li><p>건물: HTML을 해석해서 만들어진 DOM 구조</p>
</li>
<li><p>공구: DOM을 조작하기 위한 API 함수들</p>
</li>
</ul>
<p>건물은 설계도에 따라 지어진 구조물이고, 공구는 이 건물을 유지보수, 리모델링하기 위한 도구라고 하자. 그렇다면 브라우저가 생성해낸 DOM 트리가 건물, 자바스크립트로 그 DOM을 다룰 때 사용하는 다양한 메서드들이 공구라고 생각하면 된다. 이 메서드(API)가 바로 브라우저에서 자바스크립트에게 제공하는 <strong>표준화된 조작 도구 모음집</strong>인 것이다.</p>
<p>&nbsp;</p>
<p>아래 예시를 보자.</p>
<pre><code class="language-html">&lt;body&gt;
    &lt;h1 id=&quot;title&quot;&gt;제목&lt;/h1&gt;
    &lt;button&gt;제목 바꾸기 버튼&lt;/button&gt;
&lt;/body&gt;</code></pre>
<p>일단 브라우저가 위 HTML 파일을 파싱해서 DOM 트리로 변환할 것이다. 여기까지가 구조인 것이다. 이 시점에 이 구조만 가지고는 아무 것도 할 수 없다. 당연하다. 아무 것도 시키지 않았으니까…</p>
<p>&nbsp;</p>
<p>이제 버튼을 누르면 제목을 바꾸기 위해 DOM 구조에 접근해서 조작해야 한다. 이때 사용하는 것이 바로 공구, DOM API라는 것이다. 아래 자바스크립트 코드를 보자.</p>
<pre><code class="language-jsx">const h1 = document.getElementById(&#39;title&#39;);
const button = document.querySelector(&#39;button&#39;);

button.addEventListener(&quot;click&quot;, () =&gt; {
    h1.textContent = &#39;바뀐 제목입니다...&#39;;
});</code></pre>
<p><code>getElementById(&#39;title&#39;),</code> <code>querySelector(&#39;button&#39;)</code>로 아이디가 title인 요소와 button 태그인 요소를 DOM 트리에서 가져온다. 그리고 이벤트 리스너를 등록하고 <code>h1.textContent = ...</code>를 통해 노드의 텍스트 콘텐츠를 변경하는 것이다. 이와 같이 DOM 요소를 가져오고 조작하는 방법이 바로 <strong>DOM API</strong>라는 것이다.</p>
<p>이렇게 구조와 API로 분리해서 이해하는 것이 중요한 이유는 모든 DOM과 관련된 기능들이 모두 DOM 구조 위에서 DOM API를 사용해서 일어나는 일이기 때문이다. 구조를 모르면 조작 대상이 없고, API를 모르면 조작 대상을 조작할 수 없는 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSS]]></title>
            <link>https://velog.io/@rocker_nun/CSS</link>
            <guid>https://velog.io/@rocker_nun/CSS</guid>
            <pubDate>Mon, 09 Mar 2026 14:05:23 GMT</pubDate>
            <description><![CDATA[<h1 id="🎨-stylesheet">🎨 Stylesheet</h1>
<p>HTML에 스타일을 입히고 싶으면 CSS로 스타일시트를 작성해서 해당 HTML에 적용하면 된다. 스타일시트 구성은 아래와 같다.</p>
<pre><code class="language-css">h1(선택자) { color(스타일 속성) : red(스타일 값); }</code></pre>
<p>제일 먼저 스타일을 적용할 대상을 설정해야 하는데, 이때 <strong>선택자(Selector)</strong>를 이용하면 된다. </p>
<pre><code class="language-html">&lt;!doctype html&gt;
&lt;html lang=&quot;ko&quot;&gt;
  &lt;head&gt;
    &lt;title&gt;TITLE&lt;/title&gt;
  &lt;/head&gt;
  &lt;style&gt;
    h1 {
      color: white;
      background: black;
    }
  &lt;/style&gt;
  &lt;body&gt;
    &lt;h1&gt;Hello World..!&lt;/h1&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p><code>&lt;style&gt;</code> 태그 안에 스타일시트를 작성해서 HTML에 배치하면 되는데 어디에 배치하든 상관없지만, 보통 <code>&lt;head&gt;</code> 태그 안에 두는 것이 관례이다. 그리고 원활한 작업을 위해 스타일시트를 변경할 때마다 실제 적용된 스타일을 보는 습관을 가지도록 하자.</p>
<p>&nbsp;</p>
<h1 id="🔍-css3-선택자-종류">🔍 CSS3 선택자 종류</h1>
<ul>
<li><p>전체 선택자(<code>*</code>): HTML 페이지 내부 태그 모두를 선택한다. 보통 모든 요소에 공통으로 입히고 싶은 초기 스타일이 있을 경우 사용하면 좋을 듯하다.</p>
</li>
<li><p>태그 선택자(<code>태그</code>): 특정 태그들을 모두 선택한다.</p>
</li>
<li><p>아이디 선택자(<code>#아이디</code>): 특정 아이디를 가진 태그를 선택한다. 아이디는 웹 페이지 내부에서 중복되면 안 되기 때문에 특정 태그 하나에 스타일을 입히고 싶을 때 사용하자.</p>
</li>
<li><p>클래스 선택자(<code>.클래스</code>): 가장 유용하고, 가장 많이 사용되는 선택자다. 특정 클래스가 붙어 있는 모든 태그들에 적용된다.</p>
</li>
<li><p>속성 선택자(<code>선택자[속성]</code>, <code>선택자[속성=값]</code>): 특정한 속성을 가진 태그나 그 속성이 특정 값을 가지고 있는 태그들을 선택할 수 있다.</p>
</li>
<li><p>자손 선택자(<code>선택자A &gt; 선택자B</code>): 지정한 태그를 기준으로 바로 아래 단계에 위치한 태그들에 적용된다. 여기서 기준이 중요하다. 기준이 누구냐에 따라 해석이 달라질 수 있다.</p>
</li>
<li><p>후손 선택자(<code>선택자A 선택자B</code>): 지정한 태그 아래에 있는 모든 태그들에 적용된다.</p>
</li>
<li><p>반응 선택자: 사용자의 실제 반응으로 생성되는 특정한 상태를 선택한다. 마우스로 클릭한다든지(<code>active</code>), 마우스를 올려 놓는다든지(<code>hover</code>), 여러가지가 있을 수 있다.</p>
</li>
<li><p>상태 선택자: 입력 양식의 상태를 선택할 수도 있다.</p>
</li>
</ul>
<p>&nbsp;</p>
<h1 id="🧩-css-배치-속성">🧩 CSS 배치 속성</h1>
<p>CSS 배치 속성은 HTML 요소를 화면에 어떻게 보여주고, 어떤 방식으로 배치할지를 결정하는 속성이다. 대표적으로 알아야 할 개념이 <code>display</code> 속성인데, 이 속성은 요소를 Block으로 배치할지, Inline으로 배치할지 결정한다.</p>
<ul>
<li><strong>Block 요소</strong> : 한 줄을 통째로 독차지하는 형태로 배치된다. 즉, 요소 하나가 배치되면 자동으로 줄바꿈이 일어나고, 다음 요소는 그 아래 줄에 위치하게 된다. 대표적으로 <code>div</code>, <code>p</code>, <code>h1</code> 같은 태그가 이에 해당된다.</li>
<li><strong>Inline 요소</strong>: 줄바꿈 없이 한 줄 안에서 물 흐르듯이 배치된다. 마치 글자처럼 옆으로 이어지기 때문에, 여러 요소가 같은 줄에 나란히 놓일 수 있다. 대표적으로 <code>span</code>, <code>a</code> 태그가 있다.</li>
</ul>
<p>블록과 인라인 개념이 왜 필요한가? 웹 페이지에서 요소를 원하는 위치에 배치하려면, 각 태그가 어떤 방식으로 공간을 차지하는지 알아야 한다. 즉, block과 inline의 차이를 이해해야 화면을 더 보기 좋고 의도한 형태로 구성할 수 있다.</p>
<p>CSS에서 모든 HTML 요소는 기본적으로 하나의 박스(Box)로 생각할 수 있다. 이를 박스 모델이라고 하는데, 아래와 같은 구조로 이루어진다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/2cb5b304-b628-4d9f-a041-386a4cad4ad3/image.webp" alt=""></p>
<ul>
<li><strong>content(요소 내용)</strong> : 실제 텍스트나 이미지가 들어가는 영역</li>
<li><strong>padding</strong> : 내용과 테두리 사이의 여백</li>
<li><strong>border(테두리)</strong> : 요소의 경계선</li>
<li><strong>margin</strong> : 요소 바깥쪽의 여백</li>
</ul>
<p>이처럼 HTML 요소를 배치할 때는 단순히 내용만 보는 것이 아니라 내용 테두리 기준, 안쪽 영역, 바깥쪽 영역까지 함께 고려해야 한다.</p>
<p>&nbsp;</p>
<h1 id="🔧-css-레이아웃">🔧 CSS 레이아웃</h1>
<p>레이아웃이란 이미지, 텍스트 같은 요소들을 화면에서 원하는 위치에 배치하는 것을 의미한다. 웹 페이지를 만들 때는 요소를 단순히 나열하는 것이 아니라, 보기 좋고 사용하기 편하도록 정렬하는 과정이 필요하다. 이때 많이 사용하는 개념이 <strong>Flexbox</strong>와 <strong>position 속성</strong>이다.</p>
<h2 id="🎏-flexbox란">🎏 Flexbox란?</h2>
<p>요소들을 한 줄 또는 한 칸에 딱딱하게 배치하는 것이 아니라, 공간에 맞게 유연하게 정렬할 수 있도록 도와준다. Flexbox를 사용할 때 주의해야 할 점은 <strong>부모 요소에 적용해야 한다는 것</strong>이다. 자식 요소를 정렬하고 싶다면 먼저 부모와 자식의 관계를 먼저 파악한 뒤에 부모에게 <code>display: flex</code>를 적용해야 자식 요소들이 그 영향을 받아서 정렬된다.</p>
<p><code>display: flex</code>를 적용하면 자식 요소들을 원하는 방향으로 유연하게 배치할 수 있다. 이때 자주 사용하는 속성은 아래와 같다.</p>
<ul>
<li><strong>justify-content</strong> : 주축(가로) 방향으로 정렬</li>
<li><strong>align-items</strong> : 교차축(세로) 방향으로 정렬</li>
<li><strong>flex-direction</strong> : 축의 방향 변경</li>
</ul>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/83ad3c56-f5d3-48e2-a534-1f1bd582be44/image.png" alt=""></p>
<p>플렉스박스는 <strong>주축(main-axis)</strong>과 <strong>교차축(cross axis)</strong>을 기준으로 요소를 움직인다. 기본적으로 주축은 가로 방향이고, 교차축은 세로 방향이다. 원한다면 <code>flex-direction</code> 속성을 사용해서 요소들이 정렬되는 방향을 바꿀 수도 있다. 축의 기본값은 <code>row</code>이며 세로 방향으로 축을 바꾸고 싶다면 <code>column</code>으로 설정하면 된다. <code>row-reverse</code> 등으로 역순 배치 또한 가능하다. </p>
<p>&nbsp;</p>
<p>주로 사용하는 속성은 <code>justify-content</code>와 <code>align-items</code>가 있다. 하나씩 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/eadb20dc-fc2c-42c8-982c-f3a7665e0c36/image.png" alt=""></p>
<ul>
<li><code>justify-content</code>: flex item들의 주축 상 위치/여백을 결정한다.<ul>
<li><code>flex-start</code>: 주축의 시작 지점부터 flex item이 시작된다.</li>
<li><code>center</code>: flex item이 주축 중앙으로 정렬된다.</li>
<li><code>flex-end</code>: flex item이 주축 끝 지점부터 정렬된다.</li>
<li><code>space-around</code>: flex item이 동일한 여백으로 정렬된다. 이때 flex container의 시작과 끝 지점에는 flex item 사이 여백의 1/2에 해당하는 여백이 들어간다.</li>
<li><code>space-between</code>: flex item이 동일한 여백으로 정렬된다. 하지만 <code>space-around</code>와는 다르게 flex container의 시작과 끝에는 여백이 존재하지 않는다.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/7dcfb4f0-99b8-4c0e-8424-f362758f5e49/image.png" alt=""></p>
<ul>
<li><code>align-items</code>: flex item들의 교차 축 상 위치/여백을 결정한다. 설정값은 바라보는 축만 다를 뿐, <code>justify-content</code>와 동일하다. <code>stretch</code>만 flex item의 높이를 교차 축의 길이와 동일하게 세팅한다는 것만 알아두자.</li>
</ul>
<hr />

<p><em><strong>&lt;참고 자료&gt;</strong></em>
<a href="https://developer.mozilla.org/ko/docs/Learn_web_development/Core/CSS_layout/Flexbox">Flexbox - Web 개발 학습하기 | MDN</a>
<a href="https://armadillo-dev.github.io/css/what-is-flexbox/">[CSS] Flexbox 이해하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[동적 프록시(Dynamic Proxy)]]></title>
            <link>https://velog.io/@rocker_nun/%EB%8F%99%EC%A0%81-%ED%94%84%EB%A1%9D%EC%8B%9C</link>
            <guid>https://velog.io/@rocker_nun/%EB%8F%99%EC%A0%81-%ED%94%84%EB%A1%9D%EC%8B%9C</guid>
            <pubDate>Wed, 04 Mar 2026 13:43:17 GMT</pubDate>
            <description><![CDATA[<h1 id="🔦-리플렉션">🔦 리플렉션</h1>
<p><strong>리플렉션(Reflection)</strong>은 런타임에 클래스, 메서드, 필드의 메타 정보에 접근해서 객체를 만들거나 메서드를 호출하거나 값을 읽고, 바꾸는 기능이다. 쉽게 말해 실행 중인 프로그램이 자기 자신(또는 다른 객체)의 내부 구조를 들여다보고, 뜯어고칠 수 있게 해주는 기술이라고 생각하면 된다. 보통 자바 코드를 짤 때는 어떤 클래스를 쓸지, 어떤 메서드를 호출할지 컴파일 시점에 이미 다 알고 사용할 수 있다. 하지만 리플렉션을 사용하면 런타임에, 이름만 알고 있는 클래스의 내부 정보를 동적으로 알아내고 조작할 수 있다. </p>
<p>자바 API 문서를 살펴보면, 리플렉션의 주요 특징을 4가지로 요약할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/08cfec41-479c-4dc6-b566-dce0dd3edaa8/image.png" alt=""></p>
<ol>
<li><p><strong>클래스 내부 정보 알아내기</strong>: 어떤 객체가 주어졌을 때, 그 객체를 만든 클래스가 무엇인지 모르더라도 리플렉션을 쓰면 그 객체 안에 어떤 변수가 있고, 어떤 함수가 있으며, 어떻게 객체를 생성하는지 탈탈 털어볼 수 있다.</p>
</li>
<li><p><strong>알아낸 정보로 직접 조작하기</strong>: 단순히 *&quot;이런 메서드가 있네?&quot;* 하고 끝나는 것이 아니라 알아낸 정보를 바탕으로 직접 메서드를 실행시키거나, 변수의 값을 읽고 덮어쓰는 것도 가능하다. 코드에 하드코딩되어 있지 않은 동작을 런타임에 마음대로 끄집어내어 실행할 수 있는 것이다.</p>
</li>
<li><p><strong>접근 제어자 무력화</strong>: 리플렉션의 가장 강력한 기능이다. 원래 자바에서는 <code>private</code>으로 선언된 변수나 메서드는 외부 클래스에서 절대 접근할 수 없지만, 리플렉션의 <code>AccessibleObject</code>를 사용하면 이 접근 검사를 무시하고 <code>private</code> 변수의 값을 강제로 읽거나 바꿀 수 있다. 일반적으로는 <code>private</code>으로 선언된 이유가 있을 것이기 때문에 사용에 유의하도록 하자.</p>
</li>
<li><p><strong>배열의 동적 생성</strong>: 보통 배열은 크기와 타입을 미리 정해두지만, <code>Array</code> 클래스를 사용하면 실행 중에 원하는 타입과 크기를 동적으로 결정해서 배열을 만들어낼 수 있다.</p>
</li>
</ol>
<p>&nbsp;</p>
<p>정리하자면, 리플렉션은 개발자가 짠 코드를 프레임워크나 시스템이 실행 중에 유연하게 분석하고 조립하기 위해 만들어둔 만능 열쇠 정도로 생각하면 좋을 것 같다.</p>
<p>&nbsp;</p>
<p>이제 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스를 이해하기 위한 최소한의 리플렉션 기술에 대해 코드로 알아보자.</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.jdkdynamic;

import java.lang.reflect.Method;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

@Slf4j
public class ReflectionTest {

    @Test
    void reflectionV0() {
        Hello target = new Hello();

        // 공통 로직1
        log.info(&quot;Start...&quot;);
        String result1 = target.methodA();
        log.info(&quot;result1: {}&quot;, result1);

        // 공통 로직2
        log.info(&quot;Start...&quot;);
        String result2 = target.methodB();
        log.info(&quot;result2: {}&quot;, result2);

        /**
         * 13:03:01.047 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest -- Start...
         * 13:03:01.049 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest$Hello -- calling A...
         * 13:03:01.049 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest -- result1: A
         * 13:03:01.049 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest -- Start...
         * 13:03:01.049 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest$Hello -- calling B...
         * 13:03:01.049 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest -- result2: B
         */
    }

    @Slf4j
    static class Hello {
        public String methodA() {
            log.info(&quot;calling A...&quot;);
            return &quot;A&quot;;
        }

        public String methodB() {
            log.info(&quot;calling B...&quot;);
            return &quot;B&quot;;
        }
    }
}</code></pre>
<p>위 코드에서 <code>공통 로직1</code>과 <code>공통 로직2</code>는 호출하는 메서드(<code>methodA</code>, <code>methodB</code>)만 다를 뿐 완전히 같은 구조다. 이 공통된 부분을 하나의 메서드로 뽑아낼 수 있을까? 중간에 호출하는 메서드가 다르기 때문에 일반적인 방법으로는 추상화하기가 몹시 까다롭다. </p>
<p>딱 저 <strong>메서드를 호출하는 부분만 동적으로 처리</strong>할 수 있다면 얼마나 좋을까? 이때 사용하는 것이 바로 리플렉션이다. 아래 리플렉션을 사용한 코드를 보자.</p>
<pre><code class="language-java">...

@Test
void reflectionV1() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    // 현재 패키지의 Hello 클래스의 내부 정보
    Class classHello = Class.forName(&quot;com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest$Hello&quot;);

    Hello target = new Hello();

    // callA 메서드 정보
    Method methodA = classHello.getMethod(&quot;methodA&quot;);
    Object result1 = methodA.invoke(target);
    log.info(&quot;result1: {}&quot;, result1);

    // callB 메서드 정보
    Method methodB = classHello.getMethod(&quot;methodB&quot;);
    Object result2 = methodB.invoke(target);
    log.info(&quot;result2: {}&quot;, result2);

    /**
     * 13:07:35.329 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest$Hello -- calling A...
     * 13:07:35.331 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest -- result1: A
     * 13:07:35.331 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest$Hello -- calling B...
     * 13:07:35.331 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest -- result2: B
     */

     ...</code></pre>
<p>일단 클래스의 메타정보를 획득하기 위해 <code>Class.forName(&quot;해당 클래스의 패키지 경로&quot;)</code>를 사용한다. <code>getMethod()</code>를 이용해서 특정 메서드의 메타정보를 획득한 후에 <code>invoke(target)</code>로 실제 <code>target</code> 인스턴스의 <code>methodA()</code>나 <code>methodB()</code> 메서드를 동적으로 호출할 수 있다는 것이다.</p>
<p>&nbsp;</p>
<p>꽤 복잡하다. 하지만 이렇게 메서드 정보를 획득해서 메서드를 호출하도록 처리해야 동적으로 메서드를 호출할 수 있도록 추상화할 수 있기 때문이다. 아래 코드를 보자.</p>
<pre><code class="language-java">private void dynamicCall(Method method, Object target) throws InvocationTargetException, IllegalAccessException {
    log.info(&quot;Start...&quot;);
    Object result = method.invoke(target);
    log.info(&quot;result: {}&quot;, result);
}</code></pre>
<p>&nbsp;</p>
<p>사용은 호출을 원하는 메서드의 메타정보와 타깃 객체의 타입을 넘기면 된다.</p>
<pre><code class="language-java">...

@Test
void reflectionV2() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetExceptio {
    Class classHello = Class.forName(&quot;com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest$Hello&quot;);

    Hello target = new Hello();

    Method methodA = classHello.getMethod(&quot;methodA&quot;);
    dynamicCall(methodA, target);  // 메서드의 메타정보를 파라미터로 넘김

    Method methodB = classHello.getMethod(&quot;methodB&quot;);
    dynamicCall(methodB, target);  // 메서드의 메타정보를 파라미터로 넘김

    /**
     * 13:13:49.726 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest -- Start...
     * 13:13:49.728 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest$Hello -- calling A...
     * 13:13:49.728 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest -- result: A
     * 13:13:49.729 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest -- Start...
     * 13:13:49.729 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest$Hello -- calling B...
     * 13:13:49.729 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.ReflectionTest -- result: B
     */
}</code></pre>
<p>이제 메서드 이름이 하드코딩 되어있지 않고, 메타정보(<code>Method</code>)를 파라미터로 넘겨 동적으로 메서드 호출이 가능해졌다.</p>
<p>하지만 리플렉션은 런타임에 동작하기 때문에 컴파일 시점에 에러를 잡을 수 없고 속도도 상대적으로 느리다. 따라서 일반적인 비즈니스 로직보다는 프레임워크 개발이나 매우 일반적인 공통 처리가 필요할 때 주의해서 사용해야 한다.</p>
<p>&nbsp;</p>
<h1 id="⚙-jdk-동적-프록시">⚙ JDK 동적 프록시</h1>
<p>프록시는 기존 코드는 건들지 않으면서 타깃 객체에 부가적인 기능을 부여해줄 수 있다. 하지만 프록시를 적용하려는 대상 클래스가 100개라면, 프록시 클래스도 100개를 만들어야 하는 엄청난 번거로움이 따른다.</p>
<p>다행히 자바는 프록시 객체를 동적으로 런타임에 만들어주는 <strong>JDK 동적 프록시</strong> 기술을 제공한다.</p>
<pre><code class="language-java">// A 인터페이스
package com.example.advanced_spring.proxy.jdkdynamic.code;

public interface AInterface {
    String call();
}</code></pre>
<pre><code class="language-java">// A 인터페이스 구현체
package com.example.advanced_spring.proxy.jdkdynamic.code;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class AInterfaceImpl implements AInterface {
    @Override
    public String call() {
        log.info(&quot;A 호출...&quot;);
        return &quot;a&quot;;
    }
}</code></pre>
<pre><code class="language-java">// B 인터페이스
package com.example.advanced_spring.proxy.jdkdynamic.code;

public interface BInterface {
    String call();
}</code></pre>
<pre><code class="language-java">// B 인터페이스 구현체
package com.example.advanced_spring.proxy.jdkdynamic.code;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class BInterfaceImpl implements BInterface {
    @Override
    public String call() {
        log.info(&quot;B 호출...&quot;);
        return &quot;b&quot;;
    }
}</code></pre>
<p>동적 프록시는 프록시 팩토리에 의해 런타임 시 동적으로 만들어진다. 인터페이스 정보만 제공해주면 해당 인터페이스를 구현한 프록시 인스턴스를 자동으로 만들어준다.</p>
<p>내가 해야 할 일은 그저 이 프록시가 수행할 부가기능(공통 로직)을 <code>InvocationHandler</code>에 정의하는 것뿐이다. 클라이언트의 모든 요청은 프록시를 거쳐 <code>InvocationHandler</code>의 <code>invoke()</code> 메서드로 집중된다.</p>
<img src="https://velog.velcdn.com/images/rocker_nun/post/9846bf3d-421d-4e09-88f7-6d62100da698/image.png" width=500/>

<p>&nbsp;</p>
<p>아래는 <code>java.lang.reflect</code> 패키지에 있는 <code>InvocationHandler</code> 인터페이스다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/9db354fd-8c12-498c-b383-1a280eeaf614/image.png" alt=""></p>
<p>보다시피 하나의 메서드만 가진 아주 간단한 인터페이스다. <code>invoke()</code> 메서드는 리플렉션의 <code>Method</code> 인터페이스를 파라미터로 받고, 메서드를 호출할 때 전달되는 파라미터도 받는다. 동적 프록시는 클라이언트의 모든 요청을 리플렉션 정보로 변환해서 <code>InvocationHandler</code> 구현체의 <code>invoke()</code> 메서드로 넘기는 것이다. 타깃 인터페이스의 모든 메서드 요청이 하나의 메서드로 집중되기 때문에 중복되는 기능을 효과적으로 제공할 수 있다.  </p>
<p>&nbsp;</p>
<p>이제 해당 인터페이스를 구현해서 실행 시간을 측정하는 부가기능을 가진 구현체를 만들고 테스트 해보도록 하자.</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.jdkdynamic.code;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TimeInvocationHandler implements InvocationHandler {

    private final Object target;  // 프록시가 호출할 대상(범용으로 사용하기 위해 Object로 받음)

    public TimeInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException, InvocationTargetException {
        log.info(&quot;TimeProxy 실행...&quot;);
        long startTime = System.currentTimeMillis();

        // 여기서 메서드를 동적으로 호출
        Object result = method.invoke(target, args);

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info(&quot;TimeProxy 종료 resultTime={}&quot;, resultTime);
        return result;
    }
}</code></pre>
<pre><code class="language-java">package com.example.advanced_spring.proxy.jdkdynamic;

import com.example.advanced_spring.proxy.jdkdynamic.code.AInterface;
import com.example.advanced_spring.proxy.jdkdynamic.code.AInterfaceImpl;
import com.example.advanced_spring.proxy.jdkdynamic.code.BInterface;
import com.example.advanced_spring.proxy.jdkdynamic.code.BInterfaceImpl;
import com.example.advanced_spring.proxy.jdkdynamic.code.TimeInvocationHandler;
import java.lang.reflect.Proxy;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

@Slf4j
public class JdkDynamicProxyTest {

    @Test
    void dynamicA() {
        AInterface target = new AInterfaceImpl();
        TimeInvocationHandler handler = new TimeInvocationHandler(target);
        AInterface proxy = (AInterface) Proxy.newProxyInstance(
                AInterface.class.getClassLoader(),
                new Class[]{AInterface.class}, handler);

        proxy.call();
        log.info(&quot;targetClass={}&quot;, target.getClass());
        log.info(&quot;proxyClass={}&quot;, proxy.getClass());

        /**
         * 13:39:51.584 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.code.TimeInvocationHandler -- TimeProxy 실행...
         * 13:39:51.586 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.code.AInterfaceImpl -- A 호출...
         * 13:39:51.586 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.code.TimeInvocationHandler -- TimeProxy 종료 resultTime=0
         * 13:39:51.587 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.JdkDynamicProxyTest -- targetClass=class com.example.advanced_spring.proxy.jdkdynamic.code.AInterfaceImpl
         * 13:39:51.587 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.JdkDynamicProxyTest -- proxyClass=class jdk.proxy3.$Proxy12
         */
    }

    @Test
    void dynamicB() {
        BInterface target = new BInterfaceImpl();
        TimeInvocationHandler handler = new TimeInvocationHandler(target);
        BInterface proxy = (BInterface) Proxy.newProxyInstance(
                BInterface.class.getClassLoader(),
                new Class[]{BInterface.class}, handler);

        proxy.call();
        log.info(&quot;targetClass={}&quot;, target.getClass());
        log.info(&quot;proxyClass={}&quot;, proxy.getClass());

        /**
         * 13:40:09.633 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.code.TimeInvocationHandler -- TimeProxy 실행...
         * 13:40:09.635 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.code.BInterfaceImpl -- B 호출...
         * 13:40:09.635 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.code.TimeInvocationHandler -- TimeProxy 종료 resultTime=0
         * 13:40:09.636 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.JdkDynamicProxyTest -- targetClass=class com.example.advanced_spring.proxy.jdkdynamic.code.BInterfaceImpl
         * 13:40:09.636 [Test worker] INFO com.example.advanced_spring.proxy.jdkdynamic.JdkDynamicProxyTest -- proxyClass=class jdk.proxy3.$Proxy12
         */
    }
}</code></pre>
<p>위의 테스트 코드에서 A 인터페이스, B 인터페이스를 각각 구현한 시간을 측정하는 부가기능이 탑재된 프록시를 만들어봤다. 동적 프록시는 <code>Proxy</code> 클래스의 <code>newProxyInstance()</code> 정적 팩토리 메서드를 사용해서만 생성할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/6520a9fa-40be-4ada-bb7d-4cd312789d41/image.png" alt=""></p>
<p>보다시피 <code>newProxyInstance()</code> 정적 팩토리 메서드는 첫 번째 파라미터로 클래스 로더를 받아야 한다. 동적 프록시가 정의되는 클래스 로더를 지정하는 것이다. 두 번째는 동적 프록시가 구현해야 할 인터페이스다. 동적 프록시는 여러 개의 인터페이스를 구현할 수도 있기 때문에 인터페이스의 배열을 사용한다. 마지막으로 부가기능과 위임 관련 코드를 담고 있는 <code>InvocationHandler</code> 구현체를 제공해야 한다.</p>
<p>&nbsp;</p>
<p>실행 순서를 자세히 뜯어보면서 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/cc1ddeb2-2389-4a61-8c95-6cd23d2ec9e8/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/0ab47a54-2037-42ba-939d-880d4eeb2964/image.png" alt=""></p>
<ol>
<li><p><code>client</code>는 JDK 동적 프록시의 <code>call()</code>을 실행한다. </p>
</li>
<li><p>JDK 동적 프록시는 <code>InvocationHandler.invoke()</code>를 호출한다. 현재 예시 코드에서는 <code>TimeInvocationHandler</code>가 구현체로 있으므로 <code>TimeInvocationHandler.invoke()</code>가 호출된다.</p>
</li>
<li><p><code>TimeInvocationHandler</code>가 내부 로직을 수행하고, <code>method.invoke(target, args)</code>를 호출해서 <code>target</code>인 실제 객체를 호출한다.</p>
</li>
<li><p>실제 객체 인스턴스의 <code>call()</code>이 호출된다.</p>
</li>
<li><p>인스턴스의 <code>call()</code>의 실행이 끝나면 <code>TimeInvocationHandler</code>로 응답이 돌아오고, 시간 로그를 출력 후 결과를 반환한다.</p>
</li>
</ol>
<p>&nbsp;</p>
<p>정리하면 <code>AInterfaceImpl</code>, <code>BInterfaceImpl</code> 전용 프록시 클래스를 일일이 만든 것이 아니다. 런타임에 동적으로 프록시가 만들어지고 <code>TimeInvocationHandler</code>를 공통으로 사용하게 된다. 적용 대상이 100개여도 프록시 클래스를 만들 필요 없이 단일 핸들러만으로 부가기능을 부여할 수 있어 완벽하게 SRP 원칙을 지킬 수 있다.</p>
<p>&nbsp;</p>
<h2 id="🔨-jdk-동적-프록시-적용하기">🔨 JDK 동적 프록시 적용하기</h2>
<p>기존의 <code>LogTrace</code>를 적용할 수 있는 <code>InvocationHandler</code>를 만들어보자.</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.config.v2_dynamicproxy.handler;

import com.example.advanced_spring.trace.TraceStatus;
import com.example.advanced_spring.trace.logtrace.LogTrace;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogTraceBasicHandler implements InvocationHandler {

    private static final Logger log = LoggerFactory.getLogger(LogTraceBasicHandler.class);
    private final Object target;  // 프록시가 호출할 타깃 객체
    private final LogTrace logTrace;

    public LogTraceBasicHandler(Object target, LogTrace logTrace) {
        this.target = target;
        this.logTrace = logTrace;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        TraceStatus status = null;
        try {
            // LogTrace에서 사용할 메시지
            String message = method.getDeclaringClass().getSimpleName() + &quot;.&quot; + method.getName() + &quot;()&quot;;
            status = logTrace.begin(message);

            // 로직 호출
            Object result = method.invoke(target, args);

            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}
</code></pre>
<p>&nbsp;</p>
<p>이제 동적 프록시를 사용할 수 있도록 수동으로 빈 등록을 진행해보자.</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.config.v2_dynamicproxy;

import com.example.advanced_spring.proxy.config.v2_dynamicproxy.handler.LogTraceBasicHandler;
import com.example.advanced_spring.proxy.v1.ProxyOrderControllerV1;
import com.example.advanced_spring.proxy.v1.ProxyOrderControllerV1Impl;
import com.example.advanced_spring.proxy.v1.ProxyOrderRepositoryV1;
import com.example.advanced_spring.proxy.v1.ProxyOrderRepositoryV1Impl;
import com.example.advanced_spring.proxy.v1.ProxyOrderServiceV1;
import com.example.advanced_spring.proxy.v1.ProxyOrderServiceV1Impl;
import com.example.advanced_spring.trace.logtrace.LogTrace;
import java.lang.reflect.Proxy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DynamicProxyBasicConfig {

    @Bean
    public ProxyOrderControllerV1 orderControllerV1(LogTrace logTrace) {
        ProxyOrderControllerV1 orderController = new ProxyOrderControllerV1Impl(orderServiceV1(logTrace));
        ProxyOrderControllerV1 proxy = (ProxyOrderControllerV1) Proxy.newProxyInstance(
                ProxyOrderControllerV1.class.getClassLoader(),
                new Class[]{ProxyOrderControllerV1.class},
                new LogTraceBasicHandler(orderController, logTrace)
        );

        return proxy;
    }

    @Bean
    public ProxyOrderServiceV1 orderServiceV1(LogTrace logTrace) {
        ProxyOrderServiceV1 orderService =  new ProxyOrderServiceV1Impl(orderRepositoryV1(logTrace));
        ProxyOrderServiceV1 proxy = (ProxyOrderServiceV1) Proxy.newProxyInstance(
                ProxyOrderServiceV1.class.getClassLoader(),
                new Class[]{ProxyOrderServiceV1.class},
                new LogTraceBasicHandler(orderService, logTrace)
        );

        return proxy;
    }

    @Bean
    public ProxyOrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
        ProxyOrderRepositoryV1 orderRepository = new ProxyOrderRepositoryV1Impl();
        ProxyOrderRepositoryV1 proxy = (ProxyOrderRepositoryV1) Proxy.newProxyInstance(
                ProxyOrderRepositoryV1.class.getClassLoader(),
                new Class[]{ProxyOrderRepositoryV1.class},
                new LogTraceBasicHandler(orderRepository, logTrace)
        );

        return proxy;
    }
}
</code></pre>
<p>아래 그림을 보면 알 수 있듯이, 이제 프록시 클래스를 직접 일일이 만들어주는 것이 아니라 <code>OrderControllerV1</code>, <code>OrderServiceV1</code> 각각 동적으로 프록시 클래스가 생성되고 두 클래스 모두 <code>InvocationHandler</code>를 참조하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/52f985e3-706c-4ee7-9a0b-78be3ba886a6/image.png" alt=""></p>
<p>&nbsp;</p>
<p>런타임에는 클라이언트가 요청하면 스프링 빈으로 등록된 <code>OrderControllerV1</code> 프록시가 컨트롤러에서 호출된다. 해당 JDK 동적 프록시는 <code>LogTraceBasicHandler</code> 핸들러를 호출하고 로그가 찍힐 것이다. 그리고 나서 핸들러가 실제 타깃 객체인 <code>orderControllerV1Impl</code> 구현체를 호출한다. 코드를 보면 알 수 있듯이 해당 구현체는 생성자 주입으로 <code>orderServiceV1</code>을 주입 받고 있다. 근데 <code>OrderServiceV1</code>도 프록시를 반환하고 있기 때문에 서비스 프록시 클래스가 호출된다. 그 프록시 클래스가 <code>LogTraceBasicHandler</code>를 호출하고 그 핸들러가 다시 <code>orderServiceV1Impl</code> 구현체를 호출하는 흐름이다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/f8a01441-96d4-415d-8d35-eaf1850a9c04/image.png" alt=""></p>
<p>&nbsp;</p>
<p>추가로, 메서드 이름을 기준으로 특정 조건이 만족할 때만 로그를 출력하게 할 수도 있다. 로그를 남길 메서드 이름 패턴을 정의하고 생성자로 주입 받도록 하면 된다. 아래 코드를 보자.</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.config.v2_dynamicproxy.handler;

import com.example.advanced_spring.trace.TraceStatus;
import com.example.advanced_spring.trace.logtrace.LogTrace;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import org.springframework.util.PatternMatchUtils;

public class LogTraceFilterHandler implements InvocationHandler {

    private final Object target;
    private final LogTrace logTrace;
    private final String[] patterns;  // 로그를 출력하는 메서드 이름 패턴

    public LogTraceFilterHandler(Object target, LogTrace logTrace, String... patterns) {
        this.target = target;
        this.logTrace = logTrace;
        this.patterns = patterns;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        if (!PatternMatchUtils.simpleMatch(patterns, methodName)) {
            return method.invoke(target, args);
        }

        TraceStatus status = null;
        try {
            String message = method.getDeclaringClass().getSimpleName() + &quot;.&quot; + method.getName() + &quot;()&quot;;
            status = logTrace.begin(message);

            Object result = method.invoke(target, args);

            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}
</code></pre>
<p>특정 메서드 이름이 매칭되는 경우에만 <code>LogTrace</code>를 로직을 실행한다. 이름이 매칭되지 않으면 실제 로직을 바로 호출하도록 한다. 위와 같이 스프링이 제공하는 <code>PatternMatchUtils.simpleMatch()</code>를 사용하면 매칭 로직을 쉽게 적용할 수 있다. </p>
<p>여기까지 JDK 동적 프록시를 살펴본 결과, 일일이 프록시 클래스를 정의하지 않고도 동적으로 프록시 객체를 생성할 수 있다는 매우 큰 장점이 있지만, 안타깝게도 프록시가 특정 인터페이스를 반드시 구현해야 한다는 단점이 있다.</p>
<p>&nbsp;</p>
<h1 id="🎃-cglib">🎃 CGLIB</h1>
<p>JDK 동적 프록시는 인터페이스를 필수로 구현해야만 동작한다는 치명적인 단점이 있다. 만약 인터페이스 없이 구체 클래스만 있는 경우에는 어떻게 프록시를 적용해야 할까? 이때 사용하는 것이 바로 <strong>CGLIB</strong> 라이브러리다.</p>
<p>CGLIB는 바이트코드를 조작하여 동적으로 클래스를 생성하는 기술이다. 타깃 클래스를 상속받아 프록시를 생성하기 때문에 인터페이스 없이도 작동한다.</p>
<pre><code class="language-java">// 구현체 클래스만 존재
package com.example.advanced_spring.proxy.common.service;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ConcreteService {
    public void call() {
        log.info(&quot;ConcreteService 호출&quot;);
    } 
}</code></pre>
<p>&nbsp;</p>
<h2 id="⛏️-cglib-적용">⛏️ CGLIB 적용</h2>
<p>JDK 동적 프록시가 <code>InvocationHandler</code>를 사용했다면, CGLIB는 <code>MethodInterceptor</code>를 구현해야 한다.</p>
<pre><code class="language-java">package org.springframework.cglib.proxy;

public interface MethodInterceptor extends Callback {
        Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}</code></pre>
<p>여기서 <code>obj</code>는 CGLIB가 적용된 객체, <code>method</code>는 호출된 메서드, <code>args</code>는 메서드를 호출하면서 전달된 파라미터, <code>proxy</code>는 메서드 호출에 사용되는 프록시다. </p>
<p>&nbsp;</p>
<p>아래는 <code>MethodInterceptor</code>를 구현해서 CGLIB 프록시의 실행 로직을 정의한  <code>TimeMethodInterceptor</code> 구현체다.</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.cglib.code;

import java.lang.reflect.Method;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {

    private final Object target;

    public TimeMethodInterceptor(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        log.info(&quot;TimeProxy 실행&quot;);
        long startTime = System.currentTimeMillis();

        Object result = proxy.invoke(target, args);

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info(&quot;TimeProxy 종료 resultTime={}&quot;, resultTime);
        return result;
    }
}
</code></pre>
<p>CGLIB에서 타깃 메서드를 호출할 때 리플렉션의 <code>method.invoke()</code>를 사용할 수도 있지만, <code>MethodProxy</code>의 <code>proxy.invoke()</code>를 사용하는 것이 성능상 훨씬 유리하다. 내부적으로 <code>FastClass</code> 기반으로 동작하여 리플렉션의 오버헤드를 줄여준다.</p>
<p>&nbsp;</p>
<p>CGLIB는 <code>Enhancer</code> 객체를 통해 프록시를 생성한다. 타깃을 부모 클래스로 지정(<code>setSuperclass</code>)하여 상속을 통해 프록시를 덮어씌우는 방식이다.</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.cglib;

import com.example.advanced_spring.proxy.cglib.code.TimeMethodInterceptor;
import com.example.advanced_spring.proxy.common.service.ConcreteService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.cglib.proxy.Enhancer;

@Slf4j
public class CglibTest {

    @Test
    void cglib() {
        ConcreteService target = new ConcreteService();

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(ConcreteService.class);
        enhancer.setCallback(new TimeMethodInterceptor(target));
        ConcreteService proxy = (ConcreteService) enhancer.create();
        log.info(&quot;targetClass={}&quot;, target.getClass());
        log.info(&quot;proxyClass={}&quot;, proxy.getClass());

        proxy.call();

        /**
         * 15:10:25.409 [Test worker] INFO com.example.advanced_spring.proxy.cglib.CglibTest -- targetClass=class com.example.advanced_spring.proxy.common.service.ConcreteService
         * 15:10:25.411 [Test worker] INFO com.example.advanced_spring.proxy.cglib.CglibTest -- proxyClass=class com.example.advanced_spring.proxy.common.service.ConcreteService$$EnhancerByCGLIB$$9dae7170
         * 15:10:25.411 [Test worker] INFO com.example.advanced_spring.proxy.cglib.code.TimeMethodInterceptor -- TimeProxy 실행
         * 15:10:25.417 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ConcreteService -- ConcreteService 호출...
         * 15:10:25.417 [Test worker] INFO com.example.advanced_spring.proxy.cglib.code.TimeMethodInterceptor -- TimeProxy 종료 resultTime=6
         */
    }
}
</code></pre>
<p>정리하면 JDK 동적 프록시는 인터페이스를 구현해서 프록시를 만들고, CGLIB는 구체 클래스를 상속해서 프록시를 만드는 것이다.</p>
<p>&nbsp;</p>
<p>CGLIB를 사용할 때의 클래스 의존 관계를 살펴보면 클라이언트(테스트 코드)가 <code>ConcreteService</code>를 호출하면 동적 프록시는 <code>ConcreteService</code>를 상속 받아서 생성된다. 그리고 생성된 동적 프록시가 <code>MethodInterceptor</code>를 호출하는 것이다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/97ddff47-8b07-4bb1-84e6-fef57da6ae5c/image.png" alt=""></p>
<p>&nbsp;</p>
<p>실제 런타임에서는 클라이언트는 CGLIB가 만들어준 프록시를 호출하게 되고, 그 프록시가 <code>timeMethodInterceptor</code>를 호출하고, 로직을 수행하다가 중간에 <code>invoke()</code>를 통해 실제 타깃 객체를 <code>call()</code>하게 되는 것이다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/696768e3-4887-43ae-8883-aab135c66e2d/image.png" alt=""></p>
<p>짐작했겠지만, CGLIB는 클래스를 상속 받아 사용하기 때문에 몇 가지 제약이 존재한다. 먼저, 부모 클래스의 생성자를 체크해야 하고, 클래스나 메서드에 <code>final</code> 키워드가 붙어 있는지 항상 확인해줘야 한다.</p>
<hr>
<p><em><strong>&lt;참고 자료&gt;</strong></em>
<a href="https://docs.oracle.com/javase/8/docs/api/">Java™ Platform, Standard Edition 8 API Specification</a>
<a href="https://product.kyobobook.co.kr/detail/S000000935360">토비의 스프링 3.1 Vol 1: 스프링의 이해와 원리</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프록시 패턴 & 데코레이터 패턴]]></title>
            <link>https://velog.io/@rocker_nun/%ED%94%84%EB%A1%9D%EC%8B%9C-%ED%8C%A8%ED%84%B4-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@rocker_nun/%ED%94%84%EB%A1%9D%EC%8B%9C-%ED%8C%A8%ED%84%B4-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Tue, 03 Mar 2026 09:39:30 GMT</pubDate>
            <description><![CDATA[<h1 id="👥-프록시란">👥 프록시란?</h1>
<p>일반적으로 클라이언트는 서버에 필요한 것을 요청하고, 서버는 클라이언트의 요청을 처리한다. 근데 여기서 클라이언트가 서버에 바로 요청하지 않고, 제 3자를 통해서 간접적으로 서버에 요청할 수도 있다. 예를 들어, 내가 직접 장을 보러 마트에 갈 수도 있지만, 다른 누군가에게 장을 대신 봐달라 할 수 있는 것과 같다. 이 다른 누군가가 바로 <strong>프록시(Proxy)</strong>다.</p>
<p>&nbsp;</p>
<p>근데 왜 굳이 프록시를 거쳐서 처리하려고 할까? 그 이유는 프록시가 꽤 여러 역할을 수행할 수 있기 때문이다. 프록시가 중간에서 접근 제어나 캐싱, 혹은 다른 부가 기능을 수행할 수도 있고, 프록시가 다른 프록시에게 부탁할 수도 있다. 여기서 중요한 점은 클라이언트는 프록시를 통해 요청했기 때문에 그 이후 과정을 모른다는 것이다.</p>
<p>그렇다면… 클라이언트는 본인이 실제 서버에게 요청하고 있다고 믿도록 속여야 하는데 이를 어떻게 하지? 해결책은 프록시가 서버와 같은 인터페이스를 구현하도록 하는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/8afc3e86-a548-4241-9d8c-6cda23d72228/image.png" alt=""></p>
<p>이렇게 하면 실제 런타임에서 <code>Client</code>에 DI를 사용해 <code>Server</code>를 넣든, <code>Proxy</code>를 넣든 <code>Client</code> 코드에는 전혀 영향을 미치지 않는다. </p>
<p>&nbsp;</p>
<p><strong>프록시를 사용하는 이유</strong>를 간단하게 요약해보자.</p>
<ul>
<li><code>접근 제어</code><ul>
<li>권한에 따른 접근 차단</li>
<li>캐싱</li>
<li>지연 로딩</li>
</ul>
</li>
<li><code>부가 기능 추가</code><ul>
<li>원래 서버가 제공하는 기능에 더해 부가 기능을 수행</li>
<li>ex) 요청 값이나, 응답 값을 중간에 변형</li>
<li>ex) 실행 시간을 측정해서 추가 로그를 남김</li>
</ul>
</li>
</ul>
<p>&nbsp;</p>
<p>이 프록시를 사용하는 디자인 패턴에는 <code>프록시 패턴</code>과 <code>데코레이터 패턴</code>이 있다. 둘 다 프록시를 사용한다는 것은 같지만, 의도가 다르다는 것이 핵심이다. 프록시 패턴은 <strong><em>“접근 제어”</em></strong>, 데코레이터 패턴은 <strong><em>“새로운 기능 추가”</em></strong> 가 목적이다.</p>
<p>&nbsp;</p>
<h1 id="👀-프록시-패턴proxy-pattern">👀 프록시 패턴(Proxy Pattern)</h1>
<p>먼저 간단한 예제를 통해 프록시 패턴을 이해해보자.</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.pureproxy.proxy.code;

public interface Subject {
    String operation();
}</code></pre>
<p>위는 프록시를 적용하기 전의 서버 역할을 할 인터페이스다. 그리고 이 인터페이스를 구현해서 프록시 없이 직접적으로 클라이언트와 서버가 통신하는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/04fb21f6-640a-437e-bed8-32acaf1e2ef2/image.png" alt=""></p>
<p>&nbsp;</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.pureproxy.proxy.code;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class RealSubject implements Subject {

    @Override
    public String operation() {
        log.info(&quot;실제 객체 호출...&quot;);
        sleep(1000);
        return &quot;data&quot;;
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}</code></pre>
<p><code>RealSubject</code> 구현체를 사용해서 로직을 실행하도록 했다.</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.pureproxy.proxy.code;

public class ProxyPatternClient {

    private Subject subject;

    public ProxyPatternClient(Subject subject) {
        this.subject = subject;
    }

    public void execute() {
        subject.operation();
    }
}

// 테스트 코드
package com.example.advanced_spring.proxy.pureproxy.proxy;

import com.example.advanced_spring.proxy.pureproxy.proxy.code.ProxyPatternClient;
import com.example.advanced_spring.proxy.pureproxy.proxy.code.RealSubject;
import org.junit.jupiter.api.Test;

public class ProxyPatternTest {

    @Test
    void noProxyTest() {
        RealSubject realSubject = new RealSubject();
        ProxyPatternClient client = new ProxyPatternClient(realSubject);
        client.execute();
        client.execute();
        client.execute();

        /**
         * 각 요청마다 1초가 걸린 것을 확인할 수 있다.
         * 21:24:00.298 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.proxy.code.RealSubject -- 실제 객체 호출...
         * 21:24:01.301 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.proxy.code.RealSubject -- 실제 객체 호출...
         * 21:24:02.302 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.proxy.code.RealSubject -- 실제 객체 호출...
         */
    }
}</code></pre>
<p>근데 위와 같이 동일한 로직을 수행한 결과를 캐싱한다면 성능이 더 좋아지지 않을까? 이럴 때 프록시를 통해 손쉽게 캐시를 적용할 수 있다. 이제 본격적으로 프록시 패턴을 도입하도록 하자.</p>
<p>&nbsp;</p>
<p> 클래스 의존 관계는 아래와 같다. 실제 타겟 객체와 같은 인터페이스를 프록시가 구현하도록 하는 것이다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/9fb5dbd7-9c8f-4532-9d73-6568caf9035e/image.png" alt=""></p>
<p>&nbsp;</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.pureproxy.proxy.code;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CacheProxy implements Subject {

    private Subject target;  // 실제 호출할 객체의 참조
    private String cacheValue;

    public CacheProxy(Subject target) {
        this.target = target;
    }

    @Override
    public String operation() {
        log.info(&quot;프록시 호출...&quot;);
        if (cacheValue == null) {
            cacheValue = target.operation();
        }

        return cacheValue;
    }
}</code></pre>
<p>위의 <code>CacheProxy</code>는 결국 실제 객체를 호출해야 하기 때문에 실제 호출해야 할 객체의 참조를 가지고 있어야 한다. 그리고 캐시된 데이터가 없다면 실제 객체를 호출해서 값을 구하고 <code>cacheValue</code>에 저장하고 반환하고, 캐시된 데이터가 존재한다면 그대로 <code>cacheValue</code>를 반환해서 매우 빠르게 데이터를 조회할 수 있다.</p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.pureproxy.proxy;

import com.example.advanced_spring.proxy.pureproxy.proxy.code.CacheProxy;
import com.example.advanced_spring.proxy.pureproxy.proxy.code.ProxyPatternClient;
import com.example.advanced_spring.proxy.pureproxy.proxy.code.RealSubject;
import com.example.advanced_spring.proxy.pureproxy.proxy.code.Subject;
import org.junit.jupiter.api.Test;

public class ProxyPatternTest {

        ...

    @Test
    void cacheProxyTest() {
        Subject subject = new RealSubject();
        Subject clientSubject = new CacheProxy(subject);
        ProxyPatternClient client = new ProxyPatternClient(clientSubject);
        client.execute();
        client.execute();
        client.execute();

        /**
         * 21:39:55.619 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.proxy.code.CacheProxy -- 프록시 호출...
         * 21:39:55.621 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.proxy.code.RealSubject -- 실제 객체 호출...
         *
         * (기존 호출을 통해 캐시된 값이 존재하므로 값을 즉시 반환)
         * 21:39:56.623 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.proxy.code.CacheProxy -- 프록시 호출...
         * 21:39:56.624 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.proxy.code.CacheProxy -- 프록시 호출...
         */
    }
}</code></pre>
<p>테스트 결과를 살펴보면 클라이언트는 프록시를 먼저 호출하고, 프록시가 실제 객체를 호출한다. 이후 동일한 로직 결과는 캐시된 데이터를 통해 빠르게 가져오는 것을 확인할 수 있다.</p>
<p>&nbsp;</p>
<h1 id="🎆-데코레이터-패턴decorator-pattern">🎆 데코레이터 패턴(Decorator Pattern)</h1>
<p>데코레이터 패턴도 일단 간단하게 시작해보자. 이번엔 실제 객체가 <code>Component</code>라는 인터페이스를 구현한다. 클라이언트는 단순히 <code>Component</code> 인터페이스를 의존한다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/e7b680fc-f9c0-4ae0-8009-c0c8df3dfafd/image.png" alt=""></p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.pureproxy.decorator.code;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class DecoratorPatternClient {

    private Component component;

    public DecoratorPatternClient(Component component) {
        this.component = component;
    }

    public void execute() {
        String result = component.operation();
        log.info(&quot;result={}&quot;, result);
    }
}</code></pre>
<pre><code class="language-java">package com.example.advanced_spring.proxy.pureproxy.decorator.code;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class RealComponent implements Component {

    @Override
    public String operation() {
        log.info(&quot;RealComponent 실행...&quot;);
        return &quot;data&quot;;
    }
}</code></pre>
<pre><code class="language-java">package com.example.advanced_spring.proxy.pureproxy.decorator;

import com.example.advanced_spring.proxy.pureproxy.decorator.code.Component;
import com.example.advanced_spring.proxy.pureproxy.decorator.code.DecoratorPatternClient;
import com.example.advanced_spring.proxy.pureproxy.decorator.code.RealComponent;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

@Slf4j
public class DecoratorPatternTest {

    @Test
    void noDecorator() {
        Component realComponent = new RealComponent();
        DecoratorPatternClient client = new DecoratorPatternClient(realComponent);
        client.execute();

        /**
         * 22:06:32.422 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.decorator.code.RealComponent -- RealComponent 실행...
         * 22:06:32.424 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.decorator.code.DecoratorPatternClient -- result=data
         */
    }
}</code></pre>
<p>데코레이터 패턴의 의도는 <strong><em>“부가 기능을 추가”</em></strong> 하는 것이다. 이번엔 출력될 메시지를 꾸며주는  <code>MessageDecorator</code>라는 프록시를 만들어보자.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/81f14c0d-7487-4e43-b448-4d4cbcceba57/image.png" alt=""></p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.pureproxy.decorator.code;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class MessageDecorator implements Component {

    private Component component;

    public MessageDecorator(Component component) {
        this.component = component;
    }

    @Override
    public String operation() {
        log.info(&quot;MessageDecorator 실행...&quot;);

        String rawResult = component.operation();
        String decoResult = &quot;*****&quot; + rawResult + &quot;*****&quot;;
        log.info(&quot;MessageDecorator 꾸미기 적용 전={}, 적용 후={}&quot;, rawResult, decoResult);

        return decoResult;
    }
}</code></pre>
<pre><code class="language-java">package com.example.advanced_spring.proxy.pureproxy.decorator;

import com.example.advanced_spring.proxy.pureproxy.decorator.code.Component;
import com.example.advanced_spring.proxy.pureproxy.decorator.code.DecoratorPatternClient;
import com.example.advanced_spring.proxy.pureproxy.decorator.code.MessageDecorator;
import com.example.advanced_spring.proxy.pureproxy.decorator.code.RealComponent;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.message.Message;
import org.junit.jupiter.api.Test;

@Slf4j
public class DecoratorPatternTest {

    @Test
    void noDecorator() {
        Component realComponent = new RealComponent();
        DecoratorPatternClient client = new DecoratorPatternClient(realComponent);
        client.execute();

        /**
         * 22:06:32.422 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.decorator.code.RealComponent -- RealComponent 실행...
         * 22:06:32.424 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.decorator.code.DecoratorPatternClient -- result=data
         */
    }

    @Test
    void decoratorCase1() {
        Component realComponent = new RealComponent();
        MessageDecorator decorator = new MessageDecorator(realComponent);
        DecoratorPatternClient client = new DecoratorPatternClient(decorator);

        client.execute();

        /**
         * 22:19:41.223 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.decorator.code.MessageDecorator -- MessageDecorator 실행...
         * 22:19:41.225 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.decorator.code.RealComponent -- RealComponent 실행...
         * 22:19:41.225 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.decorator.code.MessageDecorator -- MessageDecorator 꾸미기 적용 전=data, 적용 후=*****data*****
         * 22:19:41.226 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.decorator.code.DecoratorPatternClient -- result=*****data*****
         */
    }
}</code></pre>
<p>이번에는 출력될 메시지를 꾸며주는 프록시 이외에 실행 시간을 측정하는 프록시를 추가해보도록 하자. 이런 식으로 여러 개의 프록시를 만들어서 다양한 부가 기능을 수행할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/a96f5f4d-344c-4b9c-b5df-304b6f3840ac/image.png" alt=""></p>
<pre><code class="language-java">package com.example.advanced_spring.proxy.pureproxy.decorator.code;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TimeDecorator implements Component {

    private Component component;

    public TimeDecorator(Component component) {
        this.component = component;
    }

    @Override
    public String operation() {
        log.info(&quot;TimeDecorator 실행...&quot;);
        long startTime = System.currentTimeMillis();

        String result = component.operation();

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info(&quot;TimeDecorator 종료 resultTime={}ms&quot;, resultTime);
        return result;
    }
}</code></pre>
<pre><code class="language-java">package com.example.advanced_spring.proxy.pureproxy.decorator;

import com.example.advanced_spring.proxy.pureproxy.decorator.code.Component;
import com.example.advanced_spring.proxy.pureproxy.decorator.code.DecoratorPatternClient;
import com.example.advanced_spring.proxy.pureproxy.decorator.code.MessageDecorator;
import com.example.advanced_spring.proxy.pureproxy.decorator.code.RealComponent;
import com.example.advanced_spring.proxy.pureproxy.decorator.code.TimeDecorator;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.message.Message;
import org.junit.jupiter.api.Test;

@Slf4j
public class DecoratorPatternTest {

    ...

    @Test
    void decoratorCase2() {
        Component realComponent = new RealComponent();
        MessageDecorator messageDecorator = new MessageDecorator(realComponent);
        TimeDecorator timeDecorator = new TimeDecorator(messageDecorator);
        DecoratorPatternClient client = new DecoratorPatternClient(timeDecorator);

        client.execute();

        /**
         * 22:30:33.623 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.decorator.code.TimeDecorator -- TimeDecorator 실행...
         * 22:30:33.625 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.decorator.code.MessageDecorator -- MessageDecorator 실행...
         * 22:30:33.625 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.decorator.code.RealComponent -- RealComponent 실행...
         * 22:30:33.625 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.decorator.code.MessageDecorator -- MessageDecorator 꾸미기 적용 전=data, 적용 후=*****data*****
         * 22:30:33.626 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.decorator.code.TimeDecorator -- TimeDecorator 종료 resultTime=1ms
         * 22:30:33.626 [Test worker] INFO com.example.advanced_spring.proxy.pureproxy.decorator.code.DecoratorPatternClient -- result=*****data*****
         */
    }
}</code></pre>
<p>보다시피 성공적으로 실행 시간까지 로그로 출력되는 것을 확인할 수 있다.</p>
<p>&nbsp;</p>
<h2 id="🤔-중복되는-부분이-발생하는-것-같은데">🤔 중복되는 부분이 발생하는 것 같은데…</h2>
<p>잘 살펴보면 <code>Decorator</code>에는 일부 중복되는 부분이 존재한다. 프록시는 항상 호출할 객체가 있어야 한다. 위의 메시지를 꾸며주는 <code>MessageDecorator</code>, 실행 시간을 뽑아내는 <code>TimeDecorator</code> 클래스를 보면 내부에 <code>component</code>를 가지고 있는 것을 볼 수 있다. 이런 중복을 제거하기 위해 아예 <code>Decorator</code>를 인터페이스가 아닌 추상 클래스로 설계할 수도 있다. </p>
<p><img src="https://velog.velcdn.com/images/rocker_nun/post/052b89f6-51f4-49e6-aba7-1cf3595dedbb/image.png" alt=""></p>
<p>&nbsp;</p>
<h2 id="🆚-인터페이스-기반-프록시-vs-클래스-기반-프록시">🆚 인터페이스 기반 프록시 vs 클래스 기반 프록시</h2>
<p>꼭 인터페이스가 아니어도 클래스 기반의 프록시를 생성할 수도 있다. 하지만 클래스 기반의 프록시는 상속을 이용하기 때문에 부모 클래스의 생성자를 호출해야 하고, 클래스에 <code>final</code> 키워드가 붙으면 상속이 불가능할 뿐더러 메서드에 <code>final</code>이 붙으면 해당 메서드를 오버라이딩 할 수 없다.</p>
<p>이렇게 상속이라는 제약으로부터 자유롭기 때문에 인터페이스 기반의 프록시를 사용하는 것이다. 프로그래밍 관점에서도 인터페이스를 사용하는 것이 역할과 구현을 명확하게 나눌 수 있기 때문에 훨씬 더 좋다. 구현을 변경할 가능성이 거의 없는 코드에서만 클래스 도입을 고려하도록 하자.</p>
]]></description>
        </item>
    </channel>
</rss>