<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>_init.log</title>
        <link>https://velog.io/</link>
        <description>백엔드 개발자</description>
        <lastBuildDate>Mon, 06 Apr 2026 01:28:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. _init.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/sangcheol_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Spring 7.0과 JSpecify: Java Null 안전성의 새로운 표준]]></title>
            <link>https://velog.io/@sangcheol_/Spring-7.0%EA%B3%BC-JSpecify-Java-Null-%EC%95%88%EC%A0%84%EC%84%B1%EC%9D%98-%EC%83%88%EB%A1%9C%EC%9A%B4-%ED%91%9C%EC%A4%80</link>
            <guid>https://velog.io/@sangcheol_/Spring-7.0%EA%B3%BC-JSpecify-Java-Null-%EC%95%88%EC%A0%84%EC%84%B1%EC%9D%98-%EC%83%88%EB%A1%9C%EC%9A%B4-%ED%91%9C%EC%A4%80</guid>
            <pubDate>Mon, 06 Apr 2026 01:28:03 GMT</pubDate>
            <description><![CDATA[<p>Java 애플리케이션 개발에서 NullPointerException(NPE)은 오랫동안 골칫거리였다. 이를 해결하기 위해 다양한 시도가 있었으나, 완벽한 표준은 부재했다. </p>
<p>최근 Spring Framework 7.0과 JSpecify 1.0의 결합은 Java 생태계의 null 안전성을 근본적으로 개선하는 중요한 전환점이 되었다. 2025년 하반기 Spring Boot 4.0 출시 이후 Spring 포트폴리오 전반에 JSpecify가 도입됨에 따라, 실무 개발자들 역시 이 새로운 표준을 명확히 이해하고 적용해야 할 시점이다.</p>
<h2 id="java-null-문제-해결을-위한-기존-접근-방식의-한계">Java Null 문제 해결을 위한 기존 접근 방식의 한계</h2>
<h3 id="왜-optional이-완벽한-대안이-될-수-없는가">왜 Optional이 완벽한 대안이 될 수 없는가</h3>
<p>Null 처리를 위해 Java 8부터 도입된 <code>Optional</code>은 유용한 도구이지만 모든 null 문제를 해결하는 만능 키는 아니다. </p>
<p>첫째, 래퍼 객체를 생성하므로 런타임 오버헤드가 발생한다. 향후 Project Valhalla를 통해 값 타입 최적화가 이루어지면 개선될 여지가 있으나, 현재로서는 성능에 영향을 미친다. 
둘째, 기존 API 시그니처를 변경해야 하므로 하위 호환성이 중요한 프로젝트에서는 사실상 마이그레이션이 불가능하다. 
셋째, <code>Optional</code>은 애초에 메서드의 반환값 용도로 설계되었다. 이를 파라미터나 클래스 필드에 남용할 경우 코드와 API의 복잡도를 불필요하게 증가시킨다.</p>
<h3 id="기존-jsr-305-기반-어노테이션의-문제점">기존 JSR-305 기반 어노테이션의 문제점</h3>
<p>과거 Spring 프레임워크는 JSR-305 기반의 비공식 스펙을 활용하여 <code>@Nullable</code> 어노테이션을 지원했다. 그러나 이 방식은 명확한 라이선스가 없어 표준 의존성으로 사용하기에 부담이 컸다. 더욱 치명적인 단점은 제네릭 타입이나 배열 요소의 null 가능성을 제대로 표현할 수 없다는 것이었다. </p>
<p>이로 인해 정적 분석 도구마다 어노테이션의 의미를 다르게 해석하는 파편화 현상이 발생하여 일관된 널 안전성을 보장하기 어려웠다.</p>
<h2 id="jspecify의-등장과-설계-철학">JSpecify의 등장과 설계 철학</h2>
<p>JSpecify는 단일 도구나 특정 벤더의 종속성에서 벗어나, Java 생태계 전반을 아우르는 명확한 문서를 작성하는 것을 목표로 출발했다. 사양에 대한 합의를 이루는 데만 5년이 걸렸을 정도로 신중하고 정교하게 설계되었다. </p>
<p>가장 큰 특징은 특정 IDE나 분석 도구에 종속되지 않는다는 점이다. JetBrains의 IntelliJ IDEA나 Uber의 NullAway 같은 도구 공급업체 및 플러그인 생태계가 동일한 사양을 바탕으로 일관된 검증 동작을 구현할 수 있도록 표준화된 지침을 제공한다.</p>
<h2 id="jspecify-실무-적용-원칙">JSpecify 실무 적용 원칙</h2>
<h3 id="non-null-기본-설정의-중요성">Non-null 기본 설정의 중요성</h3>
<p>Spring 코드베이스의 약 90%는 기본적으로 null을 허용하지 않는 non-null 구조로 이루어져 있다. 따라서 모든 필드나 반환값에 어노테이션을 일일이 붙이는 대신, 패키지 단위로 기본값을 non-null로 설정하고 예외적인 경우에만 <code>@Nullable</code>을 명시하는 방식이 적극적으로 권장된다.</p>
<p><code>package-info.java</code> 파일을 활용하여 패키지 전체에 <code>@NullMarked</code>를 적용할 수 있다.</p>
<pre><code class="language-java">@NullMarked
package org.example;

import org.jspecify.annotations.NullMarked;</code></pre>
<p>이렇게 설정하면 해당 패키지 내의 모든 타입은 기본적으로 non-null로 취급되며, null이 허용되어야 하는 곳에만 명시적으로 어노테이션을 추가한다.</p>
<pre><code class="language-java">package org.example;

import org.jspecify.annotations.Nullable;

interface TokenExtractor {
    /**
     * @param input the input to process
     * @return the extracted token or null if not found
     */
    @Nullable String extractToken(String input);
}</code></pre>
<p>주의할 점은 <code>package-info.java</code>의 설정은 정확히 해당 패키지에만 적용되며 하위 패키지에는 상속되지 않는다는 것이다. 따라서 널 안전성을 확보하려는 프로젝트 내 모든 패키지에 각각 설정 파일을 두는 것이 권장된다.</p>
<h3 id="제네릭-및-배열의-정교한-null-처리">제네릭 및 배열의 정교한 Null 처리</h3>
<p>JSpecify는 기존 방식에서 불가능했던 배열의 정교한 null 처리를 지원한다. 배열 자체가 null인 경우와 배열 내부의 요소가 null인 경우를 문법적으로 명확히 구분할 수 있다.</p>
<pre><code class="language-java">interface TokenExtractor {
    // 배열 자체는 non-null이지만, 내부 요소는 null일 수 있음을 명시
    String @Nullable[] extractTokens(String input);
}</code></pre>
<h2 id="nullaway를-통한-빌드-타임-검증-자동화">NullAway를 통한 빌드 타임 검증 자동화</h2>
<p>IDE 수준의 단순 경고를 넘어 컴파일 타임에 NPE 발생 가능성을 원천 차단하려면, NullAway와 같은 정적 분석 도구를 빌드 파이프라인에 통합해야 한다. NullAway는 오류가 발생하기 쉬운 코드를 찾아내도록 돕는 Google의 Error Prone 기반 위에서 확장된 도구다.</p>
<p>Gradle을 사용하는 프로젝트의 경우 다음과 같이 빌드 스크립트를 구성하여 검증을 자동화할 수 있다.</p>
<pre><code class="language-groovy">plugins {
    id &#39;java&#39;
    id &#39;net.ltgt.errorprone&#39; version &#39;4.1.0&#39;
}

dependencies {
    implementation &#39;org.jspecify:jspecify:1.0.0&#39;
    errorprone &#39;com.google.errorprone:error_prone_core:2.36.0&#39;
    errorprone &#39;com.uber.nullaway:nullaway:0.10.25&#39;
}

tasks.withType(JavaCompile).configureEach {
    options.errorprone {
        // 불필요한 다른 Error Prone 검사는 비활성화
        disableAllChecks = true

        // NullMarked로 지정된 코드에서만 NullAway 활성화
        option(&quot;NullAway:OnlyNullMarked&quot;, &quot;true&quot;)

        // 제네릭 및 배열 요소 추가 검사를 위한 JSpecify 모드 활성화
        option(&quot;NullAway:JSpecifyMode&quot;, &quot;true&quot;)

        // 경고 수준을 빌드 에러로 격상
        error(&quot;NullAway&quot;)
    }
}</code></pre>
<p>빌드 스크립트 작성이 번거롭다면 현재 Spring 프레임워크 내부에서도 사용 중인 Spring Gradle Nullability 플러그인(<a href="https://github.com/spring-gradle-plugins/nullability-plugin">https://github.com/spring-gradle-plugins/nullability-plugin)을</a>을) 도입하여 구성을 간소화할 수 있다.</p>
<p>위와 같이 설정하면 로직 내에서 <code>@Nullable</code>로 선언된 값을 별도의 null 체크 없이 사용하려 할 때 컴파일러가 빌드 에러를 발생시켜 잠재적인 결함을 운영 환경 배포 이전에 방지한다.</p>
<h2 id="결론">결론</h2>
<p>JSpecify는 파편화되어 있던 Java 생태계의 null 안전성을 통합하는 강력하고 실질적인 표준이다. 단순히 어노테이션을 추가하는 수준을 넘어, 도구 독립적인 사양 표준화와 강력한 빌드 타임 검증을 통해 프로덕션 환경의 안정성을 비약적으로 높여준다.</p>
<p>불필요한 <code>Optional</code> 남용을 지양하고, 패키지 단위의 <code>@NullMarked</code> 설정을 기반으로 코드 가독성을 확보하며, NullAway를 활용한 빌드 타임 검증 체계를 갖추는 것이 실무 적용의 핵심이다. 점진적인 마이그레이션 전략을 통해 코드베이스를 개선해 나간다면 NPE로 인한 런타임 장애를 극적으로 줄일 수 있을 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSRF 보안 공격]]></title>
            <link>https://velog.io/@sangcheol_/CSRF-%EB%B3%B4%EC%95%88-%EA%B3%B5%EA%B2%A9</link>
            <guid>https://velog.io/@sangcheol_/CSRF-%EB%B3%B4%EC%95%88-%EA%B3%B5%EA%B2%A9</guid>
            <pubDate>Fri, 01 Nov 2024 03:55:41 GMT</pubDate>
            <description><![CDATA[<h2 id="csrf-이해">CSRF 이해</h2>
<p>CORS와 달리 CSRF는 해커들이 애플리케이션 내부의 데이터를 훔치거나, 애플리케이션 내부에서 일부 권한 있는 작업을 수행하기 위해 사용하는 <strong>실제 보안 공격</strong>입니다.</p>
<h3 id="csrf-정의">CSRF 정의</h3>
<p>CSRF(Cross-Site Request Forgery)는 <strong>사이트 간 요청 위조</strong> 공격으로, 사용자의 동의 없이 공격자가 사용자 대신 웹 애플리케이션에서 작업을 수행하려는 시도입니다. 이는 공격자가 피해자의 인증된 세션을 이용해 의도하지 않은 행동을 하게 만드는 방식으로, 사용자는 공격이 이루어지는 것을 인지하지 못합니다.</p>
<h3 id="csrf-공격의-예시">CSRF 공격의 예시</h3>
<p>예를 들어, 사용자가 넷플릭스(netflix.com)에 로그인한 후 다른 탭에서 악성 웹사이트(evil.com)에 접속하게 되는 경우를 가정해 봅시다. 이 악성 사이트에는 &#39;아이폰 90% 할인&#39;과 같은 유혹적인 배너가 있어, 사용자가 클릭하게 됩니다.</p>
<p><strong>공격 과정 요약</strong></p>
<ol>
<li><strong>넷플릭스 로그인</strong>: 사용자가 netflix.com에 로그인하면, 넷플릭스 서버는 사용자의 브라우저에 쿠키(예: <code>abc123</code>)를 저장합니다. 이 쿠키는 넷플릭스 도메인에서만 유효합니다.</li>
<li><strong>악성 사이트 접속</strong>: 사용자가 악성 사이트(evil.com)에 접속하여 유도된 배너를 클릭합니다.</li>
<li><strong>악성 링크 클릭</strong>: 이 배너에는 악성 폼이 숨겨져 있으며, 클릭 시 <code>netflix.com/changeEmail</code>로 <code>POST</code> 요청이 전송됩니다. 이 요청은 이메일 주소를 <code>user@evil.com</code>으로 변경하려는 내용입니다.</li>
<li><strong>요청 전송</strong>: 사용자가 알지 못하는 사이에 요청이 전송되며, 쿠키 <code>abc123</code>이 첨부되어 사용자가 보낸 요청처럼 위장됩니다.</li>
<li><strong>서버 응답</strong>: 넷플릭스 서버는 이 요청이 동일한 도메인(netflix.com)에서 발생한 것으로 인식하고 이메일 주소를 변경합니다.</li>
</ol>
<h3 id="cors의-한계">CORS의 한계</h3>
<p>이 시나리오에서는 CORS가 적용되더라도 CSRF 공격이 방지되지 않습니다. <strong>임베디드 폼을 통한 요청</strong>은 동일한 도메인에서 발생하는 요청으로 처리되기 때문에, 브라우저는 요청의 출처가 악성 사이트임을 인식하지 못합니다.</p>
<h2 id="csrf-공격-방지-및-해결책">CSRF 공격 방지 및 해결책</h2>
<h3 id="csrf-공격의-문제점">CSRF 공격의 문제점</h3>
<p>CSRF 공격에서는 악성 웹사이트가 사용자를 속여 인증된 상태로 요청을 보내게 함으로써, 백엔드 서버가 해당 요청이 원래 웹사이트에서 온 것인지 아닌지 구분할 수 없게 만듭니다. 이 문제를 해결하기 위해, CSRF 토큰을 사용하여 백엔드가 요청 출처를 검증하도록 하는 방법이 필요합니다.</p>
<h3 id="csrf-토큰의-역할">CSRF 토큰의 역할</h3>
<p>CSRF 토큰은 사용자의 세션마다 고유하며 예측하기 어려운 안전한 랜덤 값으로 생성됩니다. 이를 통해 서버는 요청이 원래 웹사이트에서 온 것인지 악의적인 사이트에서 온 것인지 식별할 수 있게 됩니다. 토큰의 특징은 다음과 같습니다:</p>
<ul>
<li><strong>세션마다 고유</strong>하며, <strong>무작위 값</strong>으로 생성됩니다.</li>
<li>충분히 긴 값을 사용하여 <strong>추측하기 어렵게</strong> 만듭니다.</li>
</ul>
<h3 id="csrf-공격-방지-시나리오">CSRF 공격 방지 시나리오</h3>
<p><strong>1단계: CSRF 토큰 생성 및 전송</strong>
넷플릭스 사용자가 로그인하면, 서버는 인증 쿠키와 함께 CSRF 토큰도 생성하여 UI 애플리케이션에 전달합니다. 이 CSRF 토큰은 쿠키 형태로 전송되며, 이후 요청에서 함께 사용될 것입니다.</p>
<p><strong>2단계: 악성 사이트 접속 및 공격 시도</strong>
사용자가 다른 탭에서 악성 사이트(evil.com)에 접속해 악성 링크를 클릭할 경우, 이 링크는 넷플릭스의 <code>netflix.com/changeEmail</code>로 요청을 보냅니다. 이때 브라우저는 쿠키에 저장된 CSRF 토큰을 함께 첨부하여 서버로 전송합니다.</p>
<p><strong>3단계: 서버의 CSRF 토큰 검증</strong>
백엔드 서버는 요청을 받을 때 <strong>두 가지 위치에서 CSRF 토큰을 확인</strong>합니다:</p>
<ul>
<li><strong>쿠키에 있는 CSRF 토큰</strong></li>
<li><strong>RequestHeader 또는 RequestBody</strong>에 있는 CSRF 토큰</li>
</ul>
<p>넷플릭스 UI에서 정상 요청이 발생하는 경우, 서버는 두 위치에 있는 토큰 값이 동일함을 확인할 수 있습니다. 반면, evil.com에서는 CSRF 토큰을 포함할 수 없어 헤더/본문에는 토큰 값이 없습니다. 이로 인해 서버는 요청을 차단(403 에러)하고 CSRF 공격을 방지할 수 있습니다.</p>
<blockquote>
<p><strong>JavaScript의 제한 사항</strong>
이 과정에서 악성 사이트는 넷플릭스의 쿠키를 읽을 수 없습니다. 이는 브라우저가 Same-Origin-Policy에 따라 evil.com의 JavaScript 코드가 netflix.com의 쿠키에 접근하지 못하도록 막기 때문입니다.</p>
</blockquote>
<h3 id="요약-csrf-방지-방법">요약: CSRF 방지 방법</h3>
<ol>
<li><strong>고유한 CSRF 토큰 생성</strong>: 사용자 세션마다 고유하고 예측할 수 없는 값을 생성합니다.</li>
<li><strong>CSRF 토큰 포함 요청 검증</strong>: 요청이 발생할 때마다 헤더/본문과 쿠키에 포함된 토큰을 비교하여 유효성을 확인합니다.</li>
<li><strong>JavaScript를 통한 읽기 제한</strong>: 브라우저의 동일 출처 정책을 이용하여 악성 사이트가 타 사이트의 쿠키에 접근하지 못하게 합니다.</li>
</ol>
<p>CSRF 공격을 방지하는 이러한 접근 방식은 업계에서 널리 사용되고 있으며, 안정적인 보안을 제공합니다.</p>
<h2 id="spring-security-csrf-기본-설정">Spring Security CSRF 기본 설정</h2>
<p><strong>SecurityConfig</strong></p>
<pre><code class="language-java">http.csrf(AbstractHttpConfigurer::disable)</code></pre>
<p>지금까지는 csrf 설정을 비활성화 해둔 채로 실습을 진행하였습니다.</p>
<p><code>http.csrf(AbstractHttpConfigurer::disable)</code> 설정을 제거하여, csrf 설정을 활성화 해보겠습니다.
기본적으로 Spring Security는 데이터를 변경하거나 생성하거나 삭제하는 모든 http 메소드를 차단합니다.</p>
<p><strong>POST 요청</strong></p>
<pre><code class="language-json">{
    &quot;status&quot;: 403,
    &quot;error&quot;: &quot;Forbidden&quot;,
    &quot;message&quot;: &quot;Invalid CSRF Token &#39;null&#39; was found on the request parameter &#39;_csrf&#39; or header &#39;X-CSRF-TOKEN&#39;.&quot;,
    &quot;path&quot;: &quot;/register&quot;
}</code></pre>
<p><strong>&#39;잘못된 CSRF 토큰 &#39;null&#39;이 요청 매개변수에서 발견되었습니다&#39;</strong>라는 메시지와 함께 403 에러가 발생하였습니다.
이 에러가 발생하는 이유는 <code>post</code> 메소드를 사용하여 새 데이터를 생성하려고 하지만 요청의 일부로 CSRF 토큰을 전달하지 않기 때문입니다. CSRF 토큰이 누락되면 Spring Security는 누군가가 CSRF 공격을 통해 애플리케이션을 공격하려고 한다고 가정합니다. 따라서 403 에러를 발생시켜 이 요청 처리를 중단한 것입니다.</p>
<h2 id="spring-security의-csrf">Spring Security의 CSRF</h2>
<p><strong>CsrfToken 인터페이스</strong>
<strong>구현체</strong>: 기본적으로 <strong>DefaultCsrfToken</strong> 사용</p>
<p><strong>CsrfTokenRepository 인터페이스</strong>
<strong>구현체</strong>: 기본적으로 <strong>CookieCsrfTokenRepository</strong> 사용
Csrf는 Session보다 Cookie에 저장하는 것을 권장</p>
<p><strong>CsrfFilter</strong></p>
<ul>
<li>요청에서 CsrfToken 로드</li>
<li>토큰이 어떤 방식으로든 유효하지 않다면 <code>AccessDeniedException</code> 발생</li>
</ul>
<p><strong>SecurityConfig</strong></p>
<pre><code class="language-java">http.csrf(csrfConfig -&gt;
             csrfConfig.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));</code></pre>
<p><strong>CookieCsrfTokenRepository</strong></p>
<pre><code class="language-java">public static CookieCsrfTokenRepository withHttpOnlyFalse() {
    CookieCsrfTokenRepository result = new CookieCsrfTokenRepository();
    result.cookieHttpOnly = false;
    return result;
}</code></pre>
<ul>
<li>Cookie에서 CSRF 토큰을 읽어야 하기 때문에 <code>result.cookieHttpOnly = false</code>로 설정한다.</li>
<li>React나 Vue 등의 클라이언트 애플리케이션을 사용하는 경우 반드시 <code>withHttpOnlyFalse()</code> 사용</li>
</ul>
<blockquote>
<p>&quot;XSRF-TOKEN&quot;이라는 쿠키에 CSRF 토큰을 유지하고 AngularJS의 규칙에 따라 &quot;X-XSRF-TOKEN&quot; 헤더에서 읽는 CsrfTokenRepository 입니다. AngularJS와 함께 사용할 때는 반드시 <code>withHttpOnlyFalse()</code> 사용하세요.</p>
</blockquote>
<p>이 구조를 통해 Spring Security는 JavaScript를 통해 읽을 수 있는 CSRF 토큰을 생성하고, 이를 HTTP 요청마다 검증하여 CSRF 공격을 방지합니다.</p>
<h3 id="커스텀-csrf-필터-생성">커스텀 Csrf 필터 생성</h3>
<p><strong>CsrfCookieFilter</strong></p>
<pre><code class="language-java">package com.study.springsecsection1.filter;

import java.io.IOException;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.filter.OncePerRequestFilter;

public class CsrfCookieFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
        // 지연된 토큰을 로드하여 쿠키에 토큰 값을 렌더링
        csrfToken.getToken();
        filterChain.doFilter(request, response);
    }
}</code></pre>
<p><strong>SecurityConfig</strong></p>
<pre><code class="language-java">http.csrf(csrfConfig -&gt;
              csrfConfig.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
    .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)</code></pre>
