<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Taemin.log</title>
        <link>https://velog.io/</link>
        <description>Node.js 백엔드 개발자입니다!</description>
        <lastBuildDate>Fri, 03 Apr 2026 02:33:47 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Taemin.log</title>
            <url>https://velog.velcdn.com/images/kwaktaemin_/profile/6289c3eb-ab97-4e2e-9cc1-bfe859a30359/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Taemin.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kwaktaemin_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[TIL] TypeORM @Transactional 데코레이터는 어떻게 동작하지?]]></title>
            <link>https://velog.io/@kwaktaemin_/TIL-TypeORM-Transactional-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%98%EC%A7%80</link>
            <guid>https://velog.io/@kwaktaemin_/TIL-TypeORM-Transactional-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%98%EC%A7%80</guid>
            <pubDate>Fri, 03 Apr 2026 02:33:47 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>NestJS + TypeORM 프로젝트에서 <code>typeorm-transactional</code> 패키지의 <code>@Transactional()</code> 데코레이터를 사용하면, 서로 다른 Repository에서 실행되는 Query들이 하나의 트랜잭션으로 묶이게 된다.</p>
<pre><code class="language-typescript">@Transactional()
async createMasterUser(dto: CreateMasterUserDto) {
    // 서로 다른 repository지만 같은 트랜잭션으로 묶임.
      const savedCompany = await this.companyRepository.save(companyData);
    await this.userRepository.save({companyId: savedCompany.id, ...UserData);
}</code></pre>
<p>logging을 통해 터미널을 보니 하나의 트랜잭션으로 묶이는 걸 보고 어떻게 별도의 Repository 토큰을 가진 객체들이 하나의 트랜잭션을 공유할 수 있는지 궁금해졌다.</p>
<hr>
<h2 id="핵심-asynclocalstorage">핵심: AsyncLocalStorage</h2>
<p><code>typeorm-transactional</code>은 Node.js의 내장 API인 <strong>AsyncLocalStorage(ALS)</strong> 를 사용한다.</p>
<h3 id="asynclocalstorage란">AsyncLocalStorage란?</h3>
<p>AsyncLocalStorage는 <strong>비동기 작업 전체에 걸쳐 컨텍스트를 유지</strong>하는 기능을 제공한다. Java의 ThreadLocal과 비슷한 개념이지만, 싱글 스레드인 Node.js의 비동기 환경에 맞게 설계되엇다.</p>
<pre><code class="language-typescript">import { AsyncLocalStorage } from &#39;async_hooks&#39;;

const asyncLocalStorage = new AsyncLocalStorage();

// run() 내부에서 실행되는 모든 비동기 코드는 동일한 storage에 접근 가능
asyncLocalStorage.run({ requestId: &#39;abc-123&#39; }, async () =&gt; {
    await someAsyncFunction();         // getStore() -&gt; { requestId: &#39;abc-123&#39; }
      await anotherASyncFunction();     // getStore() -&gt; { requestId: &#39;abc-123&#39; }
});</code></pre>
<h4 id="핵심-동작-원리">핵심 동작 원리:</h4>
<ul>
<li><code>run(store, callback)</code>: 새로운 컨텍스트를 생성하고 store를 설정</li>
<li><code>getStore()</code>: 현재 비동기 컨텍스트의 store를 조회</li>
<li>callback 내에서 호출되는 <strong>모든 비동기 작업</strong>에 컨텍스트가 자동 전파됨</li>
</ul>
<hr>
<h2 id="transactional-동작-원리">@Transactional() 동작 원리</h2>
<h3 id="1단계-초기화">1단계: 초기화</h3>
<p>어플리케이션 시작 전에 트랜잭션 컨텍스트를 초기화한다.</p>
<pre><code class="language-typescript">// main.ts
import { initializeTransactionalContext, StoreageDriver, addTransactionalDataSource} from &#39;typeorm-transactional&#39;;

initializeTransactionalContext({ storageDriver: StorageDriver.AUTO });

// DataSource 등록
addTransactionalDataSource(dataSource);</code></pre>
<blockquote>
<p>⚠️ <code>initializeTrasactionalContext()</code> 는 반드시 어플리케이션 초기화 전에 호출해야 한다.</p>
</blockquote>
<h3 id="2단계-transactional-데코레이터">2단계: @Transactional() 데코레이터</h3>
<p>데코레이터가 메서드를 감싸서 트랜잭션 컨텍스트를 생성한다.</p>
<pre><code class="language-typescript">// 내부 동작을 단순화한 의사 코드
function Transactional() {
    return function(target, key, descriptor) {
        const originalMethod = descriptor.value;

      descriptor.value = async function(...args) {
          // 1. 트랜잭션 시작
        return dataSource.transaction(async (trasactionalEntityManager) =&gt; {
          // 2. AsyncLocalStorage에 트랜잭션 EntityManager 저장
          return asyncLocalStorage.run(
            { entityManager: trasactionalEntityManager },
            // 3. 원본 메서드 실행
            () =&gt; originalMethod.apply(this, args)
            );
        });
      };
    };
}</code></pre>
<h3 id="3단계-repository에서-트랜잭션-entitymanager-사용">3단계: Repository에서 트랜잭션 EntityManager 사용</h3>
<p><code>typeorm-transactional</code> 은 DataSource의 메서드들을 <strong>패치(fetch)</strong> 한다.</p>
<pre><code class="language-typescript">// 패치된 repository 메서드 (의사 코드)
async save(entity) {
  // AsyncLocalStorage에서 현재 컨텍스트 store 조회
  const store = asyncLocalStorage.getStore();

  if (store?.entityManager) {
      // 트랜잭션 컨텍스트가 존재하면 해당 EntityManager 사용
    return store.entityManager.getRepository(this.target).save(entity);
  }

  // 없으면 일반 EntityManager 사용
  return originalSave(entity);
}</code></pre>
<hr>
<h2 id="전체-flow">전체 Flow</h2>
<pre><code>1. createMasterUser() 호출
   │
2. @Transactional() 데코레이터가 가로챔
   │
3. DataSource.transaction() 시작
   │  └─ 트랜잭션용 EntityManager 생성
   │
4. AsyncLocalStorage.run(store, callback) 실행
   │  └─ store = { entityManager: 트랜잭션EM }
   │
5. ┌─ callback 내부 (async context 유지) ─────────────────┐
   │                                                    │
   │  Company save() 호출                               │
   │    └─ companyRepository.save()                     │
   │       └─ getStore() → 트랜잭션 EM 획득                │
   │       └─ 트랜잭션 EM으로 INSERT 실행                   │
   │                                                    │
   │  User Save() 호출                                  │
   │    └─ userRepository.save()                        │
   │       └─ getStore() → 동일한 트랜잭션 EM 획득           │
   │       └─ 트랜잭션 EM으로 INSERT 실행                    │
   │                                                    │
   └────────────────────────────────────────────────────┘
   │
6. 성공 → COMMIT / 예외 발생 → ROLLBACK</code></pre><hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>AsyncLocalStorage</strong></td>
<td>비동기 호출 체인 전체에서 컨텍스트(store) 공유</td>
</tr>
<tr>
<td><strong>run()</strong></td>
<td>새로운 컨텍스트 생성, 내부 모든 async 작업에 전파</td>
</tr>
<tr>
<td><strong>getStore()</strong></td>
<td>현재 컨텍스트의 store 조회</td>
</tr>
<tr>
<td><strong>DataSource 패치</strong></td>
<td>repository 메서드들이 자동으로 트랜잭션 EM 사용하도록 변경</td>
</tr>
</tbody></table>
<p><strong>결론</strong>: Repository 토큰이 달라도 같은 <code>DataSource</code>를 사용하고, <code>@Transactional()</code> 내부에서 호출되면 <strong>AsyncLocalStorage를 통해 동일한 트랜잭션 EntityManager를 공유</strong>하게 된다.</p>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://github.com/Aliheym/typeorm-transactional">typeorm-transactional GitHub</a></li>
<li><a href="https://nodejs.org/api/async_context.html">Node.js AsyncLocalStorage 공식 문서</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Spring Boot 4.0에서 Redis 직렬화 설정하기
(Jackson2 -> Jackson3 마이그레이션 )]]></title>
            <link>https://velog.io/@kwaktaemin_/TIL-Spring-Boot-4.0%EC%97%90%EC%84%9C-Redis-%EC%A7%81%EB%A0%AC%ED%99%94-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0Jackson2-Jackson3-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@kwaktaemin_/TIL-Spring-Boot-4.0%EC%97%90%EC%84%9C-Redis-%EC%A7%81%EB%A0%AC%ED%99%94-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0Jackson2-Jackson3-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</guid>
            <pubDate>Mon, 02 Mar 2026 10:43:30 GMT</pubDate>
            <description><![CDATA[<h1 id="spring-boot-40에서-redis-직렬화-설정하기-jackson2-→-jackson3-마이그레이션-삽질기">Spring Boot 4.0에서 Redis 직렬화 설정하기: Jackson2 → Jackson3 마이그레이션 삽질기</h1>
<h2 id="😮💨-시작은-단순한-에러-메시지였다">😮‍💨 시작은 단순한 에러 메시지였다.</h2>
<p>Spring Boot 4.0으로 프로젝트를 마이그레이션을 하는데, Redis 설정에서 낯선 에러를 마주쳤다.</p>
<pre><code>Unresolved reference &#39;DefaultTyping&#39;.</code></pre><p>분명히 이전 버전에서 동작이 잘 되었는데, 4.0으로 버전을 올린 후 왜 <code>DefaultTyping</code> 을 찾을 수 없나는 건지..</p>
<pre><code class="language-kotlin">// 기존 코드 (Spring Boot 3.x)
private fun redisObjectMapper(): ObjectMapper =
    JsonMapper.builder()
    .addModule(KotlinModule.Builder().build())
    .activateDefaultTyping(
        BasicPolymorphicTypeValidator.builder()
            .allowIfSubType(Any::class.java)
            .build(),
        ObjectMapper.DefaultTyping.NON_FINAL // ❗️ 에러 발생!
    )
    .build()</code></pre>
<hr>
<h2 id="🤔-문제의-원인-jackson-2---jackson-3">🤔 문제의 원인: Jackson 2 -&gt; Jackson 3</h2>
<p>Spring Boot 4.0의 가장 큰 변화 중 하나는 <strong>Jackson 2에서 Jackson 3로의 전환</strong>이다.</p>
<h3 id="패키지-변경">패키지 변경</h3>
<table>
<thead>
<tr>
<th>버전</th>
<th>패키지</th>
</tr>
</thead>
<tbody><tr>
<td>Jackson 2.x</td>
<td><code>com.fasterxml.jackson</code></td>
</tr>
<tr>
<td>Jackson 3.x</td>
<td><code>tools.jackson</code></td>
</tr>
</tbody></table>
<h3 id="defaulttyping-위치-변경">DefaultTyping 위치 변경</h3>
<p>Jackson 3에서는 <code>DefaultTyping</code> enum이 <code>ObjectMapper</code> 내부 클래스에서 별도 클래스로 분리가 되었다.</p>
<pre><code class="language-kotlin">// Jackson 2.x
ObjectMapper.DefaultTyping.NON_FINAL

// Jackson 3.x
import tolls.jackson.databind.DefaultTyping
DefaultTyping.NON_FINAL</code></pre>
<hr>
<h2 id="🛠️-첫-번째-시도-defaulttyping의-import-수정">🛠️ 첫 번째 시도: DefaultTyping의 import 수정</h2>
<p>import를 수정하고 다시 빌드를 해보았다.</p>
<pre><code class="language-kotlin">import tools.jackson.databind.DefaultTyping
import tools.jackson.databind.ObjectMapper
import tools.jackson.databind.json.JsonMapper</code></pre>
<p>그런데 이번에는 다른 에러가 발생했다.</p>
<pre><code>None of the following candidates is applicable:
constructor&lt;T : Any!&gt;(mapper: ObjectMapper, type: Class&lt;T!&gt;): Jackson2JsonRedisSerializer&lt;T&gt;
constructor&lt;T : Any!&gt;(mapper: ObjectMapper, javaType: JavaType): Jackson2JsonRedisSerializer&lt;T&gt;</code></pre><p><code>Jackson2JsonRedisSerializer</code>가 <code>tools.jackson.databind.ObjectMapper</code>를 받지 않는 것 같았다.</p>
<hr>
<h2 id="🤔-두-번째-문제-serializer-호환성">🤔 두 번째 문제: Serializer 호환성</h2>
<p>API 문서를 찾아보니</p>
<blockquote>
<p><code>@Deprecated(since=&quot;4.0&quot;, forRemoval=true)</code>
<code>public class Jackson2JsonRedisSerializer&lt;T&gt;</code></p>
</blockquote>
<p><strong><code>Jackson2JsonRedisSerializer</code>가 Spring Boot Redis 4.0에서 Deprecated가 되었다.</strong></p>
<h3 id="새로운-serializer-등장">새로운 Serializer 등장</h3>
<p>Spring Data Redis 4.0에서는 Jackson 3 전용 Serializer가 새로 추가되었다.</p>
<table>
<thead>
<tr>
<th>Serializer</th>
<th>Jackson 버전</th>
<th>상태</th>
</tr>
</thead>
<tbody><tr>
<td><code>Jackson2JsonRedisSerializer</code></td>
<td>Jackson 2.x (<code>com.fasterxml.jackson</code>)</td>
<td><strong>Deprecated</strong></td>
</tr>
<tr>
<td><code>JacksonJsonRedisSerializer</code></td>
<td>Jackson 3.x (<code>tools.jackson</code>)</td>
<td><strong>권장</strong></td>
</tr>
</tbody></table>
<p>클래스 이름에서 <code>2</code>가 빼진 것이 핵심이다.</p>
<hr>
<h2 id="🔥-최종-해결-jacksonjsonredisserializer-사용">🔥 최종 해결: JacksonJsonRedisSerializer 사용</h2>
<h3 id="완성된-redisconfig">완성된 RedisConfig</h3>
<pre><code class="language-kotlin">package com.example.dotzip.common.config

import org.springframework.beans.factory.annotation.Value
import org.springframework.cache.annotation.EnableCaching
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.cache.RedisCacheConfiguration
import org.springframework.data.redis.cache.RedisCacheManager
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.core.StringRedisTemplate
import org.springframework.data.redis.serializer.JacksonJsonRedisSerializer
import org.springframework.data.redis.serializer.RedisSerializationContext
import org.springframework.data.redis.serializer.StringRedisSerializer
import tools.jackson.databind.ObjectMapper
import tools.jackson.databind.json.JsonMapper
import tools.jackson.module.kotlin.KotlinModule

@Configuration
@EnableCaching
class RedisConfig {

    @Value(&quot;\${spring.data.redis.host}&quot;)
    private lateinit var host: String

    @Value(&quot;\${spring.data.redis.port}&quot;)
    private var port: Int = 6379

    @Bean
    fun redisConnectionFactory(): RedisConnectionFactory {
        return LettuceConnectionFactory(host, port)
    }

    @Bean
    fun redisObjectMapper(): ObjectMapper =
        JsonMapper.builder()
            .addModule(KotlinModule.Builder().build())
            .build()

    @Bean
    fun redisCacheManager(
        connectionFactory: RedisConnectionFactory,
        redisObjectMapper: ObjectMapper
    ): RedisCacheManager {
        val serializer = JacksonJsonRedisSerializer(redisObjectMapper, Any::class.java)

        val cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
            .serializeKeysWith(
                RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer())
            )
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer&lt;Any&gt;(serializer)
            )

        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(cacheConfig)
            .build()
    }

    @Bean
    fun redisTemplate(
        connectionFactory: RedisConnectionFactory,
        redisObjectMapper: ObjectMapper
    ): RedisTemplate&lt;String, Any&gt; {
        val serializer = JacksonJsonRedisSerializer(redisObjectMapper, Any::class.java)

        return RedisTemplate&lt;String, Any&gt;().apply {
            setConnectionFactory(connectionFactory)
            keySerializer = StringRedisSerializer()
            valueSerializer = serializer
            hashKeySerializer = StringRedisSerializer()
            hashValueSerializer = serializer
        }
    }

    @Bean
    fun stringRedisTemplate(connectionFactory: RedisConnectionFactory): StringRedisTemplate {
        return StringRedisTemplate(connectionFactory)
    }
}</code></pre>
<hr>
<h2 id="📋-마이그레이션-체크리스트">📋 마이그레이션 체크리스트</h2>
<p>Spring Boot 3.x -&gt; 4.0 Redis 설정 마이그레이션 시 확인해야 할 사항들:</p>
<h3 id="1-jackson-패키지-변경">1. Jackson 패키지 변경</h3>
<pre><code class="language-kotlin">// Before (Jackson 2)
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule

