<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>aya_milk</title>
        <link>https://velog.io/</link>
        <description>우유가 넘어지면 아야</description>
        <lastBuildDate>Wed, 24 Jun 2026 05:50:39 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>aya_milk</title>
            <url>https://velog.velcdn.com/images/aya-milk/profile/a6191a83-b1d7-4449-8f93-b88c939d967b/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. aya_milk. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/aya-milk" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[| Spring WebFlux | Handler와 Router]]></title>
            <link>https://velog.io/@aya-milk/Spring-WebFlux-Handler%EC%99%80-Router</link>
            <guid>https://velog.io/@aya-milk/Spring-WebFlux-Handler%EC%99%80-Router</guid>
            <pubDate>Wed, 24 Jun 2026 05:50:39 GMT</pubDate>
            <description><![CDATA[<p>Spring WebFlux는 MVC와 동일하게 Front-Controller 방식으로 설계되어 있습니다. 요청을 처리하는 흐름을 알아보겠습니다.</p>
<hr>
<h2 id="handler">Handler</h2>
<h3 id="dispatchhandler">DispatchHandler</h3>
<p><img src="https://velog.velcdn.com/images/aya-milk/post/d7dd9607-150e-4ca3-89f3-d358c97445d9/image.png" alt="DispatchHandler 동작 구조"></p>
<ul>
<li>HandlerMapping<ul>
<li>요청 URL을 기반으로 처리할 핸들러를 조회한다.</li>
</ul>
</li>
<li>HandlerAdapter<ul>
<li>핸들러를 실행할 수 있는 어댑터를 조회한다.</li>
</ul>
</li>
<li>HandlerResultHandler<ul>
<li>HandlerResult를 처리하여 응답을 생성하는 핸들러 체인</li>
</ul>
</li>
</ul>
<h3 id="exception">Exception</h3>
<p><code>HandlerAdapter</code> 구현체는 요청을 처리하는 중에 발생한 예외를 내부적으로 처리할 수 있습니다. 비동기 값을 반환하는 경우에는 처리가 지연될 수 있습니다.</p>
<hr>
<h3 id="return-value">Return Value</h3>
<p><code>Flux</code>같은 경우, 반환 값이 여러 개로 예상될 때 사용 가능합니다. 원소들은 스트리밍되며, 버퍼링되지 않습니다. 기본적으로 많은 수의 원소를 다루게 된다면 메모리 효율성이 떨어지기 때문입니다.
<code>application/json+stream</code>을 사용하면 개별적으로 기록되고, 플러시됩니다.</p>
<blockquote>
<p>JSON으로 요소를 인코딩하는 동안, 오류가 발생하면
잘못된 값으로 기록되고, 커밋되었을 수 있습니다.
경우에 따라, 요소를 버퍼링하여 오류가 없음을 체크하고
한번에 방출하는 방식이 있을 수 있습니다.
<code>Flux&lt;List&lt;Object\&gt;&gt;</code></p>
</blockquote>
<p>API 서버의 경우,</p>
<ul>
<li><code>Flux&lt;ServerSentEvent&gt;</code></li>
<li><code>Observable&lt;ServerSentEvent&gt;</code></li>
</ul>
<hr>
<h2 id="router">Router</h2>
<p><code>Router</code>는 Spring WebFlux에서 제공하는 람다 기반의 새로운 Controller 구현 방식이다.</p>
<p><code>RouterFunction</code>은 <code>HandlerFunction</code>을 호출하는 역할입니다. MVC에서 사용하는 <code>@RequestMapping</code> 역할을 한다고 볼 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[| Spring WebFlux | Flux와 Mono]]></title>
            <link>https://velog.io/@aya-milk/Spring-WebFlux-Flux%EC%99%80-Mono</link>
            <guid>https://velog.io/@aya-milk/Spring-WebFlux-Flux%EC%99%80-Mono</guid>
            <pubDate>Wed, 24 Jun 2026 04:21:40 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>💡 <strong>Mono는 0<del>1개, Flux는 0</del>N개</strong></p>
</blockquote>
<p>비동기 통신은 Publisher와 Subscriber(Provider와 Consumer)이 중요합니다. 그렇기 때문에 이번에는 Publisher에 대해 알아보겠습니다.</p>
<hr>
<h2 id="mono">Mono</h2>
<p><strong>Mono</strong>라는 단어는 &quot;<strong>하나</strong>&quot;를 의미하는 그리스어의 접두사입니다. 0개에서 최대 1개의 결과를 emit하는 Publisher입니다.</p>
<ul>
<li><code>onNext</code> : 데이터 1개 전달(1회)</li>
<li><code>onComplete</code> : 모든 작업이 성공했을 때 호출한다. 이후에 <code>onNext</code>는 호출하면 안 된다.</li>
<li><code>onError</code> : 작업 중 예외를 던진다. 이후에 <code>onNext</code>, <code>onComplete</code>를 호출하면 안 된다.<pre><code class="language-java">// &quot;Hello&quot;라는 단일 데이터를 포함하는 Mono 생성
Mono&lt;String&gt; monoJust = Mono.just(&quot;Hello&quot;);
</code></pre>
</li>
</ul>
<p>// 데이터 없이 작업 완료 신호를 전파하는 Mono 생성
Mono<Void> monoEmpty = Mono.empty();</p>
<p>// 데이터 없이 예외를 던지는 Mono 생성
Mono<String> monoError = Mono.error(new RuntimeException(&quot;Error occured&quot;);</p>
<pre><code>
---
## Flux
**Flux**라는 단어는 라틴어의 Fluxus와 Fluere에서 유래되었으며, &quot;**흐르는**&quot;, &quot;**흐름**&quot;이라는 의미를 갖고 있습니다. 0개에서 최대 n개의 데이터를 emit하는 Publisher입니다.
- `onNext` : 데이터 1개 전달(n회)
- `onComplete` : Mono와 동일
- `onError` : Mono와 동일
```java
// 1..5 5개의 데이터를 순차적으로 emit하는 Flux
Flux&lt;Integer&gt; fluxJust = Flux.just(1, 2, 3, 4, 5);

// List로 생성한 Flux
// List&lt;String&gt; names = List.of(&quot;Alice&quot;, &quot;Bob&quot;, &quot;Charlie&quot;);
Flux&lt;String&gt; fluxFromIterable = Flux.fromIterable(names);</code></pre><hr>
<h3 id="publisher의-메소드">Publisher의 메소드</h3>
<ul>
<li><p><code>map</code> : <strong>동기</strong>적인 1:1 변환</p>
<ul>
<li><code>A</code>타입의 요소를 <code>B</code>타입으로 변환</li>
<li>각 요소에 대한 메소드를 람다식 실행</li>
</ul>
</li>
<li><p><code>flatMap</code>: <strong>비동기</strong>적인 1:N 변환</p>
<ul>
<li>각 요소를 새로운 <code>Mono</code> / <code>Flux</code>로 변환하고, 하나의 Stream으로 평탄화</li>
</ul>
</li>
<li><p><code>filter</code> : 주어진 조건을 만족하는 요소만 통과</p>
</li>
<li><p><code>zip</code> : 여러 Stream 요소를 하나씩 짝지어 새로운 Stream 생성</p>
</li>
<li><p><code>merge</code> : 여러 Stream을 도착하는 순서대로 하나의 Stream으로 합침</p>
</li>
</ul>
<hr>
<h3 id="publisher의-생명주기">Publisher의 생명주기</h3>
<p><code>Mono</code>와 <code>Flux</code>는 Observable을 <code>subscribe</code>하지 않으면, 무언가 실행되는 것은 아니다. 단지 이벤트가 발생했을 때, 이벤트 처리 방법을 프로세스화한 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[| Spring WebFlux | Reactive Extension Java]]></title>
            <link>https://velog.io/@aya-milk/Spring-WebFlux-Reactive-Extension-Java</link>
            <guid>https://velog.io/@aya-milk/Spring-WebFlux-Reactive-Extension-Java</guid>
            <pubDate>Fri, 19 Jun 2026 06:17:30 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>📄 <strong>Reactive Programming</strong>
데이터 스트림(Data Stream)과 변경 전파에 관련된 선언적 프로그래밍
패러다임.</p>
</blockquote>
<p>반응형 프로그래밍의 키워드는 다음과 같습니다.</p>
<ul>
<li><strong>비동기성(Asynchrony)</strong><br>반응형 시스템은 이벤트 또는 데이터 스트림(Data Stream)을 예상할 수 없는 시간에 처리합니다.</li>
<li><strong>반응성(Responsiveness)</strong><br>반응형 시스템은 실시간으로 데이터의 변화에 반응합니다.</li>
<li><strong>탄력성(Elasticity)</strong><br>반응형 시스템은 부하나 실패에 유연하게 대응할 수 있습니다.</li>
<li><strong>메시지 기반(based Messaging)</strong><br>반응형 시스템은 메시지 기반 아키텍처를 기반으로 동작합니다.</li>
</ul>
<hr>
<h3 id="observables">Observables</h3>
<p>데이터 흐름에 맞게 <strong>알림을 보내</strong> Observer가 데이터를 사용할 수 있도록 한다. Observable은 데이터 스트림을 정의하고, Observer는 이를 구독하여 변환된 데이터를 비동기적으로 처리한다.</p>
<blockquote>
<p>💡 Collections의 Iterable과 유사하다</p>
</blockquote>
<p>Iterable - 소비자(Consumer)가 값을 요청하고, 준비될 때까지 Thread를 Blocking</p>
<p>Observable - Non-Blocking, 데이터가 준비되면 Consumer에게 Push</p>
<p>메시지 큐잉에서 주로 사용하는 Publisher인듯 하다...!</p>
<hr>
<h3 id="consume-방식">Consume 방식</h3>
<p>Observable이 데이터를 발행한 후, <strong>알림(Event)</strong> 을 전파하면, Observer는 그것을 <strong>구독(Subscribe)</strong> 하고, 데이터를 <strong>소비(Consume)</strong> 한다.</p>
<h4 id="observable의-데이터-발행">Observable의 데이터 발행</h4>
<p>Observable이 데이터를 발행한 후, 보내는 알림에는 세 종류가 있다.</p>
<pre><code class="language-java">public interface Emitter(@NonNull T&gt; {
    void onNext(@NonNull T value);
    void onError(@NonNull Throwable error);
    void onComplete();
}</code></pre>
<ul>
<li><code>onNext</code> : 데이터의 발행을 알린다.</li>
<li><code>onComplete</code> : 모든 데이터의 발행이 완료됨을 알린다. 이후 <code>onNext</code> 를 호출하면 안 된다.</li>
<li><code>onError</code> : 오류가 발생했음을 알린다. 이후에 <code>onNext</code>, <code>onComplete</code> 는 호출되지 않는다.</li>
</ul>
<h4 id="observable의-subscribe">Observable의 Subscribe</h4>
<p><strong>구독(Subscribe)</strong> 이란, <strong>데이터 발행을 수신</strong>받고, 해당 데이터를 <strong>활용</strong>하여 <strong>다른 작업을 진행</strong>하기 위한 행위를 의미한다. Observer는 <code>subscribe()</code> 메소드에서 수신한 각 알림에 대해 실행할 내용을 선언한다.</p>
<ul>
<li><code>Disposable</code><br> 사전적 의미 : 사용 후 버리는, 일회용의 / 이용 가능한<pre><code class="language-java">public interface Disposable {
  void dispose()    // 리소스를 해제, 해제 시 작업은 멱등성을 가짐.
  boolean isDisposed()    // 리소스 해제 여부를 boolean으로 반환
}</code></pre>
</li>
</ul>
<p><code>Observer</code>의 <code>subscribe()</code> 메소드의 반환 타입인 Disposable 인터페이스에 대해 짚은 후에 subscribe에 대해 더 알아보는 것이 좋을 것 같아, Disposable 인터페이스에 대한 설명을 작성했다.</p>
<p>Disposable은 Observer는 Observable을 구독하고, 구독함으로써 <strong>스트림</strong>을 생성한다. 스트림이 오래 실행될 경우, 메모리 누수가 발생하므로 이 스트림을 정리해야 할 필요성이 있다.</p>
<pre><code class="language-java">import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.Disposable;

import java.util.concurrent.TimeUnit;

public class Main {

    public static void main(String[] args) {

        Observable&lt;Long&gt; observable = Observable.interval(1, TimeUnit.SECONDS);
        Disposable disposable = observable.subscribe(System.out::println);

        new Thread(() -&gt; {
            try {
                Thread.sleep(2500);
            } catch (Exception e) {
                e.printStackTrace();
            }

            System.out.println(&quot;isDisposed: &quot; + disposable.isDisposed());
            System.out.println(&quot;called Disposable.dispose()...&quot;);
            disposable.dispose();
            System.out.println(&quot;isDisposed: &quot; + disposable.isDisposed());
        }).start();
    }
}</code></pre>
<p>Observable은 1초마다 1을 발행한다.
Observer는 이를 구독하고, 람다식을 통해 콘솔에 발행된 아이템을 출력한다.</p>
<p>이 행위는 Thread.sleep(2500)을 통해 2.5초간 실행된다.</p>
<ul>
<li>0 ~ 1 = 0</li>
<li>1 ~ 2 = 1</li>
<li>2 ~ 2.5 = null</li>
</ul>
<p>이후 Thread가 종료되고, 이전에 <code>subscribe()</code>를 사용해 구독했던 observer의 구독 리소스 해제를 진행한다.</p>
<p>이제 Disposable의 사용법을 대충 알아보았으니, <code>subscribe()</code>의 반환 형태를 알아보자.</p>
<pre><code class="language-java">public final Disposable subscribe()
public final Disposable subscribe(@NonNull Consumer&lt;? super T&gt; on Next)
public final Disposable subscribe(@NonNull Consumer&lt;? super T&gt; onNext, @NonNull(? super Throwable) onError)
public final Disposable subscribe(@NonNull Consumer&lt;? super T&gt; onNext, @NonNull(? super Throwable) onError, @NonNull Action onComplete)
public final void subscribe(@NonNull Observer&lt;? super T&gt; observer)</code></pre>
<p>인자가 없는 <code>subscribe()</code>의 경우, 테스트나 디버깅할 때 주로 사용하며, onError 이벤트가 발생하면 <code>onErrorNotImplementedException</code>을 던진다.</p>
<blockquote>
<p>💡 <code>onComplete</code> 이벤트가 발생하면, <strong>dispose()</strong>를 호출하고
<code>Observable</code>이 더 이상 데이터를 발행하지 않게 구독을 해지한다.</p>
</blockquote>
<hr>
<h3 id="operators">Operators</h3>
<p>RxJava는 다양한 연산자를 제공하여 Observable의 데이터를 변환, 조작, 필터링, 결합 등의 작업을 수행한다.</p>
<ul>
<li><code>just()</code> : 단일 항목을 emit</li>
<li><code>listOf()</code> : Iterable 또는 Future에서 Obsavable을 생성</li>
<li><code>interval()</code> : 주기적으로 emit</li>
<li><code>timer()</code> : 지정된 시간 이후에 emit</li>
</ul>
<p>등등 여러 타입의 Observable을 생성한다.</p>
<hr>
<h3 id="scheduler">Scheduler</h3>
<p>RxJava는 스케줄러를 통해 비동기 작업을 관리하고, 스레드 풀을 활용하여 병렬 처리를 지원합니다. 이를 통해 I/O 작업, 네트워크 호출, 데이터베이스 쿼리 등 비동기 작업을 효과적으로 처리할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[| Spring WebFlux | Non-Blocking I/O]]></title>
            <link>https://velog.io/@aya-milk/Spring-WebFlux-Non-Blocking-IO</link>
            <guid>https://velog.io/@aya-milk/Spring-WebFlux-Non-Blocking-IO</guid>
            <pubDate>Fri, 19 Jun 2026 03:44:54 GMT</pubDate>
            <description><![CDATA[<h2 id="배경">배경</h2>
<p><code>Spring WebFlux</code>를 배우게 된 이유는 딱히 없습니다. 재밌어 보여서 공부해보려고 합니다. Spring이 제공하는 공식 문서를 읽으며 WebFlux가 무엇인지, 왜 사용되는지에 대해 혼자 공부하기 위해 이 게시글을 열었습니다.
<a href="https://docs.spring.io/spring-framework/reference/web/webflux.html">Spring WebFlux 공식 문서</a></p>
<hr>
<h2 id="netty">Netty</h2>
<p>Netty는 Spring MVC가 WAS로 Apache Tomcat을 채택한 것처럼, Spring WebFlux가 논블로킹 IO를 위해 채택한 서버 프레임워크입니다.</p>
<blockquote>
<p><strong>Netty</strong>는 처음부터 API 사용과 구현 모두 <strong>편리한 경험</strong>을 제공한다.</p>
</blockquote>
<p>기존의 Spring MVC에서도 현재 Java21 이상의 버전을 이용하여 문제 없이 대용량의, 응답 시간이 긴 데이터를 반환할 수 있게 되었지만 여전히 Backpressure 문제 등이 남아있습니다.
* <strong>back pressure</strong> : Provider-Consumer Problem에서 Provider가 Consumer의 소비 속도를 뛰어 넘는 문제</p>
<h3 id="netty의-장점">Netty의 장점</h3>
<p>Netty는 사용 가능한 CPU 코어 수의 두 배 크기를 기본값으로 하는 EventLoopGroup 내에서 스레드 풀을 관리합니다.</p>
<h4 id="eventloop">EventLoop</h4>
<blockquote>
<p><strong>이벤트 루프</strong>는 특정 이벤트나 메시지가 발생할 때까지 대기하다가 이벤트가 발생하면 디스패치하는 디자인 패턴(혹은 구조체이다.)</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/aya-milk/post/04419bb6-4374-44bb-8837-3ef9cfc1e06a/image.png" alt=""></p>
<ul>
<li><strong>Event Loop</strong>
  &nbsp;&nbsp;무한 반복문을 실행하며 이벤트가 발생할 때까지 대기하다가 이벤트가 발생하면 해당 이벤트를 처리할 수 있는 Handler에게 디스패치한다.
  &nbsp;&nbsp;보통 특정 Channel에 대한 이벤트를 큐에 삽입할 때, 해당 이벤트를 처리할 수 있는 Handler도 같이 첨부해준다.<ul>
<li>처음 소켓이 열렸을 땐, accept()하면서 해당 Channel에 AcceptHandler를 첨부해준다.</li>
</ul>
</li>
<li><strong>Handler</strong>
이벤트를 받아 비즈니스 로직을 수행한다. (수행완료하고 결과에 맞는 이벤트를 다시 발행하기도한다.)</li>
</ul>
<p><a href="https://mark-kim.blog/understanding-event-loop/">더 자세한 내용을 참조하려면...</a></p>
<p>Spring MVC 방식으로 이해하면 DispatcherServlet의 역할으로 이해했습니다.(사견입니다.) MQ가 구현되는 방식도 비슷합니다. 스레드를 잡고 메시지가 도착했는지를 Listening하는 방식이며, Mosquitto의 경우에는 해당 작업을 <code>loop_forever</code>로 논블로킹 I/O를 구현할 수 있습니다.</p>
<p>&nbsp;&nbsp;다시 돌아와서, Netty는 연결이 설정되고 채널이 생성되면 해당 채널 중 스레드 하나와 연결됩니다. 이 채널에 대해 새 메시지를 수신하거나, 전송할 때마다 동일한 스레드가 사용됩니다.
&nbsp;&nbsp;Netty를 효율적으로 사용하기 위해서는, 논블로킹 I/O를 수행하는 이벤트 루프 내에서 블로킹 I/O 작업을 수행하면 안 됩니다.(블로킹 작업이 return하기 전까지 논블로킹 작업이 리턴되지 못 하기 때문입니다.) 따라서 I/O 바운드 처리를 위한 별도의 EventLoopGroup을 생성하거나, 표준 Java 유틸리티를 사용하여 작업을 별도의 스레드에서 실행하여야 합니다.</p>
<h2 id="reactive">Reactive</h2>
<blockquote>
<p><strong>반응형</strong>이란, 네트워크 I/O, UI 이벤트 등의 변화에 반응하는 프로그래밍 모델이다.</p>
</blockquote>
<p>Spring에서는 논블로킹 역압력(back pressure) 처리가 중요합니다. 동기 통신에서는 자연스럽게 역압력 처리가 되지만, 논블로킹 코드에서는 소비자 측의 프로세스가 과부하되지 않도록 생산 속도를 조절해야 할 필요가 있습니다.</p>
<p>예를 들면, Message Queue에서 Publisher와 Subscriber입니다.</p>
<h3 id="reactive-api">Reactive API</h3>
<p>반응형 스트림은 라이브러리나, 인프라 구성 요소로는 훌륭하지만 너무 저수준이기 때문에, 애플리케이션 API로는 적절치 않습니다. 비동기 로직을 구현하기 위해서는 고수준의 풍부한 함수형 API가 필요한데, Reactive 라이브러리에 적혀있습니다.</p>
<p>Reactor는 Spring WebFlux에 사용되는 반응형 라이브러리입니다. <code>Mono</code>와 <code>Flux</code>를 제공하여 데이터 시퀀스를 0<del>1(Mono)개, 혹은 0</del>N(Flux)개를 처리할 수 있습니다.</p>
<h3 id="controller">Controller</h3>
<p>Spring MVC에서 지원하는 @Controller 어노테이션이거나, 함수형 엔드포인트 두가지 컨트롤러를 선언할 수 있습니다.</p>
<ul>
<li>어노테이션 : MVC와의 일관성을 유지하지만 동기/비동기 구분이 어렵습니다.</li>
<li>함수형 : 람다 기반의 경량 모델. 어노테이션과 가장 큰 차이는 애플리케이션이 이 어노테이션을 통해 의도를 선언하고, 콜백을 받는 대신 요청 처리를 처음부터 끝까지 직접 담당한다는 점입니다.</li>
</ul>
<h2 id="concurrency">Concurrency</h2>
<p>MVC는 기본적으로 API 요청을 처리한다는 것이 스레드를 하나 점유한다는 의미였고, 이는 스레드 자체가 블로킹되는 것과 동일하게 보았습니다. 하지만, WebFlux는 항상 요청을 Listening하고 있기 때문에, MVC에 비해서 적은 수의 스레드를 사용합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JWT] FE에게 전달할 때, JWT는 어디에 담아야 하는가?]]></title>
            <link>https://velog.io/@aya-milk/JWT-FE%EC%97%90%EA%B2%8C-%EC%A0%84%EB%8B%AC%ED%95%A0-%EB%95%8C-JWT%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EB%8B%B4%EC%95%84%EC%95%BC-%ED%95%98%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@aya-milk/JWT-FE%EC%97%90%EA%B2%8C-%EC%A0%84%EB%8B%AC%ED%95%A0-%EB%95%8C-JWT%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EB%8B%B4%EC%95%84%EC%95%BC-%ED%95%98%EB%8A%94%EA%B0%80</guid>
            <pubDate>Sun, 17 May 2026 07:25:57 GMT</pubDate>
            <description><![CDATA[<p>인증과 인가를 구현할 때 항상 고민되었던 부분이 있었다.</p>
<blockquote>
<h3 id="jwt는-어디에-담아-보내주어야-하는가">JWT는 어디에 담아 보내주어야 하는가?</h3>
</blockquote>
<p>&nbsp;해당 부분에 대해서는, 같이 개발했던 이들과 한번쯤은 이야기해보았던 것 같다. <strong>2025년 멋쟁이 사자처럼 중앙 해커톤</strong>을 함께 했던 동료의 이야기는 <code>ResponseBody</code>가 맞다. 또 2026년 상반기, 지금 <strong>UMC</strong>를 함께 진행하고 있는 동료들과는 <code>Authorization</code> 헤더가 맞지 않겠느냐. 등 이제 처음 Security를 배우는 학생들에게는 꽤나 뜨거운 주제 중 하나가 아닐까 싶다.
&nbsp;&nbsp;필자와 같은 고민을 하는 학생들에게는 명쾌한 해답이 되었으면 하고, 나름대로 요즘같은 AI 시대에 스스로 생각하는 힘을 기르고자 이번 글을 작성하게 되었다.</p>
<hr>

<h2 id="rfc-6749">RFC 6749</h2>
<p>국제 인터넷 표준화 기구인 IETF(Internet Engineering Task Force)에서 발행하는 인터넷 기술 관련 문서인 RFC(Request For Comments, 비평을 기다리는 문서)의 6749 버전에서는 다음과 같이 이야기한다.</p>
<h3 id="accesstoken">accessToken</h3>
<p><img src="https://velog.velcdn.com/images/aya-milk/post/4e2ee9a2-a795-4abe-88af-a9bd6c6741da/image.png" alt=""></p>
<p>인가를 담당하는 서버는 액세스 토큰과 선택적으로 리프레시 토큰을 발행한다. 그리고, 다음의 파라미터를 HTTP 200 응답인 entity-body에 추가하여 생성한다.</p>
<ul>
<li>Access Toekn : 필수, 인증 서버에 의해 발행된다.</li>
<li>Token Type : 필수, 대소문자를 구분하지 않음.<ul>
<li><code>Bearer</code>, <code>MAC</code>, ...</li>
</ul>
</li>
<li>expires_in : 권장사항, 액세스 토큰이 몇초간 유효한지, 예를 들어 3600초는 ...</li>
</ul>
<p>거창하게 적어놓았지만, 요지는 <strong>&#39;HTTP 200 응답의 Entity Body에 추가하라.&#39;</strong> 라고 명시되어 있다.</p>
<h3 id="refreshtoken">refreshToken</h3>
<p><img src="https://velog.velcdn.com/images/aya-milk/post/2be67392-954d-4425-8458-f45c9d4212d6/image.png" alt=""></p>
<p>인가 서버는 웹 애플리케이션 클라이언트와 네이티브 앱 클라이언트에 대해 리프레시 토큰을 <strong>발급할 수도</strong> 있다.</p>
<p>리프레시 토큰은 <strong>반드시 비밀로</strong> 전송되고, 저장되어야 한다. 토큰을 발급한 서버와, 발급을 요청한 클라이언트끼리만 공유되어야 하며, 탈취되어서는 안된다. 반드시 TLS를 통해 전송되어야 한다.</p>
<pre><code class="language-plain">TLS(Transport Layer Security)는
온라인 네트워크에서 데이터를 안전하게 주고받기 위한 암호화 프로토콜이다.</code></pre>
<p><a href="https://docs.tosspayments.com/resources/glossary/tls">토스 개발자 센터 - TLS 관련 문서</a></p>
<p>이 정도로만 언급되어 있고, <strong>refresh token</strong>은 정확히 어디에 담아야 할지에 대한 규약은 정해져있지 않아서 더 찾아보았다.</p>
<h2 id="stack-overflow">Stack Overflow</h2>
<p><a href="https://stackoverflow.com/questions/60540104/how-worried-should-i-be-about-opening-up-a-jwt-to-an-xss-vulnerability">Stack Overflow - JWT와 XSS 취약점</a>
XSS 공격은 Cross Site Scripting 공격으로, 공격자가 대상 웹사이트로 하여금 악성 코드를 마치 웹 사이트의 일부인 것처럼 동작하도록 하게 하는 공격이라고 한다. 이 공격으로 클라이언트의 로컬 스토리지를 읽을 수 있으며, 토큰이 로컬 스토리지에 저장되면 안 된다는 것으로 귀결된다.
&nbsp;&nbsp;이를 해결하기 위한 몇가지 방법이 제시되는데</p>
<ul>
<li>accessToken은 ResponseBody, refreshToken은 set-cookie로<ul>
<li>refreshToken은 HttpOnly로 세팅</li>
</ul>
</li>
<li>(FE, BE 모두 동일한 도메인일 경우) 둘 다 HttpOnly인 set-cookie로<ul>
<li>반드시 <code>SameSite = strict</code> 옵션을 활성화</li>
</ul>
</li>
</ul>
<p>&nbsp;가장 최선의 방법은 당연히 FE와 BE가 모두 같은 도메인을 사용하여 <code>same-site = strict</code> 옵션을 부여하고, CSRF 공격도 대비하는 것이 좋다. 하지만, FE는 보통 Vercel을 이용하여 배포하는 경우가 많기 때문에 여태까지의 경험에서 시도해본 적은 없다.(물론 그렇지 않은 학생 개발자도 많으리라 생각한다. 나의 경우에는 그렇다는 것일 뿐이다.)
&nbsp;&nbsp;차선책은 Vercel 배포를 하되, Vercel의 설정을 건드려주는 것이다. <code>vercel.json</code>을 프로젝트 루트에 만들어서</p>
<pre><code class="language-json">{
       &quot;rewrites&quot;: [
        {
            &quot;source&quot;: &quot;/api/:path*&quot;,
            &quot;destination&quot;: &quot;http://{BE_HOST}/api/:path*&quot;
        }
    ]
}</code></pre>
<p>이 설정을 작성하면 Vercel의 내장 기능으로 웹 사이트 사용자는 Vercel 주소로 접근할 수 있고, Vercel에서 도메인을 프록시 처리하여 요청하기 때문에 서버는 동일 도메인으로 접근하고 있다고 인지하게 된다.
&nbsp;&nbsp;이 때, refreshToken을 <code>Set-Cookie: refreshToken=abc...; HttpOnly; Secure; SameSite=Strict;</code> 로 설정하여 통신하는 것이다. BE에서는 refreshToken을 DB나 Redis에 저장해두고, refreshToken이 우리 서버에서 발급된 것인지 인증하고, 인증에 성공하면 삭제한다.</p>
<hr>

<blockquote>
<h3 id="성미-급한-한국인-개발자를-위한-3줄-요약">성미 급한 한국인 개발자를 위한 3줄 요약...</h3>
</blockquote>
<ul>
<li>BE, FE 같은 도메인 사용 (Vercel 배포 -&gt; BE 도메인으로 프록시해라.)</li>
<li>accessToken은 Body, refreshToken은 Set-Cookie로 전달해라.</li>
<li>refreshToken을 전달할 때에는 <code>HttpOnly, Secure, SameSite = Strict</code></li>
</ul>
<hr>
<p>나의 경우에는 다음에 진행할 프로젝트에 refreshToken + Redis + Set-Cookie 전략을 적용해보려고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[멋쟁이 사자처럼 13기 중앙 해커톤 회고]]></title>
            <link>https://velog.io/@aya-milk/%EB%A9%8B%EC%9F%81%EC%9D%B4-%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-13%EA%B8%B0-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@aya-milk/%EB%A9%8B%EC%9F%81%EC%9D%B4-%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-13%EA%B8%B0-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Wed, 24 Sep 2025 03:45:36 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><em>** To be a Lazy Engineer**</em></p>
</blockquote>
<h2 id="keep-잘한-점">Keep (잘한 점)</h2>
<h3 id="열정">열정</h3>
<p>이번 중앙톤을 회고해보면, 생애 처음이라고 이야기해도 과언이 아닐 정도로 최선을 다했다. 순수히 개발만 집중하진 않았더라도, 개발을 해야겠다 생각하고 오전 11시에 앉아서 오전 1시가 다 되어서야 잠자리에 들고, 이런 나날의 반복이었다. 전혀 이 활동에 대해 지치지 않았고, 하나라도 더 빠르게 개발해서 프론트엔드 개발자 분들에게 빠르게 서버를 띄워주어야한다는 마음가짐으로 임했다.</p>
<h3 id="신기술-도전">신기술 도전</h3>
<p>이번 프로젝트에서는 RAG라는 AI 기법과 벡터 임베딩, Flask 구조에 대해 얕게 시도해보았었다. 또, 타사의 API(Payment Gateway Service)를 우리 서비스에 적용시켜보고, 금융 서비스에 있어서 동시성 제어가 얼마나 중요한지를 다시금 느끼게 되었다. 결제에 성공한 경우, 환불한 경우, 결제를 취소한 경우 등에 있어서 데이터를 어떻게 다루어야 할지 조금 더 깊게 공부하고 싶은 마음이 들었다. </p>
<h3 id="docker-with-cd">Docker with CD</h3>
<p>지속적 배포(CD)에 대해 간략하게 배워보고, Docker Container는 어떠한 방식으로 동작되는가에 대해 조금 더 깊게 공부하게 되었다. http 요청만 받아들이는 Spring 프레임워크에서, 나의 개인 호스트에 대해 발급된 https 인증서를 통해 설정파일을 만들고, nginx 컨테이너에 이를 포함하여 올리는 등, 백엔드의 기술 구조 흐름에 대해 조금 더 잘 이해하게 된 것 같다.</p>
<h2 id="problem">Problem</h2>
<h3 id="architecture">Architecture</h3>
<p>많은 프로젝트를 하진 않았다. 백엔드 개발을 시작한지 반년밖에 되지 않았고, 토이 프로젝트같은 느낌으로 현재까지 세개정도 구현했지만, 시간이 지나고 이 세번의 프로젝트를 거쳐 점점 악취를 느끼기 시작했다.</p>
<ol>
<li><p>계층형 구조의 한계</p>
<p> 계층형 아키텍처를 채용해서 사용하고 있었는데, 제공하려 하는 서비스가 많아지면 많아질 수록, 도메인  간 의존 결합이 너무 강해졌다. 타 도메인의 엔터티, JPA Repository를 끌어와서 DI한다던가가 대표적이었다. 도메인은 순수해야 하며, 의존 관계를 알게되는 순간, 클린한 아키텍처가 아니라는 생각이 이제 와서 들기 시작했다.</p>
<p> → 해결방안 - 비슷한 고민을 우아한 형제들의 개발자님들께서도 하신 것 같다.</p>
<ul>
<li><p>Hexagonal Architecture - 서비스 로직을 중심으로 의존하는 관계라 한다…</p>
<p>  클린 아키텍처의 종류중 하나라고 하는데 공부해야 할 것이 생겨서 기분이 좋다.</p>
</li>
</ul>
</li>
<li><p>Bean과 Singleton 패턴의 이해</p>
<p> 타 컨테이너나, 로컬의 API 서버(우리가 사용했던 것은 Flask.)와 통신하기 위한 RestTemplate, WebClient 등을 고려하다가 발생한 고민이다. 서비스에서 해당 객체를 호출하여 하드코딩하는 것은 해당 서비스 로직에서도 책임이 비대해지고 단순하게 생각해서도 그렇게 하면 안되리라는 막연한 고민이 들었었다. 이 생각에서 조금 더 나아간 생각이 그럼 결국 Bean, Configure, Singleton 이런 것들은 ‘어떤 상황에 사용되면 좋은가?’에 대해 생각이 들었다. 지금 상황에서는 Configure로 의존을 통합 관리하고, 여러 군데에서 사용되는 함수는 Bean 등록하여 Singleton을 적용해야 한다. 라는 앵무새와 같은 말만 읊고 있지만, 실제 서비스에서 적용해보아야 할 것 같다.</p>
<p> → 전략, 패턴에 대한 이해와 상황에 맞는 사용 경험이 필요</p>
</li>
<li><p>지속적 통합과 구조 이해</p>
<p> 프로젝트를 진행하면서, 어쩔 수 없이 개발자는 다른 개발자들의 코드를 읽고 이해해야 하는 능력이 <code>필수</code> 라고 느꼈다. 이유는 단순하다. 내가 그 로직들을 사용을 안할 리가 없지 않은가. 그런 관점에서 보았을 때, “문서화”라는 것과 그것을 이해하는 능력이 필요하다고 여실히 느꼈다. 그리고, 여러 오픈 소스들을 공부하려고 프로젝트들을 뒤져보았지만, 정말 단순한 내용이거나 너무 복잡한 내용이어서 이해하기 어려웠을 때, 참고하기 좋은 자료가 TestCase라고 했다. Test는 구현을 알 필요 없이, 서비스의 흐름만을 명시적으로 적어 제대로 흘러가고 있는지를 체크하기 위함이라 생각한다. 여기서 지속적 통합과 TDD(Test-Driven Development)라는 말이 뇌리를 스쳤다. 현재까지는 그저 스파게티 코드여도 제대로 된 응답만 출력할 수 있다면 괜찮다고 생각했지만, 이제는 속도와 유지보수에 대해 더욱 신경을 써야 할 시기라고 생각이 들었다.</p>
<p> → Test 케이스 작성과 CI 환경 구성 및 세세한 코드 리뷰(여기서는 비즈니스 흐름을 도식화 해보는 것이 좋을 것 같다.)가 필요할 것 같다.</p>
</li>
<li><p>co-work에서의 문제점 발견 방식</p>
<p> 현재까지는 프론트엔드 개발자분들께서 친절하게도 OOO 부분에서 XXX 예외(보통은 Internal Server Error, 예외처리 되지 않았는데 발생된 예외이다.)가 발생했는데, 확인 한번 부탁드려요. 라고 말씀을 주시면, “직접 ssh로 접속해 Docker Container 로그를 조회했다.” 생각만 해도 악취가 진동한다. 지금 하고 있는 프로젝트에 적용해보고 있는 기술이긴 하지만, Promtail-loki with Grafana를 통해서 서버에 있는 Docker Container의 로그 파일을 추적, 모니터링 서버에 있는 loki container에 전달한다. 그리고 Grafana에서 loki를 DataSource로 등록하여, Error라는 메세지를 라벨링 후 알림을 전송하는 방식으로 드디어 마침내 프론트엔드분들이 직접 예외를 전달하는 수고를 해결할 수 있었다.(이 과정에서 오랜 기간동안 발생하는 Log 파일들을 주기적으로 날리는 작업도 같이 했다.) 이후 조금 더 공부해보아야 할 항목이다.</p>
</li>
</ol>
<h2 id="try">Try</h2>
<p>이번 해커톤에서 가장 아쉬웠던 점은 뭐니뭐니 해도 코드 완성도에 관한 부분인 것 같다. 사용할 수 있는 기술의 유니크함들도 다소 아쉬운 점은 있었으나, In My Best였다고 생각한다. 하지만, 여러 관점에서 나와 함께한 팀원의 코드는 여전히 유지보수가 어렵고, Dirty Architecture라고 해도 무방했다.(그래서 1차 AI 예선에서도 탈락한… 것 같다 죄송스럽다.) 목표는 우선 SOLID한 설계이다. 프로젝트를 진행하면서도 항상 내 코드는 SOLID하지 않은 것 같다는 말을 입에 달고 산 만큼, 다음 프로젝트를 진행하게 된다면 이것이 1순위이다. 직접 코드를 일일이 리뷰하지 않아도 확인할 수 있게끔 문서화하는 작업과, TestCase를 유연하게 작성해두는 것(모든 마이크로 서비스에 테스트를 적용하는 것도 굉장한 손해이다.) 그리고 기술 의존적이지 않게끔 하기 위한 설정이다(Hexagonal Arch 등).</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Qdrant로 RAG 구성하기]]></title>
            <link>https://velog.io/@aya-milk/Qdrant%EB%A1%9C-RAG-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@aya-milk/Qdrant%EB%A1%9C-RAG-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 17 Aug 2025 13:56:19 GMT</pubDate>
            <description><![CDATA[<p>이번에 진행했던 멋쟁이 사자처럼 13기에서 Qdrant 데이터베이스를 이용하여 RAG 서비스(?)를 구축했던 것을 기억하기 위해 작성했다.</p>
<h2 id="qdrant--docker">Qdrant + Docker</h2>
<p>Qdrant는 여태 사용해왔던 MySQL과는 사용하는 방법이 조금 달랐다.</p>
<ul>
<li>terminal : Oracle, MySQL</li>
<li>workbench : MySQL</li>
<li>WebUI : H2</li>
</ul>
<p>이상이 사용했던 DB들이고, 아래는 Qdrant다.</p>
<ul>
<li>WebUI : Qdrant</li>
</ul>
<p>나름대로 이런 서비스들은 공식 사이트에서 다운로드할 수 있는 법이어서, 나름 생소하다면 생소한 부분이었다.</p>
<p><a href="https://qdrant.tech/documentation/quickstart/">Qdrant Official Docs - Quick Start Qdrant</a></p>
<p>로컬에서는 docker run 명령어를 사용했다.</p>
<pre><code class="language-bash">docker pull qdrant/qdrant
docker run -p 6333:6333 -p 6334:6334 \
    -v &quot;$(pwd)/qdrant_storage:/qdrant/storage:z&quot; \
    qdrant/qdrant</code></pre>
<p>-v는 Qdrant 컨테이너가 종료되어도, 기존의 저장되어있는 정보들을 저장해두고 추후 재시작해도 데이터가 남아있게 해주는 옵션이다
서버에 올린 것은 컴포즈 방식으로 올렸는데, 코드는 다음과 같다.</p>
<p>docker-compose.yml은 아래와 같이 작성하였다</p>
<pre><code class="language-bash">qdrant:
    image: qdrant/qdrant:latest
    container_name: qdrant-container
    ports:
      - &quot;6333:6333&quot;
      - &quot;6334:6334&quot;
    restart: always
    volumes:
      - qdrant-data:/qdrant/storage
volumes:
    qdrant-data:</code></pre>
<hr>
<h2 id="qdrant---webui">Qdrant - WebUI</h2>
<p><img src="https://velog.velcdn.com/images/aya-milk/post/93d397b7-c4df-4cc7-9b0a-b550e28adf1f/image.png" alt="">
보시는 것과 같이 Qdrant를 WebUI로 확인하는 모습이다.
좌측 &quot;Welcome&quot;에선 샘플 데이터를 다운로드 하는 버튼이 있고, 그것을 예제로 조금 보여드리자면<img src="https://velog.velcdn.com/images/aya-milk/post/fffc415c-9621-422b-b38e-c619ee075ad1/image.png" alt="">
(우측 JSON 데이터 위의 run하는 버튼을 누른 모습)</p>
<p>이런 식으로 포인트들을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/aya-milk/post/e8e98582-3bb3-4682-ab89-6d79371515cf/image.png" alt=""><img src="https://velog.velcdn.com/images/aya-milk/post/eaac6069-f0dc-4ae6-a9e3-56367fc7e7ca/image.png" alt=""><img src="https://velog.velcdn.com/images/aya-milk/post/6a984d5c-bd6f-4a84-92a4-043d62ca8ba1/image.png" alt=""><img src="https://velog.velcdn.com/images/aya-milk/post/06ec3b6f-2fdf-4707-bfb5-884c35aea166/image.png" alt="">
위치가 비슷한 포인트끼리는 비슷한 이미지인 것을 볼 수 있다.</p>
<hr>
<h2 id="openai">OpenAI</h2>
<p>우리의 프로젝트에서는 &quot;text-embedding-3-small&quot; 이라는 모델을 사용했다. 잠깐 용어에 대해 짚고 가자면, embed라는 것은 해당 포인트의 의미를 Vector값으로 변환하는 것을 의미한다. 그리고 이를 수행하는 AI 모델이 벡터 임베딩 모델인 것이다. </p>
<pre><code class="language-python">from openai import OpenAI

openai_client = OpenAI(api_key=API_KEY)

openai_client.embeddings.create(
            input=data,
            model=&quot;text-embedding-3-small&quot;
        ).data[0].embedding</code></pre>
<p>위는 임베딩하는 모습이다.
이 때, create로 생성된 값은 오직 해당 임베딩 모델이 반환할 수 있는 차원 개수의 벡터값이므로, 이후 벡터 DB에 값을 저장하려고 하면, 그 값을 알고 있다가 저장하여야 한다.</p>
<hr>
<h2 id="python에서-qdrant-upsert하기">Python에서 Qdrant Upsert하기</h2>
<pre><code class="language-python"># 벡터 임베딩 모델을 사용하여 벡터값으로 변환하고, data[0], 즉 벡터값이 있는 인덱스에서 꺼내옵니다.
vector_val = openai_client.embeddings.create(
                    input=&quot;아야하면우유&quot;,
                    model=&quot;text-embedding-3-small&quot;
                ).data[0].embedding

# Qdrant는 Key-Value로 노드를 구성합니다.
# Qdrant는 구성된 정보를 &quot;payload&quot;로 부릅니다.
point =    PointStruct (
            id = 1,
            payload = {
                &quot;author&quot;: &quot;아야하면 우유&quot;
            }
        )
# 이제 Qdrant에 Upsert(Update &amp; Insert)할 차례입니다.
qdrant_client.upsert(
    collection_name=COLLECTION_NAME.
    vector=vector_val,
    payload=point
)</code></pre>
<ol>
<li>임베딩하기</li>
<li>payload 구성하기</li>
<li>Qdrant에 Upsert하기</li>
</ol>
<p>생각보다 간단하다. Retrieval-Argumented-Generate 이름부터 압도됐었지만, 하고보니 간단한 것이다.</p>
<hr>
<h2 id="검색">검색</h2>
<p>원래 이런 유사도 검색에는 몇가지 방식이 있다</p>
<h4 id="1-유클리디안-유사도">1. 유클리디안 유사도</h4>
<p>두 벡터 간 직선 거리를 측정해서 계산</p>
<h4 id="2-코사인-유사도">2. 코사인 유사도</h4>
<p>두 벡터가 가리키는 방향, 즉 기준점과 두 벡터 사이에서 이루는 각의 코사인 값</p>
<h4 id="3-자카드-유사도">3. 자카드 유사도</h4>
<p>두 집합의 교집합 크기를 합집합 크기로 나눈 것, 즉 두 집합의 공통분모가 전체(합집합)에서 이루는 비율</p>
<p>&nbsp;이번 프로젝트를 진행하기 전까지만 해도 완전히 무지했었는데, 잠깐의 웹서핑으로 이 세가지가 가장 대표적인 유사도 검색인듯 싶었다. 그리고, 텍스트 간 유사도를 검색하는 데에는 &quot;코사인 유사도&quot;가 가장 성능이 좋다 라는 이야기가 있어서, 우리 프로젝트에서도 코사인 유사도 검색을 기반으로 했다.
&nbsp;&nbsp;검색하는 방식은 정말 간단한데, 아래에 바로 예시를 보이면,</p>
<pre><code class="language-python"># 여기에서 Qdrant와 Python이 어떻게 연결되는지 보여드리겠습니다.
# 우선 Qdrant에 접근할 수 있게, 의존성과 함수를 명시하겠습니다.
from qdrant_client import QdrantClient
from qdrant_clinet.models import Distance, VectorParams

# 로컬 Python &lt;-&gt; 도커 Qdrant로 통신하고 있으므로, localhost로 접근합니다.
# 컨테이너 간 통신은 host.docker.internal 등을 테스트해보시는걸 추천드립니다.
qdrant_client = QdrantClient(host=&quot;localhost&quot;, port=6333)


# Qdrant 컬렉션을 무조건 재생성합니다.
# size는 임베딩 모델이 지원하는 차원 수를 작성하시면 됩니다.
# Distance가 COSINE으로 지정되었으므로,
# 이후 search가 코사인 유사도 검색으로 이루어집니다.
qdrant_client.recreate_collection(
    collection_name=COLLECTION_NAME,
    vectors_config=VectorParams(size=1536, distance=Distance.COSINE)
# 만들어져있을 때 재생성하고 싶지 않다면, 아래의 boolean 조건을 조건문으로 분기하세요.
# qdrant_client.collection_exists(COLLECTION_NAME)

# 대망의 검색입니다.
# 우선 검색값을 임베딩합니다.
vector_embed = openai_client.create(
    input=&quot;왕이 넘어지면 킹콩&quot;,
    model=&quot;text-embedding-3-small&quot;
).data[0].embedding

# 그리고 이제 Qdrant에서 검색합니다.
qdrant_client.search(
    collection_name=COLLECTION_NAME,
    query_vector=vector_embed,
    limit=1
)    </code></pre>
<p>Qdrant의 search 메서드는 유사도를 정렬 기준으로 해서, 가장 유사도가 높은 값을 0번으로 내림차순 저장합니다. 이 때, limit 옵션이 1이라는 것은
<strong>&quot;가장 유사도가 높은 검색값을 구하라&quot;</strong>
라는 뜻이 되겠다.</p>
<hr>
<h2 id="이상으로">이상으로...</h2>
<p>정말 이 페이지 하나만으로 일단 Vector DB를 사용한 RAG의 기본 중의 기본은 다 적은 것 같다. 일단 1편은 여기서 마무리 짓는 것으로 하고, 추후에 2편, 3편을 제작할 수 있으면 제작해보도록 하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Week10]]></title>
            <link>https://velog.io/@aya-milk/Week10</link>
            <guid>https://velog.io/@aya-milk/Week10</guid>
            <pubDate>Fri, 18 Jul 2025 15:14:23 GMT</pubDate>
            <description><![CDATA[<h3 id="jwt와-세션의-차이">JWT와 세션의 차이</h3>
<ul>
<li><code>세션</code> : 로그인 -&gt; 난수 생성 -&gt; 세션 저장소에 난수와 해당 로그인 ID를 key - value로 저장 -&gt; 난수는 쿠키에 담아서 클라이언트로 보냄</li>
<li><code>JWT</code> : Header - Payload - Signature로 구성됨. 사용자의 중요한 정보는 담지 않고, 주로 이 서버에서 발행된 토큰인가만 check.</li>
</ul>
<hr>
<h3 id="jwt의-구성">JWT의 구성</h3>
<p><img src="https://velog.velcdn.com/images/aya-milk/post/e87246ac-bb38-45f7-8176-56a7f2d6c726/image.png" alt=""></p>
<blockquote>
<ul>
<li>Header : 어떤 방식으로 암호화됐는지, 이 Authorization은 어떤 방식인지 (여기서는 JWT임)</li>
</ul>
</blockquote>
<ul>
<li>Payload : 간단한 User 정보를 담아줌. 이 정보를 통해 DB에 접근 가능</li>
<li>Signature : 어떤 방식으로 암호화됐는지, 어떤 정보가 담겨있는지, Secret Key(서버마다 고유함)를 조합하여 <code>시그니처</code>를 만듬</li>
</ul>
<hr>
<h3 id="작동-방식">작동 방식</h3>
<p><img src="https://velog.velcdn.com/images/aya-milk/post/e1f1ec19-a47f-4e06-a43c-a490f471a675/image.png" alt="">
<strong><em>간단히 말하자면...</em></strong></p>
<ol>
<li>로그인 진행
&nbsp;&nbsp;&nbsp;&nbsp;1-1. 그에 맞는 JWT 발급</li>
<li>JWT를 클라이언트에게 전달</li>
<li>이후 인증을 위해서 매번 Header - Authorizaiton으로 JWT 전달
&nbsp;&nbsp;&nbsp;&nbsp;3-1. Filter단에서 이 JWT를 잡아서 인증하고, 인가(접근 허용)</li>
<li>Response를 클라이언트에게 전달</li>
</ol>
<hr>
<h3 id="filter-chain">Filter Chain</h3>
<p>인증하는 절차는 Filter Chain에 준다.
Spring 공식 문서에 따르면,</p>
<blockquote>
</blockquote>
<ul>
<li>클라이언트가 서버에 요청을 보내면 요청 URI를 통하여,
<code>Filter</code>와 <code>Servlet</code>이 처리해야 할
<code>HttpServletRequest</code>를 갖고
<code>FilterChain</code>를 구성한다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/aya-milk/post/8543e3ea-7c2d-48e0-83d1-b8d616473fb0/image.png" alt=""></p>
<p>필터는 <code>Servlet Container</code> 와 <code>DispatcherServlet</code> 사이에 존재한다.
<img src="https://velog.velcdn.com/images/aya-milk/post/744e5da2-70b2-49e7-b0c4-972396f58bdb/image.png" alt=""></p>
<p>위 그림에서는 <code>HTTP 요청</code> &lt;- 여기서 이미 FilterChain이 동작한 셈
<img src="https://velog.velcdn.com/images/aya-milk/post/75057eed-3677-47bc-8cae-c58fd62d0d80/image.png" alt=""></p>
<p>이 부분이 중요하다.</p>
<ul>
<li><code>HttpServletRequest</code> : 서버에 온 요청을 담는 객체인데, <code>Header</code>, <code>Cookie</code>, <code>Request Data</code> 등 Request의 모든 정보를 담고 있는 객체이다.</li>
<li>이 객체에서 Header의 Authorization 부분에 JWT를 담는다.</li>
<li><code>How?</code> : <code>Filter</code>에서 로그인을 검증하고, 필수 정보만 담아서 JWT화 하여, Header에 해당 JWT를 담아서 보내는 것이다.</li>
</ul>
<h4 id="🔥-반드시-지켜져야-하는-흐름">🔥 반드시 지켜져야 하는 흐름</h4>
<blockquote>
<ol>
<li>CSRF 보호 필터 - 인증이 살아있는 것을 이용하여 악의적인 요청을 보내지 않도록 보호함</li>
<li>인증 처리 - 사용자 로그인, JWT 체크 등 (예외는 인증과 인가 사이에서 처리함)</li>
<li>인가 처리 - <code>RequestMatcher(경로).permitAll()</code> 와 같이 해당 경로의 요청은 인증된 유저만 허용하거나, 모든 유저를 허용케 함</li>
</ol>
</blockquote>
<hr>
<h3 id="customizing-filter">Customizing Filter</h3>
<p>&nbsp;
구현한 코드 내부에서는 form - login 방식을 <code>disable</code> 했고, <code>Param</code> 방식으로 ID와 Password를 받아서 인증하는 방식이었다. 그 중에서
&quot;<code>UsernamePasswordAuthenticationFilter</code>&quot; 라는 것을 덮어쓰기로 했다.
(이름이 다소 길다.)</p>
<ul>
<li>attemptAuthentication : 인증을 시도할 때 사용되는 메서드이다. <code>HttpServletRequest</code>에서 Username과 Password를 받아서 토큰을 만들고 <code>return AuthenticationManager.authenticate(token)</code>를 실행한다.</li>
<li>successfulAuthentication : 인증이 성공했을 때 사용하는 메서드이다. <code>CustomUserDetails</code>형으로 유저 정보를 담고, JWT를 만든다. 그리고, Header에 JWT를 담는다.</li>
<li>unsuccessfulAuthentication : 인증이 실패했을 때이며, 401 응답을 반환하게끔 구현하였다.</li>
</ul>
<hr>
<h4 id="userdetails">UserDetails</h4>
<pre><code class="language-java">public interface UserDetails extends Serializable {
    Collection&lt;? extends GrantedAuthority&gt; getAuthorities();

    String getPassword();

    String getUsername();

    default boolean isAccountNonExpired() {
        return true;
    }

    default boolean isAccountNonLocked() {
        return true;
    }

    default boolean isCredentialsNonExpired() {
        return true;
    }

    default boolean isEnabled() {
        return true;
    }
}</code></pre>
<p>UserDetails Interface이다.</p>
<ul>
<li>이 토큰에서 값을 빼오는 메서드</li>
<li>이 토큰이 만료되었는지 체크하는 메서드</li>
<li>이 토큰이 잠겼는지 체크하는 메서드</li>
</ul>
<h2 id="등등-token과-관련된-함수들을-선언해놓는-interface같다">등등... Token과 관련된 함수들을 선언해놓는 Interface같다.</h2>
<h3 id="끝마치며">끝마치며.</h3>
<p>&nbsp;JWT 구현에 필요한 코드들은 굳이 작성치 않겠다. 아직 모든 함수들을 전부 외운 것도 아니고... 앞으로 더 작성해보며 저절로 외워지길 바랄 따름이다.
&nbsp;&nbsp;Refrash 토큰과 Access 토큰도 더 공부해봐야 할 것이며, 토큰의 취약점들도 좀 더 공부해볼 생각이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발 고찰 #1]]></title>
            <link>https://velog.io/@aya-milk/%EA%B0%9C%EB%B0%9C-%EA%B3%A0%EC%B0%B0-1</link>
            <guid>https://velog.io/@aya-milk/%EA%B0%9C%EB%B0%9C-%EA%B3%A0%EC%B0%B0-1</guid>
            <pubDate>Wed, 16 Jul 2025 05:05:32 GMT</pubDate>
            <description><![CDATA[<p>개발하다가 막히는 내용이 있으면, 다음부터는 이런 식으로 작성하려고 한다. 추가적으로, FastAPI와 리액트를 태그에 달았지만, 직접 다루지는 않았다. 메인을 Spring으로 두고, FastAPI, React와 통신하면서 생기는 일에 대해 적으려고 한다.</p>
<h3 id="1-server-to-server-archtecture">1. Server to Server Archtecture</h3>
<p>&nbsp;요즘 이슈가 되고 있는 내용인데, AI 응답을 API로 받아볼 수 있다. LLM의 수익 구조가 이제는 API 중심이 될거라나 뭐라나.
&nbsp;&nbsp;아무튼, 우리 서비스는 Spring서버와 FastAPI 서버를 따로 두어, FastAPI에서 AI 호출 및 응답을 반환하고, Spring 서버에서 그 AI 응답을 받기 위한 데이터셋을 Front-End측 RequestBody + DB query로 구성하여 AI 서버로 던지는 역할이다. AI 서버와 클라이언트가 직접 통신하는 구조도 괜찮을 것 같다고 생각하지만, 우리 서비스는 클라이언트가 요청했을 때의 시간과 장소 데이터를 추후 다른 API로 조회하려고 하므로, 이러한 요청을 묶어서 Spring에서 처리하기로 했다.</p>
<p>&nbsp;그렇다면, 메인 백서버에서 어떻게 AI서버로 통신해야 할까가 주된 관심사였다.</p>
<blockquote>
<p><strong>&quot;프론트 컨트롤러와 연결된 서비스 계층에서 해결해야 하는가?&quot;</strong>
-&gt; 서비스의 책임이 너무 무거워짐 (DB 쿼리, 데이터 파싱, AI 호출 ... )</p>
</blockquote>
<p>따라서, AI 서버와 통신하기 위한 새로운 패키지를 만들기로 했다.</p>
<ul>
<li>domain : 의존해야 하는 객체끼리 모아두는 패키지. (의존 범위 국소적)</li>
<li>global : 전역적으로 처리해야할 것들(ex. http 통신 예외, Security, JWT, 공통 응답 객체 등)</li>
<li>AI : AI 서버와 통신하기 위한 RestTemplate, 그리고 DTO를 통해 호출 및 반환</li>
</ul>
<h3 id="call-ai-server-code">Call AI Server Code</h3>
<pre><code class="language-java"> public ResponseEntity&lt;SuccessResponse&lt;?&gt;&gt; callAiServer(ToAiReq req) {
        URI uri = UriComponentsBuilder
                .fromUriString(&quot;http://localhost:5000&quot;) // FastAPI 서버 띄워지는 주소로 변경
                .path(&quot;/ai&quot;) // 해당 path로 수정 요망
                .encode()
                .build()
                .toUri();

        // 추후에 Object를 받을 Res로 선언. 이것도 global 패키지에 선언하면 될듯 -&gt; 완
        ResponseEntity&lt;SuccessResponse&lt;?&gt;&gt; responseEntity = restTemplate
                .exchange(uri,
                        HttpMethod.POST,
                        new HttpEntity&lt;&gt;(req),
                        new ParameterizedTypeReference&lt;SuccessResponse&lt;?&gt;&gt; () {}
                );

        if(responseEntity.getStatusCode().is2xxSuccessful()) {
            return responseEntity;
        } else {
            log.info(&quot;AI 서버 호출 실패&quot; + responseEntity.getStatusCode());
            throw new RuntimeException(&quot;AI 서버 호출 실패&quot; + responseEntity.getStatusCode());
        }
    }</code></pre>
<p>이 코드에는 적지 않았지만 아래와 같은 추가적인 설정을 해두었다.</p>
<ul>
<li><code>RestTemplate</code> : global/configuration에 <code>@Bean</code>으로 등록하고, 클래스 private 상수로 선언</li>
<li>이 메서드의 주인인 구현체는 <code>@Component</code> 등록</li>
</ul>
<p>&nbsp;RestTemplate에 선언된 여러 메서드들이 있지만, 여기에 쓰여진 것과 같이 ResponseEntity를 제네릭 클래스로 하려면, <code>RestTemplate.exchange()</code>가 유용한 것 같다. - 같다. 라고 이야기하는 이유는, 아직 자세히 공부하지 않아서 확정짓기 힘들어서 이렇게 적었다. 나중에 공부해볼 예정 -
&nbsp;&nbsp;그래서 이 메서드를 사용했고, 이후에 조건문으로 성공 여부를 확인하고 반환하는 모습이다. - 이것도 FastAPI 개발자와 이야기해봐야 할 것 같지만, 나머지 400이나 500번대 상태코드로 반환하는 로직을 짜두셨다면, 그냥 조건문째로 날릴 예정이다. - 그리고 반환 타입은 그냥 Http 응답 객체인 ResponseEntity로 주어서, 이것 그대로 프론트 엔드로 전달하려고 한다.</p>
<p>아직 객체지향 설계에 대해서 잘 아는 것은 아니라서, 나중에 더 공부해볼 생각이다.</p>
<ul>
<li>서버와 서버 간의 통신에 대해 책임 분리하는 법</li>
<li>RestTemplate의 동작 방식</li>
<li>타 서버 응답을 파싱해야 할 경우, 어떻게 해야 하는지</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Week9]]></title>
            <link>https://velog.io/@aya-milk/Week9</link>
            <guid>https://velog.io/@aya-milk/Week9</guid>
            <pubDate>Tue, 01 Jul 2025 09:43:59 GMT</pubDate>
            <description><![CDATA[<h3 id="global-패키지와-예외처리">Global 패키지와 예외처리</h3>
<hr>
<blockquote>
<h3 id="exception-처리">Exception 처리</h3>
</blockquote>
<ul>
<li>예외가 발생할 수 있는 계층&nbsp;<ul>
<li>모든 컨트롤러, 모든 도메인 등...
→ <code>Global package</code>에서 제어
&nbsp;&nbsp;</li>
</ul>
</li>
<li>In Business logic<pre><code> → 해당 도메인 내에서 예외처리 반환</code></pre></li>
</ul>
<hr>
<h2 id="conststaticvalue">const/StaticValue</h2>
<hr>
<p>Http Status 관련해서 정적, 불변으로 관리하기 위한 클래스.</p>
<pre><code class="language-java">public class StaticValue {

    // Success Code
    public static final int OK = 200;
    public static final int CREATED = 201;
    public static final int NO_CONTENT = 204;

    // Error Code
    public static final int BAD_REQUEST = 400;
    public static final int UNAUTHORIZED = 401;
    public static final int FORBIDDEN = 403;
    public static final int NOT_FOUND = 404;
    public static final int METHOD_NOT_ALLOWED = 405;
    public static final int CONFLICT = 409;
    public static final int INTERNAL_SERVER_ERROR = 500;

}</code></pre>
<h2 id="">&nbsp;&nbsp;</h2>
<h2 id="response---responsecode">Response - ResponseCode</h2>
<hr>
<p>Front-end 와 통신하기 위하여, 만드는 Json 형태의 클래스</p>
<ul>
<li><h4 id="baseresponse">BaseResponse</h4>
</li>
</ul>
<pre><code class="language-java">@Getter
@ToString
@RequiredArgsConstructor
public class BaseResponse {
    private final Boolean isSuccess;
    private final String code;
    private final String message;

    private final String timeStamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;));

    // 성공 여부와 BaseResponseCode
    public static BaseResponse of(Boolean isSuccess, BaseResponseCode baseResponseCode) {
        return new BaseResponse(isSuccess, baseResponseCode.getCode(), baseResponseCode.getMessage());
    }
    // 성공 여부와 BaseResponse, 메세지
    public static BaseResponse of(Boolean isSuccess, BaseResponseCode baseResponseCode, String message) {
        return new BaseResponse(isSuccess, baseResponseCode.getCode(), message);
    }
    // custom - 성공여부, 코드, 메세지 전부
    public static BaseResponse of(Boolean isSuccess, String code, String message) {
        return new BaseResponse(isSuccess, code, message);
    }</code></pre>
<ul>
<li><code>isSuccess</code> : 통신 성공 여부</li>
<li><code>code</code> : Http Status 중, 어떤 상태인지 (200 OK, 403 Forbidden ...)</li>
<li><code>message</code> : 실패한 이유
&nbsp;
&nbsp;</li>
<li><h4 id="success--errorresponse">Success &amp; ErrorResponse</h4>
<pre><code class="language-java">// Success Case
// 200 OK 응답
  public static &lt;T&gt; SuccessResponse&lt;T&gt; from(T data) {
      return new SuccessResponse&lt;&gt;(data, SuccessResponseCode.SUCCESS_OK);
  }
</code></pre>
</li>
</ul>
<p>// Error Case
// No Data
    public static ErrorResponse&lt;?&gt; from(BaseResponseCode baseResponseCode) {
        return new ErrorResponse&lt;&gt;(null, baseResponseCode);
    }</p>
<pre><code>&amp;nbsp;
&amp;nbsp;
--- 
## Exception
---
RunTime 중에서, 터지는 예외에 대해 알맞게 분기하여 예외를 던짐.

- #### Global
    `BaseResponseCode`를 상속받아서, `isSuccess`, `code`, `message`를 알맞게 입력해줌
    `@RestControllerAdvice` 를 통해서, 예외 발생 시, 자동으로 해당 예외를 던짐

``` java
    // @RequesBody - Valid(Validated)
    // like NotNull, NotBlank ...
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity&lt;ErrorResponse&lt;?&gt;&gt; handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error(&quot;MethodArgumentNotValidException : {}&quot;, e.getMessage(), e);
        ErrorResponse&lt;?&gt; errorResponse = ErrorResponse.of(
                ErrorResponseCode.INVALID_HTTP_MESSAGE_BODY,
                e.getFieldError().getDefaultMessage());
        return ResponseEntity.status(errorResponse.getHttpStatus()).body(errorResponse);
    }</code></pre><ul>
<li><h4 id="domain">Domain</h4>
  해당 Entity 내에서,
  <code>enum</code> - BaseResponseCode 상속,
  <code>exception</code> - BaseException 상속
  클래스 두 개를 정의하고, 던질 예외에 맞는 Http Status를 오버라이딩한다.</li>
</ul>
<pre><code class="language-java">// UserErrorCode
@Getter
@AllArgsConstructor
public enum UserErrorCode implements BaseResponseCode {
    USER_ALREADY_EXIST_409(&quot;USER_409&quot;, CONFLICT, &quot;이미 존재하는 사용자입니다.&quot;);

    private final String code;
    private final int httpStatus;
    private final String message;

}

// UserAlreadyExsistException
public class UserAlreadyExistException extends BaseException {
    public UserAlreadyExistException() { super(UserErrorCode.USER_ALREADY_EXIST_409); }
}</code></pre>
<p>&nbsp;
&nbsp;</p>
<hr>
<h2 id="cors-에러와-webconfig">CORS 에러와 WebConfig</h2>
<hr>
<p><code>Cross-Origin-Resource-Shared</code> : 서로 출처가 다른 자원 요구 중
이라는 뜻이다. 원래 브라우저는 같은 출처에서의 자원끼리만 공유되게끔 설정해두어서,</p>
<blockquote>
<p><code>http://localhost:8080</code> &lt;-&gt; <code>http://ANOTHER_URL</code></p>
</blockquote>
<p>위와 같은 방식으로 자원을 공유하려고 시도할 시, <code>CORS 에러</code>가  발생한다.</p>
<p>이를 설정으로 허용해줄 수 있는데, 그 방법은 다음과 같다.</p>
<pre><code class="language-java">@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping(&quot;/**&quot;)
                .allowedOriginPatterns(&quot;*&quot;)
                /* 위의 경우, 여러 URL의 CORS를 허용하고 싶을 때
                 * 현재방식의 경우, WildCard 문자인 &#39;*&#39;로 모든 URL 허용중이다.
                 * allowedOrigins() &lt; 임의의 1개 도메인 허용
                 */
                .allowCredentials(true);
    }
}
</code></pre>
<hr>
<h2 id="추가적으로">추가적으로...</h2>
<hr>
<p>Spring의 MVC 패턴(Dispatcher Servlet 등)이라던가,
예외를 던졌을 때 어떻게 잡아지는지,
Front-End와 통신할 때 발생할 수 있는 여러가지 예외,
그리고 비즈니스 로직 간 예외가 발생했을 때 어떻게 대처해야 하는지?</p>
<p>이런 것들을 공부해서, 예외 케이스를 마음껏 설정해두고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Week 6]]></title>
            <link>https://velog.io/@aya-milk/Week-6</link>
            <guid>https://velog.io/@aya-milk/Week-6</guid>
            <pubDate>Mon, 12 May 2025 16:35:10 GMT</pubDate>
            <description><![CDATA[<h3 id="게시글-조회-비즈니스-로직">게시글 조회 비즈니스 로직</h3>
<hr>
<p><code>게시글 단건 조회</code> - Get</p>
<blockquote>
<ul>
<li>postId로 findById</li>
</ul>
</blockquote>
<ul>
<li><p>RequestBody - null</p>
</li>
<li><p>Controller</p>
<pre><code class="language-java">@GetMapping(&quot;/{postId}&quot;)
  public ResponseEntity&lt;SuccessResponse&lt;?&gt;&gt; getPostById(
  @PathVariable Long postId) {
      // 서비스 계층에 위임
      PostDetailRes postDetailRes = postService.getById(postId);

      // 반환
      return ResponseEntity
              .status(HttpStatus.OK)
              .body(SuccessResponse.ok(postDetailRes));
  }</code></pre>
<p>(1) URL이 /api/post/{postId}이므로, Mapping
(2) findById로 찾은 값을 PostDetailRes에 그대로 저장
(3) 그 값을 그대로 반환 (상태나 반환이 성공적으로 이루어짐)</p>
</li>
<li><p>DTO - PostDetailRes</p>
<pre><code class="language-java">// API 명세상, data를 전부 반환해야 할 때 사용할 Record
public record PostDetailRes(
      Long id,
      String title,
      String content,
      String username,
      String password,
      PostState state,
      LocalDateTime createdAt,
      LocalDateTime updatedAt
) {
}</code></pre>
<p>&nbsp;</p>
</li>
<li><p>Service 계층</p>
<pre><code class="language-java">@Override
  public PostDetailRes getById(Long postId) {
      // 1. postId에 해당하는 Post - DB에서 조회
      Post post = postRepository.findById(postId)
              // 404 - postId에
              .orElseThrow(PostNotFoundException::new);

      // 2. PostDetailRes 반환
      return new PostDetailRes(
              post.getId(),
              post.getTitle(),
              post.getContent(),
              post.getUsername(),
              post.getPassword(),
              post.getState(),
              post.getCreatedAt(),
              post.getUpdatedAt()
      );
  }</code></pre>
</li>
<li><p>Exception 처리를 위해 새로운 패키지와 Exception 클래스를 만듦</p>
</li>
</ul>
<hr>
<p><code>게시글 전체 조회</code> - Get</p>
<blockquote>
<ul>
<li>findAll로 전체를 조회한다.</li>
</ul>
</blockquote>
<ul>
<li><p>stream 같은 반복문으로 전체 게시글을 List화</p>
</li>
<li><p>Controller</p>
<pre><code class="language-java">@GetMapping
  public ResponseEntity&lt;SuccessResponse&lt;?&gt;&gt; getAllPosts() {
      // 서비스 로직
      PostSummaryRes postSummaryRes = postService.getAll();

      // 반환
      return ResponseEntity
              .status(HttpStatus.OK)
              .body(SuccessResponse.ok(postSummaryRes));
  }</code></pre>
<p>ResponseEntity - 클라이언트 요청에 의해 서브되는 데이터(= 응답 데이터)
SuccessResponse - API 명세서 형식을 따르는 상태코드 등의 정보 포함 (개발자가 직접 짜야 하는 것 같다.)</p>
</li>
<li><p>PostSummaryRes - Java에서 제공되는 stream을 통하여 List에 모든 게시글 저장할 수 있는 클래스</p>
</li>
<li><p>그 값을 그대로 반환한다.</p>
</li>
</ul>
<p>&nbsp;</p>
<ul>
<li><p>DTO - PostSummaryRes</p>
<pre><code class="language-java">// 배려해주셔서 for문으로 작성하셨다. stream 사용법 숙지 요망
public record PostSummaryRes(
      List&lt;PostSummary&gt; postSummaryList
) {
  public record PostSummary(
          Long id,
          String title,
          String username,
          LocalDateTime createdAt
          ) {
  }
}</code></pre>
<p>&nbsp;</p>
</li>
<li><p>Service</p>
<pre><code class="language-java">@Override
  public PostSummaryRes getAll() {
      // 1. DB 에서 모든 Post 조회 (postRepository)
      List&lt;Post&gt; posts = postRepository.findAll();

      // 2. posts -&gt; PostSummaryRes 변환
      List&lt;PostSummary&gt; postSummaryList = new ArrayList&lt;&gt;();
      for(Post post : posts) {
          PostSummary postSummary = new PostSummary(
                  post.getId(),
                  post.getTitle(),
                  post.getUsername(),
                  post.getCreatedAt()
          );
          postSummaryList.add(postSummary);
      }

      // 3. 반환
      return new PostSummaryRes(postSummaryList);
  }</code></pre>
</li>
</ul>
<hr>
<p><code>게시글 수정</code> - Put</p>
<blockquote>
<ul>
<li>비밀번호가 일치해야 수정할 수 있다.</li>
</ul>
</blockquote>
<ul>
<li><p>제목과 내용만 수정한다.</p>
</li>
<li><p>Controller</p>
<pre><code class="language-java">@PutMapping(&quot;/{postId}&quot;)
  public ResponseEntity&lt;SuccessResponse&lt;?&gt;&gt; modifyPost(
          @PathVariable Long postId,
          @RequestBody ModifyPostReq modifyPostReq
  ) {
      // 서비스
      PostDetailRes postDetailRes = postService.modifyOne(postId, modifyPostReq);
      // 반환
      return ResponseEntity
              .status(HttpStatus.OK)
              .body(SuccessResponse.ok(postDetailRes));
  }</code></pre>
</li>
<li><p>URL Mapping </p>
</li>
<li><p>RequestBody도 있음</p>
</li>
<li><p>서비스 계층에 위임</p>
</li>
</ul>
<p>&nbsp;</p>
<ul>
<li><p>DTO - ModifyPostReq</p>
<pre><code class="language-java">@Getter
@NoArgsConstructor
public class ModifyPostReq {
  private String title;
  private String content;
  private String password;
}</code></pre>
<p>&nbsp;</p>
</li>
<li><p>Service</p>
<pre><code class="language-java">@Transactional
  @Override
  public PostDetailRes modifyOne(Long postId, ModifyPostReq modifyPostReq) {
      // 1. DB 에서 postId로 Post 찾기
      Post foundPost = postRepository.findById(postId)
          // 404 - 게시글 없음
              .orElseThrow(PostNotFoundException::new);

      // 2. 비밀번호 검증
      // 403 - 비밀번호 불일치
      if(!foundPost.getPassword().equals(modifyPostReq.getPassword())) {
          throw new InvalidPasswordException();
      }

      // 3. post 수정
      foundPost.modify(modifyPostReq.getTitle(), modifyPostReq.getContent());
      // PostDetailRes 반환
      return new PostDetailRes(
              foundPost.getId(),
              foundPost.getTitle(),
              foundPost.getContent(),
              foundPost.getUsername(),
              foundPost.getPassword(),
              foundPost.getState(),
              foundPost.getCreatedAt(),
              foundPost.getUpdatedAt()
      );
  }</code></pre>
</li>
<li><p>수정 등 정보의 변경이 동시에 이루어지면 안되는 작업 - Transactional Annotation</p>
</li>
<li><p>Post에 modify 메소드 생성하고, 그 메소드로 수정 (일종의 Setter)</p>
</li>
</ul>
<hr>
<p><code>게시글 삭제</code> - Delete</p>
<blockquote>
<ul>
<li>findById에 의해 delete 되어야 함</li>
</ul>
</blockquote>
<p>&nbsp;</p>
<ul>
<li><p>Controller</p>
<pre><code class="language-java">@DeleteMapping(&quot;/{postId}&quot;)
  public ResponseEntity&lt;SuccessResponse&lt;?&gt;&gt; deletePost(
          @PathVariable Long postId,
          @RequestBody DeletePostReq deletePostReq) {
      // 서비스 로직
      postService.deleteOne(postId, deletePostReq);
      // 반환
      return ResponseEntity
              .status(HttpStatus.OK)
              .body(SuccessResponse.empty());
  }</code></pre>
</li>
<li><p>URL Mapping</p>
</li>
<li><p>empty - Data(API)가 null, 즉 아무런 값도 없을 때 어떠한 데이터도 입력받지 않으므로 새로 정의함. Success.ok(null)과 똑같이 작용한다.</p>
</li>
</ul>
<p>&nbsp;</p>
<ul>
<li><p>Service</p>
<pre><code class="language-java">@Transactional
@Override
public void deleteOne(Long postId, DeletePostReq deletePostReq) {
  // 1. 게시글 존재 확인
  Post post = postRepository.findById(postId)
      // 404 - 게시글 존재하지 않음
      .orElseThrow(PostNotFoundException::new);

  // 2. 비밀번호 검증
  if(!post.getPassword().equals(deletePostReq.getPassword())) {
          // 403 - 비밀번호 불일치
          throw new InvalidPasswordException();
      }

  // 3. 삭제
  postRepository.delete(post);
}</code></pre>
</li>
<li><p>여태 작성하지 않았지만, Repository가 JpaRepository를 상속받았을 경우, <code>save</code>, <code>add</code>, <code>delete</code> 같은 명령어를 쓸 수 있다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[week5]]></title>
            <link>https://velog.io/@aya-milk/week5</link>
            <guid>https://velog.io/@aya-milk/week5</guid>
            <pubDate>Fri, 09 May 2025 13:37:17 GMT</pubDate>
            <description><![CDATA[<h3 id="개체-간-연관관계">개체 간 연관관계</h3>
<hr>
<p>&nbsp;개체는 데이터베이스의 테이블이라고 보아도 무방하다. 데이터베이스에선 외래키가 존재하듯, 개체들이 서로 공유하는 필드를 Spring에선 JoinColumn이라고 본다.</p>
<ul>
<li>단방향 연관관계 : 역방향에서 참조될 일이 없을 때 사용한다.</li>
<li>양방향 연관관계 : 상호간 참조가 필요할 때 사용한다.</li>
</ul>
<p>관계형 데이터 모델이 제시하는 개체 간 연관관계는 1:1, 1:M, N:M등이 있는데, 여기서 다대다 연관관계는 비즈니스 로직상 구현이 어렵다. 그렇기에 중간에 거쳐가는 개체를 하나 만들어서 일대다 일대다인 관계 2개로 나누어 로직을 구성한다
<code>양방향 연관관계</code>는 연관관계의 주인이 필요하다.</p>
<blockquote>
<p>Member &lt;- ManyToOne -&gt; Team</p>
</blockquote>
<p>위와 같은 관계에서는 Member가 연관관계의 주인 annotation을 달게된다.</p>
<h3 id="상속관계-매핑">상속관계 매핑</h3>
<hr>
<p>&nbsp;공통 필드를 가지는 개체들이 있을 수 있다.</p>
<blockquote>
<p>Book(id, name, price, ...)
Albom(id, name, price, ...)</p>
</blockquote>
<p>&nbsp;id, name, price필드가 중복되며, 이 필드들은 새로운 Product라는 부모 개체로 선언하는 것이 타당하겠다.
&nbsp;&nbsp;이 상속관계를 매핑하는 법도 여러가지 있는데, Join전략을 추천한다. Join 전략이 쿼리가 다소 느릴지언정 무결성 보장이 쉽기 때문이
다.</p>
<h3 id="프록시">프록시</h3>
<hr>
<p>&nbsp;위에서 사용했던 Member와 Team 개체를 예시로 들어보겠다.
<code>select member_id from member;</code> 이 SQL은 Team을 조회하지 않지만, em.find와 같은 명령으로 SQL을 보내면 N+1문제를 일으킨다. 말 그대로 1개의 SQL을 보내려고 했지만, N개의 SQL이 함께 딸려서 전송되는 것이다. 이 때 사용되는 것이 지연 로딩과 프록시 객체 타입이다. 지연 로딩을 사용함으로서 실제로 조회되지 않는 개체의 클래스 타입을 <code>프록시 타입</code>으로 반환시켜 SQL의 효율을 높인 것이다.
&nbsp;
&nbsp;
&nbsp;</p>
<ul>
<li>영속성 전이 : 모 개체를 영속성 컨테이너에 올릴 때, 같이 올려야 할 개체가 있다면 영속성 전이를 이용할 수 있다.</li>
<li>고아 객체 : 부모를 잃은 개체로서, 부모가 사라지면 같이 사라지게 할 수 있다. (게시글 삭제 시, 댓글 등을 같이 삭제하는 로직 정도?)</li>
<li>불변 객체 : 필드값 초기화만 이루어지고, 이후 수정이 불가. (집 주소를 1개만 설정할 때?)</li>
<li>값 타입 컬렉션 : 집 주소를 여러개 가져야 하는 등의 상황이 발생했을 때 사용. List&lt;&gt; new ArrayList~~</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Week_3]]></title>
            <link>https://velog.io/@aya-milk/Week3</link>
            <guid>https://velog.io/@aya-milk/Week3</guid>
            <pubDate>Sun, 06 Apr 2025 11:40:00 GMT</pubDate>
            <description><![CDATA[<h3 id="database">Database</h3>
<p>&nbsp;데이터베이스는 데이터들의 저장소라고 보면 편하다. 그렇지만 왜 HDD, SDD 이런 식이 아니라 &#39;데이터베이스&#39;라고 따로 명명하여 부르는걸까?</p>
<p>&nbsp;&nbsp;데이터베이스는 여러 사용자들이 동시에 데이터를 확인할 수 있으며, DBA라고 부르는 Database Administarator가 데이터를 확인하려는 유저들에게 권한을 부여하여, 데이터의 보안을 지킬 수 있다. 또한, 데이터에 대한 수정이 이루어지면 즉각적으로 반영되어 바로 사용자들에게 수정된 데이터를 보여주기도 하며, 데이터의 중복이 생기면 자동적으로 제거하여 데이터의 무결성을 유지하기도 한다. 이렇듯 데이터에 대해서 아주 전문적으로 관리하는 체계를 지니고 있어, 요즈음 정보사회에서 매우 중요한 데이터를 저장하는 저장소로 사용하는 것이다.</p>
<p>&nbsp;&nbsp;흔히들 데이터베이스라고 부르지만, 사실은 우리가 부르는 DB의 정확한 명칭은 &#39;DBMS&#39;이다. Data Base Management System이라고 부르는 이것은 데이터베이스의 형태, 예를 들면 관계형이라던지, NoSQL 등을 명시하고, SQL(Structured Query Language) 표준을 따르며 유저들이 쉽게 데이터를 관리할 수 있게 해준다.</p>
<hr>
<h3 id="ddldata-definition-lang">DDL(Data Definition Lang)</h3>
<ul>
<li>데이터 정의어이며, CREATE / DROP / ALTER 등의 테이블, 스키마 설정에 관한 정보를 다룬다<blockquote>
<p>create table (&nbsp;&nbsp;&nbsp;<em>TABLE SCHEMA</em>&nbsp;&nbsp;&nbsp;)
drop table [&nbsp;&nbsp;<em>CASCADE</em>&nbsp;&nbsp;] <code>// CASCADE는 참조중인 테이블이 있더라도 삭제한다.</code>
alter table [&nbsp;&nbsp;<em>TABLE NAME</em>&nbsp;&nbsp;] <code>ADD / RENAME / MODIFY ...</code></p>
</blockquote>
<h3 id="dmldata-manipulate-lang">DML(Data Manipulate Lang)</h3>
</li>
<li>데이터 조작어이며, CRUD 관련하여 데이터를 다룬다. DDL이 테이블 관련 명령어라고 하면 DML은 레코드(튜플) 관련인 셈<blockquote>
<p>select [&nbsp;&nbsp;<em>TABLE NAME</em>&nbsp;&nbsp;] from [&nbsp;&nbsp;<em>FIELD</em>&nbsp;&nbsp;]
update <em>TABLE NAME</em> set <em>FIELD = DATA</em> where <em>조건</em>
insert into <em>TABLE NAME(&nbsp;&nbsp;FIELD1, FIELD2, ...&nbsp;&nbsp;)</em> values <em>(&nbsp;&nbsp;DATA1, DATA2, ...&nbsp;&nbsp;)</em>
delete from <em>TABLE NAME</em> where <em>조건</em></p>
</blockquote>
<h3 id="dcldata-control-lang">DCL(Data Control Lang)</h3>
</li>
<li>데이터 제어어이다. 주로 사용자의 권한을 부여 및 회수하며, 트랜잭션을 반영하거나, 취소할 때 사용한다. 추후 데이터베이스를 직접 설계하여 배포할 상황이 아니라면, 그렇게 많이 쓰지 않는다.</li>
</ul>
<hr>
<h3 id="spring-bean">Spring Bean</h3>
<p>&nbsp;Spring Bean으로 등록한다는 것은 스프링 컨테이너에서 객체를 생성하고 생명주기를 관리하는 기능을 사용하겠다는 뜻이다. 우리는 AppConfig같은 파일들에 MemberService, OrderService등의 인터페이스를 적용한 impl 클래스에 MemberRepository같은 의존성을 주입해주어 생성하게끔 명시해둔다. 이렇게 한다면, getBean같은 메소드를 호출하여, Spring Container가 만들어준 객체를 사용하기 때문에, 한번의 프로세스에 주소가 같은 객체를 사용할 수 있다. 하지만 문제도 있다. 같은 주소를 사용하기 때문에, Queue같이 먼저 들어온 요청을 손상 없이 처리하고 다음 요청을 처리해야 하는데, 요청을 전부 처리하지 못했는데 다음 요청으로 인해 객체가 손상될 수 있다.
&nbsp;&nbsp;또한, Bean이 생성되지 않아도 동작해야 할 수 있다. 그럴 땐, 여러가지 방법이 있는데 주로 사용되는 방법은 Optional&lt;&gt; Bean으로 주입해주는 것.
&nbsp;&nbsp;생성자 주입해주는 이유 = 컴파일 단계에서 오류를 발생시켜, 배포 전에 안전한지 체크할 수 있기 때문.
&nbsp;&nbsp;우리가 스프링 빈을 생성하고, 그 객체에 대해 Setter 메소드를 호출 할 수 있다. 예를 들어 네트워크 객체를 만든 후, Setter로 url을 지정하는 방식으로 말이다. 그렇다면, 객체이므로 생성될 때가 있고, 소멸될 때가 있지 않을까? 그것을 지정해줄 수 있는 Annotation이 있다!</p>
<pre><code class="language-java">@PostConstruct
public void init() { } // 의존관계 주입 후 객체 생성 시점을 콜백해준다.
@PreDestroy
public void close() { } // 객체가 소멸하는 시점을 콜백해준다.</code></pre>
<p>&nbsp;&nbsp;자바 표준이므로, 어느 정도까지는 굉장히 자유롭지만, 외부 라이브러리에는 적용하지 못한다는 단점이 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Week_2]]></title>
            <link>https://velog.io/@aya-milk/Week2</link>
            <guid>https://velog.io/@aya-milk/Week2</guid>
            <pubDate>Wed, 02 Apr 2025 05:59:21 GMT</pubDate>
            <description><![CDATA[<h3 id="spring-structure">Spring Structure</h3>
<p>&nbsp;스프링을 처음 사용하는 개발자가 주로 만지는 디렉토리나 파일은 src / test / build.gradle 정도로 나눌 수 있다. src는 주로 코드 파일들이 있는 곳, test는 test code를 작성해서, 비즈니스 로직에 문제가 없는지 확인하는 곳, build.gradle은 내가 개발하는 비즈니스 로직에 필요한 종속성들을 명시하는 곳이다.
&nbsp;&nbsp;스프링을 사용하는 이유는 몇가지 있다. 그중 한개는 스프링에 내장 WAS가 있다는 것이다. 이 점 덕분에 개발 단계에서 쉽게 피드백해볼 수 있다. 그리고, 내장되어 있는 유틸리티 기능도 많은 편이다.
&nbsp;&nbsp;스프링부트 프로젝트를 실행하게 되면, 내장되어 있는 WAS인 톰캣 서버가 8080번 포트로 통신을 시작한다. 그럼 이제 <a href="http://localhost:8080">http://localhost:8080</a> 에서 내가 작성한 코드가 실행된다.
&nbsp;&nbsp;화면은 어떻게 보게 될까? 바로 Thymeleaf 템플릿 엔진의 도움을 받는다. <a href="https://start.spring.io">https://start.spring.io</a> 에서 생성한 스프링 프로젝트의 파일 트리를 자세히 보다보면 template라는 디렉토리를 볼 수 있다. 우리는 여기에 html 파일을 작성하고, Mapping 어노테이션을 달고 있는 메소드의 return값을 html 파일명으로 주어 view resolver가 찾을 수 있도록 한다. 이 때, 매개하는 역할이 Thymeleaf다. 방식은 MVC 방식을 채택했다. Model, View, Controller 세가지 역할 분담으로 나뉘는 방식이라 MVC다. Model은 송수신하는 데이터의 format같은 역할이다. View는 이 데이터를 어떻게 보여주는가에 대한 역할이며, Controller는 이 Model과 View 사이에서 데이터와 상호작용하여 Model과 View에게 그 결과를 전달한다.</p>
<h3 id="uml---class-diagram">UML - Class Diagram</h3>
<blockquote>
<p>A Association &gt; B : A class references B class (참조)
A interitance &gt; B : A class inherits B class (상속)
A Realization &gt; B : A class implements B interface (구현)
A Dependency &gt; B : A class references B class(참조)
A Aggregation &gt; B : A 클래스와 B 클래스는 각각을 의존하는 객체가 동일하므로, 연관이 없지 않다
A Composition &gt; B : A class belongs-to B class / B class has-a A class</p>
</blockquote>
<p>Association &lt;=&gt; Dependency는 연관과 종속으로 이해하면 편할 것 같다. Association같은 경우는, 피의존 클래스의 변수와 원 클래스의 변수가 밀접한 연관이 있어, 수정에 민감하다. 그런 반면에 Dependency는 property와 method의 연관성으로, 피의존 클래스의 변수를 원 클래스 메소드 파라미터로 제공하는 관계로 이해하면 좋을듯.</p>
<p>Aggregation은 객체의 구성 요소로, 다른 연관 객체에 의해 연결되어 있지만, 각자는 직접적인 연관을 가지지 않는 관계.
Composition은 서로가 강하게 연관되어 있어, 서로 중 하나가 삭제된다면, 그 객체가 불완전해지는 경우.</p>
<hr>
<h3 id="dev">Dev</h3>
<p>&nbsp;실제로 개발해보는 단계이다.</p>
<blockquote>
<ul>
<li>회원 존재(이 때 회원은 그냥 회원과 VIP회원으로 등급이 나뉜다)</li>
</ul>
</blockquote>
<ul>
<li>상품 주문 가능</li>
<li>(추후 변동 가능) 정액 할인 기능</li>
<li>DB는 아직 정하지 않았다.</li>
</ul>
<p>&nbsp;&nbsp;우리는 객체지향의 SOLID 원칙을 생각하며 개발해야 한다. 그럼 진행 단계를 가볍게 살펴보자.</p>
<h3 id="설계">설계</h3>
<p>UML Class Diagram으로 우리가 필요한 클래스 간의 종속 관계를 명시해두면 개발하기 편하다. 여기서는 추후 이 페이지를 통한 재검토가 필요하지 않은 사항이므로, 생략토록 하겠다.</p>
<h3 id="class--interface">Class &amp; Interface</h3>
<p>&nbsp;우리는 회원이라는 객체와 회원가입, 조회, 주문 등의 함수가 필요하다. 그럼 Repository 객체와 Service &amp; Oreder Interface가 필요하겠다. 단, 이 Interface를 구현하는 클래스에서는, MemberRepository 객체를 직접 생성하지 않는다. AppConfig라는 파일을 따로 생성해서 생성자 주입을 통하여 각각의 구현 클래스에 의존성을 주입시킨다.</p>
]]></description>
        </item>
    </channel>
</rss>