<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>solst_ice</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 30 Mar 2026 14:59:17 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>solst_ice</title>
            <url>https://velog.velcdn.com/images/solst_ice/profile/2c807353-bf3c-4f3a-8e0e-6b225a3e4ddd/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. solst_ice. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/solst_ice" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Qwen3-ASR 백그라운드 전사 — MLX Metal 의존성 제거]]></title>
            <link>https://velog.io/@solst_ice/Qwen3-ASR-%EB%B0%B1%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9C-%EC%A0%84%EC%82%AC-MLX-Metal-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A0%9C%EA%B1%B0</link>
            <guid>https://velog.io/@solst_ice/Qwen3-ASR-%EB%B0%B1%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9C-%EC%A0%84%EC%82%AC-MLX-Metal-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A0%9C%EA%B1%B0</guid>
            <pubDate>Mon, 30 Mar 2026 14:59:17 GMT</pubDate>
            <description><![CDATA[<h2 id="상황">상황</h2>
<p>다이나믹 아일랜드에서 녹음 정지 시 백그라운드 전사가 실행되는데, Qwen3-ASR 엔진에서 Metal GPU 크래시가 발생했다. ANE(Neural Engine)로 전환하면 해결될 줄 알았는데, 세 단계에 걸친 문제가 연쇄적으로 나타났다.</p>
<h2 id="에러-내용">에러 내용</h2>
<pre><code>IOGPUMetalError: Insufficient Permission (to submit GPU work from background)
(00000006:kIOGPUCommandBufferCallbackErrorBackgroundExecutionNotPermitted)

[METAL] Command buffer execution failed: Insufficient Permission
(to submit GPU work from background)</code></pre><h2 id="원인-분석">원인 분석</h2>
<h3 id="1차-시도-coremlasrmodel--cpuandneuralengine">1차 시도: CoreMLASRModel + <code>.cpuAndNeuralEngine</code></h3>
<p>qwen3-asr-swift 라이브러리의 <code>CoreMLASRModel</code>을 사용하고 <code>computeUnits: .cpuAndNeuralEngine</code>으로 설정. 하지만 <strong>여전히 크래시</strong>. 원인: <code>CoreMLASRModel</code>이 <code>import MLX</code>를 하고 내부에서 <code>MLXArray</code>로 데이터를 주고받음.</p>
<pre><code class="language-swift">// CoreMLASRModel.swift (라이브러리)
import MLX  // ← 이것이 문제

// CoreMLEncoder.encode() 내부
let melData: [Float] = melFeatures.asArray(Float.self)  // ← Metal 트리거</code></pre>
<p>MLX의 <code>default_device()</code>가 iOS에서 항상 GPU를 반환하므로, <code>.asArray()</code> → <code>eval()</code> → <code>gpu::eval()</code> → Metal command buffer 제출.</p>
<h3 id="2차-시도-ane-컴파일-실패">2차 시도: ANE 컴파일 실패</h3>
<p>iPhone 14 Pro (A16)에서 디코더의 ANE 컴파일 실패:</p>
<pre><code>MILCompilerForANE error: failed to compile ANE model using ANEF.
Failure translating MIL-&gt;EIR network: std::bad_cast</code></pre><p>원인: <code>CoreMLTextDecoder</code>가 <code>MLState</code>(KV 캐시)를 사용하는데, A16 이하 ANE에서 이 연산을 지원하지 않음. ANE 우선 → CPU 폴백 전략으로 해결.</p>
<h3 id="3차-전사-결과-빈-문자열">3차: 전사 결과 빈 문자열</h3>
<p>mel spectrogram 추출 시 <code>vDSP_mtrans</code> 파라미터가 원본과 달랐음:</p>
<pre><code class="language-diff"> // 원본 (WhisperFeatureExtractor.extractFeatures)
 vDSP_mtrans(filterbank, 1, &amp;filterbankT, 1, vDSP_Length(nBins), vDSP_Length(nMels))

 // 잘못된 코드
-vDSP_mtrans(melFilterbank, 1, &amp;filterbankT, 1, vDSP_Length(nMels), vDSP_Length(nBins))
+vDSP_mtrans(melFilterbank, 1, &amp;filterbankT, 1, vDSP_Length(nBins), vDSP_Length(nMels))</code></pre>
<h2 id="해결-과정">해결 과정</h2>
<h3 id="최종-해결-gpu-free-독립-래퍼-구현">최종 해결: GPU-free 독립 래퍼 구현</h3>
<p><code>Qwen3CoreMLInference</code> 클래스를 Writ 프로젝트 내에 직접 구현. 라이브러리의 <code>CoreMLASRModel</code>을 사용하지 않고, MLX를 완전히 배제:</p>
<pre><code class="language-swift">// Qwen3CoreMLInference.swift — import MLX 없음
import CoreML
import Foundation
import Accelerate
import Qwen3ASR    // CoreMLTextDecoder, Qwen3Tokenizer 등 재사용
import AudioCommon // AudioSampleLoader 재사용</code></pre>
<p>구성:</p>
<ul>
<li><strong>Mel 추출</strong>: Accelerate(CPU) 기반 직접 구현 — <code>vDSP_fft</code>, <code>vDSP_mmul</code>, <code>vvlog10f</code></li>
<li><strong>인코더</strong>: <code>MLModel</code> 직접 로드 + <code>prediction()</code> 호출 (라이브러리 CoreMLASREncoder의 model이 private이라)</li>
<li><strong>디코더</strong>: 라이브러리 <code>CoreMLTextDecoder</code>의 public API(<code>embed/decoderStep/argmax</code>) 재사용</li>
<li><strong>오디오 임베딩 슬라이스</strong>: <code>MLMultiArray</code> 포인터 직접 복사</li>
</ul>
<p>추가 주의점:</p>
<ul>
<li><strong>Hann 윈도우</strong>: <code>vDSP_hann_window(NORM)</code>은 대칭 윈도우 → 원본은 주기적 윈도우 사용</li>
<li><strong>Mel filterbank</strong>: HTK 공식이 아닌 Slaney piecewise 공식 사용 필요</li>
</ul>
<h2 id="배운-점">배운 점</h2>
<p>MLX 프레임워크는 <code>MLXArray</code> 생성만으로는 Metal을 호출하지 않지만(lazy evaluation), <code>.asArray()</code> 호출 시 <code>eval()</code> → <code>gpu::eval()</code> 경로를 타면서 Metal command buffer를 제출한다. CoreML 모델이 ANE에서 동작하더라도, 데이터 브릿지에 MLX를 사용하면 백그라운드에서 크래시한다. GPU-free를 보장하려면 <code>import MLX</code> 자체를 제거해야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[mlx-swift fmt consteval 에러 — Xcode 26 Apple Clang 21 호환성]]></title>
            <link>https://velog.io/@solst_ice/mlx-swift-fmt-consteval-%EC%97%90%EB%9F%AC-Xcode-26-Apple-Clang-21-%ED%98%B8%ED%99%98%EC%84%B1</link>
            <guid>https://velog.io/@solst_ice/mlx-swift-fmt-consteval-%EC%97%90%EB%9F%AC-Xcode-26-Apple-Clang-21-%ED%98%B8%ED%99%98%EC%84%B1</guid>
            <pubDate>Mon, 30 Mar 2026 14:53:15 GMT</pubDate>
            <description><![CDATA[<h2 id="상황">상황</h2>
<p>Writ 프로젝트를 Xcode 26.3에서 빌드하던 중, 직접 작성한 코드가 아닌 전이 의존성(WhisperKit → mlx-swift → fmt)에서 컴파일 에러가 발생했다. 프로젝트 코드는 전혀 변경하지 않았는데 갑자기 빌드가 깨진 상황.</p>
<h2 id="에러-내용">에러 내용</h2>
<pre><code>mlx-swift/Source/Cmlx/fmt/include/fmt/format-inl.h:61:24
Call to consteval function &#39;fmt::basic_format_string&lt;char, ...&gt;::basic_format_string&lt;FMT_COMPILE_STRING, 0&gt;&#39;
is not a constant expression

mlx-swift/Source/Cmlx/fmt/include/fmt/format-inl.h:62:22
Call to consteval function &#39;fmt::basic_format_string&lt;char, const char (&amp;)[7], int &amp;&gt;::basic_format_string&lt;FMT_COMPILE_STRING, 0&gt;&#39;
is not a constant expression</code></pre><p>총 5개의 유사한 에러가 <code>format-inl.h</code>에서 발생.</p>
<h2 id="원인-분석">원인 분석</h2>
<p>의존성 체인: <code>Writ → WhisperKit v0.17.0 → mlx-swift v0.31.1 → Cmlx → fmt 10.2.1</code></p>
<p>mlx-swift의 <code>Package.swift</code>에서 C++ 언어 표준을 <code>gnucxx20</code>(C++20)으로 설정:</p>
<pre><code class="language-swift">// mlx-swift/Package.swift:337
cxxLanguageStandard: .gnucxx20</code></pre>
<p>C++20 모드에서 fmt 10.2.1의 <code>core.h</code>가 <code>FMT_CONSTEVAL</code>을 <code>consteval</code>로 활성화:</p>
<pre><code class="language-cpp">// fmt/include/fmt/core.h:224-233
#ifndef FMT_CONSTEVAL
#  if ((FMT_GCC_VERSION &gt;= 1000 || FMT_CLANG_VERSION &gt;= 1101) &amp;&amp; \
       (!defined(__apple_build_version__) ||                     \
        __apple_build_version__ &gt;= 14000029L) &amp;&amp;                 \
       FMT_CPLUSPLUS &gt;= 202002L)
#    define FMT_CONSTEVAL consteval   // ← C++20 + Apple Clang 21이면 활성화
#  else
#    define FMT_CONSTEVAL
#  endif
#endif</code></pre>
<p>Apple Clang 21(Xcode 26)이 <code>consteval</code> 평가를 이전 버전보다 더 엄격하게 검증하면서, fmt 10.2.1의 <code>FMT_COMPILE_STRING</code> 패턴이 &quot;상수 표현식이 아니다&quot;라고 거부당함.</p>
<h2 id="해결-과정">해결 과정</h2>
<h3 id="시도-1-의존성-업데이트">시도 1: 의존성 업데이트</h3>
<p>mlx-swift(0.31.1), WhisperKit(0.17.0) 모두 이미 최신 릴리스. 업스트림에 아직 수정이 없음.</p>
<h3 id="시도-2-프로젝트-레벨-c-플래그">시도 2: 프로젝트 레벨 C++ 플래그</h3>
<p><code>project.yml</code>에 <code>OTHER_CPLUSPLUSFLAGS: [&quot;-DFMT_CONSTEVAL=&quot;]</code> 추가 → <strong>실패</strong>. SPM 패키지 타겟은 프로젝트 레벨 빌드 설정을 상속하지 않음.</p>
<h3 id="시도-3-로컬-패키지-오버라이드-성공">시도 3: 로컬 패키지 오버라이드 (성공)</h3>
<ol>
<li><p>mlx-swift를 서브모듈 포함하여 로컬 클론:</p>
<pre><code class="language-bash">git clone --branch 0.31.1 --recursive https://github.com/ml-explore/mlx-swift.git ../mlx-swift</code></pre>
</li>
<li><p><code>Package.swift</code>의 Cmlx 타겟 cxxSettings에 define 추가:
```diff
// mlx-swift/Package.swift:207-213
cxxSettings: cxxSettings + [
  .headerSearchPath(&quot;mlx&quot;),
  .headerSearchPath(&quot;mlx-c&quot;),
  .headerSearchPath(&quot;json/single_include/nlohmann&quot;),
  .headerSearchPath(&quot;fmt/include&quot;),
  .define(&quot;MLX_VERSION&quot;, to: &quot;&quot;0.31.1&quot;&quot;),</p>
</li>
</ol>
<ul>
<li>.define(&quot;FMT_CONSTEVAL&quot;, to: &quot;&quot;),
],<pre><code></code></pre></li>
</ul>
<ol start="3">
<li><code>project.yml</code>에 로컬 패키지 참조 추가:<pre><code class="language-yaml">packages:
WhisperKit:
 url: https://github.com/argmaxinc/WhisperKit.git
 from: &quot;0.16.0&quot;
Qwen3Speech:
 url: https://github.com/ivan-digital/qwen3-asr-swift.git
 from: &quot;0.0.7&quot;
mlx-swift:
 path: ../mlx-swift   # 로컬 오버라이드</code></pre>
</li>
</ol>
<p>SPM이 패키지 identity(<code>mlx-swift</code>)가 동일하면 로컬 버전을 우선 사용하므로, WhisperKit과 Qwen3Speech가 원격 mlx-swift 대신 패치된 로컬 버전을 참조하게 된다.</p>
<p><strong>주의</strong>: 디렉토리 이름이 패키지 identity와 일치해야 한다. <code>mlx-swift-local</code> 같은 이름을 쓰면 <code>identity doesn&#39;t match override&#39;s identity</code> 에러 발생.</p>
<h2 id="배운-점">배운 점</h2>
<p>SPM 패키지 타겟은 프로젝트 레벨 빌드 설정을 상속하지 않으므로, 전이 의존성의 C++ 빌드 플래그를 바꾸려면 로컬 패키지 오버라이드가 유일한 방법이다. <code>#ifndef</code> 가드가 있는 매크로는 <code>-D</code> define으로 선제 정의하여 우회할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[알량한 자비를 베푸는 사람이 되겠습니까? 예수님의 친구가 되겠습니까?]]></title>
            <link>https://velog.io/@solst_ice/%EC%95%8C%EB%9F%89%ED%95%9C-%EC%9E%90%EB%B9%84%EB%A5%BC-%EB%B2%A0%ED%91%B8%EB%8A%94-%EC%82%AC%EB%9E%8C%EC%9D%B4-%EB%90%98%EA%B2%A0%EC%8A%B5%EB%8B%88%EA%B9%8C-%EC%98%88%EC%88%98%EB%8B%98%EC%9D%98-%EC%B9%9C%EA%B5%AC%EA%B0%80-%EB%90%98%EA%B2%A0%EC%8A%B5%EB%8B%88%EA%B9%8C</link>
            <guid>https://velog.io/@solst_ice/%EC%95%8C%EB%9F%89%ED%95%9C-%EC%9E%90%EB%B9%84%EB%A5%BC-%EB%B2%A0%ED%91%B8%EB%8A%94-%EC%82%AC%EB%9E%8C%EC%9D%B4-%EB%90%98%EA%B2%A0%EC%8A%B5%EB%8B%88%EA%B9%8C-%EC%98%88%EC%88%98%EB%8B%98%EC%9D%98-%EC%B9%9C%EA%B5%AC%EA%B0%80-%EB%90%98%EA%B2%A0%EC%8A%B5%EB%8B%88%EA%B9%8C</guid>
            <pubDate>Mon, 24 Mar 2025 02:36:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>당신이 누군가에게 절대적으로 필요한 존재라면,
당신은 하나님의 질서에서 벗어난 것입니다.
알량한 자비를 베푸는 사람이 되겠습니까?
예수님의 친구가 되겠습니까?
[오스왈드 챔버스의 ‘주님은 나의 최고봉’ 356 묵상 중]</p>
</blockquote>
<p>이 묵상 글을 보고 ‘그럼 나는 누군가의 필요가 되는 사람이 되면 안된다는건가?’ 생각하게 됐다.
궁금한걸 못참는 나는 지피티에게 이 글을 물어보니 이렇게 대답했다.</p>
<blockquote>
<p>‘내가 누군가를 구원할 수 있다’, ‘내가 없으면 저 사람은 안된다’는 생각은
하나님보다 <strong>내 자신을 더 의지하게 만드는 위험한 태도</strong>일 수 있다는 경고입니다.
‘알량한 자비’라는 말도 그래서 쓰인 거예요.
내가 도와주는 것이 <strong>겸손한 사랑이 아니라, 내 우월함을 드러내는 얄팍한 동정심</strong>이 되기 쉽다는 뜻입니다.</p>
<p>핵심 메시지는 이겁니다.
<strong>“당신이 예수님 없이 누군가의 인생을 책임지려 할 때, 그것은 하나님의 질서를 넘는 것이다.</strong>
<strong>당신은 구원자가 아니라 예수님의 친구가 되어라.”</strong></p>
</blockquote>
<p>맞는 말이다.
그러면서 동시에 참 애매하고 어려울 것 같다는 생각이 들었다.</p>
<p>주님의 사랑을 가지고 섬기다보면 ‘나의 의’가 불쑥 고개를 쳐들 것 같다.
나의 역할을 제대로 잡고 가자.
나는 누군가를 구원할 수 있는 ‘구원자’가 아니라
어떤 사람으로 하여금 오직 <strong>예수님을 만나게 하는 ‘통로와 길’</strong>의 역할이 되자.</p>
<blockquote>
<p>저 높이 솟은 산이 되기보다,
여기 오름직한 동산이 되길.
나 가는 길만 비추기 보다는,
누군가의 길을 비춰준다면.
[한웅재, 소원]</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[아이폰에서 체크박스가 이상해요!]]></title>
            <link>https://velog.io/@solst_ice/%EC%95%84%EC%9D%B4%ED%8F%B0%EC%97%90%EC%84%9C-%EC%B2%B4%ED%81%AC%EB%B0%95%EC%8A%A4%EA%B0%80-%EC%9D%B4%EC%83%81%ED%95%B4%EC%9A%94</link>
            <guid>https://velog.io/@solst_ice/%EC%95%84%EC%9D%B4%ED%8F%B0%EC%97%90%EC%84%9C-%EC%B2%B4%ED%81%AC%EB%B0%95%EC%8A%A4%EA%B0%80-%EC%9D%B4%EC%83%81%ED%95%B4%EC%9A%94</guid>
            <pubDate>Sun, 16 Mar 2025 04:20:10 GMT</pubDate>
            <description><![CDATA[<p>최근 프로젝트에서 체크박스 UI를 구현하는 과정에서 아주 킹받는 버그를 마주쳤습니다.
이번 글에서는 그 해결 과정을 공유해볼까 합니다.</p>
<h2 id="🤔-체크박스야-너-왜-그러니">🤔 체크박스야, 너 왜 그러니?</h2>
<p>얼마 전 정부 부처 필터 기능을 가진 체크박스 UI를 만들고 있었습니다.</p>
<p>체크박스를 만드는 건 어렵지 않죠.
당연히 윈도우나 맥, 안드로이드 환경(웹, 앱 모두!)에서는 문제없이 잘 동작했어요.
그런데... iOS와 iPadOS의 브라우저와 앱에서는 이상하게 작동하더라고요.</p>
<p><img src="https://velog.velcdn.com/images/solst_ice/post/03701b43-7c56-4754-9daa-f11a5d89f630/image.gif" alt=""></p>
<p>정확히 말하면, 국토부부터 과기부까지 빠르게 연속으로 체크를 하면, 가끔씩 다음 항목이 체크되지 않고 이전 항목이 체크 해제되는 괴이한 현상이었습니다.</p>
<h2 id="🧑💻-react-상태-문제가-아니라고">🧑‍💻 React 상태 문제가 아니라고?</h2>
<p>처음엔 React에서 함수형 업데이트를 해주지 않아서라고 생각했습니다.
<code>setState(value)</code> 방식으로 상태 업데이트를 하면 문제가 생길 때가 많으니까요.
근데... 잘 썼더라고요?</p>
<pre><code class="language-typescript">function handleDepartmentChange(value: DepartmentFilterType[]) {
  setValues((values) =&gt; ({ ...values, departments: value }));
}</code></pre>
<p>&quot;그럼 범인은 누구지...?&quot; 하고 한참 헤맸습니다.</p>
<h2 id="🕵️♂️-ios-너-수상해">🕵️‍♂️ iOS, 너 수상해...</h2>
<p>이 이슈를 유심히 살펴보면서 몇 가지 단서들을 추려냈습니다.</p>
<ul>
<li>iOS와 iPadOS에서만 발생</li>
<li>Safari뿐만 아니라 Chrome에서도 동일하게 발생.
(iOS의 Chrome은 Chromium 엔진이 아니라 Webkit 엔진이다.)</li>
<li>키보드/마우스를 붙여서 사용하면 정상, 순수한 터치 환경에서만 발생.</li>
<li>천천히 터치하면 아무 문제 없는데, 빠르게 터치하면 발생.</li>
<li>버그 발생 시 이전 체크박스에 다시 Ripple 효과(MUI 터치 효과)가 나타남.</li>
</ul>
<p>한 가지 가설을 세웠습니다.</p>
<blockquote>
<p>iOS가 체크박스를 빠르게 누를 때 두 개가 너무 가까워서 하나의 더블 터치로 인식한다.</p>
</blockquote>
<p>그래서 바로 로그를 찍어보기로 했습니다.</p>
<h2 id="🖥-본격-수사-시작">🖥 본격 수사 시작</h2>
<p>(다행이도!!) iOS의 Safari는 macOS의 Safari를 통해서 로그 확인이 가능합니다.
바로 체크박스 UI에 있는 웬만한 이벤트 리스너(<code>onTouchStart</code>, <code>onTouchEnd</code>, <code>onChange</code>, <code>onDoubleClick</code>)에 로그를 찍었습니다.</p>
<p>로그 결과는 이랬습니다:</p>
<p><img src="https://velog.velcdn.com/images/solst_ice/post/9b07b042-5cce-4659-aeea-bf3d88201b7c/image.png" alt=""></p>
<p>로그를 보면 <code>onTouch</code> 관련 이벤트는 MOLIT, MOE를 차례로 터치한 것으로 잘 나옵니다.
그런데 <code>onChange</code>를 보면 이상하죠. MOLIT을 두번 터치한 것으로 인식했습니다.
심지어 <code>onDoubleClick</code> 이벤트가 발생했습니다.</p>
<p>더블 터치로 인식하는게 확실해졌습니다.</p>
<h2 id="🎯-해결은-간단했다">🎯 해결은 간단했다</h2>
<p>이걸 저만 알게 된게 아닐텐데 검색을 해도 잘 안나오더라고요.
사실 뭐라고 검색해야 할지 몰랐던게 컸을 것 같아요.</p>
<p>일단 임시로 모바일 환경에선 <code>onTouchEnd</code> 이벤트로 상태 변경을 했더니 문제가 사라졌습니다.</p>
<pre><code class="language-tsx">&lt;FormControlLabel
  key={index}
  control={&lt;Checkbox /&gt;}
  checked={value.includes(department)}
  onTouchEnd={() =&gt; handleChange(department)}
  label={t(`filter.departmentFilter.${department}`)}
/&gt;</code></pre>
<p>근데 이렇게 하면 마우스로 클릭했을 때는 동작 안 하는 문제가 생기죠.
그래서 &quot;그럼 두 이벤트를 다 쓰면 되지 않나?&quot;라고 생각했는데, 그렇게 하면 상태 업데이트가 두 번 일어나더라고요.</p>
<p>그러다가 떠오른 게 <code>event.stopPropagation()</code></p>
<blockquote>
<p>이벤트가 중복으로 발생하는 거니까, 이벤트 전파를 막아버리면 되겠구나!</p>
</blockquote>
<p>그래서 <code>onTouchEnd</code> 이벤트 리스너에 <code>event.stopPropagation()</code>을 추가했더니, 딱 한 번만 깔끔하게 이벤트가 발생하며 문제를 완벽히 해결할 수 있었답니다.</p>
<h2 id="🎉-최종-해결-코드-공개">🎉 최종 해결 코드 공개!</h2>
<p>그렇게 탄생한 최종 코드는 이렇게 생겼어요.</p>
<pre><code class="language-tsx">export default function FilterDepartment() {
  // 기타 코드...

  function handleChange(event: SyntheticEvent, filterType: DepartmentFilterType) {
    event.stopPropagation();
    event.preventDefault();

    // 기타 코드...

    onChange(sortedValues);
  }

  return (
    &lt;List disablePadding&gt;
      &lt;ListSubheader
        disableSticky
        component={ButtonBase}
        onClick={toggle}
        sx={{
          display: &#39;flex&#39;,
          justifyContent: &#39;space-between&#39;,
          alignItems: &#39;center&#39;,
          backgroundColor: &#39;inherit&#39;,
          width: &#39;100%&#39;,
        }}
      &gt;
        {t(&#39;filter.departmentFilter.title&#39;)}
        {open ? &lt;ExpandLess /&gt; : &lt;ExpandMore /&gt;}
      &lt;/ListSubheader&gt;
      &lt;Collapse in={open} unmountOnExit&gt;
        &lt;FormGroup sx={{ pl: 1 }}&gt;
          {departments.map((department, index) =&gt; (
            &lt;FormControlLabel
              key={index}
              control={&lt;Checkbox /&gt;}
              checked={value.includes(department)}
              onChange={(event) =&gt; handleChange(event, department)}
              onTouchEnd={(event) =&gt; handleChange(event, department)}
              label={t(`filter.departmentFilter.${department}`)}
            /&gt;
          ))}
        &lt;/FormGroup&gt;
      &lt;/Collapse&gt;
    &lt;/List&gt;
  );
}
/&gt;</code></pre>
<p>이렇게 해서 결국 iOS에서만 나타나는 괴상한 체크박스 문제를 완벽히 해결했습니다.</p>
<h2 id="🚀-결론">🚀 결론</h2>
<p>아주 독특하고 미묘한 iOS 터치 이벤트의 특징을 경험한 재밌는 삽질이었습니다.
여러분들도 이 글이 도움이 되어 삽질을 조금이라도 덜 하시길 바랍니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Session + Redis를 이용한 세션 중복 처리 삽질기 (with AWS ElastiCache)]]></title>
            <link>https://velog.io/@solst_ice/Spring-Session-Redis%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%84%B8%EC%85%98-%EC%A4%91%EB%B3%B5-%EC%B2%98%EB%A6%AC-%EC%82%BD%EC%A7%88%EA%B8%B0-with-AWS-ElastiCache</link>
            <guid>https://velog.io/@solst_ice/Spring-Session-Redis%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%84%B8%EC%85%98-%EC%A4%91%EB%B3%B5-%EC%B2%98%EB%A6%AC-%EC%82%BD%EC%A7%88%EA%B8%B0-with-AWS-ElastiCache</guid>
            <pubDate>Tue, 11 Mar 2025 01:25:51 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/solst_ice/post/5743a322-3110-479c-ac11-f3064d3c5c6e/image.png" alt=""></p>
<p>이번 글에서는 <strong>Spring Session을 활용한 세션 관리</strong> 과정에서 마주한 문제와 해결 과정을 공유합니다. 특히, <strong>다중 인스턴스 환경에서 세션 중복 처리 문제를 해결한 경험</strong>을 다룹니다.</p>
<h2 id="🔍-배경-및-요구사항">🔍 배경 및 요구사항</h2>
<p>최근 제품의 요구사항은 다음과 같았습니다.</p>
<ul>
<li><strong>다중 인스턴스 환경</strong>에서 무중단 배포 지원</li>
<li>배포 및 인스턴스 확장 시 <strong>사용자의 로그인 세션 유지</strong></li>
<li>JWT 대신 로그인 세션을 능동적으로 관리하고 강제로 만료시킬 필요가 있어 <strong>Spring Session + Redis</strong> 방식을 선택</li>
</ul>
<p>즉, Redis를 선택한 핵심 이유는 <strong>성능이 아니라, 다중 인스턴스 환경에서 일관된 세션 관리를 위함</strong>이었습니다.</p>
<h3 id="📌-사전-준비-gradle-의존성-추가">📌 사전 준비: Gradle 의존성 추가</h3>
<p>먼저, Spring Session과 Redis를 사용하기 위해 Gradle에 다음 의존성을 추가해야 합니다.</p>
<pre><code class="language-gradle">dependencies {
    implementation &#39;org.springframework.session:spring-session-data-redis&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39;
}</code></pre>
<h2 id="🚧-초기-문제-상황">🚧 초기 문제 상황</h2>
<p>초기에는 <code>maximumSessions(1)</code> 설정을 통해 중복 로그인을 막으려고 했습니다.</p>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

  private final CustomSessionExpiredStrategy sessionExpiredStrategy;

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
      .sessionManagement(sessionManagement -&gt;
        sessionManagement
          .maximumSessions(1)
          .expiredSessionStrategy(sessionExpiredStrategy)
      )
      .build();
  }
}</code></pre>
<p>서버가 실행 중일 때는 정상적으로 중복 로그인을 차단했지만, <strong>서버를 재시작하거나 새 인스턴스를 추가하면 중복 로그인이 허용되는 문제</strong>가 발생했습니다.</p>
<h2 id="🧐-원인-분석">🧐 원인 분석</h2>
<p>문제의 핵심은 <strong>Spring Security의 기본 <code>SessionRegistry</code>가 메모리 기반으로 동작하기 때문</strong>에 <code>.maximumSessions(1)</code>은 서버의 메모리 내에 존재하는 세션만 확인합니다.
따라서 서버가 재시작되거나 새로운 인스턴스가 추가되면 Redis에 저장된 세션 정보를 인식하지 못하고 중복 로그인을 허용하는 것입니다.</p>
<p>결국, Redis에 저장된 세션을 동기화해서 관리하려면 <strong>Redis 기반의 SessionRegistry 구현</strong>이 필요했습니다.</p>
<h2 id="🔎-spring-session의-redis-지원-springsessionbackedsessionregistry">🔎 Spring Session의 Redis 지원 (SpringSessionBackedSessionRegistry)</h2>
<p>Spring Session을 활용하면 <code>SpringSessionBackedSessionRegistry</code>가 존재하여 Redis 기반의 세션 관리를 간단히 적용할 수 있습니다.</p>
<p>하지만 여기서 중요한 설정이 있습니다.</p>
<p><strong>Spring Boot는 <code>repository-type: indexed</code> 설정이 없으면 <code>FindByIndexNameSessionRepository</code> 빈을 생성하지 않습니다.</strong>
이 빈이 생성되지 않으면 결국 <code>SpringSessionBackedSessionRegistry</code>를 사용할 수 없습니다.</p>
<h3 id="📌-redis의-데이터-저장-방식과-연관된-이유">📌 Redis의 데이터 저장 방식과 연관된 이유</h3>
<p>Redis는 기본적으로 Key-Value 형태로 데이터를 저장합니다. 하지만 Spring Session이 세션 관리를 할 때는 세션을 식별하는 정보(예: 사용자 이름, 세션 ID 등)를 추가로 저장하고 <strong>이를 인덱스화하여 접근</strong>할 수 있어야 합니다.</p>
<ul>
<li><code>repository-type: indexed</code> 설정을 하지 않은 경우, Redis에 단순히 세션 ID를 키로만 저장합니다.</li>
</ul>
<pre><code>spring:session:sessions:12345678-1234-1234-1234-123456789abc</code></pre><ul>
<li>반면, <code>repository-type: indexed</code> 설정을 적용하면, Redis에 세션을 조회하기 위한 추가적인 인덱스가 생성됩니다.</li>
</ul>
<pre><code>spring:session:sessions:expires:12345678-1234-1234-1234-123456789abc
spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user@example.com</code></pre><p>즉, 단순히 키 하나로 세션 데이터를 접근하는 것이 아니라, <strong>세션과 관련된 여러 키를 사용하여 복합적인 조회를 하기 위해 Redis 내 별도의 인덱스가 필요합니다.</strong>
그래서 Spring Boot는 명시적으로 다음 설정이 있어야 Redis에 인덱스를 기반으로 세션 데이터를 관리하도록 내부적으로 <code>FindByIndexNameSessionRepository</code> 빈을 생성합니다.</p>
<pre><code class="language-yml">spring:
  session:
    redis:
      repository-type: indexed</code></pre>
<p>이 설정이 없으면 Redis는 단순 키-값으로만 데이터를 관리하여 <strong>세션 관련 인덱스 정보를 생성하지 않기 때문</strong>에 <code>FindByIndexNameSessionRepository</code>가 생성되지 않습니다.</p>
<h3 id="🔥-해결-코드">🔥 해결 코드</h3>
<p>아래와 같이 설정을 추가하면 문제는 깔끔하게 해결됩니다.</p>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

  private final CustomSessionExpiredStrategy sessionExpiredStrategy;
  private final FindByIndexNameSessionRepository&lt;? extends Session&gt; sessionRepository;

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
      .sessionManagement(sessionManagement -&gt;
        sessionManagement
          .maximumSessions(1)
          .expiredSessionStrategy(sessionExpiredStrategy)
          .sessionRegistry(sessionRegistry())
      )
      .build();
  }

  @Bean
  public SessionRegistry sessionRegistry() {
    return new SpringSessionBackedSessionRegistry&lt;&gt;(sessionRepository);
  }
}</code></pre>
<h3 id="📌-최종-yml-설정">📌 최종 yml 설정</h3>
<p>추가적으로, 아래 설정을 정확히 입력해야 Redis가 인덱스 기반 세션 저장소로 동작합니다.</p>
<pre><code class="language-yml">spring:
  data:
    redis:
      host: localhost
      port: 6379
  session:
    redis:
      flush-mode: on-save
      namespace: spring:session
      repository-type: indexed  # 중요! 반드시 추가해야 함
      configure-action: none  # AWS ElastiCache 대응</code></pre>
<h3 id="💥-aws-환경에서-추가-주의사항">💥 AWS 환경에서 추가 주의사항</h3>
<p>AWS의 ElastiCache를 사용할 때 다음 에러가 발생할 수 있습니다.</p>
<pre><code class="language-plaintext">RedisCommandExecutionException: ERR unknown command &#39;CONFIG&#39;, with args beginning with: &#39;GET&#39; &#39;notify-keyspace-events&#39;</code></pre>
<p>이때는 위 설정에서 <code>configure-action: none</code>을 추가하여 해결할 수 있습니다.</p>
<h2 id="🎉-결론-및-소감">🎉 결론 및 소감</h2>
<p>이제 무중단 배포 환경에서도 일관된 세션 관리가 가능합니다. Redis 기반의 세션 관리를 사용한다면 반드시 <code>repository-type: indexed</code> 설정을 추가하는 것을 기억하세요!</p>
<p>비슷한 문제를 겪는 분들에게 이 글이 도움이 되길 바랍니다. 😊</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[후기] GitHub.io 블로그를 접고 Velog로 돌아온 이유]]></title>
            <link>https://velog.io/@solst_ice/Github.io-%EC%97%90%EC%84%9C-Velog%EB%A1%9C-%EB%84%98%EC%96%B4%EC%98%A8-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@solst_ice/Github.io-%EC%97%90%EC%84%9C-Velog%EB%A1%9C-%EB%84%98%EC%96%B4%EC%98%A8-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Thu, 06 Feb 2025 15:32:25 GMT</pubDate>
            <description><![CDATA[<h2 id="🔍-검색-엔진에게-참패">🔍 검색 엔진에게 참패</h2>
<p>원래 Velog 계정을 가지고 있었지만 &#39;새로 시작하는 마음&#39;으로 GitHub.io 블로그를 열었습니다.
Jekyll 테마도 예쁘게 고르고, about 페이지까지 만들었는데...
문제는 <strong>구글 검색에 전혀 노출되지 않는다는 점</strong>이었습니다.</p>
<p>sitemap.xml을 만들고 Search Console에 등록해도 인덱싱이 전혀... 안되더라고요.</p>
<p><img src="https://velog.velcdn.com/images/solst_ice/post/24942ba1-5d79-4054-9329-25a9fd6e8cae/image.png" alt=""></p>
<p><em>아니 진짜 이거 뭔데 ㅡㅡ 아무리 찾아도 이유를 모르겠어</em></p>
<p>물론 누가 보기를 원해서 시작한건 아니지만, 괜히 마음에 걸려서 Velog로 회귀했습니다.
Velog도 비슷하겠거니.. 생각하고 방치했는데 얼마 되지도 않았는데 검색이 되더라고요..?
이게 진짜 편하구나 싶었습니다.</p>
<h2 id="⏰-삽질할-시간에-글쓰기">⏰ 삽질할 시간에 글쓰기</h2>
<p>누가 말했던가요, &quot;Don&#39;t reinvent the wheel.&quot;
이미 검증된 것에 삽질하지 말고 그 위에서 진짜 가치있는걸 만들라는 말이죠.</p>
<p>GitHub.io는 <strong>부수적인 작업이 너무 많았습니다</strong>.
진정 가치 있는 글은 다 쓰고도 날짜 입력, 제목 입력, 카테고리 추가 등등
(카테고리 이름 토씨 하나 틀리면 다른 카테고리로 인식해버림..)
이건 좀 아닌거같은데 순간들이 많더라고요.</p>
<p>Velog는 좀 낫더군요.
에디터가 간결해서 오히려 글 내용에 더 집중할 수 있었습니다.
개발자 특) 마크다운 형식인게 너무 좋고
티스토리처럼 막 복잡하게 생기지 않아서 좋았어요.</p>
<h2 id="🖼️-이미지-삽입-너무-편함">🖼️ 이미지 삽입 너무 편함</h2>
<p>GitHub.io에 이미지 넣을 때마다 이런 과정을 거쳤습니다.</p>
<ol>
<li>로컬 이미지를 assets 폴더에 복사</li>
<li>상대 경로 일일이 입력</li>
<li>실수로 경로 잘못 적으면 찾아서 수정</li>
</ol>
<p>뭐 플러그인이 몇개 있던데 그것들도 막 완벽하진 않더라고요.
어찌됐든 파일을 직접 관리하는거다보니까.
그건 진짜 일반 블로그들처럼 그냥 복붙이 편하다는 생각이 듭니다.</p>
<h2 id="📱-모바일로-쓸-수-있다니-새삼">📱 모바일로 쓸 수 있다니 (새삼)</h2>
<p>뭐 진짜 당연한거긴 한데 모바일로 쓸 수 있다는게 어딥니까.
IntelliJ 라는 아주 비싼 글 작성기(?)로 쓰다보니 모바일에선 쓰기 어렵죠.
웹의 승.</p>
<h2 id="🥲-근데-이건-좀-아깝다">🥲 근데 이건 좀 아깝다</h2>
<ol>
<li><strong>전체 수정이 번거로움</strong>
Github.io는 편집기로 쓰다보니 제가 잘못 쓰고 있던 글을 싸그리 검색해서 한번에 수정하고 발행하면 되는데
그게 안돼요.
근데 이건 솔직히 많은걸 바라는거 같긴 합니다.</li>
<li><strong>일부 마크다운이 안 먹힘</strong> :
<code>&lt;small&gt;</code> 태그가 안돼요!
프리뷰에서는 적용되는 것처럼 보이는데 발행하면 적용 안되어있습니다...
글씨가 귀염뽀짝해서 좋았는데...</li>
<li><strong>PWA 미지원</strong>
핸드폰에서 <code>홈화면에 추가</code> 해서 앱처럼 보고싶은데 그냥 브라우저로 열리더라고요
아주 조금 아쉽  </li>
</ol>
<h2 id="💡-아마-계속-velog-쓸-듯">💡 아마 계속 Velog 쓸 듯</h2>
<p>티스토리, 네이버 블로그, Velog, Github.io 한번씩 다 찍먹 해보고 낸 결론입니다.
저한테는 제일 편한거 같아요.
이제 글들도 꽤 많이 써놔서 옮기기도 어려울거같기도 하네요..ㅎ
하지만 진짜 중요한건 꾸준히 하는거.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[3.3 이상 스프링 부트의 컨트롤러에서 에서 Page 객체를 그대로 응답할 때 발생한 경고]]></title>
            <link>https://velog.io/@solst_ice/3.3-%EC%9D%B4%EC%83%81-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%9D%98-%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC%EC%97%90%EC%84%9C-%EC%97%90%EC%84%9C-Page-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EA%B7%B8%EB%8C%80%EB%A1%9C-%EC%9D%91%EB%8B%B5%ED%95%A0-%EB%95%8C-%EB%B0%9C%EC%83%9D%ED%95%9C-%EA%B2%BD%EA%B3%A0</link>
            <guid>https://velog.io/@solst_ice/3.3-%EC%9D%B4%EC%83%81-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%9D%98-%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC%EC%97%90%EC%84%9C-%EC%97%90%EC%84%9C-Page-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EA%B7%B8%EB%8C%80%EB%A1%9C-%EC%9D%91%EB%8B%B5%ED%95%A0-%EB%95%8C-%EB%B0%9C%EC%83%9D%ED%95%9C-%EA%B2%BD%EA%B3%A0</guid>
            <pubDate>Wed, 11 Dec 2024 00:50:46 GMT</pubDate>
            <description><![CDATA[<h2 id="경고-메시지-발견">경고 메시지 발견</h2>
<p>3.3 이상의 스프링 부트를 사용하는 프로젝트에서
Page 객체를 컨트롤러에서 Json으로 변환하여 응답할 때
이전에는 볼 수 없었던 경고메시지가 보였습니다.</p>
<pre><code class="language-bash">2024-12-10T21:05:44.279+09:00  WARN 4209 --- [p-nio-80-exec-1] ration$PageModule$WarningLoggingModifier : Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure!
    For a stable JSON structure, please use Spring Data&#39;s PagedModel (globally via @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO))
    or Spring HATEOAS and Spring Data&#39;s PagedResourcesAssembler as documented in https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables.</code></pre>
<blockquote>
<p>PageImpl 인스턴스를 있는 그대로 직렬화하는 것은 지원되지 않으므로
결과 JSON 구조의 안정성을 보장할 수 없습니다!
안정적인 JSON 구조를 원하시면 Spring Data의 PagedModel을 사용하세요.</p>
</blockquote>
<p>오류 메시지에 포함된 링크로 들어가보면,
새롭게 추가된 PagedModel을 소개해줍니다.</p>
<h2 id="왜">왜?</h2>
<p>잉..? 왜 갑자기 이걸 쓰라고 하지?
지금까지 직렬화 잘 해주고 있었잖아?
해서 보니 이런 내용이 있었습니다.</p>
<blockquote>
<p>Creating JSON representations for Page</p>
<p>It’s common for Spring MVC controllers to try to ultimately render a representation of a Spring Data page to clients. While one could simply return Page instances from handler methods to let Jackson render them as is, <strong>we strongly recommend against this as the underlying implementation class PageImpl is a domain type.</strong> This means we might want or have to change its API for unrelated reasons, and such changes might alter the resulting JSON representation in a breaking way.</p>
<p>With Spring Data 3.1, we started hinting at the problem by issuing a warning log describing the problem. We still ultimately recommend to leverage the integration with Spring HATEOAS for a fully stable and hypermedia-enabled way of rendering pages that easily allow clients to navigate them. But as of version 3.3 Spring Data ships a page rendering mechanism that is convenient to use but does not require the inclusion of Spring HATEOAS.</p>
</blockquote>
<p><del>으악 영어!</del></p>
<blockquote>
<p>PageImpl 클래스가 <strong>도메인 타입</strong>이라서 PageImpl 클래스 자체를 직렬화 하지 않는걸 권장한다.
따라서 3.3 버전부터 PageModel을 만들었으니 이거 사용해라.</p>
</blockquote>
<p>도메인 타입? 이 무슨 뜻인가 해서 찾아보니
&quot;비즈니스 로직의 핵심 개념을 표현하는 객체 유형&quot; 이라는데
그냥 JPA의 엔티티 같은 개념이라고 이해했습니다.</p>
<p>그러니까 PageImpl로 생성된 객체를 API로 그대로 반환한다면
엔티티를 그대로 Json으로 변환해서 반환한거나 마찬가지라는거죠.</p>
<p>PageImpl에 여러 비즈니스 로직들이 들어가있어서 내부 구현이 노출될 수도 있고,
내부 필드 구조가 바뀔 수도 있기 때문에 Json 구조의 안정성을 보장 못할거라는 뜻입니다.</p>
<p>저희는 보통 엔티티를 반환하기 위해서 DTO로 변환하여 응답하니,
Spring boot가 Page 객체에 대한 DTO를 따로 만들어줬다라고 봐도 되겠네요.</p>
<h2 id="해결방법">해결방법</h2>
<p>경고를 제거하는 방법은 간단합니다.
두 가지 중 하나를 선택합니다.</p>
<ol>
<li><p>기존에 <code>Page&lt;ResponseDto&gt;</code>로 반환하던 API를 <code>PagedModel&lt;ResponseDto&gt;</code>로 변경하고
반환하는 곳에 <code>new PagedModel&lt;&gt;(Page객체)</code>를 추가해줍니다.</p>
<pre><code class="language-java">// before
@GetMapping(&quot;/example&quot;)
public ResponseEntity&lt;Page&lt;ExampleResponseDto&gt;&gt; getAll(@RequestParam int page) {
 return ResponseEntity.ok(exampleService.getAll(page));
}</code></pre>
<pre><code class="language-java">// after
@GetMapping(&quot;/example&quot;)
public ResponseEntity&lt;PagedModel&lt;ExampleResponseDto&gt;&gt; getAll(@RequestParam int page) {
 return ResponseEntity.ok(new PagedModel&lt;&gt;(exampleService.getAll(page)));
}</code></pre>
</li>
<li><p>Spring bean에 <code>@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)</code>를 붙여서
모든 Page 객체가 PagedModel 객체로 반환되도록 합니다.</p>
<pre><code class="language-java">@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)
public class MyConfiguration { }</code></pre>
<p>이렇게 하면 기존의 Page 타입이 PagedModel 타입으로 변경되어서 Json으로 직렬화됩니다.</p>
</li>
</ol>
<h2 id="⚠️주의사항⚠️">⚠️주의사항⚠️</h2>
<p><code>Page</code> 타입과 <code>PagedModel</code> 타입은 각각 반환되는 Json 형식이 다릅니다.
꼭 이점 참고하셔서 프로젝트에 적용하여 프론트 분들과 원만한 합의...가 이루어지길 바랍니다.</p>
<h4 id="page-타입">Page 타입</h4>
<pre><code class="language-json">{
  &quot;content&quot;: [/* 데이터들... */],
  &quot;pageable&quot;: {
    &quot;pageNumber&quot;: 1,
    &quot;pageSize&quot;: 10,
    &quot;sort&quot;: [],
    &quot;offset&quot;: 10,
    &quot;paged&quot;: true,
    &quot;unpaged&quot;: false
  },
  &quot;last&quot;: false,
  &quot;totalElements&quot;: 228,
  &quot;totalPages&quot;: 23,
  &quot;first&quot;: false,
  &quot;size&quot;: 10,
  &quot;number&quot;: 1,
  &quot;sort&quot;: [],
  &quot;numberOfElements&quot;: 10,
  &quot;empty&quot;: false
}</code></pre>
<h4 id="pagedmodel-타입">PagedModel 타입</h4>
<pre><code class="language-json">{
  &quot;content&quot;: [/* 데이터들... */],
  &quot;page&quot;: {
    &quot;size&quot;: 10,
    &quot;number&quot;: 1,
    &quot;totalElements&quot;: 228,
    &quot;totalPages&quot;: 23
  }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[나만의 Spring Boot 규칙들 (2. Spring Boot 편)]]></title>
            <link>https://velog.io/@solst_ice/%EB%82%98%EB%A7%8C%EC%9D%98-Spring-Boot-%EA%B7%9C%EC%B9%99%EB%93%A4-2.-Spring-Boot-%ED%8E%B8</link>
            <guid>https://velog.io/@solst_ice/%EB%82%98%EB%A7%8C%EC%9D%98-Spring-Boot-%EA%B7%9C%EC%B9%99%EB%93%A4-2.-Spring-Boot-%ED%8E%B8</guid>
            <pubDate>Sun, 08 Dec 2024 15:11:39 GMT</pubDate>
            <description><![CDATA[<h2 id="spring-boot-편">Spring Boot 편</h2>
<h3 id="1-builder-보다는-생성자">1. <code>@Builder</code> 보다는 생성자</h3>
<ul>
<li><code>@Builder</code>를 사용해서 객체를 생성하는 경우</li>
</ul>
<pre><code class="language-java">@Builder
public class ExampleDto {
  private String email;
  private String password;
}</code></pre>
<pre><code class="language-java">public ExampleDto test() {
  return ExampleDto.builder()
    .email(&quot;이메일&quot;)
    .password(&quot;비밀번호&quot;);
}</code></pre>
<ul>
<li>생성자를 사용해서 객체를 생성하는 경우</li>
</ul>
<pre><code class="language-java">@AllArgsConstructor
public class ExampleDto {
  private String email;
  private String password;
}</code></pre>
<pre><code class="language-java">public ExampleDto test() {
  return new ExampleDto(&quot;이메일&quot;, &quot;비밀번호&quot;);
}</code></pre>
<p>얼핏 보면 Builder가 어느 필드에 값이 들어가는지 한눈에 보여 가독성이 좋습니다.
하지만 Builder는 <strong>필드에 값이 들어갈 것을 보장해주지 않습니다.</strong>
필드에 값이 주어지지 않으면 <strong>null을 대입하고, 컴파일 에러를 일으키지도 않습니다.</strong>
심지어 IDE에서 자동으로 링크해주지 않아서 필드가 수정될 시 <code>ExampleDto.builder()</code> 문자열 자체를 검색해야 하는 경우도 많습니다.</p>
<pre><code class="language-java">// ExampleDto.java
@Builder
@Getter
public class ExampleDto {
  private String email;
  private String password;
  private String name;  // 필드를 추가
}

public ExampleDto test() {
  return ExampleDto.builder()
    .email(&quot;이메일&quot;)
    .password(&quot;비밀번호&quot;)  // 오류를 일으키지 않고 name에 null 대입
    .build();
}

public void willFail() {
  ExampleDto dto = test();
  dto.getName().equals(&quot;이름&quot;);  // NullPointerException: name is null
}</code></pre>
<p>따라서 생성자를 사용해서 객체를 생성할 때 아예 값이 들어가는 것을 보장해주면
더 예측 가능한 코드가 될 것입니다.</p>
<p>&#39;생성자의 몇번째 매개변수에 어떤 것이 들어갈 지 알기 힘들다&#39; 라는 단점이 있을 수 있지만,
요즘 IDE는 필요한 경우에 매개변수의 이름을 표시해주기도 하며,</p>
<p><img src="https://velog.velcdn.com/images/solst_ice/post/96b092ff-fa16-4a06-87fb-d06de7e039c4/image.png" alt=""></p>
<p><em>예시를 위해 모두 null을 넣어줬습니다. (null 일땐 무조건 보여주더라구요)</em></p>
<p>차라리 타입 불일치로 오류를 일으켜주는 것이 휴먼 에러를 더 줄일 수 있을 것입니다.</p>
<h3 id="2-주입을-사용할-때-requiredargsconstructor-어노테이션-사용">2. 주입을 사용할 때 <code>@RequiredArgsConstructor</code> 어노테이션 사용</h3>
<p>Spring 자체에서도 필드 주입을 지양하는 것은 유명합니다.
따라서 되도록이면 생성자 주입을 사용해야하는데, 이때 생성자를 직접 작성하기보단
Lombok의 <code>@RequiredArgsConsturctor</code>를 사용합니다.
<code>@RequiredArgsConsturctor</code>는 final로 선언된 필드에 대하여 생성자를 자동 생성해줍니다. 
이것으로 코드가 깔끔해집니다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class ExampleService {

  private final ExampleRepository exampleRepository;
}</code></pre>
<p>이 형태는 아래와 같은 과정을 거쳐서 만들어집니다.</p>
<pre><code class="language-java">// 처음 형태
@Service
public class ExampleService {

  private final ExampleRepository exampleRepository;

  @Autowired
  public ExampleService(ExampleRepository exampleRepository) {
    this.exampleRepository = exampleRepository;
  }
}</code></pre>
<pre><code class="language-java">@Service
public class ExampleService {

  private final ExampleRepository exampleRepository;

  // @Autowired 생략 가능
  public ExampleService(ExampleRepository exampleRepository) {
    this.exampleRepository = exampleRepository;
  }
}</code></pre>
<pre><code class="language-java">@Service
@RequiredArgsConstructor // 생성자 축약
public class ExampleService {

  private final ExampleRepository exampleRepository;
}</code></pre>
<h3 id="3-applicationyml은-각-설정값마다-필요한-도메인으로-나눔">3. application.yml은 각 설정값마다 필요한 도메인으로 나눔</h3>
<p><strong>도메인 별로 나누지 않았을 경우 (환경으로 나눔)</strong></p>
<pre><code class="language-yml"># application.yml
---
spring:
  config:
    activate:
      on-profile:
        - ci
        - local

  server:
    port: 80

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        &quot;[format_sql]&quot;: true

  datasource:
    url: ***
    username: ***
    password: ***

---
spring:
  config:
    activate:
      on-profile: alpha

  server:
    port: 5000

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: validate
    properties:
      hibernate:
        &quot;[format_sql]&quot;: true

  datasource:
    url: ***
    username: ***
    password: ***

---
spring:
  config:
    activate:
      on-profile: prod

  server:
    port: 5000

  jpa:
    show-sql: false
    hibernate:
      ddl-auto: validate
    properties:
      hibernate:
        &quot;[format_sql]&quot;: false

  datasource:
    url: ***
    username: ***
    password: ***</code></pre>
<p>&#39;이 환경에서는 이 설정값이다&#39;라고 직관적으로 보일 수 있으나,
추후 설정값을 수정해야할 때 같은 도메인들의 값들이 어디에 있는지 찾기 힘듭니다.
보통 개발할 때는 도메인 별로 설정값들이 나뉠 때가 많기 때문에
도메인 별로 설정값들을 나눠두면 유지보수 하기 편합니다.
(application-프로필이름.yml 으로 나누는 방법도 동일한 문제가 존재합니다.)</p>
<p><strong>도메인 별로 나눌 경우</strong></p>
<pre><code class="language-yml"># application.yml
spring:
  config:
    import:
      - database.yml
      - security.yml
      - springdoc.yml
      - logging.yml
      - sentry.yml
      - server.yml
      - wordpress.yml
      - messages.yml</code></pre>
<pre><code class="language-yml"># database.yml
---
spring:
  config:
    activate:
      on-profile:
        - ci
        - local

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        &quot;[format_sql]&quot;: true

  datasource:
    url: ***
    username: ***
    password: ***

---
spring:
  config:
    activate:
      on-profile: alpha

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: validate
    properties:
      hibernate:
        &quot;[format_sql]&quot;: true

  datasource:
    url: ***
    username: ***
    password: ***

---
spring:
  config:
    activate:
      on-profile: prod

  jpa:
    show-sql: false
    hibernate:
      ddl-auto: validate
    properties:
      hibernate:
        &quot;[format_sql]&quot;: false

  datasource:
    url: ***
    username: ***
    password: ***</code></pre>
<pre><code class="language-yml"># server.yml
---
spring:
  config:
    activate:
      on-profile:
        - ci
        - local

server:
  port: 80

---
spring:
  config:
    activate:
      on-profile: alpha

server:
  port: 5000

---
spring:
  config:
    activate:
      on-profile: prod

server:
  port: 5000</code></pre>
<h3 id="4-json으로-받게-되는-dto에는-class-보다-record-사용">4. Json으로 받게 되는 DTO에는 class 보다 record 사용</h3>
<p>코드가 깔끔해집니다.
<code>@Getter</code>, <code>@AllArgsConstructor</code> 같은 어노테이션도,
<code>private final</code> 같은 키워드도 붙이지 않아도 됩니다.
record에는 DTO로 사용할 때 필요한 모든 것이 이미 들어가 있습니다.
Java에서 아예 DTO로 사용하라고 만들어준 셈입니다.</p>
<pre><code class="language-java">@Getter
@AllArgsConstructor
public class MemberRequestDto {

  private final @NotBlank String email;
  private final @NotBlank String name;
  private final String phoneNumber;
  private final String memo;
  private final @NotNull MemberRole role;
}
</code></pre>
<pre><code class="language-java">public record MemberRequestDto(
  @NotBlank String email,
  @NotBlank String name,
  String phoneNumber,
  String memo,
  @NotNull MemberRole role
) {}</code></pre>
<p>하지만 이미 class로 구현되어 있다면 굳이 건드리지 않는게 좋을 것입니다.
<code>getEmail()</code>이 <code>email()</code>로 바뀔 뿐만 아니라,
<code>final</code>이 기본적으로 붙어서 필드가 바뀔 것을 산정하고 구현된 로직이 있을 수 있기 때문입니다.
만약에 record를 적용한다면, 새로운 로직 또는 프로젝트에 적용하는 것이 좋을 것 같습니다.</p>
<h3 id="5-json-dto에-사용되는-필드에는-wrapper-class-사용">5. Json DTO에 사용되는 필드에는 Wrapper Class 사용</h3>
<p>Json Response의 경우는 상관없지만,
Json Request의 경우에는 Frontend의 실수나 예상치 못한 변수로 DTO의 필드에 null이 들어올 수 있습니다.
그 null이 들어오는 것을 DTO에서 원시타입(Primitive type)으로 받는다면
예상치 못한 동작을 유발할 수 있습니다.</p>
<p>예를 들어서 아래와 같은 DTO가 있다고 가정합니다.</p>
<pre><code class="language-java">public record ExampleRequestDto(
  @NotNull int sequence,
  @NotBlank @Size(max = 255) String name
) {}</code></pre>
<p>그리고 이 DTO에 맞춰 아래와 같은 Json 형식으로 요청을 보내지만,
모종의 이유로 sequence에 null이 들어가게 된다면</p>
<pre><code class="language-json">{
  &quot;sequence&quot;: null, // 실수!
  &quot;name&quot;: &quot;example&quot;
}</code></pre>
<p>null이 들어갈 수 없는 원시타입에 맞추기 위하여,
Spring은 필드에 억지로 0을 집어넣습니다.
<strong>@NotNull 어노테이션이 먼저 동작하지 않습니다.</strong>
실패해야 할 동작이지만 요청이 성공하며 후에 오류를 잡기 위해 시간을 쓰게 되죠.</p>
<p>따라서 Json에서 변환되는 DTO에서는 웬만하면 Wrapper Class로 필드를 선언하여
좀 더 예상 가능한 작동을 하도록 합니다.</p>
<pre><code class="language-java">public record ExampleRequestDto(
  @NotNull Integer sequence,
  @NotBlank @Size(max = 255) String name
) {}</code></pre>
<p>그럼 정상적으로 null 필드에 대한 Exception이 반환되어, 올바른 조치를 취하게 할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/solst_ice/post/b891d714-9a6b-4ab9-8eff-be7e192a76ad/image.png" alt=""></p>
<h3 id="6-request-시에-사용하는-dto와-response-시에-사용하는-dto-분리">6. Request 시에 사용하는 DTO와 Response 시에 사용하는 DTO 분리</h3>
<p>두 DTO의 형태가 비슷하거나 아예 같을 때도 있는데,
그 요구사항은 수시로 바뀔 수 있습니다.
그리고 보통은 두 DTO가 달라져 나누게 되는 상황이 되는 경우가 많았습니다.
처음부터 분리하고 시작하는 것도 나쁘지 않은 방법인 것 같습니다.</p>
<p>저의 경우에는 아예 클래스명에 Request와 Response를 붙여 분리해두고,
패키지도 분리해둡니다.</p>
<p><img src="https://velog.velcdn.com/images/solst_ice/post/dd218011-c546-4c10-b9f1-28d2be9c1f80/image.png" alt=""></p>
<p>이렇게 개발 시간을 단축시킬 수 있는 몇가지 저만의 규칙을 적어봤습니다.</p>
<p>어쩌면 위의 규칙들이 성가셔보이고 &#39;굳이 이렇게까지 해야하나?&#39; 싶을 수 있는데,
해보시면 아시겠지만 이런 사소한게 2~3시간 잡아먹게 합니다... 심하면 한달까지 갈 때도 있습니다.
그러기엔 개발자에겐 시간이 생명이니 기반을 단단하게 잡고 가자구요! :)</p>
<p>앞으로 또 개발하면서 추가할만한 내용이 있으면 이 시리즈에 추가하도록 하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[나만의 Spring Boot 규칙들 (1. JPA 편)]]></title>
            <link>https://velog.io/@solst_ice/%EA%B0%9C%EB%B0%9C%EC%9D%84-%EC%A7%84%ED%96%89%ED%95%98%EB%A9%B4%EC%84%9C-%EC%A0%95%ED%95%9C-%EB%82%98%EB%A7%8C%EC%9D%98-Spring-Boot-%EA%B7%9C%EC%B9%99%EB%93%A4-1.-JPA-%ED%8E%B8</link>
            <guid>https://velog.io/@solst_ice/%EA%B0%9C%EB%B0%9C%EC%9D%84-%EC%A7%84%ED%96%89%ED%95%98%EB%A9%B4%EC%84%9C-%EC%A0%95%ED%95%9C-%EB%82%98%EB%A7%8C%EC%9D%98-Spring-Boot-%EA%B7%9C%EC%B9%99%EB%93%A4-1.-JPA-%ED%8E%B8</guid>
            <pubDate>Sun, 08 Dec 2024 13:34:41 GMT</pubDate>
            <description><![CDATA[<p>Spring Boot를 사용하여 개발을 진행하다보니 점점
&#39;이 정도는 아예 규칙으로 정해놓고 써야겠다!&#39; 싶을 정도로
휴먼 에러가 많이 발생하는 경우가 많았습니다.
저 또한 그랬기에, 한번 정리하는 마음으로 휴먼 에러를 최대한 방지하기 위한
저만의 Spring Boot 규칙과 그 이유에 대해서 설명해보고자 합니다.</p>
<h2 id="jpa-편">JPA 편</h2>
<h3 id="1-엔티티-생성-시-allargsconstructor보다-생성자를-직접-작성">1. 엔티티 생성 시 <code>@AllArgsConstructor</code>보다 생성자를 직접 작성</h3>
<p><code>deletedAt 필드가 null이 아니면 삭제되었다</code>를 사용하여
soft delete를 수행하는 간단한 Member 엔티티 입니다.</p>
<pre><code class="language-java">@Entity
@Getter
@Table(name = &quot;member&quot;)
@SQLDelete(sql = &quot;UPDATE member SET deleted_at = current_timestamp WHERE id = ?&quot;)
@AllAgrsConstructor
public class Member extends UuidEntity {

  @Comment(&quot;삭제 일자&quot;)
  private LocalDateTime deletedAt;

  @Comment(&quot;이메일&quot;)
  @Column(nullable = false)
  private String email;

  @Comment(&quot;로그인 비밀번호&quot;)
  @Column(nullable = false)
  private String password;
}</code></pre>
<p>이곳에서 깔끔한 코드로 객체를 생성하기 위해 간단하게 @AllArgsConstructor를 붙여주게 된다면
deletedAt 필드는 엔티티 생명주기에 따라서 JPA가 직접 관리할 필드이지만,
사용자가 직접 수정할 여지가 생기게 됩니다.</p>
<pre><code class="language-java">Member member = new Member(/*deletedAt*/null , &quot;이메일&quot;, &quot;비밀번호&quot;);</code></pre>
<p>그리고 이런 경우는 수없이 많이 생기죠. (ID, createdAt, createdBy, updatedAt, updatedBy...)
따라서 엔티티의 경우엔 원하는 필드만 초기화 해주기 위해서 따로 생성자를 만들어주는 것이 좋았습니다.</p>
<pre><code class="language-java">@Entity
@Getter
@Table(name = &quot;member&quot;)
@SQLDelete(sql = &quot;UPDATE member SET deleted_at = current_timestamp WHERE id = ?&quot;)
public class Member extends UuidEntity {

  // 나머지...

  public Member(String email, String password) {
    this.email = email;
    this.password = password;
  }
}</code></pre>
<pre><code class="language-java">Member member = new Member(&quot;이메일&quot;, &quot;비밀번호&quot;);</code></pre>
<h3 id="2-엔티티의-기본-생성자의-공개-범위를-protected로-두어-직접-사용할-수-없도록-함">2. 엔티티의 기본 생성자의 공개 범위를 Protected로 두어 직접 사용할 수 없도록 함</h3>
<p>아까의 Member 엔티티입니다.</p>
<pre><code class="language-java">@Entity
@Getter
@Table(name = &quot;member&quot;)
public class Member extends UuidEntity {

  @Comment(&quot;이메일&quot;)
  @Column(nullable = false)
  private String email;

  @Comment(&quot;로그인 비밀번호&quot;)
  @Column(nullable = false)
  private String password;
}</code></pre>
<p>사실 이 상태에서는 Spring Boot가 실행되지 않습니다.
엔티티에는 기본 생성자가 꼭 필요합니다.
그래서 보통은 <code>@NoArgsConstructor</code>를 붙여줍니다.</p>
<pre><code class="language-java">@Entity
@Getter
@Table(name = &quot;member&quot;)
@NoArgsConstructor
public class Member extends UuidEntity {
  // 나머지...
}</code></pre>
<p>하지만 이러면 어느 곳에서든지 기본 생성자를 사용할 수 있게되죠.
따로 규칙을 명시해두지 않았다면 다른 개발자분이 보고 @Setter를 붙이기 딱 좋은 모습입니다.
<em>Setter는 유명한 안티패턴이라는거 아시죠?</em></p>
<pre><code class="language-java">Member member = new Member();
member.setEmail(&quot;이메일&quot;);</code></pre>
<p>따라서 특별한 경우가 아니면 <code>@NoArgsConstructor</code>에 <code>AccessLevel.PROTECTED</code>를 붙여줍니다.
(private은 JPA도 사용하지 못하게 합니다.)
코드 자체에서 Setter가 아닌 생성자를 사용하도록 유도할 수 있습니다.</p>
<pre><code class="language-java">@Entity
@Getter
@Table(name = &quot;member&quot;)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends UuidEntity {
  // 나머지...
}</code></pre>
<h3 id="3-양방향-매핑은-최대한-지양">3. 양방향 매핑은 최대한 지양</h3>
<p>객체간 순환참조는 개발을 하는 데에 있어서 악입니다. <em>진짜로.</em>
그리고 엔티티의 양방향 매핑은 순환참조를 만들기에 딱 좋고, 예상치 못한 동작을 만들게 됩니다.</p>
<p>아무리 연관관계 편의 메서드를 사용한다고 하더라도
엔티티간 멤버 필드의 불일치가 존재하기 마련입니다.</p>
<p>특히 <strong>삭제가 아닌 연관관계를 해제</strong>할 때 발생합니다.
제가 경험했던 현상을 간단하게 보여드리겠습니다.</p>
<p>아래는 Client(고객사, 일)와 Member(고객, 다)를 양방향 매핑으로 연결한 예시입니다.</p>
<pre><code class="language-java">@Entity
@Table(name = &quot;member&quot;)
public class Member extends UuidEntity {

  @Comment(&quot;이메일&quot;)
  private String email;

  @Comment(&quot;로그인 비밀번호&quot;)
  private String password;

  @Comment(&quot;고객사&quot;)
  @ManyToOne(fetch = FetchType.LAZY)
  private Client client;
}</code></pre>
<pre><code class="language-java">@Entity
@Table(name = &quot;client&quot;)
public class Client extends BaseEntity {

  @Comment(&quot;이름&quot;)
  private String name;

  @OneToMany(mappedBy = &quot;client&quot;, orphanRemoval = true, cascade = CascadeType.ALL)
  private List&lt;Member&gt; members;
}</code></pre>
<p>자동 영속화를 위해 <code>CascadeType.ALL</code>과 간편한 삭제를 위해 <code>orphanRemoval</code>을 주었습니다.
<em>(이거에 대해서는 내용이 많이 복잡합니다! 잘 모르겠으면 일단 넘어가주세요.)</em></p>
<p>1번 멤버를 A고객사에서 B고객사로 변경하려고 합니다.
그러면 Member에서는 Client B를 조회한 후 client 필드에 B를 할당해주기만 하면 됩니다.</p>
<pre><code class="language-java">member.changeClient(clientB);</code></pre>
<p>그리고 양방향 매핑시에는 Client 쪽에서는 멤버 필드의 무결성을 위해
<strong>Client A의 List에서는 Member를 제거하고</strong>
Client B의 List에서는 Member를 더해주어야 합니다.</p>
<pre><code class="language-java">clientA.removeMember(member);
clientB.addMember(member);</code></pre>
<p>이때 Client A의 List에서 Member를 제거하는 동작이,
orphanRemoval에 의해서 &quot;이동&quot;이 아닌 &quot;삭제&quot;로 판단될 때가 있습니다.
(물론 영속화 과정을 꿰고 있다면 괜찮겠지만 조금만 복잡해져도 알기가 어렵습니다.)
이동만 하려 했는데 삭제가 되어버리는 것이죠.</p>
<p>그리고 이 현상은 컴파일 시점에 잡아낼 수도 없으며, 예측하기 어려울 뿐만 아니라,
심지어 데이터베이스를 사용하지 않는 단위 테스트에서도 잡아내기 어렵습니다.</p>
<p>물론 실무에서는 이 예시 말고도 예측하기 어려운 상황은 더 많습니다.
따라서 저는 특히나 양방향 매핑을 사용하지 않으려고 합니다.
<strong>데이터베이스의 구조와 엔티티의 구조가 가장 유사할 때</strong> 오류를 최소화 할 수 있다고 생각합니다.</p>
<blockquote>
<p>&quot;그럼 쿼리 메서드와 엔티티를 통한 간편한 조회가 힘들텐데요!&quot;</p>
</blockquote>
<p>하실 수도 있습니다. 이해합니다.
실제로 양방향 매핑을 사용하다가 단방향으로 전환하면 꽤 귀찮습니다.
구체적으로 이런게 안됩니다.</p>
<pre><code class="language-java">// Client의 모든 Member 가져오기
List&lt;Member&gt; members = client.getMembers();</code></pre>
<pre><code class="language-java">public interface ClientRepository extends JpaRepository&lt;Client, Long&gt; {
  // Client의 Member 중 특정 이메일을 가진 Member가 존재하는 Client 찾기
  Optional&lt;Client&gt; findByMembers_Email(String email);
}</code></pre>
<p>하지만 모두 Repository를 사용해서 대체할 수 있습니다.</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
  List&lt;Member&gt; findAllByClient(Client client);
}

// Client의 모든 Member 가져오기
List&lt;Member&gt; members = memberRepository.finAllByClient(client);</code></pre>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
  Member findByEmail(String email);
}

// Client의 Member 중 특정 이메일을 가진 Member가 존재하는 Client 찾기
Member member = memberRepository.findByEmail(&quot;이메일&quot;);
Client client = member.getClient();</code></pre>
<p>물론 Repository로만 처리하기엔 복잡한 상황이 있을 수 있습니다.</p>
<p>따라서 저는 간단한 쿼리문을 작성할 때에는 쿼리메서드를 사용하고,
검색과 같이 조건이 많은 복잡한 쿼리가 필요할 때는
고민 말고 JPQL 또는 QueryDSL을 사용하여 쿼리메서드를 만듭니다.
그 중에서도 JPQL을 권장합니다.</p>
<p>QueryDSL은 아직 빌드도구 간 연동 오류가 많이 발생합니다.
웬만하면 JPQL로 될텐데, 정 안된다면 Native Query를 사용합니다.
QueryDSL로 되는거면 JPQL로도 될 것입니다.
오히려 QueryDSL 문법을 학습하기보단 SQL과 구조가 비슷한 JPQL이 더 학습에 도움이 될 것입니다.</p>
<hr>
<p>원래는 한 게시물에 모두 적으려 했으나, JPA만 해도 꽤 길어졌네요.😅
다음 게시물에서 Spring boot 자체에 대한 규칙도 정리해보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java 프로젝트에서도 Prettier를 사용해보자]]></title>
            <link>https://velog.io/@solst_ice/Java-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C%EB%8F%84-Prettier%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@solst_ice/Java-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C%EB%8F%84-Prettier%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 08 Dec 2024 12:17:20 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/solst_ice/post/8b2ad1ae-e1c7-4249-a8d0-61389d8aa433/image.png" alt=""></p>
<h2 id="prettier">Prettier?</h2>
<p>Prettier는 node 진영에서 코드 포맷을 포맷팅 하기 위해 사용하는 라이브러리입니다.
스캔 대상에 등록된 파일 전체를 스캔하여 설정한 코드 포맷 규칙에 따라서 코드를 포맷팅 해줍니다.</p>
<p>물론, 이 라이브러리는 포맷팅에 node를 사용하기에 보통 Javascript 프로젝트에만 사용되지만,
Java 언어를 포맷팅 해주는 <code>prettier-java</code> 플러그인이 존재하기에, Java 프로젝트에도 적용해보도록 하겠습니다.</p>
<h2 id="prettier-java-설치">Prettier-Java 설치</h2>
<p>Java 프로젝트가 있는 곳에서 아래 명령어를 실행하여 <code>prettier</code>와 <code>prettier-plugin-java</code>를 설치해줍니다.</p>
<pre><code class="language-bash">npm install prettier prettier-plugin-java</code></pre>
<p>그 후, 프로젝트 루트 디렉토리에 <code>.prettierrc.json</code> 파일을 만들어 플러그인을 적용시킴과 함께 포맷팅 규칙을 설정해줍니다.</p>
<pre><code class="language-json">{
  &quot;plugins&quot;: [&quot;prettier-plugin-java&quot;],
  &quot;printWidth&quot;: 120
}</code></pre>
<blockquote>
<p>만약 git을 사용하고 있다면,
Java 프로젝트를 만든 후 node_modules 디렉토리가 ignore 되어있지 않을테니<br>꼭 .gitignore에 추가해줍시다.</p>
</blockquote>
<h2 id="prettier-java-활성화">Prettier-Java 활성화</h2>
<p>설치 후 사용하는 IDE에서 Prettier를 활성화 해주어야합니다.
워낙 유명한 라이브러리이다보니 웬만한 IDE에서는 Prettier를 지원해줍니다.
다만, Java에서 사용하려면 약간 설정을 해주어야 합니다.
IntelliJ를 기준으로 설명합니다.</p>
<ol>
<li>[설정 &gt; 도구 &gt; 저장 시 액션] 에서 <code>Prettier 실행</code>에 체크 표시를 해준 후 마우스를 올려 <code>구성...</code>에 들어갑니다.
<img src="https://velog.velcdn.com/images/solst_ice/post/c655bc52-2aaf-416e-aafc-1343ca1f7efd/image.png" alt=""></li>
<li><code>자동 Prettier 구성</code>을 선택해주고,
<code>다음 파일에 대해 실행:</code> 부분에 있는 확장자들을 모두 지워서 <code>**/*</code>가 되도록 합니다.
(기본 설정은 Javascript 언어에 대해서만 실행하기 때문에 Java 파일에서도 적용되도록 한 것입니다.)
<img src="https://velog.velcdn.com/images/solst_ice/post/990c4200-9a9a-44c5-a658-c22ec2ab8c79/image.png" alt=""></li>
</ol>
<p>완료가 되었다면, 기존 파일에서 줄바꿈 등 변화를 준 뒤 저장하여 포맷팅이 제대로 되는지 확인할 수 있습니다.</p>
<p>포맷팅이 적용되는것을 확인했다면, 아래 명령어를 사용하여 모든 파일에 대하여 Prettier 포맷팅을 적용하도록 할 수 있습니다.</p>
<pre><code class="language-bash">npx prettier --write &quot;**/*&quot;</code></pre>
<hr>
<h2 id="번외-포맷팅-잘-적용되었는지-github-action으로-검증하기">번외) 포맷팅 잘 적용되었는지 Github Action으로 검증하기</h2>
<p><code>.github/workflows/prettier-check.yml</code> 파일에 아래와 같이 작성하여<br>PR을 올릴 때 자동으로 Prettier가 잘 적용되었는지 검증할 수 있습니다.</p>
<pre><code class="language-yaml">name: Prettier Format Check

on:
  pull_request:
    branches: [&quot;develop&quot;]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: &quot;20&quot;

      - name: Install dependencies
        run: npm install

      - name: Run Prettier check
        run: npm run prettier:check

      - name: Fail if unformatted code is found
        if: failure()
        run: exit 1</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Javascript에서 var와 let의 가장 큰 차이, 범위(Scope)]]></title>
            <link>https://velog.io/@solst_ice/Javascript%EC%97%90%EC%84%9C-var%EC%99%80-let%EC%9D%98-%EA%B0%80%EC%9E%A5-%ED%81%B0-%EC%B0%A8%EC%9D%B4-%EB%B2%94%EC%9C%84Scope</link>
            <guid>https://velog.io/@solst_ice/Javascript%EC%97%90%EC%84%9C-var%EC%99%80-let%EC%9D%98-%EA%B0%80%EC%9E%A5-%ED%81%B0-%EC%B0%A8%EC%9D%B4-%EB%B2%94%EC%9C%84Scope</guid>
            <pubDate>Sun, 08 Dec 2024 11:53:47 GMT</pubDate>
            <description><![CDATA[<h3 id="문제의-시작">문제의 시작</h3>
<pre><code class="language-javascript">if (roles.includes(&quot;ROLE_ADMIN&quot;)) {
  $(&quot;#ip1&quot;).parent().parent().hide();
  for (var i = 2; i &lt;= a; i++) {
    $(&quot;#ip&quot; + i)
      .parent()
      .parent()
      .remove();
  }
} else {
  $(&quot;#ip1&quot;).parent().parent().show();
  for (var i = 2; i &lt;= a; i++) {
    $(&quot;#ip&quot; + i)
      .parent()
      .parent()
      .remove();
  }
}</code></pre>
<p>회사에서 이런식으로 한 블록 내에 두개의 for 문을 써야할 때가 있었다.
호환성을 위해 ES5 문법을 써야했고, 나는 자연스럽게 for문 하나당 var를 사용하여 i를 선언했다.</p>
<p>그랬더니 아래와 같은 경고가 뜨는것이 아닌가!
<img src="https://velog.velcdn.com/images/solst_ice/post/a76b3124-6b27-42af-ba6d-92256b85ab3f/image.png" alt=""></p>
<p>오잉 난 분명 블록 범위 내에 다른 변수를 선언한건데… 하고 찾아보니 var는 다른 언어와 같이 블록 범위로 사용되지 않는다는 것을 알 수 있었다.</p>
<h3 id="var-그리고-let과-const">var 그리고 let과 const</h3>
<p>ES6에서 추가된 문법중에 let이 있는데, let은 우리가 익히 알고 있는 것과 같이 블록 범위를 가지고, var는 함수 범위를 가진다고 한다.
그러니까, var로 선언된 변수는 해당 변수가 만들어진 함수 전체에 적용된다는 뜻.
따라서 위의 코드에 var 대신 let을 사용하면 경고가 사라진다.</p>
<h3 id="그럼-var는-왜">그럼.. var는 왜?</h3>
<p>그러면 var는 왜 이렇게 사용되고 설계되었을까 갑자기 궁금해져서 찾아보았다.</p>
<p>먼저, var는 JavaScript의 초기 버전에서 변수를 선언하는 유일한 키워드였다.
이 당시 var는 함수 범위를 가지는 것이 자연스러웠다.</p>
<p>왜냐하면, 당시 JavaScript는 간단한 웹 페이지 상호 작용을 위한 언어로 사용되었고,
함수 범위의 변수는 이러한 목적에 적합했다.
대부분의 로직은 함수 안에서 처리되고, 이벤트 리스너나 단순한 스크립트 코드로 되어있었다.</p>
<p>그러나 웹 애플리케이션의 복잡성이 증가하고, 블록 범위의 변수가 필요한 상황이 많아지면서
함수 범위의 변수만으로는 코드 관리가 어려워졌다.</p>
<p>따라서 ES6에서는 블록 범위의 변수를 선언할 수 있는 &#39;let&#39;과 &#39;const&#39; 키워드가 도입되었다.
이로써 개발자들은 변수의 스코프를 더 세밀하게 관리할 수 있게 되었다.</p>
<h3 id="결론적으로">결론적으로,</h3>
<p>var 키워드를 사용하게 된다면 이런 특성을 가진다는 것을 유의하자.</p>
<ul>
<li>함수 내에서 선언된 변수는 <strong>함수 전체</strong>에서 사용할 수 있다. 비록 그것이 블록 안에서 선언되었더라도.</li>
<li>변수는 선언 위치와 관계없이 호이스팅되어 함수의 시작 부분에서 선언된다. 변수에 값이 할당되지 않으면 undefined로 초기화된다.</li>
<li>함수 외부에서 선언된 변수는 전역 변수로 간주되며, 전역 범위에서 사용할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[git status를 사용할 때 한글이 깨져나올 때]]></title>
            <link>https://velog.io/@solst_ice/git-status%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%A0-%EB%95%8C-%ED%95%9C%EA%B8%80%EC%9D%B4-%EA%B9%A8%EC%A0%B8%EB%82%98%EC%98%AC-%EB%95%8C</link>
            <guid>https://velog.io/@solst_ice/git-status%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%A0-%EB%95%8C-%ED%95%9C%EA%B8%80%EC%9D%B4-%EA%B9%A8%EC%A0%B8%EB%82%98%EC%98%AC-%EB%95%8C</guid>
            <pubDate>Sun, 08 Dec 2024 11:44:15 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/solst_ice/post/d55cc435-3f7d-4979-8cf4-ece8d6065ae8/image.png" alt=""></p>
<p>사진에서 보이는 것과 같이 한글이 저런식으로 깨져서 나온다.
어떻게 검색해야 할 지도 몰라서 망설이다가 개발자 디스코드 방이 질문했더니 친절하게 답변해주셨다.</p>
<pre><code class="language-bash">git config --global core.quotepath false</code></pre>
<blockquote>
<p>git이 0x80 이상의 character 값을 핸들링할 때 해당 옵션이 true이면 unusual value로 취급해서 제대로 표현 안하고 유니코드 값으로 출력한다고 합니다<br>false로 해줘서 이것도 문자다~ 라고 알려주는 거에요</p>
</blockquote>
<p>...라고 하셨는데 뭔가 한글을 정상 문자라고 표시해준다는 내용인 것 같다.</p>
<p>아무튼
<img src="https://velog.velcdn.com/images/solst_ice/post/f521e787-4e23-4ebf-bfa6-5e3a1ad724e0/image.png" alt=""></p>
<p>해결~!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[외부에서 WSL에 접근할 수 있도록 하기]]></title>
            <link>https://velog.io/@solst_ice/%EC%99%B8%EB%B6%80%EC%97%90%EC%84%9C-WSL%EC%97%90-%EC%A0%91%EA%B7%BC%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8F%84%EB%A1%9D-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@solst_ice/%EC%99%B8%EB%B6%80%EC%97%90%EC%84%9C-WSL%EC%97%90-%EC%A0%91%EA%B7%BC%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8F%84%EB%A1%9D-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 08 Dec 2024 11:42:31 GMT</pubDate>
            <description><![CDATA[<p>최근 StableDiffusion을 사용하기 위해 윈도우에 WSL을 설치하고 외부에서 WSL에 접근을 할 수 있는 환경을 만들려고 했다.
그래서 신나게 윈도우에 <code>wsl --install</code>을 해주고 <code>openssh-server</code>도 설치를 해주었다.
그리고 나서 아이피를 확인해봤다.</p>
<pre><code class="language-bash">ip addr show</code></pre>
<p>그랬더니 아이피가 172.XXX로 시작하는거 아닌가…
이거 느낌이 쎄했다. 분명 집 인터넷은 192.XXX로 시작하는데…
혹시나 해서 172.XXX로 연결을 시도해봤지만 역시 연결이 되지 않았다.</p>
<p>이게 처음 WSL을 설치하면 기본적으로 네트워크가 NAT으로 설정된다고 한다.
쉽게 말해서 윈도우가 192.XXX로 들어오는 네트워크를 172.XXX로 시작하는 네트워크로 나눠서 WSL에 할당해주고 있다는 뜻이다.
그러면 해결방법은 포트포워딩.
192.XXX로 들어오는 22번 포트를 172.XXX의 22번 포트에게 할당해주면 된다.</p>
<p>파워쉘을 관리자 권한으로 실행하여 아래의 명령어를 입력한다.</p>
<pre><code class="language-powershell">netsh interface portproxy add v4tov4 listenport=&#39;외부에서_접속할_포트&#39; listenaddress=0.0.0.0 connectport=22 connectaddress=172.XXX</code></pre>
<p>그러면 윈도우가 <code>외부에서_접속할_포트</code> 로 들어온 요청들은 모두 WSL의 22번 포트로 보내줄 것이다.
<img src="https://velog.velcdn.com/images/solst_ice/post/a1aed5cf-ff91-430d-b73b-2a3f3ad888e3/image.png" alt=""></p>
<p>성공~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[정보처리기사 기출문제 오답노트]]></title>
            <link>https://velog.io/@solst_ice/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%EA%B8%B0%EC%B6%9C%EB%AC%B8%EC%A0%9C-%EC%98%A4%EB%8B%B5%EB%85%B8%ED%8A%B8</link>
            <guid>https://velog.io/@solst_ice/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%EA%B8%B0%EC%B6%9C%EB%AC%B8%EC%A0%9C-%EC%98%A4%EB%8B%B5%EB%85%B8%ED%8A%B8</guid>
            <pubDate>Sun, 08 Dec 2024 05:12:38 GMT</pubDate>
            <description><![CDATA[<h2 id="디지털-저작권-관리drm를-위한-구성요소">디지털 저작권 관리(DRM)를 위한 구성요소</h2>
<ul>
<li><strong>콘텐츠 제공자</strong>: 콘텐츠를 제공하는 저작권자</li>
<li><strong>콘텐츠 분배자</strong>: 쇼핑몰 등으로써 암호화된 콘텐츠 제공</li>
<li><strong>패키저</strong>: 콘텐츠를 메타데이터와 함께 배포 가능한 단위로 묶는 기능</li>
<li><strong>보안 컨테이너</strong>: 원본을 안전하게 유통하기 위한 전자적 보안 장치</li>
<li><strong>DRM 컨트롤러</strong>: 배포된 콘텐츠의 이용 권한을 통제</li>
<li><strong>클리어링 하우스</strong>: 디지털 라이선싱 중계 및 발급을 수행하는 정산소</li>
</ul>
<h2 id="객체지향-설계-원칙-solid">객체지향 설계 원칙 (SOLID)</h2>
<ul>
<li><strong>Single Responsibility, 단일 책임의 원칙</strong>: 하나의 클래스는 하나의 목적을 위해서 생성</li>
<li><strong>Open Close, 개방 페쇄 원칙</strong>: 소프트웨어 구성요소는 확장에는 열려있고 변경에는 닫혀있음</li>
<li><strong>Liskov Substitution, 리스코프 치환의 원칙</strong>: 상속받은 하위 클래스는 어디서나 자신의 상위 클래스로 교체할 수 있어야 함</li>
<li><strong>Interface Segregation, 인터페이스 분리의 원칙</strong>: 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 함</li>
<li><strong>Dependency Inversion, 의존성 역전의 원칙</strong>: 클래스 앞에 인터페이스를 두어 인터페이스는 그대로 두고 클래스는 언제든 바꿀 수 있도록 만듦</li>
</ul>
<h2 id="방향그래프">방향그래프</h2>
<ul>
<li>정점을 연결하는 선에 방향이 있는 그래프</li>
<li>n개의 정점이 있으면 최대 간선 수는 n(n-1)개</li>
</ul>
<h2 id="지역성">지역성</h2>
<ul>
<li><strong>시간 지역성</strong><ul>
<li>최근 사용되었던 기억 장소들이 집중적으로 엑세스함</li>
<li>참조했던 메모리는 빠른 시간에 다시 참조될 확률이 높다</li>
</ul>
</li>
<li><strong>공간 지역성</strong><ul>
<li>일정 위치의 페이지를 집중적으로 엑세스함</li>
</ul>
</li>
<li><strong>순차 지역성</strong><ul>
<li>데이터가 순차적으로 엑세스함</li>
</ul>
</li>
</ul>
<h2 id="온라인-분석-처리-olap">온라인 분석 처리 (OLAP)</h2>
<p>데이터 웨어하우스의 데이터를 전략적인 정보로 변환시켜서 의사결정을 지원하는 역할을 하는 시스템</p>
<h2 id="시맨틱-웹">시맨틱 웹</h2>
<p>리소스에 대한 정보와 자원 사이의 관계-의미 정보를 컴퓨터가 처리할 수 있는 온톨로지 형태로 표현하고 이를 컴퓨터가 처리하도록 하는 지능형 웹</p>
<h2 id="온톨로지">온톨로지</h2>
<p>실세계에 존재하는 모든 개념과 개념들의 속성, 개념관의 관계 정보를 컴퓨터가 이해할 수 있도록 서술해 놓은 개념화 명세서</p>
<h2 id="다단계-피드백-큐-mlfq">다단계 피드백 큐 (MLFQ)</h2>
<ul>
<li>FIFO와 라운드 로빈 스케줄링 기법을 혼합</li>
<li>새로운거 높은 우선순위 / 실행시간이 길어질수록 낮은 우선순위 / 마지막 단계는 라운드 로빈 방식</li>
</ul>
<h2 id="자료사전-기호">자료사전 기호</h2>
<ul>
<li><code>-</code>: 자료의 정의, &#39;~로 구성되어 있다&#39;는 것을 나타냄</li>
<li><code>+</code>: 자료의 연결을 나타냄</li>
<li><code>()</code>: 생략 가능</li>
<li><code>{}</code>: 반복</li>
<li><code>[]</code>: 여러개 중에 선택</li>
<li><code>**</code>: 주석</li>
</ul>
<h2 id="osi-7계층">OSI 7계층</h2>
<ul>
<li><strong>물리</strong>: 전기적 신호 변환</li>
<li><strong>데이터링크</strong>: 시스템간 연결, 동기화</li>
<li><strong>네트워크</strong>: 최적화된 경로 제공</li>
<li><strong>전송</strong>: 프로세스 간 신뢰성 있는 연결 보장</li>
<li><strong>세션</strong>: 송수신 간 논리적 연결</li>
<li><strong>표현</strong>: 암,복호화</li>
<li><strong>응용</strong>: 네트워크와 응용프로그램 연결</li>
</ul>
<h2 id="일정관리-모델">일정관리 모델</h2>
<ul>
<li><strong>주 공정법</strong>: 프로젝트의 시작과 끝을 나타내는 노드와 노드간의 연결로 공정 계산</li>
<li><strong>PERT</strong>: 비관치, 중간치, 낙관치 3점 추정방식</li>
<li><strong>중요 연쇄 프로젝트 관리</strong>: 주 공정법에서 자원제약사항까지 고려</li>
</ul>
<h2 id="라우팅-프로토콜">라우팅 프로토콜</h2>
<ul>
<li><strong>RIP</strong>: Bellman-Ford 알고리즘 사용</li>
<li><strong>OSPF</strong>: 다익스트라 알고리즘 사용</li>
<li><strong>BGF</strong>: 경로 벡터 알고리즘 사용</li>
</ul>
<h2 id="디지털-포렌식">디지털 포렌식</h2>
<p>범죄행위에 대한 사실을 사법기관에 제출하기 위해 디지털 증거자료를 획득, 분석, 보관, 제출, 기록하는 일련의 과정</p>
<h2 id="개발환경-인프라-구성-방식">개발환경 인프라 구성 방식</h2>
<ul>
<li><strong>온프레미스</strong>: 외부 인터넷망이 차단된 상태에서 인트라넷 망만을 활용하여 개발환경을 구축하는 방식</li>
<li><strong>클라우드</strong>: 클라우드 공급 서비스를 하는 회사들의 서비스를 임대하여 개발환경을 구축하는 방식</li>
<li><strong>하이브리드</strong>: 온프레미스와 클라우드를 혼용하는 방식</li>
</ul>
<h2 id="분석-자동화case-도구">분석 자동화(CASE) 도구</h2>
<ul>
<li><strong>상위 CASE</strong>: 요구사항 분석, 소프트웨어 설계 등 초기 개발 단계에서 사용하는 도구</li>
<li><strong>하위 CASE</strong>: 코드생성, 디버깅, 테스트 등 구현 및 유지 보수 단계에서 사용하는 도구</li>
</ul>
<h2 id="ipv6">IPv6</h2>
<p>IPv4가 가지고 있는 주소 고갈, 보안성, 이동성 지원 등의 문제점을 해결하기 위해 개발된 128bit 주소체계를 갖는 인터넷 프로토콜</p>
<h2 id="coap-constranined-application-protocol">CoAP (Constranined Application Protocol)</h2>
<ul>
<li>제약이 있는 장치들을 위한 특수한 인터넷 애플리케이션 프로토콜</li>
<li>HTTP로 쉽게 변환되도록 설계됨</li>
</ul>
<h2 id="메모리의-외부-단편화를-해결하는-기법">메모리의 외부 단편화를 해결하는 기법</h2>
<ul>
<li><strong>버디 메모리 할당</strong>: 요청한 프로세스 크기에 가장 알맞은 크기를 할당하기 위해 메모리를 2^n의 크기로 분할하여 메모리를 할당하는 기법</li>
<li><strong>통합</strong>: 인접한 단편화 영역을 찾아 하나로 통합하는 기법</li>
<li><strong>압축</strong>: 메모리의 모든 단편화 영역을 하나로 압축하는 기법</li>
</ul>
<h2 id="hipo-차트-종류">HIPO 차트 종류</h2>
<ul>
<li><strong>가시적 도표</strong>: 시스템의 전체적인 기능과 흐름을 보여주는 계층 구조도</li>
<li><strong>총체적 도표</strong>: 프로그램을 구성하는 기능을 기술한 것으로 입력, 처리, 출력에 대한 전반적인 정보를 제공하는 도표</li>
<li><strong>세부적 도표</strong>: 총체적 도표에 표시된 기능을 구성하는 기본 요소들을 상세히 기술하는 도표</li>
</ul>
<h2 id="itil정보기술-인프라-라이브러리">ITIL(정보기술 인프라 라이브러리)</h2>
<p>IT 서비스의 운영 및 관리를 돕기 위한 문서들의 집합</p>
<h2 id="커널의-유형">커널의 유형</h2>
<ul>
<li>마이크로 커널: 장치 드라이버, 프로토콜 스택, 파일 시스템과 같은 전통적인 OS의 기능들을 사용자 영역에 놓고 하드웨어 추상화를 최소화한 커널</li>
<li>모놀리식 커널: 하프로세스 관리, 동시성 관리, 메모리 관리 등을 관리자 모드에서 작동하여 사용자에게 고수준의 플랫폼을 제공하는 커널</li>
</ul>
<h2 id="테스트의-종류">테스트의 종류</h2>
<ul>
<li>단위 테스트: 사용자 요구사항에 대한 단위 모듈, 서브루틴 등을 테스트하는 단계</li>
<li>통합 테스트: 단위 테스트를 통과한 모듈 사이의 인터페이스, 통합된 컴포넌트 간의 상호작용을 검증하는 테스트 단계</li>
<li>시스템 테스트: 통합된 단위 시스템의 기능이 시스템에서 정상적으로 수행되는지를 검증하는 테스트 단계</li>
<li>인수 테스트: 계약상의 요구사항을 만족했는지 확인하기 위한 테스트 단계</li>
</ul>
<h2 id="프로세스-스케줄링">프로세스 스케줄링</h2>
<ul>
<li>선점형 스케줄링<ul>
<li>우선순위가 높은 프로세스가 현재 프로세스를 중단시키고 CPU를 점유하는 스케줄링 방식</li>
<li>라운드 로빈</li>
</ul>
</li>
<li>비선점형 스케줄링<ul>
<li>한 프로세스가 CPU를 할당받으면 작업이 종료될 때까지 다른 프로세스의 점유가 불가능한 스케줄링 방식</li>
</ul>
</li>
</ul>
<h2 id="화이트박스-테스트-vs-블랙박스-테스트">화이트박스 테스트 VS 블랙박스 테스트</h2>
<ul>
<li>화이트박스 테스트: 응용 프로그램의 내부 구조와 동작을 검사하는 소프트웨어 테스트 방식</li>
<li>블랙박스 테스트: 내부 구조와 동작을 고려하지 않고 결과값 만을 확인하는 테스트 방식</li>
</ul>
<h2 id="메모리-반입-기법">메모리 반입 기법</h2>
<ul>
<li>예상 반입 기법: 시스템의 요구를 예측하여 미리 메모리에 적재하는 방법</li>
<li>요구 반입 기법: 다음에 실행될 프로세스가 참조 요구가 있을 경우에 적재하는 기법</li>
</ul>
<h2 id="ddos-공격-방법">DDOS 공격 방법</h2>
<ul>
<li>Slow HTTP Header DoS: HTTP GET 메서드를 사용하여 헤더의 최종 끝을 알리는 개행 문자열을 전송하지 않음으로 웹 서버와 연결 상태를 장시간 지속시킴으로 연결 자원을 모두 소진시키는 서비스 거부 공격</li>
<li>Slow HTTP POST DoS: 요청 헤더의 Content-Length를 비정상적으로 크게 설정하여 메시지 바디 부분을 매우 소량으로 보내 계속 연결 상태를 유지시키는 공격 기법</li>
<li>Slow Read Attack: TCP 윈도 크기와 데이터 처리율을 감소시킨 상태에서 다수 HTTP 패킷을 지속적으로 전송하여 계속 연결 상태를 유지시키는 공격 기법</li>
</ul>
<hr>
<h2 id="보안">보안</h2>
<h3 id="해킹-공격용-도구">해킹 공격용 도구</h3>
<ul>
<li>루트킷: 시스템 침입 후 지속적으로 시스템을 장악하기 위한 기능을 제공하는 프로그램의 모음</li>
<li>크라임웨어: 불법적인 행위를 수행하기 위해 제작된 컴퓨터 프로그램</li>
</ul>
<h3 id="암호화-알고리즘">암호화 알고리즘</h3>
<ul>
<li>DES: 1975년 표준 기술로 발표한 대칭키 블록 알고리즘, 블록 크기 64bit</li>
<li>AES: 2001년 표준 기술로 발표한 대칭키 블록 알고리즘, 블록 크기 128bit</li>
</ul>
<h3 id="입력-데이터-검증-및-표현에-대한-취약점">입력 데이터 검증 및 표현에 대한 취약점</h3>
<ul>
<li>XSS(크로스 사이트 스크립트): 검증되지 않은 외부 입력 데이터가 포함된 웹 페이지가 전송되는 경우, 해당 웹 페이지를 열람함으로써 웹 페이지에 포함된 스크립트가 실행되는 공격</li>
<li>CSRF(사이트 간 요청 위조): 자신의 의지와는 무관하게 공격자가 의도한 행위를 특정 웹사이트에 요청하게 하는 공격</li>
<li>SQL 인젝션: 응용프로그램의 보안 취약점을 이용해서 악의적인 SQL 구문을 삽입, 실행시켜서 정보를 탈취하거나 조작하는 공격</li>
</ul>
<hr>
<h2 id="데이터베이스">데이터베이스</h2>
<h3 id="정규화">정규화</h3>
<ul>
<li><strong>1차</strong>: 한 도메인에 하나의 값 (&quot;서울,부산&quot; x)</li>
<li><strong>2차</strong>: 부분 함수 종속 제거</li>
<li><strong>3차</strong>: 이행함수 종속 제거</li>
<li><strong>보이스-코드</strong>: 결정자 함수이면서 후보키가 아닌 것 제거</li>
<li><strong>4차</strong>: 다치 종속성 제거</li>
<li><strong>5차</strong>: 조인 종속성 제거</li>
</ul>
<h3 id="병행-제어-기법">병행 제어 기법</h3>
<ul>
<li><strong>로킹(Locking)</strong>: 같은 자원을 엑세스 할 때 데이터를 잠가(Lock)서 순차적 진행을 보장</li>
<li><strong>낙관적 검증</strong>: 트랜잭션이 어떠한 검증도 수행하지 않고 일단 수행한 뒤, 종료시에 검증을 수행하여 데이터베이스에 반영</li>
<li><strong>타임 스탬프 순서</strong>: 트랜잭션을 실행하기 전에 타임 스탬프를 부여하여 시간에 따라 트랜잭션 작업 수행</li>
<li><strong>다중버전 동시성 제어</strong>: 트랜잭션의 타임스탬프와 데이터의 타임스탬프를 비교하여 직렬가능성이 보장되는 적절한 버전을 선택하여 접근</li>
</ul>
<h3 id="병행-제어-미보장-시-문제점">병행 제어 미보장 시 문제점</h3>
<ul>
<li>갱신 손실: 먼저 실행된 트랜잭션의 결과를 나중에 실행된 트랜잭션이 덮어쓸 때 발생하는 오류</li>
<li>현황 파악오류: 트랜잭션의 중간 수행 결과를 다른 트랜잭션이 참조하여 발생하는 오류</li>
<li>모순성: 두 트랜잭션이 동시에 실행되어 데이터베이스의 일관성이 결여되는 오류</li>
<li>연쇄복귀: 복수의 트랜잭션이 데이터 공유 시 특정 트랜잭션이 처리를 취소할 경우 트랜잭션이 처리한 곳의 부분을 취소하지 못하는 오류</li>
</ul>
<h3 id="데이터베이스-회복-기법">데이터베이스 회복 기법</h3>
<ul>
<li><strong>로그 기반 회복 기법</strong><ul>
<li><strong>지연 갱신 회복 기법</strong>: 트랜잭션이 완료되기 전까지 데이터베이스에 기록하지 않음</li>
<li><strong>즉각 갱신 회복 기법</strong>: 트랜잭션 수행 중 갱신 결과를 바로 DB에 반영</li>
</ul>
</li>
<li><strong>체크 포인트 회복 기법</strong>: 장애 발생 시 검사점 이후에 처리된 트랜잭션에 대해서만 장애 발생 이전의 상태로 복원</li>
<li><strong>그림자 페이징 회복 기법</strong>: 데이터베이스 트랜잭션 수행 시 복제본을 생성하여 데이터베이스 장애 시 이를 이용해 복구</li>
</ul>
<h3 id="데이터베이스-암호화-기법">데이터베이스 암호화 기법</h3>
<ul>
<li>API 방식: 애플리케이션 레벨에서 암호 모듈을 적용하는 애플리케이션 수정 방식</li>
<li>Plug-in 방식: 암,복호화 모듈이 DB 서버에 설치된 방식</li>
<li>TDE 방식: DB 서버의 DBMS 커널이 자체적으로 암,복호화 기능을 수행하는 방식</li>
<li>Hybrid 방식: API 방식과 Plug-in 방식을 결합하는 방식</li>
</ul>
<hr>
<h2 id="sql">SQL</h2>
<h3 id="having">HAVING</h3>
<p>HAVING 구문은 WHERE 구문 내에는 사용할 수 없는 집계 함수의 구문을 적용하여 복수행의 계산 결과를 조건별로 적용하는데 사용</p>
<h3 id="view-생성">View 생성</h3>
<pre><code class="language-sql">-- CREATE VIEW 이름 AS [SELECT문]
CREATE VIEW test_view AS
SELECT * FROM test_table WHERE a = 1;</code></pre>
<h3 id="컬럼-수정-alter">컬럼 수정 (ALTER)</h3>
<pre><code class="language-sql">-- ALTER TABLE 테이블 MODIFY 컬럼 데이터타입 [제약조건];
ALTER TABLE foo MODIFY bar INTEGER PRIMARY KEY;</code></pre>
<h3 id="권한-부여-grant">권한 부여 (GRANT)</h3>
<pre><code class="language-sql">-- GRANT 권한 ON 테이블 TO 사용자 [WITH 권한 옵션];
GRANT ALL ON STUDENT TO SYS WITH GRANT OPTION;</code></pre>
<ul>
<li><code>WITH GRANT OPTION</code>: 사용자가 권한을 받고 난 후 다른 사람들과 권한을 나누어 가질 수 있음</li>
</ul>
<hr>
<h2 id="java">Java</h2>
<ul>
<li><p><code>%x</code>: 16진수로 표시</p>
</li>
<li><p>부모 클래스에 자식 클래스와 동일한 메서드가 있으면, 부모 클래스 타입이어도 자식의 메서드로 오버라이드 됨</p>
<pre><code class="language-java">class Parent {
  void a() {}
}

class Child extends Parent {
  void a() {}
}

Parent p = new Child();
p.a() // Child의 a가 실행됨</code></pre>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS VPC를 이해해보자]]></title>
            <link>https://velog.io/@solst_ice/AWS-VPC%EB%A5%BC-%EC%9D%B4%ED%95%B4%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@solst_ice/AWS-VPC%EB%A5%BC-%EC%9D%B4%ED%95%B4%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 08 Dec 2024 05:09:04 GMT</pubDate>
            <description><![CDATA[<p>AWS를 사용하여 어플리케이션을 배포할 때 VPC의 사용은 거의 필수적이다.
이번 포스팅은 &quot;내가 이해한&quot; VPC와 그 안에서 사용되는 개념들을 정리하고 기록해보려고 한다.</p>
<p><img src="https://velog.velcdn.com/images/solst_ice/post/09b774df-ae2b-4782-9ba8-ec3dc9559960/image.png" alt=""></p>
<p>AWS에 접속하고 VPC 페이지에 들어가면 아래와 같은 많은 서비스들이 있는 것을 볼 수 있다.
지금은 여기서 Virtual Private Cloud 섹션이 있는 것들 중 내가 개발에 사용한 몇가지만 살펴본다.</p>
<ul>
<li>VPC</li>
<li>서브넷</li>
<li>라우팅 테이블</li>
<li>인터넷 게이트웨이</li>
<li>NAT 게이트웨이</li>
<li>네트워크 ACL</li>
<li>보안 그룹</li>
</ul>
<h1 id="vpc-virtual-private-cloud">VPC (Virtual Private Cloud)</h1>
<p>AWS 안에서 사용하는 내부망이라고 생각하면 된다.
이 내부망 안에서 여러개의 애플리케이션(서버)들이 배치가 될 것이고
그 어플리케이션은 VPC 위에서 서로 통신을 하게 된다.</p>
<p>좀 더 자세한 내용을 위해 VPC를 생성해보자.</p>
<h2 id="vpc-생성">VPC 생성</h2>
<p><img src="https://velog.velcdn.com/images/solst_ice/post/c06a9527-5724-4bcb-a387-a03dbf7cf094/image.png" alt=""></p>
<p>VPC를 생성할 때 <code>생성할 리소스</code> 섹션에서 <code>VPC 등</code>을 선택했을 때의 화면이다.<br>VPC만 생성한다 해도 제대로 쓸거면 어차피 다른것들을 기본적으로 다 설정해야한다.<br>사실상 이곳에 기본적인 구조가 모두 나타나 있는데, 하나하나 살펴보도록 하자.<br>이름은 운영 환경에 대한 VPC를 생성한다고 보고, 임의로 <code>production</code> 이라고 지어봤다.</p>
<h3 id="ipv4-cidr-블록">IPv4 CIDR 블록</h3>
<p>CIDR은 간단하게 <code>*.*.*.*/*</code> 형태로 &quot;아이피의 범위&quot;를 나타내는 표기법이다.<br>사진과 같이 <code>10.0.0.0/16</code> 으로 지정하면, <code>10.0.0.0</code> 부터 <code>10.0.255.255</code> 까지 65536개의 아이피를 이 VPC에서 할당할 수 있다는 의미이다.<br><code>/</code> 뒤에 붙는 숫자는 맨 앞부터 고정할 자리의 개수(2진법)를 나타내기 때문에 숫자가 커질수록 VPC 내에서 할당할 수 있는 아이피의 개수가 줄어든다.<br>위의 범위에서 <code>10.0</code> 부분이 고정되어있기 때문에 그 부분이 16이라고 보면 된다.<br>앞으로도 계속 나올테니 이해하고 넘어가자.</p>
<h3 id="가용-영역az-수">가용 영역(AZ) 수</h3>
<p>사용 가능한 영역을 뜻하는데, 앞으로 무중단 서비스 같은 것을 구현하려면 최소 2개의 가용 영역이 필요하다.<br>내가 이해하기로는 하나의 영역이 모종의 이유로 죽으면 다른 가용 영역으로 바로 갈아탈 수 있도록 하기 위함인 것 같다.<br>사진에서 다이어그램을 보면 <code>ap-northeast-2a</code>와 <code>ap-northeast-2b</code>로 두개의 가용 영역이 나눠진 것을 볼 수 있다.</p>
<h3 id="퍼블릭-서브넷-수--프라이빗-서브넷-수">퍼블릭 서브넷 수 / 프라이빗 서브넷 수</h3>
<p>서브넷은 아까 말했던 아이피의 범위를 쪼갠 것이라고 보면 된다.<br>서브넷이 총 4개라고 한다면 65536개를 4로 나눠서 16384개씩 나눠서 각자 다른 처리를 할 수 있도록 한 것이다.</p>
<p>가용 영역 마다 퍼블릭 서브넷과 프라이빗 서브넷을 둘 수 있다.<br>사진에서 가용 영역마다 두개의 서브넷이 있는 것을 볼 수 있는데, 각각 퍼블릭과 프라이빗 서브넷이다.</p>
<p>퍼블릭 서브넷은 인터넷 게이트웨이를 통해서 인터넷에 공개적으로 접근할 수 있는 영역이다.<br>인터넷 게이트웨이는 그냥 인터넷과 연결하는 문이라고 보면 된다.<br>사진에서 네트워크 연결 영역에 igw로 끝나는 것이다.
반면에 프라이빗 서브넷은 인터넷 게이트웨이에 연결되지 않아서 인터넷에 공개적으로 접근할 수 없는 영역이다.</p>
<p>보통 AWS에 애플리케이션을 배포할 때 애플리케이션들을 프라이빗 서브넷에 위치시켜서 외부에서 직접 접근할 수 없도록 한다.<br>그리고 외부에서 프라이빗 서브넷에 있는 애플리케이션에 접근해야 할 때 퍼블릭 서브넷을 통해서 허용된 곳에서만 접근할 수 있도록 한다고 보면 된다.</p>
<h3 id="nat-게이트웨이">NAT 게이트웨이</h3>
<p>위에서 프라이빗 서브넷은 인터넷과 단절된 상태라고 설명했는데, 애플리케이션을 배포하다보면 라이브러리 파일을 다운로드 하는 등 애플리케이션 내부에서 인터넷을 사용해야 할 때가 있다.<br>내부에서 외부로 요청을 하는 것이기 때문에 이는 <code>아웃바운드 요청</code>이라고 하는데, NAT 게이트웨이는 이것을 처리해주는 역할을 한다.<br>만약 애플리케이션이 모든 파일을 가지고 있고 인터넷으로부터 가져올 것이 없다고 하면 설정하지 않아도 되긴 하다.<br>NAT 게이트웨이는 인터넷 게이트웨이를 통해서 인터넷에 연결해주는 것이기 때문에 기본적으로 퍼블릭 서브넷에 위치해있어야 한다.<br>NAT 게이트웨이를 활성화 하면 그것을 위해서 인스턴스를 따로 생성하며, 가격이 상당히 비싸다.<br>정말 필요할 때만 쓰고, 만약 가격이 부담될 것 같으면 프라이빗 서브넷에 인터넷 게이트웨이를 연결하는 방법도 있다.</p>
<h3 id="vpc-엔드포인트---s3-게이트웨이">VPC 엔드포인트 - S3 게이트웨이</h3>
<p>이것도 프라이빗 서브넷이 인터넷과 단절되어있기에 필요한데, 프라이빗 서브넷에서 S3 리소스에 직접 접근할 수 있도록 해준다.<br>S3에는 보통 용량이 좀 되는 것들이 들어가있을텐데, 그것들까지 NAT 게이트웨이를 통해서 접근하다보면 비용이 훨씬 많이 나올테니, 따로 접근할 수 있는 엔드포인트를 만들어주는 것 같다.<br>어차피 AWS의 리소스이니 가능한 기능인 것 같다.</p>
<h3 id="완료">완료</h3>
<p>여기까지 보고 <code>VPC 생성</code>버튼을 누르면 지금까지 생성한 것들을 차례로 생성해준다.<br>이 아래에서는 추가적으로 알면 좋은 개념을 정리하겠다.</p>
<h2 id="라우팅-테이블">라우팅 테이블</h2>
<p>라우팅 테이블은 VPC 내에서 &quot;이 주소로 접근하면 어디로 보내면 되는지&quot;를 나타낸다.</p>
<p>아래 사진은 public 서브넷에 연결되어있는 라우팅 테이블을 조회한 것이다.
<img src="https://velog.velcdn.com/images/solst_ice/post/4ad17c07-130b-40c4-a121-17f149764d4a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/solst_ice/post/f7fa1e1b-9c0f-43d2-90bb-7721cbdfbbe8/image.png" alt=""></p>
<p>public1 서브넷과 public2 서브넷이 0.0.0.0/0 영역대로 요청할 시
igw(인터넷 게이트웨이)로 연결되는 것을 볼 수 있다.<br>외부 인터넷에 연결되어있는 것이다.</p>
<p>반면에 private 서브넷에 연결되어있는 라우팅 테이블을 조회하면 아래와 같다.
<img src="https://velog.velcdn.com/images/solst_ice/post/e1187e99-7d9b-4c9d-87bb-8eb7b7ff64ff/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/solst_ice/post/1ce8aebc-f550-4e61-96bb-8148b4ec0887/image.png" alt=""></p>
<p>private1 서브넷 하나가 연결되어있고, S3로 연결되는 vpce(VPC 엔드포인트)와 0.0.0.0/0 영역대로 요청할 시 nat(NAT 게이트웨이)로 연결되는 것을 볼 수 있다.<br>이와 동일하게 private2에 대한 라우팅 테이블도 있을 것이다.</p>
<h2 id="네트워크-aclaccess-control-list">네트워크 ACL(Access Control List)</h2>
<p>네트워크 ACL은 &quot;서브넷&quot;에 연결하는 규칙을 설정하는 곳이다.</p>
<p><img src="https://velog.velcdn.com/images/solst_ice/post/e75be650-0b2f-4229-a11e-a2bbe584efa8/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/solst_ice/post/cfd67f0e-057f-4b7f-9948-3cf0fbc0dbf4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/solst_ice/post/0fdcbaca-6feb-4dbd-bec1-d1e1b6410f34/image.jpeg" alt=""></p>
<p>사진과 같이 하나의 네트워크 ACL에 여러개의 서브넷에 대한 인바운드/아웃바운드 규칙이 정의되어있는 것을 볼 수 있다.<br>현재는 모든 아이피, 프로토콜, 포트에 대해서 허용을 해둔 상태인데,<br>물론 프라이빗은 인터넷 게이트웨이에 연결되어있지 않기 때문에 허용이 되어있다고 해도 외부에서 접근할 수 없다.</p>
<h3 id="보안-그룹">보안 그룹</h3>
<p>보안 그룹은 ACL과 같이 인바운드/아웃바운드 규칙을 설정하는 곳인데,<br>그 대상이 서브넷이 아닌 서비스 또는 애플리케이션 단위이다.<br>만약 특정 IP에서만 허용할 서비스가 있다면 이것으로 설정해줄 수 있다.</p>
<p>여기까지 VPC 설정에 대해서 아주 간단하게 알아보았다.<br>다음에는 이렇게 생성한 VPC와 Elastic Beanstalk을 사용하여 애플리케이션 배포하는 방법도 포스팅 해볼까 생각중이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Javascript 멋있고 간결하게 쓰는 방법들]]></title>
            <link>https://velog.io/@solst_ice/Javascript-%EB%A9%8B%EC%9E%88%EA%B3%A0-%EA%B0%84%EA%B2%B0%ED%95%98%EA%B2%8C-%EC%93%B0%EB%8A%94-%EB%B0%A9%EB%B2%95%EB%93%A4</link>
            <guid>https://velog.io/@solst_ice/Javascript-%EB%A9%8B%EC%9E%88%EA%B3%A0-%EA%B0%84%EA%B2%B0%ED%95%98%EA%B2%8C-%EC%93%B0%EB%8A%94-%EB%B0%A9%EB%B2%95%EB%93%A4</guid>
            <pubDate>Sun, 08 Dec 2024 04:54:02 GMT</pubDate>
            <description><![CDATA[<h2 id="3항-연산자">3항 연산자</h2>
<h4 id="bad-code">Bad Code</h4>
<pre><code class="language-javascript">function getResult(score) {
  let result;
  if (score &gt; 5) {
    result = &quot;good&quot;;
  } else if (score &lt;= 5) {
    result = &quot;bad&quot;;
  }
  return result;
}</code></pre>
<h4 id="good-code">Good Code</h4>
<pre><code class="language-javascript">function getResult(score) {
  return score &gt; 5 ? &quot;good&quot; : &quot;bad&quot;;
}</code></pre>
<h2 id="nullish-coalescing-operator-null-undefined-구별하기">Nullish coalescing operator (Null, undefined 구별하기)</h2>
<h4 id="bad-code-1">Bad Code</h4>
<pre><code class="language-javascript">function printMessage(text) {
  let message = text;
  if (text == null || text == undefined) {
    message = &quot;Nothing to display&quot;;
  }
  console.log(message);
}</code></pre>
<h4 id="good-code-1">Good Code</h4>
<pre><code class="language-javascript">function printMessage(text) {
  const message = text ?? &quot;Nothing to display&quot;;
  console.log(message);
}</code></pre>
<p>번외) 디폴드 값은 undefined 만 구별할 수 있다.</p>
<pre><code class="language-javascript">function printMessage(text = &quot;Nothing to display&quot;) {
  console.log(text);
}</code></pre>
<h2 id="object-destructuring-객체-나누기">Object Destructuring (객체 나누기)</h2>
<pre><code class="language-javascript">const person = {
  name: &quot;Julia&quot;,
  age: 20,
  phone: &quot;01077777777&quot;
};</code></pre>
<h4 id="bad-code-2">Bad Code</h4>
<pre><code class="language-javascript">function displayPerson(person) {
  displayName(person.name);
  displayPhone(person.phone);
  displayProfile(person.name, person.age);
}</code></pre>
<h4 id="good-code-2">Good Code</h4>
<pre><code class="language-javascript">function displayPerson(person) {
  const { name, age, phone } = person;
  displayName(name);
  displayPhone(phone);
  displayProfile(name, age);
}</code></pre>
<h4 id="better-code">Better Code</h4>
<pre><code class="language-javascript">function displayPerson({ name, age, phone }) {
  displayName(name);
  displayPhone(phone);
  displayProfile(name, age);
}</code></pre>
<h2 id="spread-syntax-">Spread Syntax (...)</h2>
<h3 id="object">Object</h3>
<p>두 객체를 합치기</p>
<pre><code class="language-javascript">const item = { type: &quot;shirt&quot;, size: &quot;M&quot; };
const detail = { price: 20, made: &quot;Korea&quot;, gender: &quot;M&quot; };</code></pre>
<h4 id="bad-code-3">Bad Code</h4>
<pre><code class="language-javascript">item[&quot;price&quot;] = detail.price;

// 또는
const newObject = {
  type: item.type,
  size: item.size,
  price: detail.price,
  mad: detail.made,
  gender: detail.gender
};</code></pre>
<h4 id="good-code-3">Good Code</h4>
<pre><code class="language-javascript">const shirt0 = Object.assign(item, detail);</code></pre>
<h4 id="better-code-1">Better Code</h4>
<pre><code class="language-javascript">const shirt1 = { ...item, ...detail };</code></pre>
<h3 id="array">Array</h3>
<pre><code class="language-javascript">let fruits = [&quot;수박&quot;, &quot;오렌지&quot;, &quot;바나나&quot;];

// fruits.push(&#39;딸기&#39;);
fruits = [...fruits, &quot;딸기&quot;];

// fruits.unshift(&#39;딸기&#39;);
fruits = [&quot;딸기&quot;, ...fruits];

const fruits2 = [&quot;멜론&quot;, &quot;복숭아&quot;, &quot;파인애플&quot;];

// let combined = fruits.concat(fruits2);
let combined = [...fruits, ...fruits2];</code></pre>
<h2 id="체이닝">체이닝</h2>
<pre><code class="language-javascript">const bob = {
  name: &quot;Julia&quot;,
  age: 20
};
const anna = {
  name: &quot;Julia&quot;,
  age: 20,
  job: {
    title: &quot;SoftwareEngineer&quot;
  }
};</code></pre>
<h4 id="bad-code-4">Bad Code</h4>
<pre><code class="language-javascript">function displayJobTitle(person) {
  if (person.job &amp;&amp; person.job.title) {
    console.log(person.job.title);
  }
}</code></pre>
<h4 id="good-code-4">Good Code</h4>
<pre><code class="language-javascript">function displayJobTitle(person) {
  if (person.job?.title) {
    console.log(person.job.title);
  }
}</code></pre>
<h4 id="better-code-2">Better Code</h4>
<pre><code class="language-javascript">function displayJobTitle(person) {
  const title = person.job?.title ?? &quot;No Job Yet!&quot;;
  console.log(title);
}</code></pre>
<h2 id="items-api">items API</h2>
<pre><code class="language-javascript">const items = [1, 2, 3, 4, 5, 6];</code></pre>
<h4 id="bad-code-5">Bad Code</h4>
<pre><code class="language-javascript">const evens = getAllEvens(items);
const multiple = multyplyByFour(evens);
const sum = sumArray(multiple);
console.log(sum);

function getAllEvens(items) {
  const result = [];
  for (let i = 0; i &lt; items.length; i++) {
    if (items[i] % 2 === 0) {
      result.push(items[i]);
    }
  }
  return result;
}

// +당신들이 생각하는 그 복잡한 코드들....</code></pre>
<h4 id="good-code-5">Good Code</h4>
<pre><code class="language-javascript">const evens = items.filter((num) =&gt; num % 2 === 0);
const multiple = evens.map((num) =&gt; num * 4);
const sum = multiple.reducs((a, b) =&gt; a + b, 0);
console.log(sum);</code></pre>
<h4 id="better-code-체이닝-활용">Better Code (체이닝 활용)</h4>
<pre><code class="language-javascript">const result = items
  .filter((num) =&gt; num % 2 === 0)
  .map((num) =&gt; num * 4)
  .reducs((a, b) =&gt; a + b, 0);
console.log(result);</code></pre>
<h2 id="promise-→-asyncawait">promise → async/await</h2>
<h4 id="bad-code-6">Bad Code</h4>
<pre><code class="language-javascript">function displayUser() {
  fetchUser() //
    .then((user) =&gt; {
      fetchProfile(user) //
        .then((profile) =&gt; {
          updateUI(user, profile);
        });
    });
}</code></pre>
<h4 id="good-code-6">Good Code</h4>
<pre><code class="language-javascript">async function displayUser() {
  const user = await fetchUser();
  const profile = await fetchProfile(user);
  updateUI(user, profile);
}</code></pre>
<h2 id="중복-요소-제거하기">중복 요소 제거하기</h2>
<pre><code class="language-javascript">const array = [&quot;개&quot;, &quot;고양이&quot;, &quot;강아지&quot;, &quot;말&quot;, &quot;개&quot;, &quot;고양이&quot;];
const array2 = [...new Set(array)];</code></pre>
]]></description>
        </item>
    </channel>
</rss>