// After (Jackson 3)
import tools.jackson.databind.ObjectMapper
import tools.jackson.module.kotlin.KotlinModule</code></pre>
<h3 id="2-defaulttyping-위치-변경-필요한-경우">2. DefaultTyping 위치 변경 (필요한 경우)</h3>
<pre><code class="language-kotlin">dependencies {
    implementation(&quot;tools.jackson.module:jackson-module-kotlin&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-data-redis&quot;)
}</code></pre>
<hr>
<h2 id="💡-삽질을-줄이기">💡 삽질을 줄이기</h2>
<h3 id="1-공식-마이그레이션-가이드를-먼저-확인해보기">1. 공식 마이그레이션 가이드를 먼저 확인해보기.</h3>
<p>메이저 버전 업그레이드 시 항상 공식 마이그레이션 가이드를 먼저 읽어보는 습관을 들이기로 한다.</p>
<ul>
<li><a href="https://github.com/FasterXML/jackson/blob/main/jackson3/MIGRATING_TO_JACKSON_3.md">Jackson 3 Migration Guide</a></li>
<li><a href="https://github.com/spring-projects/spring-boot/wiki">Spring Boot 4.0 Release Notes</a></li>
</ul>
<h3 id="2-api-문서에서-deprecated-확인">2. API 문서에서 Deprecated 확인</h3>
<p>에러가 발생하면 해당 클래스의 API 문서를 확인해보고 Deprecated 여부와 대체 클래스를 확인해 볼 수 있다.</p>
<ul>
<li><a href="https://docs.spring.io/spring-data/redis/docs/current/api/">Spring Data Redis API Docs</a></li>
</ul>
<hr>
<h2 id="🎯-정리">🎯 정리</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>Spring Boot 3.x</th>
<th>Spring Boot 4.0</th>
</tr>
</thead>
<tbody><tr>
<td>Jackson 버전</td>
<td>2.x</td>
<td>3.x</td>
</tr>
<tr>
<td>패키지</td>
<td><code>com.fasterxml.jackson</code></td>
<td><code>tools.jackson</code></td>
</tr>
<tr>
<td>Redis Serializer</td>
<td><code>Jackson2JsonRedisSerializer</code></td>
<td><code>JacksonJsonRedisSerializer</code></td>
</tr>
<tr>
<td>DefaultTyping</td>
<td><code>ObjectMapper.DefaultTyping</code></td>
<td><code>tools.jackson.databind.DefaultTyping</code></td>
</tr>
</tbody></table>
<p>Spring Boot 4.0 마이그레이션은 생각보다 많은 변화가 있었다. 특히 Jackson 3 전환은 Redis, REST API, 메시지 직렬화 등 다양한 곳에 영향을 미치니 꼼꼼히 확인하는게 좋을 거 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Kotlin const val과 val의 차이점]]></title>
            <link>https://velog.io/@kwaktaemin_/WIL-Kotlin-const-val%EA%B3%BC-val%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@kwaktaemin_/WIL-Kotlin-const-val%EA%B3%BC-val%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Thu, 26 Feb 2026 04:00:30 GMT</pubDate>
            <description><![CDATA[<h1 id="❓kotlin에서-const-val과-val의-차이점">❓Kotlin에서 const val과 val의 차이점</h1>
<p>Kotlin에서 <code>const val</code>과 <code>val</code>은 모두 재할당이 불가능한 값이다.
하지만 두 키워드는 <strong>컴파일 타임 상수 여부</strong>와 <strong>JVM 바이트코드 생성 방식</strong>에서 큰 차이가 있다.</p>
<hr>
<h2 id="1-val">1. <code>val</code></h2>
<p><code>val</code>은 <strong>읽기 전용 프로퍼티 (read-only property)</strong> 이다.</p>
<pre><code class="language-kotlin">val name = &quot;John&quot;</code></pre>
<ul>
<li>재할당은 불가능</li>
<li>하지만 <strong>컴파일 타임 상수가 아님</strong></li>
<li>런타임에 계산되는 값도 가능</li>
</ul>
<pre><code class="language-kotlin">val currentTime = System.currentTimeMillis()</code></pre>
<hr>
<h2 id="2-const-val-이란">2. <code>const val</code> 이란?</h2>
<p><code>const val</code>은 <strong>컴파일 타임 상수 (compile-time constant)</strong> 이다.</p>
<ul>
<li>반드시 <code>val</code>
= top-level 또는 <code>object</code> / <code>companion object</code> 내부</li>
<li>primitive 타입 또는 String</li>
<li>getter 불가</li>
<li>컴파일 시점에 값이 확정됨.</li>
</ul>
<pre><code class="language-kotlin">const val MAX_COUNT = 10</code></pre>
<hr>
<h2 id="3-jvm-관점에서-차이">3. JVM 관점에서 차이</h2>
<p>Kotlin은 JVM 위에서 동작하므로
결국 Java 바이트코드로 변환된다.</p>
<h3 id="31-object-안에서-val-사용">3.1 object 안에서 <code>val</code> 사용</h3>
<pre><code class="language-kotlin">object Constants {
    val NAME = &quot;John&quot;
}</code></pre>
<h4 id="java로-디컴파일-할-경우">Java로 디컴파일 할 경우</h4>
<pre><code class="language-java">public final class Constants {
    @NotNull
    private static final String NAME = &quot;John&quot;;

    @NotNull
    public final String getNAME() {
        return NAME;
    }

    public static final Constants INSTANCE;

    static {
        INSTANCE = new Constants();
    }
}</code></pre>
<h4 id="상수를-사용했을-때">상수를 사용했을 때</h4>
<pre><code class="language-kotlin">println(Constants.NAME)</code></pre>
<p>→ Java에서는</p>
<pre><code class="language-java">Constants.INSTANCE.getNAME();</code></pre>
<p>즉,</p>
<ul>
<li>getter 호출</li>
<li>INSTANCE 접근 필요</li>
<li>값이 인라인되지 않음</li>
</ul>
<hr>
<h3 id="32-object-안에서-const-val-사용">3.2 object 안에서 <code>const val</code> 사용</h3>
<pre><code class="language-kotlin">object Constants {
    const val NAME = &quot;John&quot;
}</code></pre>
<h4 id="java로-디컴파일-할-경우-1">Java로 디컴파일 할 경우</h4>
<pre><code class="language-java">public final class Constants {
    public static final String NAME = &quot;John&quot;;
}</code></pre>
<p>getter가 없다.</p>
<p>그리고 사용된 코드에서는</p>
<pre><code class="language-java">System.out.println(&quot;John&quot;);</code></pre>
<p>→ 값이 직접 인라인이 되다.</p>
<hr>
<h2 id="4-이런-차이가-발생하는-이유">4. 이런 차이가 발생하는 이유</h2>
<p><code>const val</code>은 JVM에서 다음과 같이 처리된다.</p>
<pre><code>public static final</code></pre><p>그리고 <strong>compile-time constant</strong>여서
사용하는 쪽 클래스의 constant pool에 값이 복사된다.</p>
<p>즉,</p>
<pre><code class="language-kotlin">println(Constants.NAME)</code></pre>
<p>은 실제 바이트코드에서</p>
<pre><code class="language-java">println(&quot;John&quot;);</code></pre>
<p>이 된다.</p>
<p>반면 <code>val</code>은</p>
<ul>
<li>객체 프로퍼티</li>
<li>getter 필요</li>
<li>INSTANCE 접근 필요</li>
<li>인라인이 되지 않음</li>
</ul>
<hr>
<h2 id="🤔-5-ide가-const-val을-추천하는-이유">🤔 5. IDE가 <code>const val</code>을 추천하는 이유</h2>
<p>IntelliJ에서 <code>val</code>보다 <code>const val</code>을 추천한다.</p>
<ul>
<li>object 내부</li>
<li>primitive / String 타입</li>
<li>값이 리터럴</li>
</ul>
<p>이유는 아래와 같다.</p>
<ol>
<li>불필요한 getter 제거</li>
<li>INSTANCE 접근 제거</li>
<li>바이트코드 단순화</li>
<li>성능에 대한 이점</li>
<li>어노테이션에서 사용 가능</li>
</ol>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Trouble Shooting] # Spring Boot Kafka 테스트 시 "Address already in use" 문제]]></title>
            <link>https://velog.io/@kwaktaemin_/Trouble-Shooting-Spring-Boot-Kafka-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%8B%9C-Address-already-in-use-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@kwaktaemin_/Trouble-Shooting-Spring-Boot-Kafka-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%8B%9C-Address-already-in-use-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Tue, 23 Dec 2025 01:06:45 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>Spring Book를 이용해서 Kafka 관련 테스트 코드를 실행하는데 아래와 같은 에러가 발생했다.</p>
<pre><code>Caused by: org.apache.kafka.common.KafkaException: Socket server failed to bind to localhost:9092: Address already in use.

Caused by: java.util.concurrent.CompletionException: java.lang.RuntimeException: Unable to start acceptor for ListenerName(PLAINTEXT)</code></pre><p>###</p>
<p><strong>docker-compose로 kafka 실행 중</strong></p>
<pre><code class="language-yaml">services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.5.0
    container_name: hhplus-zookeeper
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    ports:
      - &quot;2181:2181&quot;
    networks:
      - test-network
    restart: unless-stopped

  kafka:
    image: confluentinc/cp-kafka:7.5.0
    container_name: hhplus-kafka
    depends_on:
      - zookeeper
    ports:
      - &quot;9092:9092&quot;
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_AUTO_CREATE_TOPICS_ENABLE: &quot;true&quot;
    networks:
      - test-network
    restart: unless-stopped

networks:
  test-network:
    driver: bridge</code></pre>
<h4 id="테스트-코드">테스트 코드</h4>
<pre><code class="language-kotlin">```kotlin
@DataJpaTest
@ComponentScan(basePackages = [&quot;com.hhplus.ecommerce&quot;])
@EmbeddedKafka(
    partitions = 1,
    topics = [&quot;order-created&quot;, &quot;payment-completed&quot;, &quot;coupon-issued&quot;],
    brokerProperties = [&quot;listeners=PLAINTEXT://localhost:9092&quot;, &quot;port=9092&quot;]
)
@TestPropertySource(
    properties = [
        &quot;spring.kafka.enabled=true&quot;,
        &quot;spring.kafka.bootstrap-servers=localhost:9092&quot;,
        &quot;spring.kafka.consumer.group-id=test-group&quot;
    ]
)
class KafkaTest {
    // ...
}</code></pre>
<hr>
<h2 id="문제-원인">문제 원인</h2>
<p><strong>port가 충돌</strong>이 돼서 두 개의 kafka 인스턴스가 동일한 9092 port를 사용하려고 했음</p>
<ol>
<li><strong>Docker Compose의 Kafka</strong>: <code>localhost:9092</code> port 사용</li>
<li><strong>@EmbeddedKafka</strong>: 테스트 실행 시 <code>localhost:9092</code> port 사용하려고 시도</li>
</ol>
<h3 id="잘못된-접근">잘못된 접근</h3>
<p>처음에는 &quot;테스트를 위해서 Kafka가 필요해서 Docker로 Kafka를 띄워놓자&quot;라는 생각으로 docker-compose로 <code>kafka</code>, <code>zookeeper</code> 를 실행했다.</p>
<p>그런데 <code>@EmbeddedKafka</code>도 함께 사용하면서 <strong>동일한 port를 두번 바인딩하려는 문제</strong>가 발생했다.</p>
<hr>
<h2 id="해결-방법">해결 방법</h2>
<h3 id="✅-방법-1-embeddedkafka를-제거하고-docker-kafka-사용">✅ 방법 1: @EmbeddedKafka를 제거하고 Docker kafka 사용</h3>
<p>Docker Compose로 이미 Kafka를 띄웠다면, <code>@EmbeddedKafka</code>를 제거하고 Docker Kafka를 테스트에서도 사용하는 것이 좋습니다.</p>
<pre><code class="language-kotlin">@DataJpaTest
@ComponentScan(basePackages = [&quot;com.hhplus.ecommerce&quot;])
// @EmbeddedKafka 제거!
@TestPropertySource(
    properties = [
        &quot;spring.jpa.hibernate.ddl-auto=create-drop&quot;,
        &quot;spring.datasource.url=jdbc:h2:mem:testdb&quot;,
        &quot;spring.datasource.driver-class-name=org.h2.Driver&quot;,
        &quot;spring.jpa.database-platform=org.hibernate.dialect.H2Dialect&quot;,
        &quot;spring.kafka.enabled=true&quot;,
        &quot;spring.kafka.bootstrap-servers=localhost:9093&quot;,  // Docker의 호스트 포트 사용
        &quot;spring.kafka.consumer.group-id=test-group&quot;
    ]
)
class KafkaTest {
    // Docker Kafka 사용
}</code></pre>
<p><strong>주의사항</strong>: Docker Compose의 <code>PLAINTEXT_HOST://localhost:9093</code> 리스너를 사용한다.</p>
<p><strong>장점</strong></p>
<ul>
<li>실제 운영 환경과 유사한 통합 테스트</li>
<li>Docker Compose 설정을 테스트에서도 재사용</li>
<li>별도의 Kafka 인스턴스 관리 불필요</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>테스트 전에 Docker Kafka가 실행 중이어야 함</li>
<li>CI/CD 환경에서 Docker 설정 필요</li>
</ul>
<h3 id="✅-방법-2-docker-중지하고-embeddedkafka만-사용">✅ 방법 2: Docker 중지하고 @EmbeddedKafka만 사용</h3>
<p>완전히 격리된 테스트 환경을 원한다면 Docker를 내리고 <code>@EmbeddedKafka</code>만 사용한다.</p>
<pre><code class="language-bash"># 테스트 전
docker-compose down

# 테스트 실행
./gradlew test

# 로컬 개발 시 다시 시작
docker-compose up -d</code></pre>
<p><strong>장점</strong></p>
<ul>
<li>완전히 독립적인 테스트 환경</li>
<li>외부 의존성 없이 테스트 실행 가능</li>
<li>CI/CD 환경에서도 동일하게 동작</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>테스트할 때마다 Docker를 내렸다 올려야 함</li>
<li>로컬 개발과 테스트 환경 전환 번거로움</li>
</ul>
<h2 id="결론">결론</h2>
<p><strong>Docker Compose로 Kafka를 띄워놨다면, <code>@EmbeddedKafka</code>를 제거하고 Docker Kafka를 테스트에서 사용한다.</strong></p>
<p>핵심은 <strong>한 가지 Kafka 인스턴스만 사용</strong>하는 것이다. 두 개를 동시에 사용하려고 하면 포트 충돌이 발생하기 때문이다.</p>
<h3 id="선택-기준">선택 기준</h3>
<table>
<thead>
<tr>
<th>상황</th>
<th>추천 방법</th>
</tr>
</thead>
<tbody><tr>
<td>Docker 환경 구축되어 있음</td>
<td>Docker Kafka 사용</td>
</tr>
<tr>
<td>완전히 격리된 단위 테스트</td>
<td>@EmbeddedKafka 사용</td>
</tr>
<tr>
<td>CI/CD 환경에서만 테스트</td>
<td>@EmbeddedKafka 사용</td>
</tr>
<tr>
<td>로컬 개발 + 통합 테스트</td>
<td>Docker Kafka 사용</td>
</tr>
</tbody></table>
<h2 id="참고-멀티-포트-설정의-의미">참고: 멀티 포트 설정의 의미</h2>
<p>Docker Compose에서 두 개의 포트를 노출한 이유:</p>
<pre><code class="language-yaml">ports:
  - &quot;9092:9092&quot;  # 컨테이너 간 통신용 (PLAINTEXT://kafka:9092)
  - &quot;9093:9093&quot;  # 호스트 접근용 (PLAINTEXT_HOST://localhost:9093)</code></pre>
<ul>
<li><strong>9092</strong>: Docker 네트워크 내부에서 사용 (컨테이너끼리 통신)</li>
<li><strong>9093</strong>: 호스트(로컬 PC)에서 접근할 때 사용 (애플리케이션 → Kafka)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kafka] MacOS에서 🛠️ Apache Kafka 로컬 환경 실행 및 CLI 실습 가이드]]></title>
            <link>https://velog.io/@kwaktaemin_/Kafka-MacOS%EC%97%90%EC%84%9C-Apache-Kafka-%EB%A1%9C%EC%BB%AC-%ED%99%98%EA%B2%BD-%EC%8B%A4%ED%96%89-%EB%B0%8F-CLI-%EC%8B%A4%EC%8A%B5-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@kwaktaemin_/Kafka-MacOS%EC%97%90%EC%84%9C-Apache-Kafka-%EB%A1%9C%EC%BB%AC-%ED%99%98%EA%B2%BD-%EC%8B%A4%ED%96%89-%EB%B0%8F-CLI-%EC%8B%A4%EC%8A%B5-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Sun, 14 Dec 2025 04:28:32 GMT</pubDate>
            <description><![CDATA[<h1 id="🛠️-apache-kafka-로컬-환경-실행-및-cli-실습-가이드">🛠️ Apache Kafka 로컬 환경 실행 및 CLI 실습 가이드</h1>
<p>이 글에서는 Apache Kafka를 로컬 환경에 띄우고, CLI를 사용하여 토픽 생성, 메시지 발생 및 구독을 직접 실습하는 방법을 적어보려고 한다.</p>
<p>(이 가이드는 <strong>Docker Descktop</strong>이 설치된 MacOS 환경을 기준으로 작성되어있다.)</p>
<h2 id="1-환경-설정-docker-compose-kafka-클러스터-실행">1. 환경 설정: Docker-Compose Kafka 클러스터 실행</h2>
<p>카프카는 안정적인 실행을 위해 메터데이터를 관리하는 Zookeper와 함께 구동된다. Docker를 사용하여 이 두 서버를 동시에 실행한다.</p>
<h3 id="11-docker-composeyml-파일-작성">1.1. <code>docker-compose.yml</code> 파일 작성</h3>
<p>작업 폴더를 생성한 후, 아래 내용을 <code>docker-compose.yml</code> 파일을 작성한다. 이 설정은 카프카 브로커를 <code>localhost:9092</code> 포트로 외부에 노출한다.</p>
<pre><code class="language-yaml">services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.5.0
    container_name: zookeeper
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000

  kafka:
    image: confluentinc/cp-kafka:7.5.0
    container_name: kafka
    depends_on:
      - zookeeper
    ports:
      - &quot;9092:9092&quot; # 호스트 포트: 컨테이너 포트
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1</code></pre>
<h3 id="12-컨테이너-실행">1.2. 컨테이너 실행</h3>
<p>터미널에서 <code>docker-compose.yml</code> 파일이 있는 디렉토리로 이동하여 실행한다.</p>
<pre><code class="language-bash"># 백그라운드에서 컨테이너 실행
docker-compose up -d

# 실행 상태 확인
docker ps</code></pre>
<blockquote>
<p>💡 확인: <code>kafka</code>와 <code>zookeeper</code> 두 컨네이터 상태가 <code>Up</code>으로 표시되면 성공.</p>
</blockquote>
<hr>
<h2 id="2-cli를-이용하여-실시간-메시지-흐름-실습">2. CLI를 이용하여 실시간 메시지 흐름 실습</h2>
<p>컨테이너가 정상적으로 실행되었다면, 이제 카프카의 기본 명령어 도구를 사용하여 메시지 흐름을 알아보도록하자. 모든 명령은 카프카 컨테이너 내부에서 실행된다.</p>
<h3 id="21-컨테이너-접속-및-토픽-생성">2.1. 컨테이너 접속 및 토픽 생성</h3>
<p>메시지를 주고받기 위해 <strong>study-topic</strong>이라는 이름의 토픽을 먼저 생성한다.</p>
<pre><code class="language-bash"># 카프카 컨테이너 내부 쉘 접속
docker exec -it kafka /bin/bash

# 토픽 생성 명령어
kafka-topics --create --topic study-topic --bootstrap-server localhost:9092 --replication-factor 1</code></pre>
<h3 id="22-consumper-구독자-실행">2.2. Consumper (구독자) 실행</h3>
<p>새로운 터미널 탭을 열고, 컨테이너에 다시 접속한 뒤 Consumer를 실행한다. Consumer는 <code>--from-beginnig</code> 옵션으로 기존에 저장된 메시지부터 읽기 시작한다.</p>
<pre><code class="language-bash"># 새 터미널 탭에서 카프카 컨테이너 접속
docker exec -it kafka /bin/bash

# Consumer 실행 (구독 시작)
kafka-console-consumer --topic study-topic --from-beginning --bootstrap-server localhost:9092
# 이제 이 창은 메시지를 기다리는 상태가 된다.</code></pre>
<h3 id="23-producer-발행자-실행">2.3. Producer (발행자) 실행</h3>
<p>또 다른 새로운 터미널 탭을 열고, Producer를 실행하여 메시지를 발행한다.</p>
<pre><code class="language-bash"># 세 번째 터미널 탭에서 카프카 컨테이너 접속
docker exec -it kafka /bin/bash

# Producer 실행 (메시지 발행 시작)
kafka-console-producer --topic study-topic --bootstrap-server localhost:9092</code></pre>
<p>📌 <strong>실습 및 확인</strong></p>
<ol>
<li>Producer 터미널에 메시지를 입력하고 <strong>Enter</strong>.</li>
<li>메시지를 입력하는 즉시 Consumer 터미널에 메시지가 출력되는 것을 확인.</li>
<li><strong>Consumper 터미널을 종료하지 않은 채</strong> Producer로 메시지를 계속 보내보면 이것이 <strong>실시간 스트리밍</strong>의 핵심이라고 알 수 있다.</li>
</ol>
<h3 id="24-영속성-durability-확인">2.4. 영속성 (Durability) 확인</h3>
<p>Consumper를 종료한 후, Producer로 메시지를 5개 더 보내본다. 그리고 Consumper를 다시 실행한다.</p>
<pre><code class="language-bash"># Consumer를 다시 실행 (새로운 Consumer)
docker exec -it kafka /bin/bash
kafka-console-consumer --topic study-topic --from-beginning --bootstrap-server localhost:9092</code></pre>
<blockquote>
<p>💡 확인: Consumer가 꺼져있을 때 전송했던 5개의 메시지를 포함하여 모든 메시지가 다시 출력된다.
이는 카프카가 메시지를 <strong>디스크에 안전하게 저장(영속성)</strong> 하기 때문이다.</p>
</blockquote>
<hr>
<h2 id="3-실습-환경-정리">3. 실습 환경 정리</h2>
<p>실습을 마쳤다면, 불필요한 리소스 사용을 막기 위해 컨테이너를 종료한다.</p>
<pre><code class="language-bash"># docker-compose.yml이 있는 폴더에서 실행
docker-compose down</code></pre>
<p>이제 카프카의 기본적인 메시지 발생/구독 흐름을 이해하게 됐다. 실제 어플리케이션에 카프카를 적용하여 사용해보면서 심화적인 과정을 배우면 될 거 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kafka] Apache Kafka에 대한 개념 정리]]></title>
            <link>https://velog.io/@kwaktaemin_/Kafka-Apache-Kafka%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@kwaktaemin_/Kafka-Apache-Kafka%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 14 Dec 2025 03:59:41 GMT</pubDate>
            <description><![CDATA[<h1 id="apache-kafka-기본-개념-정리">Apache Kafka 기본 개념 정리</h1>
<h2 id="1-kafka의-정의-및-특징">1. kafka의 정의 및 특징</h2>
<p>카프카는 대규모 데이터를 실시간으로 처리를 하기 위해 설계된 <strong>분산 이벤트 스트리밍 플랫폼</strong>이다.</p>
<table>
<thead>
<tr>
<th align="left">특징</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>메시지 브로커</strong></td>
<td align="left">서비스 간 데이터를 주고받을 때 중간에서 관리해주는 중개자 역할을 한다.</td>
</tr>
<tr>
<td align="left"><strong>고가용성 (HA)</strong></td>
<td align="left">클러스터 내의 데이터를 복제(Replication)하여 특정 서버에 장애가 발생해도 서비스가 유지된다.</td>
</tr>
<tr>
<td align="left"><strong>비동기 처리</strong></td>
<td align="left">Producer가 메시지를 보내고 Consumer가 응답할 때까지 기다리지 않아 시스템 성능이 향상된다.</td>
</tr>
<tr>
<td align="left"><strong>분산 시스템</strong></td>
<td align="left">여러 대의 서버(Broker)에 데이터를 분산 저장하여 확장성(Scale-out)이 뛰어나다.</td>
</tr>
<tr>
<td align="left"><strong>영속성 (Durability)</strong></td>
<td align="left">데이터를 메모리가 아닌 <strong>디스크</strong>에 저장하며, 설정된 기간 동안 데이터를 보관한다.</td>
</tr>
</tbody></table>
<hr>
<h2 id="2-kafka의-주요-구성-요소">2. kafka의 주요 구성 요소</h2>
<h3 id="broker-브로커">Broker (브로커)</h3>
<ul>
<li>kafka 클러스터를 구성하는 개별 서버다.</li>
<li>데이터를 수신, 저장하고 consumer에게 전달하는 핵심 역할을 한다.</li>
</ul>
<h3 id="topic--partition-토픽과-파티션">Topic &amp; Partition (토픽과 파티션)</h3>
<ul>
<li><strong>Topic</strong>: 데이터가 저장되는 카테고리(이름표). (예: <code>user-login-logs</code>)</li>
<li><strong>Partition</strong>: 하나의 토픽을 여러 개로 나눈 물리적인 저장 단위.<ul>
<li>partition을 통해 병렬 처리가 가능해지고, 데이터는 파티션 내에서 <strong>Offset</strong> 순서대로 저장된다.</li>
</ul>
</li>
</ul>
<h3 id="producer--consumer-프로듀서--컨슈머">Producer / Consumer (프로듀서 / 컨슈머)</h3>
<ul>
<li><strong>Producer</strong>: 메시지를 생성하여 특정 토픽으로 발행(Publish)하는 주체.</li>
<li><strong>Consumer</strong>: 토픽을 구독(Subscribe)하여 데이터를 가져와서 처리하는 주체.</li>
<li><strong>Consumer Group</strong>: 여러 consumper가 협력하여 하나의 토픽을 병렬로 처리할 수 있게 돕는 단위.</li>
</ul>
<hr>
<h2 id="3-왜-대용량-시스템에서-kafka를-사용할까">3. 왜 대용량 시스템에서 kafka를 사용할까?</h2>
<p>대용량 시스템에서 kafka가 필수적인 이유는 아래와 같다.</p>
<ol>
<li><strong>압도적인 처리량 (High Throughput)</strong><ul>
<li>일반적인 메시지 큐와 달리 디스크 순차 쓰기(Sequential I/O)와 Zero-copy 기술을 사용하여 초당 수백만 건의 데이터를 처리할 수 있다.</li>
</ul>
</li>
<li><strong>느슨한 결합 (Decoupling)</strong><ul>
<li>데이터를 보내는 곳과 받는 곳이 서로의 존재를 몰라도 된다. kafka가 중간 큐 역할을 해주므로 시스템 간 의존성이 낮아진다.</li>
</ul>
</li>
<li><strong>데이터 유실 방지</strong><ul>
<li>데이터를 디스크에 물리적으로 저장하고 복제본을 유지하기 때문에, 갑작스러운 서버 다운에도 데이터가 안전하게 보존된다.</li>
</ul>
</li>
<li><strong>유연한 확장성</strong><ul>
<li>트래픽이 늘어나면 브로커 서버를 추가하거나 파티션을 늘리는 방식으로 아주 쉽게 성능을 확장할 수 있다.</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] 이커머스 프로젝트로 알아보는 동시성 제어]]></title>
            <link>https://velog.io/@kwaktaemin_/Spring-Boot-%EC%9D%B4%EC%BB%A4%EB%A8%B8%EC%8A%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4</link>
            <guid>https://velog.io/@kwaktaemin_/Spring-Boot-%EC%9D%B4%EC%BB%A4%EB%A8%B8%EC%8A%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4</guid>
            <pubDate>Thu, 20 Nov 2025 23:59:43 GMT</pubDate>
            <description><![CDATA[<h3 id="왜-동시성-제어인가">왜 동시성 제어인가?</h3>
<p>이커머스 서비스를 개발하다 보면 가장 신경 쓰이는 부분은 바로 <strong>데이터 정합성</strong>이다. 혼자 테스트를 통해서 실행했던 기능들이 수천 명이 동시에 &quot;결제&quot;나 &quot;선착순 쿠폰 발급&quot;을 받으려고 할 때 순식간에 무너질 수 있다.</p>
<p>그래서 공부하면서 진행한 이커머스 프로젝트에서 발생한 동시성 이슈를 해결하기 위해 <strong>비관적 락(Perssimistic Lock), 낙관적 락(Optimistic Lock), 그리고 분산 락(Distributed Lock)</strong> 을 어떻게 적용했는지, 그리고 분산 락 적용 시 겪었던 트랜잭션 범위 문제를 어떻게 해결했는지 정리하려고 한다.</p>
<hr>
<h3 id="동시성-제저-기법-비교-개념편">동시성 제저 기법 비교 (개념편)</h3>
<p>프로젝트에 적용하기 앞서, 세 가지 락의 특징을 간단히 짚고 넘어가자면</p>
<h4 id="비관적-락-perssimistic-lock">비관적 락 (Perssimistic Lock)</h4>
<ul>
<li>개념: 충돌이 발생할 수 있다는 상황을 비관적으로 가정하면서 데이터를 읽을 때부터 DB Lock을 걸어버리는 방식이다. (<code>SELECT ... FOR UPDATE</code>)</li>
<li>장점: 데이터 정합성을 강력하게 보장한다.</li>
<li>단점: 락을 잡고 있는 동안 다른 트랜잭션이 대기를 해야하기 때문에 성능 저하가 발생할 수 있고, Deadlcok 위험이 있다.</li>
<li>적용처: 주문, 결제 (데이터의 절대적인 무결성이 속도보다 중요하다고 판단)</li>
</ul>
<h4 id="낙관적-락-optimistic-lock">낙관적 락 (Optimistic Lock)</h4>
<ul>
<li>개념: 여러 서버(또는 인스턴스)가 공통으로 사용하는 저장소(주로 Redis)를 이용해서 락을 제어.</li>
<li>장점: DB 부하를 줄일 수 있고, 분산 환경에서 정합성을 보장.</li>
<li>단점: 구현이 복잡하면서 별도의 인프라(Redis 등)가 필요</li>
<li>적용처: 선착순 쿠폰 발급 (짧은 시간에 트래픽이 몰리는 구간).</li>
</ul>
<hr>
<h3 id="시나리오별-적용-전략">시나리오별 적용 전략</h3>
<p>현재 이 프로젝트에서는 각 비즈니스 로직 특성에 맞추어 다른 전략으로 진행했다.</p>
<h4 id="case-1---주문-및-결제-비관적-락">Case 1 - 주문 및 결제 (비관적 락)</h4>
<p>주문과 결제는 포인트 차감, 재고 감소 등 데이터의 정확성이 최우선이다. 트래픽이 쿠폰만큼 순간적으로 폭발할 가능성은 적지만, 동시에 같은 주문 건을 처리하는 것을 막기 위해 비관적 락을 사용했다.</p>
<pre><code class="language-kotlin">// Repository
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query(&quot;select p from Payment p where p.id = :id&quot;)
fun findByIdForUpdate(id: Long): Payment?</code></pre>
<h4 id="case-2---선착순-쿠폰-발급-분산-락">Case 2 - 선착순 쿠폰 발급 (분산 락)</h4>
<p>선착순 이벤트는 DB 락으로만 제어를 한다면 수많은 대기열이 DB 커넥션을 점유하면서 전체 서비스 장애로 이어질 수 있다. 따라서 Redis를 이용한 분산 락을 도입했다.</p>
<p>하지만, 여기서 &quot;분산 락과 DB 트랜잭션의 범위&quot; 문제가 있었다.</p>
<hr>
<h3 id="문제-상황---분산-락을-썼는데-데이터-정합성-❌">문제 상황 - 분산 락을 썼는데 데이터 정합성 ❌</h3>
<p>처음에는 아래와 같은 구조로 코드를 짰다. (데이터 정합성을 위해서 DB 락까지 이중으로 건 상황)</p>
<ul>
<li><strong>기존 로직 (문제점)</strong><ol>
<li><code>@Transactional</code> 시작</li>
<li>분산 락 획득 (Redis)</li>
<li>불안해서 DB 비관적 락도 획득</li>
<li>쿠폰 발급 로직 수행</li>
<li>분산 락 해제</li>
<li><code>@Transactional</code> 커밋</li>
</ol>
</li>
</ul>
<p>이렇게 <code>분산 락 + DB 비관적 락</code> 을 혼용하게 되면 데드락 위험이 커지면서 성능도 떨어진다. 목표는 <strong>&quot;DB 락 없이 분산락만으로 깔끔하게 처리하는 것&quot;</strong> 이었다.</p>
<p>하지만 단순히 DB 락을 빼고 분산 락만 사용한다면 동시성 이슈가 다시 발생했다. 이유는 <strong>트랜잭션 커밋 시점과 락 해제 시점이 불일치했다.</strong></p>
<ol>
<li>Thread A가 로직을 다 수행하고 <strong>락을 해제</strong>함.</li>
<li>하지만 Thread A의 DB 트랜잭션은 아직 <strong>커밋되지 않음</strong> (DB 반영전)</li>
<li>그 사이 Thread B가 락을 획득하고 데이터를 읽음. -&gt; <strong>Thread A의 변경 상항을 못 보고 과거 데이터를 읽음</strong></li>
<li>결국 정합성 깨짐.</li>
</ol>
<hr>
<h3 id="해결-facade-패턴으로-락-범위-제어하기">해결 Facade 패턴으로 락 범위 제어하기</h3>
<p>이 문제를 해결하기 위해서는 <strong>&quot;락의 범위가 트랜잭션의 범위보다 커야 한다.&quot;</strong> 즉, 트랜잭션이 완전히 커밋된 후에 락을 풀어야 한다.</p>
<p>이를 위해서 <strong>Facade 패턴</strong>을 적용하여 비즈니스 로직(트랜잭션)을 락으로 감싸는 구조로 리팩토링했다.</p>
<ol>
<li>사용자가 쿠폰 발급 시 Redisson Lock을 획득하고 획득 성공 여부 판단</li>
<li>쿠폰 발급 트랜잭션을 시작하고 쿠폰을 발급하는 로직 실행.</li>
<li>트랜잭션 커밋 -&gt; DB 반영</li>
<li>획득한 Redisson Lock 해제</li>
</ol>
<h4 id="코드-예시-kotlin">코드 예시 (Kotlin)</h4>
<p><code>CouponService (트랜잭션 담당)</code> 순수한 비즈니스 로직만 담당하며, <code>@Transactional</code>을 가진다.</p>
<pre><code class="language-kotlin">@Service
class CouponService(
    private val couponRepository: CouponRepository
) {
    @Transactional // 트랜잭션은 여기서만 동작.
    fun issueCoupon(couponId: Long, userId: Long) {
        val coupon = couponRepository.findById(couponId)
            ?: throw IllegalArgumentException(&quot;쿠폰이 없습니다.&quot;)

        coupon.decreaseQuantity() // 수량 감소

        // 발급 로직
    }
}</code></pre>
<p><code>CoupnFacade (락 제어 담당)</code> 트랜잭션 없이 락의 획득 및 해제만 담당하면서, 실제 로직은 Service에 위임한다.</p>
<pre><code class="language-kotlin">@Component
class CouponFacade(
    private val redissonClient: RedissonClient,
    private val couponSerivce: CouponService
) {
    fun issueCouponWithLock(userId: Long, couponId: Long) {
        val lock = redissonClient.getLock(&quot;coupon_lock:$couponId&quot;)

        try {
            // 락 획득 시도
            val available = lock.tryLock(10, 1, TimeUnit.SECONDS)

            if (!available) {
                return
            }

            couponService.issueCoupon(couponId, userId)
        } catch (e: InterruptedException) {
            throw RuntimeException(e)
        } finally {
            if(lock.isLocked &amp;&amp; lock.isHeldByCurrentThread) {
                lock.unlock()
            }
        }
    }
}</code></pre>
<hr>
<h3 id="결론">결론</h3>
<p>이번 프로젝트를 통해서 상황에 맞는 동시성 제어 방식이 무엇인지 깊이 고민할 수 있었다.</p>
<ol>
<li>비관적 락: 데이터 정합성이 중요한 결제/주문 로직에 적합하다고 판단.</li>
<li>분산 락: 트래픽이 몰리는 선착순 이벤트에 적합하며, DB 부하를 줄인다.</li>
<li>트랜잭션과 락의 범위: 분산 락 사용 시 반드시 <strong>&quot;Lock 획득 -&gt; Transaction 시작/종료 -&gt; Lock 해제&quot;</strong> 순서를 지켜야 데이터 정합성이 깨지지 않는다.</li>
</ol>
<p>단순히 &quot;락을 걸었다&quot;에서 끝나는게 아니라, <strong>DB의 격리 수준과 트랜잭션의 생명주기기</strong>까지 고려해야 완벽한 동시성 제어가 가능하단걸 알게되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 의존성 역전 원칙 (Dependency Inversion Principle)]]></title>
            <link>https://velog.io/@kwaktaemin_/TIL-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%97%AD%EC%A0%84-%EC%9B%90%EC%B9%99-Dependency-Inversion-Principle</link>
            <guid>https://velog.io/@kwaktaemin_/TIL-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%97%AD%EC%A0%84-%EC%9B%90%EC%B9%99-Dependency-Inversion-Principle</guid>
            <pubDate>Tue, 04 Nov 2025 05:42:30 GMT</pubDate>
            <description><![CDATA[<p>과제를 진행하다 레이어드 아키텍처를 적용하던 중, <strong>&quot;Repository 인터페이스는 Domain에, 구현체는 Infrastructure에&quot;</strong> 라는 요구사항을 보고, 보통 상위 계층이 하위 계층을 호출하는 거 아닌가?
라는 생각이 들었다.</p>
<p>이 의문을 해결하는 핵심이 <strong>의존성 역전 원칙</strong>이었다. SOLID 원칙 중 마지막 &quot;D&quot;에 해당하는 이 원칙은, 제대로 이해하면 아키텍처 설계의 핵심을 꿰뚫을 수 있다.</p>
<hr>
<h2 id="🤔-문제-상황">🤔 문제 상황</h2>
<pre><code class="language-kotlin">@Service
class OrderService(
    private val mySqlOrderRepository: MySqlOrderRepository  // 구체 클래스에 의존 💥
) {
    fun createOrder(order: Order): Order {
        return mySqlOrderRepository.save(order)
    }
}

@Repository
class MySqlOrderRepository {
    fun save(order: Order): Order {
        // MySQL에 저장하는 구체적인 로직
        val sql = &quot;INSERT INTO orders ...&quot;
        // ...
        return order
    }
}</code></pre>
<p><strong>문제점</strong></p>
<ol>
<li><strong>MySQL에서 MongoDB로 변경하려면?</strong> -&gt; <code>OrderService</code> 코드 수정이 필요 💥</li>
<li><strong>테스트하려면</strong> -&gt; 실제 MySQL이 필요 💥</li>
<li><strong>Service가 저수준 모듈(DB)에 의존</strong> -&gt; 비즈니스 로직이 기술에 종속 💥</li>
</ol>
<hr>
<h2 id="🎯-의존성-역전-원칙이란">🎯 의존성 역전 원칙이란?</h2>
<p>Robert C. Martin이 정의한 원칙!</p>
<blockquote>
<p><strong>A. 고수준 모듈은 저수준 모듈에 의존해서는 안된다. 둘 다 추상화에 의존</strong></p>
<p><strong>B. 주상화는 세부사항에 의존해서는 안되고, 세부사항이 추상화에 의존해야 한다.</strong></p>
</blockquote>
<h3 id="용어-정리">용어 정리</h3>
<table>
<thead>
<tr>
<th>용어</th>
<th>의미</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>고수준 모듈</strong></td>
<td>비즈니스 규칙, 정책</td>
<td>OrderService, Domain</td>
</tr>
<tr>
<td><strong>저수준 모듈</strong></td>
<td>기술적 세부사항, 구현</td>
<td>MySqlRepository, EmailService</td>
</tr>
<tr>
<td><strong>추상화</strong></td>
<td>인터페이스, 추상 클래스</td>
<td>OrderRepository 인터페이스</td>
</tr>
<tr>
<td><strong>세부사항</strong></td>
<td>구체적인 구현</td>
<td>MySqlOrderRepository 구현체</td>
</tr>
</tbody></table>
<hr>
<h2 id="📊-전통적-의존성-vs-역전된-의존성">📊 전통적 의존성 vs 역전된 의존성</h2>
<h3 id="전통적-의존성-나쁜-예">전통적 의존성 (나쁜 예)</h3>
<pre><code>┌─────────────────┐
│  OrderService   │  (고수준)
└────────┬────────┘
         ↓ 의존 (직접 참조)
┌─────────────────┐
│ MySqlRepository │  (저수준)
└─────────────────┘</code></pre><p><strong>문제:</strong> 고수준 모듈이 저수준 모듈에 의존 → 변경에 취약</p>
<hr>
<h3 id="역전된-의존성-좋은-예">역전된 의존성 (좋은 예)</h3>
<pre><code>┌─────────────────┐
│  OrderService   │  (고수준)
└────────┬────────┘
         ↓ 의존 (인터페이스)
┌─────────────────────┐
│ OrderRepository     │  (추상화 - Interface)
└─────────┬───────────┘
          ↑ 구현 (implements)
┌─────────────────────┐
│ MySqlRepository     │  (저수준)
└─────────────────────┘</code></pre><p><strong>해결</strong>: 고수준과 저수준 모두 추상화에 의존 -&gt; 유연함!</p>
<hr>
<h2 id="🔑-코드로-이해하기">🔑 코드로 이해하기</h2>
<h3 id="❌-나쁜-예-dip-위반">❌ 나쁜 예: DIP 위반</h3>
<pre><code class="language-kotlin">// Service가 구체 클래스에 직접 의존
@Service
class OrderService(
    private val mySqlOrderRepository: MySqlOrderRepository  // 💥 구체 클래스
) {
    fun createOrder(order: Order): Order {
        return mySqlOrderRepository.save(order)
    }
}

@Repository
class MySqlOrderRepository {
    fun save(order: Order): Order {
        // MySQL 저장 로직
    }
}</code></pre>
<p><strong>문제점:</strong></p>
<pre><code class="language-kotlin">// MongoDB로 변경하려면 Service 코드 수정 필요!
@Service
class OrderService(
    // private val mySqlOrderRepository: MySqlOrderRepository  // 삭제
    private val mongoOrderRepository: MongoOrderRepository     // 추가
) {
    fun createOrder(order: Order): Order {
        return mongoOrderRepository.save(order)  // 메서드 호출도 변경
    }
}</code></pre>
<hr>
<h3 id="✅-좋은-예-dip-적용">✅ 좋은 예: DIP 적용</h3>
<p><strong>1단계: 인터페이스 정의 (Domain Layer)</strong></p>
<pre><code class="language-kotlin">// domain/order/OrderRepository.kt
package com.hhplus.ecommerce.domain.order

// ✅ 고수준 모듈(Domain)이 정의하는 인터페이스
interface OrderRepository {
    fun save(order: Order): Order
    fun findById(id: Long): Order?
    fun findByUserId(userId: Long): List&lt;Order&gt;
}</code></pre>
<p><strong>2단계: Service는 인터페이스에 의존 (Application Layer)</strong></p>
<pre><code class="language-kotlin">// application/order/OrderService.kt
package com.hhplus.ecommerce.application.order

import com.hhplus.ecommerce.domain.order.OrderRepository  // 인터페이스
import com.hhplus.ecommerce.domain.order.Order

@Service
class OrderService(
    private val orderRepository: OrderRepository  // ✅ 추상화에 의존!
) {
    fun createOrder(order: Order): Order {
        return orderRepository.save(order)
    }
}</code></pre>
<p><strong>3단계: 구현체들 (Infrastructure Layer)</strong></p>
<pre><code class="language-kotlin">// infrastructure/order/MySqlOrderRepository.kt
package com.hhplus.ecommerce.infrastructure.order

import com.hhplus.ecommerce.domain.order.OrderRepository  // Domain 인터페이스
import com.hhplus.ecommerce.domain.order.Order

@Repository
@Primary  // 기본 구현체
class MySqlOrderRepository : OrderRepository {
    override fun save(order: Order): Order {
        // MySQL 저장 로직
        println(&quot;MySQL에 저장&quot;)
        return order
    }

    override fun findById(id: Long): Order? {
        // MySQL 조회 로직
    }
}</code></pre>
<pre><code class="language-kotlin">// infrastructure/order/MongoOrderRepository.kt
package com.hhplus.ecommerce.infrastructure.order

import com.hhplus.ecommerce.domain.order.OrderRepository
import com.hhplus.ecommerce.domain.order.Order

@Repository
class MongoOrderRepository : OrderRepository {
    override fun save(order: Order): Order {
        // MongoDB 저장 로직
        println(&quot;MongoDB에 저장&quot;)
        return order
    }

    override fun findById(id: Long): Order? {
        // MongoDB 조회 로직
    }
}</code></pre>
<p><strong>장점:</strong></p>
<ul>
<li>DB 변경 시 Service 코드는 전혀 수정할 필요없음.</li>
<li><code>application.yml</code> 또는 <code>@Primary</code>만 변경하면 끝.</li>
</ul>
<hr>
<h2 id="🔁-의존성-방향이-역전되는-이유">🔁 의존성 방향이 역전되는 이유</h2>
<h3 id="전통적-방식">전통적 방식</h3>
<pre><code class="language-kotlin">// OrderService.kt (상위 모듈)
class OrderService(
    private val repository: MySqlOrderRepository  // 하위 모듈 직접 참조
)

// MySqlOrderRepository.kt (하위 모듈)
class MySqlOrderRepository {
    // 구현
}</code></pre>
<p><strong>의존성 방향:</strong> <code>OrderService</code> -&gt; <code>MySqlOrderRepository</code></p>
<hr>
<h3 id="dip-적용">DIP 적용</h3>
<pre><code class="language-kotlin">// Domain Layer
interface OrderRepository {  // 고수준 모듈이 정의
    fun save(order: Order): Order
}

// Application Layer
class OrderService(
    private val repository: OrderRepository  // 인터페이스 의존
)

// Infrastructure Layer
class MySqlOrderRepository : OrderRepository {  // 인터페이스 구현
    override fun save(order: Order): Order { ... }
}</code></pre>
<p><strong>의존성 방향:</strong></p>
<ul>
<li><code>OrderService</code> -&gt; <code>OrderRepository</code> (인터페이스)</li>
<li><code>MySqlOrderRepository</code> -&gt; <code>OrderRepository</code> (인터페이스 구현)</li>
</ul>
<p><strong>핵심:</strong> 구현체가 인터페이스에 의존하므로, 의존성 방향이 <strong>역전</strong>됨.</p>
<p>전통정: 상위 -&gt; 하위
역전됨: 상위 -&gt; 인터페이스 &lt;- 하위</p>
<hr>
<h2 id="🎯-실제-적용-예시">🎯 실제 적용 예시</h2>
<h3 id="예시1-알림-시스템">예시1: 알림 시스템</h3>
<pre><code class="language-kotlin">// ❌ DIP 위반
@Service
class OrderService(
    private val smtpEmailService: SmtpEmailService  // SMTP에 강하게 결합
) {
    fun createOrder(order: Order) {
        // ...
        smtpEmailService.sendEmail(
            to = &quot;user@example.com&quot;,
            subject = &quot;주문 완료&quot;,
            body = &quot;주문이 완료되었습니다&quot;
        )
    }
}

class SmtpEmailService {
    fun sendEmail(to: String, subject: String, body: String) {
        // SMTP 프로토콜로 이메일 전송
    }
}</code></pre>
<p>이 코드의 문제는 이메일을 slack으로 알림을 변경하려면 <strong>Service 전체 수정이 필요</strong>하다.</p>
<pre><code class="language-kotlin">// ✅ DIP 적용
// domain/port/NotificationPort.kt (Domain이 정의)
interface NotificationPort {
    fun sendOrderConfirmation(order: Order)
}

// application/order/OrderService.kt
@Service
class OrderService(
    private val notificationPort: NotificationPort  // 추상화에 의존
) {
    fun createOrder(order: Order) {
        // ...
        notificationPort.sendOrderConfirmation(order)  // 구현 방법은 모름!
    }
}

// infrastructure/notification/EmailNotificationAdapter.kt
@Component
class EmailNotificationAdapter : NotificationPort {
    override fun sendOrderConfirmation(order: Order) {
        // SMTP로 이메일 전송
    }
}

// infrastructure/notification/SlackNotificationAdapter.kt
@Component
class SlackNotificationAdapter : NotificationPort {
    override fun sendOrderConfirmation(order: Order) {
        // Slack API로 메시지 전송
    }
}</code></pre>
<p>이 코드의 장점은 이메일에서 slacke으로 변경을 할 때 <strong>Service 코드 수정이 불필요</strong>하다.</p>
<hr>
<h2 id="🧪-테스트-용이성">🧪 테스트 용이성</h2>
<h3 id="❌-dip-미적용-테스트-어려움">❌ DIP 미적용 (테스트 어려움)</h3>
<pre><code class="language-kotlin">class OrderServiceTest {
    @Test
    fun `주문 생성 테스트`() {
        val service = OrderService(
            MySqlOrderRepository()
        )

        // MySQL이 없으면 테스트 불가
    }
}</code></pre>
<hr>
<h3 id="✅-dip-적용-테스트-쉬움">✅ DIP 적용 (테스트 쉬움)</h3>
<pre><code class="language-kotlin">// 테스트용 Fake Repository
class FakeOrderRepository : OrderRepository {
    private val storage = mutableMapOf&lt;Long, Order&gt;()

    override fun save(order: Order): Order {
        storage[order.id!!] = order
        return order
    }

    override fun findById(id: Long): Order? = storage[id]
}

class OrderServiceTest {
    @Test
    fun `주문 생성 테스트`() {
        // ✅ DB 없이도 테스트 가능!
        val fakeRepository = FakeOrderRepository()
        val service = OrderService(fakeRepository)

        val order = service.createOrder(
            Order.create(userId = 1L, items = listOf(...))
        )

        assertNotNull(order.id)
    }

    @Test
    fun `주문 생성 시 알림 전송 테스트`() {
        // ✅ Mock으로 쉽게 검증
        val mockNotification = mock&lt;NotificationPort&gt;()
        val service = OrderService(
            orderRepository = fakeRepository,
            notificationPort = mockNotification
        )

        service.createOrder(order)

        verify(mockNotification).sendOrderConfirmation(any())
    }
}</code></pre>
<hr>
<h2 id="📁-레이어드-아키텍처에서-dip">📁 레이어드 아키텍처에서 DIP</h2>
<pre><code>┌─────────────────────────────────────┐
│  Application Layer                  │
│  ┌────────────────┐                 │
│  │ OrderService   │                 │
│  └────────┬───────┘                 │
└───────────┼─────────────────────────┘
            ↓ 의존 (인터페이스)
┌───────────────────────────────────────┐
│  Domain Layer                         │
│  ┌────────────────────────────────┐   │
│  │ OrderRepository (interface)    │   │  ← 고수준이 정의!
│  └────────────────────────────────┘   │
└───────────────────────────────────────┘
            ↑ 구현 (implements)
┌───────────────────────────────────────┐
│  Infrastructure Layer                 │
│  ┌────────────────────────────────┐   │
│  │ MySqlOrderRepository           │   │  ← 저수준이 구현!
│  │ (implements OrderRepository)   │   │
│  └────────────────────────────────┘   │
└───────────────────────────────────────┘</code></pre><p><strong>핵심</strong>은 Domain이 인터페이스를 정의하고, infrastructure가 구현을 한다.</p>
<hr>
<p>의존성 역전 원칙은 단순히 &quot;인터페이스를 만들자&quot;가 아니라 &quot;누가 인터페이스를 정의하고 소유하는가&quot; 의 문제다. 고수준 모듈이 필요로 하는 것을 인터페이스로 정의하고, 저수준 모듈이 그것을 구현하게 만드는 것. 이것이 의존성을 역전시키는 핵심이다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 클린 아키텍처 (Clean Architecture)]]></title>
            <link>https://velog.io/@kwaktaemin_/TIL-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-Clean-Architecture</link>
            <guid>https://velog.io/@kwaktaemin_/TIL-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-Clean-Architecture</guid>
            <pubDate>Mon, 03 Nov 2025 06:09:55 GMT</pubDate>
            <description><![CDATA[<p>레이어드 아키텍처를 공부하고 적용을 해보면서, 레이어드 아키텍처도 계층을 나누는데, 클린 이키텍처는 왜 존재하고, 어떻게 다른지 궁금했다.</p>
<p>클린 아키텍처는 레이어드 아키텍처의 문제점을 해결하고 <strong>더 엄격한 의존성 규칙</strong>을 적용한 아키텍처 패턴이다. <strong>Rovert C.Martin</strong>이 제안한 이 패턴은 <strong>비즈니스 로직을 외부로부터 완전히 독립</strong>시키는 것을 목표로 한다.</p>
<h2 id="🙀-레이어드-아키텍처의-한계">🙀 레이어드 아키텍처의 한계</h2>
<p>레이어드 아키텍처에서는 아래와 같은 구조를 사용했다.</p>
<pre><code>Presentation -&gt; Application -&gt; Domain &lt;- Infrastructure</code></pre><p>하지만 규모가 커지면 어떤 문제가 발생할까?</p>
<pre><code class="language-kotlin">// ❌ 레이어드 아키텍처의 흔한 실수
@Service
class OrderService(
    private val orderRepository: OrderRepository,  // Domain 인터페이스
    private val emailService: EmailService,        // 외부 서비스
    private val paymentGateway: PaymentGateway    // 외부 API
) {
    fun createOrder(order: Order) {
        // 비즈니스 로직이 외부 시스템과 강하게 결합됨 💥
        orderRepository.save(order)
        emailService.sendOrderConfirmation(order)   // SMTP 의존
        paymentGateway.processPayment(order)        // PG사 API 의존
    }
}</code></pre>
<p>위 코드의 문제점은 아래와 같다.</p>
<ul>
<li>비즈니스 로직이 외부 시스템(이메일, 결제)과 강하게 결합</li>
<li>SMTP 서버가 없으면 테스트 불가능</li>
<li>결제 게이트웨이를 바꾸려면 Service 수정 필요</li>
</ul>
<hr>
<h2 id="🎯-클린-아키텍처란">🎯 클린 아키텍처란?</h2>
<p><strong>의존성은 항상 안쪽(고수준 정책)을 향한다</strong> -&gt; 이것이 클린 아키텍처의 핵심 규칙이다.</p>
<h3 id="동심원-구조">동심원 구조</h3>
<pre><code>┌─────────────────────────────────────────────────┐
│  Frameworks &amp; Drivers (최외곽)                    │
│  ┌───────────────────────────────────────────┐  │
│  │  Interface Adapters                       │  │
│  │  ┌─────────────────────────────────────┐  │  │
│  │  │  Application Business Rules         │  │  │
│  │  │  ┌───────────────────────────────┐  │  │  │
│  │  │  │  Enterprise Business Rules    │  │  │  │
│  │  │  │  (Entities)                   │  │  │  │
│  │  │  └───────────────────────────────┘  │  │  │
│  │  │  (Use Cases)                        │  │  │
│  │  └─────────────────────────────────────┘  │  │
│  │  (Controllers, Presenters, Gateways)      │  │
│  └───────────────────────────────────────────┘  │
│  (Web, DB, UI, External Interfaces)             │
└─────────────────────────────────────────────────┘</code></pre><p>의존성 방향은 항상 <strong>외부 -&gt; 내부</strong>이다.</p>
<h2 id="4개의-계층">4개의 계층</h2>
<h3 id="entities-enterprise-business-rules">Entities (Enterprise Business Rules)</h3>
<p>가장 안쪽 - 핵심 비즈니스 규칙</p>
<pre><code class="language-kotlin">// domain/order/Order.kt
data class Order(
    val id: Long?,
    val userId: Long,
    val items: List&lt;OrderItem&gt;,
    private var status: OrderStatus,
    private val _totalAmount: BigDecimal
) {
    val totalAmount: BigDecimal get() = _totalAmount

    // ✅ 비즈니스 규칙만 포함 (외부 의존성 ZERO)
    fun cancel() {
        require(status == OrderStatus.PENDING) {
            &quot;대기 중인 주문만 취소할 수 있습니다&quot;
        }
        status = OrderStatus.CANCELLED
    }

    fun complete() {
        require(status == OrderStatus.PENDING) {
            &quot;대기 중인 주문만 완료할 수 있습니다&quot;
        }
        status = OrderStatus.COMPLETED
    }

    fun canCancel(): Boolean = status == OrderStatus.PENDING

    companion object {
        fun create(userId: Long, items: List&lt;OrderItem&gt;): Order {
            require(items.isNotEmpty()) { &quot;주문 상품이 비어있습니다&quot; }

            val totalAmount = items.sumOf { it.calculateAmount() }

            return Order(
                id = null,
                userId = userId,
                items = items,
                status = OrderStatus.PENDING,
                _totalAmount = totalAmount
            )
        }
    }
}</code></pre>
<p>특징은</p>
<ul>
<li>순수 비즈니스 로직만 포함</li>
<li>프레임워크, DB, UI 등 모든 외부 의존성으로부터 독립</li>
<li>변경 빈도가 가장 낮음</li>
</ul>
<hr>
<h3 id="use-case-application-business-rules">Use Case (Application Business Rules)</h3>
<p>애플리케이션 특화 비즈니스 규칙</p>
<pre><code class="language-kotlin">// application/order/usecase/CreateOrderUseCase.kt
interface CreateOrderUseCase {
    fun execute(command: CreateOrderCommand): Order
}

// application/order/usecase/CreateOrderUseCaseImpl.kt
@Component
class CreateOrderUseCaseImpl(
    private val orderRepository: OrderRepository,        // 포트(인터페이스)
    private val stockRepository: StockRepository,        // 포트(인터페이스)
    private val notificationPort: NotificationPort,      // 포트(인터페이스)
    private val paymentPort: PaymentPort                 // 포트(인터페이스)
) : CreateOrderUseCase {

    override fun execute(command: CreateOrderCommand): Order {
        // 1. 재고 확인 및 차감
        val stock = stockRepository.findById(command.productId)
            ?: throw ProductNotFoundException()
        stock.decrease(command.quantity)

        // 2. 주문 생성 (Entity가 비즈니스 규칙 처리)
        val order = Order.create(
            userId = command.userId,
            items = command.items
        )

        // 3. 결제 처리 (포트를 통해)
        paymentPort.processPayment(order)

        // 4. 알림 전송 (포트를 통해)
        notificationPort.sendOrderConfirmation(order)

        // 5. 저장
        stockRepository.save(stock)
        return orderRepository.save(order)
    }
}</code></pre>
<p>특징은</p>
<ul>
<li>애플리케이션의 use case 구현</li>
<li>Entity를 조율</li>
<li>외부 시스템과의 통신은 포트(interface)를 통해서 진행.</li>
</ul>
<h3 id="interface-adapterscontrollers-presenters-gateways">Interface Adapters(Controllers, Presenters, Gateways)</h3>
<p>외부와 내부를 연결하는 어댑터</p>
<pre><code class="language-kotlin">// adapter/in/web/OrderController.kt (Input Adapter)
@RestController
@RequestMapping(&quot;/api/orders&quot;)
class OrderController(
    private val createOrderUseCase: CreateOrderUseCase
) {
    @PostMapping
    fun createOrder(@RequestBody request: CreateOrderRequest): OrderResponse {
        // DTO → Command 변환
        val command = CreateOrderCommand(
            userId = request.userId,
            items = request.items.map { 
                OrderItem(it.productId, it.quantity) 
            }
        )

        // Use Case 호출
        val order = createOrderUseCase.execute(command)

        // Domain → DTO 변환
        return OrderResponse.from(order)
    }
}</code></pre>
<pre><code class="language-kotlin">// adapter/out/persistence/OrderRepositoryAdapter.kt (Output Adapter)
@Repository
class OrderRepositoryAdapter(
    private val jpaRepository: JpaOrderRepository
) : OrderRepository {  // Domain의 포트 구현

    override fun save(order: Order): Order {
        val entity = OrderEntity.from(order)
        val saved = jpaRepository.save(entity)
        return saved.toDomain()
    }

    override fun findById(id: Long): Order? {
        return jpaRepository.findById(id)
            .map { it.toDomain() }
            .orElse(null)
    }
}</code></pre>
<pre><code class="language-kotlin">// adapter/out/notification/EmailNotificationAdapter.kt (Output Adapter)
@Component
class EmailNotificationAdapter(
    private val emailService: EmailService
) : NotificationPort {  // Domain의 포트 구현

    override fun sendOrderConfirmation(order: Order) {
        emailService.send(
            to = getUserEmail(order.userId),
            subject = &quot;주문 확인&quot;,
            body = &quot;주문이 완료되었습니다. 주문번호: ${order.id}&quot;
        )
    }
}</code></pre>
<p>특징</p>
<ul>
<li><strong>Controller</strong>: 웹 요청을 Use Case 호출로 변환 (Input Adapter)</li>
<li><strong>Repository Adapter</strong>: Domain 포트를 실제 DB 연동으로 구현 (Output Adapter)</li>
<li><strong>Notification Adapter</strong>: Domain 포트를 실제 이메일 서비스로 구현 (Output Adapter)</li>
</ul>
<h3 id="framworks--drivers">Framworks &amp; Drivers</h3>
<p>가장 바깥쪽 - 프레임워크와 도구</p>
<pre><code class="language-kotlin">// infrastructure/config/EmailConfig.kt
@Configuration
class EmailConfig {
    @Bean
    fun emailService(): EmailService {
        return SmtpEmailService(/* SMTP 설정 */)
    }
}</code></pre>
<p>특징</p>
<ul>
<li>Spring, JPA, SMTP 등 구체적인 기술</li>
<li>가장 자주 변경되는 계층</li>
<li>내부 계층에 영향을 주지 않음</li>
</ul>
<hr>
<h2 id="🔑-핵심-개념-포트와-어댑터-hexagonal-architecture">🔑 핵심 개념: 포트와 어댑터 (Hexagonal Architecture)</h2>
<p>클린 아키텍처는 헥사고날 아키텍처(Ports And Adapter)와 매우 유사하다.</p>
<h3 id="port">Port</h3>
<p>Domain이 정의하는 interface</p>
<pre><code class="language-kotlin">// domain/port/out/NotificationPort.kt (Output Port)
interface NotificationPort {
    fun sendOrderConfirmation(order: Order)
    fun sendShippingNotification(order: Order)
}

// domain/port/out/PaymentPort.kt (Output Port)
interface PaymentPort {
    fun processPayment(order: Order): PaymentResult
    fun refund(order: Order): RefundResult
}

// domain/port/in/CreateOrderUseCase.kt (Input Port)
interface CreateOrderUseCase {
    fun execute(command: CreateOrderCommand): Order
}</code></pre>
<h3 id="adapter">Adapter</h3>
<p>포트의 구현체</p>
<pre><code class="language-kotlin">// adapter/out/notification/EmailNotificationAdapter.kt
@Component
class EmailNotificationAdapter : NotificationPort {
    override fun sendOrderConfirmation(order: Order) {
        // SMTP로 이메일 전송
    }
}

// adapter/out/notification/SlackNotificationAdapter.kt
@Component
class SlackNotificationAdapter : NotificationPort {
    override fun sendOrderConfirmation(order: Order) {
        // Slack API로 메시지 전송
    }
}</code></pre>
<p><strong>장점:</strong> email에서 slack으로 변경해도 Domain, Use case는 수정 불필요.</p>
<hr>
<h2 id="📁-클린-아키텍처-프로젝트-구조">📁 클린 아키텍처 프로젝트 구조</h2>
<pre><code>src/main/kotlin/com/hhplus/ecommerce/
│
├── domain/                          # 1️⃣ Entities (최내부)
│   ├── order/
│   │   ├── Order.kt                # 비즈니스 규칙
│   │   ├── OrderItem.kt
│   │   └── OrderStatus.kt
│   │
│   └── port/                        # 포트 정의
│       ├── in/                      # Input Port (Use Case 인터페이스)
│       │   ├── CreateOrderUseCase.kt
│       │   └── CancelOrderUseCase.kt
│       │
│       └── out/                     # Output Port
│           ├── OrderRepository.kt
│           ├── NotificationPort.kt
│           └── PaymentPort.kt
│
├── application/                     # 2️⃣ Use Cases
│   └── order/
│       ├── CreateOrderUseCaseImpl.kt
│       └── CancelOrderUseCaseImpl.kt
│
├── adapter/                         # 3️⃣ Interface Adapters
│   ├── in/                          # Input Adapter
│   │   └── web/
│   │       └── OrderController.kt
│   │
│   └── out/                         # Output Adapter
│       ├── persistence/
│       │   └── OrderRepositoryAdapter.kt
│       │
│       ├── notification/
│       │   ├── EmailNotificationAdapter.kt
│       │   └── SlackNotificationAdapter.kt
│       │
│       └── payment/
│           └── TossPaymentAdapter.kt
│
└── infrastructure/                  # 4️⃣ Frameworks &amp; Drivers
    ├── config/
    │   ├── DatabaseConfig.kt
    │   └── EmailConfig.kt
    │
    └── external/
        └── TossPaymentClient.kt</code></pre><hr>
<h2 id="🔄-의존성-흐름">🔄 의존성 흐름</h2>
<pre><code>┌──────────────────────────────────────────┐
│  Controller (Adapter In)                 │
└────────────┬─────────────────────────────┘
             ↓ 호출
┌──────────────────────────────────────────┐
│  Use Case (Application)                  │
└─────┬──────────────────┬─────────────────┘
      ↓ 의존             ↓ 의존
┌─────────────┐    ┌──────────────────────┐
│   Entity    │    │   Port (인터페이스)   │
│  (Domain)   │    │   (Domain)           │
└─────────────┘    └──────────┬───────────┘
                              ↑ 구현
                   ┌──────────────────────┐
                   │  Adapter Out         │
                   │  (Persistence 등)    │
                   └──────────────────────┘</code></pre><p>핵심: 모든 의존성이 Domain을 바라봄</p>
<h2 id="🆚-레이어드-아키텍처-vs-클린-아키텍처">🆚 레이어드 아키텍처 VS 클린 아키텍처</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>레이어드 아키텍처</th>
<th>클린 아키텍처</th>
</tr>
</thead>
<tbody><tr>
<td><strong>구조</strong></td>
<td>수평적 계층 (Layer)</td>
<td>동심원 구조 (Circle)</td>
</tr>
<tr>
<td><strong>의존성</strong></td>
<td>상위 → 하위</td>
<td>외부 → 내부</td>
</tr>
<tr>
<td><strong>Domain 위치</strong></td>
<td>중간 계층</td>
<td>최중심</td>
</tr>
<tr>
<td><strong>외부 시스템</strong></td>
<td>Service에서 직접 호출</td>
<td>Port를 통해 간접 호출</td>
</tr>
<tr>
<td><strong>테스트</strong></td>
<td>외부 시스템 Mock 필요</td>
<td>Port만 Mock</td>
</tr>
<tr>
<td><strong>유연성</strong></td>
<td>중간</td>
<td>매우 높음</td>
</tr>
<tr>
<td><strong>복잡도</strong></td>
<td>낮음</td>
<td>높음</td>
</tr>
<tr>
<td><strong>적용 시기</strong></td>
<td>중소 규모 프로젝트</td>
<td>대규모, 장기 프로젝트</td>
</tr>
</tbody></table>
<hr>
<h2 id="🤔-언제-클린-아키텍처를-사용할까">🤔 언제 클린 아키텍처를 사용할까?</h2>
<h3 id="✅-클린-아키텍처가-적합한-경우">✅ 클린 아키텍처가 적합한 경우</h3>
<ol>
<li>비즈니스 로직이 복잡한 경우<ul>
<li>금융, 이커머스, 헬스케어 등</li>
</ul>
</li>
<li>외부 시스템 의존성이 많은 경우<ul>
<li>결제 게이트웨이, 이메일 서비스, SMS, Push 알림 등</li>
</ul>
</li>
<li>장기 운영 프로젝트<ul>
<li>기술 스택 변경 가능성이 높은 경우</li>
</ul>
</li>
<li>테스트가 중요한 경우<ul>
<li>TDD를 적극적으로 적용하는 프로젝트</li>
</ul>
</li>
</ol>
<h3 id="❌-클린-아키텍처가-과한-경우">❌ 클린 아키텍처가 과한 경우</h3>
<ol>
<li>단순 CRUD 애플리케이션<ul>
<li>복잡한 비즈니스 로직이 없는 경우</li>
</ul>
</li>
<li>프로토타입이나 MVP<ul>
<li>빠른 개발이 최우선인 경우</li>
</ul>
</li>
<li>소규모 팀<ul>
<li>아키텍처 유지보수 비용이 부담되는 경우</li>
</ul>
</li>
</ol>
<hr>
<h2 id="✅-클린-아키텍처의-장점">✅ 클린 아키텍처의 장점</h2>
<h3 id="1-독립성">1. 독립성</h3>
<pre><code class="language-kotlin">// ✅ Domain과 Use Case는 외부 기술을 전혀 모름
class Order {
    // Spring, JPA, SMTP 등 외부 의존성 ZERO
}

// ✅ 외부 시스템 교체가 자유로움
// EmailNotificationAdapter → SlackNotificationAdapter
// TossPaymentAdapter → KakaoPaymentAdapter</code></pre>
<h3 id="2-테스트-용이성">2. 테스트 용이성</h3>
<pre><code class="language-kotlin">class CreateOrderUseCaseTest {
    @Test
    fun `주문 생성 시 알림이 전송된다`() {
        // Mock 생성이 쉬움 (인터페이스만 구현)
        val mockNotification = mock&lt;NotificationPort&gt;()
        val useCase = CreateOrderUseCaseImpl(
            orderRepository = mockOrderRepository,
            notificationPort = mockNotification
        )

        useCase.execute(command)

        verify(mockNotification).sendOrderConfirmation(any())
    }
}</code></pre>
<h3 id="3-비즈니스-로직-보호">3. 비즈니스 로직 보호</h3>
<ul>
<li>UI가 바뀌어도 Domain은 안전</li>
<li>DB가 바뀌어도 Domain은 안전</li>
<li>framwork가 바뀌어도 Domain은 안전</li>
</ul>
<h2 id="⚠️-클린-아키텍처의-단점">⚠️ 클린 아키텍처의 단점</h2>
<h3 id="1-높은-초기-비용">1. 높은 초기 비용</h3>
<ul>
<li>보일러 플레이트 코드 증가</li>
<li>인터페이스와 구현체를 각각 관리</li>
</ul>
<h3 id="2-러닝커브">2. 러닝커브</h3>
<ul>
<li>팀원 모두가 아키텍처를 이해해야 함</li>
<li>잘못 적용하면 오히려 복잡도만 증가</li>
</ul>
<h3 id="3-과도한-추상화-위험">3. 과도한 추상화 위험</h3>
<pre><code class="language-kotlin">interface FindUserPort {
    fun findById(id: Long): User?
}

interface SaveUserPort {
    fun save(user: User): User
}

// ✅ 적절한 수준
interface UserRepository {
    fun findById(id: Long): User?
    fun save(user: User): User
}</code></pre>
<hr>
<h2 id="️-실무-팁">‼️ 실무 팁</h2>
<h3 id="1-작게-시작하기">1. 작게 시작하기</h3>
<p>처음부터 완벽한 클린 아키텍처를 적용하려 하지 말기.</p>
<p><strong>1단계:</strong> 레이어드 아키텍처로 시작
<strong>2단계:</strong> Domain을 중심으로 리팩토링
<strong>3단계:</strong> 외부 의존성을 Port로 분리
<strong>4단계:</strong> Adapter 패턴 적용</p>
<h3 id="2-port는-domain-관점에서-정의">2. Port는 Domain 관점에서 정의</h3>
<pre><code class="language-kotlin">// ❌ 기술 중심
interface SmtpEmailPort {
    fun sendSmtpEmail(to: String, subject: String, body: String)
}

// ✅ Domain 중심
interface NotificationPort {
    fun notifyOrderCreated(order: Order)
    fun notifyOrderShipped(order: Order)
}</code></pre>
<h3 id="3-과도한-분리는-금물">3. 과도한 분리는 금물</h3>
<pre><code class="language-kotlin">// ❌ 너무 세분화
interface FindOrderByIdPort
interface FindOrderByUserIdPort
interface SaveOrderPort
interface DeleteOrderPort

// ✅ 적절한 그룹핑
interface OrderRepository {
    fun findById(id: Long): Order?
    fun findByUserId(userId: Long): List&lt;Order&gt;
    fun save(order: Order): Order
    fun delete(id: Long)
}</code></pre>
<h2 id="📊-정리">📊 정리</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>핵심 규칙</strong></td>
<td>의존성은 항상 안쪽(Domain)을 향한다</td>
</tr>
<tr>
<td><strong>구조</strong></td>
<td>Entity → Use Case → Adapter → Framework</td>
</tr>
<tr>
<td><strong>장점</strong></td>
<td>독립성, 테스트 용이성, 유연성</td>
</tr>
<tr>
<td><strong>단점</strong></td>
<td>초기 비용, 학습 곡선, 복잡도</td>
</tr>
<tr>
<td><strong>적용 시기</strong></td>
<td>복잡한 비즈니스 로직, 장기 프로젝트</td>
</tr>
</tbody></table>
<p>핵심 원칙:</p>
<ul>
<li>Domain은 최중심에, 순수하게</li>
<li>외부 의존성은 Port/Adapter로 분리</li>
<li>의존성은 항상 내부를 향하도록</li>
<li>과도한 추상화는 피하되, 핵심은 지키기</li>
</ul>
<hr>
<p>클린 아키텍처는 <strong>&quot;완벽한 설계&quot;</strong> 가 아니라 <strong>&quot;비즈니스 로직을 보호하는 도구&quot;</strong> 다. 프로젝트의 규모와 복잡도에 맞게 적절히 적용하는 것이 중요하다. 처음부터 모든 것을 완벽하게 하려고 하지 말고, Domain을 중심을 두고 점진적으로 개선을 해나가면 좋을 거 같다.
<del>DDD 공부</del>? 😓~~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] 레이어드 아키텍처 (Layered Architecture)]]></title>
            <link>https://velog.io/@kwaktaemin_/Spring-Boot-%EB%A0%88%EC%9D%B4%EC%96%B4%EB%93%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-Layered-Architecture</link>
            <guid>https://velog.io/@kwaktaemin_/Spring-Boot-%EB%A0%88%EC%9D%B4%EC%96%B4%EB%93%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-Layered-Architecture</guid>
            <pubDate>Sun, 02 Nov 2025 12:18:37 GMT</pubDate>
            <description><![CDATA[<p>이커머스 프로젝트를 진행하면서 &quot;레이어드 아키텍처로 설계하라&quot; 는 요구사항이 있었다. 처음에는 단순히 Controller-Service-Repository 구조만 생각했는데, 제대로 된 레이어드 아키텍처는 훨씬 더 체계적이고 명확한 책임 분리가 필요했다.</p>
<p>특히 &quot;도메인 모델이 비즈니스 규칙을 포함해야 한다&quot;는 요구사항을 보고, Service에 모든 로직을 때려박는 방식이 얼마나 잘못된 것인지 깨달았다. 그래서 레이어드 아키텍처에 대해서 알아보고 싶어서 글에 정리를 해보게 됐다.</p>
<h2 id="🤔-문제-상황">🤔 문제 상황</h2>
<pre><code class="language-kotlin">// ❌ 흔히 보는 잘못된 코드
@Service
class OrderService(
    private val orderRepository: OrderRepository,
    private val stockRepository: StockRepository
) {
    fun createOrder(productId: Long, quantity: Int): Order {
        val stock = stockRepository.findById(productId)

        // Service에 비즈니스 규칙이 흩어짐
        if (stock.quantity &lt; quantity) {
            throw Exception(&quot;재고 부족&quot;)
        }
        stock.quantity -= quantity  // 직접 수정 💥

        val totalAmount = stock.price * quantity  // Service에서 계산 💥

        val order = Order(
            productId = productId,
            quantity = quantity,
            totalAmount = totalAmount
        )

        stockRepository.save(stock)
        return orderRepository.save(order)
    }
}</code></pre>
<p><strong>문제점</strong></p>
<ul>
<li>비즈니스 규칙이 Service에 흩어져있음.</li>
<li>Domain 객체가 단순 데이터 컨테이너로 전락 (Anemic Domain Model)</li>
<li>같은 로직이 여러 Service에 중복될 가능성</li>
<li>테스트하기 어려움</li>
</ul>
<h2 id="레이어드-아키텍처란">레이어드 아키텍처란?</h2>
<p><strong>관심사의 분리(Separation of Concerns)</strong> 를 통해 시스템을 계층으로 나누는 아키텍처 패턴이다. 각 계층은 명확한 책임을 가지며, 상위 계층은 하위 계층에만 의존한다.</p>
<h3 id="4계층-구조">4계층 구조</h3>
<pre><code>┌─────────────────────────────────────┐
│   Presentation Layer (표현 계층)      │  ← 외부와의 접점 (Controller, DTO)
├─────────────────────────────────────┤
│   Application Layer (응용 계층)       │  ← 비즈니스 흐름 조율 (Service)
├─────────────────────────────────────┤
│   Domain Layer (도메인 계층)          │  ← 핵심 비즈니스 규칙 (Domain Model)
├─────────────────────────────────────┤
│   Infrastructure Layer (인프라 계층)   │  ← 데이터 저장, 외부 연동
└─────────────────────────────────────┘</code></pre><h2 id="프로젝트-구조">프로젝트 구조</h2>
<pre><code>src/main/kotlin/com/hhplus/ecommerce/
│
├── presentation/                    # 1️⃣ Presentation Layer
│   └── order/
│       ├── OrderController.kt
│       └── dto/
│           ├── CreateOrderRequest.kt
│           └── OrderResponse.kt
│
├── application/                     # 2️⃣ Application Layer
│   └── order/
│       └── OrderService.kt
│
├── domain/                          # 3️⃣ Domain Layer (핵심!)
│   ├── order/
│   │   ├── Order.kt                # 비즈니스 규칙 포함
│   │   └── OrderRepository.kt      # 인터페이스만
│   │
│   └── product/
│       ├── Stock.kt                # 비즈니스 규칙 포함
│       └── ProductRepository.kt    # 인터페이스만
│
└── infrastructure/                  # 4️⃣ Infrastructure Layer
    └── order/
        └── InMemoryOrderRepository.kt  # 구현체</code></pre><h2 id="각-계층의-역할">각 계층의 역할</h2>
<h3 id="1️⃣-presentation-layer-표현-계층">1️⃣ Presentation Layer (표현 계층)</h3>
<p>역할: HTTP 요청/응답 처리, DTO 변환</p>
<pre><code class="language-kotlin">@RestController
@RequestMapping(&quot;/api/orders&quot;)
class OrderController(
    private val orderService: OrderService
) {
    @PostMapping
    fun createOrder(@RequestBody request: CreateOrderRequest): OrderResponse {
        // 1. 입력 검증 (DTO)
        // 2. Application Layer 호출
        val order = orderService.createOrder(
            userId = request.userId,
            items = request.items
        )
        // 3. DTO로 변환하여 응답
        return OrderResponse.from(order)
    }
}</code></pre>
<p>DTO가 Presentation에 있는 이유:</p>
<ul>
<li>외부 통신 전용 객체</li>
<li>API 스펙 변경이 Domain에 영향 없음</li>
<li>필요한 정보만 선택적으로 노출</li>
</ul>
<h3 id="2️⃣-application-layer-응용-계층">2️⃣ Application Layer (응용 계층)</h3>
<p>역할: 비즈니스 흐름 조율, 트랜잭션 관리</p>
<pre><code class="language-kotlin">// ✅ Service는 흐름만 조율
@Service
class OrderService(
    private val orderRepository: OrderRepository,
    private val stockRepository: StockRepository
) {
    @Transactional
    fun createOrder(userId: Long, items: List&lt;OrderItem&gt;): Order {
        // 1. 재고 조회
        val stock = stockRepository.findById(items.first().productId)
            ?: throw ProductNotFoundException()

        // 2. Domain 객체에 비즈니스 규칙 실행 위임
        stock.decrease(items.first().quantity)

        // 3. Domain 객체가 주문 생성 로직 처리
        val order = Order.create(
            userId = userId,
            items = items
        )

        // 4. 저장
        stockRepository.save(stock)
        return orderRepository.save(order)
    }
}</code></pre>
<p>Application Layer가 하는 일:</p>
<ul>
<li>✅ 여러 Repository에서 데이터 조회</li>
<li>✅ Domain 객체들 간의 상호작용 조율</li>
<li>✅ 트랜잭션 경계 관리</li>
<li>❌ 비즈니스 규칙 직접 구현 (Domain에 위임!)</li>
</ul>
<h3 id="3️⃣-domain-layer-도메인-계층---가장-중요-🌟">3️⃣ Domain Layer (도메인 계층) - 가장 중요! 🌟</h3>
<p>역할: 핵심 비즈니스 규칙 포함</p>
<pre><code class="language-kotlin">// domain/product/Stock.kt
data class Stock(
    val productId: Long,
    private var quantity: Int  // private으로 캡슐화
) {
    // ✅ 비즈니스 규칙을 Domain이 직접 처리
    fun decrease(amount: Int) {
        require(amount &gt; 0) { &quot;감소 수량은 양수여야 합니다&quot; }

        // 비즈니스 규칙: 재고 부족 시 예외
        if (quantity &lt; amount) {
            throw InsufficientStockException(
                &quot;재고 부족: 요청 $amount, 현재 $quantity&quot;
            )
        }

        quantity -= amount
    }

    fun isAvailable(amount: Int): Boolean = quantity &gt;= amount
}</code></pre>
<pre><code class="language-kotlin">// domain/order/Order.kt
data class Order(
    val id: Long?,
    val userId: Long,
    val items: List&lt;OrderItem&gt;,
    private val _totalAmount: BigDecimal
) {
    val totalAmount: BigDecimal get() = _totalAmount

    companion object {
        // ✅ 생성 로직도 Domain이 관리
        fun create(userId: Long, items: List&lt;OrderItem&gt;): Order {
            require(items.isNotEmpty()) { &quot;주문 상품이 비어있습니다&quot; }

            val totalAmount = items.sumOf { it.calculateAmount() }

            return Order(
                id = null,
                userId = userId,
                items = items,
                _totalAmount = totalAmount
            )
        }
    }
}</code></pre>
<p>Domain Layer의 특징:</p>
<ul>
<li>순수 비즈니스 로직만 포함</li>
<li>어떤 프레임워크에도 의존하지 않음 (Spring, JPA 등)</li>
<li>테스트가 가장 쉬움</li>
</ul>
<h3 id="4️⃣-infrastructure-layer-인프라-계층">4️⃣ Infrastructure Layer (인프라 계층)</h3>
<p>역할: Repository 인터페이스의 실제 구현</p>
<pre><code class="language-kotlin">// domain/product/ProductRepository.kt (인터페이스만!)
interface ProductRepository {
    fun save(product: Product): Product
    fun findById(id: Long): Product?
    fun findAll(): List&lt;Product&gt;
}
kotlin// infrastructure/product/InMemoryProductRepository.kt (구현체)
@Repository
class InMemoryProductRepository : ProductRepository {
    private val storage = ConcurrentHashMap&lt;Long, Product&gt;()
    private val idGenerator = AtomicLong(1)

    override fun save(product: Product): Product {
        val id = product.id ?: idGenerator.getAndIncrement()
        val newProduct = product.copy(id = id)
        storage[id] = newProduct
        return newProduct
    }

    override fun findById(id: Long): Product? {
        return storage[id]
    }

    override fun findAll(): List&lt;Product&gt; {
        return storage.values.toList()
    }
}</code></pre>
<hr>
<h2 id="🎯-의존성-역전-원칙-dip">🎯 의존성 역전 원칙 (DIP)</h2>
<p><strong>핵심:</strong> Domain이 Infrastructure를 의존하지 않고, Infrastructure가 Domain을 의존한다.</p>
<pre><code>전통적인 방식 (나쁜 예):
Service → Repository 구현체 (강한 결합)

레이어드 아키텍처 (좋은 예):
Service → Repository 인터페이스 ← Repository 구현체 (약한 결합)</code></pre><p>장점:</p>
<ul>
<li>InMemory → JPA → MongoDB로 변경해도 Domain, Application은 수정 불필요</li>
<li>Mock 객체로 쉽게 테스트 가능</li>
</ul>
<h2 id="⚠️-자주하는-실수">⚠️ 자주하는 실수</h2>
<h3 id="1-couponstatus를-infrastructure에-두는-실수">1. CouponStatus를 Infrastructure에 두는 실수</h3>
<pre><code>// ❌ 잘못된 위치
infrastructure/coupon/CouponStatus.kt

// ✅ 올바른 위치
domain/coupon/CouponStatus.kt</code></pre><p>이유: <code>CouponStatus</code>는 도메인 개념이지 구현 세부사항이 아니다!</p>
<h3 id="2-dto를-domain으로-착각하는-실수">2. DTO를 Domain으로 착각하는 실수</h3>
<pre><code>// ❌ 잘못된 위치
presentation/order/dto/Order.kt  // 이건 DTO여야 함

// ✅ 올바른 구조
domain/order/Order.kt                    // Domain Model
presentation/order/dto/OrderResponse.kt  // DTO</code></pre><h3 id="3-service에-비즈니스-로직을-넣는-실수">3. Service에 비즈니스 로직을 넣는 실수</h3>
<pre><code>// ❌ 나쁜 예: Service에 비즈니스 규칙
@Service
class StockService {
    fun decreaseStock(stock: Stock, amount: Int) {
        if (stock.quantity &lt; amount) {
            throw Exception(&quot;재고 부족&quot;)
        }
        stock.quantity -= amount  // 외부에서 직접 수정
    }
}

// ✅ 좋은 예: Domain 모델이 비즈니스 규칙 포함
class Stock {
    private var quantity: Int

    fun decrease(amount: Int) {
        if (quantity &lt; amount) {
            throw InsufficientStockException()
        }
        quantity -= amount
    }
}</code></pre><h3 id="✅-장점">✅ 장점</h3>
<h4 id="1-유지보수성">1. 유지보수성</h4>
<pre><code>* 변경의 영향 범위가 명확하게 제한됨
* 각 계층이 독립적으로 진화 가능</code></pre><h4 id="2-테스트-용이성">2. 테스트 용이성</h4>
<pre><code class="language-kotlin">// Domain 로직 단위 테스트 (프레임워크 없이)
class StockTest {
    @Test
    fun `재고가 부족하면 예외가 발생한다`() {
        val stock = Stock(productId = 1, quantity = 5)

        assertThrows&lt;InsufficientStockException&gt; {
            stock.decrease(10)
        }
    }
}</code></pre>
<h4 id="3-기술-독립성">3. 기술 독립성</h4>
<ul>
<li>Domain은 순수 비즈니스 로직만 포함</li>
<li>프레임워크 교체 가능 (Spring → Ktor 등)</li>
</ul>
<h4 id="📊-정리">📊 정리</h4>
<table>
<thead>
<tr>
<th>계층</th>
<th>역할</th>
<th>변경 이유</th>
<th>의존성</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Presentation</strong></td>
<td>HTTP 처리</td>
<td>API 스펙 변경</td>
<td>Application</td>
</tr>
<tr>
<td><strong>Application</strong></td>
<td>흐름 조율</td>
<td>비즈니스 프로세스 변경</td>
<td>Domain</td>
</tr>
<tr>
<td><strong>Domain</strong></td>
<td>비즈니스 규칙</td>
<td>도메인 규칙 변경</td>
<td>없음 (순수)</td>
</tr>
<tr>
<td><strong>Infrastructure</strong></td>
<td>데이터 저장</td>
<td>저장소 기술 변경</td>
<td>Domain</td>
</tr>
</tbody></table>
<p>핵심 원칙:</p>
<ul>
<li>Domain은 어디에도 의존하지 않음 (순수)</li>
<li>비즈니스 규칙은 Domain Model에</li>
<li>Application은 흐름 조율만</li>
<li>Infrastructure는 Domain 인터페이스 구현</li>
</ul>
<hr>
<p>제대로 된 레이어드 아키텍처를 적용하면, 코드의 책임이 명확해지고 테스트와 유지보수가 훨씬 쉬워진다. Service에 모든 걸 때려박는 습관에서 벗어나, Domain이 중심이 되는 설계를 하자!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[WIL] 항해 플러스 백엔드 1주차]]></title>
            <link>https://velog.io/@kwaktaemin_/WIL-%ED%95%AD%ED%95%B4-%ED%94%8C%EB%9F%AC%EC%8A%A4-%EB%B0%B1%EC%97%94%EB%93%9C-1%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@kwaktaemin_/WIL-%ED%95%AD%ED%95%B4-%ED%94%8C%EB%9F%AC%EC%8A%A4-%EB%B0%B1%EC%97%94%EB%93%9C-1%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 26 Oct 2025 04:27:40 GMT</pubDate>
            <description><![CDATA[<h2 id="📝-wil-테스트에-대한-올바른-인식--동시성-제어">📝 WIL: 테스트에 대한 올바른 인식 &amp; 동시성 제어</h2>
<h3 id="🎯-이번-주에-겪었던-문제">🎯 이번 주에 겪었던 문제</h3>
<p>이번 주차를 진행하며 다음과 같은 문제를 겪었다.
• <strong>TDD(Test Driven Development)</strong> 에 대한 잘못된 인식이 있었음
• 무조건 비즈니스 로직을 구현하기 전에 테스트 코드를 작성해야 한다고 오해하고 있었음
• 모든 기능에 대해 테스트 코드를 작성해야 한다고 생각했었음
• <strong>단일 책임 원칙(SRP)</strong> 에 따른 테스트 코드 작성 방식의 중요성을 깨달음
• 멀티 스레드 환경에서의 동시성 제어 방법 고민</p>
<h3 id="🔍-시도해본-것들">🔍 시도해본 것들</h3>
<p>문제를 해결하기 위해 아래와 같은 시도를 진행했다.
• <strong>PointService</strong> 에 단일 책임 원칙이 무시된 채 너무 많은 책임이 집중되어 있었는데,
validation 로직을 UserPoint 도메인 클래스로 분리
• 멀티 스레드 환경에서 동시성 제어를 위해 <strong>ReentrantLock</strong> 사용
• 테스트 코드 작성 시 단일 책임 원칙을 지키기 위해 각 클래스 단위로 테스트 코드 작성</p>
<h3 id="✅-문제를-어떻게-해결했는가">✅ 문제를 어떻게 해결했는가?</h3>
<p>• 서비스 레이어에서 중복 역할을 수행하던 validation 로직을 도메인 객체로 분리하면서 책임이
명확해지고 테스트 범위도 깔끔하게 정리됨
• <strong>ReentrantLock</strong>을 도입하여 동시에 여러 스레드가 접근하는 상황을 제어하여 데이터 정합성을
보장함
• 클래스 단위로 테스트 코드를 작성하면서 테스트 대상의 역할이 명확해지고 유지보수성이 향상됨</p>
<h3 id="💡-새롭게-알게-된-것">💡 새롭게 알게 된 것</h3>
<p>시도를 통해 아래와 같은 점을 새롭게 알게 된 것들이 있다.
• TDD는 구현 전에 무조건 테스트를 작성하는 것이 아니라, 설계를 가이드하고 피드백 루프를 촉진하는 철학적 접근이라는 것
• 모든 기능에 대해 테스트 코드를 작성할 필요는 없으며, 핵심 비즈니스 로직 중심으로 테스트하는 것이 효율적이라는 점
• 멀티 스레드 환경에서는 동시성 제어가 필수이며, 그렇지 않으면 데이터 정합성 문제가 발생할 수 있음</p>
<h3 id="🔥-지난-목표-회고">🔥 지난 목표 회고</h3>
<p>지난주에 설정했던 목표는 다음과 같은 결과가 나왔다.</p>
<p><strong>✅ 잘된 점</strong>
• 과제를 하루 전날 제출하여 일정 관리가 비교적 잘 됨</p>
<p><strong>❌ 아쉬운 점</strong>
• 동시성 제어에 대한 이해도가 아직 부족하다고 느낌
• 컨디션 관리를 못 했음
<u>→ 더 많은 학습이 필요함</u></p>
<h3 id="🎯-다음-목표-설정">🎯 다음 목표 설정</h3>
<p>📌 단기 목표</p>
<ul>
<li>이커머스 프로젝트 과제를 기간 내에 완수하기</li>
<li>동시성 제어 이론 + 실습 코드 더 많이 경험해보기</li>
</ul>
<p>반복적인 성장을 위한 루틴으로 가져갈 예정!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] 동시성 제어]]></title>
            <link>https://velog.io/@kwaktaemin_/Spring-Boot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4</link>
            <guid>https://velog.io/@kwaktaemin_/Spring-Boot-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4</guid>
            <pubDate>Tue, 21 Oct 2025 11:21:04 GMT</pubDate>
            <description><![CDATA[<h2 id="spring-boot에서-동시성-제어하기">spring Boot에서 동시성 제어하기</h2>
<hr>
<p>Study 과제 중 TDD를 기반으로 테스트 코드를 작성하는데, 포인트 충전/사용 시스템을 <code>Kotlin</code> 으로
개발하면서 요구 사항에 <strong>&quot;동시성 문제 해결&quot;</strong> 이 있었다. 실제 데이터베이스를 사용하는게 아닌
인메모리를 사용하게 되면서 DB의 Transaction 없이 동시성을 처리를 해야했다.</p>
<p>그래서 Kotlin, Java에 내장되어있는 lock 매커니즘을 이용해서 처리를 해서 정리를 해보려고 한다.</p>
<h2 id="문제-상황">문제 상황</h2>
<hr>
<pre><code class="language-kotlin">class PointService(
    private val userPointTable: UserPointTable,
    private val pointHistoryTable: PointHistoryTable
) {
    fun chargeUserPoint(userId: Long, amount: Long): UserPoint {
        val current = userPointTable.selectById(userId)  // 현재: 1000
        val newPoint = current.point + amount             // 1000 + 100 = 1100
        return userPointTable.insertOrUpdate(userId, newPoint)
    }
}</code></pre>
<p>만약 두 명의 사용자가 동시에 100 포인트를 충전한다면?</p>
<ul>
<li>예상: 1000 -&gt; 1200</li>
<li>실제: 1000 -&gt; 1100</li>
</ul>
<h3 id="race-condition-발생">Race Condition 발생</h3>
<pre><code class="language-kotlin">Thread A: selectById(1) → 1000 읽음
Thread B: selectById(1) → 1000 읽음
Thread A: 1000 + 100 = 1100
Thread B: 1000 + 100 = 1100
Thread A: insertOrUpdate(1, 1100)
Thread B: insertOrUpdate(1, 1100) 💥 덮어쓰기!</code></pre>
<p>Spring Boot는 멀티 스레드 환경이기 때문에 각 HTTP 요청이 별도 스레드에서 처리되며, 같은 데이터에 여러 스레드가 동시 접근할 수 있다.</p>
<h2 id="해결-방법">해결 방법</h2>
<hr>
<p>내장되어 있는 <code>ConcurrentHashMap</code>을 사용해서 해결을 했다.</p>
<pre><code class="language-kotlin">// ❌ HashMap은 thread-safe하지 않음
private val userLocks = HashMap&lt;Long, Any&gt;()

// ✅ ConcurrentHashMap은 thread-safe
private val userLocks = ConcurrentHashMap&lt;Long, Lock&gt;()</code></pre>
<p><code>if 조건문</code> 으로 처리를 했던 코드는 <code>require()</code> 함수로 대체를 한다.</p>
<pre><code class="language-kotlin">// Kotlin답게 - 간결하고 명확
require(amount &gt; 0) { &quot;충전 금액은 양수여야 합니다.&quot; }
require(amount % 100 == 0L) { &quot;포인트 사용은 100 단위로만 가능합니다.&quot; }

// 조건이 true여야 할 것을 작성!</code></pre>
<p>장점은 다음과 같다.</p>
<ul>
<li>✅ kotlin의 표현식 기반 문법과 잘 어울림</li>
<li>✅ 간결하고 직관적</li>
<li>✅ JVM 최적화 지원</li>
<li>✅ 별도의 라이브러리 불필요</li>
</ul>
<h2 id="마무리">마무리</h2>
<hr>
<p>Kotlin으로 Spring Boot를 개발한다면, 언어의 특성을 살려 더 간결하고 안전한 동시성 제어를 구현할 수 있다. Redis나 외부 라이브러리를 이용하지 않고 동시성 제어를 할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[NestJS] GraphQL File upload]]></title>
            <link>https://velog.io/@kwaktaemin_/NestJS-GraphQL-File-upload</link>
            <guid>https://velog.io/@kwaktaemin_/NestJS-GraphQL-File-upload</guid>
            <pubDate>Tue, 14 Oct 2025 12:08:55 GMT</pubDate>
            <description><![CDATA[<h2 id="왜-graphql">왜 GraphQL?</h2>
<hr>
<p>원래 회사에서 <code>REST API</code>를 통해서 클라이언트와 API 통신을 하고 있었는데 불필요한 <code>join</code>문과 수시로 바뀌는 기획에 대응을 하기 위해서 클라이언트 입맛에 맞게 데이터를 조회할 수 있도록 <strong>GraphQL</strong>을
도입하기로 했다.</p>
<p>사용을 하면서 GraphQL로 file을 upload 하는 API를 개발할 때 2022년도에 구현을 하지 못해서 file
upload는 <code>REST API</code>를 사용했던 경험이 있다.</p>
<p>하지만 이번에는 온전히 <code>GraphQL</code>을 사용하면서 file upload 기능을 구현하고 싶어서 글을 정리하게 됐다.</p>
<h2 id="graphql로-file-upload-하기">GraphQL로 file upload 하기</h2>
<hr>
<p>먼저 <code>GraphQL</code>에서 <code>Mutation</code>을 이용하여 file upload를 구현 할 것이다. file을 upload를 하기 위해서 <code>graphql-upload</code> 라이브러리를 사용할 것이다.</p>
<pre><code class="language-bash">&gt; npm i graphql-upload
&gt; npm i -D @types/graphql-upload</code></pre>
<p>위 명령어를 통해서 <code>graphql-upload</code> 라이브러리를 설치해준다. (<code>yarn</code>으로 설치해도 됨)
이때, nestjs <code>src/main.ts</code> 에서 graphql upload express를 활성화해줘야 한다.</p>
<pre><code class="language-ts">import { NestFactory } from &#39;@nestjs/core&#39;;
import { AppModule } from &#39;./app.module&#39;;
import graphqlUploadExpress from &#39;graphql-upload/graphqlUploadExpress.mjs&#39;;

async function bootstrap() {
    const app = await NestFactory.create(AppModule);

    app.use(graphqlUploadExpress());
    await app.listen(3000);
}

bootstrap();</code></pre>
<p>이때, 주의사항은 <code>import graphqluploadExpress from &#39;graphql-upload&#39;</code> 혹은
<code>import { graphqlUploadExpress } from &#39;graphql-upload&#39;</code>
이렇게 import를 하게 되면 해당 모듈을 찾지 못하는 에러가 발생한다.</p>
<p>무조건 <code>graphql-upload/graphqlUploadExpress.mjs</code>에서 import를 해야한다!</p>
<p>다음 단계는 이제 file을 upload 할 <code>resolver</code>에서 파라미터 처리를 해줘야 하는데 기존제 REST API를 사용할 때 처럼 file과 관련된 데코레이터를 붙여줘야 한다.</p>
<pre><code class="language-ts">import { Args, Mutation, Resolver } from &#39;@nestjs/graphql&#39;
import GraphqlUpload, { FileUpload } from &#39;graphql-upload/GraphQLUpload.mjs&#39;

@Resolver()
export class UploadResolver {
    constructor() {}

    @Mutation(() =&gt; String)
    async uploadFile(@Args(&#39;file&#39;, { type: () =&gt; GraphqlUpload }) file: FileUpload) {
        console.log(file);
    }
}</code></pre>
<p>이렇게 앞서 <code>src/main.ts</code>에서 import 한 거 처럼 특정 경로에서 import를 해준다.
위 예제 코드를 작성하고 <code>playground</code> 혹은 <code>graphiql</code> 이 곳에서 테스트를 할 수 없다.
왜냐하면 이 두 곳은 file upload를 지원하지 않는다.</p>
<p>그래서 찾은게 <code>Altair</code>다. UI/UX가 훌륭하다. <code>Altair</code>를 사용해서 file을 upload 하고,
schema를 작성해준다.</p>
<p><img src="https://velog.velcdn.com/images/kwaktaemin_/post/50f47bc6-9832-4adf-aa8d-371932d9f785/image.png" alt=""></p>
<p>위에 이미지처럼 변수를 만들어서 사용해도 좋고 편한 방법이 있다면 편한 방법으로 하면된다.
이렇게 설정을 했으면 이제 server로 request를 해주면 <code>console.log(file)</code>을 통해 터미널을 확인해보면</p>
<pre><code class="language-json">{
    filename: string;
    mimetype: string;
    encoding: string;
}</code></pre>
<p>이런식으로 file 정보를 얻을 수 있다. 이 값을 통해서 AWS S3 Bucket에 upload를 할 수 있다.</p>
<h2 id="마무리">마무리</h2>
<hr>
<p>이처럼 GraphQL을 이용해서 file upload를 하는 방법에 대해서 알아봤다. 별거 아니지만 과거에 완료하지 못한 기능을 구현하게 돼서 REST API에 의존하지 않을 수 있었다.
<del>과거에 구현하지 못한 GraphQL file upload를 구현해서 뿌듯함,,</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] Local에서 Docker image build 후 추출 및 압축 처리]]></title>
            <link>https://velog.io/@kwaktaemin_/Docker-Local%EC%97%90%EC%84%9C-Docker-image-build-%ED%9B%84-%EC%B6%94%EC%B6%9C-%EB%B0%8F-%EC%95%95%EC%B6%95-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@kwaktaemin_/Docker-Local%EC%97%90%EC%84%9C-Docker-image-build-%ED%9B%84-%EC%B6%94%EC%B6%9C-%EB%B0%8F-%EC%95%95%EC%B6%95-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Tue, 14 Oct 2025 11:53:19 GMT</pubDate>
            <description><![CDATA[<h2 id="local에서-docker-image-build-후-추출-및-압축-처리하기">Local에서 Docker image build 후 추출 및 압축 처리하기</h2>
<hr>
<p>Docker image를 local에서 build를 한 후 이를 저장하고 압축해서 보관하거든 다른 환경으로 이동할 수 있다. 솔직히 DockerHub에 image를 등록해서 사용할 수 있지만 리소스가 부족한 환경에서는 이 방법이 괜찮다고 생각했다.</p>
<p>아래에서는 image build 후 추출 및 압축 처리를 하는 방법을 설명을 하려고 한다.</p>
<h2 id="1-docker-image-build">1. Docker image build</h2>
<hr>
<p>먼저 Docker image를 build하기 위해서 <code>Dockerfile</code>을 작성해야 한다. (예제는 nodeJS)</p>
<pre><code class="language-Dockerfile">FROM node:18-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm install -g typescript ts-node

EXPOSE 8080

CMD [&quot;npm&quot;, &quot;start&quot;]</code></pre>
<h2 id="2-docker-image-저장">2. Docker image 저장</h2>
<hr>
<p>Dockerfile을 작성하고 나면 이 파일을 이용해서 image build를 해준다.</p>
<pre><code class="language-bash">docker save -o my-image.tar my-image</code></pre>
<p>만약 여러 image를 하나의 압축파일로 만들고 싶을 때 <code>-o &quot;압축 파일 이름&quot;</code> 뒤에 image 이름들을 적어주면 된다.</p>
<pre><code class="language-bash">docker save -o my-images.tar my-image my-image2</code></pre>
<h2 id="3-압축한-docker-image-load">3. 압축한 Docker image load</h2>
<hr>
<p>압축한 이미지 파일을 다시 이미지로 불러오기 위해서 아래와 같은 명령어를 사용할 수 있다.</p>
<pre><code class="language-bash">docker load &lt; my-image.tar</code></pre>
<p>다시 docker에 image를 build가 됐고, 확인을 하기 위해서는</p>
<pre><code class="language-bash">docker image</code></pre>
<p>명령어를 통해서 확인할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Trouble Shooting] Docker compose]]></title>
            <link>https://velog.io/@kwaktaemin_/Trouble-Shooting-Docker-compose-1vz0yrr6</link>
            <guid>https://velog.io/@kwaktaemin_/Trouble-Shooting-Docker-compose-1vz0yrr6</guid>
            <pubDate>Tue, 14 Oct 2025 11:45:30 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<hr>
<p>2개의 서버와 mongoDB, MySQL을 <code>docker-compose</code>를 이용해서 build를 하고, 실행하는 단계에서 문제가 발생했다. MySQL과 연결되어 있는 한 서버에서 <strong>Database Connection</strong> 관련 에러가 발생했고,</p>
<p>이 문제는 MySQL docker image가 build 되면서 docker container가 준비 상태가 아니라서 발생한 문제였다.</p>
<h2 id="해결">해결</h2>
<hr>
<p>해결하기 위해서 <code>depends_on</code>만 사용하지 않고, <code>healthcheck</code> 옵션을 사용해서 MySQL docker container가 준비 상태에 도달할 때까지 MySQL을 사용하는 docker container server는 대기를 하도록 했다.</p>
<pre><code class="language-yaml">  mysql:
    image: mysql:latest
    container_name: mysql
    restart: always
    ports:
      - &#39;3309:3306&#39;
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_DATABASE}
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - custom_network
    healthcheck:
      test: [&#39;CMD&#39;, &#39;mysqladmin&#39;, &#39;ping&#39;, &#39;-h&#39;, &#39;localhost&#39;, &#39;-uroot&#39;, &#39;-proot&#39;]
      timeout: 20s
      retries: 5</code></pre>
<p>MySQL docker container가 <code>healthcheck</code> 옵션을 사용하면서 준비 상태까지 도달하게 되면 MySQL을 사용하는 docker container server는 그제서야 docker container 생성을 실행하게 된다.</p>
<pre><code class="language-yaml">    depends_on:
      mysql:
        condition: service_healthy
      mongo:
        condition: service_started
    networks:
      - custom_network</code></pre>
<p>MySQL을 사용하는 docker container server에서는 <code>depends_on</code> 옵션에서 <code>mysql</code> docker container service가 <code>service_healthy</code> 상태에 도달할 때까지 대기하도록 했다.</p>
<p>이렇게 <code>yaml</code> 파일을 작성하고 <code>docker-compose up -d</code> 명령어를 실행하게 되면 기대했던 대로 MySQL docker container가 준비 상태에 도달할 때 까지 대기를 하면서 <strong>Database Connection</strong> 관련 에러가 발생하지 않게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] ECS EC2 배포(1) - VPC]]></title>
            <link>https://velog.io/@kwaktaemin_/AWS-ECS-EC2-%EB%B0%B0%ED%8F%AC1-VPC</link>
            <guid>https://velog.io/@kwaktaemin_/AWS-ECS-EC2-%EB%B0%B0%ED%8F%AC1-VPC</guid>
            <pubDate>Wed, 12 Feb 2025 01:27:56 GMT</pubDate>
            <description><![CDATA[<p>요즘에 ECS를 통해서 배포를 할 때 <strong>Fargate</strong>를 사용하는 경우가 많다. <strong>Fargate</strong>는 요청에 따라서 자동으로 확장되는 서버를 제공하면서 서버리스 형태의
서비스를 제공한다.</p>
<p>하지만 <strong>Fargate</strong>는 <strong>EC2</strong> 대비 비용이 비교적 비싼 편이다. 따라서 초기 비용을 줄이기 위해 <strong>EC2</strong>를 사용하는 경우가 많다.</p>
<p>이 포스트에서는 <strong>EC2</strong>를 사용해서 배포하는 방법을 알아보고자 한다.</p>
<h2 id="vpc">VPC</h2>
<p>ECS로 배포를 하기 전에 먼저 VPC를 생성해야 한다.</p>
<p>VPC는 네트워크 범위를 정의하고, 서브넷, 인터넷 게이트웨이, 라우팅 테이블 등을 생성하여 네트워크 환경을 구성한다.</p>
<h3 id="vpc-생성">VPC 생성</h3>
<p>vpc를 생성하기 위해서 <u>IPv4 CIDR 블록</u>또는 <u>IP주소 범위</u>를 입력해야 한다.</p>
<p>수동으로 입력할 수 있지만 CIDR 블록의 크기는 <u>/16에서 /28 사이로 입력을 해야 한다.</u></p>
<p><img src="/assets/img/post/screenshot-2025-02-11-15-13-42.png" alt="VPC 생성"></p>
<p>기본적으로 제공되는 <strong>10.0.0.0/16</strong> 블록을 사용할 수 있지만 사용자가 원하는 블록을 입력할 수 있다.</p>
<p><code>10.0.0.0</code>은 IP 주소를 가리키고, <code>/16</code>은 네트워크 범위를 가리키는데, 이 때 네트워크 범위는 2^(32-n) 개의 네트워크
범위를 가지게 된다.</p>
<p>따라서 <code>/16</code>은 2^(32-16) = 65536개의 네트워크 범위를 가지게된다.</p>
<p><img src="/assets/img/post/screenshot-2025-02-11-16-29-32.png" alt="VPC 생성1"></p>
<p>위와 같이 IP주소와 범위를 지정해주고 생성을 해주면 된다.</p>
<p>IP주소는 어떤 IP 주소를 할당해야 하는지 헷갈릴 수 있다.</p>
<h4 id="어떤-ip-주소를-할당해야-할지">어떤 IP 주소를 할당해야 할지?</h4>
<p>VPC에서는 CIDR 블록을 선택할 떄 고려해야 할 사항이 있다.</p>
<h5 id="1-사설-ip-대역을-사용">1. 사설 IP 대역을 사용</h5>
<p>VPC에서는 공개 IP가 아니라 사설 IP를 사용한다.
<strong>RFC 1918</strong> 표준에 따라 아래와 같은 사설 IP 대역을 사용해야 한다.</p>
<ul>
<li>10.0.0.0 (AWS에서 가장 많이 사용됨)</li>
<li>172.16.0.0</li>
<li>192.168.0.0</li>
</ul>
<blockquote>
<p>💡 AWS에서는 10.0.0.0 IP 대역을 많이 사용한다.</p>
</blockquote>
<h5 id="2-예상-네트워크-크기에-맞게-cidr-블록-선택">2. 예상 네트워크 크기에 맞게 CIDR 블록 선택</h5>
<p>VPC 내에서 EC2, RDS, ALB, Lambda, ECS 등 AWS 리소스가 몇 개나 필요한지 예측을 해야 한다.</p>
<ul>
<li>대규모 서비스: 10.0.0.0/16 (최대 65536개의 IP 주소)</li>
<li>중규모 서비스: 10.0.0.0/20 (최대 4096개의 IP 주소)</li>
<li>소규모 서비스: 10.0.0.0/24 (최대 256개의 IP 주소)</li>
</ul>
<blockquote>
<p>💡 서브넷을 분할할 계획이라면 <code>/16</code>, 서브넷에서 <code>/24</code>를 사용하는 것이 좋다.</p>
</blockquote>
<h5 id="3-서브넷을-어떻게-나눌-것인지">3. 서브넷을 어떻게 나눌 것인지</h5>
<p>VPC내에서 서브넷을 나누어 사용할 계획이라면 미리 CIDR 블록의 범위를 크게 설정하는 것이 좋다.
예를들어 VPC를 <code>10.0.0.0/16</code>을 사용하고, 서브넷은 <code>/24</code> 범위로 나누는 경우가 많다.</p>
<table>
<thead>
<tr>
<th>VPC CIDR 블록</th>
<th>서브넷</th>
</tr>
</thead>
<tbody><tr>
<td>10.0.0.0/16</td>
<td>10.0.1.0/24 (public subnet)</td>
</tr>
<tr>
<td></td>
<td>10.0.2.0/24 (private subnet)</td>
</tr>
<tr>
<td></td>
<td>10.0.3.0/24 (database subnet)</td>
</tr>
</tbody></table>
<blockquote>
<p>💡 VPC는 큰 범위로 설정하고, 서브넷은 보다 작은 범위로 설정하는 것이 관리하기 편하다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] Local에서 Docker 이미지 빌드 후 추출 및 압축 처리하는 방법]]></title>
            <link>https://velog.io/@kwaktaemin_/Docker-Local%EC%97%90%EC%84%9C-Docker-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B9%8C%EB%93%9C-%ED%9B%84-%EC%B6%94%EC%B6%9C-%EB%B0%8F-%EC%95%95%EC%B6%95-%EC%B2%98%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@kwaktaemin_/Docker-Local%EC%97%90%EC%84%9C-Docker-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B9%8C%EB%93%9C-%ED%9B%84-%EC%B6%94%EC%B6%9C-%EB%B0%8F-%EC%95%95%EC%B6%95-%EC%B2%98%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Fri, 07 Feb 2025 08:28:16 GMT</pubDate>
            <description><![CDATA[<p>Docker 이미지를 로컬에서 빌드를 한 후에 이를 저장하고 압축해서 보관하거든 다른 환경으로 이동할 수 있다. 솔직히 DockerHub에
이미지를 등록해서 사용하는 방법이 나을 수 있지만 리소스가 부족한 환경에서는 이 방법이 괜찮다고 생각한다.</p>
<p>아래는 이미지 빌드 후 추출 및 압축 처리를 하는 벙법을 설명을 하고자 한다.</p>
<h2 id="1-docker-이미지-빌드">1. Docker 이미지 빌드</h2>
<p>먼저 Docker 이미지를 빌드하기 위해서 <code>Dockerfile</code>을 작성해야 한다. (여기서는 node로 예시를 들겠다.)</p>
<pre><code class="language-Dockerfile">FROM node:18-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm install -g typescript ts-node

EXPOSE 8080

CMD [&quot;npm&quot;, &quot;start&quot;]</code></pre>
<p>Dockerfile을 작성하고 나면 이 파일을 이용해서 이미지를 빌드를 해준다.</p>
<pre><code class="language-bash">docker build -t my-image .</code></pre>
<p>my-image:1.0을 해주면 version을 1.0으로 지정해줄 수 있고, 적지 않으면 latest로 지정된다.</p>
<p><code>.</code>은 현재 디렉토리를 가리킨다.</p>
<blockquote>
<p>이미지 목록을 확인하려면 <code>docker images</code> 명령어를 사용하면 된다.</p>
</blockquote>
<h2 id="2-docker-이미지-저장">2. Docker 이미지 저장</h2>
<p>이미지를 빌드 후에 이미지 저장을 위해서 아래와 같은 명령어를 사용한다.</p>
<pre><code class="language-bash">docker save -o my-image.tar my-image</code></pre>
<p>만약 여러 이미지를 하나의 압축파일로 만들고 싶을 땐 <code>-o &quot;압축 파일 이름&quot;</code> 뒤에 이미지 이름들을 적어주면 된다.</p>
<pre><code class="language-bash">docker save -o my-images.tar my-image my-image2</code></pre>
<ol start="3">
<li>압축한 docker 이미지 load</li>
</ol>
<p>압축한 이미지 파일을 다시 이미지로 불러오기 위해서 아래와 같은 명령어를 사용할 수 있다.</p>
<pre><code class="language-bash">docker load &lt; my-image.tar</code></pre>
<p>다시 docker에 image를 빌드가 됐고, 확인하고 싶으면</p>
<pre><code class="language-bash">docker images</code></pre>
<p>명령어를 사용하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Trouble Shooting] Docker compose]]></title>
            <link>https://velog.io/@kwaktaemin_/Trouble-Shooting-Docker-compose</link>
            <guid>https://velog.io/@kwaktaemin_/Trouble-Shooting-Docker-compose</guid>
            <pubDate>Fri, 07 Feb 2025 07:46:11 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p>2개의 서버와 mongoDB, MySQL을 <code>docker-compose</code>를 이용해서 build 하고 실행하는 단계에서 문제가 발생했다.
MySQL과 연결되어 있는 한 서버에서 <strong>Database Connection</strong> 관련 에러가 발생했고, 이 문제는 MySQL image가 build가 되면서 container가 준비 상태가 아니라서 발생한 문제였다.</p>
<p><code>depends_on</code> 옵션을 사용했지만 <code>depends_on</code>은 단순히 실행 순서를 보장하는 것이며 container가 준비 상태인지 확인하지 않아서 발생한 문제였다.</p>
<h2 id="해결">해결</h2>
<p>해결하기 위해서 <code>depends_on</code>만 사용하지 않고, <code>healthcheck</code> 옵션을 사용해서 MySQL container가 준비 상태에 도달할 때까지 MySQL을 사용하는 서버는 대기를 하도록 했다.</p>
<pre><code class="language-yaml">  mysql:
    image: mysql:latest
    container_name: mysql
    restart: always
    ports:
      - &#39;3309:3306&#39;
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_DATABASE}
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - custom_network
    healthcheck:
      test: [&#39;CMD&#39;, &#39;mysqladmin&#39;, &#39;ping&#39;, &#39;-h&#39;, &#39;localhost&#39;, &#39;-uroot&#39;, &#39;-proot&#39;]
      timeout: 20s
      retries: 5</code></pre>
<p>MySQL container가 <code>healthcheck</code> 옵션을 사용하면서 준비 상태까지 도달하게 되면 MySQL을 사용하는 서버는 그제서야 container 생성을 실행하게 된다.</p>
<pre><code class="language-yaml">    depends_on:
      mysql:
        condition: service_healthy
      mongo:
        condition: service_started
    networks:
      - custom_network</code></pre>
<p>MySQL을 사용하는 서버에서는 <code>depends_on</code> 옵션에서 <code>mysql</code> 서비스가 <code>service_healthy</code> 상태가 될 때까지 대기하도록 했다.</p>
<p>이렇게 <code>yaml</code> 파일을 작성하고 <code>docker-compose up -d</code> 명령어를 실행하게 되면 기대했던 대로 MySQL container가 준비 상태가 될 때 까지 대기를 하면서</p>
<p><strong>Database Connection</strong> 관련 에러가 발생하지 않게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[NodeJS] FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory]]></title>
            <link>https://velog.io/@kwaktaemin_/NodeJS-FATAL-ERROR-Reached-heap-limit-Allocation-failed-JavaScript-heap-out-of-memory</link>
            <guid>https://velog.io/@kwaktaemin_/NodeJS-FATAL-ERROR-Reached-heap-limit-Allocation-failed-JavaScript-heap-out-of-memory</guid>
            <pubDate>Fri, 20 Dec 2024 07:23:46 GMT</pubDate>
            <description><![CDATA[<p>NodeJS를 이용해서 대용량 데이터 처리 관련해서 개발을 하다 아래와 같은 에러 메시지를 만났다.</p>
<pre><code class="language-shell">FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory</code></pre>
<p>이 에러는 그냥 말 그대로 메모리 사용량이 증가하면서 Javascript엔진이 터져버린 것이다.</p>
<p>NodeJs의 기본적인 메모리 제한은 512MB다. 그럼 내가 처리를 하려고 하는 이 API가 메모리 한계를 뚫은 것...</p>
<p>메모리 누수가 존재하다 생각해서 코드를 봤지만 누수가 될만한 코드는 없었고, 단지 180만번의 반복문을 돌려야 하는 코드로 인해서 메모리 점유율이 상당했던것.</p>
<p>해결 방법으로는</p>
<ol>
<li>더 큰 메모리를 할당한다.</li>
<li>메모리 누수를 개선한다.</li>
</ol>
<p>2번 방법은 찾아봤을 때 해당되지 않아서 1번 방법을 사용을 해보려고 한다.
우선 현재 메모리를 확인해 줄 필요가 있다.</p>
<pre><code class="language-shell">node -e &#39;console.log(v8.getHeapStatistics().heap_size_limit/(1024*1014)&#39;</code></pre>
<p>그 후 아래와 같이 늘리고 싶은 용량을 적으면 메모리 용량이 변경된다.</p>
<pre><code class="language-shell">export NODE_OPTIONS=--max_old_space_size=5000</code></pre>
<p>이렇게 매번 대용량의 작업을 할 때 용량을 늘려줘야 하는건가.. 맘 같아서 캐시 메모리에 넣어서 작업을 진행하던지 하고 싶지만</p>
<p>이 프로젝트는 회사에서 단발성이기 때문에 이것저것 도입을하기가 무척 애매모호한 상황이라서 어려운 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] JWT 발급 오류]]></title>
            <link>https://velog.io/@kwaktaemin_/Spring-Boot-JWT-%EB%B0%9C%EA%B8%89-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@kwaktaemin_/Spring-Boot-JWT-%EB%B0%9C%EA%B8%89-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Thu, 10 Oct 2024 09:07:25 GMT</pubDate>
            <description><![CDATA[<h3 id="개요">개요</h3>
<hr>
<p>Spring Boot를 이용해서 Spring Security 실습을 하는 도중 JWT 발급 관련해서 아래와 같은 에러가 발생했다.</p>
<blockquote>
<p>Unable to determine a suitable MAC or Signature algorithm for the specified key using available heuristics: either the key size is too weak be used with available algorithms, or the key size is unavailable (e.g. if using a PKCS11 or HSM (Hardware Security Module) key store). If you are using a PKCS11 or HSM keystore, consider using the JwtBuilder.signWith(Key, SecureDigestAlgorithm) method instead.</p>
</blockquote>
<p>위 내용과 같이 에러 메시지를 자세히 살펴보니 <strong>secretKey 사이즈가 부족</strong>하다는 것이었다.</p>
<h3 id="해결-방법">해결 방법</h3>
<hr>
<p>해결 방법은 간단하게도 <u>secretKey의 사이즈를 더 늘려주었더니 해결</u>이되었다. 참고로 <strong>HS256 알고리즘</strong>을 사용하는 경우, <strong>SecretKey</strong>는 최소한 <strong>256비트 (32바이트)</strong> 이상의 길이를 가져야 한다.</p>
]]></description>
        </item>
    </channel>
</rss>