<p>제가 만든 이 커스텀 필터를 <code>BasicAuthenticationFilter</code> 실행이 완료된 후에 실행되도록 하고 싶습니다.
왜 여기서 <code>BasicAuthenticationFilter</code>를 명시하려고 하는 걸까요?</p>
<p>이유는 매우 간단합니다
이제부터는 httpBasic 형식을 사용하여 자격 증명을 전달함으로써 로그인 작업을 수행할 것입니다
따라서 httpBasic 형식을 사용하여 자격 증명을 보낼 때마다 이 필터는 자격 증명을 추출하여 실제 인증을 수행하는 역할을 할 것입니다. 인증이 완료되면 Spring Security 프레임워크는 <code>CookieCsrfTokenRepository</code> 클래스의 도움으로 CSRF 토큰을 생성할 것입니다. 모든 작업이 완료되면 저는 이 필터가 실행되어 지연된 토큰을 읽고 실제 값을 로드하도록 하고 싶습니다.</p>
<p><strong>CsrfFilter</strong>에서 UI 애플리케이션의 요청에서 CSRF 토큰 값을 어디서 읽어야 하는지 알려줘야 합니다.</p>
<p><strong>SecurityConfig</strong></p>
<pre><code class="language-java">CsrfTokenRequestAttributeHandler csrfTokenRequestAttributeHandler = new CsrfTokenRequestAttributeHandler();

http.csrf(csrfConfig -&gt; csrfConfig.csrfTokenRequestHandler(csrfTokenRequestAttributeHandler)
        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))</code></pre>
<p><strong>SecurityConfig</strong></p>
<pre><code class="language-java">http.sessionManagement(sessionConfig -&gt; sessionConfig.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))</code></pre>
<ul>
<li><code>SessionCreationPolicy.ALWAYS</code>: 항상 세션을 생성. 이렇게 하면 동일한 세션을 반복해서 재사용하여 보안 API에 접근</li>
<li>따라서 세션 설정에 따라 생성되는 jsessionId는 SecurityContextHolder에 자동으로 저장되지 않을 것입니다.</li>
<li>우리는 jsessionId를 수동으로 SecurityContextHolder 안에 저장하거나 Spring 프레임워크가 이를 처리할 수 있도록 설정해야합니다.</li>
</ul>
<p><strong>SecurityConfig</strong></p>
<pre><code class="language-java">http.securityContext(contextConfig -&gt; contextConfig.requireExplicitSave(false))</code></pre>
<p>Spring 프레임워크에 jsessionId 세부 정보나 로그인된 인증 세부 정보를 SecurityContextHolder에 저장하지 않겠다고 알리는 것입니다. 대신 Spring Security 프레임워크가 이를 처리하도록 하고 싶습니다.</p>
<p><strong>Postman 요청</strong>
<img src="https://velog.velcdn.com/images/sangcheol_/post/f80fdaba-dc5f-4d8d-bfa8-8598d32684a3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/ba89f2a1-d535-46fe-a93f-e2b76e4eb53c/image.png" alt=""></p>
<p><code>jsessionid</code>와 <code>XSRF-TOKEN</code>이 응답 쿠키에 담긴다.</p>
<pre><code class="language-json">{
    &quot;status&quot;: 403,
    &quot;error&quot;: &quot;Forbidden&quot;,
    &quot;message&quot;: &quot;Invalid CSRF Token &#39;null&#39; was found on the request parameter &#39;_csrf&#39; or header &#39;X-XSRF-TOKEN&#39;.&quot;,
    &quot;path&quot;: &quot;/register&quot;
}</code></pre>
<p>이 쿠키를 이용하여 POST 요청을 보내면 여전히 403 에러가 발생한다.
<code>X-XSRF-TOKEN</code> 헤더에 csrf 토큰이 담겨있지 않기 때문이다.</p>
<p><code>XSRF-TOKEN</code> 쿠키의 토큰 값을 <code>X-XSRF-TOKEN</code> 헤더에 담은 후 요청을 보내면 POST 요청이 성공하게 된다.
<img src="https://velog.velcdn.com/images/sangcheol_/post/e5480f58-d28e-4aa0-9e34-04da4438604b/image.png" alt=""></p>
<h3 id="ui-프레임워크-로그인-로직---csrf-설정-추가">UI 프레임워크 로그인 로직 - CSRF 설정 추가</h3>
<p><strong>LoginComponent</strong></p>
<pre><code class="language-typescript">...
import { getCookie } from &#39;typescript-cookie&#39;;

