<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>MAGNUM OPUS</title>
        <link>https://velog.io/</link>
        <description>html_programming_language</description>
        <lastBuildDate>Mon, 19 Oct 2026 14:51:22 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>MAGNUM OPUS</title>
            <url>https://velog.velcdn.com/images/dev_alchemist/profile/8f01117e-f84a-46e1-98ae-930d6f57247d/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. MAGNUM OPUS. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_alchemist" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Spring SOAP Web Services: WSDL과 XML 계약 이해하기]]></title>
            <link>https://velog.io/@dev_alchemist/Spring-SOAP-Web-Services-WSDL%EA%B3%BC-XML-%EA%B3%84%EC%95%BD-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_alchemist/Spring-SOAP-Web-Services-WSDL%EA%B3%BC-XML-%EA%B3%84%EC%95%BD-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 19 Oct 2026 14:51:22 GMT</pubDate>
            <description><![CDATA[<p>REST/JSON 방식이 대세여도 금융·공공·기업 시스템에서는 SOAP 기반 연계가 남아 있는 경우가 있다. SOAP은 단순한 XML 메시지가 아니라 <strong>메시지 Envelope, 계약(WSDL), 스키마(XSD)</strong>를 중심으로 데이터를 교환하는 프로토콜 규약이다.</p>
<h2 id="soap-요청-구조-이해하기">SOAP 요청 구조 이해하기</h2>
<pre><code class="language-xml">&lt;soap:Envelope xmlns:soap=&quot;http://schemas.xmlsoap.org/soap/envelope/&quot;
               xmlns:pay=&quot;https://example.com/payment&quot;&gt;
    &lt;soap:Body&gt;
        &lt;pay:GetPaymentStatusRequest&gt;
            &lt;pay:paymentId&gt;p-100&lt;/pay:paymentId&gt;
        &lt;/pay:GetPaymentStatusRequest&gt;
    &lt;/soap:Body&gt;
&lt;/soap:Envelope&gt;</code></pre>
<p>위 XML은 구조를 설명하기 위한 임의 예시다. 실제 요소의 이름·네임스페이스·자료형은 서비스 XSD에 따라 달라진다.</p>
<h2 id="wsdl과-xsd의-역할">WSDL과 XSD의 역할</h2>
<ul>
<li><strong>XSD</strong>: XML 데이터의 자료형과 구조를 정의한다.</li>
<li><strong>WSDL</strong>: 서비스의 작업, 요청·응답, 바인딩, 주소 등 계약을 기술한다.</li>
<li><strong>SOAP Envelope</strong>: 실제 전달되는 메시지를 감싼다.</li>
</ul>
<p>Spring Web Services에서는 <code>@Endpoint</code>와 <code>@PayloadRoot</code>, <code>@ResponsePayload</code> 같은 어노테이션으로 XML 요청을 처리할 수 있다. XSD로부터 JAXB Java 클래스를 생성해 강타입 객체로 메시지를 다루는 방식도 사용한다. Spring Boot 4에서는 웹서비스 스타터가 <code>spring-boot-starter-webservices</code>로 정리되어 있다.</p>
<h2 id="클라이언트-구현의-핵심">클라이언트 구현의 핵심</h2>
<p><code>WebServiceTemplate</code> 또는 <code>WebServiceGatewaySupport</code>로 SOAP 요청을 전송할 수 있다. 중요한 것은 HTTP 주소뿐 아니라 <strong>요청 네임스페이스, SOAP Action, TLS, 인증 방식</strong>을 계약에 맞추는 일이다.</p>
<h2 id="rest와-비교할-때">REST와 비교할 때</h2>
<table>
<thead>
<tr>
<th>기준</th>
<th>SOAP</th>
<th>REST 방식 API</th>
</tr>
</thead>
<tbody><tr>
<td>메시지 규격</td>
<td>XML Envelope</td>
<td>JSON 등 다양한 표현 가능</td>
</tr>
<tr>
<td>계약</td>
<td>WSDL/XSD가 중심</td>
<td>OpenAPI 등 선택 가능</td>
</tr>
<tr>
<td>오류</td>
<td>SOAP Fault</td>
<td>HTTP 상태 코드와 오류 본문</td>
</tr>
<tr>
<td>적용 사례</td>
<td>계약이 엄격한 기존 시스템</td>
<td>웹·모바일 서비스에 널리 사용</td>
</tr>
</tbody></table>
<p>SOAP을 다룰 때 가장 흔한 실수는 XML 태그 이름만 맞추고 네임스페이스 URI를 무시하는 것이다. 로컬 이름이 같아도 네임스페이스가 다르면 다른 요소다.</p>
<p><strong>SOAP의 핵심은 기술이 오래되었다는 사실보다, 외부 시스템과 맺은 데이터 계약을 정확히 구현하는 데 있다.</strong></p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>SOAP은 XML Envelope 기반 메시지 프로토콜이다.</li>
<li>WSDL과 XSD는 서비스 계약과 데이터 구조를 정의한다.</li>
<li>SOAP Body의 네임스페이스는 요소 이름만큼 중요하다.</li>
<li>Spring Web Services는 Endpoint와 Template 기반 API를 제공한다.</li>
<li>SOAP Fault는 REST HTTP 오류 응답과 구분해 처리한다.</li>
<li>외부 시스템 계약에 맞춘 인증·TLS·스키마 검증이 필요하다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Cache 실전 입문: Cacheable, Evict, TTL과 데이터 일관성]]></title>
            <link>https://velog.io/@dev_alchemist/Spring-Cache-%EC%8B%A4%EC%A0%84-%EC%9E%85%EB%AC%B8-Cacheable-Evict-TTL%EA%B3%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9D%BC%EA%B4%80%EC%84%B1</link>
            <guid>https://velog.io/@dev_alchemist/Spring-Cache-%EC%8B%A4%EC%A0%84-%EC%9E%85%EB%AC%B8-Cacheable-Evict-TTL%EA%B3%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9D%BC%EA%B4%80%EC%84%B1</guid>
            <pubDate>Mon, 19 Oct 2026 14:50:29 GMT</pubDate>
            <description><![CDATA[<p>상품 상세를 조회할 때 매 요청마다 데이터베이스에서 같은 내용을 가져오는 것은 비효율적일 수 있다. 캐시는 <strong>자주 사용하는 계산 또는 데이터 조회 결과를 재사용</strong>해 응답 지연과 DB 부담을 줄이는 방법이다.</p>
<h2 id="메서드-결과를-캐싱하기">메서드 결과를 캐싱하기</h2>
<pre><code class="language-java">import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class CatalogQueryService {
    private final CatalogRepository repository;

    public CatalogQueryService(CatalogRepository repository) {
        this.repository = repository;
    }

    @Cacheable(cacheNames = &quot;itemDetails&quot;, key = &quot;#itemId&quot;)
    public ItemDetail getDetail(long itemId) {
        return repository.findDetail(itemId);
    }
}</code></pre>
<p>위 코드에서 <code>CatalogRepository</code>와 <code>ItemDetail</code>은 별도 정의한 타입이며 실제 실행에는 캐시 제공자 설정과 <code>@EnableCaching</code>이 필요하다. 캐시가 적중하면 메서드 본문을 실행하지 않고 저장된 값을 재사용한다.</p>
<h2 id="데이터-수정-시-캐시는-어떻게-될까">데이터 수정 시 캐시는 어떻게 될까?</h2>
<pre><code class="language-java">import org.springframework.cache.annotation.CacheEvict;

@CacheEvict(cacheNames = &quot;itemDetails&quot;, key = &quot;#itemId&quot;)
public void updateItem(long itemId, UpdateItemRequest request) {
    repository.update(itemId, request);
}</code></pre>
<p>수정 이후 캐시를 비우면 다음 조회가 DB에서 새 값을 가져올 수 있다. 그러나 수정 도중 트랜잭션이 실패한 경우의 동작, 여러 캐시 키, 경합 상태 등은 별도로 고려해야 한다. 이 메서드는 수정 서비스 클래스에 배치하는 형태의 <strong>발췌 예제</strong>다.</p>
<h2 id="로컬-캐시와-분산-캐시">로컬 캐시와 분산 캐시</h2>
<table>
<thead>
<tr>
<th>방식</th>
<th>장점</th>
<th>유의점</th>
</tr>
</thead>
<tbody><tr>
<td>JVM 메모리 캐시</td>
<td>매우 빠르고 단순함</td>
<td>인스턴스마다 값이 다를 수 있음</td>
</tr>
<tr>
<td>Redis 등 분산 캐시</td>
<td>여러 인스턴스가 같은 캐시 사용</td>
<td>네트워크·운영·직렬화 비용 발생</td>
</tr>
</tbody></table>
<p>Spring Cache는 인터페이스 추상화에 가깝다. <strong>TTL</strong>이나 캐시 크기 제한은 사용 중인 캐시 제공자(Caffeine, Redis 등) 설정에 달려 있다.</p>
<h2 id="캐시를-붙이기-전에">캐시를 붙이기 전에</h2>
<ol>
<li>조회 병목을 프로파일링한다.</li>
<li>데이터가 얼마나 오래되어도 되는지 결정한다.</li>
<li>캐시 키의 식별 범위를 설계한다. 사용자별 데이터가 섞이지 않도록 주의한다.</li>
<li>캐시 미스가 동시에 폭증하는 상황을 대비한다.</li>
<li>적중률과 DB 부하 감소를 측정한다.</li>
</ol>
<p>특히 <strong>캐시 키에 사용자 권한·언어·통화 등 응답에 영향을 주는 차원이 빠지면 정보 노출이나 잘못된 응답이 발생</strong>할 수 있다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>Spring Cache는 메서드 결과 캐싱을 단순하게 적용할 수 있다.</li>
<li>Cacheable은 조회 결과를 재사용하고 Evict는 캐시 무효화를 돕는다.</li>
<li>TTL 및 용량 제한은 캐시 공급자 설정을 확인한다.</li>
<li>멀티 인스턴스 로컬 캐시는 서로 다른 값을 가질 수 있다.</li>
<li>캐시 키에는 응답을 바꾸는 주요 변수가 포함돼야 한다.</li>
<li>성능 개선은 적중률과 지연 시간 지표로 검증한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin으로 작성하는 Spring Boot: Java와 다른 점]]></title>
            <link>https://velog.io/@dev_alchemist/Kotlin%EC%9C%BC%EB%A1%9C-%EC%9E%91%EC%84%B1%ED%95%98%EB%8A%94-Spring-Boot-Java%EC%99%80-%EB%8B%A4%EB%A5%B8-%EC%A0%90</link>
            <guid>https://velog.io/@dev_alchemist/Kotlin%EC%9C%BC%EB%A1%9C-%EC%9E%91%EC%84%B1%ED%95%98%EB%8A%94-Spring-Boot-Java%EC%99%80-%EB%8B%A4%EB%A5%B8-%EC%A0%90</guid>
            <pubDate>Mon, 19 Oct 2026 14:49:42 GMT</pubDate>
            <description><![CDATA[<p>Spring Boot는 Java뿐 아니라 Kotlin으로도 구현할 수 있다. Kotlin은 <strong>널 안정성, 데이터 클래스, 간결한 문법</strong>에 강점이 있지만 Java 기반 라이브러리의 동작 방식까지 바뀌는 것은 아니다.</p>
<h2 id="데이터-클래스와-null-안정성">데이터 클래스와 Null 안정성</h2>
<pre><code class="language-kotlin">data class ItemDto(
    val id: Long,
    val name: String,
    val description: String? = null
)</code></pre>
<p><code>description</code>에만 <code>?</code>를 붙였기 때문에 널을 받을 수 있다. 런타임에서 외부 시스템이 잘못된 값을 전달하거나 Java 타입이 플랫폼 타입으로 노출되면 별도 검증이 필요할 수 있다.</p>
<h2 id="spring의-생성자-주입도-간결하게">Spring의 생성자 주입도 간결하게</h2>
<pre><code class="language-kotlin">import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@Service
class GreetingService {
    fun greeting(): String = &quot;새로운 상품을 확인하세요&quot;
}

@RestController
class GreetingController(private val service: GreetingService) {
    @GetMapping(&quot;/hello&quot;)
    fun hello() = service.greeting()
}</code></pre>
<p>주 생성자 하나면 별도 <code>@Autowired</code> 없이 의존성을 주입할 수 있다. 최신 Spring Initializr에서 Kotlin을 선택하면 필요한 Kotlin 플러그인과 의존성이 자동으로 생성된다.</p>
<h2 id="jpa와-kotlin의-프록시-문제">JPA와 Kotlin의 프록시 문제</h2>
<p>Kotlin 클래스는 기본적으로 <code>final</code>이다. 반면 JPA는 엔티티 생성자, 프록시, 바이트코드 처리 조건이 있다. Kotlin Spring/JPA 플러그인(<code>all-open</code>, <code>no-arg</code>)과 엔티티 설계를 확인해야 한다. 엔티티를 무조건 데이터 클래스로 만드는 것도 주의가 필요하다. <code>equals</code>/<code>hashCode</code>와 지연 로딩 필드가 예상치 못한 문제를 만들 수 있기 때문이다.</p>
<h2 id="coroutines와-webflux">Coroutines와 WebFlux</h2>
<p>Kotlin의 <code>suspend</code> 함수와 <code>Flow</code>는 비동기 코드를 순차적인 문법으로 표현할 수 있다. Reactor <code>Mono</code>·<code>Flux</code>와 연동할 수 있지만, 코루틴 함수 안에서 블로킹 DB 호출을 한다고 저절로 논블로킹이 되는 것은 아니다.</p>
<pre><code class="language-kotlin">import kotlinx.coroutines.delay

suspend fun loadSummary(): String {
    delay(100)
    return &quot;요약 준비 완료&quot;
}</code></pre>
<p><code>delay</code>는 스레드를 블로킹하지 않는 중단 함수의 예시다. 실제 DB·HTTP 호출도 적절한 드라이버와 클라이언트를 선택해야 한다.</p>
<p>Kotlin 도입은 Java를 모두 버리는 결정이 아니라 <strong>팀의 언어 선택</strong>이다. 언어보다 경계 분리, 테스트, 예외 처리, 관측성 원칙이 더 오래간다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>Kotlin의 널 안정성은 개발 시 오류를 줄이는 데 유용하다.</li>
<li>Spring의 생성자 주입을 주 생성자로 간결하게 표현할 수 있다.</li>
<li>JPA 사용 시 Kotlin final 클래스 및 기본 생성자 제약을 검토한다.</li>
<li>코루틴 suspend와 Flow는 비동기 코드를 다루는 도구다.</li>
<li>suspend 자체가 블로킹 호출을 논블로킹으로 바꾸지는 않는다.</li>
<li>Spring 설계 원칙은 Java와 Kotlin 모두 동일하다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring GraphQL 시작하기: 스키마, Query, Resolver, 관측성]]></title>
            <link>https://velog.io/@dev_alchemist/Spring-GraphQL-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-%EC%8A%A4%ED%82%A4%EB%A7%88-Query-Resolver-%EA%B4%80%EC%B8%A1%EC%84%B1</link>
            <guid>https://velog.io/@dev_alchemist/Spring-GraphQL-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-%EC%8A%A4%ED%82%A4%EB%A7%88-Query-Resolver-%EA%B4%80%EC%B8%A1%EC%84%B1</guid>
            <pubDate>Mon, 19 Oct 2026 14:48:48 GMT</pubDate>
            <description><![CDATA[<p>상품 상세 화면이 상품명·가격·판매자·리뷰 수만 필요하지만 기존 REST API는 불필요한 필드까지 반환할 수 있다. GraphQL은 <strong>클라이언트가 필요한 필드를 선언하는 질의 방식</strong>을 제공한다.</p>
<h2 id="rest와-graphql의-관점-차이">REST와 GraphQL의 관점 차이</h2>
<p>REST는 리소스와 HTTP 메서드 중심으로 설계한다. GraphQL은 Schema를 통해 타입과 조회·변경 작업을 정의한다. 둘 중 하나가 항상 우월한 것은 아니다. 캐싱, 권한, 운영 복잡도, 팀의 클라이언트 요구사항에 따라 선택한다.</p>
<h2 id="스키마로-api-계약-정의하기">스키마로 API 계약 정의하기</h2>
<p>Spring for GraphQL과 웹 스타터를 추가한 다음 <code>src/main/resources/graphql/schema.graphqls</code>를 정의한다.</p>
<pre><code class="language-graphql">type Query {
    product(id: ID!): Product
}

type Product {
    id: ID!
    name: String!
    price: Int!
}</code></pre>
<p><code>!</code>는 Null이 허용되지 않는다는 뜻이다. 서버가 실제로 Null을 반환하면 해당 필드 오류가 상위 결과에 영향을 줄 수 있으므로 타입 선언을 신중히 정한다.</p>
<pre><code class="language-java">import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;

record Product(String id, String name, int price) {}

@Controller
public class ProductGraphqlController {
    @QueryMapping
    public Product product(@Argument String id) {
        return new Product(id, &quot;무선 키보드&quot;, 49000);
    }
}</code></pre>
<pre><code class="language-graphql">query {
    product(id: &quot;p-10&quot;) {
        name
        price
    }
}</code></pre>
<p>위 예제는 저장소 대신 고정 객체를 반환한다. 실제 시스템에서는 Service/Repository 연결과 입력 검증이 필요하다.</p>
<h2 id="graphql의-흔한-성능-함정">GraphQL의 흔한 성능 함정</h2>
<p>중첩 필드마다 DB를 조회하면 REST의 N+1과 비슷한 문제가 발생한다. Spring GraphQL에서는 DataLoader 등을 통해 배치 로딩을 고려할 수 있다. 또한 사용자별 접근 권한, 최대 질의 깊이와 복잡도, 페이지네이션 정책이 필요하다.</p>
<h2 id="graphql-관측성">GraphQL 관측성</h2>
<p>오류가 발생했을 때 HTTP 상태 코드만으로 충분한 정보를 얻지 못할 수 있다. 필드 리졸버 처리 시간, 데이터 로더 병목, 에러 경로를 메트릭과 트레이스로 관측한다. 요청에 고객의 개인정보가 포함될 수 있으므로 질의 변수 전체를 로그에 남기지 않는다.</p>
<p><strong>GraphQL의 장점은 필요한 데이터를 선택적으로 요청하는 것이고, 과제는 복잡한 조회와 권한을 서버가 통제해야 한다는 점</strong>이다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>GraphQL은 타입화된 스키마로 API 계약을 정의한다.</li>
<li>QueryMapping과 Argument로 조회 리졸버를 구성할 수 있다.</li>
<li>클라이언트가 필요한 필드를 선택하는 방식이 핵심이다.</li>
<li>깊은 중첩 쿼리에는 N+1과 쿼리 복잡도 제한이 중요하다.</li>
<li>GraphQL도 리소스별 권한 검사가 필수다.</li>
<li>리졸버 단위 지표와 트레이스를 통해 성능을 추적할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot 배포 흐름: Docker 이미지에서 Kubernetes와 Azure까지]]></title>
            <link>https://velog.io/@dev_alchemist/Spring-Boot-%EB%B0%B0%ED%8F%AC-%ED%9D%90%EB%A6%84-Docker-%EC%9D%B4%EB%AF%B8%EC%A7%80%EC%97%90%EC%84%9C-Kubernetes%EC%99%80-Azure%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@dev_alchemist/Spring-Boot-%EB%B0%B0%ED%8F%AC-%ED%9D%90%EB%A6%84-Docker-%EC%9D%B4%EB%AF%B8%EC%A7%80%EC%97%90%EC%84%9C-Kubernetes%EC%99%80-Azure%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Mon, 19 Oct 2026 14:48:04 GMT</pubDate>
            <description><![CDATA[<p>로컬에서 <code>java -jar</code>로 정상 실행되는 애플리케이션이라도 서버에 배포하면 환경변수, 포트, 네트워크, 상태 확인 등 새로운 문제가 발생한다. 배포는 코드만 복사하는 작업이 아니라 <strong>실행 환경을 재현하는 과정</strong>이다.</p>
<h2 id="실행-가능한-jar-만들기">실행 가능한 JAR 만들기</h2>
<pre><code class="language-bash">./mvnw clean package
java -jar target/*.jar</code></pre>
<p>Windows에서는 Maven Wrapper의 <code>.cmd</code>를 사용하고, JAR 경로는 실제 생성된 파일명으로 지정한다.</p>
<h2 id="컨테이너-이미지-만들기">컨테이너 이미지 만들기</h2>
<p>Spring Boot Maven 플러그인은 Cloud Native Buildpacks를 활용해 이미지를 빌드할 수 있다.</p>
<pre><code class="language-bash">./mvnw spring-boot:build-image \
  -Dspring-boot.build-image.imageName=example/catalog-api:0.1.0</code></pre>
<p>빌드에 적합한 컨테이너 런타임이 필요하다. 이미지 이름은 레지스트리 정책에 맞게 바꾼다. 전통적인 Dockerfile을 작성할 수도 있지만, 먼저 Buildpacks와 Dockerfile의 차이를 이해하자.</p>
<h2 id="kubernetes에서는-프로세스가-아니라-워크로드를-관리한다">Kubernetes에서는 프로세스가 아니라 워크로드를 관리한다</h2>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog-api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: catalog-api
  template:
    metadata:
      labels:
        app: catalog-api
    spec:
      containers:
        - name: catalog-api
          image: example/catalog-api:0.1.0
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: catalog-api
spec:
  selector:
    app: catalog-api
  ports:
    - port: 80
      targetPort: 8080</code></pre>
<p>이 설정은 <strong>학습용 최소 예제</strong>다. 이미지를 노드에서 가져올 수 있는 레지스트리에 올려야 하고, 애플리케이션에 Actuator와 readiness 프로브 기능이 제대로 설정되어 있어야 한다. 운영에서는 리소스 요청/제한, 보안 컨텍스트, 롤링 업데이트, Secret 등을 추가한다.</p>
<pre><code class="language-bash">kubectl apply -f deployment.yaml
kubectl get pods
kubectl get svc
kubectl logs deployment/catalog-api</code></pre>
<h2 id="azure-배포를-생각할-때">Azure 배포를 생각할 때</h2>
<p>Azure에서도 실행 방식은 선택할 수 있다. 관리형 애플리케이션 서비스, 컨테이너 서비스, AKS 같은 쿠버네티스 기반 서비스는 운영 책임이 다르다. 처음부터 AKS를 사용하기보다 앱 규모와 운영 인력을 고려해 <strong>가장 단순한 적절한 호스팅</strong>을 고르는 것이 좋다.</p>
<p>민감한 연결 문자열은 Kubernetes Secret이나 클라우드 시크릿 관리 서비스를 통해 주입하고 이미지에 포함하지 않는다. Azure 관련 서비스·가격·지원 런타임은 변동하므로 배포 직전에 공식 문서를 확인해야 한다.</p>
<p><strong>배포 완료의 기준은 컨테이너가 Running인 것이 아니라, 실제 요청을 처리하고 장애 시 복구할 수 있는 상태</strong>다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>JAR 빌드와 컨테이너 이미지 빌드는 서로 다른 단계다.</li>
<li>Buildpacks를 활용하면 별도의 Dockerfile 없이 이미지를 만들 수 있다.</li>
<li>Kubernetes Deployment는 복제본, Service는 네트워크 접근을 관리한다.</li>
<li>Readiness와 Liveness의 목적은 다르다.</li>
<li>Secret과 환경변수는 애플리케이션 이미지에 하드코딩하지 않는다.</li>
<li>Azure 호스팅 옵션은 운영 난이도와 요구사항을 기준으로 선택한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot Actuator와 관측성: 로그·메트릭·트레이스 연결하기]]></title>
            <link>https://velog.io/@dev_alchemist/Spring-Boot-Actuator%EC%99%80-%EA%B4%80%EC%B8%A1%EC%84%B1-%EB%A1%9C%EA%B7%B8%EB%A9%94%ED%8A%B8%EB%A6%AD%ED%8A%B8%EB%A0%88%EC%9D%B4%EC%8A%A4-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_alchemist/Spring-Boot-Actuator%EC%99%80-%EA%B4%80%EC%B8%A1%EC%84%B1-%EB%A1%9C%EA%B7%B8%EB%A9%94%ED%8A%B8%EB%A6%AD%ED%8A%B8%EB%A0%88%EC%9D%B4%EC%8A%A4-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 19 Oct 2026 14:47:15 GMT</pubDate>
            <description><![CDATA[<p>애플리케이션이 실행된다고 해서 정상적으로 운영되고 있는 것은 아니다. 요청 지연, DB 커넥션 고갈, 외부 API 실패 같은 문제를 이해하려면 <strong>Observability(관측성)</strong>가 필요하다.</p>
<h2 id="세-가지-관측-수단">세 가지 관측 수단</h2>
<ul>
<li><strong>로그(Logs)</strong>: 개별 사건이 왜 발생했는지 기록한다.</li>
<li><strong>메트릭(Metrics)</strong>: 요청 수, 응답 시간, 오류 비율처럼 집계 가능한 수치다.</li>
<li><strong>트레이스(Traces)</strong>: 여러 서비스를 통과한 요청의 실행 경로를 연결한다.</li>
</ul>
<p>Spring Boot Actuator는 상태 확인, 정보, 메트릭 등 운영 엔드포인트를 제공한다. 스타터를 추가하면 기본적으로 <code>/actuator/health</code>를 사용할 수 있다.</p>
<pre><code class="language-yaml">management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      show-details: never</code></pre>
<pre><code class="language-bash">curl http://localhost:8080/actuator/health
# {&quot;status&quot;:&quot;UP&quot;}</code></pre>
<p>실제 출력은 헬스 인디케이터 구성과 애플리케이션 상태에 따라 달라진다. <code>/actuator</code> 아래의 모든 항목을 공개하지 않는다. 특히 <code>env</code>, <code>configprops</code>, <code>heapdump</code> 같은 엔드포인트는 민감정보가 노출될 수 있다.</p>
<h2 id="sli와-slo의-차이">SLI와 SLO의 차이</h2>
<p>SLI는 사용자 체감 품질을 나타내는 지표이고 SLO는 그 지표의 목표치다. 예를 들어 주문 API의 95백분위 응답시간(p95), 5xx 비율, 결제 성공률을 지표로 정할 수 있다. CPU만 보는 것보다 사용자 경험과 직접 연결된다.</p>
<h2 id="요청-하나를-여러-서비스에서-추적하기">요청 하나를 여러 서비스에서 추적하기</h2>
<p>Micrometer Observation과 분산 트레이싱을 조합하면 서비스별 스팬(span)을 연결할 수 있다. Spring Boot 버전에 맞는 Micrometer/OpenTelemetry 스타터와 exporter를 선택한다. 운영 백엔드는 Prometheus/Grafana, OpenTelemetry Collector 등 환경에 맞게 구성한다.</p>
<pre><code class="language-text">HTTP 주문 요청 (traceId=abc)
  ├─ order-api span
  ├─ inventory-service span
  └─ payment-service span</code></pre>
<p>추적 ID를 로그와 함께 연결하면 특정 주문의 지연 원인을 좁힐 수 있다. 단, 로그에 주민번호·비밀번호·토큰·카드번호를 그대로 남기면 안 된다.</p>
<h2 id="오래된-관측-서비스-예제-읽는-방법">오래된 관측 서비스 예제 읽는 방법</h2>
<p>과거 Tanzu Observability/Wavefront 중심 설정은 서비스 정책이나 제품 상황이 달라질 수 있다. 특정 SaaS 연동 방법을 그대로 고정하기보다 <strong>Micrometer의 계측 개념과 exporter 구성</strong>을 중심으로 학습하는 편이 오래간다.</p>
<p>운영 환경에서는 <strong>헬스 체크, 지표 대시보드, 알림 임계값, 오류 추적</strong>이 함께 있어야 실제 장애 대응에 도움이 된다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>Actuator는 애플리케이션 운영 엔드포인트를 제공한다.</li>
<li>로그·메트릭·트레이스는 서로 다른 관찰 관점이다.</li>
<li>헬스 체크 결과는 사용 가능한 구성요소에 따라 달라질 수 있다.</li>
<li>운영 엔드포인트는 공개 범위와 인증 정책을 제한해야 한다.</li>
<li>SLI는 측정 지표, SLO는 그 지표의 목표다.</li>
<li>Micrometer와 OpenTelemetry는 계측과 관측 백엔드 연계를 돕는다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 테스트 전략: Controller, 통합 테스트, 계약 테스트, REST Docs]]></title>
            <link>https://velog.io/@dev_alchemist/Spring-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A0%84%EB%9E%B5-Controller-%ED%86%B5%ED%95%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B3%84%EC%95%BD-%ED%85%8C%EC%8A%A4%ED%8A%B8-REST-Docs</link>
            <guid>https://velog.io/@dev_alchemist/Spring-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A0%84%EB%9E%B5-Controller-%ED%86%B5%ED%95%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B3%84%EC%95%BD-%ED%85%8C%EC%8A%A4%ED%8A%B8-REST-Docs</guid>
            <pubDate>Mon, 19 Oct 2026 14:46:23 GMT</pubDate>
            <description><![CDATA[<p>웹 API를 개발할 때 성공 응답만 브라우저에서 확인하면 수정 과정에서 회귀 버그를 놓치기 쉽다. Spring 테스트는 <strong>테스트할 범위를 어디까지 띄울 것인지</strong>에 따라 크게 구분할 수 있다.</p>
<h2 id="테스트-피라미드-이해하기">테스트 피라미드 이해하기</h2>
<table>
<thead>
<tr>
<th>테스트</th>
<th>목적</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>단위 테스트</td>
<td>순수 비즈니스 규칙</td>
<td>빠르고 외부 의존성이 적다</td>
</tr>
<tr>
<td>웹 슬라이스 테스트</td>
<td>HTTP 라우팅/직렬화/검증</td>
<td>MVC 계층에 집중한다</td>
</tr>
<tr>
<td>통합 테스트</td>
<td>컨테이너와 실제 구성 통합</td>
<td>더 느리지만 실제 흐름에 가깝다</td>
</tr>
<tr>
<td>계약 테스트</td>
<td>서비스 사이 요청·응답 호환성</td>
<td>배포 독립성이 큰 환경에서 효과적이다</td>
</tr>
</tbody></table>
<h2 id="가장-작은-단위부터-검증하기">가장 작은 단위부터 검증하기</h2>
<pre><code class="language-java">import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class PriceCalculatorTest {
    @Test
    void discount() {
        int finalPrice = 20_000 - 20_000 * 15 / 100;
        assertEquals(17_000, finalPrice);
    }
}</code></pre>
<p>순수 계산 로직이 있다면 Spring 컨텍스트를 띄울 필요가 없다. 반면 <code>@WebMvcTest</code>는 Controller와 관련된 웹 인프라를 제한적으로 로딩한다.</p>
<pre><code class="language-java">import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(HealthController.class)
class HealthControllerTest {
    @Autowired MockMvc mvc;

    @Test
    void returnHealth() throws Exception {
        mvc.perform(get(&quot;/health-check&quot;))
                .andExpect(status().isOk())
                .andExpect(content().string(&quot;ok&quot;));
    }
}</code></pre>
<p>위 테스트는 <code>/health-check</code>에서 <code>ok</code>를 반환하는 <code>HealthController</code>가 있다는 가정의 발췌 예제다. Boot 4에서는 <code>@WebMvcTest</code> import 패키지가 구버전과 달라졌을 수 있으므로 생성된 프로젝트 의존성을 확인한다. 보안 필터가 포함된다면 인증 설정을 테스트에 맞춰야 한다.</p>
<h2 id="통합-테스트에서는-무엇을-확인할까">통합 테스트에서는 무엇을 확인할까?</h2>
<p><code>@SpringBootTest</code>는 전체 애플리케이션 컨텍스트를 띄운다. DB와 외부 인프라까지 검증해야 한다면 Testcontainers를 사용해 테스트용 컨테이너를 구성할 수 있다. 테스트가 실행 환경에 영향을 받지 않도록 데이터 초기화·정리 방식을 정해야 한다.</p>
<h2 id="소비자-주도-계약-테스트">소비자 주도 계약 테스트</h2>
<p>마이크로서비스 A가 B의 API를 호출한다면 B가 반환하는 응답 구조가 바뀌는 순간 A가 장애를 겪을 수 있다. <strong>Consumer-Driven Contract</strong>는 소비자에게 필요한 계약을 명세하고 제공자가 이를 충족하는지 검증한다. Spring Cloud Contract는 이 흐름을 지원한다.</p>
<h2 id="spring-rest-docs는-왜-테스트와-함께-쓰나">Spring REST Docs는 왜 테스트와 함께 쓰나?</h2>
<p>Spring REST Docs는 통과한 테스트에서 요청·응답 스니펫을 만들 수 있다. 문서가 코드에서 멀어지는 문제를 줄이지만 테스트에 없는 API는 자동으로 완벽히 문서화되지 않는다. 필드 설명, 오류 사례, 인증 조건도 테스트 대상에 포함해야 한다.</p>
<p><strong>좋은 테스트는 프레임워크를 많이 띄우는 테스트가 아니라, 실패했을 때 어떤 계약이 깨졌는지 바로 알려주는 테스트</strong>다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>비즈니스 규칙은 빠른 단위 테스트로 우선 검증한다.</li>
<li>웹 슬라이스 테스트는 Controller의 HTTP 계약에 집중한다.</li>
<li>통합 테스트는 실제 빈 구성과 DB 연결까지 검증한다.</li>
<li>Boot 4에서는 일부 테스트 자동 설정 패키지 이름이 다르다.</li>
<li>계약 테스트는 서비스 사이 API 호환성 문제를 조기에 찾는다.</li>
<li>Spring REST Docs는 테스트 결과를 문서의 근거로 사용한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Cloud로 마이크로서비스 연결하기: Gateway부터 Circuit Breaker까지]]></title>
            <link>https://velog.io/@dev_alchemist/Spring-Cloud%EB%A1%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0-Gateway%EB%B6%80%ED%84%B0-Circuit-Breaker%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@dev_alchemist/Spring-Cloud%EB%A1%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0-Gateway%EB%B6%80%ED%84%B0-Circuit-Breaker%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Mon, 19 Oct 2026 14:45:32 GMT</pubDate>
            <description><![CDATA[<p>서비스를 주문·결제·상품으로 분리하면 코드가 작아지는 대신 <strong>서버 주소 찾기, 설정 배포, 장애 전파</strong>가 새로운 문제가 된다. Spring Cloud는 이러한 분산 시스템 문제를 다루는 여러 도구를 제공한다.</p>
<h2 id="마이크로서비스-구성-요소">마이크로서비스 구성 요소</h2>
<pre><code class="language-text">클라이언트
   ↓
API Gateway
   ├── 상품 서비스
   ├── 주문 서비스 → 결제 서비스
   └── 사용자 서비스</code></pre>
<ul>
<li><strong>Gateway</strong>: 공통 경로 처리, 인증 연계, 요청 라우팅</li>
<li><strong>Service Discovery(Eureka 등)</strong>: 서비스 인스턴스 위치 등록·조회</li>
<li><strong>LoadBalancer</strong>: 여러 인스턴스 가운데 호출 대상을 선택</li>
<li><strong>Config Server</strong>: 여러 서비스의 환경별 설정을 중앙 관리</li>
<li><strong>Circuit Breaker</strong>: 장애가 난 의존 서비스로 무한정 요청을 보내지 않도록 차단</li>
</ul>
<p>모든 서비스에 이 다섯 가지가 필요하다는 뜻은 아니다. Kubernetes라면 서비스 검색을 자체 제공하기 때문에 Eureka를 별도로 쓰지 않을 수도 있다.</p>
<h2 id="gateway의-책임">Gateway의 책임</h2>
<p>클라이언트가 서비스마다 다른 주소를 알지 않아도 되도록 Gateway가 경로를 분배한다. 하지만 Gateway 하나에서 모든 비즈니스 권한을 판단하고 내부 서비스를 무방비로 공개하면 위험하다. 각 서비스에도 리소스 권한 검사가 필요하다.</p>
<h2 id="circuit-breaker의-상태-모델">Circuit Breaker의 상태 모델</h2>
<pre><code class="language-text">CLOSED  → 요청 정상 통과
OPEN    → 일정 기간 요청 빠르게 실패
HALF_OPEN → 제한된 시험 요청으로 복구 확인</code></pre>
<p>서킷 브레이커는 요청 지연과 장애 확산을 줄이지만, 실패한 요청의 비즈니스 결과를 복구해 주지는 않는다. 타임아웃, 재시도, 지수 백오프, 대체 응답을 <strong>함께</strong> 고려해야 한다. 특히 결제 API 재시도에는 중복 결제를 막을 멱등성 키가 필요하다.</p>
<h2 id="중앙-설정은-어떻게-다를까">중앙 설정은 어떻게 다를까?</h2>
<p>Config Server는 설정을 한곳에서 조회하게 하지만, <strong>설정 변경이 모든 실행 중 인스턴스에 즉시 반영되는 것은 아니다.</strong> 리프레시 방식과 롤링 배포 정책을 별도로 결정한다. 비밀정보는 Config Server 저장소에서 평문으로 관리하지 않는다.</p>
<h2 id="msa를-시작하기-전에">MSA를 시작하기 전에</h2>
<p>초기 서비스에서는 모놀리식 애플리케이션을 모듈화하는 것만으로도 충분할 수 있다. 네트워크 호출은 같은 프로세스의 메서드 호출보다 실패 모드가 많다. <strong>서비스 분할은 팀과 배포가 독립적으로 움직일 필요가 있을 때</strong> 의미가 커진다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>Gateway는 외부 요청을 내부 서비스에 연결한다.</li>
<li>서비스 디스커버리는 실행 중인 인스턴스의 위치를 찾는다.</li>
<li>LoadBalancer는 호출 대상 인스턴스를 선택한다.</li>
<li>Circuit Breaker는 장애 전파를 줄이지만 업무 복구를 대신하지 않는다.</li>
<li>중앙 설정과 비밀정보 저장은 별개의 운영 문제다.</li>
<li>Kubernetes 사용 시 일부 Spring Cloud 구성 요소는 불필요할 수 있다.</li>
<li>초기에는 잘 나눈 모놀리식 구조가 더 단순할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Batch와 Cloud Task로 대량 데이터 작업 나누기]]></title>
            <link>https://velog.io/@dev_alchemist/Spring-Batch%EC%99%80-Cloud-Task%EB%A1%9C-%EB%8C%80%EB%9F%89-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%91%EC%97%85-%EB%82%98%EB%88%84%EA%B8%B0</link>
            <guid>https://velog.io/@dev_alchemist/Spring-Batch%EC%99%80-Cloud-Task%EB%A1%9C-%EB%8C%80%EB%9F%89-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%91%EC%97%85-%EB%82%98%EB%88%84%EA%B8%B0</guid>
            <pubDate>Mon, 19 Oct 2026 14:44:40 GMT</pubDate>
            <description><![CDATA[<p>관리자가 매일 수만 건의 주문을 정산한다고 해 보자. 이를 하나의 API 요청에서 모두 처리하면 타임아웃이나 중간 실패가 발생하기 쉽다. <strong>배치 처리</strong>는 많은 데이터를 작업 단위로 나눠 읽고, 가공하고, 저장하는 방식이다.</p>
<h2 id="chunk-기반-배치의-기본-구성">Chunk 기반 배치의 기본 구성</h2>
<pre><code class="language-text">Job
 └─ Step
     ├─ ItemReader (데이터 읽기)
     ├─ ItemProcessor (변환·검증)
     └─ ItemWriter (저장)</code></pre>
<p>읽기·가공·저장을 일정 건수의 Chunk로 반복 처리한다. 예를 들어 500건마다 커밋하면 한 건의 실패로 전체 작업을 처음부터 다시 수행하지 않도록 설계할 수 있다. 다만 오류 발생 시 실제 재시작 지점은 Reader와 JobRepository의 상태 관리에 달려 있다.</p>
<h2 id="순수-처리-로직을-먼저-분리하자">순수 처리 로직을 먼저 분리하자</h2>
<pre><code class="language-java">import org.springframework.batch.item.ItemProcessor;

record RawPayment(long orderId, int amount, boolean refunded) {}
record Settlement(long orderId, int amount) {}

public class SettlementProcessor
        implements ItemProcessor&lt;RawPayment, Settlement&gt; {
    @Override
    public Settlement process(RawPayment item) {
        if (item.refunded()) return null; // 해당 항목 필터링
        return new Settlement(item.orderId(), item.amount());
    }
}</code></pre>
<p><code>null</code>은 Spring Batch의 Processor에서 항목을 필터링한다는 의미다. 전체 Job을 실행하려면 Reader·Writer·Step·Job·메타데이터 저장소 구성이 추가되어야 한다.</p>
<h2 id="재실행-가능하게-만드는-규칙">재실행 가능하게 만드는 규칙</h2>
<ul>
<li><strong>중복 쓰기 방지</strong>: <code>orderId</code> 같은 업무 키로 유니크 제약을 적용한다.</li>
<li><strong>JobParameters</strong>: 처리할 날짜와 실행 구분값을 명시한다.</li>
<li><strong>실패 복구</strong>: 재시작 가능한 Reader·Writer를 선택하고 체크포인트 상태를 관리한다.</li>
<li><strong>운영 지표</strong>: 성공/실패 건수와 소요 시간을 남긴다.</li>
</ul>
<p>Spring Cloud Task는 <strong>시작해서 작업을 수행한 뒤 종료하는 짧은 애플리케이션</strong>의 실행 정보를 관리한다. Batch는 내부 대량 처리 모델에, Task는 짧은 실행 작업의 수명 주기에 더 초점이 있다. 두 기술을 함께 사용하는 구성도 가능하다.</p>
<p>스케줄링은 언제 실행할지를 결정할 뿐, 재처리 정책을 자동으로 해결하지 않는다. 정산 같은 핵심 비즈니스에는 Batch의 체크포인트와 idempotency를 설계해야 한다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>Batch는 Job과 Step으로 대량 작업을 구성한다.</li>
<li>Chunk 방식은 처리 항목을 일정 단위로 트랜잭션 처리한다.</li>
<li>ItemProcessor에서 null을 반환하면 해당 항목을 필터링할 수 있다.</li>
<li>재실행 시 중복 반영을 막는 업무 키와 제약조건이 중요하다.</li>
<li>JobParameters와 실행 상태를 관리하면 복구를 설계하기 쉽다.</li>
<li>Cloud Task는 실행 후 종료하는 작업의 수명 주기를 다룬다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Integration과 Cloud Stream: 이벤트 파이프라인 구성하기]]></title>
            <link>https://velog.io/@dev_alchemist/Spring-Integration%EA%B3%BC-Cloud-Stream-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_alchemist/Spring-Integration%EA%B3%BC-Cloud-Stream-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 19 Oct 2026 14:43:53 GMT</pubDate>
            <description><![CDATA[<p>외부 시스템에서 파일을 읽고, 형식을 변환하고, 이벤트를 다른 서비스에 전달하는 작업은 단순 HTTP 컨트롤러만으로 관리하기 어렵다. <strong>메시지 기반 파이프라인</strong>은 데이터를 수집·가공·전달하는 단계를 분리하는 방법이다.</p>
<h2 id="spring-integration의-기본-구성">Spring Integration의 기본 구성</h2>
<p>메시지는 <code>payload</code>와 <code>headers</code>로 이루어진다. <code>MessageChannel</code>이 단계들을 연결하고, 어댑터는 파일·메일·메시징 시스템 등 외부 자원과 통신한다.</p>
<pre><code class="language-text">파일 수신 → 입력 채널 → 검증 → 변환 → 출력 채널 → 외부 전송</code></pre>
<pre><code class="language-java">import java.util.function.Function;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class EventFunctions {
    @Bean
    Function&lt;String, String&gt; normalizeEvent() {
        return value -&gt; value.trim().toUpperCase();
    }
}</code></pre>
<p>위 <code>Function</code>은 입력을 가공하는 <strong>순수 변환 함수</strong>다. Spring Cloud Stream의 함수형 모델과 함께 쓰면 <code>normalizeEvent-in-0</code>와 <code>normalizeEvent-out-0</code> 같은 바인딩 이름을 사용할 수 있다. 실제 메시징 브로커 연결에는 적합한 바인더 의존성과 설정이 필요하다.</p>
<h2 id="cloud-stream과-데이터-파이프라인">Cloud Stream과 데이터 파이프라인</h2>
<p>Spring Cloud Stream은 메시징 플랫폼을 추상화하여 함수를 이벤트 입출력에 연결한다. RabbitMQ/Kafka 같은 브로커마다 바인더가 다르며, 메시지 직렬화와 컨슈머 그룹을 분명히 설계해야 한다.</p>
<p>Spring Cloud Data Flow는 스트리밍/배치 파이프라인을 <strong>배포·실행·운영</strong>하는 플랫폼 성격이 강하다. 단순한 앱 안에서 몇 개 함수를 연결하는 것과 운영 부담이 다르다.</p>
<h2 id="데이터-품질과-장애-처리">데이터 품질과 장애 처리</h2>
<ul>
<li>스키마 버전을 명시하고 새로운 필드가 추가되어도 소비자가 깨지지 않도록 설계한다.</li>
<li>변환 단계마다 검증 실패 레코드의 처리 경로를 분리한다.</li>
<li>처리량·큐 적체·재시도 횟수를 관측한다.</li>
<li>메시지 순서 보장은 파티션/컨슈머 구성을 고려한다.</li>
</ul>
<p>Spring Integration이 모든 서비스의 필수 구성 요소는 아니다. <strong>파일·레거시 시스템·메시지 브로커가 복잡하게 연결되는 지점</strong>에서 장점이 커진다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>메시지 채널은 처리 단계를 느슨하게 연결한다.</li>
<li>Spring Integration은 다양한 외부 자원 연결을 돕는다.</li>
<li>Cloud Stream은 함수와 메시징 바인더를 연결한다.</li>
<li>Data Flow는 파이프라인 운영과 배포 관점의 도구다.</li>
<li>이벤트 스키마와 실패 레코드 처리 정책이 중요하다.</li>
<li>간단한 동기 API에는 과도한 메시징 인프라를 도입할 필요가 없다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[WebSocket·STOMP·RSocket로 실시간 기능 만들기]]></title>
            <link>https://velog.io/@dev_alchemist/WebSocketSTOMPRSocket%EB%A1%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EA%B8%B0%EB%8A%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@dev_alchemist/WebSocketSTOMPRSocket%EB%A1%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EA%B8%B0%EB%8A%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Mon, 19 Oct 2026 14:43:04 GMT</pubDate>
            <description><![CDATA[<p>채팅 서비스는 폴링으로도 만들 수 있다. 하지만 초당 여러 메시지가 오가는 경우 매번 HTTP 요청을 반복하는 것보다 <strong>연결을 유지하며 이벤트를 주고받는 방식</strong>이 자연스러울 수 있다.</p>
<h2 id="websocket과-stomp의-차이">WebSocket과 STOMP의 차이</h2>
<p>WebSocket은 양방향 통신 채널을 제공하는 프로토콜이다. STOMP는 그 위에서 목적지·구독·발행 같은 메시지 규칙을 제공한다. Spring은 STOMP 메시지 라우팅을 편리하게 구성해 준다.</p>
<pre><code class="language-java">import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;

@Configuration
@EnableWebSocketMessageBroker
public class ChatSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker(&quot;/topic&quot;);
        registry.setApplicationDestinationPrefixes(&quot;/app&quot;);
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint(&quot;/chat-socket&quot;)
                .setAllowedOriginPatterns(&quot;https://chat.example.com&quot;);
    }
}</code></pre>
<p>위 코드는 메시지 브로커와 연결 지점을 설정한다. Spring WebSocket 스타터와 클라이언트가 필요하며, <code>https://chat.example.com</code>은 배포 Origin으로 바꿔야 한다. 임의 Origin 전체 허용은 운영 서비스에 적절하지 않다.</p>
<pre><code class="language-java">import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

record ChatMessage(String roomId, String content) {}

@Controller
public class ChatMessageController {
    @MessageMapping(&quot;/messages&quot;)
    @SendTo(&quot;/topic/messages&quot;)
    public ChatMessage send(@Payload ChatMessage message) {
        return message;
    }
}</code></pre>
<p>브라우저는 <code>/app/messages</code>로 메시지를 보내고 <code>/topic/messages</code>를 구독할 수 있다. 이 예시는 모든 구독자에게 공개하는 형태이므로 실제 채팅방에서는 <strong>방별 라우팅, 인증, 접근 권한 검증</strong>을 반드시 추가한다.</p>
<h2 id="rsocket은-무엇이-다를까">RSocket은 무엇이 다를까?</h2>
<p>RSocket은 요청-응답 외에도 서버 스트리밍, 양방향 채널 등 여러 통신 모델을 지원한다. Kotlin <code>Flow</code>나 Reactor <code>Flux</code>와 조합하면 채팅·상태 스트리밍을 선언적으로 다룰 수 있다. 그러나 브라우저 지원, 팀 난이도, 배포 네트워크 호환성을 검토해야 한다.</p>
<h2 id="실시간-서비스-운영-이슈">실시간 서비스 운영 이슈</h2>
<ul>
<li>연결 종료 후 재연결과 누락 메시지 동기화는 별도 설계한다.</li>
<li>단일 서버의 단순 브로커는 다중 인스턴스 환경에서 한계가 있다.</li>
<li>메시지 크기·빈도 제한과 사용자별 권한 검사 없이는 악용될 수 있다.</li>
<li>메시지 영속 저장은 WebSocket/STOMP 자체가 보장하지 않는다.</li>
</ul>
<p><strong>실시간 전송 프로토콜과 메시지를 저장하는 시스템은 서로 다른 책임</strong>이다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>WebSocket은 통신 채널, STOMP는 메시지 라우팅 규칙이다.</li>
<li>Spring에서는 MessageBroker와 MessageMapping을 사용할 수 있다.</li>
<li>인증뿐 아니라 구독 목적지에 대한 권한 검사가 필요하다.</li>
<li>브라우저 연결 종료와 재연결 시 메시지 누락을 고려한다.</li>
<li>RSocket은 스트리밍과 양방향 채널 패턴을 지원한다.</li>
<li>실시간 전송과 메시지 영속성은 분리해서 설계한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 메시징 입문: RabbitMQ·Redis Pub/Sub·JMS·Pub/Sub 비교]]></title>
            <link>https://velog.io/@dev_alchemist/Spring-%EB%A9%94%EC%8B%9C%EC%A7%95-%EC%9E%85%EB%AC%B8-RabbitMQRedis-PubSubJMSPubSub-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@dev_alchemist/Spring-%EB%A9%94%EC%8B%9C%EC%A7%95-%EC%9E%85%EB%AC%B8-RabbitMQRedis-PubSubJMSPubSub-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Mon, 19 Oct 2026 14:42:08 GMT</pubDate>
            <description><![CDATA[<p>주문 API가 결제 완료 후 이메일·포인트·추천 이벤트를 모두 동기 실행하면, 알림 서비스 장애가 주문 응답 지연으로 이어질 수 있다. <strong>메시징</strong>은 생산자와 소비자의 실행 시간을 분리해 이런 결합을 줄인다.</p>
<h2 id="큐와-pubsub은-어떻게-다른가">큐와 Pub/Sub은 어떻게 다른가?</h2>
<p>큐는 일반적으로 경쟁 소비자 가운데 하나가 메시지를 처리하도록 설계한다. Pub/Sub은 주제를 구독한 여러 소비자가 이벤트를 받는 모델이다. 다만 실제 전달과 재처리 보장은 브로커의 구체적인 기능에 따라 달라진다.</p>
<table>
<thead>
<tr>
<th>기술</th>
<th>핵심 특성</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>RabbitMQ</td>
<td>AMQP 큐와 라우팅</td>
<td>주문 후 알림 작업</td>
</tr>
<tr>
<td>Redis Pub/Sub</td>
<td>구독 중인 연결에 실시간 전달</td>
<td>일시적인 상태 신호</td>
</tr>
<tr>
<td>JMS</td>
<td>Java 메시징 API 추상화</td>
<td>사내 메시징 시스템 연계</td>
</tr>
<tr>
<td>Google Cloud Pub/Sub</td>
<td>관리형 이벤트 메시징</td>
<td>클라우드 비동기 처리</td>
</tr>
</tbody></table>
<p>Redis Pub/Sub은 보통 구독자가 끊어진 동안의 메시지를 보관하지 않는다. 재처리와 지속성을 요구하면 Redis Streams 또는 별도 브로커를 검토한다.</p>
<h2 id="rabbitmq로-메시지-보내기">RabbitMQ로 메시지 보내기</h2>
<p>Spring AMQP 의존성을 추가하고 RabbitMQ 브로커를 실행한 후, <code>RabbitTemplate</code>을 사용한다.</p>
<pre><code class="language-java">import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;

@Service
public class ShippingEventPublisher {
    private final RabbitTemplate rabbit;

    public ShippingEventPublisher(RabbitTemplate rabbit) {
        this.rabbit = rabbit;
    }

    public void requested(long orderId) {
        rabbit.convertAndSend(&quot;shipping.exchange&quot;, &quot;shipping.requested&quot;, orderId);
    }
}</code></pre>
<p><code>shipping.exchange</code>와 바인딩된 큐·라우팅 키·컨버터·직렬화 정책을 별도로 설정해야 한다. 예제는 발행 API의 핵심 흐름만 나타낸다.</p>
<h2 id="실패를-가정한-설계가-필요하다">실패를 가정한 설계가 필요하다</h2>
<p>메시지 전달은 네트워크 장애·재시도 때문에 <strong>중복</strong>될 수 있다. 따라서 소비자는 <code>eventId</code> 같은 식별자로 중복 처리 방지(idempotency)를 구현하는 편이 안전하다. 소비 중 실패하면 재시도 정책과 DLQ(Dead Letter Queue)를 통해 잘못된 메시지를 분리할 수 있다.</p>
<p>DB 커밋 전에 이벤트를 발행하면 롤백된 주문에 대한 알림이 나갈 수 있다. 중요한 이벤트는 <strong>트랜잭셔널 아웃박스 패턴</strong>처럼 데이터 저장과 발행을 연결하는 방식을 검토하자.</p>
<p>메시징을 도입하면 사용자의 응답이 빨라질 수 있지만, 그 대신 <strong>관측성, 운영, 재처리, 순서 보장</strong>을 직접 설계해야 한다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>메시징은 서비스 간 시간적 결합을 낮춘다.</li>
<li>큐와 Pub/Sub은 소비 방식과 전달 보장이 다르다.</li>
<li>Redis Pub/Sub은 메시지 지속 저장을 기본 보장하지 않는다.</li>
<li>RabbitMQ는 Exchange·Queue·Routing Key를 구분해야 한다.</li>
<li>소비자는 중복 처리와 재시도에 대비해야 한다.</li>
<li>DB 변경과 이벤트 발행의 일관성에는 아웃박스가 유용하다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[OAuth2 로그인과 Vault로 인증 정보 안전하게 관리하기]]></title>
            <link>https://velog.io/@dev_alchemist/OAuth2-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EA%B3%BC-Vault%EB%A1%9C-%EC%9D%B8%EC%A6%9D-%EC%A0%95%EB%B3%B4-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_alchemist/OAuth2-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EA%B3%BC-Vault%EB%A1%9C-%EC%9D%B8%EC%A6%9D-%EC%A0%95%EB%B3%B4-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 19 Oct 2026 14:41:17 GMT</pubDate>
            <description><![CDATA[<p>Google이나 GitHub로 로그인하는 기능은 단순히 외부 API를 호출하는 일이 아니다. 사용자를 외부 제공자에게 인증시키고, <strong>인가 코드(Authorization Code)</strong>를 안전하게 교환한 뒤 애플리케이션 세션을 확립하는 과정이다.</p>
<h2 id="oauth2와-openid-connect의-역할">OAuth2와 OpenID Connect의 역할</h2>
<p>OAuth2는 권한 위임 프레임워크다. OIDC는 OAuth2 위에 사용자 인증을 위한 표준 계층을 더한다. 일반적인 소셜 로그인에서는 <code>openid</code> 범위를 지원하는 제공자라면 OIDC를 사용하고, <code>id_token</code>의 발급자·대상·서명·만료 등을 검증한다.</p>
<p>브라우저 기반 코드 흐름은 대략 다음과 같다.</p>
<pre><code class="language-text">브라우저 → 애플리케이션 로그인 시작
       → 인증 제공자의 동의/로그인
       → 애플리케이션 콜백으로 authorization code 반환
       → 서버가 code를 토큰으로 교환
       → 사용자 식별 후 자체 세션 생성</code></pre>
<p>코드와 토큰을 브라우저 URL이나 로그에 남기지 말고, <code>state</code>, PKCE, 정확한 redirect URI 등 보안 구성을 사용한다.</p>
<h2 id="spring-security-oauth2-client">Spring Security OAuth2 Client</h2>
<p>Boot 4에서는 <code>spring-boot-starter-security-oauth2-client</code>를 사용한다. 일부 오래된 스타터 이름은 더 이상 신규 프로젝트 기준으로 권장되지 않는다.</p>
<pre><code class="language-yaml">spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}</code></pre>
<p>GitHub에서 OAuth App을 등록하고 콜백 URL을 앱과 일치시켜야 한다. 위 설정은 OAuth2 클라이언트 <strong>설정 예시</strong>이며, 실제 동작에는 SecurityFilterChain의 <code>oauth2Login(...)</code> 및 외부 제공자 설정이 필요하다.</p>
<h2 id="vault가-필요한-이유">Vault가 필요한 이유</h2>
<p>개발 환경에서는 <code>.env</code>나 OS 환경변수로 시작할 수 있다. 하지만 배포 환경이 늘고 접근 통제·감사·비밀정보 회전이 필요하다면 HashiCorp Vault 같은 비밀 저장소를 검토한다. Vault는 비밀값 버전 관리, 접근 정책, 짧은 수명의 동적 자격증명 등을 제공할 수 있다.</p>
<p>서비스 계정에는 <strong>필요한 경로 읽기 권한만</strong> 부여한다. Vault 토큰을 소스코드에 하드코딩하면 중앙 비밀 저장소를 도입한 의미가 사라진다.</p>
<h2 id="실무-체크-포인트">실무 체크 포인트</h2>
<ul>
<li><code>client-secret</code>을 Git에 커밋하지 않는다.</li>
<li>토큰을 프론트엔드 로컬 스토리지에 무조건 저장하지 않는다. XSS, CSRF, 서버 렌더링 방식까지 포함해 위협 모델을 결정한다.</li>
<li>자체 서비스의 사용자 ID와 외부 공급자 계정 ID를 구분한다.</li>
<li>로그인 실패, 계정 연결 해제, 재로그인 시나리오를 테스트한다.</li>
</ul>
<p><strong>OAuth2는 로그인 버튼의 구현 기술이고, 비밀정보 관리는 이를 안전하게 운영하기 위한 별도의 설계 문제</strong>다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>OAuth2는 권한 위임, OIDC는 표준화된 사용자 인증을 다룬다.</li>
<li>인가 코드 흐름의 state, PKCE, redirect URI를 올바르게 설정한다.</li>
<li>Spring Boot 4 신규 프로젝트에서는 보안 OAuth2 스타터를 확인한다.</li>
<li>클라이언트 비밀키는 저장소가 아닌 안전한 환경에 보관한다.</li>
<li>Vault는 비밀정보 접근 정책·회전·감사에 유용하다.</li>
<li>외부 사용자 식별과 자체 회원 계정 정책을 분리해야 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security 핵심: 인증과 인가, 세션, CSRF 이해하기]]></title>
            <link>https://velog.io/@dev_alchemist/Spring-Security-%ED%95%B5%EC%8B%AC-%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80-%EC%84%B8%EC%85%98-CSRF-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_alchemist/Spring-Security-%ED%95%B5%EC%8B%AC-%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80-%EC%84%B8%EC%85%98-CSRF-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 19 Oct 2026 14:40:32 GMT</pubDate>
            <description><![CDATA[<p>서비스에 로그인 기능이 생기면 사용자 식별과 권한 확인을 구분해야 한다. <strong>인증(Authentication)</strong>은 누구인지 확인하는 과정이고, <strong>인가(Authorization)</strong>는 그 사용자가 요청한 기능을 실행할 수 있는지 확인하는 과정이다.</p>
<h2 id="요청은-먼저-보안-필터를-지난다">요청은 먼저 보안 필터를 지난다</h2>
<p>Spring Security는 <code>SecurityFilterChain</code>을 통해 요청을 검사한다. 컨트롤러에 도착하기 전 인증 정보와 접근 규칙을 확인할 수 있다.</p>
<pre><code class="language-java">import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -&gt; auth
                .requestMatchers(&quot;/&quot;, &quot;/catalog/**&quot;).permitAll()
                .requestMatchers(&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
                .anyRequest().authenticated()
        );
        http.formLogin(form -&gt; form.permitAll());
        return http.build();
    }
}</code></pre>
<p>위 설정은 <strong>세션 기반 폼 로그인</strong>을 기본으로 하며, 실제 사용자 저장소와 비밀번호 인코더를 별도 구성해야 한다. 허용할 정적 리소스와 로그아웃 경로도 서비스에 맞게 결정한다.</p>
<h2 id="비밀번호를-어떻게-저장할까">비밀번호를 어떻게 저장할까?</h2>
<p>평문 비밀번호 저장은 금지한다. <code>PasswordEncoder</code>를 사용해 단방향 해시 값을 저장한다. 운영 서비스라면 적절한 bcrypt/Argon2 파라미터와 비밀번호 정책을 검토한다.</p>
<pre><code class="language-java">import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}</code></pre>
<p>위 <code>@Bean</code> 메서드는 <code>@Configuration</code> 클래스 안에 배치해야 한다.</p>
<h2 id="csrf를-쉽게-끄면-왜-위험할까">CSRF를 쉽게 끄면 왜 위험할까?</h2>
<p>브라우저가 쿠키를 자동 전송하는 세션 기반 로그인에서는 <strong>CSRF 공격</strong>을 고려해야 한다. 단순히 API 개발이 귀찮다는 이유로 <code>csrf.disable()</code>을 복사하지 않는다. 반면 브라우저 쿠키에 의존하지 않는 특정 토큰 기반 API에서는 위협 모델에 따라 다른 구성이 필요할 수 있다.</p>
<h2 id="ldap와-spa-보안">LDAP와 SPA 보안</h2>
<p>LDAP는 디렉터리 서버를 기반으로 조직 사용자 인증을 통합할 때 활용한다. Angular/React 같은 SPA는 API 서버와 로그인 경계를 어떻게 분리할지 결정해야 한다. 세션 쿠키 방식이라면 SameSite·Secure·HttpOnly, CORS, CSRF를 함께 검토하고, OIDC 방식이라면 표준 로그인 흐름을 사용한다.</p>
<p><strong>프론트엔드 화면에서 버튼을 숨기는 것은 인가가 아니다.</strong> API 서버가 리소스 소유자와 권한을 매 요청에서 검증해야 한다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>인증은 사용자 확인, 인가는 접근 권한 확인이다.</li>
<li>SecurityFilterChain은 요청 처리 전에 접근 규칙을 적용한다.</li>
<li>비밀번호는 적절한 PasswordEncoder 해시로 저장한다.</li>
<li>세션과 쿠키를 쓴다면 CSRF 방어가 중요하다.</li>
<li>SPA의 CORS와 로그인 전략은 별개로 설계해야 한다.</li>
<li>버튼 숨김 대신 서버에서 객체 단위 권한 검사를 수행한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring WebFlux와 R2DBC: 논블로킹 I/O는 언제 필요할까?]]></title>
            <link>https://velog.io/@dev_alchemist/Spring-WebFlux%EC%99%80-R2DBC-%EB%85%BC%EB%B8%94%EB%A1%9C%ED%82%B9-IO%EB%8A%94-%EC%96%B8%EC%A0%9C-%ED%95%84%EC%9A%94%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@dev_alchemist/Spring-WebFlux%EC%99%80-R2DBC-%EB%85%BC%EB%B8%94%EB%A1%9C%ED%82%B9-IO%EB%8A%94-%EC%96%B8%EC%A0%9C-%ED%95%84%EC%9A%94%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Mon, 19 Oct 2026 14:39:39 GMT</pubDate>
            <description><![CDATA[<p>Spring MVC는 요청당 스레드를 사용하는 전통적인 서버 모델에 익숙한 개발자가 배우기 좋다. 반면 Spring WebFlux는 <strong>논블로킹 I/O와 리액티브 스트림</strong>을 기반으로 많은 동시 I/O 작업을 효율적으로 처리할 수 있다. 무조건 빠른 것이 아니라 문제에 맞을 때 선택해야 한다.</p>
<h2 id="mono와-flux">Mono와 Flux</h2>
<ul>
<li><code>Mono&lt;T&gt;</code>: 0개 또는 1개의 결과를 담는 비동기 스트림</li>
<li><code>Flux&lt;T&gt;</code>: 0개 이상의 연속된 결과를 담는 비동기 스트림</li>
</ul>
<pre><code class="language-java">import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

Mono&lt;String&gt; single = Mono.just(&quot;주문 접수&quot;);
Flux&lt;Integer&gt; numbers = Flux.range(1, 3)
        .map(number -&gt; number * 10);</code></pre>
<p>스트림 선언만으로 네트워크 작업이 발생한 것은 아니다. 실제 구독 시점에 파이프라인이 실행되는 경우가 많다.</p>
<h2 id="논블로킹-api-만들기">논블로킹 API 만들기</h2>
<p>Spring WebFlux 스타터로 시작한다.</p>
<pre><code class="language-java">import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
public class StatusController {
    @GetMapping(&quot;/products/{id}/status&quot;)
    public Mono&lt;String&gt; status(@PathVariable long id) {
        return Mono.just(&quot;product=&quot; + id + &quot;, state=READY&quot;);
    }
}</code></pre>
<p>이 예제 자체는 단순 계산이라 WebFlux 성능상 이점을 보여주지 않는다. 실제로는 외부 서비스나 논블로킹 DB 접근이 병목일 때 의미가 있다.</p>
<h2 id="r2dbc와-jdbc는-같은-프로토콜이-아니다">R2DBC와 JDBC는 같은 프로토콜이 아니다</h2>
<p>JDBC 드라이버는 일반적으로 블로킹 방식이다. WebFlux 컨트롤러에서 JDBC나 <code>Thread.sleep()</code>을 호출하면 이벤트 루프를 막을 수 있다. R2DBC는 관계형 DB를 리액티브 방식으로 접근하기 위한 규격이다.</p>
<pre><code class="language-java">import org.springframework.data.repository.reactive.ReactiveCrudRepository;

public interface ActivityRepository extends ReactiveCrudRepository&lt;Activity, Long&gt; {
    Flux&lt;Activity&gt; findByUserId(long userId);
}</code></pre>
<p><code>Activity</code>는 R2DBC 매핑 엔티티이며 <code>Flux</code> import가 필요하다는 점을 기억하자. 위 코드는 <strong>Repository 인터페이스의 사용 형태</strong>를 설명하는 예제이며, 실행하려면 엔티티·DB 드라이버·스키마 설정이 추가되어야 한다.</p>
<h2 id="reactive-redis는-어디에-사용할까">Reactive Redis는 어디에 사용할까?</h2>
<p>Spring Data Redis에는 논블로킹 접근을 위한 Reactive API가 있다. 요청 흐름 전체가 논블로킹일 때 가치가 크다. 다만 캐시 적중률과 직렬화 비용이 성능에 더 중요한 경우도 있다.</p>
<h2 id="자주-막히는-부분">자주 막히는 부분</h2>
<ul>
<li><code>block()</code>을 HTTP 핸들러 안에서 남용하면 리액티브 처리 이점이 줄어든다.</li>
<li>트랜잭션은 JDBC/JPA와 R2DBC에서 연결 방식과 적용 문맥이 다르다.</li>
<li>예외는 <code>onErrorResume</code>, <code>onErrorMap</code> 등으로 스트림 안에서 처리할 수 있다.</li>
<li>WebFlux는 사용법이 복잡해질 수 있으므로 간단한 CRUD 서버에서는 MVC가 더 나을 수 있다.</li>
</ul>
<p><strong>Reactive는 속도를 높이는 만능 옵션이 아니라, 고동시성 I/O를 다루기 위한 프로그래밍 모델</strong>이다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>Mono는 최대 한 개, Flux는 여러 결과를 표현한다.</li>
<li>WebFlux는 논블로킹 I/O 상황에서 장점이 있다.</li>
<li>블로킹 JDBC 호출을 이벤트 루프에서 수행하면 병목이 생긴다.</li>
<li>R2DBC는 리액티브 방식의 관계형 DB 접근 기술이다.</li>
<li>Reactive Redis도 전체 호출 체인이 논블로킹일 때 의미가 크다.</li>
<li>단순 CRUD 프로젝트라면 Spring MVC가 더 단순할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MongoDB·Neo4j·Cassandra·Redis: 데이터 모델에 맞게 저장소 선택하기]]></title>
            <link>https://velog.io/@dev_alchemist/MongoDBNeo4jCassandraRedis-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%AA%A8%EB%8D%B8%EC%97%90-%EB%A7%9E%EA%B2%8C-%EC%A0%80%EC%9E%A5%EC%86%8C-%EC%84%A0%ED%83%9D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_alchemist/MongoDBNeo4jCassandraRedis-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%AA%A8%EB%8D%B8%EC%97%90-%EB%A7%9E%EA%B2%8C-%EC%A0%80%EC%9E%A5%EC%86%8C-%EC%84%A0%ED%83%9D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 19 Oct 2026 14:38:51 GMT</pubDate>
            <description><![CDATA[<p>관계형 데이터베이스 하나로 대부분의 서비스를 만들 수 있지만, 특정 조회 패턴에서는 다른 저장 모델이 유리할 수 있다. 기술 이름보다 <strong>데이터 접근 패턴과 일관성 요구</strong>를 먼저 결정해야 한다.</p>
<h2 id="서로-다른-데이터-모델">서로 다른 데이터 모델</h2>
<table>
<thead>
<tr>
<th>저장소</th>
<th>주된 모델</th>
<th>활용 예</th>
</tr>
</thead>
<tbody><tr>
<td>MongoDB</td>
<td>문서(Document)</td>
<td>상품 상세의 가변 속성</td>
</tr>
<tr>
<td>Neo4j</td>
<td>그래프(Node/Relationship)</td>
<td>사용자·상품 연결 탐색</td>
</tr>
<tr>
<td>Cassandra</td>
<td>분산 와이드 컬럼</td>
<td>대량 쓰기·분산 조회</td>
</tr>
<tr>
<td>Redis</td>
<td>키-값/자료구조</td>
<td>세션, 캐시, 단기 데이터</td>
</tr>
<tr>
<td>Apache Geode 계열</td>
<td>분산 인메모리 데이터 그리드</td>
<td>저지연 분산 상태 관리</td>
</tr>
</tbody></table>
<p>그렇다고 Redis를 항상 영속 DB로, MongoDB를 항상 관계형 DB 대신 선택해야 한다는 뜻은 아니다. 운영 복잡도와 팀의 경험도 중요한 조건이다.</p>
<h2 id="mongodb-문서-만들기">MongoDB 문서 만들기</h2>
<p>Spring Data MongoDB 스타터와 MongoDB 연결이 필요하다.</p>
<pre><code class="language-java">import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Document(&quot;catalog_items&quot;)
public class CatalogItem {
    @Id
    private String id;
    private String name;
    private java.util.Map&lt;String, String&gt; specs;

    protected CatalogItem() {}

    public CatalogItem(String name, java.util.Map&lt;String, String&gt; specs) {
        this.name = name;
        this.specs = specs;
    }
}</code></pre>
<pre><code class="language-java">import java.util.List;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface CatalogRepository extends MongoRepository&lt;CatalogItem, String&gt; {
    List&lt;CatalogItem&gt; findByNameContaining(String term);
}</code></pre>
<p>구조가 유연해도 인덱스가 없으면 검색이 느리다. <strong>자주 조회하는 필드의 인덱스와 문서 크기</strong>를 고려해야 한다.</p>
<h2 id="관계-탐색과-분산-쓰기">관계 탐색과 분산 쓰기</h2>
<p>Neo4j에서는 <code>@Node</code>, <code>@Relationship</code>로 관계 구조를 표현하고 <code>Neo4jRepository</code>를 사용한다. Cassandra에서는 파티션 키 설계가 조회 효율을 좌우한다. 둘 다 RDBMS의 JOIN과 같은 사용 방식을 그대로 가져오면 기대한 성능이 나오지 않을 수 있다.</p>
<h2 id="rest-자동-공개와-오래된-예제-구별">REST 자동 공개와 오래된 예제 구별</h2>
<p>Spring Data REST는 JPA뿐 아니라 일부 NoSQL Repository를 HTTP에 노출하는 기능과 결합할 수 있다. 다만 기술별 현재 지원 상태와 버전을 확인해야 한다. 특히 <strong>Pivotal GemFire/Apache Geode 관련 오래된 예제는 현재 Boot와 직접 호환된다고 단정하면 안 된다.</strong> 학습 목적에서는 분산 인메모리 데이터 그리드라는 개념을 이해하고, 실제 도입 전 유지보수 상황을 검증한다.</p>
<p><strong>저장소 선택의 기준은 익숙한 문법이 아니라, 가장 중요한 조회 쿼리와 장애 상황에서 어떤 일관성이 필요한지</strong>다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>MongoDB는 문서 단위 데이터 모델에 적합하다.</li>
<li>Neo4j는 관계를 반복 탐색하는 그래프 문제에 강점이 있다.</li>
<li>Cassandra는 파티션 키와 조회 패턴 중심으로 설계한다.</li>
<li>Redis는 캐시·세션 등에 자주 사용하지만 일관성·유실 조건을 알아야 한다.</li>
<li>NoSQL도 인덱스와 데이터 모델링이 필수다.</li>
<li>오래된 Geode/GemFire 예제는 현재 버전 호환성을 별도로 점검한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Data JPA로 주문 데이터 저장하기: Entity부터 MySQL까지]]></title>
            <link>https://velog.io/@dev_alchemist/Spring-Data-JPA%EB%A1%9C-%EC%A3%BC%EB%AC%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0-Entity%EB%B6%80%ED%84%B0-MySQL%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@dev_alchemist/Spring-Data-JPA%EB%A1%9C-%EC%A3%BC%EB%AC%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0-Entity%EB%B6%80%ED%84%B0-MySQL%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Mon, 19 Oct 2026 14:38:02 GMT</pubDate>
            <description><![CDATA[<p>객체 중심 Java 코드와 테이블 중심 RDBMS 사이에는 구조 차이가 있다. JPA는 <strong>ORM(Object-Relational Mapping)</strong> 규약으로 객체를 테이블에 매핑하고, Spring Data JPA는 반복적인 Repository 코드를 줄여준다.</p>
<h2 id="entity와-dto를-왜-분리할까">Entity와 DTO를 왜 분리할까?</h2>
<p>Entity는 DB 저장과 변경 감지의 단위다. 반면 DTO는 API 입력·출력 계약이다. Entity를 그대로 JSON으로 응답하면 연관관계가 노출되거나 지연 로딩 문제가 생길 수 있다.</p>
<pre><code class="language-java">package com.example.shop.order;

import jakarta.persistence.*;

@Entity
@Table(name = &quot;shop_orders&quot;)
public class ShopOrder {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String productCode;

    @Column(nullable = false)
    private int quantity;

    protected ShopOrder() {}

    public ShopOrder(String productCode, int quantity) {
        this.productCode = productCode;
        this.quantity = quantity;
    }

    public Long getId() { return id; }
    public String getProductCode() { return productCode; }
    public int getQuantity() { return quantity; }
}</code></pre>
<p><code>jakarta.persistence.*</code>는 최신 Jakarta Persistence 네임스페이스다. 옛 예제의 <code>javax.persistence.*</code>를 섞으면 컴파일 문제가 생긴다.</p>
<pre><code class="language-java">import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ShopOrderRepository extends JpaRepository&lt;ShopOrder, Long&gt; {
    List&lt;ShopOrder&gt; findByProductCode(String productCode);
}</code></pre>
<p><code>save</code>, <code>findById</code>, <code>findAll</code>은 이미 제공된다. <code>findByProductCode</code>는 메서드 이름으로 쿼리를 유도한다. 복잡한 쿼리는 <code>@Query</code>, Querydsl 등 별도 접근을 검토한다.</p>
<h2 id="로컬-mysql-연결-예시">로컬 MySQL 연결 예시</h2>
<p>Spring Data JPA, MySQL Driver를 추가하고 DB·사용자를 미리 생성한다.</p>
<pre><code class="language-yaml">spring:
  datasource:
    url: jdbc:mysql://localhost:3306/shopdb
    username: ${DB_USER}
    password: ${DB_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: validate
    open-in-view: false</code></pre>
<p><code>ddl-auto: validate</code>는 엔티티와 테이블 스키마가 일치하는지 검사한다. 테이블을 만드는 역할은 하지 않는다. 실무에서는 Flyway나 Liquibase 같은 <strong>스키마 마이그레이션 도구</strong>로 변경 이력을 관리하는 편이 안전하다.</p>
<h2 id="변경-감지-n1-트랜잭션">변경 감지, N+1, 트랜잭션</h2>
<p>영속 상태의 Entity 변경은 트랜잭션 안에서 변경 감지될 수 있다. 하지만 모든 연관관계를 한 번에 가져오는 것은 아니다. 주문 목록마다 회원을 따로 조회하면 <strong>N+1 쿼리</strong>가 발생할 수 있다. SQL 로그를 확인하고 필요한 범위에서 fetch join, EntityGraph 또는 DTO 프로젝션을 사용하자.</p>
<p>Spring Data REST로 Repository를 HTTP에 노출할 수도 있지만 실무에서 권한·검증·DTO 정책이 중요하다면 별도 Controller/Service를 두는 편이 통제하기 좋다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>JPA는 객체와 관계형 테이블을 매핑하는 표준이고 Spring Data JPA는 Repository 작성을 지원한다.</li>
<li>Entity는 영속성 모델, DTO는 외부 API 계약이다.</li>
<li>최근 Spring 환경에서는 <code>jakarta.persistence</code>를 사용한다.</li>
<li>실서비스는 DDL 자동 생성보다 마이그레이션 도구를 고려한다.</li>
<li>지연 로딩과 N+1을 SQL 로그로 검증해야 한다.</li>
<li>Repository를 REST로 직접 공개할 때는 접근 제어가 중요하다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JdbcTemplate과 트랜잭션: 데이터가 중간에 저장되지 않게 만들기]]></title>
            <link>https://velog.io/@dev_alchemist/JdbcTemplate%EA%B3%BC-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EC%A4%91%EA%B0%84%EC%97%90-%EC%A0%80%EC%9E%A5%EB%90%98%EC%A7%80-%EC%95%8A%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@dev_alchemist/JdbcTemplate%EA%B3%BC-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EC%A4%91%EA%B0%84%EC%97%90-%EC%A0%80%EC%9E%A5%EB%90%98%EC%A7%80-%EC%95%8A%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Mon, 19 Oct 2026 14:37:08 GMT</pubDate>
            <description><![CDATA[<p>쇼핑몰에서 주문을 저장한 뒤 재고를 감소시키는 상황을 생각해 보자. 주문만 저장되고 재고 감소가 실패하면 데이터가 서로 맞지 않는다. 이때 필요한 것이 <strong>트랜잭션(Transaction)</strong>이다.</p>
<h2 id="jdbc를-편하게-사용하는-jdbctemplate">JDBC를 편하게 사용하는 JdbcTemplate</h2>
<p>기본 JDBC에서는 Connection, Statement, ResultSet, 예외 처리를 반복해야 한다. <code>JdbcTemplate</code>은 이 공통 작업을 감싸 SQL에 집중하게 해 준다. Spring JDBC와 DB 드라이버, DataSource 설정이 필요하다.</p>
<pre><code class="language-java">import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class InventoryRepository {
    private final JdbcTemplate jdbc;

    public InventoryRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    public int decrease(long productId, int count) {
        return jdbc.update(&quot;&quot;&quot;
            UPDATE inventory
               SET stock = stock - ?
             WHERE product_id = ? AND stock &gt;= ?
            &quot;&quot;&quot;, count, productId, count);
    }
}</code></pre>
<p>재고 감소 SQL의 핵심은 <code>stock &gt;= ?</code> 조건이다. 조회 후 따로 수정하는 것보다 동시에 구매가 몰릴 때 재고가 음수로 내려가는 상황을 줄인다. 반환값이 <code>0</code>이면 재고가 부족하거나 상품이 없다.</p>
<h2 id="트랜잭션-경계는-서비스-계층에서">트랜잭션 경계는 서비스 계층에서</h2>
<pre><code class="language-java">import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class PurchaseService {
    private final InventoryRepository inventory;
    private final PurchaseRepository purchases;

    public PurchaseService(InventoryRepository inventory, PurchaseRepository purchases) {
        this.inventory = inventory;
        this.purchases = purchases;
    }

    @Transactional
    public long purchase(long productId, int count) {
        if (inventory.decrease(productId, count) != 1) {
            throw new IllegalStateException(&quot;재고가 부족합니다.&quot;);
        }
        return purchases.insert(productId, count);
    }
}</code></pre>
<p><code>PurchaseRepository</code>는 주문 행을 INSERT하고 생성된 ID를 반환하는 사용자 정의 저장소라고 가정한다. 같은 DataSource와 트랜잭션 매니저를 사용하면 작업이 함께 커밋·롤백된다.</p>
<h2 id="롤백에-관한-오해">롤백에 관한 오해</h2>
<ul>
<li>기본적으로 <code>RuntimeException</code>과 <code>Error</code> 발생 시 롤백된다. 체크 예외까지 처리하려면 롤백 규칙을 지정한다.</li>
<li>같은 객체 내부 호출은 프록시를 우회할 수 있다.</li>
<li>DB 작업과 외부 결제 API 호출은 하나의 로컬 DB 트랜잭션으로 원자성을 보장할 수 없다. <strong>아웃박스·보상 트랜잭션</strong> 등이 필요할 수 있다.</li>
<li>트랜잭션은 데이터를 무조건 안전하게 만드는 마법이 아니다. 격리 수준, 잠금, 제약조건이 함께 필요하다.</li>
</ul>
<p><strong>트랜잭션은 단순히 <code>@Transactional</code>을 붙이는 문법이 아니라, 어떤 작업을 하나의 실패 단위로 볼지 정하는 설계</strong>다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>JdbcTemplate은 반복적인 JDBC 자원 관리 코드를 줄인다.</li>
<li><code>@Transactional</code>은 같은 트랜잭션에 참여하는 DB 작업을 묶는다.</li>
<li>서비스 메서드를 트랜잭션 경계로 삼는 구성이 일반적이다.</li>
<li>기본 롤백 대상과 체크 예외의 차이를 이해한다.</li>
<li>자기 호출에서는 트랜잭션 프록시가 적용되지 않을 수 있다.</li>
<li>재고 같은 경쟁 자원은 조건부 SQL·잠금·제약조건이 중요하다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring에서 파일 업로드, 비동기 처리, 예약 작업 구현하기]]></title>
            <link>https://velog.io/@dev_alchemist/Spring%EC%97%90%EC%84%9C-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%EC%98%88%EC%95%BD-%EC%9E%91%EC%97%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_alchemist/Spring%EC%97%90%EC%84%9C-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%EC%98%88%EC%95%BD-%EC%9E%91%EC%97%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 19 Oct 2026 14:35:46 GMT</pubDate>
            <description><![CDATA[<p>파일을 저장하고, 오래 걸리는 외부 API를 호출하고, 매일 특정 시점에 통계를 집계하는 작업은 서로 달라 보인다. 하지만 공통점은 <strong>HTTP 요청 안에서 끝낼 일과 백그라운드로 넘길 일</strong>을 구분해야 한다는 것이다.</p>
<h2 id="파일-업로드-경로와-크기부터-제한하기">파일 업로드: 경로와 크기부터 제한하기</h2>
<p>Multipart 업로드는 파일 이름을 그대로 서버 경로에 이어 붙이면 경로 조작 공격에 노출될 수 있다. 서버가 파일명을 새로 만들고 업로드 폴더를 통제해야 한다.</p>
<pre><code class="language-yaml">spring:
  servlet:
    multipart:
      max-file-size: 5MB
      max-request-size: 6MB</code></pre>
<pre><code class="language-java">import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping(&quot;/attachments&quot;)
public class AttachmentController {
    private final Path root = Path.of(&quot;uploads&quot;).toAbsolutePath();

    @PostMapping
    ResponseEntity&lt;String&gt; upload(@RequestParam(&quot;file&quot;) MultipartFile file) throws IOException {
        if (file.isEmpty()) return ResponseEntity.badRequest().body(&quot;빈 파일입니다.&quot;);
        Files.createDirectories(root);
        Path target = root.resolve(UUID.randomUUID() + &quot;.bin&quot;);
        file.transferTo(target);
        return ResponseEntity.ok(target.getFileName().toString());
    }
}</code></pre>
<p>코드는 경로 조작을 피하기 위한 최소 개념 예제다. <strong>실서비스에서는 파일 내용 검사, 악성 파일 탐지, 인증/인가, 접근 제한, 확장자/MIME 정책, 외부 스토리지</strong> 등을 추가해야 한다. 사용자 입력을 실행 파일로 저장하거나 업로드 폴더를 무분별하게 정적 공개하면 안 된다.</p>
<h2 id="비동기-메서드의-역할">비동기 메서드의 역할</h2>
<pre><code class="language-java">import java.util.concurrent.CompletableFuture;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class ReceiptService {
    @Async
    public CompletableFuture&lt;String&gt; sendReceipt(long orderId) {
        return CompletableFuture.completedFuture(&quot;주문 &quot; + orderId + &quot; 알림 처리&quot;);
    }
}</code></pre>
<p>설정 클래스에는 <code>@EnableAsync</code>가 필요하다. <code>@Async</code>는 프록시를 통한 호출에서 작동한다. <strong>같은 클래스 내부에서 자기 메서드를 호출하면 비동기가 적용되지 않을 수 있다.</strong> 실제 실행 정책은 <code>ThreadPoolTaskExecutor</code>로 제한하고 장애·재시도 전략을 세운다.</p>
<h2 id="정기-작업은-비동기와-다르다">정기 작업은 비동기와 다르다</h2>
<pre><code class="language-java">import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class ReportJob {
    @Scheduled(cron = &quot;0 0 2 * * *&quot;, zone = &quot;Asia/Seoul&quot;)
    public void refreshDailyReport() {
        System.out.println(&quot;새벽 2시 집계 시작&quot;);
    }
}</code></pre>
<p>스케줄링에는 <code>@EnableScheduling</code>이 필요하다. 서버를 3개 실행하면 작업도 3번 수행될 수 있으므로 분산 환경에서는 락, 외부 스케줄러, 중복 처리 방지를 고려한다.</p>
<p>짧은 작업은 웹 요청에서 처리하고, 실패에 대한 재시도가 필요한 대규모 작업은 메시지 큐나 배치 작업으로 분리하는 것이 낫다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>파일 업로드는 크기와 저장 경로를 반드시 제한한다.</li>
<li>서버 생성 파일명과 별도 저장 공간으로 위험을 줄인다.</li>
<li><code>@Async</code>는 <code>@EnableAsync</code>와 프록시 호출 조건을 이해해야 한다.</li>
<li>작업 스레드 풀과 실패 처리를 따로 설정한다.</li>
<li><code>@Scheduled</code>는 서버 인스턴스마다 실행될 수 있다.</li>
<li>분산 예약 작업에는 중복 실행 방지 설계가 필요하다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring REST API 설계: CRUD, HTTP 상태 코드, 클라이언트 호출]]></title>
            <link>https://velog.io/@dev_alchemist/Spring-REST-API-%EC%84%A4%EA%B3%84-CRUD-HTTP-%EC%83%81%ED%83%9C-%EC%BD%94%EB%93%9C-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%ED%98%B8%EC%B6%9C</link>
            <guid>https://velog.io/@dev_alchemist/Spring-REST-API-%EC%84%A4%EA%B3%84-CRUD-HTTP-%EC%83%81%ED%83%9C-%EC%BD%94%EB%93%9C-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%ED%98%B8%EC%B6%9C</guid>
            <pubDate>Mon, 19 Oct 2026 14:34:57 GMT</pubDate>
            <description><![CDATA[<p>API 설계의 출발점은 URL이 아니라 <strong>리소스가 무엇인가</strong>를 정하는 것이다. 주문 정보를 다룬다면 <code>/orders</code>는 주문 컬렉션, <code>/orders/{id}</code>는 개별 주문을 뜻한다. 작업 이름을 URL에 반복하기보다 HTTP 메서드로 의도를 표현한다.</p>
<table>
<thead>
<tr>
<th>동작</th>
<th>요청</th>
<th>일반적인 성공 상태</th>
</tr>
</thead>
<tbody><tr>
<td>목록</td>
<td><code>GET /orders</code></td>
<td>200</td>
</tr>
<tr>
<td>생성</td>
<td><code>POST /orders</code></td>
<td>201</td>
</tr>
<tr>
<td>단건 조회</td>
<td><code>GET /orders/10</code></td>
<td>200 또는 404</td>
</tr>
<tr>
<td>전체 수정</td>
<td><code>PUT /orders/10</code></td>
<td>200 또는 204</td>
</tr>
<tr>
<td>삭제</td>
<td><code>DELETE /orders/10</code></td>
<td>204</td>
</tr>
</tbody></table>
<h2 id="메모리-저장소로-요청-흐름-이해하기">메모리 저장소로 요청 흐름 이해하기</h2>
<p>실제 서비스에서는 Repository를 DB로 교체해야 하지만 HTTP 동작을 배우기에는 메모리 기반 예제가 충분하다.</p>
<pre><code class="language-java">package com.example.shop.api;

import java.net.URI;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

record OrderRequest(String productName, int quantity) {}
record OrderResponse(long id, String productName, int quantity) {}

@RestController
@RequestMapping(&quot;/orders&quot;)
public class OrderController {
    private final ConcurrentHashMap&lt;Long, OrderResponse&gt; orders = new ConcurrentHashMap&lt;&gt;();
    private final AtomicLong sequence = new AtomicLong();

    @GetMapping
    Collection&lt;OrderResponse&gt; list() { return orders.values(); }

    @GetMapping(&quot;/{id}&quot;)
    ResponseEntity&lt;OrderResponse&gt; get(@PathVariable long id) {
        OrderResponse result = orders.get(id);
        return result == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(result);
    }

    @PostMapping
    ResponseEntity&lt;OrderResponse&gt; create(@RequestBody OrderRequest body) {
        long id = sequence.incrementAndGet();
        OrderResponse saved = new OrderResponse(id, body.productName(), body.quantity());
        orders.put(id, saved);
        return ResponseEntity.created(URI.create(&quot;/orders/&quot; + id)).body(saved);
    }

    @DeleteMapping(&quot;/{id}&quot;)
    ResponseEntity&lt;Void&gt; delete(@PathVariable long id) {
        return orders.remove(id) == null
                ? ResponseEntity.notFound().build()
                : ResponseEntity.noContent().build();
    }
}</code></pre>
<p>동작을 이해하기 위한 최소 예제이므로 <strong>음수 수량 검증, 영속화, 수정 API, 정렬/페이지네이션, 인증은 생략</strong>했다. 실무에서는 요청 DTO에 Validation을 적용하고 Service와 Repository를 나눈다.</p>
<pre><code class="language-bash">curl -i -X POST http://localhost:8080/orders \
  -H &#39;Content-Type: application/json&#39; \
  -d &#39;{&quot;productName&quot;:&quot;무선 마우스&quot;,&quot;quantity&quot;:2}&#39;
curl http://localhost:8080/orders/1</code></pre>
<p>Windows PowerShell에서는 <code>curl</code> 별칭 동작이 다를 수 있으므로 <code>curl.exe</code>를 사용할 수 있다.</p>
<h2 id="api-소비자와의-통신">API 소비자와의 통신</h2>
<p><strong>RestClient</strong>는 일반적인 동기 HTTP 호출에 적합하다. Spring Boot 4에서 <code>spring-boot-starter-restclient</code>를 사용하면 관련 의존성을 편리하게 구성한다.</p>
<pre><code class="language-java">import org.springframework.web.client.RestClient;

RestClient client = RestClient.create(&quot;https://api.example.com&quot;);
String response = client.get()
        .uri(&quot;/status&quot;)
        .retrieve()
        .body(String.class);</code></pre>
<p>비동기·리액티브 방식이 필요하면 WebClient를 검토한다. 예전 자료에서 자주 보이는 <code>RestTemplate</code>보다 신규 동기 코드에서는 <code>RestClient</code>를 우선 고려하자.</p>
<h2 id="cors-hateoas-spring-data-rest">CORS, HATEOAS, Spring Data REST</h2>
<ul>
<li><strong>CORS</strong>: 브라우저가 다른 Origin의 응답을 읽을 수 있도록 서버가 허용 범위를 지정한다. 인증을 대신하는 보안 장치는 아니다.</li>
<li><strong>HATEOAS</strong>: 응답에 다음에 탐색할 수 있는 링크를 포함한다. 복잡한 리소스 간 탐색을 표현할 때 유용하다.</li>
<li><strong>Spring Data REST</strong>: Repository를 HTTP 리소스로 자동 공개한다. 공개 범위와 검증·권한을 명확히 통제해야 한다.</li>
</ul>
<p>REST API는 <code>200</code>을 돌려주는 코드보다 <strong>오류 응답을 일관되게 정의하는 일</strong>이 중요하다. <code>@RestControllerAdvice</code>와 <code>ProblemDetail</code>을 활용하면 클라이언트가 에러를 구조적으로 처리하기 쉬워진다.</p>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>리소스 URL과 HTTP 메서드로 CRUD 의도를 표현한다.</li>
<li>생성 성공은 보통 201과 Location 헤더를 사용한다.</li>
<li>도메인 검증과 인증은 Controller 예제와 별도로 설계해야 한다.</li>
<li>새 동기 HTTP 클라이언트는 RestClient를 우선 고려한다.</li>
<li>CORS는 브라우저 정책이며 인증/인가를 대체하지 않는다.</li>
<li>Spring Data REST의 자동 공개 범위는 신중하게 관리한다.</li>
<li>상태 코드와 오류 본문을 서비스 전체에서 일관되게 만든다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>