<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>wongi-kim.log</title>
        <link>https://velog.io/</link>
        <description>혼자 공부하는 블로그라 부족함이 많아요 https://www.notion.so/18067a27ac7e4f4790dde645fb3bf3d3?pvs=4</description>
        <lastBuildDate>Tue, 28 Apr 2026 05:50:27 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>wongi-kim.log</title>
            <url>https://velog.velcdn.com/images/wongi-kim/profile/e0187c23-e9d2-4685-91e5-fb1792ea8ef3/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. wongi-kim.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/wongi-kim" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Kotlin 문법 정리 - 클래스 & 프로퍼티]]></title>
            <link>https://velog.io/@wongi-kim/Kotlin-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC-%ED%81%B4%EB%9E%98%EC%8A%A4-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0</link>
            <guid>https://velog.io/@wongi-kim/Kotlin-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC-%ED%81%B4%EB%9E%98%EC%8A%A4-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0</guid>
            <pubDate>Tue, 28 Apr 2026 05:50:27 GMT</pubDate>
            <description><![CDATA[<h1 id="클래스와-프로퍼티-class--property">클래스와 프로퍼티 (Class &amp; Property)</h1>
<p>자바에서는 클래스를 만들 때 필드(Field), 생성자(Constructor), Getter/Setter를 모두 작성해야 하지만, 코틀린은 이를 프로퍼티(Property)라는 개념으로 통합하여 한 줄로 해결한다.</p>
<h2 id="1-클래스-선언과-생성자">1. 클래스 선언과 생성자</h2>
<p>코틀린은 클래스 이름 바로 옆에 생성자를 정의하는 주 생성자(Primary Constructor) 방식을 사용한다.</p>
<pre><code class="language-kotlin">// 코틀린: 단 한 줄로 필드 생성, 생성자, Getter/Setter까지 해결
class User(val name: String, var age: Int)

// 자바였다면?
/*
public final class User {
    private final String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() { return name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}
*/</code></pre>
<h2 id="2-프로퍼티-property">2. 프로퍼티 (Property)</h2>
<p>코틀린에서는 클래스의 필드를 프로퍼티라고 부른다.</p>
<p>val 프로퍼티: 내부적으로 private 필드 + public Getter를 생성한다.</p>
<p>var 프로퍼티: 내부적으로 private 필드 + public Getter + public Setter를 생성한다.</p>
<p>사용할 때는 자바처럼 .getName()을 호출할 필요 없이 점(.) 연산자로 바로 접근한다.</p>
<pre><code class="language-kotlin">val user = User(&quot;John Doe&quot;, 20)
println(user.name) // 내부적으로 getName() 호출
user.age = 21      // 내부적으로 setAge() 호출</code></pre>
<h2 id="3-커스텀-getter--setter">3. 커스텀 Getter / Setter</h2>
<p>기본적인 Getter/Setter 외에 특정한 로직이 필요할 때 직접 정의할 수 있다. 이때 실제 값을 담고 있는 field (Backing Field)라는 키워드를 사용한다.</p>
<pre><code class="language-kotlin">class Person(var name: String) {
    var nickname: String = &quot;&quot;
        set(value) {
            // 입력값을 대문자로 바꿔서 저장하는 로직
            field = value.uppercase()
        }

    val isAdult: Boolean
        get() = this.age &gt;= 20 // 별도의 필드 없이 계산된 값만 반환 (Custom Getter)
}</code></pre>
<h2 id="4-초기화-블록-init">4. 초기화 블록 (init)</h2>
<p>주 생성자에는 코드를 작성할 공간이 없다. 만약 객체가 생성되는 시점에 로직을 실행하고 싶다면 init 블록을 사용한다.</p>
<pre><code class="language-kotlin">class Student(val name: String) {
    init {
        println(&quot;학생 객체가 생성되었습니다: $name&quot;)
        require(name.isNotEmpty()) { &quot;이름은 비어있을 수 없습니다.&quot; }
    }
}</code></pre>
<h2 id="5-데이터-클래스-data-class">5. 데이터 클래스 (Data Class)</h2>
<p>데이터를 담는 것이 목적인 클래스라면 data 키워드만 붙여보자. toString(), equals(), hashCode(), copy() 등을 컴파일러가 자동으로 만들어준다. (자바의 Record와 유사)</p>
<pre><code class="language-kotlin">data class Book(val title: String, val author: String)

val book1 = Book(&quot;Kotlin&quot;, &quot;JetBrains&quot;)
println(book1) // Book(title=Kotlin, author=JetBrains) 가 예쁘게 출력됨</code></pre>
<h3 id="요약하자면">요약하자면?</h3>
<ul>
<li>간결성: 주 생성자를 통해 필드 선언과 동시에 초기화가 가능하다.</li>
<li>프로퍼티: Getter/Setter를 일일이 만들지 않아도 코틀린이 알아서 생성해 준다.</li>
<li>field: 커스텀 Setter에서 실제 값을 가리키기 위해 사용하는 예약어이다.</li>
<li>data class: 데이터를 다루는 클래스를 위한 최고의 편의 기능을 제공한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin 문법 정리 - 함수]]></title>
            <link>https://velog.io/@wongi-kim/Kotlin-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC-%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@wongi-kim/Kotlin-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC-%ED%95%A8%EC%88%98</guid>
            <pubDate>Mon, 27 Apr 2026 01:50:19 GMT</pubDate>
            <description><![CDATA[<h1 id="함수-function">함수 (Function)</h1>
<p>코틀린에서 함수는 독립적으로 존재할 수 있으며, 자바보다 훨씬 간결하고 유연하게 선언할 수 있어 &#39;코틀린의 꽃&#39;이라 불린다.</p>
<h2 id="1-함수-선언-기본">1. 함수 선언 기본</h2>
<p>자바와 가장 큰 차이점은 파라미터의 이름이 타입보다 먼저 오며, 반환 타입이 함수 정의 끝에 위치한다는 점이다.</p>
<pre><code class="language-kotlin">fun sum(a: Int, b: Int): Int {
    return a + b
}</code></pre>
<h2 id="2-단일-식-함수-expression-body">2. 단일 식 함수 (Expression Body)</h2>
<p>함수의 몸체가 단 한 줄의 식으로 이루어져 있다면, 중괄호{}와 return을 생략하고 =를 사용하여 축약할 수 있다.</p>
<pre><code class="language-kotlin">// 반환 타입까지 추론이 가능하여 생략할 수 있다.
fun sum(a: Int, b: Int) = a + b</code></pre>
<h2 id="3-top-level-function-최상위-함수">3. Top-Level Function (최상위 함수)</h2>
<p>코틀린은 클래스 밖에서도 함수를 선언할 수 있다. 이는 자바에서 유틸리티 성격의 메서드를 만들기 위해 무의미한 클래스(UtilClass)를 생성해야 했던 불편함을 해결해준다.</p>
<p>Kotlin: 클래스 없이 함수만 선언해도 어디서든 접근 가능.</p>
<p>Java 비유: 자바의 static 메서드와 유사하게 동작하지만, 훨씬 가독성이 좋고 코드가 깔끔해진다.</p>
<pre><code class="language-kotlin">// Utils.kt 파일
fun log(message: String) {
    println(&quot;Log: $message&quot;)
}

class Service {
    fun run() {
        log(&quot;서비스 실행&quot;) // 클래스 외부 함수를 바로 사용
    }
}</code></pre>
<h2 id="4-기본-파라미터와-오버로딩-default-parameters">4. 기본 파라미터와 오버로딩 (Default Parameters)</h2>
<p>자바에서는 파라미터 개수가 다를 때마다 여러 개의 메서드를 오버로딩(Overload)해야 했다. 
코틀린은 기본값(Default Value)을 설정하여 이를 함수 하나로 처리한다.</p>
<pre><code class="language-kotlin">// 함수 하나로 3가지 케이스를 모두 대응 가능
fun sum(a: Int, b: Int, c: Int = 0) = a + b + c

fun main() {
    sum(1, 2)       // c는 기본값 0 사용 (결과: 3)
    sum(1, 2, 3)    // c에 3 대입 (결과: 6)

    // Named Arguments: 이름을 지정하면 순서와 상관없이 대입 가능
    sum(b = 10, a = 5) 
}</code></pre>
<h2 id="5-unit-반환-java의-void">5. Unit 반환 (Java의 void)</h2>
<p>반환값이 없는 함수의 경우 자바에서는 void를 사용하지만, 코틀린은 Unit이라는 객체를 반환한다. (생략 가능)</p>
<pre><code class="language-kotlin">fun printSum(a: Int, b: Int): Unit { // : Unit은 생략 가능
    println(a + b)
}</code></pre>
<h3 id="요약하자면">요약하자면?</h3>
<p>간결성: = 하나로 함수를 정의할 수 있는 단일 식 함수 지원.</p>
<p>유연성: 클래스에 얽매이지 않는 최상위 함수 제공.</p>
<p>효율성: 기본 파라미터를 통해 복잡한 오버로딩 없이 함수 하나로 다양한 케이스 대응.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin 문법 정리 - Null Safety]]></title>
            <link>https://velog.io/@wongi-kim/Kotlin-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC-Null-Safety</link>
            <guid>https://velog.io/@wongi-kim/Kotlin-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC-Null-Safety</guid>
            <pubDate>Fri, 24 Apr 2026 05:02:02 GMT</pubDate>
            <description><![CDATA[<h2 id="null-safety-null-안전성">Null Safety (Null 안전성)</h2>
<p>코틀린의 가장 큰 특징은 <strong>컴파일 단계에서 NullPointerException(NPE)을 방지</strong>하도록 설계되었다는 점이다.</p>
<h3 id="1-null을-대입하려면-타입-지정이-필수다">1. Null을 대입하려면 타입 지정이 필수다</h3>
<p>코틀린은 타입 추론을 지원하지만, <code>null</code>을 대입할 때는 이야기가 다르다. </p>
<pre><code class="language-kotlin">var name = null // (X) 컴파일러가 타입을 추론할 수 없음</code></pre>
<p>어떤 타입의 Null인지 알 수 없기 때문에, Null을 허용하려면 반드시 <strong>명시적 타입 지정</strong>과 함께 <code>?</code> 키워드를 붙여야 한다.</p>
<pre><code class="language-kotlin">var name : String? = null // (O)
name = &quot;John Doe&quot;</code></pre>
<h3 id="2-string과-string은-서로-다른-타입이다">2. <code>String?</code>과 <code>String</code>은 서로 다른 타입이다</h3>
<p>가장 흔히 하는 착각이 <code>String?</code>을 단순히 <code>String</code>에 <code>null</code>이 추가된 상태로 보는 것이지만, 코틀린에서 이 둘은 <strong>완전히 다른 타입</strong>으로 취급된다.</p>
<p>Java와 비교해보자, Java 에서는 래퍼클래스와 기본 타입이 존재한다.
예시를 들어보자면 Java에서는 정수를 표기할 때 <code>Integer</code> 와 <code>int</code> 를 사용할 수 있는데
두 타입은 <code>null</code>의 허용 여부를 보여주는 단편적인 예시이다.</p>
<blockquote>
<pre><code class="language-java">Integer num1 = null;
int num2 = 0;
num2 = num1; // 오류 발생! (언박싱 과정에서 NPE 위험)</code></pre>
<p>이처럼 코틀린에서도 <code>null</code>이 가능한 타입(<code>String?</code>)의 값을 <code>null</code>이 불가능한 타입(<code>String</code>)에 그냥 대입할 수는 없다.</p>
</blockquote>
<h3 id="3-null을-안전하게-처리하는-방법">3. Null을 안전하게 처리하는 방법</h3>
<h4 id="a-강제-타입-변환-"><strong>A. 강제 타입 변환 (!!)</strong></h4>
<p><code>!!</code>를 사용하면 Nullable 타입을 Non-Null 타입으로 강제로 바꿀 수 있다.</p>
<pre><code class="language-kotlin">var name : String? = null
var name2 : String = name!! // &quot;내가 책임질게, 무조건 null 아니야!&quot;</code></pre>
<ul>
<li><strong>주의:</strong> 만약 <code>name</code>이 실제로 <code>null</code>이라면 런타임에 에러가 발생한다. 이는 개발자의 책임이며, 가급적 사용을 지양해야 하는 <strong>좋지 않은 방법</strong>이다.</li>
</ul>
<h4 id="b-내장-함수-let---사용-권장"><strong>B. 내장 함수 <code>?.let { }</code> 사용 (권장)</strong></h4>
<p>코틀린이 제공하는 가장 세련된 방법 중 하나다. </p>
<blockquote>
<p><strong>의미:</strong> &quot;null이 아니면(<code>?.</code>) 다음 내용을 실행하자(<code>let { }</code>)&quot;</p>
</blockquote>
<pre><code class="language-kotlin">fun main() {
    var name : String? = &quot;John Doe&quot;
    var name2 : String = &quot;&quot;

    name?.let {
        name2 = it // it은 null이 아닌 name을 가리킴
    }
}</code></pre>
<h4 id="c-안전한-호출과-엘비스-연산자"><strong>C. 안전한 호출(<code>?.</code>)과 엘비스 연산자(<code>?:</code>)</strong></h4>
<ul>
<li><strong><code>?.</code> (Safe Call):</strong> <code>null</code>이면 뒤의 코드를 실행하지 않고 바로 <code>null</code>을 반환한다.</li>
<li><strong><code>?:</code> (Elvis Operator):</strong> <code>null</code>일 경우 지정한 기본값을 대신 사용한다.<pre><code class="language-kotlin">val length = name?.length ?: 0 // name이 null이면 0을 반환</code></pre>
</li>
</ul>
<hr>
<h3 id="요약하자면">요약하자면?</h3>
<ol>
<li><strong><code>?</code></strong>: 이 변수는 null일 수도 있음을 선언.</li>
<li><strong><code>String?</code> ≠ <code>String</code></strong>: 둘은 엄격히 다른 타입이므로 직접 대입 불가.</li>
<li><strong><code>?.let</code></strong>: null이 아닐 때만 안전하게 코드를 수행하는 가장 코틀린스러운 방법.</li>
<li><strong><code>!!</code></strong>: 강제로 null이 아님을 선언하지만, NPE 위험이 커서 권장하지 않음.</li>
<li><strong><code>?.</code></strong>: null이면 실행하지 않음 (Safe Call)</li>
<li><strong><code>?:</code></strong>: null이면 이 값을 대신 사용함 (Elvis Operator)</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin 문법 정리 - List, Array]]></title>
            <link>https://velog.io/@wongi-kim/Kotlin-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC-List-Array</link>
            <guid>https://velog.io/@wongi-kim/Kotlin-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC-List-Array</guid>
            <pubDate>Fri, 24 Apr 2026 03:41:39 GMT</pubDate>
            <description><![CDATA[<h2 id="배열과-리스트-array--list">배열과 리스트 (Array &amp; List)</h2>
<p>코틀린에서 데이터를 담는 대표적인 자료구조이다. 가장 큰 차이점은 크기 변경이 가능한가와 수정 가능한가에 있다.</p>
<h3 id="array-배열">Array (배열)</h3>
<p>자바의 배열과 유사하다. 크기가 정해져 있으며, 한 번 생성하면 크기를 늘리거나 줄일 수 없다.</p>
<pre><code class="language-kotlin">fun main() {
    // arrayOf를 사용하여 생성
    val array = arrayOf(1, 2, 3)

    // 인덱스로 접근 및 수정 가능
    array[0] = 10
    println(array[0]) // 10

    // array.size로 크기 확인 가능
}</code></pre>
<h3 id="list-리스트">List (리스트)</h3>
<p>코틀린의 리스트는 크게 두 가지로 나뉜다. 기본적으로 listOf로 만드는 리스트는 <strong>수정이 불가능(Immutable)</strong>하다는 점이 중요하다.</p>
<blockquote>
</blockquote>
<p>Immutable List (수정 불가)
생성 시점에 정해진 데이터를 읽기만 할 수 있으며, 요소를 추가하거나 변경할 수 없다.</p>
<pre><code class="language-kotlin">val list = listOf(1, 2, 3)
// list[0] = 10 // (X) 에러 발생: 수정 불가</code></pre>
<blockquote>
<p>Mutable List (수정 가능)
요소를 추가, 삭제, 수정할 수 있는 리스트이다. Java의 ArrayList와 비슷하다.</p>
</blockquote>
<pre><code class="language-kotlin">val mutableList = mutableListOf(1, 2, 3)

mutableList.add(4)      // 요소 추가
mutableList.removeAt(0) // 0번 인덱스 삭제
mutableList[0] = 10     // 요소 수정</code></pre>
<h3 id="요약하자면">요약하자면?</h3>
<ul>
<li><p>Array: 크기 고정, 내부 값 수정 가능.</p>
</li>
<li><p>List (listOf): 크기 고정, 내부 값 수정 불가.</p>
</li>
<li><p>MutableList (mutableListOf): 크기 가변, 내부 값 수정 가능.</p>
</li>
</ul>
<blockquote>
</blockquote>
<p>Tip: 코틀린에서는 데이터의 일관성을 위해 가능하면 수정 불가능한 List를 먼저 사용하고, 데이터 변경이 꼭 필요한 경우에만 MutableList를 사용하는 것을 권장한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin 문법 정리 - 조건문, 반복문]]></title>
            <link>https://velog.io/@wongi-kim/Kotlin-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC-%EC%A1%B0%EA%B1%B4%EB%AC%B8-%EB%B0%98%EB%B3%B5%EB%AC%B8</link>
            <guid>https://velog.io/@wongi-kim/Kotlin-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC-%EC%A1%B0%EA%B1%B4%EB%AC%B8-%EB%B0%98%EB%B3%B5%EB%AC%B8</guid>
            <pubDate>Fri, 24 Apr 2026 03:37:43 GMT</pubDate>
            <description><![CDATA[<h1 id="참고한-자료">참고한 자료</h1>
<blockquote>
<p><a href="https://www.youtube.com/watch?v=OtHkb6wAI5U">https://www.youtube.com/watch?v=OtHkb6wAI5U</a></p>
</blockquote>
<hr>
<h2 id="조건문">조건문</h2>
<p>자바에서는 <code>if-else</code> 또는 <code>switch-case</code> 문으로 조건문을 작성할 수 있다.</p>
<p>Kotlin에서 <code>if</code> 문은 <code>when</code>으로 치환할 수 있다.
<code>when</code>은 Java의 <code>switch</code>와 비슷하지만 조금 더 강력한 조건들을 지정할 수 있다. (범위 지정, 값 비교, 식 사용 등)</p>
<h3 id="조건문이-식으로-성립된다">조건문이 식으로 성립된다.</h3>
<p>또한 중요한 점은 Kotlin에서의 조건문(<code>when</code>, <code>if</code>)은 <strong>식(Expression)</strong>으로 치환될 수 있다는 점이다. 즉, 조건문의 결과값을 변수에 바로 대입할 수 있다.</p>
<pre><code class="language-kotlin">fun main() {
    var i = 5

    var result = if (i &gt; 10) { // 조건문 통과 후 바로 result로 대입
        &quot;10 보다 크다&quot;
    } else if (i &gt; 5) {
        &quot;5보다 크다&quot;
    } else {
        &quot;5다.&quot;
    }
    print(result) // &quot;5다.&quot; 가 출력된다.
}

// when으로 바꾼다면 다음과 같은 식이 된다.
fun main() {
    var i = 5

    var result = when { // 조건문 통과 후 바로 result로 대입
        i &gt; 10 -&gt; {
            &quot;10 보다 크다&quot;
        }
        i &gt; 5 -&gt; {
            &quot;5보다 크다&quot;
        }
        else -&gt; {
            &quot;5다.&quot;
        }
    }
    print(result) // &quot;5다.&quot; 가 출력된다.
}</code></pre>
<h3 id="삼항-연산자">삼항 연산자</h3>
<p>자바에서는 보통 다음과 같이 사용된다.</p>
<pre><code class="language-java">boolean result = i &gt; 10 ? true : false; </code></pre>
<p>Kotlin에는 별도의 삼항 연산자가 없다. 
대신 <code>if</code>를 식 그대로 적어주면 삼항 연산과 동일한 결과를 확인할 수 있다.</p>
<pre><code class="language-kotlin">var result = if (i &gt; 10) true else false</code></pre>
<hr>
<h2 id="반복문">반복문</h2>
<p>반복문은 보통 리스트(List) 같은 자료형을 순회할 때 자주 사용된다.</p>
<p>Kotlin은 <code>for in</code> 형태를 사용하며, Java의 향상된 for문이나 Python과 유사한 형태라 직관적이다.</p>
<h3 id="다양한-for-in-사용법">다양한 for in 사용법</h3>
<pre><code class="language-kotlin">fun main() {
    val numbers = listOf(1, 2, 3, 4, 5) 

    // 1. 리스트 전체 순환
    for (item in numbers) {
        print(item)
    }

    // 2. 특정 범위 지정 (.. 사용 시 마지막 숫자 포함: 0123)
    for (i in 0..3) {
        print(i) 
    }

    // 3. 리스트 크기만큼 반복 (until 사용 시 마지막 미포함: 01234)
    // 0..numbers.size-1 와 동일하게 작동한다.
    for (i in 0 until numbers.size) {
        print(i)
    }

    // 4. 건너뛰며 반복 (step 사용)
    for (i in 0..10 step 2) {
        print(i) // 0, 2, 4, 6, 8, 10 출력
    }
}</code></pre>
<h3 id="foreach">forEach</h3>
<p>함수형 스타일의 <code>forEach</code> 역시 가능하다. </p>
<pre><code class="language-kotlin">fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)

    // 일반적인 형태
    numbers.forEach { item -&gt;
        print(item)
    }

    // 더 간결하게 it 키워드 사용 (파라미터가 하나일 때 생략 가능)
    numbers.forEach { print(it) }
}</code></pre>
<h3 id="while문">while문</h3>
<p>Java와 완벽하게 동일한 형태로 사용 가능하다.
<code>break</code>, <code>continue</code> 둘 다 가능하므로 기존에 알고 있는 형태로 사용하자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin 문법 정리 - 변수]]></title>
            <link>https://velog.io/@wongi-kim/Kotlin-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@wongi-kim/Kotlin-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 23 Apr 2026 03:13:36 GMT</pubDate>
            <description><![CDATA[<h1 id="참고한-영상">참고한 영상</h1>
<blockquote>
<p><a href="https://www.youtube.com/watch?v=OtHkb6wAI5U">https://www.youtube.com/watch?v=OtHkb6wAI5U</a></p>
</blockquote>
<h1 id="변수">변수</h1>
<h2 id="변수의-종류">변수의 종류</h2>
<p>코틀린 변수는 두 가지 키워드로 구분된다.</p>
<ul>
<li><p>val (Value): 읽기 전용(Read-only). 초기화 후 재할당이 불가능하며, Java의 final과 유사하다.</p>
</li>
<li><p>var (Variable): 가변(Mutable). 선언 후 값을 자유롭게 변경할 수 있다.</p>
</li>
</ul>
<h2 id="타입-추론">타입 추론</h2>
<p>코틀린 변수는 타입 추론을 지원한다
타입 추론이란 자료형(데이터 타입)을 명시하지 않아도 된다는 것을 의미한다.</p>
<p>정확히는 변수 선언 시 자료형이 명시되지 않았다면 대입되는 값을 통해 컴파일러가 타입을 지정하는 것이다.</p>
<pre><code class="language-kotlin">var name1 = &quot;kotlin&quot; // String으로 추론
var name2 : String = &quot;java&quot; // 명시적 타입 지정</code></pre>
<p>모두 String 으로 인식이 된다는 소리.</p>
<p>단, 타입을 지정해줄 때는 <code>: Type</code> 과 같은 형태로 변수 명 뒤에 붙이면 된다.</p>
<h2 id="타입-추론의-제약">타입 추론의 제약</h2>
<p>타입 추론은 편리한 기능이지만 다음과 같은 주의점을 숙지해야한다.</p>
<p><strong>초기화 필수</strong></p>
<p>초기화 필수란 무엇일까?</p>
<pre><code class="language-kotlin">val name1 // (X) 에러 발생 : 타입을 알 수 없음
// name1 = &quot;kotlin&quot; // 애초에 선언 시 초기화 되지 않았다는 오류가 표기

val name2 : String // (O) 선언 시점이 아닌 추후 초기화를 위해 타입을 지정
name2 = &quot;kotlin&quot; // 정상적으로 컴파일</code></pre>
<h2 id="null-safe">Null Safe</h2>
<p>코틀린 변수는 기본적으로 Null을 허용하지 않는다.
Java Spring 에선 Null을 위한 방어코드를 작성하는 일이 빈번한데, Kotlin 에서는 기본적으로
변수에서 부터 <code>null</code>을 허용하지 않기 때문에 조금 더 안전하게 코드 작성이 가능하다.</p>
<p>다만 변수가 <code>null</code>을 허용하기 위해선 타입 뒤에 <code>?</code>를 붙이는 것으로 <code>null</code>을 허용할 수 있다.</p>
<pre><code class="language-kotlin">var name1 : String? = &quot;kotlin&quot; // null 허용 </code></pre>
<p>위와 같은 상황일 때 <code>var</code>를 통한 선언으로 가변 변수 적용이 되며
추 후 <code>null</code> 로 값이 변경될 수 있다.</p>
<p>물론 <code>val</code> 도 <code>null</code> 초기화가 가능하다. 따라서 </p>
<pre><code class="language-kotlin">val name2 : String? = null</code></pre>
<p>과 같이 선언하면 <strong>불변이면서 Null인 상태</strong> 도 가능하다는 것</p>
<p>간단하게 코드 블럭으로 살펴보자면</p>
<pre><code class="language-kotlin">var name1 : String? = &quot;kotlin&quot; 
name1 = null // var이므로 재할당 가능 + Nullable이므로 null 가능

val name2 : String? = null 
// name2 = &quot;java&quot; // Error: val은 재할당 불가! (불변이면서 null인 상태 유지)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[MSA Shopping Mall - 끄적임... 이후]]></title>
            <link>https://velog.io/@wongi-kim/MSA-Shopping-Mall-%EB%81%84%EC%A0%81%EC%9E%84...-%EC%9D%B4%ED%9B%84</link>
            <guid>https://velog.io/@wongi-kim/MSA-Shopping-Mall-%EB%81%84%EC%A0%81%EC%9E%84...-%EC%9D%B4%ED%9B%84</guid>
            <pubDate>Mon, 07 Apr 2025 11:08:23 GMT</pubDate>
            <description><![CDATA[<p>지난 끄적임에서 왜 이렇게 안되는지에 대해 적었는데
(물론 포스트맨에서 사용해서 그런 걸 수 있긴 하지만)</p>
<p>일단 얘기해보자면 코드 자체는 그렇게 크게 문제가 없었다.
다만 Gateway든 다른 서비스에서든 생성한 커스텀 헤더를 클라이언트쪽에서 확인할 수 없는 문제였다.</p>
<p>즉, 생성된 커스텀 헤더들은 서버에서만 통한다는 얘기....</p>
<p>여기서 내가 생각하고 구현한 구조와 흐름과
통상적인 웹 동작에서의 구조와 흐름에 대한 차이 대해 조금 정리해보고자 한다.</p>
<h1 id="내가-생각한-버전">내가 생각한 버전</h1>
<h2 id="1-로그인-요청">1. 로그인 요청</h2>
<p>일단 내가 구현한 흐름 자체는 로그인 요청이 수행되면 서버에서는 JWT를 생성하여 쿠키나 헤더를 통해 Gateway로 전달이 된다.</p>
<h2 id="2-gateway">2. Gateway</h2>
<p>Gateway로 넘어온다면 쿠키에서 JWT를 추출하고 
추출한 JWT를 기반으로 커스텀 헤더를 생성하여 Down Stream을 진행한다.</p>
<h1 id="통상적인-웹-동작">통상적인 웹 동작</h1>
<h2 id="1-로그인-요청-1">1. 로그인 요청</h2>
<p>일반적인 웹 시스템에서는 사용자가 로그인하면 서버는 JWT(또는 세션 ID)를 HttpOnly 쿠키 또는 LocalStorage에 저장하게 된다.</p>
<p>이후의 모든 요청에서는 클라이언트(브라우저 또는 앱) 가 이 토큰을 보관하고,
서버로의 요청 시마다 Authorization 헤더에 붙이거나, 자동으로 쿠키를 전송하게 된다.</p>
<pre><code class="language-http">Authorization: Bearer &lt;JWT_TOKEN&gt;
혹은
Cookie: accessToken=xxx;</code></pre>
<p>이 구조에서 핵심은 <strong>&quot;클라이언트가 토큰을 소유&quot;</strong>하고 있다는 점이다.
그래서 사용자는 로그인 이후에도 새로고침이나 다른 페이지 이동 등에서도 인증 상태를 유지할 수 있게 된다.</p>
<h2 id="서버-인증-처리">서버 인증 처리</h2>
<p>서버에서는 요청을 받을 때마다 클라이언트가 보낸 JWT를 검증하여 인증 처리를 진행하고,
필요한 경우에는 내부적으로 사용자 정보를 조회하거나 권한 확인을 하게 된다.
이 과정에서 추가적인 커스텀 헤더를 붙이는 경우도 있지만, 그것은 클라이언트가 아닌 서버 간 통신에서만 주로 사용된다.</p>
<hr>
<h1 id="구조적-차이와-문제-포인트">구조적 차이와 문제 포인트</h1>
<p>정리하면, 내가 구현한 구조에서는
&quot;JWT → 커스텀 헤더로 변환 → 이후 요청은 커스텀 헤더 기반&quot; 이라는 방식이고,
<strong>클라이언트는 이 커스텀 헤더들을 알 수 없고, 사용할 수 없는 구조이다.</strong></p>
<p>즉, 클라이언트는 인증 정보를 소유하지 않기 때문에, 로그인 이후 인증 유지가 어려워지는 문제가 생긴다.
(한 번 로그인하면 커스텀 헤더가 서버 간 통신에만 사용되므로, 클라이언트가 직접 다음 요청을 보낼 수 없는 구조)</p>
<p>그래서 생긴 이슈:</p>
<ul>
<li>Postman에서 직접 커스텀 헤더를 입력하면 정상 작동</li>
<li>하지만 브라우저나 앱에서는 커스텀 헤더를 자동으로 생성하거나 보낼 수 없음</li>
<li>따라서 로그인 이후 상태를 유지하거나 인증 요청을 할 수 없음</li>
</ul>
<h1 id="이슈를-해결하기-위해">이슈를 해결하기 위해</h1>
<p>AI님들과 얘기를 조금 해봤는데
여러가지 방안을 알려주었다.</p>
<table>
<thead>
<tr>
<th>방안</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>JWT를 HttpOnly 쿠키로 계속 유지</td>
<td>서버에서 매 요청마다 JWT를 검증. 일반적인 방식이며 보안 좋음</td>
</tr>
<tr>
<td>JWT를 LocalStorage에 저장하고 Authorization 헤더에 직접 붙이기</td>
<td>클라이언트가 제어 가능. SPA나 모바일 앱에서 많이 사용</td>
</tr>
<tr>
<td>커스텀 헤더 기반 인증 유지 시</td>
<td>클라이언트에 해당 헤더 값을 전달하는 구조를 별도로 설계하거나, Gateway가 지속적으로 붙여주는 구조 유지 필요</td>
</tr>
</tbody></table>
<p>그런데 일단 내가 생각한 방안은 커스텀 헤더 기반 인증이기 때문에 다음 포스팅 부터는 
커스텀 헤더 기반 인증 유지 하는 방법으로 문제를 해결하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MSA Shopping Mall - 잠깐의 끄적임...]]></title>
            <link>https://velog.io/@wongi-kim/MSA-Shopping-Mall-%EC%9E%A0%EA%B9%90%EC%9D%98-%EB%81%84%EC%A0%81%EC%9E%84</link>
            <guid>https://velog.io/@wongi-kim/MSA-Shopping-Mall-%EC%9E%A0%EA%B9%90%EC%9D%98-%EB%81%84%EC%A0%81%EC%9E%84</guid>
            <pubDate>Sat, 29 Mar 2025 08:02:42 GMT</pubDate>
            <description><![CDATA[<p>계속 구현하면서 느낀건데..</p>
<p>Postman의 한계인건지, 내 코드가 너무 조잡한건지
그것도 아니라면 내가 아직 MSA에서의 인증 방식에 대해 제대로 깨닫지 못한건지..</p>
<p>굳이 JWT를 통해서 CustomHeader를 생성하고 Downstream까지 해놓고 매 요청마다 JWT를 계속 추가되어 기존의 CustomHeader들은 모두 무용지물이 되는 상황이다.</p>
<p>어떤 상황인지 공부도 필요하고... 코드 수정에 들어가야 할 것 같다... ㅠ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MSA Shopping Mall - 10 (RabbitMQ)]]></title>
            <link>https://velog.io/@wongi-kim/MSA-Shopping-Mall-10-RabbitMQ</link>
            <guid>https://velog.io/@wongi-kim/MSA-Shopping-Mall-10-RabbitMQ</guid>
            <pubDate>Sat, 29 Mar 2025 05:20:47 GMT</pubDate>
            <description><![CDATA[<p>이제 드디어 메시지큐를 이용한 작업을 시작할 예정이다.</p>
<h1 id="시작하기-전에">시작하기 전에...</h1>
<p>먼저 RabbitMQ를 이용한 작업을 할 차례였고,
RabbitMQ를 통한 메시지 처리도 API Gateway 통해야 하는 줄 알았다.</p>
<p>그리고 각 서비스마다 독립적인 DB를 가지기 때문에 테이블간의 연관관계 설정을 위해서,
예를 들면 Product가 User을 FK로 가지기 위해서 연관 관계를 설정하는 타이밍을 위해</p>
<p>User 서비스에서 User정보를 넘겨주려는 API 역시 생성하려 하였다.</p>
<p>이 두가지 논점을 먼저 해결하고 이번 포스팅의 목적인 메시지큐 구축 작업을 작성하도록 하겠다.</p>
<h1 id="user-정보-api">User 정보 API</h1>
<p>먼저 User 정보를 위한 API 설계는 진행하지 않았다.</p>
<p>이유 역시 두가지 정도가 있는데</p>
<blockquote>
</blockquote>
<ol>
<li>기존에 생성한 CustomHeader인 X-User-Id에 User를 식별할 정보가 담겨져있다.<blockquote>
</blockquote>
</li>
<li>메시지 큐를 이용한 데이터 교환은 비동기 작업이다.</li>
</ol>
<p>이 두 가지 내용이다.</p>
<h2 id="customheader">CustomHeader</h2>
<p>X-User-Id에는 UserId로 식별할 수 있는 email이 담겨져있고,
해당 헤더는 <strong>이미 존재하는지 확인하는 작업</strong>후 생성되는 헤더이기 때문에 <strong>일반적인 상황에서는</strong> 추가적으로 확인하는 작업이 필요하지 않다.</p>
<p>따라서 Product를 생성하는 동기 처리까지 오기 전에 이미 검증을 끝내었기 때문에 API를 생성할 이유가 없는 것이다.</p>
<p>다만 User 서비스에 실제 해당 ID(email)가 존재하는지 100% 보장할 수는 없기 때문에 문제가 발생할 수 있으나 해당 문제는 밑에서 다시 다루도록 하겠다.</p>
<h2 id="메시지큐는-비동기-작업">메시지큐는 비동기 작업</h2>
<p>위의 이유가 사실 따지고보자면 1개의 이유이긴 하다.
일단 내용을 보자</p>
<blockquote>
</blockquote>
<p>Product 서비스에서 Product를 생성하는 과정 : 동기 작업
Product 서비스에서 User 서비스 API를 호출하여 정보 교환 : 동기 작업</p>
<p>그렇지만 소제목으로 적어뒀듯이 메시지큐는 비동기 작업이다.</p>
<h3 id="그렇다면-왜-메시지큐를-통한-비동기-작업이-필요할까">그렇다면 왜 메시지큐를 통한 비동기 작업이 필요할까?</h3>
<p>이유는 User의 삭제, 변경등의 이유이다.</p>
<p>기존 RDB의 Cascade 역할을 대체하기 위함으로도 볼 수 있는 과정으로
서로 독립된 DB를 사용하는 만큼 데이터 동기화를 통한 <strong>일관성(Consistency)</strong> 원칙을 지키기 위함이다.</p>
<p>정확하게는 최종적 일관성(Eventual Consistency) 을 보장하기 위함이다.
(나중에 SAGA 패턴에 대해 공부하고 글을 적어보자...)</p>
<h3 id="user-확인이-정말-필요-없나">User 확인이 정말 필요 없나?</h3>
<p>위에서 했던 얘기를 조금 더 이어나가보자면 
<strong>(잘못된 정보 주의)</strong>
사실 확인하는게 데이터 자체의 안정성 면에서는 더 좋다고 생각된다.
해당 부분을 해결하려면 Auth - User 관계처럼 로그인할 때 한 번의 HTTP 동기 처리가 이루어지는 것 처럼 User 정보를 받아오는것으로 해결할 수 있다.</p>
<p><strong>단 처리해야 하는 시점을 명확히 해야 한다.
내가 생각하기에 USER를 확인하는 시점은 Product가 생성될 때 1번만 User 정보를 받아오면 된다.</strong></p>
<p>해당 과정은 비동기 처리로 하기에는 적절하지 않은 동기 작업이기에 비동기 처리에 엮을 필요가 없다는 생각이다.</p>
<hr>
<p>이쯤으로 User 정보 API에 대한 얘기를 마치고 본격적으로 RabbitMQ 작업을 진행하겠다.</p>
<h1 id="rabbitmq">RabbitMQ</h1>
<p>처음에 했던 얘기를 다시 가져오자면 RabbitMQ 메시지 처리를 API Gateway를 통해야 하는 줄 알았지만 둘의 역할은 다르다.</p>
<blockquote>
</blockquote>
<ul>
<li>API Gateway는 주로 HTTP 요청을 라우팅하고 인증, 로깅 등의 기능을 제공하는 반면,</li>
<li>RabbitMQ는 비동기 메시지 큐로, 서비스 간의 직접적인 통신을 줄이고 비동기 이벤트 처리를 가능하게 한다.</li>
</ul>
<p>따라서 각 서비스가 RabbitMQ를 직접 퍼블리싱(Publish)하고 서브스크라이브(Subscribe)하는 구조로 설계하는 게 일반적이다.</p>
<h2 id="기본적인-흐름">기본적인 흐름</h2>
<p>기본적으로 User 서비스에서 User 삭제 이벤트를 발생시키면, Product 서비스가 이를 수신하여 관련 데이터를 정리하는 구조다.
RabbitMQ를 활용하는 이유는 서비스 간의 결합도를 낮추고, 데이터 동기화 문제를 해결하기 위함이다.</p>
<h3 id="흐름-요약">흐름 요약</h3>
<ol>
<li>User 서비스에서 사용자가 삭제되면, UserEventPublisher가 삭제 이벤트를 발행(Publish).</li>
<li>RabbitMQ가 메시지를 큐(Queue)에 저장.</li>
<li>Product 서비스가 해당 메시지를 수신(Subscribe)하여, 삭제된 사용자와 연결된 상품을 삭제.</li>
</ol>
<h2 id="rabbitmqconfig">RabbitMQConfig</h2>
<p>먼저 RabbitMQConfig를 통신하려는 서비스 (현재는 User - Product) 양쪽에 작성한다.</p>
<pre><code class="language-java">@Configuration
public class RabbitMQConfig {

    // Exchange 설정
    public static final String EXCHANGE = &quot;user.exchange&quot;;

    // Queue 설정
    public static final String USER_UPDATED_QUEUE = &quot;user.updated.queue&quot;;

    // RoutingKey 설정
    public static final String USER_UPDATED_ROUTING_KEY = &quot;user.updated&quot;;

    // Bean 등록
    // exchange 설정
    @Bean
    public DirectExchange userExchange() {
        return new DirectExchange(EXCHANGE);
    }

    @Bean
    public Queue userDeletedQueue() {
        return new Queue(USER_DELETED_QUEUE);
    }

    // binding 설정
    @Bean
    public Binding userDeletedBinding(Queue userDeletedQueue, DirectExchange exchange) {
        return BindingBuilder.bind(userDeletedQueue)
                .to(exchange)
                .with(USER_DELETED_ROUTING_KEY);
    }

    @Bean
    public MessageConverter jsonMessageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(jsonMessageConverter());
        return rabbitTemplate;
    }
}</code></pre>
<p>일단 간략하게 User삭제를 위한 Exchange, Queue, RoutingKey 를 선언해줬다.
일반적으로 아래와 같이 사용되며</p>
<blockquote>
</blockquote>
<p>Exchange(교환기): 메시지를 전달하는 중심 허브 역할.
Queue(큐): 메시지를 저장하고, 구독한 서비스가 가져가도록 함.
Routing Key(라우팅 키): 특정 큐에 메시지를 전달하기 위한 식별자.
RabbitTemplate: 메시지를 변환하고, RabbitMQ에 전송하는 역할.</p>
<p>해당 서비스에서는 다음과 같은 역할을 가진다.</p>
<blockquote>
</blockquote>
<p>DirectExchange: 특정 라우팅 키를 가진 메시지를 해당 큐로 보냄.
Queue: 삭제 이벤트를 저장하는 큐.
Binding: RoutingKey를 기준으로 큐를 Exchange에 연결.
RabbitTemplate: JSON 메시지를 변환하여 RabbitMQ로 전송하는 역할.</p>
<h2 id="usereventpublisher">UserEventPublisher</h2>
<p>다음으로는 User와 관련된 Event가 발생되었을 때 이벤트를 발급할 수 있는 코드를 작성하도록 하겠다. (User Service 내부)</p>
<pre><code class="language-java">@Service
public class UserEventPublisher {

    private final RabbitTemplate rabbitTemplate;

    @Autowired
    public UserEventPublisher(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    public void publishUserDeleted(String userId) {  // 삭제 이벤트 추가
        rabbitTemplate.convertAndSend(
                RabbitMQConfig.EXCHANGE,
                RabbitMQConfig.USER_DELETED_ROUTING_KEY,
                userId
        );
    }
}</code></pre>
<p>publishUserDeleted(userId): 삭제된 유저 ID를 RabbitMQ에 발행.
Exchange(user.exchange) → Queue(user.deleted.queue)로 메시지가 전달됨.</p>
<h2 id="해당-이벤트를-비지니스로직에">해당 이벤트를 비지니스로직에</h2>
<p>해당 이벤트를 비지니스로직에 작성하므로써
요청이 들어올 경우 이벤트를 발급한다.</p>
<pre><code class="language-java">@Transactional
public void deleteUser(String email) {
    User user = userRepository.findByEmail(email)
            .orElseThrow(() -&gt; new CustomException(ErrorCode.USER_NOT_FOUND));

    userRepository.delete(user);
    eventPublisher.publishUserDeleted(email);
}</code></pre>
<h2 id="usereventlistener">UserEventListener</h2>
<p>이제 발급받은 event를 수신할 UserEventListener를 Product 서비스 내에 작성한다.</p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class UserEventListener {

    private final ProductRepository productRepository;

    @RabbitListener(queues = RabbitMQConfig.USER_DELETED_QUEUE)
    public void handleUserDeletedEvent(String userId) {

        List&lt;Product&gt; products = productRepository.findByOwner(userId);

        if (!products.isEmpty()) {
            productRepository.deleteAll(products);
        }
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[MSA Shopping Mall - 9 (trouble Shooting)]]></title>
            <link>https://velog.io/@wongi-kim/MSA-Shopping-Mall-9-trouble-Shooting</link>
            <guid>https://velog.io/@wongi-kim/MSA-Shopping-Mall-9-trouble-Shooting</guid>
            <pubDate>Thu, 27 Mar 2025 08:48:44 GMT</pubDate>
            <description><![CDATA[<h1 id="microservice의-경우">MicroService의 경우</h1>
<p>트러블슈팅을 먼저 진행하고 가야 할 것 같다.</p>
<p>그렇지만 따로 문제가 생겼던 코드는 커밋을 해두지 않았기에 실행했던 스크린샷과 같은 자료가 없고,
그냥 이러한 문제를 겪었었다 정도를 적는 시간이 될 것 같다.</p>
<h2 id="트러블-슈팅">트러블 슈팅</h2>
<p>내가 겪었던 문제는 필터에서의 <strong>익명 인증 객체가 설정되는 문제</strong>였다.</p>
<p>나는 일단 MSA 구조 자체에서 Gateway에 보안적인 부분을 모두 할당? 한다는 점으로 이해하고 작업했었으며, 그로 인해 CustomHeader를 생성했고 해당 헤더들을 각 서비스에서 검증만하는 방식으로 구현했다.</p>
<p>이러한 방식을 통해
API Gateway : </p>
<ul>
<li>JWT 토큰 검증 및 인증 객체 생성 후 Gateway내에 저장</li>
<li>CustomHeader 생성 및 DownStream 진행</li>
</ul>
<p>각 서비스 : </p>
<ul>
<li>DownStream 방식으로 전해진 헤더 검증</li>
<li>리소스 접근</li>
</ul>
<p>의 방식으로 진행된다고 생각했고, <code>DownStream 방식으로 전해진 헤더 검증</code> 을 SecurityConfig와 더불어 <code>Filter를 통해 구현</code>한다면 리소스 접근까지 해결할 수 있으리라 생각했다.</p>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, SignatureAuthFilter signatureAuthFilter) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(authorize -&gt; authorize
                .requestMatchers(&quot;/v1/auth/**&quot;).permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(signatureAuthFilter, UsernamePasswordAuthenticationFilter.class)
            .sessionManagement(session -&gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }
}
</code></pre>
<p>처음에 저렇게 Filter를 생성했는데 바로 여기서 익명 인증 객체의 문제가 발생했다.</p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
@Slf4j
public class SignatureAuthFilter extends OncePerRequestFilter {

    private final SignatureUtil signatureUtil;
    private final List&lt;String&gt; excludedPaths = List.of(&quot;/v1/auth/login&quot;, &quot;/v1/users/signup&quot;, &quot;/v1/users/email&quot;);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        System.out.println(&quot;DISPATCHER TYPE : &quot; + request.getDispatcherType());

        String path = request.getRequestURI();

        // 화이트리스트 URL 예외 처리 (필터 통과)
        if (excludedPaths.contains(path)) {
            filterChain.doFilter(request, response);
            return;  //  반드시 return 추가
        }

        String userId = request.getHeader(&quot;X-User-Id&quot;);
        String timestamp = request.getHeader(&quot;X-Timestamp&quot;);
        String signature = request.getHeader(&quot;X-Signature&quot;);

        // 헤더 값 검증 실패 시 즉시 리턴
        if (userId == null || timestamp == null || signature == null) {
            response.sendError(HttpStatus.UNAUTHORIZED.value(), &quot;Missing authentication headers&quot;);
            return;
        }

        log.debug(&quot;Received authentication headers: userId={}, timestamp={}, signature={}&quot;, userId, timestamp, signature);

        // 타임스탬프 검증
        if (!signatureUtil.isTimestampValid(timestamp)) {
            response.sendError(HttpStatus.UNAUTHORIZED.value(), &quot;Timestamp expired&quot;);
            return;
        }

        // 서명 검증
        if (!signatureUtil.validateSignature(userId, timestamp, signature)) {
            response.sendError(HttpStatus.UNAUTHORIZED.value(), &quot;Invalid signature&quot;);
            return;
        }

        // 인증 정보 설정 (ROLE_USER 부여)
        List&lt;GrantedAuthority&gt; authorities = List.of(new SimpleGrantedAuthority(&quot;ROLE_USER&quot;));

        UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(userId, null, authorities);

        SecurityContextHolder.getContext().setAuthentication(authentication);

        log.debug(&quot;Authentication: {}&quot;, SecurityContextHolder.getContext().getAuthentication());

        // 검증 완료 후 필터 체인 실행
        filterChain.doFilter(request, response);
    }
}</code></pre>
<p>위의 코드는 구현한 필터인데 구체적으로 문제가 발생했던 지점은 다음과 같은 부분이다.</p>
<pre><code class="language-java">// 인증 정보 설정 (ROLE_USER 부여)
List&lt;GrantedAuthority&gt; authorities = List.of(new SimpleGrantedAuthority(&quot;ROLE_USER&quot;));

UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(userId, null, authorities);

SecurityContextHolder.getContext().setAuthentication(authentication);</code></pre>
<p>조금 보기 힘들 수 있으니 익명 인증이 활성화 되는 흐름을 먼저 정리해보겠다.
(다음 정리하는 내용은 SecurityConfig에서 설정해놓은 필터의 순서로 볼 수 있다.)</p>
<blockquote>
</blockquote>
<ol>
<li>요청이 들어옴<blockquote>
</blockquote>
</li>
<li>Filter Chain 실행<blockquote>
</blockquote>
</li>
<li>Custom Filter 실행
3-1. <code>SecurityContextHolder.getContext().setAuthentication(auth)</code> 를 제대로 설정하지 않으면 인증되지 않은 상태가 유지됨<blockquote>
</blockquote>
</li>
<li>SecurityContext의 Authentication 값 확인 <code>SecurityContextHolder.getContext().setAuthentication()</code> 값이 <code>null</code>이거나 <code>isAuthenticated() == false</code>일 경우 <code>AnonymousAuthenticationToken</code> 이 자동으로 설정됨 (자동으로 익명 인증 객체가 생성된다는 뜻)<blockquote>
</blockquote>
</li>
<li>이후 인증이 필요한 경로에 접근할 경우 지속해서 <code>AnonymousAuthenticationToken</code>가 인증 객체로 사용된다.<blockquote>
</blockquote>
</li>
<li>SecurityConfig에서 보면 CustomFilter 후에 <code>UsernamePasswordAuthenticationFilter</code> 를 실행하도록 하였는데 <code>UsernamePasswordAuthenticationFilter</code> 는 JWT 토큰과 관련된 인증 필터이므로 <code>SignatureAuthFilter</code> 이후 아무 역할을 하지 않는다.<blockquote>
</blockquote>
</li>
<li>SecurityContext가 유지되지 않는다면, 다음 요청 시에도 익명 인증 객체가 설정됨
<code>SecurityContextHolder.getContext().setAuthentication(auth)</code>을 제대로 설정하지 않으면
매 요청마다 SecurityContext가 AnonymousAuthenticationToken을 생성하게 됨
결국 모든 요청이 익명 사용자로 처리됨</li>
</ol>
<p>이렇게 정리해도 어려운건 매한가지 인것 같다.</p>
<p>다만 4번 부터 문제일 것이라 생각되었고, 조금 크리티컬한 문제는 CustomFilter 실행 후 아무런 역할을 하지 않는 필터가 작동하기 때문에 <code>AnonymousAuthenticationFilter</code> 이전에 인증 객체를 설정하는 커스텀 필터를 작동하면 문제가 해결될 것이라 생각하고 필터의 순서를 바꿔도 보았다.</p>
<p><strong>아쉽게도 이전에 실행하는 필터를 변경한다해도 문제가 없어지지는 않았다.</strong></p>
<h2 id="해결법">해결법</h2>
<p>그래서 나는 필터 방식을 버리고 Interceptor 방식을 택했다.</p>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig implements WebMvcConfigurer {

    private final GatewayAuthenticationInterceptor gatewayAuthenticationInterceptor;

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .cors(cors -&gt; cors.configurationSource(corsConfigurationSource()))
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(sessionManagement -&gt;
                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .authorizeHttpRequests(auth -&gt; auth
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                        .requestMatchers(&quot;/v1/users/email&quot;,&quot;/v1/users/signup&quot;).permitAll()
                        .requestMatchers(&quot;/v1/users/**&quot;).permitAll()  // 모든 /v1/users/** 경로 허용
                        .anyRequest().permitAll()  // 다른 모든 요청도 허용
                );
        return http.build();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(gatewayAuthenticationInterceptor)
                .addPathPatterns(&quot;/**&quot;)
                .excludePathPatterns(
                    &quot;/error&quot;,
                    &quot;/v1/users/signup&quot;,
                    &quot;/v1/users/email&quot;,
                    &quot;/v1/auth/login&quot;
                );
    }
}

@Component
public class GatewayAuthenticationInterceptor implements HandlerInterceptor {

    @Autowired
    private GatewaySignatureVerifier signatureVerifier;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String userId = request.getHeader(&quot;X-User-Id&quot;);
        String timestamp = request.getHeader(&quot;X-Timestamp&quot;);
        String signature = request.getHeader(&quot;X-Signature&quot;);

        if (userId == null || timestamp == null || signature == null) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }

        boolean isValid = signatureVerifier.isValidSignature(
            userId,
            Long.parseLong(timestamp),
            signature
        );

        if (!isValid) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }

        return true;
    }
} </code></pre>
<p>인터셉터 방식을 적용하기 위해 일단 Spring Security의 모든 경로에 대해 접근을 허용하도록 변경했다.</p>
<p>그 후 모든 경로에 인터셉터를 추가하였고, 따라서 모든 요청에 헤더를 검증할 수 있도록 구현되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MSA Shopping Mall - 8 (Signature)]]></title>
            <link>https://velog.io/@wongi-kim/MSA-Shopping-Mall-8-Signature</link>
            <guid>https://velog.io/@wongi-kim/MSA-Shopping-Mall-8-Signature</guid>
            <pubDate>Thu, 27 Mar 2025 08:48:31 GMT</pubDate>
            <description><![CDATA[<p>이번 포스팅에서 진행할 내용은 X-User-Id의 보안적 취약점을 해결하기 위한 포스팅이다.</p>
<p>6번 포스팅의 하단부분에 다음과 같이 적어뒀었다.</p>
<blockquote>
</blockquote>
<p>보안적인 주의점
일단 X-User-Id 헤더와 같은 경우 Client에서의 직접적인 입력을 반드시 막아야 한다.</p>
<blockquote>
<p>Gateway에서 입력해야 인증객체가 설정되는 것이기 때문에 외부에서 입력할 경우 정상적으로 작동하지 않을 가능성이 높으며 탈취와 같은 위험이 존재할 수 있다.</p>
</blockquote>
<p>이러한 문제를 해결하기 위해 몇 가지 방법이 존재했는데
나는 이번에 Signature를 생성하는 방법으로 하고자 한다.</p>
<p>가장 보안이 좋다고 알려져있는 방식은 내부 통신만 허용하는 방식인데...
어려워서 포기했다... (다음에 공부해보고 도입해보도록 하겠다.)</p>
<hr>
<p>여튼 <code>X-User-Id</code> 방식의 보안적 취약을 해결하는 보편적인 방법은 
<code>X-User-Id</code> 에 기반하여 <code>X-Signature</code> 를 API GateWay에서 생성하는 방식이다.</p>
<p>이렇게 gateway에서 두 가지 헤더가 나오게 된다면 각 마이크로 서비스에서는
<code>X-Signature</code> 검증만 통과하면 된다.</p>
<blockquote>
</blockquote>
<p>TCP 3-Way Handshake에서 각 패킷이 인증 정보를 포함하듯이, 
X-Signature 방식도 API Gateway가 검증 가능한 정보를 포함하여 
보안성을 강화하는 방식이다.</p>
<p>다만 예시를 이렇게 들었다고 해서 해당 방식이 동일하지는 않다.</p>
<p>3-Way Handshake는 <strong>양방향 통신을 위해 연결을 설정하는 과정</strong>이며,
X-Signature는 <strong>서버 간 신뢰성을 검증하는 보안 장치</strong>의 개념이다.</p>
<h1 id="signature-구현">Signature 구현</h1>
<p>앞서 Signature가 가져야 하는 특성이 있다.</p>
<ul>
<li>HMAC(해시 기반 메시지 인증 코드) 또는  RSA 서명을 사용하여야 한다.</li>
<li>X-Signature는 유효 기간을 포함하여 재사용 방지(Replay Attack 방어)를 위한 타임스탬프를 추가해야 한다.</li>
</ul>
<p>이 두 가지 특성을 염두하고 구현을 시작하도록 하겠다.</p>
<h3 id="hmac해시-기반-메시지-인증-코드">HMAC(해시 기반 메시지 인증 코드)</h3>
<p>이번에는 HMAC을 이용하여 구현하도록 하겠다.</p>
<h2 id="signatureutil">SignatureUtil</h2>
<p>SignatureUtil 클래스를 구현하도록 하겠다.
먼저 Signature를 생성하기 위해 SecretKey부터 생성해야 한다.
<code>openssl rand -hex 32</code>
명령어를 터미널에 입력하여 시크릿 키를 생성해야 한다.</p>
<p>그리고 암호화 알고리즘을 설정하여 해싱할 수 있도록 한다.
<code>private static final String HMAC_ALGORITHM = &quot;HmacSHA256&quot;;</code></p>
<p>이제 다음으로는 직접 서명을 생성해야 한다.</p>
<p>jwt token으로 부터 가져온 userId와 현재 시간을 기준으로 Timestamp를 생성하여 서명 문자열을 생성한다.</p>
<p><code>String message = userId + &quot;:&quot; + timestamp;</code></p>
<pre><code class="language-java">Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
mac.init(secretKeySpec);
byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
</code></pre>
<p>먼저 지정한 알고리즘을 통해 mac인스턴스를 초기화 하고 비밀키를 설정한다.
그 후 서명 문자열을 해시 계산하고 계산된 해시를 Base64로 인코딩하여 서명을 생성한다.</p>
<pre><code class="language-java">@Component
public class GatewaySignatureUtil {

    @Value(&quot;${gateway.security.secret-key}&quot;)
    private String secretKey;

    private static final String HMAC_ALGORITHM = &quot;HmacSHA256&quot;;

    public String createSignature(String userId, long timestamp) {
        String message = userId + &quot;:&quot; + timestamp;
        try {
            Mac mac = Mac.getInstance(HMAC_ALGORITHM);
            SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
            mac.init(secretKeySpec);
            byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(hash);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            throw new RuntimeException(&quot;Failed to create signature&quot;, e);
        }
    }
} </code></pre>
<h2 id="customheader-추가">CustomHeader 추가</h2>
<p>6번 포스팅 코드에 추가하면 되며, 추가할 내용도 매우 간단한다.</p>
<p>기존에 작성했던 Filter에 Util 클래스 의존성을 추가해주고
Signature 만 생성할 수 있도록 한다.</p>
<pre><code class="language-java">// 타임스탬프 생성
long timestamp = System.currentTimeMillis();

// 서명 생성
String signature = gatewaySignatureUtil.createSignature(userId, timestamp);

// 새로운 헤더 추가
ServerWebExchange mutatedExchange = exchange.mutate()
        .request(builder -&gt; builder
                .header(&quot;X-User-Id&quot;, userId)
                .header(&quot;X-Timestamp&quot;, String.valueOf(timestamp))
                .header(&quot;X-Signature&quot;, signature))
                .build();</code></pre>
<p>여기까지가 Gateway에서 진행할 내용이다.</p>
<h1 id="각-서비스에서">각 서비스에서</h1>
<p>일단 UserService에서 진행하였으며, 따로 Filter를 구현해서 인증 객체를 설정하는 방법이 아닌 Interceptor를 생성하여 모든 요청을 가로채는 방식으로 구현했다.</p>
<h2 id="securityconfig">SecurityConfig</h2>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig implements WebMvcConfigurer {

    private final GatewayAuthenticationInterceptor gatewayAuthenticationInterceptor;

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(sessionManagement -&gt;
                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .authorizeHttpRequests(auth -&gt; auth
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                        .requestMatchers(&quot;/v1/users/email&quot;,&quot;/v1/users/signup&quot;).permitAll()
                        .requestMatchers(&quot;/v1/users/**&quot;).permitAll()  // 모든 /v1/users/** 경로 허용
                        .anyRequest().permitAll()  // 다른 모든 요청도 허용
                );
        return http.build();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(gatewayAuthenticationInterceptor)
                .addPathPatterns(&quot;/**&quot;)
                .excludePathPatterns(
                    &quot;/error&quot;,
                    &quot;/v1/users/signup&quot;,
                    &quot;/v1/users/email&quot;,
                    &quot;/v1/auth/login&quot;
                );
    }
}</code></pre>
<p>SecurityConfig에서 봐야 할 곳은 모든 경로에 대한 접근을 허용했다는 점이다.
모든 경로의 대한 접근을 허용한 후 모든 경로에 Interceptor를 작동하게 하여 매 요청마다 검증하는 방식으로 구현되었다.</p>
<pre><code class="language-java">@Component
public class GatewayAuthenticationInterceptor implements HandlerInterceptor {

    @Autowired
    private GatewaySignatureVerifier signatureVerifier;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String userId = request.getHeader(&quot;X-User-Id&quot;);
        String timestamp = request.getHeader(&quot;X-Timestamp&quot;);
        String signature = request.getHeader(&quot;X-Signature&quot;);

        if (userId == null || timestamp == null || signature == null) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }

        boolean isValid = signatureVerifier.isValidSignature(
            userId,
            Long.parseLong(timestamp),
            signature
        );

        if (!isValid) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }

        return true;
    }
} </code></pre>
<h2 id="보완할-점과-고려해야-할-점">보완할 점과 고려해야 할 점</h2>
<h3 id="인터셉터는-spring-security-필터보다-먼저-실행된다">인터셉터는 Spring Security 필터보다 먼저 실행된다.</h3>
<p>지금은 모든 요청에 대해 permitAll() 방식으로 되어있기 때문에 문제가 생길 가능성은 없지만 고려해야 할 점은 다음과 같다.</p>
<blockquote>
</blockquote>
<p>인터셉터에서 인증 실패 시 false를 반환하는 방식과 
Spring Security 자체적인 인증/인가 로직의 충돌 가능성이 있다.</p>
<p>특정 엔드포인트에 hasRole(), hasAuthority(), 같은 접근 제어가 걸릴 경우 문제가 발생할 수 있다.</p>
<h2 id="서버-간-요청-재사용replay-attack-방어-필요">서버 간 요청 재사용(Replay Attack) 방어 필요</h2>
<p>해당 부분은 구현하면서 발견했던 문제점이다.</p>
<p>현재 X-Timestamp 기반으로 검증을 수행하지만, timestamp 값이 유효한지 확인하는 로직이 없기 때문에 다음 포스팅에 짤막하게 추가하고 넘어가도록 하겠다.</p>
<h2 id="spring-webflux와의-호환성">Spring WebFlux와의 호환성</h2>
<p>이건 계속 고민하고 있는 문제였다.
아마 추후에도 MVC 기반과 WebFlux 기반의 문제 때문에 충돌할 가능성이 있다고 생각된다.</p>
<p>만약 Spring WebFlux 기반으로 완전 전환한다면, WebFilter 방식으로 인터셉터를 대체하는 것이 더 적절할 수도 있을 것 같지만 일단은 현재 방식으로 구현하도록 하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MSA Shopping Mall - 7(Spring WebFlux, exchange)]]></title>
            <link>https://velog.io/@wongi-kim/MSA-Shopping-Mall-7Spring-WebFlux-exchange</link>
            <guid>https://velog.io/@wongi-kim/MSA-Shopping-Mall-7Spring-WebFlux-exchange</guid>
            <pubDate>Thu, 20 Mar 2025 16:52:33 GMT</pubDate>
            <description><![CDATA[<p>일단 지난 포스팅에서 얘기하지 않았던 Mono와 Flux를 알아볼 겸?
사실상 Spring WebFlux를 알아야 이해가 가능하기 때문에 Spring WebFlux를 공부하도록 하겠다.</p>
<h1 id="spring-webflux">Spring WebFlux</h1>
<p>Spring WebFlux에 대해 조금 더 알고 가야 한다.</p>
<blockquote>
<p><a href="https://inpa.tistory.com/entry/%F0%9F%91%A9%E2%80%8D%F0%9F%92%BB-%EB%8F%99%EA%B8%B0%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%B8%94%EB%A1%9C%ED%82%B9%EB%85%BC%EB%B8%94%EB%A1%9C%ED%82%B9-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC">https://inpa.tistory.com/entry/👩‍💻-동기비동기-블로킹논블로킹-개념-정리</a> [Inpa Dev 👨‍💻:티스토리]
위 블로그에서 자세하게 나와있는 것 같다.</p>
</blockquote>
<p>먼저 Spring WebFlux를 한 문장으로 정의하면</p>
<blockquote>
</blockquote>
<p>Spring WebFlux 는 비동기 + 논블로킹 + 반응형 기반의 웹 프레임워크</p>
<p>라고 정의할 수 있는데... 벌써부터 어질하다.</p>
<p>또한 기존의 Spring MVC는 동기 + 블로킹I/O 기반으로 되어있다면
Spring WebFlux는 비동기 + 논블로킹 기반으로 되어있다는 특징이 존재한다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>Spring MVC (Blocking)</th>
<th>Spring WebFlux (Non-blocking)</th>
</tr>
</thead>
<tbody><tr>
<td>처리 방식</td>
<td>Thread-per-request (요청마다 스레드 할당)</td>
<td>Event-driven + Reactor 기반</td>
</tr>
<tr>
<td>동작 방식</td>
<td>요청이 완료될 때까지 스레드가 대기</td>
<td>요청 후 바로 반환, 데이터가 준비되면 이벤트 기반 처리</td>
</tr>
<tr>
<td>I/O 처리</td>
<td>Blocking I/O (동기)</td>
<td>Non-blocking I/O (비동기)</td>
</tr>
<tr>
<td>반환 타입</td>
<td><code>User</code>, <code>List&lt;User&gt;</code></td>
<td><code>Mono&lt;User&gt;</code>, <code>Flux&lt;User&gt;</code></td>
</tr>
<tr>
<td>성능</td>
<td>동시 요청이 많아지면 스레드 부족</td>
<td>적은 스레드로 더 많은 요청 처리 가능</td>
</tr>
</tbody></table>
<p>표로 요약해보면 다음과 같이 나온다.</p>
<h2 id="기존-spring-mvc의-동기blocking-io-모델">기존 Spring MVC의 동기(Blocking I/O) 모델</h2>
<p>Spring MVC는 서블릿(Servlet) 기반의 프레임워크로, 내부적으로 동기(Blocking) 방식의 I/O 처리를 사용한다.</p>
<h3 id="spring-mvc의-주요-특징">Spring MVC의 주요 특징</h3>
<ol>
<li>Thread-per-request 모델</li>
</ol>
<ul>
<li>요청이 들어오면 스레드를 할당하고, 응답이 끝날 때까지 해당 스레드가 차단됨.</li>
<li>요청 수가 많아지면 스레드가 부족해지고, 성능이 저하됨.</li>
</ul>
<ol start="2">
<li>Blocking I/O (동기 방식)</li>
</ol>
<ul>
<li>데이터베이스 쿼리, API 호출 등의 작업이 끝날 때까지 스레드가 대기(Block)함.</li>
<li>CPU 리소스를 비효율적으로 사용하게 됨.</li>
</ul>
<h2 id="spring-webflux와-비동기non-blocking-io">Spring WebFlux와 비동기(Non-blocking) I/O</h2>
<p>Spring WebFlux는 Reactor 기반의 비동기 논블로킹(Non-blocking) 방식을 사용한다.
즉, 요청이 들어와도 스레드가 대기하지 않고, 이벤트가 발생하면 비동기적으로 처리한다.</p>
<p>###Spring WebFlux의 주요 특징</p>
<ol>
<li>Event Loop 모델</li>
</ol>
<ul>
<li>요청을 스레드 하나에서 여러 개 처리 가능</li>
<li>기존의 스레드 차단 방식(Blocking I/O) 대신 비동기 이벤트 기반(Non-blocking I/O) 사용</li>
</ul>
<p>2.Reactor 기반의 Reactive Streams 지원</p>
<ul>
<li>Mono(01개) 또는 Flux(0N개)를 반환</li>
<li>데이터가 준비되면 이벤트 기반으로 Subscriber가 데이터를 받음</li>
<li>기존 Blocking 코드보다 더 많은 동시 요청 처리 가능</li>
</ul>
<h2 id="비동기">비동기</h2>
<p>비동기는 다들 알듯이 프로그래밍에서 비동기는 작업이 독립적으로 실행되며, 작업이 시작되면 해당 작업이 완료될 때까지 기다리지 않고 다음 코드를 실행할 수 있다는 것을 의미한다.</p>
<h2 id="논-블로킹">논 블로킹</h2>
<p>논블록킹은 단어에서 알 수 있듯이 다른 요청의 작업을 처리하기 위해 현재 작업을 block(차단, 대기) 하냐 안하냐의 유무를 나타내는 프로세스의 실행 방식이다.</p>
<h2 id="reactor">Reactor</h2>
<blockquote>
</blockquote>
<p>Reactor는 Spring WebFlux에서 비동기 스트림을 처리하기 위해 사용하는 라이브러리</p>
<ul>
<li>Reactive Streams 표준을 구현한 라이브러리이며, Publisher-Subscriber 패턴을 기반으로 동작한다.</li>
<li>Mono 와 Flux라는 두 가지 기본 데이터 타입을 제공한다.</li>
</ul>
<h3 id="reactive-streams">Reactive Streams</h3>
<p>Reactive Streams는 비동기 + 논블로킹 + 백프레셔 지원을 위한 표준 스펙으로
<strong>데이터 스트림을 효율적으로 처리할 수 있도록 설계된 비동기 데이터 흐름 표준</strong></p>
<h4 id="reactive-streams가-필요한-이유">Reactive Streams가 필요한 이유</h4>
<p>위에서 잠깐 얘기했듯이 기존의 Spring MVC 즉, 블로킹 방식에서는 데이터의 소비 속도보다
공급 속도가 빠르면 성능 저하 및 메모리 과부하가 발생한다.</p>
<hr>
<p>이쯤에서 Reactor는 잠깐 끊어야겠다.
당연히 Spring WebFlux가 Reactor 기반이니까 계속 공부하는게 나을 수 있지만 
지금은 어떤 특성을 가지고 있는지 알고만 가는게 맞다고 생각된다.</p>
<p><strong>여튼 요약하자면 Spring WebFlux 는 대용량의 데이터와 대규모 트래픽을 유연하고 안정적으로 처리할 수 있는 라이브러리 기반으로 제작된 프레임워크라는 것이다.</strong></p>
<hr>
<h2 id="mono--flux-reactive-streams-구현체">Mono &amp; Flux (Reactive Streams 구현체)</h2>
<p>Spring WebFlux에서는 Reactor 라이브러리를 사용하며, Reactor는 Reactive Streams를 구현하고 있다.
Mono와 Flux는 Reactive Streams 인터페이스를 구현한 Reactive 타입이다.</p>
<table>
<thead>
<tr>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>Mono&lt;T&gt;</code></td>
<td>0~1개의 데이터 처리</td>
</tr>
<tr>
<td><code>Flux&lt;T&gt;</code></td>
<td>0~N개의 데이터 스트림 처리</td>
</tr>
</tbody></table>
<p>Mono와 Flux는 각각 위와 같은 특성을 가지고 있는데 지금까지 작성했던 내용을 보고
지난 포스팅에서의 반환 타입이 Mono인 것을 보면 왜 Mono를 사용했는지 알 수 있다.</p>
<h3 id="왜-mono였을까">왜 Mono였을까?</h3>
<ol>
<li><p>MSA의 가장 큰 특징은 각 서비스(컴포넌트)가 이벤트를 <strong>비동기</strong>적으로 송수신 하며 자신의 업무를 계속해나간다.</p>
</li>
<li><p>Spring WebFlux 는 비동기 + <strong>논블로킹</strong> + 반응형 기반의 웹 프레임워크이며,
Spring WebFlux의 filter() 메서드는 논블로킹 방식으로 작동해야 한다.
( Spring WebFlux는 비동기 + 논블로킹 기반의 프레임워크이므로, 내부에서 실행되는 모든 필터(WebFilter) 역시 논블로킹 방식으로 동작해야 성능을 극대화할 수 있다. )</p>
</li>
<li><p>JWT 필터에서 단일 인증 객체를 처리하기 때문이다. (0~1개의 데이터 처리)</p>
</li>
</ol>
<p>이러한 3가지 이유로 Filter에서 Mono를 통해 인증 객체를 전달하는 것이다.</p>
<h1 id="exchange">Exchange</h1>
<p>대충 하면 안되겠지만 이만하면 겉핥기정도는 된 것 같다.
그래서 다음 내용으로 Exchage에 관한 내용을 짧막하게 적고 가겠다.</p>
<p>Exchange가 어디서 나왔냐면... <code>SecurityConfig</code>나 <code>JwtFilter</code>에 모두 나왔다.</p>
<p>일단 Spring WebFlux가 Netty기반으로 되어있다.
기존의 Spring MVC 에서는 Tomcat 기반이었고, Tomcat은 Servlet기반의 블로킹 형태의 WAS다.
그렇다면 Spring WebFlux는 Netty 기반이고, Netty는 논블로킹 형태의 WAS다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>HttpServletRequest</th>
<th>ServerWebExchange</th>
</tr>
</thead>
<tbody><tr>
<td>동기/비동기</td>
<td>동기 (Blocking)</td>
<td>비동기 (Non-blocking)</td>
</tr>
<tr>
<td>요청 읽기 방식</td>
<td>InputStream을 사용 (한 번만 읽기 가능)</td>
<td>exchange.getRequest().getBody() (다중 읽기 가능)</td>
</tr>
<tr>
<td>반응형 지원</td>
<td>지원하지 않음</td>
<td>지원</td>
</tr>
</tbody></table>
<p>이러한 특징으로 Spring Webflux를 사용하는 MSA에서 ServerWebExchange를 사용한다.
(exchange는 Request + Response를 한 번에 관리할 수 있기 때문에 WebFlux에서 사용된다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MSA Shopping Mall - 6(API Gateway)]]></title>
            <link>https://velog.io/@wongi-kim/MSA-Shopping-Mall-6API-Gateway</link>
            <guid>https://velog.io/@wongi-kim/MSA-Shopping-Mall-6API-Gateway</guid>
            <pubDate>Tue, 18 Mar 2025 07:51:49 GMT</pubDate>
            <description><![CDATA[<p>이번 포스팅은 API Gateway 부분을 구현할 차례다.</p>
<p>지난 포스팅에서 User와 Auth간의 소통을 구현했었는데
별개의 서버에서 API를 불러다 사용하는 방식으로 구현했었다.</p>
<p>그렇지만 MSA는 서버간의 직접 소통을 권하지는 않는다.</p>
<p>이 문제를 해결하기 위해서 API Gateway를 설정하고 모든 서비스는 API Gateway를 통하여 소통하도록 구현해야 한다.</p>
<h1 id="jwtutil">JwtUtil</h1>
<p>먼저 gateway에 JwtUtil을 구현한다.</p>
<p>Auth-Service 에서 받아온 로그인을 위한 인증 토큰을 사용하기 위해 필요한 구성이다.</p>
<pre><code class="language-java">@Component
public class JwtUtil {

    // TODO : Secret Key 환경 변수로 수정
    private final String SECRET_KEY = &quot;2~~&quot;;

    public String extractEmail(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY.getBytes(StandardCharsets.UTF_8))
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parser()
                    .setSigningKey(SECRET_KEY.getBytes(StandardCharsets.UTF_8))
                    .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}</code></pre>
<p>지금은 간단한 구현이기 때문에 SECRET_KEY 자체를 하드코딩 해두었다.</p>
<p>그 다음으로는 정보 추출과 토큰 검증이다.</p>
<h1 id="jwtauthenticationfilter">JwtAuthenticationFilter</h1>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter implements WebFilter {
    private final JwtUtil jwtUtil;
    private static final List&lt;String&gt; EXCLUDED_PATHS = List.of(&quot;/v1/auth/login&quot;, &quot;/v1/users/email&quot;, &quot;/v1/users/signup&quot;);

    @Override
    public Mono&lt;Void&gt; filter(ServerWebExchange exchange, WebFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();

        if (EXCLUDED_PATHS.contains(path)) {
            return chain.filter(exchange);
        }

        String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (authHeader == null || !authHeader.startsWith(&quot;Bearer &quot;)) {
            return onError(exchange, &quot;Missing or Invalid Authorization Header&quot;, HttpStatus.UNAUTHORIZED);
        }

        String token = authHeader.substring(7);
        if (!jwtUtil.validateToken(token)) {
            return onError(exchange, &quot;Invalid JWT Token&quot;, HttpStatus.UNAUTHORIZED);
        }

        // userId 추출
        String userId = jwtUtil.extractEmail(token);

        // userId를 헤더에 추가해서 downstream service로 넘겨줌
        ServerWebExchange mutatedExchange = exchange.mutate()
                .request(builder -&gt; builder.header(&quot;X-User-Id&quot;, userId))
                .build();

        return chain.filter(mutatedExchange);
    }

    private Mono&lt;Void&gt; onError(ServerWebExchange exchange, String err, HttpStatus status) {
        exchange.getResponse().setStatusCode(status);
        return exchange.getResponse().setComplete();
    }
}</code></pre>
<p>전체 코드는 이러하며 각 코드가 하는 일을 조금 살펴보도록 하겠다.</p>
<h2 id="webfilter">WebFilter</h2>
<p>일단 MSA 관점에서 API Gateway 레이어에서 인증 처리는 대표적인 패턴이다.</p>
<p>WebFilter는 Spring WebFlux의 비동기 논블로킹 필터로서, Gateway의 필터 체인에서 모든 요청 전처리 역할을 수행한다.</p>
<p>이 말은 다음과 같이 정리할 수 있다.</p>
<ul>
<li>servlet filter가 아닌 Reactive 환경에 맞춰 설계된 필터</li>
<li>비동기 스트림에서 효율적인 성능 제공</li>
<li><code>ServerWebExchange</code> 로 요청과 응답을 통째로 다룰 수 있어 헤더 수정, Body 조작 등 유연성 확보</li>
<li>API Gateway 레이어에서 권한 체크, 토큰 파싱, 인증 실패 차단 등 전방위 컨트롤 가능</li>
</ul>
<h3 id="webflux">WebFlux</h3>
<blockquote>
</blockquote>
<p><a href="https://adjh54.tistory.com/232">https://adjh54.tistory.com/232</a></p>
<h2 id="serverwebexchange">ServerWebExchange</h2>
<p>ServerWebExchange는 불변 객체로 mutate로 복제 후 헤더에 X-User-Id라는 헤더를 추가하여
다른 서비스로 넘겨주는 방식이다.</p>
<pre><code class="language-java">ServerWebExchange mutatedExchange = exchange.mutate()
    .request(builder -&gt; builder.header(&quot;X-User-Id&quot;, userId))
    .build();</code></pre>
<h1 id="security-config">Security Config</h1>
<pre><code class="language-java">@Configuration
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) throws Exception {
        http
                .csrf(ServerHttpSecurity.CsrfSpec::disable)
                .authorizeExchange(exchanges -&gt; exchanges
                        .pathMatchers(&quot;/v1/auth/**&quot;).permitAll()
                        .pathMatchers(&quot;/v1/users/**&quot;).permitAll()
                        .pathMatchers(&quot;/logout&quot;).permitAll()
                        .pathMatchers(&quot;/error&quot;).permitAll()
                        .anyExchange().authenticated()
                )
                .addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);

        return http.build();
    }
}</code></pre>
<p>평소에 사용하던 Security Config 와 비슷하지만
authorizeExchange 라던가 pathMatchers 처럼 경로를 허용하는 방식등 기존의
Web MVC 패턴이 아닌 경로 형태로 진행된다.</p>
<p>위와 같이 구현하면 다른 서비스에서도 인증이 가능하다.</p>
<h1 id="보안적인-주의점">보안적인 주의점</h1>
<p>일단 X-User-Id 헤더와 같은 경우 Client에서의 직접적인 입력을 반드시 막아야 한다.</p>
<p>Gateway에서 입력해야 인증객체가 설정되는 것이기 때문에 외부에서 입력할 경우 정상적으로 작동하지 않을 가능성이 높으며 탈취와 같은 위험이 존재할 수 있다.</p>
<h1 id="끝">끝!</h1>
<p>이번 포스팅은 조금 공부가 더 필요할 것 같기 때문에 다음 포스팅에서는 WebFlux, Mono등 조금 더 용어나 그 외의 주요 특징들을 다루는 글을 적도록 하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MSA Shopping Mall - Trouble Shooting 1]]></title>
            <link>https://velog.io/@wongi-kim/MSA-Shopping-Mall-Trouble-Shooting-1</link>
            <guid>https://velog.io/@wongi-kim/MSA-Shopping-Mall-Trouble-Shooting-1</guid>
            <pubDate>Fri, 14 Mar 2025 16:07:55 GMT</pubDate>
            <description><![CDATA[<p>처음 FeignClient를 사용했을 때 인터넷 자료가 전부 PathVariable 방식이었다.</p>
<p>물론 지금은 PathVariable 방식을 사용해도 큰 문제가 없으나
나중을 고려해서 RequestBody 방식으로 수정하여 사용하려 했다.</p>
<pre><code class="language-java">@FeignClient(name = &quot;msa-user-service&quot;, url = &quot;http://localhost:8081&quot;) // in local environment 
public interface UserServiceClient {
    @GetMapping (&quot;v1/users/email&quot;)
    UserDto findUserByEmail(@RequestBody EmailRequestDto email);
}</code></pre>
<p>UserController 내에서도 당연히 정보를 얻어오는것이니까 GetMapping을 사용했고
그에따라 FeignClient 역시 GetMapping으로 설정했다.</p>
<p>처음에는 이렇게 하고 포스트맨에서 api를 호출해보니 403에러가 발생했다.</p>
<h2 id="403">403?</h2>
<p>403이면 뭐 SecurityConfig에서 CORS 설정을 안해줬겠지 하고 부랴부랴 UserService에 CORS 설정을 해주었다.</p>
<p>왜 CORS 부터 의심했냐면 결국 두 가지의 서비스이기 때문에 port 번호가 달라 접근할 수 없다고 생각했다.</p>
<h2 id="그래도-403">그래도 403</h2>
<p>여튼 UserService에 CORS 설정을 해두어도 계속 403 에러가 발생했다.
<img src="https://velog.velcdn.com/images/wongi-kim/post/66f1713f-5d6b-452d-9c6e-78b67b0c8c12/image.png" alt="">
<img src="https://velog.velcdn.com/images/wongi-kim/post/00908d2d-a6c4-48d3-a131-e5f127569676/image.png" alt=""></p>
<p>그렇다면 의심해볼 내용은 역시 FeignClient일 것 같다.
FeignClient를 계속 찾아보면서 </p>
<blockquote>
<p><a href="https://yijoon009.tistory.com/entry/Parameter-Handling-in-Spring-Boot-Using-Feign-Client">https://yijoon009.tistory.com/entry/Parameter-Handling-in-Spring-Boot-Using-Feign-Client</a></p>
</blockquote>
<p>위의 블로그 내용이 어쩌다가 서치가 되었는데</p>
<p>RequestBody의 주의점으로 <strong>GET 요청에는 적합하지 않다</strong> 라는 내용이 존재한다.</p>
<h2 id="post로-바꾸자">POST로 바꾸자</h2>
<p>그래서 두 곳의 메서드 모두 POST 로 변경했다.</p>
<p><img src="https://velog.velcdn.com/images/wongi-kim/post/37de9a9c-752b-4ee8-a870-e100e00e25fd/image.png" alt=""></p>
<p>그리고 실행해보면...</p>
<p><img src="https://velog.velcdn.com/images/wongi-kim/post/3270d0ef-36d7-4098-b66c-b0186ef411bd/image.png" alt=""></p>
<p>로그인 결과가 잘 나오는걸 볼 수 있다.</p>
<h1 id="뒤늦게-알게-된-사실">뒤늦게 알게 된 사실</h1>
<p>AuthService만의 문제라고 생각해서 AuthService의 로그만 보고있었다.</p>
<p><img src="https://velog.velcdn.com/images/wongi-kim/post/25373604-213c-416e-8721-36e91debbf39/image.png" alt=""></p>
<p>그렇지만 403만 알려주니까 왜 그런지 몰랐는데...</p>
<p><img src="https://velog.velcdn.com/images/wongi-kim/post/09fc9719-225e-4a37-b78a-0e2424002e28/image.png" alt=""></p>
<p>UserService에서 친절하게 얘기하고 있었다.
POST 요청 들어오는데 POST 요청은 없어서 못받는다고...</p>
<h1 id="왜-feign-client는">왜 Feign Client는</h1>
<p>RequestBody를 보낼 때 POST만 가능할까?</p>
<p>답은 이전 포스트에서 얘기했었다.</p>
<blockquote>
</blockquote>
<p>Feign Client는 Spring Cloud에서 제공하는 HTTP 클라이언트로, REST API 호출을 인터페이스 기반으로 간결하게 구현할 수 있도록 도와주는 라이브러리</p>
<p>라고 요약했는데 다른 말로 풀어보면
<strong>Feign Client는 HTTP 클라이언트를 구현한 인터페이스라서 HTTP 요청의 특성을 그대로 따르게된다.</strong></p>
<p>결국 HTTP 요청 본문을 포함할 수 있는 메서드가 POST, PUT, PATCH 메서드 밖에 없다.</p>
<p>아마 조금 더 Feign Client를 일찍 찾아봤다면 하지 않았을 실수 같다...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MSA Shopping Mall - 5(User, Auth, FeignClient)]]></title>
            <link>https://velog.io/@wongi-kim/MSA-Shopping-Mall-5User-Auth</link>
            <guid>https://velog.io/@wongi-kim/MSA-Shopping-Mall-5User-Auth</guid>
            <pubDate>Fri, 14 Mar 2025 15:32:08 GMT</pubDate>
            <description><![CDATA[<p>이번 포스팅에서는 User와 Auth 부분을 구현할 예정이다.</p>
<h1 id="user">User</h1>
<p>그렇지만 User는 매우 간단하다.</p>
<p>지난 포스팅에서 얘기 했듯이 회원을 등록하는게 User-Service가 하는 일이다.</p>
<p>다만 1가지 추가가 되었는데 Auth에서 해당 User를 찾을 수 있도록,
Email을 통한 Read 작업이라고 할 수 있다.</p>
<h2 id="회원-가입">회원 가입</h2>
<p>회원 가입은 매우 간단하다.</p>
<pre><code class="language-java">    @Transactional
    public void signup(SignupRequestDto requestDto) {
        String encodedPassword = passwordEncoder.encode(requestDto.getPassword());

        // 저장하기 전 User 객체 생성
        User newUser = User.builder()
                .username(requestDto.getUsername())
                .email(requestDto.getEmail())
                .password(encodedPassword)
                .phoneNumber(requestDto.getPhoneNumber())
                .role(UserRole.USER)
                .build();

        // User가 이미 존재하는지 확인
        if(userRepository.findByEmail(newUser.getEmail()).isEmpty()) {
            userRepository.save(newUser);
        } else {
            throw new CustomException(ErrorCode.CONFLICT_USER);
        }
    }</code></pre>
<p> 단순하게 Dto에 담긴 내용을 꺼내 이미 <code>newUser</code>로 객체 생성한 뒤</p>
<p>조건을 통해 isEmpty를 만족한다면 기존에 존재하지 않은 email로 인식하여 가입에 성공하고,
그 외의 경우는 중복 처리를 해준다.</p>
<h2 id="회원-조회">회원 조회</h2>
<p>회원 조회 역시 간단하다.</p>
<pre><code class="language-java">    @Transactional(readOnly = true)
    public UserDto findEmail(String email) {
        User user = userRepository.findByEmail(email)
                .orElseThrow(() -&gt; new CustomException(ErrorCode.USER_NOT_FOUND));

        return UserDto.builder()
                .id(user.getId())
                .username(user.getUsername())
                .email(user.getEmail())
                .phoneNumber(user.getPhoneNumber())
                .role(user.getRole().toString())
                .build();
    }</code></pre>
<p>딱히 설명할 게 없으므로 Pass하도록 하겠다.</p>
<h1 id="auth">Auth</h1>
<h2 id="그-전에">그 전에..</h2>
<p>일단 서비스 로직을 작성하기 전에 잠깐 얘기하자면</p>
<p><strong>User와 Auth를 분리하기로 했다.</strong></p>
<p>가능하면 지금부터 분리할 수 있는 내용은 분리하는게 조금 더 낫다는 판단이 있었기 때문이다.</p>
<h2 id="초기-설정">초기 설정</h2>
<p>일단 Auth 서비스는 현재 시점 기준으로 db를 따로 갖고있지 않는다.
아마 OAuth나 그 외의 서드파티 로그인을 구현한다 해도 DB를 가지고 있지 않을 예정이다.</p>
<p>User 서비스의 어떤 방식으로 가입했는지 구분할 수 있는 컬럼이 생기는 변화가 존재할 예정이다.</p>
<p>따라서 매번 하던 SpringBoot 의존성 추가에 JPA라거나 DB 관련한 드라이버는 제외해야 한다.
또한 properties, yml 역시 db연결 부분을 제외해줘야 한다.</p>
<p>내용추가</p>
<h2 id="로그인">로그인</h2>
<p>로그인 시점에서 Auth 서비스는 입력받은 email을 기반으로 회원이 존재하는지 확인해야 한다.</p>
<p>하지만 DB를 소유하고 있지 않기 때문에 User 서비스의 DB에서 email에 맞는 User 정보를 가져와야 한다.</p>
<p>그리고 가져온 정보를 토대로 JWT를 발급한다.</p>
<h3 id="userserviceclient--feignclient-">UserServiceClient ( FeignClient )</h3>
<p>먼저 <code>UserServiceClient</code> 라는 인터페이스를 생성해준다.</p>
<pre><code class="language-java">@FeignClient(name = &quot;msa-user-service&quot;, url = &quot;http://localhost:8081&quot;) // in local environment 
public interface UserServiceClient {
    @PostMapping (&quot;v1/users/email&quot;)
    UserDto findUserByEmail(@RequestBody EmailRequestDto email);
}</code></pre>
<p>먼저 코드를 보면 <code>FeignClient</code>라는게 보인다.</p>
<blockquote>
</blockquote>
<ul>
<li><a href="https://velog.io/@choidongkuen/FeignClient-%EC%82%AC%EC%9A%A9%EB%B2%95%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B4%85%EC%8B%9C%EB%8B%A4.-Spring-Cloud-OpenFeign">https://velog.io/@choidongkuen/FeignClient-사용법에-대해-알아봅시다.-Spring-Cloud-OpenFeign</a></li>
<li><a href="https://velog.io/@skyepodium/2019-10-06-1410-%EC%9E%91%EC%84%B1%EB%90%A8">https://velog.io/@skyepodium/2019-10-06-1410-작성됨</a></li>
</ul>
<p>잘 정리된 글이 있어 가져왔다.</p>
<p>일단 여기서 간단하게 요약한다면 </p>
<blockquote>
</blockquote>
<p>Feign Client는 Spring Cloud에서 제공하는 <strong>HTTP 클라이언트로, REST API 호출을 인터페이스 기반으로 간결하게 구현</strong>할 수 있도록 도와주는 라이브러리</p>
<blockquote>
</blockquote>
<p>일반적인 RestTemplate 또는 WebClient보다 코드가 간단하고, 선언적인 방식으로 API 호출 가능</p>
<p>대충 HTTP 클라이언트의 기능을 간단한 코드로 구현한 내용이라고 생각하면 된다.</p>
<h3 id="다시-코드로-넘어와서">다시 코드로 넘어와서</h3>
<pre><code class="language-java">@FeignClient(name = &quot;msa-user-service&quot;, url = &quot;http://localhost:8081&quot;) // in local environment 
public interface UserServiceClient {
    @PostMapping (&quot;v1/users/email&quot;)
    UserDto findUserByEmail(@RequestBody EmailRequestDto email);
}</code></pre>
<p>해당 코드를 통해 
<code>http://localhost:8081/v1/users/email</code> 에 접근하여 결과를 받을 수 있다는 점이다.</p>
<p>따라서 저 코드를 통해 받는 정보는 위에서 작성했던 회원 조회의 결과값이다.</p>
<h3 id="authservice">AuthService</h3>
<p>이제 위에서 구현한 코드를 실제로 사용할 타이밍이다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class AuthService {

    private final UserServiceClient userServiceClient;
    private final JwtUtil jwtUtil;
    private final PasswordEncoder passwordEncoder;

    public String login(LoginRequestDto requestDto) {
        // 1. email 을 통해 UserService 에서 UserDto 로 객체 생성
        EmailRequestDto emailRequestDto = new EmailRequestDto(requestDto.getEmail());
        UserDto user = userServiceClient.findUserByEmail(emailRequestDto);

        // 2. UserDto 의 password 와 request 의 password 를 비교
        if (!passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) {
            throw new CustomException(ErrorCode.AUTHENTICATION_FAILED);
        }

        // 3. JWT 토큰 생성
        return jwtUtil.generateToken(user.getEmail());
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[MSA Shopping Mall - 4(Poly Repo, 개발 순서)]]></title>
            <link>https://velog.io/@wongi-kim/MSA-Shopping-Mall-4Poly-Repo%EB%B6%80%ED%84%B0-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@wongi-kim/MSA-Shopping-Mall-4Poly-Repo%EB%B6%80%ED%84%B0-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Fri, 14 Mar 2025 08:26:24 GMT</pubDate>
            <description><![CDATA[<p>이번 프로젝트는 Poly Repo부터 시작해볼 생각이다.</p>
<p>일단 Mono Repo와 Poly Repo의 차이를 간략하게 알아보자</p>
<h1 id="mono-repo-vs-polymulti-repo">Mono Repo vs Poly(Multi) Repo</h1>
<ul>
<li>Mono Repo : msa-shopping-mvp 하나로 통합</li>
</ul>
<p>즉 우리가 그냥 Github에 생성하는 하나의 Repo를 말하는 것 같다.
아마 지금까지 해온 Monolithic의 Mono랑 같은 맥략인듯 하다.</p>
<ul>
<li>Poly(Multi) Repo : user-service, order-service, cart-service 등으로 분리 (운영 확장성↑)</li>
</ul>
<p>MSA의 특징은 각 서비스가 독립적으로 분리되어있고, 그에 따라 DB 역시 독립적으로 존재한다는 것이다. </p>
<p>즉, Repo 역시 여러개 존재하는 구조가 Poly(Multi) Repo이다.</p>
<p>내가 생각하기에 MSA 구조인 만큼 MVP를 만든다 하여도 Poly Repo 구조로 시작해보는게 나을 것 같다. 
<del>(뭐 CI/CD가 힘들 수 있지만 어떻게든 되겠지)</del></p>
<h2 id="polymulti-repo-설정">Poly(Multi) Repo 설정</h2>
<p>딱히 설정이라고 할 건 없다.</p>
<p>다만 repo를 여러개 생성하는 것인데... 그 repo를 하나의 Repo로 묶어서 관리하는 방법도 존재하겠지만</p>
<p>나는 이번에 Organization을 생성하여 관리하도록 할 예정이다.</p>
<p>Organization을 생성하면 권한 관리와 협업이 어렵움과 CI/CD 구성의 불편함도 해소 될 수 있다.</p>
<h1 id="개발-순서">개발 순서</h1>
<h2 id="를-정하기-전에-api-gateway에-대해-조금-더-알아보자">를 정하기 전에 API Gateway에 대해 조금 더 알아보자</h2>
<p>예전에 잠깐 Node.js를 봤을 때, 정확히는 Express.js를 사용할 때 routes를 생성해서 하는 방식이
API Gateway랑 비슷한 방식일것이라고 짐작으로 예상했었다.</p>
<p>다만 Express에서는 단순하게 클라이언트의 요청을 핸들러로 넘기는,
REST API의 엔드포인트 관리만을 담당한다.</p>
<p>그에 반해 API Gateway는 각각의 마이크로 서비스의 요청을 라우팅하기 때문에 
라우팅 범위에서도 꽤나 다르게 작동하며  <code>보안, 로드 밸런싱, 장애 대응, 트래픽 제한</code> 등의 
추가 기능 역시 API Gateway에서 담당한다.</p>
<p>일단 내가 중요하다고 생각했던 내용은 라우팅에 관련한 내용이며 라우팅과 Security관련된 내용을
API Gateway에 작성해야 하고 따라서 개발 순서도 Gateway가 1순위이며 그 외에 각 서비스로 진행될 줄 알았다.</p>
<h2 id="개발-순서-1">개발 순서</h2>
<p>그렇지만 라우팅을 하기 전에 각 서비스에서 비지니스 로직을 구현하고,
각 서비스에서 제작된 API들을 API Gateway쪽으로 넘기는 방식으로 진행 될 것 같다.</p>
<h2 id="가장-처음으로">가장 처음으로</h2>
<p>그래서 가장 처음 개발할 서비스 로직은 아마 User (&amp; Auth) 쪽이지 않을까 싶다.</p>
<p>개발할 내용으로는 다음과 같으리라 예상된다.</p>
<blockquote>
</blockquote>
<p><strong>User Service</strong></p>
<ol>
<li>회원 가입</li>
</ol>
<blockquote>
</blockquote>
<p><strong>Auth Service</strong>
2. 로그인 (당장에는 email 로그인이기 때문에 User에 위치할 가능성이 높음)
3. JWT 발급</p>
<p>각 서비스에서 JWT 발급이 끝난다면</p>
<blockquote>
</blockquote>
<p><strong>API Gateway</strong></p>
<ol>
<li>JWT 검증 및 사용자 정보 추출(Filter)</li>
<li>인증된 요청을 각 서비스로 전달</li>
</ol>
<p>검증까지 필요하다면 API Gateway전에 상품을 올리는 product Service로 검증해볼 수 있겠다.</p>
<h1 id="끝">끝!</h1>
<p>다음 포스팅 부터는 진짜 개발이 진행될 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MSA Shopping Mall - 3(API Gateway 및 기술 스택 선정)]]></title>
            <link>https://velog.io/@wongi-kim/MSA-Shopping-Mall-3API-Gateway-%EB%B0%8F-%EA%B8%B0%EC%88%A0-%EC%8A%A4%ED%83%9D-%EC%84%A0%EC%A0%95</link>
            <guid>https://velog.io/@wongi-kim/MSA-Shopping-Mall-3API-Gateway-%EB%B0%8F-%EA%B8%B0%EC%88%A0-%EC%8A%A4%ED%83%9D-%EC%84%A0%EC%A0%95</guid>
            <pubDate>Tue, 11 Mar 2025 07:47:42 GMT</pubDate>
            <description><![CDATA[<p>일단 이번 포스팅에서 API Gateway와 그 외의 기술 스택을 제대로 선정하고 진행할 계획이다.</p>
<p>확정된 기술은 다음과 같으며</p>
<blockquote>
</blockquote>
<p>MSA 기반이므로 각 서비스의 기술 스택 확정
Spring Boot → User/Auth, Product, Order, Cart 서비스
MySQL → 관계형 데이터 저장
Redis → 로그인 토큰 캐싱</p>
<p>아래의 기술은 조금 더 선별하는 과정이 필요하다.</p>
<blockquote>
</blockquote>
<p>Kafka or RabbitMQ → 주문 상태 변경 이벤트 처리 고려
Docker &amp; Kubernetes → 서비스 배포 고려
API Gateway 필요 여부 검토 (ex: Spring Cloud Gateway, Kong API Gateway)
CI/CD 파이프라인 구성 고려 (ex: GitHub Actions, Jenkins)</p>
<h2 id="kafka-or-rabbitmq">Kafka or RabbitMQ</h2>
<p>일단 MSA 구조에서 해당 기술 스택이 필요한 이유는 다음과 같다.</p>
<blockquote>
</blockquote>
<ul>
<li>주문이 생성되거나 상태가 변경될 때(확인됨, 배송 중, 완료됨 등) 다른 서비스(예: 결제, 알림, 배송)와 비동기적으로 이벤트를 주고받아야 함.</li>
<li>비동기 메시징을 통해 서비스 간 결합도를 낮추고 확장성을 높일 수 있음.</li>
</ul>
<p>표를 통해 살펴보면 각 스택은 이러한 특징을 가지고 있다.</p>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>Kafka</th>
<th>RabbitMQ</th>
</tr>
</thead>
<tbody><tr>
<td>아키텍처</td>
<td>분산 로그 기반의 스트리밍 플랫폼</td>
<td>메시지 큐 기반의 브로커</td>
</tr>
<tr>
<td>메시지 처리</td>
<td>높은 처리량, 로그 리텐션이 가능</td>
<td>낮은 지연 시간, 빠른 메시지 전달</td>
</tr>
<tr>
<td>데이터 영속성</td>
<td>메시지를 로그에 저장하여 내구성이 뛰어남</td>
<td>메시지를 소비하면 삭제됨 (옵션 조정 가능)</td>
</tr>
<tr>
<td>사용 사례</td>
<td>주문 트래킹, 실시간 분석, 로그 처리</td>
<td>주문 생성, 결제 완료, 재고 감소 등 이벤트 큐</td>
</tr>
</tbody></table>
<p>일단 대용량 처리나 로그 분석, 트래킹이나 스트리밍의 기능이  MVP 단계에서 필요하지 않기 때문에 Kafka 자체는 오버엔지니어링이라 보여질 수 있다.</p>
<ul>
<li>RabbitMQ → 주문 생성/취소 이벤트, 재고 업데이트 등의 메시징 용도</li>
<li>Kafka → 실시간 로그 분석 또는 주문 상태 스트리밍이 필요하다면 고려</li>
</ul>
<p>따라서 <strong>MVP에서는 RabbitMQ 먼저 적용 후, 필요 시 Kafka 확장</strong></p>
<h2 id="docker--kubernetes">Docker &amp; Kubernetes</h2>
<p>MSA 구조는 각 서비스가 독립적으로 배포되어야 하는 구조이기 때문에
<strong>환경 일관성을 유지하고, 운영 효율성을 높이기 위해 컨테이너 기반 배포 필요하다.</strong></p>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>Docker Compose</th>
<th>Kubernetes (K8s)</th>
</tr>
</thead>
<tbody><tr>
<td>사용 목적</td>
<td>로컬 개발, 작은 규모의 서비스 배포</td>
<td>대규모 서비스 운영 및 오케스트레이션</td>
</tr>
<tr>
<td>오토스케일링</td>
<td>X (수동 확장 필요)</td>
<td>O (자동 확장 가능)</td>
</tr>
<tr>
<td>장애 복구</td>
<td>X (컨테이너 죽으면 직접 재시작)</td>
<td>O (자동 복구)</td>
</tr>
<tr>
<td>로드 밸런싱</td>
<td>X (수동 설정 필요)</td>
<td>O (내장 로드 밸런서 제공)</td>
</tr>
<tr>
<td>운영 복잡도</td>
<td>쉬움</td>
<td>어렵지만 강력함</td>
</tr>
</tbody></table>
<p><strong>Docker Compose로 MVP 배포 후 Kubernetes 확장은 나중에 고려</strong></p>
<h2 id="api-gateway-필요-여부">API Gateway 필요 여부</h2>
<p>MSA에서는 여러 개의 서비스가 각각 API를 노출하기 때문에 API Gateway가 있으면 인증, 로드 밸런싱, CORS, API 버전 관리 등을 중앙에서 처리 가능하다.</p>
<p>또한 API Gateway 없이 클라이언트가 모든 서비스에 직접 요청하면 관리가 복잡해지고, 보안 리스크 증가할 수 있다.</p>
<h3 id="그럼-어떤-api-gateway-옵션이-존재할까">그럼 어떤 API Gateway 옵션이 존재할까</h3>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>Spring Cloud Gateway</th>
<th>Kong API Gateway</th>
</tr>
</thead>
<tbody><tr>
<td>특징</td>
<td>Spring 기반, 마이크로서비스 친화적</td>
<td>Nginx 기반, 성능 최적화</td>
</tr>
<tr>
<td>확장성</td>
<td>Spring Boot 프로젝트와 자연스럽게 통합</td>
<td>플러그인으로 기능 확장 가능</td>
</tr>
<tr>
<td>OAuth/JWT 지원</td>
<td>O</td>
<td>O</td>
</tr>
<tr>
<td>로드 밸런싱</td>
<td>O</td>
<td>O</td>
</tr>
<tr>
<td>운영 복잡도</td>
<td>Spring 경험이 있으면 쉬움</td>
<td>설정이 복잡할 수 있음</td>
</tr>
</tbody></table>
<p>다행?이도 Spring Cloud Gateway라는게 존재하고 Spring Boot 환경과 자연스럽게 통합이 가능한 API Gateway가 존재한다.</p>
<p>다만 트래픽이 많아질수록 Kong이 고려되는 경우도 있다고 한다.</p>
<p><strong>MVP 단계에서는 Spring Cloud Gateway 사용 후, 트래픽 증가 시 Kong으로 확장 고려</strong></p>
<h2 id="cicd">CI/CD</h2>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>GitHub Actions</th>
<th>Jenkins</th>
</tr>
</thead>
<tbody><tr>
<td>설치 필요 여부</td>
<td>X (클라우드 제공)</td>
<td>O (서버 직접 운영 필요)</td>
</tr>
<tr>
<td>확장성</td>
<td>적은 설정으로 사용 가능</td>
<td>복잡하지만 강력한 기능 제공</td>
</tr>
<tr>
<td>커뮤니티 지원</td>
<td>GitHub과 통합, 빠른 업데이트</td>
<td>기업에서 많이 사용하지만 유지보수 필요</td>
</tr>
<tr>
<td>비용</td>
<td>무료 (기본 사용량)</td>
<td>인프라 유지 비용 발생</td>
</tr>
</tbody></table>
<p>Jenkins는 기업에서 많이 사용하는 엔터프라이즈급 CI/CD를 구축할 때 필요하다.</p>
<p>MVP 단계에서는 GitHub Actions을 당연히 사용해보겠지만
고도화 단계에서도 Jenkins는 고려해봐야 할 것 같다.</p>
<h1 id="너무-mvp만-생각하는거-아닌가">너무 MVP만 생각하는거 아닌가..?</h1>
<p>위에서 정리한 내용을 보면 너무 MVP위주로 상대적으로 규모가 작은 스택만을 선별했다고 볼 수 있는데,
개발을 하면서 적절한 기술 스택을 선정하는 것 역시 중요하다고 생각한다.</p>
<p>뿐만 아니라 내가 여기서 사용해본 기술이 몇 개 되지 않는 점 역시 존재하기 때문에
MVP 에서 실제 서비스가 가능한 프로젝트로 바꾼다 하여도 결국에는 다 경험을 해봐야 하는 스택들이다.</p>
<p>이렇게 기술 스택을 고민하면 캠프를 진행할때 이따금씩 튜터님들이 말씀해주셨던 내용이 생각이 난다.<strong>기술 스택을 정해서 하나만 확 파보는 것이 취업에 더 도움이 될 수 있다는 내용이다.</strong></p>
<p>물론 최근 공고를 봐도 해당 기술스택을 해본게 아닌 경험이 많은 사람을 찾는 내용도 많지만 다양한 기술을 사용하는 공고도 많이 보인다.</p>
<p>MVP를 만들고 고도화 시키는게 조금 번거롭다고 생각되기도 하고, MVP만 만들고 제풀에 꺾일 수 있겠지만 그래도 나는 가능하면 많은 스택을 경험해보는 것도 중요하다고 생각된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MSA Shopping Mall - 2(API 명세)]]></title>
            <link>https://velog.io/@wongi-kim/MSA-Shopping-Mall-2API-%EB%AA%85%EC%84%B8</link>
            <guid>https://velog.io/@wongi-kim/MSA-Shopping-Mall-2API-%EB%AA%85%EC%84%B8</guid>
            <pubDate>Mon, 10 Mar 2025 12:11:27 GMT</pubDate>
            <description><![CDATA[<p>저번 포스팅에 이어 2번째 포스팅은 API 명세 부분이다.</p>
<p>일단 MVP의 목표로 다음과 같이 선정했었는데</p>
<blockquote>
</blockquote>
<p>User Service → 회원가입 &amp; 로그인 (JWT)
Product Service → 상품 목록 조회
Cart Service → 장바구니 추가
Order Service → 주문 생성</p>
<p>아무래도 고도화 할 때 OAuth나 MFA 같은 기능을 추가하기 위해서는 
인증 서비스를 분리하는 것이 더 효과적이면서 보안적인 향상이 이루어 질 것이란 판단이 있기 때문에</p>
<blockquote>
</blockquote>
<p>User Service → 회원가입 
Auth Server → 로그인 (JWT)
Product Service → 상품 목록 조회
Cart Service → 장바구니 추가
Order Service → 주문 생성</p>
<p>정도로 추가될 것 같다.</p>
<p>전체 서비스의 API 명세는 다음과 같이 나온다.
<img src="https://velog.velcdn.com/images/wongi-kim/post/956f6b95-7e35-4e88-a36b-94d72f5a6d9a/image.png" alt=""></p>
<p>각 명세의 사용법과 Reponse는 링크에서 확인할 수 있다.</p>
<blockquote>
</blockquote>
<p>API 명세 : <a href="https://ten-tornado-b09.notion.site/MVP-API-1b06d194f5f980f881a1f8a48cbe963b?pvs=4">https://ten-tornado-b09.notion.site/MVP-API-1b06d194f5f980f881a1f8a48cbe963b?pvs=4</a></p>
<p>ERD는 개인적으로 작성해서 문서화 했기 때문에 따로 포스팅하지 않도록 하겠으며,
다음 포스팅은 <strong>각 서비스의 초기 설정과 API Gateway에 대해 적어 보도록 하겠다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MSA Shopping Mall - 1(Project Overview)]]></title>
            <link>https://velog.io/@wongi-kim/MSA-Shopping-Mall-1Project-Overview</link>
            <guid>https://velog.io/@wongi-kim/MSA-Shopping-Mall-1Project-Overview</guid>
            <pubDate>Sat, 08 Mar 2025 07:17:52 GMT</pubDate>
            <description><![CDATA[<p>일단 Hot-item-collector 를 리팩토링한다고 하긴 했는데
사실 아예 새로운 서비스가 될 수도 있으며 말그대로 리팩토링하는 과정이 담길 수 있다.</p>
<p>그리고 서비스 런칭 수준으로 프로젝트를 끌어올렸을 때 기준을 잡는다면 소요 기간은 아마 30-50일 사이로 잡히지 않을까 싶다.</p>
<blockquote>
<p>짤막한 일정 선정 이유</p>
</blockquote>
<ul>
<li>0일 ~ 2주 : MVP</li>
<li>3주 ~ : 고도화 및 배포</li>
</ul>
<p>일단 이번 포스팅에서는 핵심 기능 정의와 기술 스택 선별 그리고 해당 기술을 선택한 이유까지만 적어 보도록 하겠다.</p>
<h1 id="1차-목표---mvp">1차 목표 - MVP</h1>
<p>MVP에는 다음과 같은 기능을 담고자 한다.</p>
<ul>
<li>User Service → 회원가입 &amp; 로그인 (JWT)</li>
<li>Product Service → 상품 목록 조회</li>
<li>Cart Service → 장바구니 추가</li>
<li>Order Service → 주문 생성</li>
</ul>
<p>일단 MSA를 경험하는게 먼저일 것 같다는 생각이 들어서 MVP를 위의 4가지 기능으로 선정했고, MVP외의 기능은 추후에 확장하며 진행하겠다.</p>
<h1 id="핵심-기능-정의">핵심 기능 정의</h1>
<h2 id="사용자-관리user-service">사용자 관리(User Service)</h2>
<p>회원 가입, 로그인, 비밀번호 변경
JWT 기반 인증 및 OAuth2 소셜 로그인 (카카오, 네이버 등)</p>
<h2 id="상품-관리product-service">상품 관리(Product Service)</h2>
<p>상품 목록 조회, 검색
카테고리 및 브랜드별 필터링
상품 상세 페이지</p>
<h2 id="장바구니cart-service">장바구니(Cart Service)</h2>
<p>사용자가 상품을 장바구니에 추가/삭제
수량 변경</p>
<h2 id="주문-및-결제order--payment-service">주문 및 결제(Order &amp; Payment Service)</h2>
<p>주문 생성 (배송 정보 입력)
결제 처리 (카드, 가상계좌, 네이버페이 등)
주문 상태 변경 (결제 완료 → 배송 중 → 배송 완료)</p>
<h2 id="재고-관리inventory-service">재고 관리(Inventory Service)</h2>
<p>주문 시 재고 감소
재고 부족 알림</p>
<h2 id="리뷰-및-평점review-service">리뷰 및 평점(Review Service)</h2>
<p>상품에 대한 리뷰 작성
별점 평가
추천 및 검색(Search &amp; Recommendation Service)</p>
<p>인기 상품 추천
Elasticsearch를 활용한 검색 최적화</p>
<h2 id="고객-서비스cs-service">고객 서비스(CS Service)</h2>
<p>문의하기, 1:1 채팅 상담
환불/반품 처리</p>
<h2 id="이벤트-및-쿠폰-관리coupon-service">이벤트 및 쿠폰 관리(Coupon Service)</h2>
<p>프로모션, 쿠폰 할인 적용
특정 유저 대상 이벤트</p>
<h2 id="배송-관리delivery-service">배송 관리(Delivery Service)</h2>
<p>배송 추적
배송 상태 업데이트</p>
<hr>
<h1 id="기술-스택">기술 스택</h1>
<p>일단 MSA에 초점을 맞춘 프로젝트기 때문에 프론트는 추후에 구현이 가능하거나 러닝커브를 고려한 후 선별을 하도록 하겠다.</p>
<p>기술 스택은 다음과 같이 정했다.</p>
<blockquote>
</blockquote>
<p>Backend : Java(Spring Boot 3.x)</p>
<blockquote>
<p>DBMS</p>
</blockquote>
<p>1) MySQL : 정형 데이터 관리
2) MongoDB : 비정형 데이터 관리 (로그, 채팅 메시지, 리뷰 등)</p>
<blockquote>
<p>통신 방식 
서비스 간 RESTful API
RabbitMQ(이벤트 기반 처리)</p>
</blockquote>
<h2 id="rabbitmq-vs-kafka">RabbitMQ vs Kafka</h2>
<blockquote>
</blockquote>
<p><a href="https://yay-dev.tistory.com/157">https://yay-dev.tistory.com/157</a></p>
<p>왜 RabbitMQ를 쓰냐면...</p>
<p>사실 MSA구조의 서비스를 개발 심지어 초기에는 MVP를 목표로하기 때문에 대용량 처리나 매우 복잡한 아키텍쳐를 가질 필요가 없다는 판단이기 때문이다.</p>
<p>또한 추후에 CS관련 기술을 고려했을 때 아마도 1:1 채팅이 될 예정인데
지연 시간이 짧은 RabbitMQ가 메시지 처리 브로커로 조금 더 알맞지 않을까 싶기 때문이다.</p>
<p><del>(맘 같아선 서비스 간 통신은 Kafka, 채팅 서비스에서는 RabbitMQ였지만  작은 규모의 서비스에서 기술 다 때려넣는게 좋지 않은건 아니까..)</del></p>
<p>즉, 정리해보면 다음과 같이 정리되겠다.</p>
<blockquote>
</blockquote>
<ul>
<li>RabbitMQ: 지연 시간이 짧고, CS(1:1 채팅) 같은 빠른 응답이 필요한 서비스에 적합  </li>
<li>Kafka: 대용량 스트리밍 데이터 처리에 강점이 있지만, 초기 MSA에서는 과도한 복잡성 초래 가능  </li>
</ul>
<p><strong>👉 결론: RabbitMQ를 사용하여 이벤트 기반 처리를 하되, 추후 Kafka 도입을 고려</strong>  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[벌써 3월]]></title>
            <link>https://velog.io/@wongi-kim/%EB%B2%8C%EC%8D%A8-3%EC%9B%94</link>
            <guid>https://velog.io/@wongi-kim/%EB%B2%8C%EC%8D%A8-3%EC%9B%94</guid>
            <pubDate>Sat, 08 Mar 2025 06:18:02 GMT</pubDate>
            <description><![CDATA[<p>벌써 3월인데 아직도 취업이.. ㅠㅠ</p>
<p>일단 지금까지 코테나 기존에 작성했던 면접 질문들만 보고 있었는데</p>
<p>실전으로 코드를 작성할 기회 자체는 조금 없었던 것 같기 때문에 새로운 프로젝트를 진행해볼 생각이다.</p>
<p>얼마전 대학 동기와 얘기했을 때 친구 기준 사이드 프로젝트는 2가지 정도로 나뉜다는 것 같다고 얘기해줬다.</p>
<blockquote>
</blockquote>
<ol>
<li>내가 알고있고 사용해본 기술을 다 때려넣어서 만든 프로젝트</li>
<li>실제 서비스 런칭을 목표로 진행하는 프로젝트</li>
</ol>
<p>뭐 매번 그랬..? 공모전은 예외로 치면 항상 1번이 나에게는 주류였고, 이번에 생각난 목표를 도전해볼 겸 이번에도 1번의 형태로 진행될 것 같다.</p>
<p>일단 이번 목표는 예전에 진행했던 Hot-item-collector 서비스를 리팩토링 하는 겸 MSA 형태로 구현해볼 예정이다.</p>
]]></description>
        </item>
    </channel>
</rss>