export class LoginComponent implements OnInit {
  ...
  validateUser(loginForm: NgForm) {
    this.loginService.validateLoginDetails(this.model).subscribe(
      responseData =&gt; {
        this.model = &lt;any&gt; responseData.body;
        this.model.authStatus = &#39;AUTH&#39;;
        window.sessionStorage.setItem(&quot;userdetails&quot;,JSON.stringify(this.model));
        let xsrf = getCookie(&quot;XSRF-TOKEN&quot;)!; // XSRF-TOKEN 쿠키에서 꺼내기
        window.sessionStorage.setItem(&quot;XSRF-TOKEN&quot;,xsrf); // 세션 스토리지에 저장
        this.router.navigate([&#39;dashboard&#39;]);
      });
  }
  ...
}</code></pre>
<ol>
<li><code>XSRF-TOKEN</code> 쿠키에서 꺼내기</li>
<li>세션 스토리지에 저장</li>
</ol>
<p><strong>XhrInterceptor</strong></p>
<pre><code class="language-typescript">import { Injectable } from &#39;@angular/core&#39;;
import { HttpInterceptor,HttpRequest,HttpHandler,HttpErrorResponse, HttpHeaders } from &#39;@angular/common/http&#39;;
import {Router} from &#39;@angular/router&#39;;
import {tap} from &#39;rxjs/operators&#39;;
import { User } from &#39;src/app/model/user.model&#39;;

@Injectable()
export class XhrInterceptor implements HttpInterceptor {

  user = new User();
  constructor(private router: Router) {}

  intercept(req: HttpRequest&lt;any&gt;, next: HttpHandler) {
    let httpHeaders = new HttpHeaders();
    if(sessionStorage.getItem(&#39;userdetails&#39;)){
      this.user = JSON.parse(sessionStorage.getItem(&#39;userdetails&#39;)!);
    }
    if(this.user &amp;&amp; this.user.password &amp;&amp; this.user.email){
      httpHeaders = httpHeaders.append(&#39;Authorization&#39;, &#39;Basic &#39; + window.btoa(this.user.email + &#39;:&#39; + this.user.password));
    }

    // csrf 검증 추가
    let xsrf = sessionStorage.getItem(&#39;XSRF-TOKEN&#39;);
    if(xsrf){
      httpHeaders = httpHeaders.append(&#39;X-XSRF-TOKEN&#39;, xsrf);
    }

    httpHeaders = httpHeaders.append(&#39;X-Requested-With&#39;, &#39;XMLHttpRequest&#39;);
    const xhr = req.clone({
      headers: httpHeaders
    });
  return next.handle(xhr).pipe(tap(
      (err: any) =&gt; {
        if (err instanceof HttpErrorResponse) {
          if (err.status !== 401) {
            return;
          }
          this.router.navigate([&#39;dashboard&#39;]);
        }
      }));
  }
}</code></pre>
<p><strong>인터셉터에 CSRF 설정</strong></p>
<pre><code class="language-typescript">let xsrf = sessionStorage.getItem(&#39;XSRF-TOKEN&#39;);
if(xsrf){
  httpHeaders = httpHeaders.append(&#39;X-XSRF-TOKEN&#39;, xsrf);
}</code></pre>
<p>인터셉터는 API 서버에 요청할 때마다 요청을 가로챈다. 로그인 할 때 세션 스토리지에 담아두었던 <code>XSRF-TOKEN</code>를 가져와   <code>X-XSRF-TOKEN</code> 헤더에 담아보낸다.</p>
<p><strong>logout 로직</strong></p>
<pre><code class="language-typescript">ngOnInit(): void {
  window.sessionStorage.setItem(&quot;userdetails&quot;,&quot;&quot;);
  window.sessionStorage.setItem(&quot;XSRF-TOKEN&quot;,&quot;&quot;);
  this.router.navigate([&#39;/login&#39;]);
}</code></pre>
<blockquote>
<p><strong>취약점</strong>
로그아웃을 하면 세션 스토리지에서의 <code>userdetails</code>과 <code>XSRF-TOKEN</code>은 사라지지만, Cookie에는 <code>userdetails</code>과 <code>XSRF-TOKEN</code>이 여전히 존재하게 된다. 따라서 공용 PC에서 로그인을 한 후 로그아웃을 했다고 안심할 수 없다. 왜냐하면 쿠키에서 값을 가져와 세션 스토리지에 넣어버리고 요청하면 되기 때문이다.</p>
</blockquote>
<h2 id="csrf-무시">CSRF 무시</h2>
<p>공개 API인 경우 POST 요청에서도 CSRF를 허용해야 할 경우가 있다. CSRF를 허용할 경우 해야할 설정에 대해 알아보자.</p>
<p><strong>SpringConfig</strong></p>
<pre><code class="language-java">.csrf(csrfConfig -&gt; csrfConfig.ignoringRequestMatchers(&quot;/contact&quot;, &quot;/register&quot;))</code></pre>
<p>위와 같이 <code>ignoringRequestMatchers()</code> 메서드를 이용하면 된다.</p>
<blockquote>
<p><strong>참고</strong>
다른 백엔드 API를 통해서만 호출할 수 있는 애플리케이션을 구축하려는 경우, UI 애플리케이션을 통해서가 아니라면 이러한 시나리오에서는 CSRF를 비활성화할 수 있습니다. 왜냐하면 해커 시나리오는 UI 애플리케이션과 브라우저를 사용할 때만 가능하기 때문입니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security 내부 동작 흐름]]></title>
            <link>https://velog.io/@sangcheol_/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EB%82%B4%EB%B6%80-%EB%8F%99%EC%9E%91-%ED%9D%90%EB%A6%84</link>
            <guid>https://velog.io/@sangcheol_/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EB%82%B4%EB%B6%80-%EB%8F%99%EC%9E%91-%ED%9D%90%EB%A6%84</guid>
            <pubDate>Thu, 24 Oct 2024 18:00:09 GMT</pubDate>
            <description><![CDATA[<p>스프링 시큐리티의 내부 동작 흐름은 HTTP 요청을 처리하는 과정에서 어떻게 인증과 보안을 관리하는지 보여주는 중요한 개념입니다. 이를 이해하면 스프링 시큐리티가 웹 애플리케이션의 보안을 어떻게 강화하는지 명확해집니다. 이 글에서는 스프링 시큐리티의 기본적인 인증 흐름을 단계별로 설명하겠습니다.</p>
<h3 id="1-클라이언트의-요청-처리">1. 클라이언트의 요청 처리</h3>
<p>먼저, 최종 사용자가 브라우저 또는 Postman 같은 도구를 이용해 애플리케이션에 HTTP(S) 요청을 보냅니다. 만약 해당 요청이 스프링 시큐리티로 보호된 엔드포인트로 들어오면, 이 요청은 서블릿 필터 체인에 있는 스프링 시큐리티 필터에 의해 가로채집니다. 스프링 시큐리티는 여러 개의 필터(예: 5개, 10개, 20개 등)를 가지고 있으며, 각 필터는 설정에 따라 요청을 하나씩 처리하게 됩니다.</p>
<h3 id="2-자격-증명-처리">2. 자격 증명 처리</h3>
<p>사용자가 올바른 자격 증명(예: 사용자 이름과 비밀번호)을 제공했다고 가정하면, 이 자격 증명은 <code>HttpServletRequest</code> 객체에 포함되어 HTTP 헤더나 본문(body)에 위치하게 됩니다. 하지만 애플리케이션 전체에서 <code>HttpServletRequest</code> 객체를 직접 사용할 수 없으므로, 스프링 시큐리티 필터는 먼저 자격 증명 부분을 <code>Authentication</code> 객체로 변환합니다. 이 <code>Authentication</code> 객체에는 <code>username</code>, <code>password</code>, <code>isAuthenticated</code> 같은 필드가 존재하며, 객체가 처음 생성될 때 <code>isAuthenticated</code> 필드는 <code>false</code>로 설정됩니다.</p>
<h3 id="3-authentication-manager로-전달">3. Authentication Manager로 전달</h3>
<p>생성된 <code>Authentication</code> 객체는 <code>AuthenticationManager</code>로 전달됩니다. <code>AuthenticationManager</code>는 인증을 완료하는 책임을 가지며, 그 결과를 다시 필터로 전달합니다. 다만, 실제로 인증을 처리하지는 않고, 인증 요청을 적절한 곳으로 전달하는 역할만 합니다.</p>
<h3 id="4-authentication-provider에-위임">4. Authentication Provider에 위임</h3>
<p><code>AuthenticationManager</code>는 실제 인증을 처리하기 위해 <code>AuthenticationProvider</code>에게 이 역할을 위임합니다. 스프링 시큐리티는 여러 가지 인증 프로바이더를 제공하며, 필요에 따라 커스텀 프로바이더를 구현할 수도 있습니다.</p>
<h3 id="5-사용자-정보-로드">5. 사용자 정보 로드</h3>
<p><code>AuthenticationProvider</code>는 실제 인증을 수행하기 위해 <code>UserDetailsService</code> 또는 <code>UserDetailsManager</code>의 도움을 받아 사용자 정보를 로드합니다. <code>AuthenticationProvider</code>는 <code>Authentication</code> 객체의 사용자 이름을 기반으로 <code>UserDetails</code> 객체를 찾고, <code>UserDetailsManager</code>를 통해 해당 사용자의 세부 정보를 가져옵니다.</p>
<h3 id="6-비밀번호-검증">6. 비밀번호 검증</h3>
<p><code>AuthenticationProvider</code>는 사용자가 입력한 비밀번호와 DB에 저장된 비밀번호를 비교하기 위해 <code>PasswordEncoder</code>를 사용합니다. 이 <code>PasswordEncoder</code>는 비밀번호를 평문으로 저장하거나 해싱 알고리즘을 통해 암호화하여 저장하는 역할을 하며, 이를 통해 비밀번호를 안전하게 관리할 수 있습니다.</p>
<h3 id="7-인증-성공실패-처리">7. 인증 성공/실패 처리</h3>
<p><code>PasswordEncoder</code>를 통해 비밀번호가 일치하는 경우, <code>AuthenticationProvider</code>는 인증이 성공했다고 <code>AuthenticationManager</code>에게 알립니다. 이때 <code>Authentication</code> 객체의 <code>isAuthenticated</code> 필드 값은 <code>true</code>로 변경됩니다. <code>AuthenticationManager</code>는 이 필드를 통해 인증 성공 여부를 알 수 있습니다.</p>
<h3 id="8-인증-결과-반환">8. 인증 결과 반환</h3>
<p>인증이 완료된 <code>Authentication</code> 객체는 다시 스프링 시큐리티 필터로 반환됩니다.</p>
<h3 id="9-securitycontext에-저장">9. SecurityContext에 저장</h3>
<p>스프링 시큐리티 필터는 인증된 정보를 <code>SecurityContext</code>에 저장합니다. 이 <code>Authentication</code> 객체는 세션 ID와 연관되며, 이후 동일한 브라우저에서 동일한 보호된 페이지에 접근하면 <code>SecurityContext</code>에서 세부 정보를 로드합니다. 이렇게 하면 최초 요청 이후에는 매번 인증을 새로 하지 않고, 이미 인증된 사용자 정보를 사용해 빠르게 응답을 처리할 수 있습니다.</p>
<h3 id="10-최종-응답-처리">10. 최종 응답 처리</h3>
<p>마지막으로 인증 결과에 따라 클라이언트에게 응답이 전송됩니다. 인증에 성공한 경우 API 응답을 반환하고, 인증에 실패한 경우에는 <code>401</code>(Unauthorized) 또는 <code>403</code>(Forbidden) 오류 메시지가 반환됩니다.</p>
<p>스프링 시큐리티는 이와 같은 철저한 인증 흐름을 통해 웹 애플리케이션의 보안을 강화하며, 이를 통해 사용자는 안전하게 애플리케이션을 이용할 수 있고, 개발자는 간편하게 보안 설정을 적용할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[IntelliJ] Java Code Style imports 세팅]]></title>
            <link>https://velog.io/@sangcheol_/IntelliJ-Java-Code-Style-imports-%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@sangcheol_/IntelliJ-Java-Code-Style-imports-%EC%84%B8%ED%8C%85</guid>
            <pubDate>Tue, 01 Oct 2024 05:51:41 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 진행할 때, <strong>코딩 컨벤션</strong>을 정하면 코드의 <strong>일관성</strong>과 <strong>가독성</strong>을 높이는 데 도움이 됩니다. 보통 <a href="https://naver.github.io/hackday-conventions-java/#import-grouping">네이버 코드 컨벤션</a>이나 <a href="https://google.github.io/styleguide/javaguide.html">구글 코드 컨벤션</a>을 참고하는 경우가 많습니다.</p>
<p>저는 개인적으로 <strong>IntelliJ</strong>의 기본 코드 컨벤션을 선호하지만, <strong>import 관련 규칙</strong>만큼은 네이버 코드 컨벤션을 따르고 싶을 때가 있습니다. 이 경우 아래 설정을 적용할 수 있습니다. 또한, 네이버 코드 컨벤션에는 <strong>jakarta</strong> 패키지가 누락되어 있으므로, 해당 규칙을 사용할 때는 아래 설정을 추가하는 것이 좋습니다.</p>
<p><strong>import 순서</strong></p>
<ol>
<li>static imports</li>
<li>java.</li>
<li>javax.</li>
<li>jakarta.</li>
<li>org.</li>
<li>net.</li>
<li>8, 9를 제외한 com.*</li>
<li>1 ~ 7, 9를 제외한 패키지에 있는 클래스</li>
<li>나의 프로젝트 패키지 (ex: com.mypackage.*)</li>
</ol>
<p><strong>Setting -&gt; Code Style -&gt; Java</strong>
<img src="https://velog.velcdn.com/images/sangcheol_/post/afe7d26e-974c-4e44-9f39-16385777b667/image.png" alt=""></p>
<!-- {"width":774} -->
<ul>
<li><p><code>General</code> 에서 <code>Use single class import</code> 체크
<img src="https://velog.velcdn.com/images/sangcheol_/post/0435b7fc-bc46-47a4-8961-6315a044d7a4/image.png" alt=""></p>
</li>
<li><p><code>Class count to use import with &#39;*&#39;: 99</code> 설정</p>
</li>
<li><p><code>Names count to use static import with &#39;*&#39;: 1</code> 설정</p>
</li>
</ul>
<p><strong>imports 설정</strong>
가장 밑 설정에서 <code>+</code> 버튼을 누르면 <code>Add Package</code>와 <code>Add Blank</code>가 나오는데 두가지를 이용하여 아래와 같이 설정</p>
<pre><code class="language-java">&lt;blank line&gt;
import static all other imports
&lt;blank line&gt;
import java.*
&lt;blank line&gt;
import javax.*
&lt;blank line&gt;
import jakarta.*
&lt;blank line&gt;
import org.*
&lt;blank line&gt;
import net.*
&lt;blank line&gt;
import com.*
&lt;blank line&gt;
import all other imports
&lt;blank line&gt;
import com.mypackage.* // 나의 패키지 명으로 변경</code></pre>
<p><strong>결과 예시</strong></p>
<pre><code class="language-java">package com.ourposapp.user.domain;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import jakarta.persistence.AttributeOverride;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import com.ourposapp.common.model.BaseTimeEntity;
import com.ourposapp.common.model.Phone;
import com.ourposapp.global.error.ErrorCode;
import com.ourposapp.global.error.exception.EntityNotFoundException;
import com.ourposapp.global.error.exception.InvalidAddressException;
import com.ourposapp.global.jwt.dto.JwtTokenDto;
import com.ourposapp.global.util.DateTimeUtils;
import com.ourposapp.user.application.user.dto.UserAddressUpdateDto;
import com.ourposapp.user.domain.user.constant.LoginType;
import com.ourposapp.user.domain.user.constant.Role;

// ...</code></pre>
<p><strong>참조</strong>
<a href="https://naver.github.io/hackday-conventions-java/#import-grouping">https://naver.github.io/hackday-conventions-java/#import-grouping</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Querydsl 페이징 (Querydsl 5.0 이후)]]></title>
            <link>https://velog.io/@sangcheol_/Querydsl-%ED%8E%98%EC%9D%B4%EC%A7%95-Querydsl-5.0-%EC%9D%B4%ED%9B%84</link>
            <guid>https://velog.io/@sangcheol_/Querydsl-%ED%8E%98%EC%9D%B4%EC%A7%95-Querydsl-5.0-%EC%9D%B4%ED%9B%84</guid>
            <pubDate>Sun, 08 Sep 2024 12:56:38 GMT</pubDate>
            <description><![CDATA[<p>Querydsl 5.0 이후 <code>fetchResults()</code> 와 <code>fetchCount()</code> 는 deprecated 되었다.</p>
<p><strong>deprecated 이유</strong></p>
<p><code>QueryResults.getOffset()</code> 또는 <code>QueryResults.getLimit()</code> 대신 <code>fetch()</code>를 사용하는 것이 더 성능이 좋으므로 이 함수를 사용하는 것이 좋습니다. 또한 모든 방언에 대해 카운트 쿼리가 제대로 생성되지 않을 수도 있습니다. 예를 들어, JPA에서는 그룹화 표현식이나 having 절이 여러 개 있는 쿼리에 대해 카운트 쿼리를 생성할 수 없습니다. 쿼리 결과 형식으로 투영을 가져옵니다. 쿼리 결과의 총 행 수가 필요하지 않은 경우 <code>fetch()</code>를 대신 사용하세요.</p>
<p><code>fetchCount()</code> : <code>fetch().size()</code>로 구현</p>
<p>따라서 아래와 같은 방법으로 Page 객체를 만들 수 있다.</p>
<pre><code class="language-java">public Page&lt;MemberTeamDto&gt; searchPageSimple(MemberSearchCond condition, Pageable pageable) {
    // paging query
    List&lt;MemberTeamDto&gt; content = queryFactory
        .select(new QMemberTeamDto(
            member.id.as(&quot;memberId&quot;),
            member.username,
            member.age,
            team.id.as(&quot;teamId&quot;),
            team.name.as(&quot;teamName&quot;)
        ))
        .from(member)
        .leftJoin(member.team, team)
        .where(
            usernameEq(condition.getUsername()),
            teamNameEq(condition.getTeamName()),
            ageGoe(condition.getAgeGoe()),
            ageLoe(condition.getAgeLoe())
        )
        .offset(pageable.getOffset())
        .limit(pageable.getPageSize())
        .fetch();

    // count query
    JPAQuery&lt;Long&gt; countQuery = queryFactory
        .select(member.count())
        .from(member)
        .leftJoin(member.team, team)
        .where(
            usernameEq(condition.getUsername()),
            teamNameEq(condition.getTeamName()),
            ageGoe(condition.getAgeGoe()),
            ageLoe(condition.getAgeLoe())
        );

    return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
}</code></pre>
<p><code>PageableExecutionUtils</code> 이용 장점</p>
<ul>
<li>특정한 상황에서 count 쿼리가 나가지 않는 최적화 적용</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링부트 3.0 이상] p6spy 초기 설정]]></title>
            <link>https://velog.io/@sangcheol_/p6spy-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@sangcheol_/p6spy-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Sun, 08 Sep 2024 12:52:41 GMT</pubDate>
            <description><![CDATA[<p><strong>build.gradle</strong></p>
<pre><code class="language-groovy">dependencies {
    // P6Spy
    implementation &#39;com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0&#39;    
}</code></pre>
<ul>
<li>스프링부트 3.0 이상인 경우 반드시 1.9.0 버전 사용</li>
</ul>
<p><strong>P6SpyFormatter</strong></p>
<pre><code class="language-java">package study.community.config.p6spy;

import java.util.Locale;

import org.hibernate.engine.jdbc.internal.FormatStyle;

import com.p6spy.engine.logging.Category;
import com.p6spy.engine.spy.appender.MessageFormattingStrategy;

public class P6SpyFormatter implements MessageFormattingStrategy {

    private static final String NEW_LINE = &quot;\n&quot;;
    private static final String TAP = &quot;\t&quot;;
    private static final String CREATE = &quot;create&quot;;
    private static final String ALTER = &quot;alter&quot;;
    private static final String DROP = &quot;drop&quot;;
    private static final String COMMENT = &quot;comment&quot;;

    @Override
    public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared, String sql, String url) {
        if (sql.trim().isEmpty()) {
            return formatByCommand(category);
        }
        return formatBySql(sql, category) + getAdditionalMessages(elapsed);
    }

    private static String formatByCommand(String category) {
        return NEW_LINE
            + &quot;Execute Command : &quot;
            + NEW_LINE
            + NEW_LINE
            + TAP
            + category
            + NEW_LINE
            + NEW_LINE
            + &quot;----------------------------------------------------------------------------------------------------&quot;;
    }

    private String formatBySql(String sql, String category) {
        if (isStatementDDL(sql, category)) {
            return NEW_LINE
                + &quot;Execute DDL : &quot;
                + NEW_LINE
                + FormatStyle.DDL
                .getFormatter()
                .format(sql);
        }
        return NEW_LINE
            + &quot;Execute DML : &quot;
            + NEW_LINE
            + FormatStyle.BASIC
            .getFormatter()
            .format(sql);
    }

    private String getAdditionalMessages(long elapsed) {
        return NEW_LINE
            + NEW_LINE
            + String.format(&quot;Execution Time: %s ms&quot;, elapsed)
            + NEW_LINE
            + &quot;----------------------------------------------------------------------------------------------------&quot;;
    }

    private boolean isStatementDDL(String sql, String category) {
        return isStatement(category) &amp;&amp; isDDL(sql.trim().toLowerCase(Locale.ROOT));
    }

    private boolean isStatement(String category) {
        return Category.STATEMENT.getName().equals(category);
    }

    private boolean isDDL(String lowerSql) {
        return lowerSql.startsWith(CREATE)
            || lowerSql.startsWith(ALTER)
            || lowerSql.startsWith(DROP)
            || lowerSql.startsWith(COMMENT);
    }
}</code></pre>
<p><strong>P6SpyEventListener</strong></p>
<pre><code class="language-java">package study.community.config.p6spy;

import java.sql.SQLException;

import com.p6spy.engine.common.ConnectionInformation;
import com.p6spy.engine.event.JdbcEventListener;
import com.p6spy.engine.spy.P6SpyOptions;

public class P6SpyEventListener extends JdbcEventListener {

    @Override
    public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException e) {
        P6SpyOptions.getActiveInstance().setLogMessageFormat(P6SpyFormatter.class.getName());
    }
}
</code></pre>
<p><strong>P6SpyConfig</strong></p>
<pre><code class="language-java">package study.community.config.p6spy;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Profile({&quot;default&quot;, &quot;test&quot;})
@Configuration
public class P6SpyConfig {

    @Bean
    public P6SpyEventListener p6SpyCustomEventListener() {
        return new P6SpyEventListener();
    }

    @Bean
    public P6SpyFormatter p6SpyCustomFormatter() {
        return new P6SpyFormatter();
    }
}</code></pre>
<p><strong>application.yml</strong></p>
<pre><code class="language-yaml">spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/community
    username: sa
    password:
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create

logging:
  level:
    p6spy: info

decorator:
  datasource:
    p6spy:
      enable-logging: true</code></pre>
<p><strong>결과</strong></p>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/b5bac19b-36f9-4417-ab41-5281931eeeed/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/dd97456e-9a3f-4896-99a7-9c4b491ab7eb/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/6bf49ce3-2fe2-482f-8d3b-dd9174bc9e03/image.png" alt=""></p>
<p><strong>참조</strong></p>
<p><a href="https://www.inflearn.com/community/questions/1032603/p6spy-%ED%8F%AC%EB%A7%B7-%EC%84%A4%EC%A0%95-%EA%B3%B5%EC%9C%A0%ED%95%A9%EB%8B%88%EB%8B%A4">P6Spy 포맷 설정 공유합니다. - 인프런 | 커뮤니티 질문&amp;답변</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[BaseEntity, BaseTimeEntity 분리 설정]]></title>
            <link>https://velog.io/@sangcheol_/BaseEntity-BaseTimeEntity-%EB%B6%84%EB%A6%AC-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@sangcheol_/BaseEntity-BaseTimeEntity-%EB%B6%84%EB%A6%AC-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Sun, 08 Sep 2024 12:44:54 GMT</pubDate>
            <description><![CDATA[<p><strong>BaseTimeEntity</strong></p>
<pre><code class="language-java">package study.datajpa.entity;

import java.time.LocalDateTime;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;

import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import lombok.Getter;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {

    @CreatedDate
    @Column(updatable = false) // 수정되지 않도록
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}</code></pre>
<p><strong>BaseEntity</strong></p>
<pre><code class="language-java">package study.datajpa.entity;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;

import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import lombok.Getter;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity extends BaseTimeEntity {

    @CreatedBy
    @Column(updatable = false) // 수정되지 않도록
    private String createdBy;

    @LastModifiedBy
    private String lastModifiedBy;
}</code></pre>
<p><strong>BaseEntity와 BaseTimeEntity 분리 이유</strong></p>
<p>생성일(<code>CreatedDate</code>)과 수정일(<code>LastModifiedDate</code>)은 거의 모든 테이블에서 사용하지만, 생성자(<code>CreatedBy</code>)와 수정자(<code>LastModifiedBy</code>)는 사용하지 않는 테이블들도 존재한다. </p>
<p>따라서 두 클래스를 분리하여 생성일과 수정일만 필요한 클래스는 <code>BaseTimeEntity</code>를 상속받고 
생성일, 수정일, 생성자, 수정자가 모두 필요한 클래스는 <code>BaseEntity</code>를 상속 받아 이용하면 된다.</p>
<p><strong>Application</strong></p>
<pre><code class="language-java">package study.datajpa;

import java.util.Optional;
import java.util.UUID;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing // 추가
@SpringBootApplication
public class DataJpaApplication {

    public static void main(String[] args) {
        SpringApplication.run(DataJpaApplication.class, args);
    }

    @Bean
    public AuditorAware&lt;String&gt; auditorProvider() {
        return () -&gt; Optional.of(UUID.randomUUID().toString());
    }

}</code></pre>
<p><code>@EnableJpaAuditing</code> 추가</p>
<p><strong>생성자, 수정자 로직</strong></p>
<pre><code class="language-java">@Bean
public AuditorAware&lt;String&gt; auditorProvider() {
    return () -&gt; Optional.of(UUID.randomUUID().toString());
}</code></pre>
<p>예제에서는 UUID를 이용했지만, 스프링 시큐리티를 이용했다면, <code>SecurityContextHolder</code>에서 id값을 가져와 넣어주면 된다.</p>
<h3 id="참고자료">참고자료</h3>
<p>자바 ORM 표준 JPA 프로그래밍 - 김영한</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[인프콘] 객체지향은 여전히 유용한가 - 조영호님 세미나 정리]]></title>
            <link>https://velog.io/@sangcheol_/-%EB%8B%98-%EC%84%B8%EB%AF%B8%EB%82%98-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@sangcheol_/-%EB%8B%98-%EC%84%B8%EB%AF%B8%EB%82%98-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 29 Aug 2024 07:50:44 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.inflearn.com/course/%EC%9D%B8%ED%94%84%EC%BD%982024-%EB%8B%A4%EC%8B%9C%EB%B3%B4%EA%B8%B0">[지금 무료] 인프콘 2024 다시보기 강의 | 인프런 - 인프런</a></p>
<p>객체지향은 여전히 유용한가? 라는 주제의 조영호님 세미나를 듣고 정리해보았다.</p>
<p><strong>객체지향은 여전히 유용할까?</strong></p>
<p>위의 질문보다 올바른 질문은 무엇일까? 더 건설적인 질문은 무엇일까?</p>
<p>먼저 객체지향과 절차지향 설계는 각각 어떤 상황에서 더 적합하며, 각 방식의 장단점은 무엇일지에 대해 예시를 통해 알아보자.</p>
<h2 id="예제-도메인-설명">예제 도메인 설명</h2>
<p><em>장바구니에 할인 프로모션 적용</em></p>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/fa8358de-84fa-4dc2-960b-31521d1cb7f3/image.png" alt=""></p>
<blockquote>
<p>장바구니에 <strong>할인을 적용할지 말지</strong>를 판단하고 <strong>조건</strong>을 충족하면 할인을 적용</p>
</blockquote>
<p><strong>기본 조건</strong></p>
<p>Cart 전체 금액 ≥ Promotion 기준 금액</p>
<h2 id="절차적인-설계">절차적인 설계</h2>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/f6a2a448-5dd6-4f00-946b-ccfb3648163d/image.png" alt=""></p>
<p><strong>절차적인 방식</strong></p>
<ul>
<li><strong>데이터</strong>와 <strong>프로세스</strong>를 별도의 클래스로 구현하는 방식</li>
<li><code>Promotion</code>과 <code>Cart</code> 클래스는 <strong>데이터</strong></li>
</ul>
<p><strong>Promotion - 데이터</strong></p>
<pre><code class="language-java">public class Promotion {

    private Long cartId;
    private Long basePrice;

    public Money getBasePrice() {
        return basePrice;
    }

    public void setCart(Long cartId) {
        this.cartId = cartId;
    }
}</code></pre>
<p><strong>Cart - 데이터</strong></p>
<pre><code class="language-java">public class Cart {

    private List&lt;CartLineItem&gt; items = new ArrayList&lt;&gt;();

    public Long getTotalPrice() {
        return items.stream().mapToLong(CartLineItem::getPrice).sum();
    }

    public int getTotalQuantity() {
        return items.stream().mapToInt(CartLineItem::getQuantity).sum();
    }
}</code></pre>
<p><strong>Promotion</strong></p>
<ul>
<li><code>basePrice</code>라는 기준 값을 가지는 속성</li>
<li>속성을 외부에 제공하는 <code>Getter</code></li>
</ul>
<p><strong>Cart</strong></p>
<ul>
<li><code>CartLineItem</code>을 리스트로 가짐</li>
<li>토탈 금액 반환 <code>Getter</code></li>
<li>전체 수량 반환 <code>Getter</code></li>
</ul>
<p>위 두가지가 데이터이다.</p>
<p>절차적인 설계는 이 데이터를 가지고 어떤 로직을 처리할 때 <strong>그 로직을 데이터를 담고있지 않은 별도의 클래스에 구현하는 방식</strong>이다.</p>
<p><strong>PromotionProcess - 프로세스</strong></p>
<pre><code class="language-java">public class PromotionProcess {

    public void apply(Promotion promotion, Cart cart) {
        if (isApplicableTo(promotion, cart)) {
            promotion.setCart(cart.getId());
        }
    }

    private boolean isApplicableTo(Promotion promotion, Cart cart) {
        return cart.getTotalPrice() &gt;= promotion.getBasePrice();
    }
}</code></pre>
<p><code>isApplicableTo()</code></p>
<ul>
<li>메서드를 통해 카트의 전체 금액과 프로모션 기준 금액을 비교</li>
</ul>
<p><code>apply()</code></p>
<ul>
<li><code>isApplicableTo()</code> 조건을 만족하면 프로모션에 카트를 세팅</li>
</ul>
<h2 id="객체지향적인-설계">객체지향적인 설계</h2>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/db676882-528a-4ffc-853e-ebecbf70706a/image.png" alt=""></p>
<p>객체지향적인 방식이란 <strong>Promotion</strong>, <strong>Cart</strong> 라는 데이터와 <strong>PromotionProcess</strong>라는 로직을 가지는 프로세스가 하나의 클래스에 같이 뭉쳐놓은 것을 말한다.</p>
<p><strong>Promotion</strong></p>
<pre><code class="language-java">public class Promotion {

    private Cart cart;
    private Long basePrice;

    public void apply(Cart cart) {
        if (cart.getTotalPrice() &gt;= basePrice) {
            this.cart = cart;
        }
    }
}</code></pre>
<p><strong>Cart</strong></p>
<pre><code class="language-java">public class Cart {

    private List&lt;CartLineItem&gt; items = new ArrayList&lt;&gt;();

    public Long getTotalPrice() {
        return items.stream().mapToLong(CartLineItem::getPrice).sum();
    }

    public int getTotalQuantity() {
        return items.stream().mapToInt(CartLineItem::getQuantity).sum();
    }
}</code></pre>
<p><strong>절차적인 설계와 객체지향 설계</strong></p>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/3bdeafc5-5014-421f-9b95-f6cb9daba11d/image.png" alt=""></p>
<h2 id="첫-번째-요구사항-변경---데이터-변경">첫 번째 요구사항 변경 - 데이터 변경</h2>
<p>우리는 객체지향적인 방식과 절차지향적인 방식 중 어느 방식으로 설계할지 결정해야 한다.</p>
<p><strong>변경</strong>이라는 키워드에 초점을 맞추어 요구사항이 변경 될 때 두 방식 중 어떤 방식이 좋은 지 판단해보자</p>
<p>할인 여부를 판단하는데 필요한 <strong>데이터를 변경</strong>한다면?</p>
<p>이전 기준: <strong>Cart 전체 금액 ≥ Promotion 기준 금액</strong></p>
<p>변경된 기준: <strong>Promotion 최소 금액 ≥ Cart 전체 금액 ≤ Promotion의 최대 금액</strong> </p>
<h3 id="절차적인-설계---데이터-변경">절차적인 설계 - 데이터 변경</h3>
<p><strong>Promotion</strong></p>
<pre><code class="language-java">public class Promotion {

    private Long cartId;
    private Long minPrice; // 변경
    private Long maxPrice; // 변경

    // 추가
    public Long getMinPrice() {
        return minPrice;
    }

    // 추가
    public Long getMaxPrice() {
        return maxPrice;
    }

    public void setCart(Long cartId) {
        this.cartId = cartId;
    }

}</code></pre>
<p><strong>PromotionProcess</strong></p>
<pre><code class="language-java">public class PromotionProcess {

    public void apply(Promotion promotion, Cart cart) {
        if (isApplicableTo(promotion, cart)) {
            promotion.setCart(cart.getId());
        }
    }

    private boolean isApplicableTo(Promotion promotion, Cart cart) {
        // 변경
        return cart.getTotalPrice() &gt;= promotion.getMinPrice() &amp;&amp; 
                 cart.getTotalPrice() &lt;= promotion.getMaxPrice();
    }
}</code></pre>
<p>절차적인 설계는 데이터와 그 데이터를 사용하는 프로세스가 별도의 클래스에 구현되어 있기 때문에 <strong>데이터를 바꾸면</strong> 그 데이터를 사용하는 프로세스도 같이 바뀌어야 한다.</p>
<p>⇒ 결합도가 높다!</p>
<h3 id="객체지향-설계---데이터-변경">객체지향 설계 - 데이터 변경</h3>
<p><strong>Promotion</strong></p>
<pre><code class="language-java">public class Promotion {

    private Cart cart;
    private Long minPrice; // 변경
    private Long maxPrice; // 변경

    public void apply(Cart cart) {
        // 변경
        if (cart.getTotalPrice() &gt;= minPrice &amp;&amp;
            cart.getTotalPrice() &lt;= maxPrice) {
            this.cart = cart;
        }
    }
} </code></pre>
<p>객체지향의 경우 데이터를 사용하는 로직이 동일한 클래스 안에 숨겨져 있다.</p>
<p>따라서 <code>Promotion</code> 클래스의 로직만 변경하면 된다.</p>
<h3 id="데이터-변경에서의-객체지향-절차지향-정리"><strong>데이터 변경에서의 객체지향, 절차지향 정리</strong></h3>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/362b29f2-9651-4d48-9b1c-a683f5762de3/image.png" alt=""></p>
<p><strong>절차지향</strong>: 데이터를 바꾸면 데이터를 사용하는 모든 프로세스들도 같이 바뀐다. ⇒ 결합도가 높다</p>
<p><strong>객체지향</strong>: 캡슐화 덕분에 데이터의 요구사항이 변경이 되면 한 클래스만 바꾸면 된다. ⇒ 결합도가 낮다</p>
<p><strong>데이터가 바뀔 때 절차적인 설계보다는 객체지향적인 설계가 더 좋다!</strong></p>
<h2 id="두-번째-요구사항-변경---새로운-할인-조건-추가">두 번째 요구사항 변경 - 새로운 할인 조건 추가</h2>
<p>장바구니 <strong>총수량(quantity)</strong>을 기준으로 할인 여부를 판단하는 <strong>새로운 타입을 추가</strong>한다면?</p>
<p><strong>기존 조건</strong></p>
<p>Cart 전체 금액 ≥ Promotion 기준 금액</p>
<p><strong>변경된 조건</strong></p>
<p>Cart 전체 금액 ≥ Promotion 기준 금액</p>
<p>Cart 총수량 ≥ Promotion 총수량</p>
<h3 id="절차적인-설계---새로운-할인-조건-추가">절차적인 설계 - 새로운 할인 조건 추가</h3>
<p><strong>코드 리팩토링</strong></p>
<p><strong>Promotion</strong></p>
<pre><code class="language-java">public class Promotion {
    // 추가
    public enum ConditionType {
        PRICE, QUANTITY
    }

    private ConditionType conditionType; // 추가

    private Long cartId;
    private Long basePrice;
    private int baseQuantity; // 추가

    public Long getBasePrice() {
        return basePrice;
    }

    // 추가
    public ConditionType getConditionType() {
        return conditionType;
    }

    // 추가
    public int getBaseQuantity() {
        return baseQuantity;
    }

    public void setCart(Long cartId) {
        this.cartId = cartId;
    }
}</code></pre>
<p><strong>PromotionProcess</strong></p>
<pre><code class="language-java">public class PromotionProcess {

    public void apply(Promotion promotion, Cart cart) {
        if (isApplicableTo(promotion, cart)) {
            promotion.setCart(cart.getId());
        }
    }

    private boolean isApplicableTo(Promotion promotion, Cart cart) {
        // 변경
        switch (promotion.getConditionType()) {
            case PRICE:
                return cart.getTotalPrice() &gt;= promotion.getBasePrice();
            case QUANTITY:
                return cart.getTotalQuantity() &gt;= promotion.getBaseQuantity();
        }
        return false;
    }
}</code></pre>
<p>절차적인 방식은 데이터에 타입이 추가 되더라도 데이터를 구현하고 있는 Promotion 클래스와 이 데이터를 사용해서 로직을 처리하는 프로모션 프로세스가 무조건 같이 변경된다.</p>
<h3 id="객체지향-설계---새로운-할인-조건-추가">객체지향 설계 - 새로운 할인 조건 추가</h3>
<p>객체지향 설계에서는 새로운 타입, 즉 동일한 일을 하지만 구현 방법이 다른 여러가지 타입을 구현하기 위해서는 다형성을 이용</p>
<p>가격을 통해 할인 여부를 판단하는 조건과 수량을 통해 할인 여부를 판단하는 조건이 프로모션 클래스 입장에서는 동일</p>
<p>둘다 할인이 가능해? 라는 판단</p>
<p>인터페이스 추가</p>
<p><strong>DiscountCondition - 할인 가능 여부 추상화</strong></p>
<pre><code class="language-java">public interface DiscountCondition {
    boolean isApplicableTo(Cart cart);
}</code></pre>
<p><strong>Promotion</strong></p>
<pre><code class="language-java">public class Promotion {

    private Cart cart;
    private DiscountCondition condition; // 추가

    public void apply(Cart cart) {
        // 변경
        if (condition.isApplicableTo(cart)) {
            this.cart = cart;
        }
    }
}</code></pre>
<p><strong>PriceCondition</strong></p>
<pre><code class="language-java">public class PriceCondition implements DiscountCondition {

    private Long basePrice;

    @Override
    public boolean isApplicableTo(Cart cart) {
        return cart.getTotalPrice() &gt;= basePrice;
    }
}</code></pre>
<p>기존 코드에서는 <code>Promotion</code> 클래스 안에 <code>basePrice</code>  속성이 존재하였지만 수정된 코드에서는 <code>basePrice</code>가 다른 클래스로 이동했다.</p>
<p>객체지향적인 설계에서는 어떤 로직을 처리하려고 했을 때 그 로직에 사용되는 데이터들이 적합한 클래스로 분배가되고 결과적으로 분산이 된다.</p>
<p><strong>QuantityCondition</strong></p>
<pre><code class="language-java">public class QuantityCondition implements DiscountCondition {

    private int baseQuantity;

    @Override
    public boolean isApplicableTo(Cart cart) {
        return cart.getTotalQuantity() &gt;= baseQuantity;
    }
}</code></pre>
<h3 id="새로운-할인-조건-추가에서의-객체지향-절차지향-정리"><strong>새로운 할인 조건 추가에서의 객체지향, 절차지향 정리</strong></h3>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/7ab79b24-693c-418a-b16e-96607f52a5f2/image.png" alt=""></p>
<p>절차지향 설계에 비해 객체지향 설계는 굉장히 큰 리팩토링이 필요하다는 것을 알 수 있다.</p>
<p>이러한 경우에는 절차지향 설계가 더욱 편리하다고 생각할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/d139679f-c65f-4969-b3c9-1f4e58baccc1/image.png" alt=""></p>
<p>하지만 객체지향 설계에서는 만약 이 이후에 가격, 수량 이 외에 할인여부를 체크하는 로직이 추가된다고 해도 기존의 코드를 수정할 필요없이 새로운 클래스만 추가해주면 된다는 장점이 있다. 반면에 절차적인 설계는 내부의 코드를 무조건 수정해주어야 한다.</p>
<p>객체지향 설계는 할인 조건을 계속해서 확장하고 싶다면 <code>DiscountCondition</code>이라고 하는 인터페이스를 구현하도록 강제한다. 설계가 발전하고 진화할수록 동일한 구조로 계속 일관된 구조를 가지며 코드의 사이즈가 커질 수 있다는 점은 객체지향 설계의 가장 큰 장점이다.</p>
<p>반면에 절차지향적인 설계는 일관된 구조를 강제하기 어렵다.</p>
<p>쉽게 말해 객체지향적인 설계는 일관성 있는 설계가 가능하여 코드의 사이즈가 커지더라도 코드의 복잡성을 줄일 수 있다.</p>
<blockquote>
<p>그렇다면 객체지향적인 설계가 무조건 절차지향적인 설계보다 좋을까?</p>
</blockquote>
<h2 id="세-번째-요구사항-변경---새로운-기능-추가">세 번째 요구사항 변경 - 새로운 기능 추가</h2>
<blockquote>
<p><strong>장바구니 항목</strong>을 이용해서 할인 여부를 판단하는 기능 추가
장바구니에 있는 <strong>특정한 상품 하나</strong>가 그 <strong>조건</strong>을 만족하면 할인을 적용할 수 있는 로직 추가</p>
</blockquote>
<p><strong>CartLineItem 금액 ≥ Promotion 기준 금액</strong></p>
<p>*CartLineItem = 장바구니에 있는 특정한 상품 하나</p>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/edda9860-3ad4-4045-aabd-c65b81ba5202/image.png" alt=""></p>
<p>왼쪽의 <strong>금액만 가지고 할인 여부를 판단했던 구조</strong>와 오른쪽의 <strong>금액과 수량을 가지고 할인여부를 판단하기 위해 인터페이스를 추가해서 코드를 리팩토링 했을 때</strong>의 변경의 영향이 다르다.</p>
<p>이 두가지를 가지고 비교해보자.</p>
<h3 id="절차적인-설계---새로운-기능-추가-금액만-가지고-할인-여부를-판단했던-구조">절차적인 설계 - 새로운 기능 추가 (금액만 가지고 할인 여부를 판단했던 구조)</h3>
<p><strong>Promotion</strong></p>
<pre><code class="language-java">public class Promotion {

    private Long cartId;
    private Long basePrice;

    public Long getBasePrice() {
        return basePrice;
    }

    public void setCart(Long cartId) {
        this.cartId = cartId;
    }
}</code></pre>
<p><strong>PromotionProcess</strong></p>
<pre><code class="language-java">public class PromotionProcess {

    public void apply(Promotion promotion, Cart cart) {
        if (isApplicableTo(promotion, cart)) {
            promotion.setCart(cart.getId());
        }
    }

    private boolean isApplicableTo(Promotion promotion, Cart cart) {
        return cart.getTotalPrice() &gt;= promotion.getBasePrice();
    }

    // 추가
    public boolean isApplicableTo(Promotion promotion, CartLineItem item) {
        return item.getPrice() &gt;= promotion.getBasePrice();
    }
}</code></pre>
<p>단순히 장바구니 항목을 가지고 할인여부를 판단하는 메서드를 추가하면 된다.</p>
<p>절차지향 설계에서는 <code>PromotionProcess</code>의 메서드만 수정하면 된다.</p>
<h3 id="객체지향-설계---새로운-기능-추가-금액만-가지고-할인-여부를-판단했던-구조">객체지향 설계 - 새로운 기능 추가 (금액만 가지고 할인 여부를 판단했던 구조)</h3>
<p><strong>Cart</strong></p>
<pre><code class="language-java">public class Cart {

    private List&lt;CartLineItem&gt; items = new ArrayList&lt;&gt;();

    public Long getTotalPrice() {
        return items.stream().mapToLong(CartLineItem::getPrice).sum();
    }

    public int getTotalQuantity() {
        return items.stream().mapToInt(CartLineItem::getQuantity).sum();
    }
}</code></pre>
<p><strong>Promotion</strong></p>
<pre><code class="language-java">public class Promotion {

    private Cart cart;
    private Long basePrice;

    public void apply(Cart cart) {
        if (cart.getTotalPrice() &gt;= basePrice) {
            this.cart = cart;
        }
    }

    // 추가
    public boolean isApplicableTo(CartLineItem item) {
        return item.getPrice() &gt;= basePrice;
    }
}</code></pre>
<p>객체지향의 경우 <code>Promotion</code> 안에 있는 새로운 메서드를 추가하면 된다.</p>
<blockquote>
<p>즉, 절차적인 방식이나 객체지향적인 방식이나 영향도가 똑같다.</p>
</blockquote>
<h3 id="절차적인-설계---새로운-기능-추가-금액-수량을-가지고-할인-여부를-판단했던-구조">절차적인 설계 - 새로운 기능 추가 (금액, 수량을 가지고 할인 여부를 판단했던 구조)</h3>
<p><strong>Promotion</strong></p>
<pre><code class="language-java">public class Promotion {
    public enum ConditionType {
        PRICE, QUANTITY
    }

    private ConditionType conditionType;

    private Long cartId;
    private Long basePrice;
    private int baseQuantity;

    public Long getBasePrice() {
        return basePrice;
    }

    public ConditionType getConditionType() {
        return conditionType;
    }

    public int getBaseQuantity() {
        return baseQuantity;
    }

    public void setCart(Long cartId) {
        this.cartId = cartId;
    }
}</code></pre>
<p><strong>PromotionProcess</strong></p>
<pre><code class="language-java">public class PromotionProcess {

    public void apply(Promotion promotion, Cart cart) {

        if (isApplicableTo(promotion, cart)) {
            promotion.setCart(cart.getId());
        }
    }

    private boolean isApplicableTo(Promotion promotion, Cart cart) {
        switch (promotion.getConditionType()) {
            case PRICE:
                return cart.getTotalPrice() &gt;= promotion.getBasePrice();
            case QUANTITY:
                return cart.getTotalQuantity() &gt;= promotion.getBaseQuantity();
        }
        return false;
    }

    // 추가
    public boolean isApplicableTo(Promotion promotion, CartLineItem item) {
        switch (promotion.getConditionType()) {
            case PRICE:
                return item.getPrice() &gt;= promotion.getBasePrice();
            case QUANTITY:
                return item.getQuantity() &gt;= promotion.getBaseQuantity();
        }
        return false;
    }
}</code></pre>
<p>금액, 수량을 가지고 할인 여부를 판단했던 구조에서 장바구니에 있는 <strong>특정한 상품 하나</strong>가 그 <strong>조건</strong>을 만족하면 할인을 적용할 수 있는 로직 추가할 경우에도 동일하게 <strong>메서드 하나만 추가</strong>하면 된다.</p>
<p>즉, 절차적인 설계에서는 새로운 기능을 추가할 때 단순히 메서드 하나만 추가하면 된다.</p>
<p>대신, 여기에 새로운 할인 타입이 추가되면 설계가 굉장히 어려워지게 된다. 왜냐하면 여러군데에 있는 케이스문을 모두 수정해주어야 하기 때문이다.</p>
<p>하지만 새로운 할인 타입이 추가되지않고 단순히 기능만 추가한다고 하면 위의 절차적인 설계는 충분히 좋은 설계라고 말할 수 있다.</p>
<h3 id="객체지향적인-설계---새로운-기능-추가-금액-수량을-가지고-할인-여부를-판단했던-구조">객체지향적인 설계 - 새로운 기능 추가 (금액, 수량을 가지고 할인 여부를 판단했던 구조)</h3>
<p><strong>DiscountCondition</strong></p>
<pre><code class="language-java">public interface DiscountCondition {

    boolean isApplicableTo(Cart cart);
    boolean isApplicableTo(CartLineItem item); // 추가
}</code></pre>
<p>인터페이스에 새로운 오퍼레이션 추가</p>
<p><strong>PriceCondition</strong></p>
<pre><code class="language-java">public class PriceCondition implements DiscountCondition {

    private Long basePrice;

    @Override
    public boolean isApplicableTo(Cart cart) {
        return cart.getTotalPrice() &gt;= basePrice;
    }

    // 추가
    @Override
    public boolean isApplicableTo(CartLineItem item) {
        return item.getPrice() &gt;= basePrice;
    }
}</code></pre>
<p><strong>QuantityCondition</strong></p>
<pre><code class="language-java">public class QuantityCondition implements DiscountCondition {

    private int baseQuantity;

    @Override
    public boolean isApplicableTo(Cart cart) {
        return cart.getTotalQuantity() &gt;= baseQuantity;
    }

    // 추가
    @Override
    public boolean isApplicableTo(CartLineItem item) {
        return item.getQuantity() &gt;= baseQuantity;
    }
}</code></pre>
<p>인터페이스의 오퍼레이션을 추가했으므로 그 구현체인 <code>PriceCondition</code>와 <code>QuantityCondition</code>의 코드의 수정이 필요하다.</p>
<p>객체지향적인 설계는 다형성을 사용해서 클래스의 계층 구조를 구현해놨을 때</p>
<p>타입이 확장되는 데는 굉장히 유리하지만,</p>
<p>그 타입에 새로운 오퍼레이션이 추가되면 전체적으로 코드의 수정이 일어난다.</p>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/5334c03c-524f-4a57-a48d-2fdd8585911c/image.png" alt=""></p>
<p><strong>이런 경우에 절차적인 설계가 객체지향적인 설계보다 더 좋다고 판단하면 된다.</strong></p>
<h3 id="정리">정리</h3>
<p>현재 변경하는 것들이 <strong>타입이 확장되는 경우가 더 많은 지</strong> 아니면 <strong>타입은 거의 고정이고 그 타입의 종류를 사용하는 기능이 추가되는 경우가 많은 지</strong>에 따라서 <strong>절차적인 설계</strong>가 좋을 수 있고 <strong>객체지향적인 설계</strong>가 좋을 수 있다.</p>
<h2 id="데이터의-변환">데이터의 변환</h2>
<p>지금까지는 시스템의 상태 변경에 대해서만 이야기를 하였다.</p>
<p>이번에는 데이터를 변환하는 것에 상황에 대해 이야기해보자.</p>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/8ef1ad75-a94b-4996-ad78-56abfe84babf/image.png" alt=""></p>
<p><strong>입력</strong>: <code>Cart</code>, <code>Promotion</code></p>
<p><strong>출력</strong>: <code>CartWithPromotion</code></p>
<p><code>Cart</code>와 <code>Promotion</code>이라고 하는 입력데이터를 잘 조합해서 시스템에 넣어둔 후 두개의 데이터를 가공하여 <code>CartWithPromotion</code>이라는 새로운 데이터를 출력해주는 로직을 새로 구현한다고 가정해보자.</p>
<h3 id="절차적인-설계---데이터-변환">절차적인 설계 - 데이터 변환</h3>
<p><strong>Promotion</strong></p>
<pre><code class="language-java">public class Promotion {
    public enum ConditionType {
        PRICE, QUANTITY
    }

    private ConditionType conditionType;

    private Long cartId;
    private Long basePrice;
    private int baseQuantity;

    // Getter, Setter ...
}</code></pre>
<p><strong>PromotionProcess</strong></p>
<pre><code class="language-java">public class PromotionProcess {

    // 추가
    public CartWithPromotion convertCartWithPromotion(Promotion promotion, 
                                                      Cart cart) {
        CartWithPromotion result = new CartWithPromotion();

        result.setTotalPrice(cart.getTotalPrice());
        result.setTotalQuantity(cart.getTotalQuantity());
        result.setPromotionBasePrice(promotion.getBasePrice());
        result.setPromotionBaseQuantity(promotion.getBaseQuantity());

        return result;
    }
}</code></pre>
<p>절차적인 설계에서 데이터의 변환은 굉장히 쉽다.</p>
<p>왜냐하면 절차적인 설계에서는 이미 데이터가 분리되어 있다. 또한 데이터는 노골적으로 이 데이터를 가져다가 쓰세요 라고 자기의 타입을 제공해준다. 따라서 <code>PromotionProcess</code>에서는 <code>CartWithPromotion</code> 객체를 생성하여 거기에 데이터에 있는 것들을 빼서 넣어주면 된다.</p>
<p>이렇게 되면 굉장히 심플하게 데이터의 변환이 된다.</p>
<h3 id="객체지향-설계---데이터-변환">객체지향 설계 - 데이터 변환</h3>
<p>객체지향의 경우에는 굉장히 어렵다.</p>
<p><strong>Promotion</strong></p>
<pre><code class="language-java">public class Promotion {

    private Cart cart;
    private DiscountCondition condition;

    // 추가
    public CartWithPromotion convertCartWithPromotion() {
        CartWithPromotion result = new CartWithPromotion();

        result.setTotalPrice(cart.getTotalPrice());
        result.setTotalQuantity(cart.getTotalQuantity());

        if (condition instanceof PriceCondition) {
            result.setPromotionBasePrice(
                ((PriceCondition)condition).getBasePrice());
        }

        if (condition instanceof QuantityCondition) {
            result.setPromotionBaseQuantity(
                ((QuantityCondition)condition).getBaseQuantity());
        }

        return result;
    }
}</code></pre>
<p>객체지향은 동일한 할인 여부를 판단하는 여러 개의 타입들이 있다. 따라서 각각의 타입들마다 사용하는 데이터들이 개별적인 클래스로 분배가 된다.</p>
<p><strong>PriceCondition</strong></p>
<pre><code class="language-java">public class PriceCondition implements DiscountCondition {

    private Long basePrice;
}</code></pre>
<p>기준 금액은 <code>PriceCondition</code> 클래스에 들어있다.</p>
<p><strong>QuantityCondition</strong></p>
<pre><code class="language-java">public class QuantityCondition implements DiscountCondition {

    private int baseQuantity;
}</code></pre>
<p>기준 수량은 <code>QuantityCondition</code> 클래스에 들어있다.</p>
<p>객체지향적인 설계에서 데이터를 변환하기 위해서는 여러가지 타입에서 그 타입에 적합한 데이터를 뿌려주어야 한다. 따라서 현재의 객체가 어떤 타입인지를 판단하여 적절한 타입으로 타입 캐스팅한 후 필요한 데이터를 끄집어내는 로직이 필요하다.</p>
<p><strong>절차적인 설계가 유리한 경우</strong></p>
<p>타입이 확장이 되지 않거나 그 기능이 추가가 되고 그 데이터를 처리하는 로직인 경우에는 객체지향적인 설계보다 절차적인 설계가 훨씬 좋다.</p>
<h2 id="절차적인-설계-vs-객체지향-설계">절차적인 설계 vs 객체지향 설계</h2>
<p><strong>절차적인 설계</strong></p>
<ul>
<li>포맷 변경을 위한 데이터 변환</li>
<li><strong>데이터</strong> 중심</li>
<li>데이터 노출</li>
<li>기능 추가에 유리</li>
</ul>
<p><strong>객체지향 설계</strong></p>
<ul>
<li>규칙에 기반한 상태 변경</li>
<li><strong>행동</strong> 중심</li>
<li>데이터 캡슐화</li>
<li>타입 확장에 유리</li>
</ul>
<p>객체지향 설계는 데이터의 응집도 단위로 클래스를 분해한게 아니라 행위의 응집도 단위로 클래스를 분리했기 때문에 데이터 변환이 필요할 때 <code>instanceof</code>가 필요할 수 밖에 없다.</p>
<p>절차적인 설계는 같이 돌아다니는 데이터를 한 군데로 모은다. 따라서 데이터 중심적인 설계에서는 절차지향적인 설계가 좋다. 반면 객체지향적인 설계는 데이터를 캡슐화한다.</p>
<h2 id="레이어-아키텍처에서의-객체지향과-절차지향">레이어 아키텍처에서의 객체지향과 절차지향</h2>
<p>객체지향적인 설계와 절차지향적인 설계를 어디에 적합한지를 판단하여 적절히 섞어 사용해야 한다.</p>
<p><strong>레이어 아키텍처</strong></p>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/e89bdc75-0e03-478a-853d-a0b16d765733/image.png" alt=""></p>
<p>레이어 아키텍처로 예를 들어보자.</p>
<h3 id="도메인-레이어"><strong>도메인 레이어</strong></h3>
<p>규칙 기반의 상태 변경</p>
<p>도메인 레이어는 대부분의 경우 시스템의 상태 변경을 담당한다. 위의 예시의 모든 클래스들은 도메인 로직을 담당한다. 여기에서는 대부분의 경우 특별한 로직을 특별한 규칙을 기반으로 시스템의 상태 변경을 야기하는 로직이 도메인 레이어에 들어가게 된다.</p>
<p>도메인 로직의 가장 큰 목적은 시스템을 A라는 상태에서 B라는 상태로 변경하는 것이다. A라는 상태에서 B라는 상태로 바뀔 땐 대부분 특별한 룰들이 추가되게 된다. 또한 이것들은 비즈니스가 바뀔 때 마다 계속 급격하게 바뀌게 된다.</p>
<p><strong>따라서 도메인 레이어는 객체지향적으로 짜는 것이 유리하다.</strong></p>
<h3 id="프레젠테이션-레이어"><strong>프레젠테이션 레이어</strong></h3>
<p>프리젠테이션 레이어는 <strong>데이터의 변환</strong>을 담당한다.</p>
<p><strong>PromotionController</strong></p>
<pre><code class="language-java">@RestController
public class PromotionController {

    private PromotionService promotionService;

    @PutMapping(&quot;/promotions/{promotionId}/apply/{cartId}&quot;)
    public PromotionResponse apply(@PathVariable(&quot;promotionId&quot;) Long promotionId, 
                                   @PathVariable(&quot;cartId&quot;) Long cartId) {
        Promotion promotion = promotionService.apply(promotionId, cartId);
        return new PromotionResponse(promotion);
    }
}</code></pre>
<p>프리젠테이션 레이어가 하는 일은 대부분 외부의 <strong>불확실한 데이터를 변환</strong>하거나 검증하거나 <strong>외부에 아웃풋을 내보내는 일</strong>이다. 따라서 <strong>프리젠테이션 레이어는 절차적</strong>으로 짜는 것이 유리하다.</p>
<h3 id="서비스-레이어">서비스 레이어</h3>
<p>서비스 레이어는 애플리케이션의 플로우를 처리한다. 말 그대로 절차이다.</p>
<p><strong>PromotionService</strong></p>
<pre><code class="language-java">@Service
public class PromotionService {

    private PromotionRepository promotionRepository;
    private CartRepository cartRepository;

    public Promotion apply(Long promotionId, Long cartId) {

        Promotion promotion = promotionRepository.findById(promotionId);
        Cart cart = cartRepository.findById(cartId);

        promotion.apply(cart);

        return promotion;
    }
}</code></pre>
<p>DB에서 데이터를 읽고 그 객체가 어떤 일을 하라고 요청을 보내고 그 결과를 통해 response를 만든다. 어떤 특정한 알고리즘에 따라 어떤 일을 해야 됩니다 라고 하는 룰을 순서대로 정의한다.</p>
<p>따라서 <strong>서비스 레이어도 절차적</strong>으로 짜고 있다.</p>
<h3 id="퍼시스턴스-레이어">퍼시스턴스 레이어</h3>
<p>퍼시스턴스 레이어도 마찬가지로 절차적으로 짜고 있다.</p>
<p><strong>PromotionRepository</strong></p>
<pre><code class="language-java">@Repository
public class PromotionRepository {

    private JdbcClient jdbcClient;

    public Promotion findById(Long promotionId) {
        jdbcClient.sql(
            &quot;select from promotion where id = :promotionId&quot;)
            .param(&quot;promotionId&quot;, promotionId)
            .query((rs, rowNum) -&gt; new Promotion())
            .single();
    }
}</code></pre>
<p>퍼시스턴스 레이어는 DB에 있는 데이터를 읽어서 객체로 변환하거나 데이터베이스에 저장하거나 업데이트 해야할 객체를 데이터에 SQL이나 또는 특정한 형태로 변환하는게 퍼시스턴스 레이어의 역할이다.</p>
<p>따라서 우리는 <strong>퍼시스턴스 레이어도 절차적</strong>으로 코드를 작성하고 있다.</p>
<h2 id="결론">결론</h2>
<p>즉, 이미 우리는 객체지향적인 설계와 절차적인 설계를 혼합하여 사용하고 있다. </p>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/89a09a46-6590-4858-afd4-4ded26c9feac/image.png" alt=""></p>
<p>만약 데이터를 조회하는 로직이 있다고 하면 여기는 객체지향적인 설계가 낄 여지가 전혀 없다. DB에서 데이터를 읽어와서 그냥 화면에 보내는 로직은 어떠한 룰이 전혀 필요없기 때문이다. 레이어를 오고가는 데이터는 모든 레이어가 다 공유한다.</p>
<p>다시말해, 실제로 애플리케이션을 짤 때 객체지향적으로 짜는 비율보다 절차지향적으로 짜는 비율이 훨씬 많다.</p>
<p><strong>객체지향은 여전히 유용한가요?</strong></p>
<p>라는 질문 보다는</p>
<p><strong>객체지향은 언제 유용한가요?</strong></p>
<p>라는 질문이 더욱 올바른 질문이다.</p>
<p>만약 객체지향이 유용하지 않다고 생각하는 느낀다면, 현재 작업하는 영역 또는 기능이 데이터를 처리하는 것이거나 룰이 급격하게 변하지 않는 경우일 가능성이 높다.</p>
<p>시간이 지나서 그 영역이 굉장히 복잡해지고 룰들이 추가되어야 한다면 그때는 객체지향을 적용하는 것이 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스에서 PK 값을 관리할 때 JPA의 동작 방식]]></title>
            <link>https://velog.io/@sangcheol_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4%EC%97%90%EC%84%9C-PK-%EA%B0%92%EC%9D%84-%EA%B4%80%EB%A6%AC%ED%95%A0-%EB%95%8C-JPA%EC%9D%98-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@sangcheol_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4%EC%97%90%EC%84%9C-PK-%EA%B0%92%EC%9D%84-%EA%B4%80%EB%A6%AC%ED%95%A0-%EB%95%8C-JPA%EC%9D%98-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Sun, 21 Apr 2024 11:15:57 GMT</pubDate>
            <description><![CDATA[<h2 id="jpa의-기본-동작-방식">JPA의 기본 동작 방식</h2>
<p><img src="https://velog.velcdn.com/images/sangcheol_/post/2d73e7b8-6e01-4c9d-9866-31ad4addf08b/image.png" alt="JPA 쓰기 지연 SQL 저장소"></p>
<p><strong>JPA</strong>(Java Persistence API)를 사용하여 Entity를 저장할 때, JPA에서는 <code>persist()</code> 메소드를 사용합니다. <code>persist()</code> 메소드는 Entity를 영구 저장소에 저장하는 작업을 의미합니다. 이 과정에서 INSERT 쿼리가 실행됩니다. 여기서 주목할 점은 <code>persist()</code> 메소드가 호출되었을 때, <strong>즉시 INSERT 쿼리가 실행되는 것이 아니라는 것</strong>입니다.</p>
<p>대신, JPA는 Entity를 영속화(persist)하고, 해당 Entity의 변경사항을 추적합니다. 이 변경사항은 일시적으로 쓰기 지연 SQL 저장소에 보관됩니다. 이 저장소는 변경사항들이 즉시 데이터베이스에 반영되지 않고, 트랜잭션이 커밋될 때까지 기다리는 곳입니다.</p>
<p>트랜잭션이 커밋될 때, JPA는 쓰기 지연 SQL 저장소에 저장된 INSERT 쿼리를 실행하여 데이터베이스에 영구적인 변경을 적용합니다. 이를 통해 트랜잭션 단위로 모든 변경사항이 원자적으로 처리되고, 데이터 일관성을 보장할 수 있습니다. 따라서 <code>persist()</code> 메소드는 단순히 INSERT 쿼리를 실행하는 것이 아니라, 영속화되는 Entity의 변경사항을 추적하여 트랜잭션이 커밋될 때 실행될 수 있도록 준비합니다.</p>
<p>이 방식은 성능을 향상시키고, 데이터베이스 작업을 최적화할 수 있도록 해줍니다.</p>
<h2 id="auto-increment와-jpa의-동작-방식">Auto increment와 JPA의 동작 방식</h2>
<p>auto increment를 사용하게 되면 데이터베이스에서 기본 키 값을 관리하게 됩니다. 그리고 JPA는 트랜잭션 커밋 시점에 쓰기 지연 SQL 저장소에 저장된 쿼리를 실행하여 데이터베이스에 변경사항을 반영합니다.</p>
<p>그러나 한 트랜잭션 안에서 <code>em.persist(member)</code>를 한 이후에 <code>em.find(Member.class, member.getId())</code>를 호출하면, 데이터베이스에는 아직 해당 데이터가 존재하지 않기 때문에 데이터를 조회할 수 없는 문제가 발생하게 됩니다.</p>
<p>JPA는 이런 문제를 해결하기 위해 해당 Entity가 auto increment를 사용한 경우, <code>em.persist()</code>를 호출하는 시점에 즉시 데이터베이스에 쿼리를 실행하여 데이터를 저장합니다. 따라서 데이터가 즉시 데이터베이스에 저장되므로 <code>em.find()</code>를 호출해도 데이터를 조회할 수 있게 됩니다. 이렇게 함으로써 즉각적인 데이터 접근을 가능하게 합니다.</p>
<p><code>member_id</code> 필드가 auto increment를 사용하는 방식으로 생성되는 예시를 살펴보겠습니다.</p>
<h2 id="테스트-코드">테스트 코드</h2>
<p>위에서 설명한 내용을 테스트 코드로 확인해보겠습니다.
auto increment를 사용하지 않는 경우와 사용하는 경우의 차이점을 확인할 수 있습니다.</p>
<h3 id="auto-increment를-사용하지-않는-경우">auto increment를 사용하지 않는 경우</h3>
<p><strong>Member Entity</strong></p>
<pre><code class="language-java">import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
public class Member {

    @Id
    @Column(name = &quot;member_id&quot;)
    private Long id;

    @Column(name = &quot;username&quot;)
    private String username;
}</code></pre>
<p><strong>테스트 코드</strong></p>
<pre><code class="language-java">import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.beans.factory.annotation.Autowired;

import jakarta.persistence.EntityManager;

import org.junit.jupiter.api.Test;

@Transactional
@SpringBootTest
class JpaAutoIncrementTest {

    @Autowired
    private EntityManager em;

    @Test
    void test() {
        Member member = new Member();
        member.setId(1L);
        member.setUsername(&quot;hello&quot;);

        System.out.println(&quot;========비영속=======&quot;);

        em.persist(member);

        System.out.println(&quot;========영속=======&quot;);

        tx.commit();
        System.out.println(&quot;========커밋 후========&quot;);
    }
}</code></pre>
<p>위는 auto increment를 사용하지 않는 경우의 테스트 코드입니다.</p>
<p><strong>테스트 결과</strong></p>
<pre><code class="language-SQL">========비영속=======
========영속=======
Hibernate: 
    /* insert for
        hellojpa.Member */insert 
    into
        Member (username, member_id) 
    values
        (?, ?)
========커밋 후========</code></pre>
<p><code>persist()</code> 메소드를 호출한 시점에 INSERT 쿼리가 실행되지 않고, 트랜잭션이 커밋될 때 실행됨을 알 수 있습니다.</p>
<h3 id="auto-increment를-사용하는-경우">auto increment를 사용하는 경우</h3>
<p><strong>Member Entity</strong></p>
<pre><code class="language-java">import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;member_id&quot;)
    private Long id;

    @Column(name = &quot;username&quot;)
    private String username;
}</code></pre>
<p>auto increment를 사용하는 경우, <code>@GeneratedValue(strategy = GenerationType.IDENTITY)</code>를 사용하여 데이터베이스에서 기본 키 값을 관리하도록 설정합니다.</p>
<p><strong>테스트 코드</strong></p>
<pre><code class="language-java">import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.beans.factory.annotation.Autowired;

import jakarta.persistence.EntityManager;

import org.junit.jupiter.api.Test;

@Transactional
@SpringBootTest
class JpaAutoIncrementTest {

    @Autowired
    private EntityManager em;

    @Test
    void test() {
        Member member = new Member();
        // member.setId(1L);
        member.setUsername(&quot;hello&quot;);

        System.out.println(&quot;========비영속=======&quot;);

        em.persist(member);
        System.out.println(&quot;member.getId() = &quot; + member.getId());

        System.out.println(&quot;========영속=======&quot;);

        tx.commit();
        System.out.println(&quot;========커밋 후========&quot;);
    }
}</code></pre>
<p>위는 auto increment를 사용하는 경우의 테스트 코드입니다.</p>
<p><strong>테스트 결과</strong></p>
<pre><code class="language-SQL">========비영속=======
Hibernate: 
    /* insert for
        hellojpa.Member */insert 
    into
        Member (username, member_id) 
    values
        (?, default)
member.getId() = 1
========영속=======
========커밋 후========</code></pre>
<p>auto increment를 사용한 경우, <code>persist()</code> 메소드를 호출한 시점에 즉시 데이터베이스에 INSERT 쿼리가 실행되어 데이터가 저장됨을 알 수 있습니다.</p>
<p>또한, member에 id값을 <code>member.setId()</code> 해준 적이 없는데, <code>member.getId()</code>를 호출하면 1이라는 값이 출력됨을 알 수 있습니다. 이는 데이터베이스에서 자동으로 생성된 기본 키 값을 JPA가 가져와서 member 객체에 저장했기 때문입니다.</p>
<h2 id="정리">정리</h2>
<ul>
<li>데이터베이스에서 PK 값을 관리하지 사용하지 않는 경우, <code>persist()</code> 메소드를 호출한 시점에 즉시 데이터베이스에 INSERT 쿼리가 실행되지 않고, 쿼리는 쓰기 지연 SQL 저장소에 저장되었다가 트랜잭션이 커밋될 때 실행됩니다.</li>
<li>데이터베이스에서 PK 값을 관리하는 경우, <code>persist()</code> 메소드를 호출한 시점에 즉시 데이터베이스에 INSERT 쿼리가 실행됩니다.</li>
</ul>
<h2 id="참고문헌">참고문헌</h2>
<ul>
<li><a href="https://product.kyobobook.co.kr/detail/S000000935744">자바 ORM 표준 JPA 프로그래밍</a>, 김영한 저 </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[싱글톤 패턴과 무상태 (stateless) 설계]]></title>
            <link>https://velog.io/@sangcheol_/%EC%8B%B1%EA%B8%80%ED%86%A4-%ED%8C%A8%ED%84%B4%EA%B3%BC-stateless-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@sangcheol_/%EC%8B%B1%EA%B8%80%ED%86%A4-%ED%8C%A8%ED%84%B4%EA%B3%BC-stateless-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Sun, 02 Apr 2023 10:34:56 GMT</pubDate>
            <description><![CDATA[<h2 id="싱글톤-패턴">싱글톤 패턴</h2>
<p><strong>싱글톤 패턴</strong>은 전체 응용프로그램에서 클래스의 인스턴스를 하나만 만들고 그 인스턴스를 모두가 공유해서 접근할 수 있도록 소프트웨어 개발에 널리 사용되는 디자인 패턴이다. </p>
<p>싱글톤 패턴에서는 클래스의 단일 인스턴스가 생성되고 해당 클래스에 대한 모든 후속 요청은 동일한 인스턴스를 반환한다.</p>
<h2 id="상태stateful-vs-무상태stateless">상태(Stateful) vs 무상태(Stateless)</h2>
<h3 id="상태stateful">상태(Stateful)</h3>
<p>싱글톤을 설계할 때 이를 구현하는 방법은 크게 Stateful한 설계 방식과 Stateless 설계 방식으로 나눌 수 있다. stateful 싱글톤은 변경 가능한 상태를 가진 싱글톤이고, stateless 싱글톤은 변경 가능한 상태가 없는 싱글톤을 말한다.</p>
<p>Java의 예제 코드를 통해 stateful 설계에 대해 자세히 알아보자.</p>
<pre><code class="language-java">public class MyStatefulSingleton {
    private static MyStatefulSingleton instance;
    private int count; // 상태 유지 필드

    private MyStatefulSingleton() {}

    public static MyStatefulSingleton getInstance() {
        if (instance == null) {
            instance = new MyStatefulSingleton();
        }
        return instance;
    }

    public void incrementCount() {
        count++;  // 문제가 될 수 있는 부분
    }

    public int getCount() {
        return count;
    }
}</code></pre>
<p>위의 예제에서 <code>MyStatefulSingleton</code>은 변경 가능한 <code>count</code> 필드가 있는 상태 저장 싱글톤이다. <code>incrementCount()</code> 메서드가 호출될 때마다 <code>count</code> 값이 1씩 증가하게 된다.</p>
<h3 id="상태stateful-설계의-문제점">상태(Stateful) 설계의 문제점</h3>
<p>간단한 테스트 코드를 통해 상태 설계의 문제를 알아보자.</p>
<pre><code class="language-java">import org.assertj.core.api.Assertions;
import org.junit.Test;

public class SingletonTest {

    @Test
    public void testStatefulSingleton() throws InterruptedException {
        MyStatefulSingleton s1 = MyStatefulSingleton.getInstance();
        MyStatefulSingleton s2 = MyStatefulSingleton.getInstance();

        Thread t1 = new Thread(() -&gt; {
            for (int i = 0; i &lt; 1000000; i++) {
                s1.incrementCount();
            }
        });
        Thread t2 = new Thread(() -&gt; {
            for (int i = 0; i &lt; 1000000; i++) {
                s2.incrementCount();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        int count = s1.getCount();
        System.out.println(&quot;Count: &quot; + count);
    }
}</code></pre>
<p>두 개의 스레드를 만들고 <code>count</code>를 1000000번 증가시키는 루프를 만들어 스레드를 실행시키면, <code>count</code> 값이 2000000이 될꺼라는 예측과 다른 결과가 나온다는 것을 알 수 있다.</p>
<p><code>MyStatefulSingleton</code>과 같은 상태 저장 싱글톤을 갖는 것은 보기에는 편리해 보일 수 있지만 다중 스레드 환경에서 아주 커다란 문제를 일으킬 수 있다. 여러 스레드가 <code>MyStatefulSingleton</code>의 동일한 인스턴스에 동시에 접근하게 되면 예측할 수 없는 동작으로 이어지는 심각한 문제가 발생할 수 있다.</p>
<h3 id="무상태stateless">무상태(Stateless)</h3>
<p>이러한 문제를 방지하기 위해서는 Stateless 싱글톤 방식으로 설계하는 것이 좋다.
다음의 Java 예제를 살펴보자.</p>
<pre><code class="language-java">public class MyStatelessSingleton {
    private static final MyStatelessSingleton INSTANCE = new MyStatelessSingleton();

    private MyStatelessSingleton() {}

    public static MyStatelessSingleton getInstance() {
        return INSTANCE;
    }
    ...
}</code></pre>
<p>위의 예제 코드에서 <code>MyStatelessSingleton</code>은 변경 가능한 상태가 없는 무상태 싱글톤이다. 변경 가능한 상태가 없기 때문에 동시성 문제에 대한 걱정이 없이 여러 스레드에서 안전하게 엑세스할 수 있다.</p>
<h2 id="결론">결론</h2>
<p>싱글톤을 stateless 방식으로 설계하는 것은 동시성 문제를 피하고 코드를 더 예측 가능하고 유지 관리할 수 있도록 만드는 데 도움이 된다. stateful 싱글톤 방식이 편리해 보일 수 있지만 다중 스레드 환경에서 찾아내기 힘든 아주 큰 문제를 일으킬 수 있다.</p>
<p>java spring 프레임워크에서 스프링 컨테이너는 기본적으로 싱글톤 컨테이너이다. 스프링 빈은 항상 무상태(stateless)로 설계 해야함을 기억하자.</p>
]]></description>
        </item>
    </channel>
</rss>