<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>kanguk_o.log</title>
        <link>https://velog.io/</link>
        <description>꾸준히 성장하는 개발자</description>
        <lastBuildDate>Sun, 27 Jul 2025 12:34:47 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>kanguk_o.log</title>
            <url>https://velog.velcdn.com/images/kanguk_o/profile/f03020b2-de08-458a-8d25-b06b7814dac2/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. kanguk_o.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kanguk_o" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Effective Java 톺아보기 01]]></title>
            <link>https://velog.io/@kanguk_o/Effective-Java-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0-01</link>
            <guid>https://velog.io/@kanguk_o/Effective-Java-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0-01</guid>
            <pubDate>Sun, 27 Jul 2025 12:34:47 GMT</pubDate>
            <description><![CDATA[<h3 id="1-생성자-대신-정적-팩토리-메소드를-고려하라">1. 생성자 대신 정적 팩토리 메소드를 고려하라</h3>
<p>클래스의 인스턴스를 얻는 가장 기본적이고 쉬운 방법은 Pulic 생성자다.</p>
<pre><code class="language-java">
class Study {
    private Long id;
    private String name;
    private int limit;
    private StudyStatus studyStatus;

    public Study() {}
    public Study(String name, int limit){
        this.name = name;
        this.limit = limit;
    }
}

→ 정통적인 Public 생성자 사용법</code></pre>
<p>그런데, 이런 public 생성자만 사용해서 인스턴스를 생성할 때 불편함을 느낀적은 없을까?</p>
<p>위 예제에서는 Study의 이름과 수용인원만 있기에 생성자도 두 개의 파라미터만 넣으면 되고 두 파라미터는 타입도 다르기에 알아보기도 어렵지 않다.<br>하지만, 필드값이 3~4개가 넘어가고 변수 타입이 동일한것들도 많아진다면 어떨까? 
해당 클래스의 인스턴스를 생성하는것의 난이도는 확 올라간다. </p>
<p>그렇기에 우리는 정적 팩토리 메소드(static factory method)를 만들어 사용할 수 있다. 
(디자인 패턴의 팩토리 메소드 패턴(Factory Method)와는 다르다.)</p>
<p>클래스의 인스턴스를 반환하는 단순한 정적 메서드인 이 정적 팩토리 메소드는 이미 많이 쓰이고 있는데, 대표적으로 기본형 변수의 래퍼클래스(Wrapper Class)을 보면 알 수 있다.
다음은 boolean 타입 의 래퍼 클래스인 Boolean의 valueOf 정적 팩토리 메서드이다. </p>
<pre><code class="language-java">public static Boolean valueOf(boolean b){
    return b ? Boolean.TRUE : Boolean.FALSE;
}

→ valueOf 메서드는 boolean 기본형 변수를 Boolean 객체 참조로 변환해서 반환한다. </code></pre>
<h3 id="장점">장점</h3>
<h4 id="첫번째-이름을-가질수-있다">첫번째 이름을 가질수 있다.</h4>
<p>변수가 많은 클래스의 public 생성자의 가장 큰 문제중 하나는 내가 입력할 파라미터를 하나하나
구분하는게 쉽지 않을 뿐더러 파라미터와 생성자 만으로는 내가 반환받을 객체의 특성을 한번에 이해하기 어렵다.</p>
<p>예를들어 Study 클래스의 생성자 Study(String, int, StudyStatus) 를 보고 단 번에 어떤 특성의 Study 인스턴스인지 알 수 있을까? </p>
<p>반면 정적 팩토리 메소드를 사용하면 Study.newStudy를 사용한다면 과연 전자와 후자중 어느것이 &#39;새로운 스터디&#39; 를 생성한다는 점을 알기 쉬울까? </p>
<p>내가 전달 할 파라미터의 타입, 갯수에 따라서 public 생성자를 여러개 만들어서 다양한 매개변수에 대응할 수 있다. 하지만, 모두 결국 public 생성자이고 매개변수의 종류나 타입만 달라지기에 가독성이 떨어지는건 동일하다. </p>
<p>아니 오히려 생성자의 종류가 많아짐에따라 임의의 생황에서 어느 생성자를 호출해야 할지 혼동하기 쉽다. </p>
<p>반면, 이름을 가질 수 있는 정적 팩토리 메서드에는 이런 문제가 발생하지 않는다. 한 클래스에서 시그니처는 같지만 기대되는 특성이 다른 인스턴스가 필요하다면 생성자를 정적 팩토리 메소드로 바꾸고 네이밍을 통해 그러한 특성의 차이를 드러내는게 좋다. </p>
<pre><code class="language-java">public static Study newStudy(String name, int limit) {
    return new Study(name, limit, DRAFT);
}

public static Study endedStudy(String name, int limit) {
    return new Study(name, limit, ENDED);
}


→ 동일한 시그니처이지만 메소드명을 통해 반환될 스터디의 특성을 추측할 수 있다.</code></pre>
<h3 id="2-호출-될-때-마다-인스턴스를-새로-생성하지-않아도-된다">2. 호출 될 때 마다 인스턴스를 새로 생성하지 않아도 된다.</h3>
<p>정적 팩토리 메소드를 사용하면 매번 인스턴스를 새로 생성하지 않고 기존에 만들어두거나
생성한 인스턴스를 캐싱해서 재활용 함으로써 불필요한 객체 생성을 피할수 있다.</p>
<p>위에서 설명한 Boolean.valueOf(boolean)이 이런 장점을 사용한 대표적인 예이다. </p>
<p>특히나, 규모가 커서 생성 비용이 큰 객체의 경우 요청될 때마다 생성하게되면 비용소모가 상당히 커짐으로써 성능이 떨어질 수밖에 없는데, 정적 팩토리 메소드를 사용해 이런 성능저하를 막을 수 있다. </p>
<p>디자인 패턴의 플라이웨이트 패턴(Flyweight pattern)도 이와 비슷하다고 할 수 있다.
이처럼 같은 요청에는 같은 인스턴스를 반환하는 방식으로 인스턴스의 라이프 사이클을 통제할 수 있는데, </p>
<p>이처럼 같은 요청에는 같은 인스턴스를 반환하는 방식으로 인스턴스의 라이프 사이클을 통제할 수 있는데, 이러한 클래스를 인스턴스 통제(instance-controlled) 클래스라 부른다. </p>
<p>이렇게 인스턴스를 통제하면 싱글톤 패턴을 적용할수도 있고, <strong>인스턴스화 불가(noninstantiable)</strong>로 만들수도 있다. </p>
<p>이와같이 인스턴스를 통제해서 동일한 값에 동일한 인스턴스(a==b &amp;&amp; a.equals(b))는 <strong>플라이웨이트 패턴의 핵심</strong>이고 열거형(enum)은 인스턴스가 하나만 만들어짐을 보장한다. </p>
<p>간단한 플라이웨이트 패턴을 사용해 정적 팩토리 메소드를 사용해보자. </p>
<pre><code class="language-java">package me.catsbi.effectivejavastudy.item1;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;


public class StudyFactory {
    private static final Map&lt;String, Study&gt; store = new HashMap&lt;&gt;();
    private static StudyFactory instance = new StudyFactory();

    private StudyFactory() { }

    public static StudyFactory getInstance() {
        if (Objects.isNull(instance)) {
            instance = new StudyFactory();
        }
        return instance;
    }

    public synchronized Study getStudy(String name, int limit) {
        Study study = store.get(name);
        if (Objects.isNull(study)) {
            study = Study.newStudy(name, limit);
            store.put(name, study);
        }
        return study;
    }
}</code></pre>
<h3 id="3-반환-타입의-하위-타입-객체를-반환할-수-있는-능력이-있다">3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.</h3>
<p>Arrays 유틸 클래스에서 asList를 통해 List Collection을 만들어 사용해본적이 있다면, 이 세 번째 장점을 이해할 수 있다. 반환객체의 클래스를 자유롭게 선택할 수 있다는 유연성은 내가 구현체를 공개하지 않고 구현체를 반환할 수 있기에 API를 작게 유지할 수 있다. </p>
<p>이 장점이 인터페이스 기반 프레임워크의 핵심 기술이기도 하다. 
예를들어 자바에서 제공하는 유틸 클래스중 java.util.Collections에서는 수정 불가, 동기화 기능 등이 들어간 컬렉션 구현체를 제공해주는데 모두 정적 팩토리 메서드를 통해 얻도록 한다. </p>
<p>이때, 이 컬렉션 프레임워크는 구현체를 따로 공개하지 않기에 API의 외견에서 구현체가지 신경쓸 필요가 없어 컴팩트한 개발이 가능해진다. 그렇기에 우리는 인터페이스에 정의된 메서드만 인지하면 되기에 사용법 학습에 대한 비용도 최소한으로 사용이 가능해진다. </p>
<p>다음은 java.util.Arrays 유틸클래스의 정적 팩토리 메서드인 asList()이다. </p>
<pre><code class="language-java">public static &lt;T&gt; List&lt;T&gt; asList(T... a) {
    return new ArrayList&lt;&gt;(a);
}

→ List의 하위 구현체인 ArrayList로 값을 래핑해 반환한다. 하지만 사용자는 이러한 구현체까지 알 필요가 없다.</code></pre>
<p>Java 8 부터는 인터페이스에서도 static 키워드를 통해 정적 메서드를 가질 수 있다. 그렇기에 public 정적 멤버들도 공통된 부분들은 인터페이스에 위치시켜도 상관이 없어졌다. 다만 private 정적 메서드는 아직 java 9 이상에서만 허락되기에 정적 필드와 정적 멤버 클래스는 여전히 public이어야 한다. </p>
<h3 id="4-입력-매개변수에-따라-다른-클래스의-객체를-반환할-수-있다">4. 입력 매개변수에 따라 다른 클래스의 객체를 반환할 수 있다</h3>
<p>단순히 하위타입을 반환한다는 점을 넘어 파라미터의 상태(값, 크기등) 에 따라 다른 하위타입을 반환 할 수도 있다.<br>예를 들어, 강의를 위해 강의실 객체 인스턴스를 만들어야 하는 상황에서 항상 같은 인원을 수용하는 동일한 강의실이 아니라 인원수에 따라 다른 타입의 강의실(small, medium, big)을 반환받고 싶을때, 정적 팩토리 메소드를 사용하면 호출 할때 전달하는 수강인원 파라미터로 매번 적절한 구현체를 생성해 반환해줄 수 있다.</p>
<pre><code class="language-java">또한, 호출하는 입장에서는 그런 내부 구현체까진 알 필요도 없기에 의존관계가 생기지 않는다. 

import java.rmi.NoSuchObjectException;
import java.util.Objects;

public class ClassRoomFactory {
    public static final String NOT_FOUND_CLASS_ROOM_FROM_LIMIT_COUNT = &quot;Not Found ClassRoom from limitCount:&quot;;
    private static ClassRoomFactory instance = new ClassRoomFactory();

    private ClassRoomFactory() {
    }

    public static ClassRoomFactory getInstance() {
        if (Objects.isNull(instance)) {
            instance = new ClassRoomFactory();
        }
        return instance;
    }

    public static ClassRoom getClassRoom(int limitCount) throws NoSuchObjectException {
        if (SmallClassRoom.supported(limitCount)) {
            return new SmallClassRoom();
        }

        if (MediumClassRoom.supported(limitCount)) {
            return new MediumClassRoom();
        }

        if (BigClassRoom.supported(limitCount)) {
            return new BigClassRoom();
        }
        throw new NoSuchObjectException(NOT_FOUND_CLASS_ROOM_FROM_LIMIT_COUNT + limitCount);
    }


}
→ 수강인원에 따라 Small, Medium, Big 강의실중 적절한 강의실 인스턴스가 생성되어 반환된다. 
</code></pre>
<h3 id="5정적-팩토리-메소드를-작성하는-시점에는-반환할-객체의-클래스가-없어도-된다">5.정적 팩토리 메소드를 작성하는 시점에는 반환할 객체의 클래스가 없어도 된다.</h3>
<p>위에서 작성한 코드들에서는 다 이미 구현되어있는 구현체를 기준으로 유연함을 제공해줬다. 
하지만, 이를 넘어서 정적 팩토리 메서드를 작성하는 시점에 구현되있지 않은 객체의 클래스를 반환할수도 있다. </p>
<p>이 부분이 서비스 제공자 프레임워크(Service Provider Framework)의 근간이 되는 개념으로 제공자(provider)가 서비스의 구현체이다.  </p>
<p>그리고 이 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제해서 클라이언트를 구현체로부터 분리해준다. (DIP) </p>
<p>서비스 제공자 프레임워크는 다음 3개의 핵심 컴포넌트로 이뤄진다.</p>
<ul>
<li>서비스 인터페이스(service interface): 구현체의 동작을 정의한다.</li>
<li>제공자 등록 API(provider registration API): 제공자가 구현체를 등록 할 때 사용하는 제공자 등록 API</li>
<li>서비스 접근 API(service access API): 클라이언트가 인스턴스를 얻을 때 사용하는 서비스 접근 API</li>
</ul>
<p>클라이언트는 서비스 접근 API를 이용해서 원하는 구현체를 가져올 수 있는데, 조건을 명시하지 않을 경우 기본 구현체 혹은 지원하는 구현체들을 돌아가며 반환한다. </p>
<p>이러한 서비스 접근 API가 서비스 제공자 프레임워크의 근간인 유여한 정적 팩토리 메소드의 실체다. </p>
<p>그 밖에 서비스 제공자 인터페이스(Service Provider Interface)라는 컴포넌트가 쓰이기도하는데 이는 서비스 인터페이스의 인스턴스를 생성하는 팩토리 객체를 설명해준다. 이러한 서비스 제공자 인터페이스가 없다면 
리플렉션을 이용해서 구현체를 인스턴스화 해준다. </p>
<p>구현 예 : JDBC
**  ⇒ Connection: 서비스 인터페이스 역할
  ⇒ DriverManager.registerDriver: 제공자 등록 API 역할
  ⇒ DriverManager.getConnection : 서비스 접근 API 역할
  ⇒ Driver: 서비스 제공자 인터페이스 역할**</p>
<p><strong>단점</strong>
첫 번째, 상속을 할 땐 public or protected 생성자가 필요하기에 정적 팩토리 메서드만 사용하면 하위 클래스를 만들 수 없다. </p>
<p>컬렉션 프레임워크의 유틸리티 구현 클래스들을 상속할 수 없다.</p>
<p>(interface에 정의한 정적 메소드를 상속해서 오버라이딩 할 수 없다는 의미)
이러한 제약은 상속보다는 컴포지션(위임)을 유도하고 불변 타입으로 만들기7 위해서 이 제약을 지켜야한다는 점에서 오히려 장점이 될 수도 있다. </p>
<p>두 번째, 정적 팩토리 메소드는 프로그래머가 찾기 힘들다.
public 생성자는 API 설명에도 나와있기 때문에 사용자가 쓰기가 명확하지만, 정적 팩토리 메서드는 인스턴스화 하기위한 방법을 직접 찾아야한다. 
그러기위해 API 문서 작성을 잘 작성해놓고, 정적 팩토리 메소드명을 관례를 최대한 따라서 짓는 방식으로 사용자가 찾기 쉽도록 해야 한다.  다음은 정적 패토리 메소드에서 사용하는 명명 방식이다. </p>
<p>from: 매개변수를 하나를 받아 해당 타입의 인스턴스를 반환하는 형변환 메서드</p>
<ul>
<li>⇒ Date d = Date.from(instant);
of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드</li>
</ul>
<ul>
<li><p>⇒ Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
valueOf: from과 of의 더 자세한 버전</p>
</li>
<li><p>⇒ BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
instance or getInstance: (매개변수가 있다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.</p>
</li>
<li><p>⇒ StackWalker luke = StackWalker.getInstance(options);
create or newInstance: instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다. </p>
</li>
<li><p>⇒ Object newArray = Array.newInstance(classObject, arrayLen);
getType: getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 쓴다.</p>
</li>
<li><p>⇒ FileStore fs = Files.getFileStore(path) 
newType: newInstance와 같으나 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 쓴다.</p>
</li>
<li><p>⇒ BufferedReader br = Files.newBufferedReader(path)
type: getType과 newType의 간결한 버전
⇒ List<Complaint> litany = Collections.list(legacyLitany);</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin 톺아보기 2]]></title>
            <link>https://velog.io/@kanguk_o/Kotlin-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0-2</link>
            <guid>https://velog.io/@kanguk_o/Kotlin-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0-2</guid>
            <pubDate>Sun, 27 Jul 2025 08:45:04 GMT</pubDate>
            <description><![CDATA[<h2 id="1-컬렉션-api-확장">1. 컬렉션 API 확장</h2>
<p>코틀린은 자체 컬렉션 클래스를 정의하지 않고, 자바 컬렉션을 확장하여 더 풍부한 API를 제공합니다.</p>
<pre><code class="language-kotlin">fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)

    // 자바에서는 복잡한 코드가 필요하지만 코틀린에서는 간단
    val evenNumbers = numbers.filter { it % 2 == 0 }
    val doubled = numbers.map { it * 2 }
    val sum = numbers.sum()

    println(&quot;짝수: $evenNumbers&quot;)        // [2, 4]
    println(&quot;2배: $doubled&quot;)             // [2, 4, 6, 8, 10]
    println(&quot;합계: $sum&quot;)                // 15
}</code></pre>
<h2 id="2-함수-파라미터-디폴트-값과-이름붙인-인자">2. 함수 파라미터 디폴트 값과 이름붙인 인자</h2>
<h3 id="디폴트-파라미터">디폴트 파라미터</h3>
<p>함수 오버로딩 없이도 다양한 방식으로 함수를 호출할 수 있습니다.</p>
<pre><code class="language-kotlin">fun createUser(
    name: String,
    age: Int = 0,
    email: String = &quot;&quot;,
    isActive: Boolean = true
) {
    println(&quot;User: $name, Age: $age, Email: $email, Active: $isActive&quot;)
}

fun main() {
    createUser(&quot;김철수&quot;)                              // 나머지는 디폴트 값
    createUser(&quot;이영희&quot;, 25)                         // age만 지정
    createUser(&quot;박민수&quot;, 30, &quot;park@example.com&quot;)      // email까지 지정
}</code></pre>
<h3 id="이름붙인-인자">이름붙인 인자</h3>
<p>함수 호출 시 가독성을 크게 향상시킵니다.</p>
<pre><code class="language-kotlin">fun main() {
    // 이름붙인 인자로 순서에 상관없이 호출 가능
    createUser(
        name = &quot;최지우&quot;,
        isActive = false,
        age = 28,
        email = &quot;choi@example.com&quot;
    )

    // 일부만 이름을 붙여도 됨
    createUser(&quot;정수민&quot;, email = &quot;jung@example.com&quot;, age = 32)
}</code></pre>
<h2 id="3-최상위-함수와-프로퍼티">3. 최상위 함수와 프로퍼티</h2>
<p>클래스 없이도 함수와 프로퍼티를 직접 선언할 수 있어 코드 구조가 더 유연해집니다.</p>
<pre><code class="language-kotlin">// utils.kt 파일
val APP_VERSION = &quot;1.0.0&quot;
const val MAX_RETRY_COUNT = 3

fun formatDate(timestamp: Long): String {
    return SimpleDateFormat(&quot;yyyy-MM-dd&quot;, Locale.getDefault())
        .format(Date(timestamp))
}

fun isValidEmail(email: String): Boolean {
    return email.contains(&quot;@&quot;) &amp;&amp; email.contains(&quot;.&quot;)
}

// main.kt 파일
fun main() {
    println(&quot;앱 버전: $APP_VERSION&quot;)
    println(&quot;오늘 날짜: ${formatDate(System.currentTimeMillis())}&quot;)
    println(&quot;이메일 유효성: ${isValidEmail(&quot;test@example.com&quot;)}&quot;)
}</code></pre>
<h2 id="4-확장-함수와-프로퍼티">4. 확장 함수와 프로퍼티</h2>
<p>외부 라이브러리 클래스를 포함해 모든 클래스의 API를 소스코드 수정 없이 확장할 수 있습니다.</p>
<pre><code class="language-kotlin">// String 클래스 확장
fun String.isEmailValid(): Boolean {
    return this.contains(&quot;@&quot;) &amp;&amp; this.contains(&quot;.&quot;)
}

fun String.removeWhitespace(): String {
    return this.replace(&quot;\\s&quot;.toRegex(), &quot;&quot;)
}

// Int 클래스 확장
fun Int.isEven(): Boolean = this % 2 == 0

val Int.squared: Int
    get() = this * this

fun main() {
    val email = &quot;user@example.com&quot;
    println(email.isEmailValid())           // true

    val text = &quot;Hello World&quot;
    println(text.removeWhitespace())        // HelloWorld

    val number = 5
    println(number.isEven())                // false
    println(number.squared)                 // 25
}</code></pre>
<h2 id="5-중위-호출-infix-functions">5. 중위 호출 (Infix Functions)</h2>
<p>인자가 하나인 메소드나 확장 함수를 더 깔끔한 구문으로 호출할 수 있습니다.</p>
<pre><code class="language-kotlin">// 중위 함수 정의
infix fun Int.power(exponent: Int): Int {
    return Math.pow(this.toDouble(), exponent.toDouble()).toInt()
}

infix fun String.shouldBe(expected: String): Boolean {
    return this == expected
}

fun main() {
    // 일반 호출
    println(2.power(3))              // 8

    // 중위 호출 - 더 자연스러운 표현
    println(2 power 3)               // 8

    val result = &quot;Hello&quot;
    println(result shouldBe &quot;Hello&quot;) // true

    // 코틀린 표준 라이브러리의 중위 함수들
    val map = mapOf(1 to &quot;one&quot;, 2 to &quot;two&quot;, 3 to &quot;three&quot;)
    println(map)                     // {1=one, 2=two, 3=three}
}</code></pre>
<h2 id="6-문자열-처리-함수">6. 문자열 처리 함수</h2>
<p>코틀린은 정규식과 일반 문자열 처리에 유용한 다양한 함수를 제공합니다.</p>
<pre><code class="language-kotlin">fun main() {
    val text = &quot;kotlin-java-python&quot;

    // 분할
    println(text.split(&quot;-&quot;))                    // [kotlin, java, python]

    // 정규식 사용
    val phoneNumber = &quot;010-1234-5678&quot;
    val digits = phoneNumber.replace(Regex(&quot;[^0-9]&quot;), &quot;&quot;)
    println(digits)                             // 01012345678

    // 문자열 조작
    val email = &quot;  USER@EXAMPLE.COM  &quot;
    println(email.trim().lowercase())           // user@example.com

    // 조건부 처리
    val fileName = &quot;document.pdf&quot;
    if (fileName.endsWith(&quot;.pdf&quot;)) {
        println(&quot;PDF 파일입니다&quot;)
    }

    // 패턴 매칭
    val pattern = Regex(&quot;&quot;&quot;(\d{3})-(\d{4})-(\d{4})&quot;&quot;&quot;)
    val match = pattern.find(&quot;연락처: 010-1234-5678&quot;)
    match?.let {
        println(&quot;전화번호: ${it.value}&quot;)         // 전화번호: 010-1234-5678
        println(&quot;지역번호: ${it.groupValues[1]}&quot;) // 지역번호: 010
    }
}</code></pre>
<h2 id="7-삼중-따옴표-문자열">7. 삼중 따옴표 문자열</h2>
<p>이스케이프가 많이 필요한 문자열을 더 깔끔하게 표현할 수 있습니다.</p>
<pre><code class="language-kotlin">fun main() {
    // 일반 문자열 - 이스케이프 필요
    val jsonString = &quot;{\&quot;name\&quot;: \&quot;John\&quot;, \&quot;age\&quot;: 30, \&quot;city\&quot;: \&quot;Seoul\&quot;}&quot;

    // 삼중 따옴표 - 이스케이프 불필요
    val jsonStringRaw = &quot;&quot;&quot;
        {
            &quot;name&quot;: &quot;John&quot;,
            &quot;age&quot;: 30,
            &quot;city&quot;: &quot;Seoul&quot;
        }
    &quot;&quot;&quot;.trimIndent()

    // 정규식 패턴
    val regexPattern = &quot;&quot;&quot;^\d{3}-\d{4}-\d{4}$&quot;&quot;&quot;

    // 파일 경로 (Windows)
    val windowsPath = &quot;&quot;&quot;C:\Users\Documents\file.txt&quot;&quot;&quot;

    // HTML 템플릿
    val htmlTemplate = &quot;&quot;&quot;
        &lt;!DOCTYPE html&gt;
        &lt;html&gt;
        &lt;head&gt;
            &lt;title&gt;${&quot;코틀린 예제&quot;}&lt;/title&gt;
        &lt;/head&gt;
        &lt;body&gt;
            &lt;h1&gt;환영합니다!&lt;/h1&gt;
            &lt;p&gt;현재 시간: ${System.currentTimeMillis()}&lt;/p&gt;
        &lt;/body&gt;
        &lt;/html&gt;
    &quot;&quot;&quot;.trimIndent()

    println(jsonStringRaw)
    println(htmlTemplate)
}</code></pre>
<h2 id="8-실전-예제-모든-기능-조합">8. 실전 예제: 모든 기능 조합</h2>
<pre><code class="language-kotlin">// 확장 함수와 중위 함수
infix fun String.matches(pattern: String): Boolean = 
    Regex(pattern).matches(this)

fun String.formatPhoneNumber(): String = 
    this.replace(Regex(&quot;[^0-9]&quot;), &quot;&quot;)
        .let { digits -&gt;
            when {
                digits.length == 11 -&gt; &quot;${digits.substring(0,3)}-${digits.substring(3,7)}-${digits.substring(7)}&quot;
                digits.length == 10 -&gt; &quot;${digits.substring(0,3)}-${digits.substring(3,6)}-${digits.substring(6)}&quot;
                else -&gt; this
            }
        }

// 최상위 함수
fun validateUser(
    name: String,
    phone: String = &quot;&quot;,
    email: String = &quot;&quot;,
    printResult: Boolean = true
): Boolean {
    val phonePattern = &quot;&quot;&quot;^010-\d{4}-\d{4}$&quot;&quot;&quot;
    val emailPattern = &quot;&quot;&quot;^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$&quot;&quot;&quot;

    val isValid = name.isNotBlank() &amp;&amp; 
                  (phone.isEmpty() || phone matches phonePattern) &amp;&amp;
                  (email.isEmpty() || email matches emailPattern)

    if (printResult) {
        val result = &quot;&quot;&quot;
            사용자 검증 결과:
            - 이름: $name
            - 전화번호: ${phone.ifEmpty { &quot;미입력&quot; }}
            - 이메일: ${email.ifEmpty { &quot;미입력&quot; }}
            - 유효성: ${if (isValid) &quot;통과&quot; else &quot;실패&quot;}
        &quot;&quot;&quot;.trimIndent()

        println(result)
    }

    return isValid
}

fun main() {
    val phoneNumbers = listOf(&quot;01012345678&quot;, &quot;010-1234-5678&quot;, &quot;0212345678&quot;)

    phoneNumbers
        .map { it.formatPhoneNumber() }
        .forEach { println(&quot;포맷된 번호: $it&quot;) }

    // 이름붙인 인자와 디폴트 파라미터 활용
    validateUser(
        name = &quot;김개발자&quot;,
        phone = &quot;010-1234-5678&quot;,
        email = &quot;dev@example.com&quot;
    )
}</code></pre>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin 톺아보기 ]]></title>
            <link>https://velog.io/@kanguk_o/Kotlin-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@kanguk_o/Kotlin-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 27 Jul 2025 07:31:35 GMT</pubDate>
            <description><![CDATA[<h2 id="1-함수-정의와-변수-선언">1. 함수 정의와 변수 선언</h2>
<h3 id="함수-정의">함수 정의</h3>
<ul>
<li><code>fun</code> 키워드를 사용하여 함수를 정의합니다.</li>
</ul>
<pre><code class="language-kotlin">fun greet(name: String): String {
    return &quot;Hello, $name!&quot;
}

// 표현식 함수 (간단한 경우)
fun add(a: Int, b: Int) = a + b</code></pre>
<h3 id="변수-선언">변수 선언</h3>
<ul>
<li><code>val</code>: 읽기 전용 변수 (불변)</li>
<li><code>var</code>: 변경 가능한 변수 (가변)</li>
</ul>
<pre><code class="language-kotlin">val readOnlyValue = 10        // 읽기 전용, 변경 불가
var mutableValue = 20         // 변경 가능
mutableValue = 30             // OK

// val readOnlyValue = 15     // 컴파일 에러!</code></pre>
<h2 id="2-문자열-템플릿">2. 문자열 템플릿</h2>
<p>문자열 템플릿을 사용하면 문자열 연결 없이도 코드가 간결해집니다.</p>
<ul>
<li>변수 이름 앞에 <code>$</code>를 붙이거나</li>
<li>식을 <code>${}</code>로 둘러싸면 변수나 식의 값을 문자열에 삽입할 수 있습니다.</li>
</ul>
<pre><code class="language-kotlin">val name = &quot;코틀린&quot;
val version = 1.8
val message = &quot;안녕하세요, $name 버전 $version입니다!&quot;

// 복잡한 식의 경우
val price = 1000
val tax = 0.1
val total = &quot;총 가격: ${price * (1 + tax)}원&quot;

println(message)  // 안녕하세요, 코틀린 버전 1.8입니다!
println(total)    // 총 가격: 1100.0원</code></pre>
<h2 id="3-데이터-클래스-값-객체-클래스">3. 데이터 클래스 (값 객체 클래스)</h2>
<p>코틀린에서는 값 객체 클래스를 아주 간결하게 표현할 수 있습니다.</p>
<pre><code class="language-kotlin">// 기본 데이터 클래스
data class Person(val name: String, var age: Int)

// 사용 예시
val person = Person(&quot;홍길동&quot;, 25)
println(person.name)    // 홍길동
person.age = 26         // var이므로 변경 가능

// 자동으로 제공되는 메소드들
val person2 = Person(&quot;홍길동&quot;, 26)
println(person == person2)    // true (equals 자동 구현)
println(person)               // Person(name=홍길동, age=26) (toString 자동 구현)</code></pre>
<h2 id="4-if문-식으로서의-if">4. if문 (식으로서의 if)</h2>
<p>다른 언어와 달리 코틀린의 if문은 식(expression)이며 값을 반환할 수 있습니다.</p>
<pre><code class="language-kotlin">val max = if (a &gt; b) a else b

// 복잡한 조건식
val result = if (score &gt;= 90) {
    &quot;A학점&quot;
} else if (score &gt;= 80) {
    &quot;B학점&quot;
} else {
    &quot;C학점&quot;
}

// 삼항 연산자 대신 사용
val status = if (isOnline) &quot;온라인&quot; else &quot;오프라인&quot;</code></pre>
<h2 id="5-when문-강력한-switch">5. when문 (강력한 switch)</h2>
<p>코틀린의 <code>when</code>은 자바의 <code>switch</code>와 비슷하지만 훨씬 더 강력합니다.</p>
<pre><code class="language-kotlin">when (grade) {
    &#39;A&#39; -&gt; println(&quot;우수&quot;)
    &#39;B&#39;, &#39;C&#39; -&gt; println(&quot;보통&quot;)  // 여러 값 동시 처리
    &#39;D&#39; -&gt; println(&quot;미흡&quot;)
    else -&gt; println(&quot;재시험&quot;)
}

// 식으로 사용
val description = when (grade) {
    &#39;A&#39; -&gt; &quot;우수&quot;
    &#39;B&#39;, &#39;C&#39; -&gt; &quot;보통&quot;
    else -&gt; &quot;기타&quot;
}

// 범위와 함께 사용
when (score) {
    in 90..100 -&gt; &quot;A&quot;
    in 80..89 -&gt; &quot;B&quot;
    in 70..79 -&gt; &quot;C&quot;
    else -&gt; &quot;F&quot;
}</code></pre>
<h2 id="6-스마트-캐스팅-smart-casting">6. 스마트 캐스팅 (Smart Casting)</h2>
<p>어떤 변수의 타입을 검증하고 나면 굳이 명시적으로 캐스팅하지 않아도 됩니다.
컴파일러가 자동으로 타입 변환을 해줍니다.</p>
<pre><code class="language-kotlin">fun printLength(obj: Any) {
    if (obj is String) {
        // obj가 자동으로 String으로 캐스팅됨
        println(&quot;문자열 길이: ${obj.length}&quot;)
    }
}

when (x) {
    is Int -&gt; println(&quot;정수: ${x + 1}&quot;)      // x는 자동으로 Int
    is String -&gt; println(&quot;문자열: ${x.uppercase()}&quot;)  // x는 자동으로 String
    is Boolean -&gt; println(&quot;불린: ${!x}&quot;)     // x는 자동으로 Boolean
}</code></pre>
<h2 id="7-반복문-for-while-do-while">7. 반복문 (for, while, do-while)</h2>
<h3 id="for문">for문</h3>
<p>자바의 for문보다 코틀린의 for문이 더 편리합니다.</p>
<pre><code class="language-kotlin">// 범위 반복
for (i in 1..5) {
    println(i)  // 1, 2, 3, 4, 5
}

// 컬렉션 반복
val fruits = listOf(&quot;사과&quot;, &quot;바나나&quot;, &quot;오렌지&quot;)
for (fruit in fruits) {
    println(fruit)
}

// 인덱스와 함께 반복
for ((index, fruit) in fruits.withIndex()) {
    println(&quot;$index: $fruit&quot;)
}

// Map 반복
val map = mapOf(&quot;A&quot; to 1, &quot;B&quot; to 2, &quot;C&quot; to 3)
for ((key, value) in map) {
    println(&quot;$key = $value&quot;)
}</code></pre>
<h3 id="while과-do-while">while과 do-while</h3>
<pre><code class="language-kotlin">var count = 0
while (count &lt; 5) {
    println(count)
    count++
}

do {
    println(&quot;최소 한 번은 실행&quot;)
} while (false)</code></pre>
<h2 id="8-범위-range">8. 범위 (Range)</h2>
<p><code>1..5</code>와 같은 식으로 범위를 만들 수 있습니다.</p>
<pre><code class="language-kotlin">// 기본 범위
val range1 = 1..10          // 1부터 10까지 (10 포함)
val range2 = 1 until 10     // 1부터 9까지 (10 제외)
val range3 = 10 downTo 1    // 10부터 1까지 (역순)
val range4 = 1..10 step 2   // 1, 3, 5, 7, 9 (2씩 증가)

// 포함 여부 검사
val number = 5
if (number in 1..10) {
    println(&quot;1부터 10 사이의 숫자입니다&quot;)
}

if (number !in 11..20) {
    println(&quot;11부터 20 사이의 숫자가 아닙니다&quot;)
}

// 문자 범위
for (c in &#39;a&#39;..&#39;z&#39;) {
    print(c)  // abcdefghijklmnopqrstuvwxyz
}</code></pre>
<h2 id="9-예외-처리">9. 예외 처리</h2>
<p>코틀린의 예외 처리는 자바와 비슷하지만, 함수가 던질 수 있는 예외를 선언하지 않아도 됩니다.</p>
<pre><code class="language-kotlin">fun divide(a: Int, b: Int): Int {
    if (b == 0) {
        throw IllegalArgumentException(&quot;0으로 나눌 수 없습니다&quot;)
    }
    return a / b
}

// 예외 처리
try {
    val result = divide(10, 0)
    println(result)
} catch (e: IllegalArgumentException) {
    println(&quot;오류: ${e.message}&quot;)
} catch (e: Exception) {
    println(&quot;예상치 못한 오류: ${e.message}&quot;)
} finally {
    println(&quot;정리 작업&quot;)
}

// try를 식으로 사용
val result = try {
    divide(10, 2)
} catch (e: Exception) {
    -1  // 기본값
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Promise.all(): 여러 비동기 작업을 동시에 처리하기]]></title>
            <link>https://velog.io/@kanguk_o/Promise.all-%EC%97%AC%EB%9F%AC-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%9E%91%EC%97%85%EC%9D%84-%EB%8F%99%EC%8B%9C%EC%97%90-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kanguk_o/Promise.all-%EC%97%AC%EB%9F%AC-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%9E%91%EC%97%85%EC%9D%84-%EB%8F%99%EC%8B%9C%EC%97%90-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 19 Feb 2025 11:43:21 GMT</pubDate>
            <description><![CDATA[<h1 id="promiseall-여러-비동기-작업을-동시에-처리하기">Promise.all(): 여러 비동기 작업을 동시에 처리하기</h1>
<h2 id="들어가며">들어가며</h2>
<p>현대 웹 개발에서 여러 비동기 작업을 효율적으로 처리하는 것은 매우 중요합니다. 특히 여러 API를 호출하거나, 다수의 파일을 처리해야 할 때 Promise.all()은 강력한 도구가 됩니다. 이번 글에서는 Promise.all()의 개념부터 실제 사용 사례까지 자세히 알아보겠습니다.</p>
<h2 id="promiseall이란">Promise.all()이란?</h2>
<p>Promise.all()은 여러 프로미스를 병렬로 처리하고 모든 프로미스가 완료될 때까지 기다리는 메서드입니다. 배열로 전달된 모든 프로미스가 성공적으로 이행되면, 각 프로미스의 결과값을 배열로 반환합니다.</p>
<h3 id="기본-문법">기본 문법</h3>
<pre><code class="language-javascript">Promise.all([promise1, promise2, promise3])
  .then(results =&gt; {
    // results는 각 프로미스의 결과값을 담은 배열
  })
  .catch(error =&gt; {
    // 어느 하나라도 실패하면 에러 처리
  });</code></pre>
<h2 id="주요-특징">주요 특징</h2>
<ol>
<li><p><strong>병렬 실행</strong></p>
<ul>
<li>모든 프로미스가 동시에 실행됩니다.</li>
<li>전체 실행 시간은 가장 오래 걸리는 프로미스의 실행 시간과 같습니다.</li>
</ul>
</li>
<li><p><strong>실패 처리</strong></p>
<ul>
<li>하나의 프로미스라도 실패하면 전체가 실패로 처리됩니다.</li>
<li>첫 번째 발생한 에러가 catch 블록으로 전달됩니다.</li>
</ul>
</li>
<li><p><strong>결과 배열</strong></p>
<ul>
<li>결과는 입력된 프로미스 배열과 동일한 순서를 보장합니다.</li>
<li>실행 완료 순서와 관계없이 입력 순서대로 결과가 저장됩니다.</li>
</ul>
</li>
</ol>
<h2 id="실제-사용-예제">실제 사용 예제</h2>
<h3 id="1-여러-api-동시-호출">1. 여러 API 동시 호출</h3>
<pre><code class="language-javascript">async function fetchUserData() {
  try {
    const [profile, posts, friends] = await Promise.all([
      fetch(&#39;/api/profile&#39;),
      fetch(&#39;/api/posts&#39;),
      fetch(&#39;/api/friends&#39;)
    ]);

    const results = await Promise.all([
      profile.json(),
      posts.json(),
      friends.json()
    ]);

    return {
      profile: results[0],
      posts: results[1],
      friends: results[2]
    };
  } catch (error) {
    console.error(&#39;데이터 로딩 실패:&#39;, error);
    throw error;
  }
}</code></pre>
<h3 id="2-파일-처리">2. 파일 처리</h3>
<pre><code class="language-javascript">async function processMultipleFiles(files) {
  const filePromises = files.map(file =&gt; processFile(file));

  try {
    const results = await Promise.all(filePromises);
    console.log(&#39;모든 파일 처리 완료:&#39;, results);
  } catch (error) {
    console.error(&#39;파일 처리 중 오류 발생:&#39;, error);
  }
}</code></pre>
<h3 id="3-이미지-프리로딩">3. 이미지 프리로딩</h3>
<pre><code class="language-javascript">function preloadImages(imageUrls) {
  const loadImage = url =&gt; {
    return new Promise((resolve, reject) =&gt; {
      const img = new Image();
      img.onload = () =&gt; resolve(img);
      img.onerror = () =&gt; reject(new Error(`Failed to load ${url}`));
      img.src = url;
    });
  };

  return Promise.all(imageUrls.map(loadImage));
}</code></pre>
<h2 id="주의사항과-팁">주의사항과 팁</h2>
<ol>
<li><p><strong>에러 처리</strong></p>
<ul>
<li>항상 catch 블록을 사용하여 에러를 적절히 처리해야 합니다.</li>
<li>개별 프로미스의 실패를 허용하려면 Promise.allSettled()를 고려하세요.</li>
</ul>
</li>
<li><p><strong>성능 고려</strong></p>
<ul>
<li>너무 많은 프로미스를 동시에 실행하면 리소스 문제가 발생할 수 있습니다.</li>
<li>필요한 경우 배치 처리를 고려하세요.</li>
</ul>
</li>
<li><p><strong>타임아웃 처리</strong></p>
<pre><code class="language-javascript">function timeoutPromise(ms, promise) {
const timeout = new Promise((_, reject) =&gt; {
 setTimeout(() =&gt; reject(new Error(&#39;Timeout&#39;)), ms);
});

return Promise.race([promise, timeout]);
}
</code></pre>
</li>
</ol>
<p>// 사용 예제
const promises = [promise1, promise2, promise3];
Promise.all(promises.map(p =&gt; timeoutPromise(5000, p)))
  .then(results =&gt; console.log(&#39;모든 작업 완료:&#39;, results))
  .catch(error =&gt; console.error(&#39;작업 실패:&#39;, error));</p>
<pre><code>
## 마치며
Promise.all()은 여러 비동기 작업을 효율적으로 처리할 수 있게 해주는 강력한 도구입니다. 적절한 에러 처리와 함께 사용한다면, 복잡한 비동기 로직을 깔끔하게 처리할 수 있습니다. 특히 여러 API를 동시에 호출하거나 다수의 파일을 처리해야 하는 상황에서 큰 힘을 발휘합니다.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[ 의존성과 단일 책임 원칙(SRP) 의 고찰]]></title>
            <link>https://velog.io/@kanguk_o/%EC%9D%98%EC%A1%B4%EC%84%B1%EA%B3%BC-%EB%8B%A8%EC%9D%BC-%EC%B1%85%EC%9E%84-%EC%9B%90%EC%B9%99SRP-%EC%9D%98-%EA%B3%A0%EC%B0%B0</link>
            <guid>https://velog.io/@kanguk_o/%EC%9D%98%EC%A1%B4%EC%84%B1%EA%B3%BC-%EB%8B%A8%EC%9D%BC-%EC%B1%85%EC%9E%84-%EC%9B%90%EC%B9%99SRP-%EC%9D%98-%EA%B3%A0%EC%B0%B0</guid>
            <pubDate>Tue, 11 Feb 2025 13:54:56 GMT</pubDate>
            <description><![CDATA[<p>요즘 공부하면서 의존성과 단일 책임 원칙에 대한 고찰을 해보고자 한다</p>
<ol>
<li>별도 메서드로 처리하는 경우
장점:
기존 클래스 내에서 해결 가능 → 새로운 의존성을 추가하지 않아도 됨
코드 변경이 적고, 빠르게 적용 가능
단점:
해당 클래스의 책임이 커질 수 있음
다른 기능과 결합도가 높아질 가능성이 있음 (특히 기존 클래스가 이미 많은 역할을 하고 있다면)</li>
<li>별도 클래스로 분리하는 경우
장점:
단일 책임 원칙(SRP)을 지킬 수 있음 → 유지보수성과 테스트 용이성이 증가
관심사가 분리되므로 다른 클래스와의 결합도가 낮아짐
단점:
새로운 의존성이 생김 → 기존 클래스에서 새로운 클래스를 호출해야 함
코드의 복잡성이 약간 증가할 수 있음
언제 별도 클래스로 분리해야 할까?
✅ 기존 클래스가 이미 여러 역할을 하고 있다면? → 분리하는 것이 좋음
✅ 새로운 기능이 기존 기능과 명확히 구분되는 역할을 한다면? → 분리하는 것이 좋음
✅ 재사용 가능성이 높은 기능이라면? → 분리하는 것이 좋음
✅ 반복적으로 호출되는 코드라면? → 분리해서 관리하는 것이 좋음</li>
</ol>
<p>반대로,
❌ 작은 기능이고 재사용 가능성이 낮다면? → 기존 클래스의 메서드로 처리해도 무방함
❌ 불필요한 분리로 코드가 오히려 복잡해진다면? → 메서드로 유지하는 것이 좋음</p>
<p>결국, 코드의 응집도(cohesion)와 결합도(coupling) 를 고려해서 결정하는 게 핵심</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[면접 예상 질문]]></title>
            <link>https://velog.io/@kanguk_o/%EB%A9%B4%EC%A0%91-%EC%98%88%EC%83%81-%EC%A7%88%EB%AC%B8</link>
            <guid>https://velog.io/@kanguk_o/%EB%A9%B4%EC%A0%91-%EC%98%88%EC%83%81-%EC%A7%88%EB%AC%B8</guid>
            <pubDate>Wed, 08 Jan 2025 17:19:49 GMT</pubDate>
            <description><![CDATA[<ul>
<li><p>개인별 맞춤 질문</p>
<ol>
<li>MSA와 모놀리틱의 차이점은 무엇이라고 생각하는가?
MSA는 기능별로 독립적인 마이크로 서비스로 분리되어 유연한 배포 및 확장성을 가지고
모놀리식은 단일 코드베이스로 관리 되어 구조는 단순하지만 변경시 전체 배포가 필요</li>
<li>Spring Batch란 무엇이고 왜 사용했는가?
대규모 데이터 처리와 배치작업을 효율적으로 관리하기 위한것으로 데이터 마이그레이션이나
대량 데이터 처리에 사용됨</li>
<li>경쟁 상태와 교착상태에 대해서 설명해주세요.
경쟁상태는 자원접근 순서에 따라 결과가 달라지는 것이고 
교착 상태는 두개의 프로세스가 자원을 서로 대기하며 멈추는 상태입니다 </li>
<li>동시성 문제란 무엇이고 어떻게 해결할 수 있는가?
동시성 문제는 다수의 프로세스가 공유자원에 동시에 접근할때 발생하는 문제 이고 
동기화 및 락 , 트랜잭션 관리 등으로 해결 합니다.</li>
<li>DB의 인덱스란 무엇이고 복합 인덱스와 커버링 인덱스가 무엇인지 설명해주세요.
DB인덱스는 데이터를 빠르게 조회하기위한 자료구조
복합인덱스는 여러컬럼을 조합한 인덱스
커버링 인덱스는 인덱스 만으로 데이터를 조회하는 방식</li>
<li>N+1 문제가 발생하는 이유는?
N+1은 데이터 조회시 부모 데이터 1번과 자식데이터 N번 추가 조회하면서 발생하는 것</li>
<li>MSA의 트랜잭션 전략은 어떻게 수립할 수 있는가?
MSA 환경에서 SAGA 패턴이나 OutBox패턴 같은 보상 트랜잭션이나 2PC 같은 분산 트랜잭션 관리 방식을 사용</li>
<li>MSA에서 왜? Config Server를 사용해야 할까요?
MSA 환경에서 각 서비스의 설정을 중앙에서 관리 해 일관성과 유지보수를 높일수 있기때문입니다</li>
<li>MSA 환경에서 Message Bus(Kafka) 서비스가 Down되는 경우, 어떻게 조치를 취할 수 있고, 어떻게 대비해야하는가?
DLQ를 설정 또는 DLQ를 데이터베이스에 저장 및 클러스터 구성 및 모니터링으로 대비 </li>
<li>Bean과 IoC에 대해서 설명해주세요.
bean은 스프링 컨테이너가 관리하는 객체이고 Ioc는 객체의 생성과 생명주기를 컨테이너가 관리는 개념 </li>
<li>Spring Boot의 DispatcherServlet에 대해서 설명해주세요.
클라이언트 요청을 받고 컨트롤러고 전달해 응답을 처리하는 스프링 핵심 프론트 컨트롤러</li>
<li>filter, interceptor, aop에 대해서 설명해주세요.
fileter는 요청 전후의 웹 필터링 interceptor는 컨트롤러 호출 전후 처리 AOP는 비지니스 로직 외부의 부가 가능을 처리합니다</li>
<li>트랜잭션이란 무엇이고 ACID 특징에 대해서 설명해주세요.
트랜잭션의 작업의 논리적 단위 , ACID는 트랜잭션의 원자성 일관성 고립성 지속성을 의미</li>
<li>JWT란 무엇이고 어떻게 동작하는가?
JWT는 Json기반의 토큰으로 인정 정보를 저장하며 서명된 토큰을 통해 인증 권한을 검증</li>
<li>VM과 컨테이너의 차이점에 대해서 아는가?
VM은 하드웨어를 가상화 하고 컨테이너는 OS를 공유해 경량화된 환경에서 애플리케이션을 실행</li>
<li>TCP/UDP에 대해서 설명해주세요.
TCP는 신뢰성있는 데이터 전송을 보장 UDP는 빠른 전송이 필요한 상황에서 사용 </li>
<li>Redis에서 원자적 처리에 대해서 설명해주세요.
Redis는 단일 싱글 스레드로 동작하며 명령 실행이 순차적이여서 원자성을 보장 </li>
<li>ORM이란 무엇이고 왜 태어났는가?
ORM은 객체와 RDB를 매핑하여 SQL을 자동으로 생성해 개발의 생산성을 높이기 위해 탄생 </li>
<li>스프링 배치에서 멱등성이란 무엇을 의미하는가?
멱등성은 동일 데ㅐ이터를 여러번 처리를 해도 결과가 변하지 않음을 의미</li>
<li>Spring Batch에서 트랜잭션 관리를 왜 청크 단위로 하는가?
청크 단위 처리는 메모리 효율성을 높이고 트랜잭션 롤백 범위를 제한해 성능 과 신뢰성을 높이기 위함 </li>
</ol>
</li>
<li><p>면접 예상 질문</p>
<h3 id="기술-관련-질문">기술 관련 질문</h3>
<ul>
<li><p><strong>Redis Lua Script를 활용한 원자적 처리를 적용했던 경험을 설명해주세요</strong></p>
<p>  대규모 쿠폰 발급 이벤트에서 수만건의 동시요청 테스트를 진행 햇을때 데드락과 중복발급 문제가 발생
  처음에는 redis 분산락으로 동시성 제어를 했으나 락을 얻고 해제하는데 시간이 소요됨에 따라 </p>
<p>  대규모 쿠폰 발급시 그 시간이 증가하게 되어 최적화 하고자 lua script를 사용을 했고</p>
<p>  lua script는 redis 서버에서 단일 트랜잭션으로 실행 대기때문에 race condition을 방지하고
  redis 서버 내에서 실행 되기때문에 락을 얻고 해제하는 시간이 없기때문에
  응답시간이 약 80프로 개선되었습니다</p>
</li>
<li><p><strong>kafka를 도입하여 비동기 처리를 구현 했던 프로젝트에서 발생한 주요 장애와 이를 해결한 방법</strong></p>
<p>  쿠폰 발급 및 결제 완료 시에 이메일 알림을 실시간으로 발송 하면서 지연이 발생 </p>
<p>  kafka Topic을 생성 하여 발급 이벤트 했고 이메일 알림서비스가 비동기 메세지를 처리하고도록 구현을 했지만 
  메세지 소비 속도가 느려지는 문제가 발생</p>
<p>  파티션을 늘리고 컨슈머 스레드를 확장하여 처리 속도를 개선 </p>
<p>  발송 지연이 단축되었고 대량 발급 상황에서 안정적인 처리가 가능했습니다.</p>
</li>
<li><p>Spring Batch를 사용하여 대용량 데이터 처리 문제를 해결했던 사례를 설명 해주세요</p>
<p>  만료된 쿠폰과 유효기간이 지난 데이터를 정리하면서 데이터 베이스 부하가 증가</p>
<p>  Spring Batch 를 도입하며 chunk 기반으로 데이터를 처리했고</p>
<p>  처음에는 Pageable 를 사용하여 읽어왔지만 데이터를 절반 밖에 읽어 오지못하여 </p>
<p>  항상 페이지를 첫번째 즉 0번째부터 읽어오도록 설정을 하여 적은 리소스로 데이터 처리</p>
<p>  그후 zero offset방식으로 맨뒤의 아이디를 읽어와 처리하는 방식으로 처리하여 처리 속도 
  데이터 삭제 속도 50% 향상된 경험이 있습니다.</p>
</li>
<li><p>API 응답 속도 개선을 위해 Redis를 활용했던 방식을 구체적으로 설명</p>
<p>  자주 조회 되는 데이터를 Redis 캐시에 저장 </p>
<p>  데이터 갱신 시 캐시를 동기화 하도록 TTL 설정 </p>
<ul>
<li>평균 API 응답 시간이 40% 단축되었습니다.</li>
<li>캐시 히트율이 높아져 데이터베이스 부하가 크게 감소했습니다.</li>
</ul>
</li>
<li><p>Docker 와 Jenkins를 사용한 CI/CD 구축 경험을 설명</p>
<p>  각 서비스 폴더에 JenkinsFile을 만들어 푸시시 각 서비스 폴더에 변경이 있을때만 
  Docker 이미지를 빌드를 했으며 모노 레포에서도 각각 유연한 배포가 이루어 지도록 설정 
  그후 AWS ECR 에 이미지를 저장하고 각 서비스의 태스크를 정의하고 fargate로 
  Rolling 업데이트를 진행을 했습니다 </p>
<h3 id=""></h3>
</li>
</ul>
</li>
</ul>
<pre><code>### 프로젝트 관련질문

- &quot;Jari-Otte&quot; 프로젝트에서 가장 도전적이었던 문제는 무엇이었고, 이를 어떻게 해결했나요?

    선착순 쿠폰 발급시 대규모 동시 요청으로 인해 서버 부하가 급격히 증가 했고 
    쿠폰 재고 데이터가 충돌 
    redis lua 스크립트를 활용하여 쿠폰 재고상태를 원자적으로 제어 
    쿠폰 발급시 실시간 이메일 알림을 kafka를 도입하여 예약 이벤트를 비동기적으로 처리 
    DB와 분리된 메세징 시스템으로 부하를 분산 

- &quot;Deep-I&quot; 프로젝트에서 데이터를 수집하고 처리하는 로직에서 가장 중요한 부분은 무엇이었나요?

    라즈베리파이로 수집한 데이터를 실시간으로 파싱하고, 데이터 일관성을 보장하는 로직이며
    데이터 수집시 타임스탬프 기반 정렬을 적용해 시간대 간 일관성 유지
    Redis를 사용해 자주 조회되는 데이터를 캐싱하여 데이터 처리속도 향상

- &quot;Jari-Otte&quot;에서 MSA 아키텍처를 적용하며 배운 점은 무엇인가요?

    서비스 분리의 중요성 동기 비동기 통신 활용 데이터 일관성 

- 쿠폰 발급에서 Redis와 Kafka를 동시에 활용한 이유와 그 결과를 설명해주세요.

    redis 는 쿠폰의 재고 즉 수량을 관리 하기 위해 원자적 연산에 사용을 하였고
    fafka는 쿠폰 발급 이벤트를 메시징 시스템으로 관리를 하여 비동기 알림 및 로그 저장 을 했습니다

- 사용자 API 설계 및 보안을 강화했던 경험이 있다면 알려주세요.

    JWT 인증 및 redis를 활용해 리프래쉬 토큰을 관리 함으로서 보안을 강화
    사용자 요청 데이터의 유효성을 검증하는 필터를 추가 

- &quot;창 헬스케어&quot;에서 혈압 정보 API 설계 시 고려했던 보안 및 성능 요소는 무엇이었나요?

    인증 권한 관리를 위해 JWT를 사용 

    MySQL 인덱스를 최적화 하여 조회 성능 개선 


### CS 관련 질문</code></pre><ul>
<li><p>CS 관련 질문</p>
<h3 id="1-자료구조"><strong>1. 자료구조</strong></h3>
<h3 id="배열array와-연결-리스트linked-list의-차이점은-무엇인가요"><strong>배열(Array)와 연결 리스트(Linked List)의 차이점은 무엇인가요?</strong></h3>
<p>  &quot;배열은 메모리에 연속적으로 저장되며, 인덱스를 사용하여 O(1) 시간에 접근할 수 있습니다. 하지만 크기가 고정되어 있어 삽입이나 삭제 시 O(n) 시간이 걸리는 단점이 있습니다.</p>
<p>  반면, 연결 리스트는 노드와 포인터로 이루어져 있어 메모리가 비연속적이고 삽입/삭제가 빠릅니다. 하지만 순차적으로 접근해야 하므로 O(n) 시간이 걸립니다.&quot;</p>
<hr>
<h3 id="해시-테이블hash-table의-작동-원리를-설명해주세요-충돌이-발생하면-어떻게-처리하나요"><strong>해시 테이블(Hash Table)의 작동 원리를 설명해주세요. 충돌이 발생하면 어떻게 처리하나요?</strong></h3>
<p>  &quot;해시 테이블은 키를 해시 함수로 변환해 특정 인덱스에 값을 저장합니다. 충돌이 발생하면 체이닝 방식으로 같은 인덱스에 연결 리스트로 연결하거나, 오픈 어드레싱 방식으로 비어 있는 공간을 탐색해 저장합니다.&quot;</p>
<hr>
<h3 id="스택stack과-큐queue의-차이점과-활용-사례를-설명해주세요"><strong>스택(Stack)과 큐(Queue)의 차이점과 활용 사례를 설명해주세요.</strong></h3>
<p>  &quot;스택은 LIFO 구조로, 마지막에 추가된 데이터가 먼저 제거됩니다. 함수 호출 스택이나 뒤로가기 기능에 주로 사용됩니다.</p>
<p>  큐는 FIFO 구조로, 먼저 들어온 데이터가 먼저 처리됩니다. 작업 대기열이나 BFS 탐색에서 활용됩니다.&quot;</p>
<hr>
<h3 id="트리tree와-그래프graph의-차이점은-무엇인가요"><strong>트리(Tree)와 그래프(Graph)의 차이점은 무엇인가요?</strong></h3>
<p>  &quot;트리는 계층적 구조로 루트 노드에서 시작하며, 사이클이 없습니다. 반면, 그래프는 모든 노드가 자유롭게 연결될 수 있으며, 방향성과 사이클이 허용됩니다.&quot;</p>
<hr>
<h3 id="이진-탐색-트리binary-search-tree의-특징과-삽입-삭제-과정에-대해-설명해주세요"><strong>이진 탐색 트리(Binary Search Tree)의 특징과 삽입, 삭제 과정에 대해 설명해주세요.</strong></h3>
<p>  &quot;이진 탐색 트리는 왼쪽 자식 노드는 부모보다 작고, 오른쪽 자식 노드는 부모보다 큰 구조를 가집니다.</p>
<p>  삽입 시에는 루트부터 비교하며 적절한 위치에 추가합니다. 삭제 시에는 자식 노드의 개수에 따라 처리 방식이 다릅니다. 자식이 없는 경우 바로 삭제하고, 하나인 경우 부모와 자식을 연결하며, 두 개인 경우 오른쪽 서브트리의 최소값으로 대체합니다.&quot;</p>
<hr>
<h3 id="2-알고리즘"><strong>2. 알고리즘</strong></h3>
<h3 id="정렬-알고리즘의-시간-복잡도를-비교해주세요-예를-들어-quick-sort와-merge-sort의-차이점은-무엇인가요"><strong>정렬 알고리즘의 시간 복잡도를 비교해주세요. 예를 들어, Quick Sort와 Merge Sort의 차이점은 무엇인가요?</strong></h3>
<p>  &quot;Quick Sort는 평균적으로 O(n log n)의 시간 복잡도를 가지며, 메모리를 추가로 사용하지 않는 제자리 정렬입니다. 하지만 최악의 경우 O(n²)까지 느려질 수 있습니다.</p>
<p>  반면, Merge Sort는 항상 O(n log n) 시간 복잡도를 가지며 안정적인 정렬입니다. 다만, 추가 메모리가 필요하다는 단점이 있습니다.&quot;</p>
<hr>
<h3 id="dfs깊이-우선-탐색와-bfs너비-우선-탐색의-차이점과-활용-사례를-설명해주세요"><strong>DFS(깊이 우선 탐색)와 BFS(너비 우선 탐색)의 차이점과 활용 사례를 설명해주세요.</strong></h3>
<p>  &quot;DFS는 스택을 사용하며, 깊이를 우선으로 탐색합니다. 퍼즐 해결이나 백트래킹에 적합합니다.</p>
<p>  BFS는 큐를 사용하며, 너비를 우선으로 탐색합니다. 최단 경로를 찾거나 레벨별 탐색이 필요한 경우 유용합니다.&quot;</p>
<hr>
<h3 id="동적-프로그래밍dynamic-programming이란-무엇인가요-어떤-문제를-해결할-때-유용한가요"><strong>동적 프로그래밍(Dynamic Programming)이란 무엇인가요? 어떤 문제를 해결할 때 유용한가요?</strong></h3>
<p>  &quot;동적 프로그래밍은 중복되는 문제를 해결한 결과를 저장하여 다시 계산하지 않도록 하는 방법입니다. 피보나치 수열, 배낭 문제처럼 하위 문제의 결과를 재활용할 수 있는 문제에서 유용합니다.&quot;</p>
<hr>
<h3 id="이진-탐색binary-search이-가능한-조건과-시간-복잡도를-설명해주세요"><strong>이진 탐색(Binary Search)이 가능한 조건과 시간 복잡도를 설명해주세요.</strong></h3>
<p>  &quot;이진 탐색은 데이터가 정렬되어 있어야 사용 가능합니다. 탐색 범위를 절반씩 줄여나가기 때문에 O(log n)의 시간 복잡도를 가집니다.&quot;</p>
<hr>
<h3 id="캐시cache의-작동-원리와-lruleast-recently-used-알고리즘에-대해-설명해주세요"><strong>캐시(Cache)의 작동 원리와 LRU(Least Recently Used) 알고리즘에 대해 설명해주세요.</strong></h3>
<p>  &quot;캐시는 자주 사용하는 데이터를 저장하여 빠르게 접근할 수 있게 해주는 메모리입니다.</p>
<p>  LRU 알고리즘은 최근에 사용되지 않은 데이터를 먼저 제거하는 방식으로, 해시맵과 이중 연결 리스트를 활용해 구현합니다.&quot;</p>
<hr>
<h3 id="3-네트워크"><strong>3. 네트워크</strong></h3>
<h3 id="http와-https의-차이점은-무엇인가요"><strong>HTTP와 HTTPS의 차이점은 무엇인가요?</strong></h3>
<p>  &quot;HTTP는 평문으로 데이터를 전송하기 때문에 보안에 취약합니다.</p>
<p>  HTTPS는 TLS/SSL을 사용하여 데이터를 암호화하며, 인증서를 통해 데이터의 신뢰성을 보장합니다.&quot;</p>
<hr>
<h3 id="restful-api란-무엇이고-rest의-주요-설계-원칙을-설명해주세요"><strong>RESTful API란 무엇이고, REST의 주요 설계 원칙을 설명해주세요.</strong></h3>
<p>  &quot;RESTful API는 자원을 URL로 표현하고, HTTP 메서드(GET, POST, PUT, DELETE)를 사용해 데이터를 처리합니다.</p>
<p>  REST의 주요 설계 원칙은 클라이언트-서버 구조, 무상태성, 캐시 가능성 등이 있습니다.&quot;</p>
<hr>
<h3 id="tcp와-udp의-차이점은-무엇인가요-각각의-활용-사례를-들어주세요"><strong>TCP와 UDP의 차이점은 무엇인가요? 각각의 활용 사례를 들어주세요.</strong></h3>
<p>  &quot;TCP는 연결 지향적 프로토콜로 데이터의 신뢰성을 보장합니다. 예를 들어, HTTP와 파일 전송에 사용됩니다.</p>
<p>  UDP는 비연결형으로 빠르지만 신뢰성이 낮습니다. 주로 스트리밍이나 온라인 게임에서 사용됩니다.&quot;</p>
<hr>
<h3 id="dnsdomain-name-system의-작동-원리를-설명해주세요"><strong>DNS(Domain Name System)의 작동 원리를 설명해주세요.</strong></h3>
<p>  &quot;DNS는 도메인 이름을 IP 주소로 변환합니다.</p>
<p>  클라이언트는 로컬 캐시를 먼저 확인하고, 없을 경우 루트 DNS → TLD DNS → 권한 DNS 순으로 요청해 IP를 반환받습니다.&quot;</p>
<hr>
<h3 id="corscross-origin-resource-sharing가-발생하는-이유와-이를-해결하는-방법은-무엇인가요"><strong>CORS(Cross-Origin Resource Sharing)가 발생하는 이유와 이를 해결하는 방법은 무엇인가요?</strong></h3>
<p>  &quot;CORS는 다른 도메인 간 요청을 할 때 발생합니다. 이를 해결하려면 서버에서 <code>Access-Control-Allow-Origin</code> 헤더를 설정해야 합니다.&quot;</p>
<hr>
<h3 id="4-운영체제"><strong>4. 운영체제</strong></h3>
<h3 id="프로세스process와-스레드thread의-차이점은-무엇인가요"><strong>프로세스(Process)와 스레드(Thread)의 차이점은 무엇인가요?</strong></h3>
<p>  &quot;프로세스는 독립적인 실행 단위로, 각자 메모리 공간을 가집니다. 반면, 스레드는 프로세스 내에서 실행되는 작업 단위로, 메모리를 공유합니다.&quot;</p>
<hr>
<h3 id="멀티스레딩에서-발생할-수-있는-문제와-이를-해결하기-위한-방법은-무엇인가요"><strong>멀티스레딩에서 발생할 수 있는 문제와 이를 해결하기 위한 방법은 무엇인가요?</strong></h3>
<p>  &quot;멀티스레딩에서는 데드락이나 레이스 컨디션이 발생할 수 있습니다. 이를 해결하기 위해 뮤텍스나 세마포어 같은 동기화 기법을 사용합니다.&quot;</p>
<hr>
<h3 id="컨텍스트-스위칭context-switching이란-무엇이며-왜-필요한가요"><strong>컨텍스트 스위칭(Context Switching)이란 무엇이며, 왜 필요한가요?</strong></h3>
<p>  &quot;컨텍스트 스위칭은 CPU가 현재 작업 상태를 저장하고 다른 작업으로 전환하는 과정입니다. 여러 프로세스나 스레드를 효율적으로 실행하기 위해 필요합니다.&quot;</p>
<hr>
<h3 id="5-데이터베이스"><strong>5. 데이터베이스</strong></h3>
<h3 id="sql과-nosql의-차이점은-무엇인가요"><strong>SQL과 NoSQL의 차이점은 무엇인가요?</strong></h3>
<p>  &quot;SQL은 관계형 데이터베이스로, 정해진 스키마를 따릅니다. NoSQL은 비관계형으로, 유연한 스키마를 제공합니다.</p>
<p>  SQL은 복잡한 쿼리에 강하고, NoSQL은 대규모 데이터와 분산 처리에 적합합니다.&quot;</p>
<hr>
<h3 id="트랜잭션transaction이란-무엇이며-acid-특성을-설명해주세요"><strong>트랜잭션(Transaction)이란 무엇이며, ACID 특성을 설명해주세요.</strong></h3>
<p>  &quot;트랜잭션은 데이터베이스에서 하나의 논리적 작업 단위입니다.</p>
<p>  ACID는 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 의미합니다.&quot;</p>
<hr>
<h3 id="인덱스index의-원리와-장단점에-대해-설명해주세요"><strong>인덱스(Index)의 원리와 장단점에 대해 설명해주세요.</strong></h3>
<p>  &quot;인덱스는 데이터를 효율적으로 검색하기 위한 구조입니다. 검색 속도를 향상시키지만, 삽입과 삭제 시 추가적인 비용이 발생합니다.&quot;</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Batch ItemReader의 페이지 처리 문제]]></title>
            <link>https://velog.io/@kanguk_o/Batch-ItemReader%EC%9D%98-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%B2%98%EB%A6%AC-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@kanguk_o/Batch-ItemReader%EC%9D%98-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%B2%98%EB%A6%AC-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Fri, 15 Nov 2024 12:42:52 GMT</pubDate>
            <description><![CDATA[<p>현상:</p>
<ul>
<li><p>기존 코드가 currentPage를 증가시키며 페이지 단위로 데이터를 순차적으로 가져오도록 설계되었음.
하지만, fetchNextPage에서 매번 currentPage가 증가해 다음 페이지로 이동하지 못하는 문제가 발생했음.
결과적으로 항상 첫 페이지(0번 페이지)만 처리되고, 다른 페이지의 데이터는 읽히지 않음.
원인:</p>
</li>
<li><p>currentPage가 증가하지 않는 문제는 아니었으나, 다음 페이지를 요청할 때 반환된 데이터가 비어있을 경우 noMoreData 플래그가 설정되어 데이터 순환이 종료되었음.
데이터 제공 서비스(CouponServiceClient)가 페이지 기반으로 동작하지 않는 경우도 고려되지 않음.
해결 방법:</p>
</li>
<li><p>fetchNextPage를 실제로 currentPage를 증가시키며 다음 페이지 데이터를 요청하도록 수정.
한 페이지에서 데이터가 비어도 다음 페이지를 확인하도록 수정.
서비스의 CouponServiceClient가 올바르게 페이지 데이터를 반환하는지 확인.</p>
</li>
</ul>
<p><strong>최종 코드 비교</strong>
기존 코드 </p>
<pre><code class="language-java">private Page&lt;UserCouponDto&gt; fetchFirstPage() {
    Page&lt;UserCouponDto&gt; page = couponServiceClient.getExpiredUserCoupons(
        currentDate, 
        0, // 항상 첫 페이지를 요청
        pageSize
    );

    if (!page.hasContent()) {
        noMoreData = true;
    }

    return page;
}</code></pre>
<ul>
<li><p>장점:
모든 페이지를 순환하며 데이터를 가져옴.
데이터 전체 처리에 적합.</p>
</li>
<li><p>단점:
특정 상황에서 페이지 이동을 제한해야 할 경우 부적합.
첫 페이지만 처리해야 하는 경우 요구사항 충족 불가</p>
</li>
</ul>
<p>*<em>수정된 코드 *</em></p>
<pre><code class="language-java">private Page&lt;UserCouponDto&gt; fetchFirstPage() {
    Page&lt;UserCouponDto&gt; page = couponServiceClient.getExpiredUserCoupons(
        currentDate, 
        0, // 항상 첫 페이지를 요청
        pageSize
    );

    if (!page.hasContent()) {
        noMoreData = true;
    }

    return page;
}</code></pre>
<ul>
<li><p>장점:
항상 첫 페이지 데이터만 처리하도록 고정.
요구사항에 맞게 동작하며 페이지 이동 방지.</p>
</li>
<li><p>단점:
데이터가 많은 경우, 다른 페이지 데이터는 처리되지 않음.
첫 페이지 데이터가 업데이트되지 않으면 중복 처리 가능성.</p>
</li>
</ul>
<p><strong>트러블 슈팅 과정 요약</strong></p>
<ul>
<li><p>문제 분석:
초기 페이지 순환 로직 문제를 확인.
로그를 통해 currentPage와 noMoreData의 상태를 점검.
CouponServiceClient에서 반환되는 페이지 데이터가 올바른지 확인.</p>
</li>
<li><p>로직 개선:
페이지 순환 방식에서 항상 첫 페이지를 처리하도록 로직 변경.
기존 currentPage 증가 로직 제거.</p>
</li>
<li><p>테스트:
다양한 시나리오에서 데이터 반환과 페이지 이동 여부를 테스트.
첫 페이지만 처리되는지 확인.</p>
</li>
<li><p>결과:
두 방식 모두 테스트 및 검증 완료.
페이지 순환 방식과 첫 페이지 고정 방식 중 요구사항에 따라 선택 가능.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Monolithic vs MSA]]></title>
            <link>https://velog.io/@kanguk_o/Monolithic-vs-MSA</link>
            <guid>https://velog.io/@kanguk_o/Monolithic-vs-MSA</guid>
            <pubDate>Thu, 07 Nov 2024 05:52:35 GMT</pubDate>
            <description><![CDATA[<p>프로젝트중 모놀리식 아키텍쳐를 MSA로 이관하는 과정을 거쳤는데 
두 아키텍쳐를 성능을 비교 해보려 합니다 </p>
<h3 id="모놀리식">모놀리식</h3>
<p><img src="https://velog.velcdn.com/images/kanguk_o/post/6bcc9056-9f6c-4759-94e2-2e061424d3a5/image.png" alt=""></p>
<h3 id="msa">MSA</h3>
<p><img src="https://velog.velcdn.com/images/kanguk_o/post/8ee192b2-21f7-4c8f-a900-25f23bdb0be7/image.png" alt=""></p>
<p><strong>Response Times (응답 시간):</strong></p>
<p>모놀리식에서는 응답 시간이 약 5,000ms에 가까운 높은 수준으로 나타납니다.
MSA에서는 응답 시간이 100ms 이내로 크게 감소했으며, 50% 응답 시간도 매우 짧은 편입니다.</p>
<p>응답 시간이 큰 폭으로 줄어들어, 시스템의 응답성 및 성능이 98% 개선되었습니다.</p>
<p><strong>RPS가 차이나는 이유:</strong>
MSA는 기능이 여러 개의 독립적인 서비스로 분리되어 있기 때문에 동시에 더 많은 요청을 처리할 수 있습니다. 반면 모놀리식 아키텍처는 모든 요청을 단일 애플리케이션에서 처리하기 때문에 MSA RPS가 낮아질 수밖에 없습니다</p>
<p><strong>응답 시간:</strong>
MSA에서는 각 서비스가 독립적으로 작동하기 때문에 병목 현상이 줄어들고, 병렬 처리 능력이 향상되어 응답 시간이 낮고 일관성 있게 유지됩니다. 반면 모놀리식 아키텍처에서는 여러 기능이 단일 애플리케이션에 통합되어 있어, 특정 기능의 부하가 증가하면 전체 응답 시간이 증가하게 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DispatcherServlet과 @ModelAttribute]]></title>
            <link>https://velog.io/@kanguk_o/DispatcherServlet%EA%B3%BC-ModelAttribute</link>
            <guid>https://velog.io/@kanguk_o/DispatcherServlet%EA%B3%BC-ModelAttribute</guid>
            <pubDate>Thu, 31 Oct 2024 14:23:06 GMT</pubDate>
            <description><![CDATA[<h1 id="dispatcherservlet과-modelattribute">DispatcherServlet과 @ModelAttribute</h1>
<h2 id="spring-mvc의-핵심-컴포넌트">Spring MVC의 핵심 컴포넌트</h2>
<h3 id="1-dispatcherservlet-이란">1. DispatcherServlet 이란?</h3>
<ul>
<li>Spring MVC의 프론트 컨트롤러(Front Controller)</li>
<li>모든 웹 요청의 진입점</li>
<li>요청을 적절한 컨트롤러에게 위임하고 응답을 처리</li>
</ul>
<h4 id="dispatcherservlet의-처리-흐름">DispatcherServlet의 처리 흐름</h4>
<ol>
<li>클라이언트 요청 접수</li>
<li>HandlerMapping을 통해 적절한 컨트롤러 검색</li>
<li>컨트롤러 실행</li>
<li>뷰 렌더링</li>
<li>응답 반환</li>
</ol>
<pre><code class="language-java">@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix(&quot;/WEB-INF/views/&quot;);
        resolver.setSuffix(&quot;.jsp&quot;);
        return resolver;
    }
}</code></pre>
<h3 id="2-modelattribute-활용">2. @ModelAttribute 활용</h3>
<ul>
<li>모델 객체를 파라미터나 리턴 값으로 처리</li>
<li>폼 데이터를 객체로 바인딩</li>
<li>유효성 검증 지원</li>
</ul>
<h4 id="예제-1-기본-사용법">예제 1: 기본 사용법</h4>
<pre><code class="language-java">public class UserForm {
    private String name;
    private String email;

    // getters and setters
}

@Controller
public class UserController {
    @PostMapping(&quot;/user/create&quot;)
    public String createUser(@ModelAttribute UserForm userForm) {
        // userForm.getName(), userForm.getEmail() 사용 가능
        return &quot;userResult&quot;;
    }
}</code></pre>
<h4 id="예제-2-모델-데이터-추가">예제 2: 모델 데이터 추가</h4>
<pre><code class="language-java">@Controller
public class UserController {
    @ModelAttribute(&quot;categories&quot;)
    public List&lt;String&gt; categories() {
        return Arrays.asList(&quot;일반&quot;, &quot;VIP&quot;, &quot;VVIP&quot;);
    }

    @GetMapping(&quot;/user/form&quot;)
    public String userForm() {
        // categories 자동으로 모델에 추가됨
        return &quot;userForm&quot;;
    }
}</code></pre>
<h3 id="3-실제-활용-사례">3. 실제 활용 사례</h3>
<h4 id="회원가입-폼-처리">회원가입 폼 처리</h4>
<pre><code class="language-java">@Controller
public class RegistrationController {
    @GetMapping(&quot;/register&quot;)
    public String showForm(Model model) {
        model.addAttribute(&quot;userForm&quot;, new UserForm());
        return &quot;registerForm&quot;;
    }

    @PostMapping(&quot;/register&quot;)
    public String processForm(@ModelAttribute(&quot;userForm&quot;) @Valid UserForm userForm,
                            BindingResult result) {
        if (result.hasErrors()) {
            return &quot;registerForm&quot;;
        }
        // 회원가입 처리
        return &quot;redirect:/welcome&quot;;
    }
}</code></pre>
<h3 id="4-장점과-특징">4. 장점과 특징</h3>
<ul>
<li><p><strong>DispatcherServlet</strong></p>
<ul>
<li>중앙 집중식 요청 처리</li>
<li>일관된 예외 처리</li>
<li>구성 요소의 유연한 변경</li>
</ul>
</li>
<li><p><strong>@ModelAttribute</strong></p>
<ul>
<li>코드 간소화</li>
<li>자동 데이터 바인딩</li>
<li>검증 로직 통합</li>
</ul>
</li>
</ul>
<h3 id="5-마무리">5. 마무리</h3>
<ul>
<li>Spring MVC의 핵심 구성요소 이해</li>
<li>효율적인 웹 애플리케이션 개발</li>
<li>클린 코드 작성에 도움</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[마이크로서비스 아키텍처(MSA)에서 연관관계를 처리하는 방법]]></title>
            <link>https://velog.io/@kanguk_o/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98MSA%EC%97%90%EC%84%9C-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EB%A5%BC-%EC%B2%98%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@kanguk_o/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98MSA%EC%97%90%EC%84%9C-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EB%A5%BC-%EC%B2%98%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Wed, 23 Oct 2024 09:30:10 GMT</pubDate>
            <description><![CDATA[<p>마이크로서비스 아키텍처(MSA)에서 연관관계를 처리하는 방법을 아주 쉽게 설명해드리겠습니다! </p>
<p>상상해보세요! 우리가 장난감 가게를 운영한다고 해볼까요? 
각각의 장난감(서비스)은 자기만의 집(데이터베이스)에서 살고 있어요.</p>
<ol>
<li><strong>ID 참조 방식</strong> (가장 간단한 방법)<pre><code class="language-java">// 장난감 서비스
public class Toy {
 private Long id;
 private String name;
 private Long childId;  // 아이의 ID만 가지고 있어요
}
</code></pre>
</li>
</ol>
<p>// 아이 서비스
public class Child {
    private Long id;
    private String name;
}</p>
<pre><code>
2. **REST API 호출 방식** (다른 집에 전화해서 물어보기)
```java
// 장난감 서비스에서 아이 정보가 필요할 때
@Service
public class ToyService {
    private final RestTemplate restTemplate;

    public ToyWithChild getToyWithChild(Long toyId) {
        Toy toy = toyRepository.findById(toyId);

        // 아이 서비스에 전화해서 정보를 물어봐요
        Child child = restTemplate.getForObject(
            &quot;http://child-service/children/&quot; + toy.getChildId(), 
            Child.class
        );

        return new ToyWithChild(toy, child);
    }
}</code></pre><ol start="3">
<li><p><strong>이벤트 방식</strong> (소식을 전달하기)</p>
<pre><code class="language-java">// 아이가 새 장난감을 받았을 때 알림을 보내요
public class ToyService {
 public void giveToyToChild(Long toyId, Long childId) {
     // 장난감을 아이와 연결해요
     Toy toy = toyRepository.findById(toyId);
     toy.setChildId(childId);

     // 다른 서비스들에게 소식을 전해요
     eventPublisher.publish(new ToyGivenEvent(toyId, childId));
 }
}</code></pre>
</li>
</ol>
<p>쉽게 설명하면:</p>
<ol>
<li>ID 참조는 마치 친구 집 주소만 알고 있는 것처럼, 다른 서비스의 ID만 저장해요</li>
<li>REST API 호출은 실제로 친구 집에 전화해서 물어보는 것과 같아요</li>
<li>이벤트 방식은 마치 학교 방송처럼, 중요한 소식이 있을 때 모두에게 알려주는 거예요</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker 기반 Jenkins를 이용한 애플리케이션 자동 배포
]]></title>
            <link>https://velog.io/@kanguk_o/Docker-%EA%B8%B0%EB%B0%98-Jenkins%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%9E%90%EB%8F%99-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@kanguk_o/Docker-%EA%B8%B0%EB%B0%98-Jenkins%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%9E%90%EB%8F%99-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Thu, 10 Oct 2024 04:08:41 GMT</pubDate>
            <description><![CDATA[<h2 id="소개">소개</h2>
<p>CI/CD 파이프라인에서 Jenkins는 자동화 도구로 널리 사용됩니다. 특히, Docker를 사용하면 Jenkins를 손쉽게 설치하고 실행할 수 있으며, Docker 컨테이너에서 일관된 환경에서 애플리케이션을 빌드하고 배포할 수 있습니다.
Docker 컨테이너로 Jenkins를 실행하고, 이를 통해 애플리케이션을 빌드하여 Docker 이미지로 생성하고 AWS에 배포하는 하는 중 권한 관련 문제 트러블이 생겨 정리 해보고자 합니다</p>
<h2 id="요구-사항">요구 사항</h2>
<ul>
<li>Docker가 설치된 서버</li>
<li>GitHub 저장소 (애플리케이션 소스 코드)</li>
<li>AWS S3 및 IAM 설정 (배포 용도)</li>
</ul>
<h2 id="jenkins-docker-컨테이너-실행">Jenkins Docker 컨테이너 실행</h2>
<h3 id="1-jenkins-docker-이미지-실행">1. Jenkins Docker 이미지 실행</h3>
<p>먼저 Docker를 이용해 Jenkins를 실행합니다. 공식 Jenkins 이미지를 사용하여 Docker 컨테이너를 실행할 수 있습니다.</p>
<pre><code class="language-bash">docker run -d -p 8080:8080 -p 50000:50000 \
  --name jenkins \
  -v jenkins_home:/var/jenkins_home \
  -v /var/run/docker.sock:/var/run/docker.sock \
  jenkins/jenkins:lts</code></pre>
<p>**  -v /var/run/docker.sock:/var/run/docker.sock** 옵션을 통해 Jenkins가 호스트의 Docker 데몬에 접근할 수 있게 합니다. 
이 설정은 Jenkins가 Docker 컨테이너를 관리하고 빌드할 수 있도록 도와줍니다.
jenkins_home 볼륨은 Jenkins 설정과 데이터를 지속적으로 유지하기 위해 필요합니다.</p>
<h2 id="2-jenkins-초기-설정">2. Jenkins 초기 설정</h2>
<p>Jenkins가 처음 실행되면 브라우저를 통해 초기 설정을 완료해야 합니다.</p>
<ol>
<li>브라우저에서 http://&lt;서버_IP&gt;:8080으로 접속합니다.</li>
<li>Jenkins가 제공하는 초기 암호를 입력합니다. 초기 암호는 컨테이너 내 /var/jenkins_home/secrets/initialAdminPassword에 저장되어 있습니다.</li>
</ol>
<pre><code class="language-bash">docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword</code></pre>
<h2 id="jenkins-pipeline-설정">Jenkins Pipeline 설정</h2>
<p>Jenkins에서 애플리케이션을 빌드하고, Docker 이미지를 생성한 후, 이를 AWS에 배포하는 파이프라인을 구성합니다.</p>
<ol>
<li>Jenkinsfile 작성
프로젝트의 루트 디렉터리에 Jenkinsfile을 추가하여 파이프라인을 정의합니다. 아래는 Gradle 기반의 애플리케이션을 빌드하고 Docker 이미지를 생성한 후 AWS에 배포하는 파이프라인 예시입니다.</li>
</ol>
<pre><code class="language-java">pipeline {
    agent any

    environment {
        DOCKER_IMAGE = &#39;my-docker-repo/my-app:latest&#39;
        AWS_S3_BUCKET = credentials(&#39;AWS_S3_BUCKET&#39;)
        AWS_REGION = credentials(&#39;AWS_REGION&#39;)
        AWS_ACCESS_KEY = credentials(&#39;AWS_ACCESS_KEY&#39;)
        AWS_SECRET_KEY = credentials(&#39;AWS_SECRET_KEY&#39;)
    }

    stages {
        stage(&#39;Clone Repository&#39;) {
            steps {
                git branch: &#39;main&#39;, url: &#39;https://github.com/your-repo/your-app.git&#39;
            }
        }

        stage(&#39;Build Application&#39;) {
            steps {
                sh &#39;./gradlew clean build&#39;
            }
        }

        stage(&#39;Build Docker Image&#39;) {
            steps {
                script {
                    withCredentials([usernamePassword(credentialsId: &#39;dockerhub&#39;, usernameVariable: &#39;DOCKER_USER&#39;, passwordVariable: &#39;DOCKER_PASS&#39;)]) {
                        sh &#39;&#39;&#39;
                            echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin
                            docker build -t $DOCKER_IMAGE .
                            docker push $DOCKER_IMAGE
                        &#39;&#39;&#39;
                    }
                }
            }
        }

        stage(&#39;Configure AWS&#39;) {
            steps {
                sh &#39;&#39;&#39;
                    echo &quot;cloud:&quot; &gt; src/main/resources/application.yml
                    echo &quot;  aws:&quot; &gt;&gt; src/main/resources/application.yml
                    echo &quot;    s3:&quot; &gt;&gt; src/main/resources/application.yml
                    echo &quot;      bucket: $AWS_S3_BUCKET&quot; &gt;&gt; src/main/resources/application.yml
                    echo &quot;    region:&quot; &gt;&gt; src/main/resources/application.yml
                    echo &quot;      static: $AWS_REGION&quot; &gt;&gt; src/main/resources/application.yml
                    echo &quot;    stack:&quot; &gt;&gt; src/main/resources/application.yml
                    echo &quot;      auto: false&quot; &gt;&gt; src/main/resources/application.yml
                    echo &quot;    credentials:&quot; &gt;&gt; src/main/resources/application.yml
                    echo &quot;      accessKey: $AWS_ACCESS_KEY&quot; &gt;&gt; src/main/resources/application.yml
                    echo &quot;      secretKey: $AWS_SECRET_KEY&quot; &gt;&gt; src/main/resources/application.yml
                &#39;&#39;&#39;
            }
        }

        stage(&#39;Deploy Application&#39;) {
            steps {
                sh &#39;&#39;&#39;
                    docker stop my-app || true
                    docker rm my-app || true
                    docker run -d --name my-app -p 8080:8080 \
                    -e AWS_ACCESS_KEY=$AWS_ACCESS_KEY \
                    -e AWS_SECRET_KEY=$AWS_SECRET_KEY \
                    -e AWS_REGION=$AWS_REGION \
                    -e AWS_S3_BUCKET=$AWS_S3_BUCKET \
                    $DOCKER_IMAGE
                &#39;&#39;&#39;
            }
        }
    }

    post {
        always {
            sh &#39;docker logout&#39;
        }
    }
}</code></pre>
<ol start="2">
<li>Jenkins 파이프라인 실행
Jenkins 대시보드에서 새로운 Pipeline Job을 생성하고, 위에서 작성한 Jenkinsfile을 통해 파이프라인을 실행합니다.</li>
</ol>
<p>Jenkins 대시보드에서 &quot;New Item&quot;을 클릭하고, 파이프라인을 선택하여 새로운 Job을 생성합니다.
소스 코드 관리에서 GitHub 저장소 URL을 입력하고, Jenkinsfile의 위치를 지정합니다.
파이프라인이 실행되면 Jenkins가 GitHub에서 소스 코드를 가져와 애플리케이션을 빌드하고, Docker 이미지를 생성 및 배포합니다.</p>
<h2 id="docker-권한-문제-해결">Docker 권한 문제 해결</h2>
<p>Jenkins가 Docker 데몬에 접근할 수 없는 경우, 다음과 같은 권한 오류가 발생할 수 있습니다:</p>
<pre><code class="language-java">WARNING! Your password will be stored unencrypted in /var/jenkins_home/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credential-stores
Login Succeeded
+ docker build -t kanguk148/jenkins:test .
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
            Install the buildx component to build images with BuildKit:
            https://docs.docker.com/go/buildx/
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post &quot;http://%2Fvar%2Frun%2Fdocker.sock/v1.47/build?buildargs=%7B%7D&amp;cachefrom=%5B%5D&amp;cgroupparent=&amp;cpuperiod=0&amp;cpuquota=0&amp;cpusetcpus=&amp;cpusetmems=&amp;cpushares=0&amp;dockerfile=Dockerfile&amp;labels=%7B%7D&amp;memory=0&amp;memswap=0&amp;networkmode=default&amp;rm=1&amp;shmsize=0&amp;t=kanguk148%2Fjenkins%3Atest&amp;target=&amp;ulimits=%5B%5D&amp;version=1&quot;: dial unix /var/run/docker.sock: connect: permission denied
script returned exit code 1</code></pre>
<p>이 문제는 Jenkins 사용자가 Docker에 접근할 권한이 없기 때문에 발생합니다. 이를 해결하려면 Jenkins 사용자를 docker 그룹에 추가해야 합니다.</p>
<p>Docker 소켓의 권한을 확인합니다</p>
<pre><code>docker exec -it -u root jenkins bash
ls -l /var/run/docker.sock</code></pre><p>소켓의 권한이 적절하지 않다면, 다음 명령어로 권한을 수정합니다:</p>
<pre><code class="language-java">chmod 666 /var/run/docker.sock
</code></pre>
<p>Jenkins 컨테이너 내부의 Docker 그룹 GID가 호스트 시스템의 Docker 그룹 GID와 일치하는지 확인합니다. 일치하지 않는다면, 다음 명령어로 수정합니다:</p>
<pre><code class="language-java">groupmod -g &lt;호스트_도커_GID&gt; docker</code></pre>
<p>Jenkins 사용자를 Docker 그룹에 다시 추가합니다:</p>
<pre><code class="language-java">usermod -aG docker jenkins
</code></pre>
<p>Jenkins 컨테이너를 재시작합니다:</p>
<pre><code>docker restart jenkins
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[CORS(Cross-Origin Resource Sharing)에 대한 이해와 설정 방법]]></title>
            <link>https://velog.io/@kanguk_o/CORSCross-Origin-Resource-Sharing%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%ED%95%B4%EC%99%80-%EC%84%A4%EC%A0%95-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@kanguk_o/CORSCross-Origin-Resource-Sharing%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%ED%95%B4%EC%99%80-%EC%84%A4%EC%A0%95-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sun, 06 Oct 2024 15:23:10 GMT</pubDate>
            <description><![CDATA[<h3 id="1-cors란-무엇인가"><strong>1. CORS란 무엇인가?</strong></h3>
<p>CORS(Cross-Origin Resource Sharing)는 웹 브라우저에서 도메인 간 리소스 요청을 제어하는 보안 메커니즘입니다. 즉, 브라우저는 한 도메인(origin)에서 다른 도메인(origin)의 리소스를 요청할 때 보안 규칙을 적용하는데, 이 과정에서 동작하는 규칙이 CORS입니다.</p>
<p><strong>기본 원리</strong>
웹 애플리케이션은 기본적으로 <strong>동일 출처 정책(Same-Origin Policy)</strong>에 의해 보호됩니다. 동일 출처 정책은 다음 세 가지가 모두 일치하는 경우에만 리소스를 자유롭게 공유하도록 허용합니다:</p>
<ul>
<li>프로토콜 (http, https)</li>
<li>호스트 (domain.com)</li>
<li>포트 (:8080, :443 등)</li>
</ul>
<h3 id="2-cors-작동-방식"><strong>2. CORS 작동 방식</strong></h3>
<p>CORS 요청이 일어날 때, 브라우저는 서버에 HTTP 요청을 보내며 &quot;이 요청이 안전한지&quot; 확인합니다. 서버는 특정 헤더를 응답에 포함하여 브라우저에게 &quot;이 요청은 허용된다&quot;고 알려줍니다. 브라우저는 이를 바탕으로 자원을 사용하도록 허가받습니다.</p>
<p><strong>예: CORS 요청 흐름</strong>
클라이언트(브라우저)에서 API 요청을 보냅니다 (예: <a href="https://frontend.com%EC%97%90%EC%84%9C">https://frontend.com에서</a> <a href="https://api.backend.com%EC%9C%BC%EB%A1%9C">https://api.backend.com으로</a> 요청).</p>
<p>서버(api.backend.com)는 응답에서 Access-Control-Allow-Origin 헤더를 포함시켜, 어느 도메인에서 접근이 가능한지 명시합니다.</p>
<pre><code>Access-Control-Allow-Origin: https://frontend.com</code></pre><p>브라우저는 응답에 포함된 Access-Control-Allow-Origin 값을 확인하고, 요청을 허용할지 결정합니다.</p>
<h3 id="3-cors-요청의-유형"><strong>3. CORS 요청의 유형</strong></h3>
<p>CORS 요청에는 두 가지 주요 유형이 있습니다: 단순(Simple) 요청과 예비(Preflight) 요청.</p>
<p><strong>1) 단순 요청(Simple Request)</strong></p>
<p>HTTP 메서드가 GET, POST, HEAD이고, Content-Type이 일반적인 값(application/x-www-form-urlencoded, text/plain, multipart/form-data)인 경우에는 단순 요청으로 처리됩니다. 이 경우, 브라우저는 CORS 정책을 자동으로 처리하고, 서버는 Access-Control-Allow-Origin 헤더만 포함시키면 됩니다.</p>
<p><strong>2) 예비 요청(Preflight Request)</strong></p>
<p>CORS 요청이 안전하지 않은 메서드(PUT, DELETE 등) 또는 커스텀 헤더를 포함하는 경우, 브라우저는 먼저 서버에 OPTIONS 메서드로 <strong>예비 요청(Preflight Request)</strong>을 보내 서버가 이 요청을 허용할지 확인합니다. 서버는 이를 처리하고 CORS 정책에 맞는 응답을 반환해야 합니다.</p>
<p>예비 요청의 응답 헤더 예시:</p>
<pre><code>Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization</code></pre><h3 id="4-cors-설정-방법-spring-boot-기준"><strong>4. CORS 설정 방법 (Spring Boot 기준)</strong></h3>
<p>스프링 부트(Spring Boot)에서는 CORS를 쉽게 설정할 수 있습니다. 주로 두 가지 방법으로 설정 가능합니다.</p>
<p><strong>1) Controller 레벨에서 CORS 설정</strong>
특정 컨트롤러나 핸들러 메서드에 대해서만 CORS를 허용하고 싶다면 @CrossOrigin 어노테이션을 사용할 수 있습니다.</p>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/api&quot;)
public class MyController {

    @CrossOrigin(origins = &quot;https://frontend.com&quot;)  // 특정 도메인에만 허용
    @GetMapping(&quot;/data&quot;)
    public ResponseEntity&lt;String&gt; getData() {
        return ResponseEntity.ok(&quot;Hello, CORS!&quot;);
    }
}</code></pre>
<p>이 설정은 특정 경로와 특정 메서드에만 적용되며, <a href="https://frontend.com">https://frontend.com</a> 도메인에서만 접근할 수 있습니다.</p>
<p><strong>2) Global CORS 설정 (전역 설정)</strong>
애플리케이션 전체에 대한 CORS 정책을 설정하고 싶다면 WebMvcConfigurer를 사용해 전역적으로 설정할 수 있습니다.</p>
<pre><code class="language-java">import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping(&quot;/**&quot;)  // 모든 경로에 대해 CORS 허용
                .allowedOrigins(&quot;https://frontend.com&quot;)  // 특정 Origin 허용
                .allowedMethods(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;)  // 허용할 HTTP 메서드
                .allowedHeaders(&quot;Content-Type&quot;, &quot;Authorization&quot;)  // 허용할 헤더
                .allowCredentials(true);  // 쿠키나 인증 정보도 함께 전송 허용
    }
}</code></pre>
<p>이 방식은 애플리케이션 전체에 적용되며, 모든 API 경로에서 지정한 도메인에 대해서만 CORS를 허용하게 됩니다.</p>
<h3 id="5-cors에서-발생할-수-있는-문제-및-해결-방법"><strong>5. CORS에서 발생할 수 있는 문제 및 해결 방법</strong></h3>
<p>1) CORS 정책 오류
브라우저에서 &quot;CORS 정책 오류&quot;가 발생할 수 있습니다. 이는 주로 서버가 요청한 Origin을 허용하지 않았기 때문입니다. 서버 응답에 Access-Control-Allow-Origin 헤더가 없거나 잘못된 경우에 이런 오류가 발생합니다.</p>
<pre><code>Access to XMLHttpRequest at &#39;https://api.backend.com&#39; from origin &#39;https://frontend.com&#39; has been blocked by CORS policy: No &#39;Access-Control-Allow-Origin&#39; header is present on the requested resource.</code></pre><p><strong>해결 방법:</strong></p>
<p>서버에서 올바르게 Access-Control-Allow-Origin 헤더를 설정했는지 확인해야 합니다.
필요하다면, 예비 요청에 대한 처리를 추가로 설정해야 할 수 있습니다.</p>
<p><strong>2) 쿠키 인증 관련 문제</strong>
만약 클라이언트가 쿠키나 인증 정보를 함께 보내려면, Access-Control-Allow-Credentials: true 설정을 추가로 해야 합니다. 이때 allowedOrigins는 와일드카드(*)를 사용할 수 없습니다.</p>
<pre><code class="language-java">registry.addMapping(&quot;/**&quot;)
        .allowedOrigins(&quot;https://frontend.com&quot;)
        .allowCredentials(true);  // 인증 정보 허용</code></pre>
<h3 id="6-최신-동향과-best-practice"><strong>6. 최신 동향과 Best Practice</strong></h3>
<p>보안 강화: CORS는 기본적으로 보안을 강화하는 기술이므로, 가능한 한 최소한의 도메인만 허용하는 것이 중요합니다. allowedOrigins에 모든 도메인(*)을 허용하는 것은 위험할 수 있습니다.
동적 CORS 처리: 복잡한 시스템에서는 사용자의 권한이나 상황에 따라 동적으로 CORS 정책을 적용할 수 있습니다.
HTTPS 사용: CORS는 보안과 밀접한 관련이 있으므로, 특히 민감한 데이터가 다뤄지는 경우에는 반드시 HTTPS 프로토콜을 통해 CORS 요청을 처리하는 것이 권장됩니다.</p>
<h3 id="결론"><strong>결론</strong></h3>
<p>CORS는 도메인 간 리소스 공유를 안전하게 허용하는 중요한 보안 메커니즘입니다. 스프링 부트에서는 @CrossOrigin 어노테이션이나 WebMvcConfigurer를 통해 쉽게 설정할 수 있으며, 올바르게 설정하지 않으면 클라이언트에서 CORS 오류가 발생할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DispatcherServlet 서블릿 / 핸들러 매핑]]></title>
            <link>https://velog.io/@kanguk_o/DispatcherServlet-%EC%84%9C%EB%B8%94%EB%A6%BF-%ED%95%B8%EB%93%A4%EB%9F%AC-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@kanguk_o/DispatcherServlet-%EC%84%9C%EB%B8%94%EB%A6%BF-%ED%95%B8%EB%93%A4%EB%9F%AC-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Fri, 27 Sep 2024 01:20:54 GMT</pubDate>
            <description><![CDATA[<ul>
<li><strong>DispatcherServlet도 부모 클래스에서 HttpServlet 서블릿을 상속 받아서 사용하고, 서블릿으로 동작한다.</strong><ul>
<li><strong>DispatcherServlet → FramworkServlet → HttpServletBean → HttpServlet</strong></li>
</ul>
</li>
<li>SpringBoot는 <strong>DispatcherServlet을 서블릿으로 자동으로 등록하면서 모든 경로(url=”/”)에 대해서 매핑한다.</strong></li>
</ul>
<h2 id="요청-흐름">요청 흐름</h2>
<ul>
<li>서블릿이 호출되면 HttpServlet이 제공하는 service()가 호출된다.</li>
<li>스프링 MVC는 <strong>DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이드 해두었다.</strong></li>
<li>FrameworkServlet.service()를 시작으로 여러 메서드가 호출되면서 <strong>DispatcherServlet.doDispatch()가 호출된다.</strong></li>
</ul>
<p><img src="https://velog.velcdn.com/images/kanguk_o/post/b754e7ea-30d8-45ff-aabb-064f8c394639/image.png" alt="">
<strong>동작 순서</strong></p>
<p>1. <strong>핸들러 조회</strong> : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.</p>
<p>2. <strong>핸들러 어댑터 조회</strong> : 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.</p>
<p>3. <strong>핸들러 어댑터 실행</strong> : 핸들러 어댑터를 실행한다.</p>
<p>4. <strong>핸들러 실행</strong> : 핸들러 어댑터가 실제 핸들러를 실행한다.</p>
<p>5. <strong>ModelAndView</strong> 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환한다.</p>
<p>6. <strong>viewResolver</strong> 호출  : 뷰 리졸버를 찾고 실행한다.</p>
<p>7. <strong>View 반환</strong> : 뷰 리졸버를 뷰의 논리 이름을 물리 이름으로 바꾸고, 랜더링 역할을 담당하는 뷰 객체를 반환한다.</p>
<p>8. <strong>뷰 렌더링</strong> : 뷰를 통해서 뷰를 렌더링 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] SSE(EventSource) 이벤트 리스너로 실시간 데이터 처리하기]]></title>
            <link>https://velog.io/@kanguk_o/Spring-Boot-SSEEventSource-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A6%AC%EC%8A%A4%EB%84%88%EB%A1%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kanguk_o/Spring-Boot-SSEEventSource-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A6%AC%EC%8A%A4%EB%84%88%EB%A1%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 25 Sep 2024 06:10:33 GMT</pubDate>
            <description><![CDATA[<p><strong>Server-Sent Events(SSE)</strong>는 서버가 클라이언트로 실시간 데이터를 푸시할 수 있는 웹 표준 기술입니다. 주로 실시간 알림, 주식 가격 업데이트, 실시간 대시보드 등에서 사용됩니다. 이번 글에서는 Spring Boot로 SSE를 구현하는 방법과 JavaScript를 이용한 클라이언트 이벤트 리스너를 소개합니다.</p>
<p><strong>SSE란?</strong>
SSE는 서버에서 클라이언트로 일방향 실시간 데이터 전송을 제공하는 기술입니다. 웹 브라우저의 EventSource API를 사용하여 클라이언트에서 서버에 연결을 열고, 서버는 지속적으로 클라이언트에게 데이터를 보낼 수 있습니다.</p>
<ul>
<li>단방향 통신: 서버에서 클라이언트로 데이터를 보냅니다.</li>
<li>자동 재연결: 연결이 끊어지면 클라이언트가 자동으로 다시 연결을 시도합니다.</li>
<li>텍스트 기반 데이터 전송: 일반적으로 JSON이나 텍스트 형식의 데이터를 주고받습니다.</li>
</ul>
<p><strong>SSE와 WebSocket 차이점</strong>
SSE: 서버에서 클라이언트로 일방향 푸시. HTTP 기반이며 설정이 간단.
WebSocket: 클라이언트와 서버 간 양방향 통신 가능. 양방향 데이터 전송이 필요하면 WebSocket이 적합.</p>
<p><strong>Spring Boot로 SSE 구현하기</strong></p>
<ol>
<li>프로젝트 설정
먼저 Spring Boot 프로젝트에서 spring-boot-starter-web 의존성을 추가합니다.<br></li>
<li>SSE 서버 구현
SseEmitter 클래스를 사용하여 SSE 기능을 제공할 수 있습니다. 아래는 서버에서 클라이언트로 메시지를 실시간으로 전송하는 간단한 예제입니다.</li>
</ol>
<pre><code class="language-java">import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@RestController
public class SseController {

    @GetMapping(&quot;/sse-stream&quot;)
    public SseEmitter streamSse() {
        SseEmitter emitter = new SseEmitter(30000L); // 타임아웃 30초

        ExecutorService service = Executors.newSingleThreadExecutor();

        service.execute(() -&gt; {
            try {
                for (int i = 0; i &lt; 10; i++) {
                    emitter.send(&quot;서버에서 보낸 메시지: &quot; + i);
                    TimeUnit.SECONDS.sleep(1); // 1초마다 전송
                }
                emitter.complete();
            } catch (IOException | InterruptedException e) {
                emitter.completeWithError(e);
            } finally {
                service.shutdown();
            }
        });

        return emitter;
    }
}</code></pre>
<p><strong>3. Spring Boot 애플리케이션 실행</strong>
서버가 실행되면 /sse-stream 경로로 클라이언트가 연결하여 실시간 데이터를 받을 수 있습니다.</p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;title&gt;SSE Example&lt;/title&gt;
    &lt;script&gt;
        document.addEventListener(&quot;DOMContentLoaded&quot;, function () {
            const eventSource = new EventSource(&quot;http://localhost:8080/sse-stream&quot;);

            eventSource.onmessage = function (event) {
                console.log(&quot;서버에서 받은 메시지:&quot;, event.data);
                const messageContainer = document.getElementById(&quot;messages&quot;);
                messageContainer.innerHTML += &quot;&lt;p&gt;&quot; + event.data + &quot;&lt;/p&gt;&quot;;
            };

            eventSource.onerror = function (event) {
                console.error(&quot;SSE 오류 발생:&quot;, event);
                eventSource.close();
            };
        });
    &lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Server-Sent Events (SSE)&lt;/h1&gt;
    &lt;div id=&quot;messages&quot;&gt;&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><p>실행 흐름</p>
<ul>
<li>클라이언트는 /sse-stream에 연결하고, 서버에서 실시간으로 데이터를 받습니다.</li>
<li>서버는 SseEmitter를 통해 1초 간격으로 데이터를 클라이언트로 푸시합니다.</li>
<li>클라이언트는 onmessage 리스너로 서버에서 전송된 데이터를 화면에 표시합니다.</li>
</ul>
<p><strong>SSE의 장점과 활용 사례</strong>
장점:</p>
<ul>
<li>자동 재연결: 네트워크 문제가 발생해도 클라이언트가 자동으로 재연결을 시도합니다.</li>
<li>단순성: 서버에서 클라이언트로 일방향 실시간 데이터를 쉽게 전송할 수 있습니다.</li>
<li>HTTP 기반: HTTP/2를 사용할 경우 다수의 클라이언트와 연결을 효율적으로 처리할 수 있습니다.</li>
</ul>
<p><strong>활용 사례:</strong></p>
<ul>
<li>실시간 알림 시스템: 사용자에게 실시간 알림을 전송할 수 있습니다.</li>
<li>실시간 대시보드: 서버에서 발생하는 이벤트를 실시간으로 대시보드에 반영할 수 있습니다.</li>
<li>주식 가격 업데이트: 실시간 주식 데이터를 클라이언트로 스트리밍하여 시장 변화를 즉각적으로 반영합니다.</li>
</ul>
<p><strong>결론</strong>
SSE(Server-Sent Events)는 서버에서 클라이언트로 실시간 데이터를 푸시하는 데 매우 유용한 기술입니다. Spring Boot에서는 SseEmitter 클래스를 사용하여 간단하게 구현할 수 있으며, 클라이언트에서는 HTML5의 EventSource API를 통해 서버 데이터를 실시간으로 처리할 수 있습니다. 실시간 데이터 전송이 필요한 다양한 애플리케이션에서 SSE를 활용해 보세요!</p>
<p><strong>참고 자료:</strong></p>
<ul>
<li>MDN - Server-Sent Events</li>
<li>Spring Documentation - SseEmitter</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 프록시 패턴(Proxy Pattern)이란? - 개념 및 예제
]]></title>
            <link>https://velog.io/@kanguk_o/Java-%ED%94%84%EB%A1%9D%EC%8B%9C-%ED%8C%A8%ED%84%B4Proxy-Pattern%EC%9D%B4%EB%9E%80-%EA%B0%9C%EB%85%90-%EB%B0%8F-%EC%98%88%EC%A0%9C</link>
            <guid>https://velog.io/@kanguk_o/Java-%ED%94%84%EB%A1%9D%EC%8B%9C-%ED%8C%A8%ED%84%B4Proxy-Pattern%EC%9D%B4%EB%9E%80-%EA%B0%9C%EB%85%90-%EB%B0%8F-%EC%98%88%EC%A0%9C</guid>
            <pubDate>Sun, 22 Sep 2024 15:19:00 GMT</pubDate>
            <description><![CDATA[<p><strong>프록시 패턴(Proxy Pattern)</strong></p>
<p>프록시(Proxy)는 대리자, 대변인이라는 뜻을 가진 단어다. 대리자/대변인은 다른 누군가를 대신해 그 역할을 수행하는 존재를 말한다. 따라서 프록시 패턴은 특정 객체의 대리자나 대변인 역할을 하는 프록시 객체를 제공하는 디자인 패턴입니다.
프록시 패턴을 사용함으로써 클라이언트는 특정 객체를 직접 참조하여 접근하는 것이 아닌 프록시 객체를 통해 상호작용합니다.</p>
<p><strong>프록시 객체의 장단점</strong>
<strong>장점</strong></p>
<ul>
<li><p>접근 제어 : 클라이언트가 실제 객체에 직접 접근하지 않도록 제어하여 객체의 접근을 관리하고 권한 검사 등을 수행할 수 있습니다.</p>
</li>
<li><p>지연 초기화 : 실제 객체의 생성 및 초기화를 지연시키는 데 사용하여 필요한 순간만에 생성 및 초기화하여 성능을 최적화할 수 있습니다.</p>
</li>
<li><p>캐싱 : 결과를 캐싱하여 중복 계산을 피하고 성능을 향상시킬 수 있습니다.</p>
</li>
<li><p>유효성 검사 : 실제 객체에 접근하기 전에 데이터의 유효성 검사를 통해 검증할 수 있습니다.</p>
</li>
<li><p>원격 액세스 : 원격 프록시를 사용하여 다른 시스템에서 실행 중인 객체에 접근할 수 있으며 분산 시스템에서 객체 간 통신을 용이하게 한습니다.</p>
</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li><p>복잡성 증가 : 추가적인 객체를 도입하기 때문에 코드의 복잡성이 증가할 수 있습니다.</p>
</li>
<li><p>성능 저하 : 프록시 객체에 접근하는 데 추가적인 오버헤드가 발생할 수 있으며, 일부 성능 저하가 발생할 수 있습니다.</p>
</li>
<li><p>디자인 복잡성 : 프록시 패턴을 오용하면 코드를 과도하게 복잡하게 만들 수 있습니다.</p>
</li>
</ul>
<p><strong>프록시 패턴의 구조</strong></p>
<p>프록시 패턴은 클라이언트가 접근할 Subject와 이에 대한 구현체인 RealSubject, Proxy가 존재합니다.
<img src="https://velog.velcdn.com/images/kanguk_o/post/cb173e33-9ff2-41aa-80d5-272e5d829e38/image.png" alt=""></p>
<ul>
<li><p>Subject : Proxy와 RealSubject가 모두 구현하는 인터페이스로 클라이언트가 프록시와 실제 대상을 동일하게 다룰 수 있도록 정의합니다.
RealSubject : 클라이언트가 직접 상호작용하는 실제 객체 입니다.</p>
</li>
<li><p>Proxy : RealSubject를 감싸며 실제 작업을 수행하는 주체로 클라이언트와 RealSubject 사이에 위치한 중간 객체다. RealSubject의 같은 이름의 메서드를 호출하며, 클라이언트의 요청을 처리하기 전이나 후에 추가적인 작업을 수행할 수 있습니다</p>
</li>
</ul>
<p><strong>프록시 패턴 예제</strong></p>
<p>여행 예약 시스템이 있다고 가정합시다.</p>
<p><strong>Reservation.java</strong></p>
<pre><code class="language-java">public class Reservation {
    private int reservationId;
    private String travelerName;
    private String destination;

    public Reservation(int reservationId, String travelerName, String destination) {
        this.reservationId = reservationId;
        this.travelerName = travelerName;
        this.destination = destination;
    }

    public int getReservationId() {
        return reservationId;
    }

    public String getTravelerName() {
        return travelerName;
    }

    public String getDestination() {
        return destination;
    }
}</code></pre>
<ul>
<li>여행의 예약 정보를 포함하는 Reservation 클래스다. 예약 id와 여행자 이름, 목적지 등을 포함하고 있습니다.</li>
<li>여행을 예약하고 조회하기 위한 서비스를 프록시를 적용하지 않은 경우와 적용한 경우로 구현하여 비교해 봅시다.</li>
</ul>
<p><strong>프록시를 사용하지 않은 경우</strong></p>
<p><strong>ReservationService.java</strong></p>
<pre><code class="language-java">import java.util.ArrayList;
import java.util.List;

public class ReservationService {
    private List&lt;Reservation&gt; reservations = new ArrayList&lt;&gt;();

    public void addReservation(Reservation reservation) {
        reservations.add(reservation);
    }

    public Reservation findReservation(int reservationId) {
        for (Reservation reservation : reservations) {
            if (reservation.getReservationId() == reservationId) {
                return reservation;
            }
        }
        return null;
    }
}
</code></pre>
<ul>
<li>ddReservation 메서드를 통해 Reservation 객체를 예약 리스트에 추가하여 여행에 대한 예약을 할 수 있고, findReservation 메서드를 통해 예약된 예약 정보를 조회할 수 있는 간단한 서비스 입니다</li>
</ul>
<p><strong>Main.java</strong></p>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {
        ReservationService reservationService = new ReservationService();

        // 여행 예약 추가
        Reservation reservation = new Reservation(1, &quot;홍길동&quot;, &quot;프랑스 파리&quot;);
        reservationService.addReservation(reservation);

        // 여행 예약 검색
        Reservation findReservation = reservationService.findReservation(1);
        if (findReservation != null) {
            System.out.println(&quot;예약 번호: &quot; + findReservation.getReservationId() +
                    &quot;, 여행자: &quot; + findReservation.getTravelerName() +
                    &quot;, 목적지: &quot; + findReservation.getDestination());
        } else {
            System.out.println(&quot;예약 정보를 찾을 수 없습니다.&quot;);
        }
    }
}</code></pre>
<ul>
<li>위 경우 클라이언트가 여행을 예약하거나 예약된 여행을 조회하기 위해 ReservationService 클래스를 접근하고 있습니다.</li>
<li>이는 클라이언트 코드가 직접 해당 객체에 접근하고 상호작용하는 것입니다.</li>
</ul>
<p><strong>프록시를 사용하는 경우</strong></p>
<p><strong>ReservationServiceSubject.java</strong></p>
<pre><code class="language-java">public interface ReservationServiceSubject {
    void addReservation(Reservation reservation);
    Reservation findReservation(int reservationId);
}</code></pre>
<ul>
<li>클라이언트가 접근할 Subject인 ReservationServiceSubject 인터페이스 입니다.</li>
</ul>
<p><strong>ReservationServiceRealSubject.java</strong></p>
<pre><code class="language-java">import java.util.ArrayList;
import java.util.List;

public class ReservationServiceRealSubject implements ReservationServiceSubject {
    private List&lt;Reservation&gt; reservations = new ArrayList&lt;&gt;();

    public void addReservation(Reservation reservation) {
        reservations.add(reservation);
    }

    public Reservation findReservation(int reservationId) {
        for (Reservation reservation : reservations) {
            if (reservation.getReservationId() == reservationId) {
                return reservation;
            }
        }
        return null;
    }
}</code></pre>
<ul>
<li>예약과 조회 기능이 구현되어 있는 ReservationServiceRealSubject 클래스로 ReservationServiceSubject 인터페이스를 구현합니다.</li>
<li>앞에서 프록시를 사용하지 않는 경우인 ReservationService 클래스와 같은 기능을 합니다.</li>
</ul>
<p><strong>ReservationServiceProxy.java</strong></p>
<pre><code class="language-java">class ReservationServiceProxy implements ReservationServiceSubject {
    private ReservationServiceRealSubject reservationService = new ReservationServiceRealSubject();

    public void addReservation(Reservation reservation) {
        // 여행 예약을 추가하기 전에 어떤 작업을 수행할 수 있음
        reservationService.addReservation(reservation);
    }

    public Reservation findReservation(int reservationId) {
        // 여행 예약을 조회하기 전에 어떤 작업을 수행할 수 있음
        return reservationService.findReservation(reservationId);
    }
}</code></pre>
<ul>
<li>ReservationServiceProxy 클래스는 실제 서비스인 ReservationServiceRealSubject 클래스를 감싼 프록시 객체입니다.</li>
<li>ReservationServiceRealSubject 객체를 가지고 있으며, 여행 예약이나 조회를 하기 전에 특정 작업을 수행하고 realService의 기능을 호출하여 실제 예약 및 조회를 합니다.</li>
<li>주석 부분에는 여행을 예약 및 조회하기 위한 권한 제어, 입력 데이터의 유효성 검사 등을 수행할 수 있습니다.</li>
</ul>
<p><strong>ProxyMain.java</strong></p>
<pre><code class="language-java">public class ProxyMain {
    public static void main(String[] args) {
        ReservationServiceProxy reservationService = new ReservationServiceProxy();

        // 여행 예약 추가
        Reservation reservation = new Reservation(1, &quot;홍길동&quot;, &quot;프랑스 파리&quot;);
        reservationService.addReservation(reservation);

        // 여행 예약 검색
        Reservation findReservation = reservationService.findReservation(1);
        if (findReservation != null) {
            System.out.println(&quot;예약 번호: &quot; + findReservation.getReservationId() +
                    &quot;, 여행자: &quot; + findReservation.getTravelerName() +
                    &quot;, 목적지: &quot; + findReservation.getDestination());
        } else {
            System.out.println(&quot;예약 정보를 찾을 수 없습니다.&quot;);
        }
    }
}</code></pre>
<ul>
<li>위 경우 클라이언트가 여행을 예약하거나 예약된 여행을 조회하기 위해 ReservationServiceProxy 클래스를 접근하고 있습니다.</li>
<li>ReservationServiceProxy의 기능을 통해 특정 작업을 수행하고 예약하거나 조회할 수 있다.</li>
<li>이는 클라이언트 입장에서 요청이 ReservationServiceRealSubject에 접근하는지, 아니면 
ReservationServiceProxy에 접근하는지 알 수 없습니다.</li>
</ul>
<p><strong>정리
프록시 패턴의 중요 포인트는 다음과 같다.</strong></p>
<ul>
<li>프록시는 실제 서비스와 같은 이름의 메서드를 구현하고, 이때 인터페이스를 사용합니다.</li>
<li>프록시는 실제 서비스에 대한 참조 변수를 갖습니다.(합성)</li>
<li>프록시는 실제 서비스의 같은 이름을 가진 메서드를 호출하고 그 값을 클라이언트에게 돌려줍니다.</li>
<li>프록시는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수 있습니다.</li>
</ul>
<p> 
프록시 패턴은 제어 흐름을 조정하기 위한 목적으로 중간에 계층을 도입하여 여러 기능을 추가하거나 제어하면서 실제 서비스를 대신 수행하는 패턴입니다.
 
추가로 앞서 설명한 예제의 구조를 살펴보면, 개방 폐쇄 원칙(OCP)과 의존성 역전 원칙(DIP)를 준수하고 있다. 
프록시 패턴은 OCP와 DIP를 준수하도록 설계된 패턴이기 때문입니다.</p>
<p> 
 </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS S3]]></title>
            <link>https://velog.io/@kanguk_o/AWS-S3</link>
            <guid>https://velog.io/@kanguk_o/AWS-S3</guid>
            <pubDate>Fri, 13 Sep 2024 01:16:52 GMT</pubDate>
            <description><![CDATA[<h2 id="s3-기본-개념"><strong>S3 기본 개념</strong></h2>
<p>AWS S3는 객체 스토리지 서비스로, 데이터를 &quot;버킷(bucket)&quot;에 저장하는 방식입니다. 각 버킷은 사용자가 지정한 이름과 권한을 가지며, 내부에 저장된 데이터는 객체(object)로 관리됩니다. S3는 확장성, 보안, 높은 가용성을 제공하여 다양한 애플리케이션에서 데이터 저장소로 활용됩니다.</p>
<h4 id="주요-기능">주요 기능</h4>
<ul>
<li><p>버킷(Bucket): 파일을 저장하는 기본 단위로, 하나의 버킷 안에 수많은 파일(객체)을 저장할 수 있습니다.</p>
</li>
<li><p>객체(Object): 파일 그 자체로, 메타데이터와 데이터로 이루어져 있습니다.</p>
</li>
<li><p>ACL(Access Control List): 각 객체와 버킷에 대해 접근 권한을 설정할 수 있는 기능입니다.</p>
</li>
<li><p>버전 관리: S3는 파일의 여러 버전을 관리할 수 있습니다.</p>
</li>
<li><p>라이프사이클: 파일을 자동으로 아카이브하거나 삭제할 수 있는 규칙을 설정할 수 있습니다.</p>
</li>
</ul>
<h4 id="2-spring-boot와-aws-s3-연동하기">2. Spring Boot와 AWS S3 연동하기</h4>
<p>Spring Boot와 S3를 연동하기 위해서는 AWS SDK를 사용해야 합니다. Spring Boot는 다양한 AWS 서비스를 쉽게 연동할 수 있도록 라이브러리 및 설정을 지원합니다.</p>
<p>필수 설정 및 라이브러리 추가</p>
<pre><code>implementation &#39;software.amazon.awssdk:s3:2.20.0&#39;
implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;</code></pre><p>**
AWS 자격 증명 설정**
AWS 자격 증명을 설정해야 S3와 통신할 수 있습니다. 기본적으로, ~/.aws/credentials 파일에 자격 증명을 설정할 수 있습니다.</p>
<pre><code>[default]
aws_access_key_id = YOUR_ACCESS_KEY
aws_secret_access_key = YOUR_SECRET_KEY</code></pre><p><strong>S3 클라이언트 설정</strong>
Spring Boot 애플리케이션에서 S3 클라이언트를 설정하는 방법입니다.</p>
<pre><code class="language-java">import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class S3Config {

    @Bean
    public S3Client s3Client() {
        return S3Client.builder()
                .region(Region.AP_NORTHEAST_2) // 서울 리전
                .credentialsProvider(ProfileCredentialsProvider.create())
                .build();
    }
}
</code></pre>
<p>**
S3 파일 업로드 및 다운로드 서비스 구현**</p>
<ul>
<li>파일 업로드 서비스
S3에 파일을 업로드하는 서비스를 구현할 수 있습니다.</li>
</ul>
<pre><code class="language-java">import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;

@Service
public class S3Service {

    private final S3Client s3Client;
    private final String bucketName = &quot;your-bucket-name&quot;;

    public S3Service(S3Client s3Client) {
        this.s3Client = s3Client;
    }

    public void uploadFile(MultipartFile file) throws IOException {
        Path tempFile = Paths.get(System.getProperty(&quot;java.io.tmpdir&quot;), file.getOriginalFilename());

        file.transferTo(tempFile.toFile());

        PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                .bucket(bucketName)
                .key(file.getOriginalFilename())
                .build();

        s3Client.putObject(putObjectRequest, tempFile);
    }
}</code></pre>
<p><strong>+ 파일 다운로드 서비스</strong>
S3에서 파일을 다운로드하는 서비스도 쉽게 구현할 수 있습니다.</p>
<pre><code class="language-java">import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import software.amazon.awssdk.services.s3.S3Client;

@Service
public class S3DownloadService {

    private final S3Client s3Client;
    private final String bucketName = &quot;your-bucket-name&quot;;

    public S3DownloadService(S3Client s3Client) {
        this.s3Client = s3Client;
    }

    public InputStream downloadFile(String fileName) {
        GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                .bucket(bucketName)
                .key(fileName)
                .build();

        return s3Client.getObject(getObjectRequest);
    }
}</code></pre>
<p><strong>컨트롤러 구현</strong>
파일을 업로드하고 다운로드할 수 있는 API를 제공합니다.</p>
<pre><code class="language-java">import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.http.ResponseEntity;
import java.io.IOException;

@RestController
@RequestMapping(&quot;/s3&quot;)
public class S3Controller {

    private final S3Service s3Service;
    private final S3DownloadService s3DownloadService;

    public S3Controller(S3Service s3Service, S3DownloadService s3DownloadService) {
        this.s3Service = s3Service;
        this.s3DownloadService = s3DownloadService;
    }

    @PostMapping(&quot;/upload&quot;)
    public ResponseEntity&lt;String&gt; uploadFile(@RequestParam(&quot;file&quot;) MultipartFile file) {
        try {
            s3Service.uploadFile(file);
            return ResponseEntity.ok(&quot;File uploaded successfully!&quot;);
        } catch (IOException e) {
            return ResponseEntity.status(500).body(&quot;File upload failed!&quot;);
        }
    }

    @GetMapping(&quot;/download/{fileName}&quot;)
    public ResponseEntity&lt;InputStreamResource&gt; downloadFile(@PathVariable String fileName) {
        InputStream fileStream = s3DownloadService.downloadFile(fileName);
        return ResponseEntity.ok(new InputStreamResource(fileStream));
    }
}</code></pre>
<p><strong>3. 보안 및 최적화</strong>
S3와의 연동에서 보안과 비용 최적화를 고려하는 것은 매우 중요합니다.</p>
<ul>
<li><p>IAM 역할 및 정책: 특정 리소스에 대한 권한을 제한하는 정책을 적용해야 합니다. IAM 역할을 사용하여 S3 버킷에 대한 액세스를 최소화하는 것이 좋습니다.</p>
</li>
<li><p>S3 라이프사이클 정책: S3에 저장된 데이터를 비용 효율적으로 관리하기 위해, 데이터를 일정 기간 후 아카이브하거나 삭제하는 라이프사이클 정책을 설정할 수 있습니다.</p>
</li>
<li><p>암호화: S3에 저장된 데이터를 암호화하는 것이 좋습니다. AWS에서 제공하는 서버 측 암호화(SSE)를 활용하면 안전하게 데이터를 보호할 수 있습니다.</p>
</li>
</ul>
<ol start="4">
<li>마무리
이번 글에서는 AWS S3의 기본 개념과 Spring Boot를 이용한 S3 연동 방법을 살펴보았습니다. AWS S3는 확장성과 보안성을 제공하기 때문에 다양한 애플리케이션에서 데이터 저장소로 활용할 수 있습니다. Spring Boot 애플리케이션에서 S3와 연동하면 파일 업로드 및 다운로드 기능을 쉽게 구현할 수 있으며, 비용 최적화와 보안을 고려하여 설계할 수 있습니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot에서 AWS Lambda를 사용하는 경우: 서비스 활용 사례]]></title>
            <link>https://velog.io/@kanguk_o/Spring-Boot%EC%97%90%EC%84%9C-AWS-Lambda%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EA%B2%BD%EC%9A%B0-%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%99%9C%EC%9A%A9-%EC%82%AC%EB%A1%80</link>
            <guid>https://velog.io/@kanguk_o/Spring-Boot%EC%97%90%EC%84%9C-AWS-Lambda%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EA%B2%BD%EC%9A%B0-%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%99%9C%EC%9A%A9-%EC%82%AC%EB%A1%80</guid>
            <pubDate>Thu, 12 Sep 2024 06:32:01 GMT</pubDate>
            <description><![CDATA[<p>AWS Lambda는 서버리스(Serverless) 아키텍처에서 핵심적인 컴퓨팅 서비스로, 코드를 실행할 때만 리소스를 할당하고 비용을 청구하여 매우 경제적입니다. Spring Boot는 강력한 애플리케이션 프레임워크로, Java 개발자들이 효율적으로 웹 애플리케이션을 개발하는 데 자주 사용됩니다. 이 두 가지 기술을 결합하면, 서버 관리 없이도 복잡한 비즈니스 로직을 처리하고 확장성 있는 서버리스 애플리케이션을 개발할 수 있습니다.</p>
<p>이 블로그에서는 Spring Boot와 AWS Lambda를 함께 사용하는 서비스 사례를 알아보고, 이를 통해 어떤 이점이 있는지 살펴보겠습니다.</p>
<h3 id="aws-lambda와-spring-boot의-통합"><strong>AWS Lambda와 Spring Boot의 통합</strong></h3>
<p>Spring Boot 애플리케이션은 보통 웹 서버나 클라우드 인프라에서 지속적으로 실행되는 서비스로 동작합니다. 반면, AWS Lambda는 요청이 있을 때만 코드를 실행하는 서버리스 방식입니다. AWS Lambda는 컴퓨팅 리소스를 자동으로 관리해주기 때문에, 트래픽 변동이 많거나 자주 실행되지 않는 작업에 적합합니다.</p>
<p>Spring Boot와 AWS Lambda의 통합은 Spring Cloud Function 라이브러리를 사용하여 간편하게 이뤄질 수 있습니다. Spring Cloud Function을 통해 Spring의 익숙한 의존성 주입, 트랜잭션 관리 등을 Lambda 함수에서도 사용할 수 있습니다.</p>
<ul>
<li>주요 서비스 사례</li>
<li>비동기 처리 및 이벤트 기반 서비스</li>
<li>이미지 및 파일 처리</li>
<li>알림 시스템</li>
<li>백엔드 API 및 마이크로서비스</li>
<li>사용자 데이터 분석 및 로그 처리</li>
</ul>
<h4 id="1-비동기-처리-및-이벤트-기반-서비스">1. 비동기 처리 및 이벤트 기반 서비스</h4>
<p>AWS Lambda는 이벤트 기반으로 동작하는 서비스와 매우 잘 맞습니다. Spring Boot 애플리케이션에서 AWS Lambda를 사용하면, 이벤트가 발생할 때만 특정 로직을 실행할 수 있어 매우 효율적입니다. 이와 같은 이벤트 기반 작업으로는 다음과 같은 서비스가 있습니다:</p>
<ul>
<li><p>S3 파일 업로드 후 이미지 처리: 사용자가 업로드한 이미지를 AWS S3에 저장하면, S3 이벤트가 Lambda를 트리거하여 이미지 크기를 조정하거나 썸네일을 생성하는 작업을 실행할 수 있습니다.</p>
</li>
<li><p>SNS/SQS를 통한 알림 서비스: SNS 주제를 구독하고, SQS 큐에 메시지를 전달하는 방식으로 비동기 알림을 처리할 수 있습니다. 거래 플랫폼에서 새로운 메시지나 댓글이 달렸을 때 사용자에게 알림을 전송하는 기능을 Lambda로 구현하면 서버 자원을 효율적으로 사용할 수 있습니다.</p>
</li>
</ul>
<pre><code class="language-java">import org.springframework.cloud.function.adapter.aws.SpringBootRequestHandler;
import org.springframework.stereotype.Component;

@Component
public class S3EventHandler extends SpringBootRequestHandler&lt;String, String&gt; {

    public String handleRequest(String input) {
        // S3 파일 업로드 이벤트 처리 로직
        return &quot;S3 이벤트 처리 완료: &quot; + input;
    }
}</code></pre>
<p>이 코드에서는 S3 이벤트가 발생할 때마다 Lambda가 트리거되어 파일 업로드에 따른 비즈니스 로직을 처리합니다.</p>
<h4 id="2이미지-및-파일-처리">2.이미지 및 파일 처리</h4>
<p>이미지 및 파일 처리는 거래 플랫폼이나 전자상거래 시스템에서 자주 발생하는 작업입니다. Spring Boot와 AWS Lambda를 결합하여 이미지를 자동으로 처리하는 시스템을 구축할 수 있습니다.</p>
<p>예를 들어, 중고 거래 플랫폼에서는 사용자가 상품 이미지를 업로드하면 이를 크기 조정하고 최적화하는 작업이 필요합니다. AWS Lambda는 이미지 업로드와 같은 이벤트가 발생할 때만 트리거되므로, 트래픽이 많지 않은 경우에도 서버 리소스를 지속적으로 소비하지 않고 처리할 수 있습니다.</p>
<h4 id="사용-사례">사용 사례:</h4>
<ul>
<li>이미지 썸네일 생성: 사용자가 업로드한 상품 이미지의 썸네일을 자동으로 생성하고, 이 이미지를 S3에 저장.</li>
<li>파일 확장자 검증: 사용자가 잘못된 확장자의 파일을 업로드했을 때 이를 필터링하여 오류 메시지를 반환.</li>
</ul>
<h4 id="3-알림-시스템">3. 알림 시스템</h4>
<p>비즈니스에서 알림 서비스는 매우 중요한 기능 중 하나입니다. 거래 플랫폼에서는 사용자가 상품에 대한 댓글이나 메시지를 받았을 때 알림을 보내는 작업이 빈번하게 발생합니다. AWS Lambda는 SNS나 SQS와 같은 서비스와 함께 작동하여 비동기적으로 알림 메시지를 전달하는 데 적합합니다.</p>
<p>예를 들어, 사용자가 관심 상품에 댓글을 남기면 Lambda가 자동으로 트리거되어 푸시 알림을 발송하거나 이메일을 전송할 수 있습니다. 이러한 작업은 요청이 있을 때만 발생하므로, 상시 실행되는 서버보다 Lambda의 활용이 비용 효율적입니다.</p>
<h4 id="4-백엔드-api-및-마이크로서비스">4. 백엔드 API 및 마이크로서비스</h4>
<p>AWS Lambda는 Spring Boot의 일부 백엔드 API를 서버리스 방식으로 처리하는 데 매우 적합합니다. 특히 마이크로서비스 아키텍처에서 Lambda는 특정 기능을 독립적으로 분리하여 처리할 수 있어 유지보수와 확장성 측면에서 장점이 큽니다.</p>
<p>Lambda 함수는 API Gateway와 연동되어 HTTP 요청을 처리할 수 있으며, 특정 작업을 위해 비즈니스 로직을 실행하고, 필요 시 Spring Boot 애플리케이션에 데이터를 저장하거나 반환하는 구조로 동작합니다.</p>
<pre><code class="language-java">import java.util.function.Function;
import org.springframework.stereotype.Component;

@Component
public class ApiHandler implements Function&lt;String, String&gt; {

    @Override
    public String apply(String input) {
        return &quot;처리된 데이터: &quot; + input;
    }
}</code></pre>
<p>이 코드는 간단한 API 요청을 처리하여 응답을 반환하는 Lambda 함수의 예시입니다. API Gateway와 연동하면 HTTP 요청이 발생할 때마다 Lambda가 동작하게 됩니다.</p>
<h4 id="5-사용자-데이터-분석-및-로그-처리">5. 사용자 데이터 분석 및 로그 처리</h4>
<p>AWS Lambda는 대규모 데이터를 처리하거나 분석하는 작업에 유용하게 사용됩니다. 예를 들어, 중고 거래 플랫폼에서 사용자의 검색 기록이나 거래 내역을 분석하여 개인화된 추천 시스템을 구현할 수 있습니다. 이 경우 Lambda를 이용해 정기적으로 로그 데이터를 처리하거나 분석하는 작업을 수행할 수 있습니다.</p>
<p>CloudWatch Logs나 S3에 저장된 로그 데이터를 Lambda에서 분석하여, 사용자의 행동 패턴을 파악하고 그에 맞춘 상품 추천이나 마케팅을 진행할 수 있습니다.</p>
<p>사용 사례:</p>
<ul>
<li>사용자 활동 로그 분석: 정기적으로 사용자 활동 로그를 분석하고, 비즈니스 인사이트를 도출.</li>
<li>맞춤형 추천 시스템: 사용자의 검색 이력을 기반으로 개인화된 상품 추천 제공.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot & Redis] - JWT 로그아웃 구현하기]]></title>
            <link>https://velog.io/@kanguk_o/Spring-Boot-Redis-JWT-%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kanguk_o/Spring-Boot-Redis-JWT-%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 02 Sep 2024 08:56:19 GMT</pubDate>
            <description><![CDATA[<p>JWT (JSON Web Token)를 이용한 인증 시스템에서 로그아웃 기능을 구현하는 방법 중 하나는 Redis를 사용하는 것입니다. 이 글에서는 Spring Boot와 Redis를 사용하여 JWT 로그아웃을 구현하는 방법에 대해 다룹니다.</p>
<p><strong>1. JWT 로그아웃의 필요성</strong></p>
<p>JWT는 서버가 상태를 저장하지 않는 Stateless 인증 방식입니다. 클라이언트는 JWT를 발급받아 이후 요청 시마다 이를 서버에 전달합니다. 하지만, 로그아웃을 하려면 발급된 JWT를 무효화해야 합니다. 그렇지 않으면 JWT의 유효 기간이 끝날 때까지 해당 토큰으로 계속 인증을 할 수 있습니다.</p>
<p><strong>2. Redis를 사용한 JWT 무효화 방법</strong></p>
<p>Redis는 매우 빠른 인메모리 데이터 저장소로, JWT의 무효화 정보를 저장하는 데 적합합니다. 로그아웃 시, 해당 JWT를 Redis에 저장하고, 이후 요청 시 이 토큰이 Redis에 있는지 확인하여 유효성을 판단할 수 있습니다.</p>
<p><strong>3. Spring Boot 프로젝트 설정</strong></p>
<p>3.1. 의존성 추가</p>
<p>먼저 build.gradle 파일에 Redis와 Spring Security, JWT 관련 의존성을 추가합니다.</p>
<pre><code>dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;
    implementation &#39;io.jsonwebtoken:jjwt:0.9.1&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-validation&#39;
}</code></pre><p>application.yml 파일에 Redis 설정을 추가합니다.</p>
<pre><code>spring:
  redis:
    host: localhost
    port: 6379</code></pre><ol start="4">
<li>JWT 로그아웃 구현</li>
</ol>
<p>4.1. 로그아웃 서비스 구현</p>
<p>JWT 로그아웃 시 토큰을 Redis에 저장하는 LogoutService 클래스를 만듭니다.</p>
<pre><code class="language-java">  @Override
    public String logout(String accessToken) {
        String email = jwtUtils.getUserEmailFromToken(accessToken);
        // RefreshToken 삭제
        redisTemplate.delete(&quot;RT:&quot; + email);
        return null;
    }</code></pre>
<ol>
<li>String email = jwtUtils.getUserEmailFromToken(accessToken);</li>
</ol>
<p>이 라인은 전달받은 AccessToken에서 사용자의 이메일 주소를 추출합니다.</p>
<p>jwtUtils.getUserEmailFromToken(accessToken): JwtUtils 클래스의 메서드를 호출하여, JWT 토큰에서 사용자 정보를 파싱하여 이메일 주소를 추출합니다.</p>
<ol start="2">
<li>redisTemplate.delete(&quot;RT:&quot; + email);</li>
</ol>
<p>이 부분은 추출한 이메일 주소를 키로 사용하여, Redis에 저장된 해당 사용자의 RefreshToken을 삭제합니다.</p>
<p>Redis: Redis는 인메모리 데이터 저장소로, 토큰 정보를 빠르게 저장하고 조회하는 데 사용됩니다.
redisTemplate.delete(“RT:” + email): Redis에서 RT:email 키로 저장된 값을 삭제합니다. </p>
<p>여기서 RT:는 Redis에서 RefreshToken을 구분하기 위한 접두사로 사용됩니다.
RefreshToken 삭제의 의미: 로그아웃 시 사용자의 RefreshToken을 삭제함으로써 해당 사용자가 더 이상 유효한 AccessToken을 재발급받을 수 없도록 만듭니다. 즉, 로그아웃한 사용자는 다시 로그인을 하지 않는 한, 새로운 AccessToken을 받을 수 없습니다.</p>
<p>3.return null;</p>
<p>이 부분은 메서드의 반환 타입이 String이지만, 특별히 반환할 값이 없음을 의미합니다. 실제로 로그아웃 작업이 성공적으로 수행된 경우, 반환할 값이 없기 때문에 null을 반환합니다.</p>
<p>4.2. JWT 유효성 검사 필터</p>
<p>로그아웃된 토큰을 체크하는 JWT 필터를 추가합니다.</p>
<pre><code class="language-java"> @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String authHeader = request.getHeader(JwtUtils.AUTHORIZATION_HEADER);

        if (authHeader != null &amp;&amp; authHeader.startsWith(JwtUtils.BEARER_PREFIX)) {
            String token = authHeader.substring(JwtUtils.BEARER_PREFIX.length());
            log.info(&quot;token : {}&quot;, token);
            try {
                if (jwtUtils.validateToken(token)) {
                    String email = jwtUtils.getUserEmailFromToken(token);
                    // RefreshToken 존재 여부 확인
                    String refreshToken = redisTemplate.opsForValue().get(&quot;RT:&quot; + email);
                    if (refreshToken == null) {
                        throw new JwtException(&quot;로그아웃된 사용자입니다.&quot;);
                    }
                    // 요청 메서드가 PUT 또는 DELETE일 때만 자신의 이메일 확인
                    if ((&quot;PUT&quot;.equalsIgnoreCase(request.getMethod()) || &quot;DELETE&quot;.equalsIgnoreCase(request.getMethod()))) {
                        String requestedEmail = request.getParameter(&quot;email&quot;); // URL 파라미터에서 이메일 추출
                        if (requestedEmail == null || !requestedEmail.equalsIgnoreCase(email)) {
                            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                            response.getWriter().write(&quot;접근이 거부되었습니다. 자신의 이메일이 아닙니다.&quot;);
                            return;
                        }
                    }
                    // 인증된 사용자 및 역할을 요청 속성에 설정
                    request.setAttribute(&quot;AuthenticatedUser&quot;, email);
                } else {
                    throw new JwtException(&quot;유효하지 않거나 이미 만료된 토큰입니다.&quot;);
                }
            } catch (JwtException e) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().write(&quot;JWT 인증 실패: &quot; + e.getMessage());
                return;
            }
        } else if (!isExcludedPath(request.getRequestURI())) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.getWriter().write(&quot;토큰이 누락되었습니다.&quot;);
            return;
        }
        filterChain.doFilter(request, response);
    }</code></pre>
<ol start="5">
<li>로그아웃 컨트롤러</li>
</ol>
<p>로그아웃 요청을 처리하는 API를 작성합니다.</p>
<pre><code class="language-java">@PostMapping(&quot;/logout&quot;)
    public ResponseEntity&lt;String&gt; logout(@RequestHeader(&quot;Authorization&quot;) String token) {
        String expiredToken = userService.logout(token.replace(JwtUtils.BEARER_PREFIX, &quot;&quot;));
        return ResponseEntity.ok()
                .header(JwtUtils.AUTHORIZATION_HEADER, JwtUtils.BEARER_PREFIX + expiredToken)
                .body(&quot;로그아웃 되었습니다.&quot;);
    }
</code></pre>
<p>Redis를 활용하여 JWT 기반 인증 시스템에서 로그아웃 기능을 구현하는 방법을 알아보았습니다. 이 방법은 토큰의 만료 시간까지 로그아웃된 토큰을 추적할 수 있어, 사용자가 로그아웃한 후에도 안전하게 시스템을 보호할 수 있습니다. Redis의 빠른 성능 덕분에 성능 저하 없이 무효화된 토큰을 관리할 수 있습니다.</p>
<p>참고 자료
Spring Boot 공식 문서 <a href="https://spring.io/projects/spring-boot">https://spring.io/projects/spring-boot</a>
Redis 공식 문서 <a href="https://redis.io/docs/latest/">https://redis.io/docs/latest/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[면접 준비 step1]]></title>
            <link>https://velog.io/@kanguk_o/%EB%A9%B4%EC%A0%91-%EC%A4%80%EB%B9%84-step1</link>
            <guid>https://velog.io/@kanguk_o/%EB%A9%B4%EC%A0%91-%EC%A4%80%EB%B9%84-step1</guid>
            <pubDate>Fri, 30 Aug 2024 04:59:20 GMT</pubDate>
            <description><![CDATA[<h3 id="1-restful한-api를-설계하는-장점은">1. RESTful한 API를 설계하는 장점은?</h3>
<p>RESTful API는 HTTP 표준을 활용해 클라이언트-서버 간 통신을 단순화하고 확장성을 제공합니다. 상태 비저장성으로 인해 서버 부하가 적고, 클라이언트는 독립적으로 서버와 통신할 수 있습니다.</p>
<ul>
<li><p><strong>RESTful API가 상태 비저장성을 유지하는 이유는 무엇인가요?</strong></p>
<ul>
<li>각 요청이 독립적으로 처리되어 서버는 클라이언트의 이전 상태를 기억할 필요가 없기 때문에 확장성과 안정성이 높아집니다.</li>
</ul>
</li>
<li><p><strong>RESTful API에서 HTTP 메서드의 역할은 각각 무엇인가요?</strong></p>
<ul>
<li><code>GET</code>, <code>POST</code>, <code>PUT</code>, <code>DELETE</code>와 같은 메서드는 각각 자원의 조회, 생성, 수정, 삭제를 의미하여 자원에 대한 명확한 행동을 정의합니다.</li>
</ul>
</li>
<li><p><strong>RESTful API 설계에서 URI의 가독성과 일관성을 유지하는 방법은 무엇인가요?</strong></p>
<ul>
<li>URI는 명사 기반으로 설계하고, 일관된 경로 구조와 소문자를 사용하여 유지보수와 가독성을 높입니다.</li>
</ul>
</li>
</ul>
<h3 id="2-적절한-관심사-분리가-필요한-이유는">2. 적절한 관심사 분리가 필요한 이유는?</h3>
<p>관심사 분리는 코드의 모듈화와 유지보수를 쉽게 하여 변경 시 다른 부분에 미치는 영향을 최소화합니다. 이를 통해 팀 간 협업이 원활해지고, 코드의 재사용성이 높아집니다.</p>
<ul>
<li><p><strong>관심사 분리가 코드 유지보수에 어떻게 도움이 되나요?</strong></p>
<ul>
<li>코드의 특정 부분이 변경될 때, 그 변경이 다른 부분에 미치는 영향을 최소화하여 수정과 확장이 용이합니다.</li>
</ul>
</li>
<li><p><strong>관심사 분리가 객체지향 설계 원칙(SOLID)과 어떻게 관련이 있나요?</strong></p>
<ul>
<li>단일 책임 원칙(SRP)은 관심사 분리의 핵심으로, 코드의 각 모듈이 하나의 책임만 가지도록 하여 가독성과 유지보수성을 높입니다.</li>
</ul>
</li>
<li><p><strong>관심사 분리를 통해 코드의 재사용성을 어떻게 높일 수 있나요?</strong></p>
<ul>
<li>모듈화된 코드는 독립적으로 동작할 수 있어, 다른 프로젝트나 시스템에서 쉽게 재사용할 수 있습니다.</li>
</ul>
</li>
</ul>
<h3 id="3-setter를-무분별하게-사용하면-안되는-이유는">3. Setter를 무분별하게 사용하면 안되는 이유는?</h3>
<p>Setter의 과도한 사용은 객체의 캡슐화를 약화시켜 데이터의 무결성을 해치고, 객체 간 결합도를 높여 유지보수를 어렵게 만듭니다. 또한, 불변성을 깨트려 예측 가능성을 저하시킬 수 있습니다.</p>
<ul>
<li><p><strong>객체지향 프로그래밍에서 캡슐화의 개념과 Setter의 관계는 무엇인가요?</strong></p>
<ul>
<li>캡슐화는 객체의 내부 상태를 보호하며, Setter는 외부에서 그 상태를 변경할 수 있게 하여 캡슐화를 약화시킬 수 있습니다.</li>
</ul>
</li>
<li><p><strong>Setter를 과도하게 사용하면 객체의 불변성에 어떤 영향을 미치나요?</strong></p>
<ul>
<li>Setter는 객체의 상태를 외부에서 변경할 수 있게 하여, 불변성을 유지하지 못하게 하고 상태 일관성을 저해할 수 있습니다.</li>
</ul>
</li>
<li><p><strong>Setter를 남용하면 코드의 결합도가 높아지는 이유는 무엇인가요?</strong></p>
<ul>
<li>Setter를 통해 외부에서 객체의 상태를 직접 변경하면, 객체 간 의존성이 증가하여 코드의 결합도가 높아집니다.</li>
</ul>
</li>
</ul>
<h3 id="4-nosql과-rdbms에-대해-아는대로-설명해주세요">4. NoSQL과 RDBMS에 대해 아는대로 설명해주세요.</h3>
<p>RDBMS는 관계형 데이터를 정규화된 테이블 구조로 관리하며, 트랜잭션을 통해 일관성과 무결성을 보장합니다. NoSQL은 비정형 데이터와 대규모 데이터를 처리하는 데 유연하며, 수평 확장성이 뛰어납니다.</p>
<ul>
<li><p><strong>NoSQL 데이터베이스의 주요 유형은 무엇인가요?</strong></p>
<ul>
<li>문서형, 키-값, 열 기반, 그래프 데이터베이스 등 다양한 유형이 있으며, 각각 특정 데이터 구조에 최적화되어 있습니다.</li>
</ul>
</li>
<li><p><strong>RDBMS가 관계형 데이터베이스를 효율적으로 관리할 수 있는 이유는 무엇인가요?</strong></p>
<ul>
<li>ACID 특성을 통해 데이터 일관성을 유지하고, SQL을 사용해 복잡한 쿼리를 효율적으로 처리합니다.</li>
</ul>
</li>
<li><p><strong>NoSQL 데이터베이스가 수평 확장성을 잘 지원하는 이유는 무엇인가요?</strong></p>
<ul>
<li>데이터가 분산된 노드에 저장되며, 노드를 추가하는 것만으로 성능을 확장할 수 있어 대규모 데이터를 효율적으로 처리합니다.</li>
</ul>
</li>
</ul>
<h3 id="5-객체지향-프로그래밍이란-무엇이고-어떻게-활용할-수-있나요">5. 객체지향 프로그래밍이란 무엇이고 어떻게 활용할 수 있나요?</h3>
<p>객체지향 프로그래밍(OOP)은 객체와 클래스 개념을 활용하여 코드의 재사용성과 유지보수성을 높이는 프로그래밍 패러다임입니다. 상속, 다형성, 캡슐화, 추상화를 통해 복잡한 시스템을 효과적으로 관리할 수 있습니다.</p>
<ul>
<li><p><strong>객체지향 프로그래밍(OOP)의 네 가지 주요 원칙은 무엇인가요?</strong></p>
<ul>
<li>캡슐화, 상속, 다형성, 추상화로, 각각 객체의 데이터 보호, 코드 재사용성, 유연성, 복잡도 관리를 도와줍니다.</li>
</ul>
</li>
<li><p><strong>OOP에서 클래스와 객체의 차이점은 무엇인가요?</strong></p>
<ul>
<li>클래스는 객체를 생성하기 위한 설계도이며, 객체는 클래스에 기반하여 생성된 구체적인 실체입니다.</li>
</ul>
</li>
<li><p><strong>OOP의 상속이 코드를 재사용하는 데 어떻게 도움을 주나요?</strong></p>
<ul>
<li>상속을 통해 자식 클래스는 부모 클래스의 속성과 메서드를 물려받아 코드 중복을 줄이고 재사용성을 높입니다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[이펙티브 자바] 정적팩토리 메소드 와 생성자]]></title>
            <link>https://velog.io/@kanguk_o/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EC%A0%95%EC%A0%81%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%86%8C%EB%93%9C-%EC%99%80-%EC%83%9D%EC%84%B1%EC%9E%90</link>
            <guid>https://velog.io/@kanguk_o/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EC%A0%95%EC%A0%81%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%86%8C%EB%93%9C-%EC%99%80-%EC%83%9D%EC%84%B1%EC%9E%90</guid>
            <pubDate>Wed, 28 Aug 2024 07:55:58 GMT</pubDate>
            <description><![CDATA[<p>Java에서 객체를 생성하는 방법에는 여러 가지가 있습니다. 그 중 대표적인 방법은 <strong>생성자</strong>를 사용하는 것이며, <strong>정적 팩토리 메소드</strong>를 활용하는 것도 널리 사용됩니다. 정적 팩토리 메소드 중에서 <code>from</code>, <code>of</code>, <code>to</code> 메소드가 자주 사용됩니다. 정적 팩토리 메소드와 생성자의 차이점을 비교하여 설명하겠습니다.</p>
<h3 id="생성자constructor">생성자(Constructor)</h3>
<p>생성자는 클래스의 인스턴스를 생성하는 데 사용되는 특별한 메소드입니다. 주로 다음과 같은 특징이 있습니다.</p>
<ul>
<li><p><strong>클래스명과 동일한 이름</strong>: 생성자는 클래스의 이름과 같으며 반환 타입이 없습니다.</p>
</li>
<li><p><strong>단순한 객체 생성</strong>: 생성자는 객체를 초기화하는 기본적인 방법으로 널리 사용됩니다.</p>
</li>
<li><p><strong>상속 및 오버로딩 지원</strong>: 생성자는 오버로딩(같은 이름의 생성자를 여러 개 정의할 수 있음)을 지원합니다.</p>
</li>
<li><p><strong>명확한 초기화</strong>: 생성자는 객체를 생성하는 즉시 초기화 코드를 실행할 수 있습니다.</p>
</li>
</ul>
<p><strong>예제:</strong></p>
<pre><code class="language-java">public class Person {
    private String name;
    private int age;

    // 생성자
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}</code></pre>
<h3 id="정적-팩토리-메소드-static-factory-methods">정적 팩토리 메소드 (Static Factory Methods)</h3>
<p>정적 팩토리 메소드는 클래스의 인스턴스를 생성하는 방법 중 하나로, 객체를 반환하는 정적 메소드입니다. from, of, to와 같은 메소드는 다양한 방식으로 객체를 생성하는 데 사용됩니다.</p>
<p>*<em>from 메소드
*</em>
•    용도: 주로 다른 타입의 객체나 데이터를 기반으로 새로운 객체를 생성할 때 사용됩니다.</p>
<p>•    장점: 가독성을 높이고, 다양한 타입의 인스턴스를 쉽게 생성할 수 있습니다.</p>
<p><strong>예제:</strong></p>
<pre><code class="language-java">public class Date {
    private int day;
    private int month;
    private int year;

    // 생성자가 아닌 정적 팩토리 메소드
    public static Date from(String dateString) {
        // 문자열을 파싱하여 Date 객체 생성
        String[] parts = dateString.split(&quot;-&quot;);
        return new Date(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2]));
    }
}</code></pre>
<p><strong>of 메소드</strong></p>
<p>•용도: 값을 받아 객체를 생성할 때 사용됩니다. 간결하고 명확한 방법으로 객체를 생성할 수 있습니다.</p>
<p>•유연성: 다양한 파라미터 조합을 지원하거나, 다른 방법으로 객체를 생성할 수 있습니다. 예를 들어, Color.of(int red, int green, int blue)는 다양한 색상 객체를 생성할 수 있습니다.</p>
<p>•장점: 코드의 의도를 명확히 하고, 객체 생성 시 명확한 입력 값을 받을 수 있습니다.</p>
<pre><code class="language-java">public class Color {
    private int red;
    private int green;
    private int blue;

    // 정적 팩토리 메소드
    public static Color of(int red, int green, int blue) {
        return new Color(red, green, blue);
    }
}</code></pre>
<p><strong>to 메소드</strong></p>
<p>•    용도: 특정 형식의 데이터를 변환하거나 다른 타입으로 변환할 때 사용됩니다. 보통 기존 객체를 다른 형식으로 변환할 때 사용됩니다.</p>
<p>•    장점: 변환 로직을 캡슐화하여 코드의 유지보수를 용이하게 합니다.</p>
<p><strong>예제:</strong></p>
<pre><code class="language-java">public class Person {
    private String name;
    private int age;

    // 정적 팩토리 메소드
    public static Person fromUser(User user) {
        return new Person(user.getName(), user.getAge());
    }
}</code></pre>
<h3 id="정적-팩토리-메소드-비교">정적 팩토리 메소드 비교</h3>
<p><strong>장점</strong></p>
<p><strong>생성자</strong>
•    직관성: 객체 생성의 기본적인 방법으로 직관적입니다.</p>
<p>•    명확성: 생성자의 파라미터를 통해 객체의 상태를 초기화합니다.
    <br>
<strong>정적 팩토리 메소드</strong>
    •    이름 지정 가능: 메소드 이름을 통해 객체 생성의 의도를 명확히 할 수 있습니다 (from, of, to 등).
•    유연성: 다양한 파라미터 조합을 지원하거나 다른 방식으로 객체를 생성할 수 있습니다.</p>
<p>•    캡슐화: 객체 생성 로직을 메소드 내부에 캡슐화하여 코드의 유지보수를 용이하게 합니다.</p>
<p>•    싱글턴 패턴 적용 가능: 정적 팩토리 메소드를 활용하여 싱글턴 패턴과 같은 패턴을 쉽게 구현할 수 있습니다.
<br>
<strong>단점</strong>
<br></p>
<p><strong>생성자</strong>
•    제한된 이름: 생성자는 클래스명과 동일하기 때문에 생성자 이름을 다양하게 설정할 수 없습니다.</p>
<p>•    코드 중복: 동일한 파라미터를 가진 여러 생성자가 있을 경우, 코드 중복이 발생할 수 있습니다.
    <br>
<strong>정적 팩토리 메소드</strong>
•    추적 어려움: 메소드 이름에 따라 객체 생성의 로직이 다를 수 있어, 코드의 추적이 어려울 수 있습니다.</p>
<p>•    상속 제한: 정적 팩토리 메소드는 상속할 수 없습니다.</p>
<br>

<h2 id="성능-비교"><strong>성능 비교</strong></h2>
<p>case 1: 객체의 생성자를 사용하여 반환 하는 경우
<img src="https://velog.velcdn.com/images/kanguk_o/post/fa4ff46b-9dcc-4e16-91d8-f56955c66e9e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kanguk_o/post/fd1942ad-39bb-4fbf-a1d8-d2a554d6f2af/image.png" alt=""></p>
<p>case 2: 정적 팩토리 메소드를 사용하여 반환 하는경우 
<img src="https://velog.velcdn.com/images/kanguk_o/post/01f66ce3-5952-4f81-b875-a97177b572f3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kanguk_o/post/fc4aa5ed-a3a5-4b38-b755-4cd25490196f/image.png" alt=""></p>
<h3 id="결론">결론</h3>
<p>성능 측면에서는 일반적인 객체 생성에서는 생성자와 정적 팩토리 메소드의 성능 차이는 미미할 수 있습니다. 그러나 복잡한 생성 로직이 필요하거나 객체 생성을 최적화해야 하는 경우, 정적 팩토리 메소드가 더 유리할 수 있습니다.</p>
<p>명확성 및 유지보수 측면에서는 정적 팩토리 메소드는 객체 생성의 의도를 명확히 하고, 복잡한 생성 로직을 캡슐화할 수 있어 유지보수 측면에서 장점을 제공합니다. </p>
<p>생성자는 직관적이며 기본적인 객체 생성에 적합하기 때문에 개발 할때 상황에 맞게 생성자와 정적 팩토리 메소드를 선택하여 사용하면 될것 같습니다.</p>
]]></description>
        </item>
    </channel>
</rss>