<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>path__find_er.log</title>
        <link>https://velog.io/</link>
        <description>제가 공부한 내용을 적은 것이기 때문에 틀릴 수 있습니다.</description>
        <lastBuildDate>Mon, 08 Jun 2026 08:47:26 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>path__find_er.log</title>
            <url>https://velog.velcdn.com/images/path__find_er/profile/23ed0480-3a36-4163-bb30-b9972a6debb5/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. path__find_er.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/path__find_er" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[홈서버에 CI/CD 구축하고 최적화하기]]></title>
            <link>https://velog.io/@path__find_er/%ED%99%88%EC%84%9C%EB%B2%84%EC%97%90-CI-CD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B3%A0-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@path__find_er/%ED%99%88%EC%84%9C%EB%B2%84%EC%97%90-CI-CD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B3%A0-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 08 Jun 2026 08:47:26 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>집에 안쓰는 구형 맥북이 있다. </p>
<pre><code>MacBook Pro 2017
CPU : i5 어쩌구 인가? 기억 안남
RAM : 8GB
GPU : 모름 아무튼 내장 그래픽
SSD : 256GB 인가 아무튼 그쯤</code></pre><p>이 정도 스펙인데 집에서 뒹굴거리길래 홈서버를 구축해서 요긴하게 써먹고 있다. </p>
<p>홈서버를 구축하면서 </p>
<ul>
<li>집 공유기를 통해서 포트포워딩도 해보고</li>
<li>DDNS 설정도 해보고 </li>
<li>개인 프로젝트 배포도 해보고</li>
<li>CI/CD 구축도 해보고 
아무튼 잘 굴려먹고 있었다. </li>
</ul>
<p>물론 CI/CD 구축할땐 그냥 AI로 짰다. 
그냥 아하 이렇게 하는 거구나? 하는 느낌</p>
<p>그런데.. </p>
<h2 id="문제-발생">문제 발생</h2>
<p>그런데 어느 순간부터 배포하려고 PR 날리면 Actions가 너무 오래 돌다가 timeout 나기 시작했다. </p>
<p><img src="https://velog.velcdn.com/images/path__find_er/post/b459cddc-c040-4234-a748-452b72c6dd52/image.png" alt=""></p>
<p><del>이것 저것 찔러보면서 해결하려고 애쓴 모습</del></p>
<p>AI로 코드를 짰으므로, 클로드 열심히 돌려가면서 원인을 찾으려고 애썼지만 문제는 지속됐다. </p>
<h3 id="문제-현상">문제 현상</h3>
<ol>
<li>Actions 실행 중 반응 없다가 time out</li>
<li>홈서버 접속해서 docker ps 로 상태를 확인하려고 하면 반응 없음 </li>
<li>그때마다 colima 자체를 껐다가 다시 켜야하는 상황 </li>
</ol>
<p>그래서 찾은 문제의 원인은 바로 ...</p>
<blockquote>
<p>💡 OOM Out Of Memory
메모리가 터진 것이였다. </p>
</blockquote>
<pre><code>8GB RAM 사용 현황:
  macOS:           ~3GB
  Colima VM:       ~1GB
  기존 컨테이너들:     ~1GB
  Maven 빌드:       ~2GB+
  ─────────────────────
  합계: 7GB+ → OOM → Colima VM 강제 종료 → docker ps 블로킹</code></pre><p>내 맥북은 구형이기 때문에 Docker가 해당 os버전까지 지원해주지 않는다. (건방지게)
그래서 Colima라는 Linux 가상 머신을 띄워서 도커를 띄우고 있었다. 
그게 리소스를 꽤 먹는데다가 컨테이너도 왕창 띄워놨었다. </p>
<p>그리고 결정적으로 <strong>빌드 자체를 홈서버에서 하고 있었다</strong>
그 커다란 maven 의존성 덩어리를 배포할때마다 빌드하고 있었으니 간당간당했던 내 맥북이 뻗어버린 것이다. </p>
<p>정리하자면 </p>
<ul>
<li>구형 맥북에</li>
<li>Colima위에 </li>
<li>도커 띄우고</li>
<li>컨테이너 왕창 띄우고</li>
<li>게다가 <strong>빌드 자체를 홈서버에서 하고 있었는데다</strong></li>
<li>클로드 코드가 짜준 코드여서 근본적인 원인 탐색을 못하고 있었다. </li>
</ul>
<p>그래서 이 참에 그냥 CI/CD 과정을 배워보기로 했다. </p>
<h2 id="cicd란">CI/CD란?</h2>
<blockquote>
<p>📝
<strong>CI</strong> (Continuous Integration) — 코드를 push할 때마다 자동으로 빌드·테스트
<strong>CD</strong> (Continuous Deployment) — CI 통과 후 자동으로 서버에 배포</p>
<p>git push 하나로 → 빌드 → 테스트 → 배포 전부 자동화</p>
</blockquote>
<p>결국 푸시만 하면 알잘딱 빌드와 테스트, 배포까지 해주는 아주 똘똘한 방식이다. </p>
<h2 id="지금까지의-방식">지금까지의 방식</h2>
<p><img src="https://velog.velcdn.com/images/path__find_er/post/f8a156df-e7ae-4358-8a37-e5fdcdf5cc5e/image.png" alt=""></p>
<ol>
<li>Actions VM 실행</li>
<li>github Repo에서 코드를 checkout 해옴</li>
<li>VM 위에서 maven 빌드 </li>
<li>JAR 파일 생성 </li>
<li>SCP로 JAR 파일 홈서버로 전송 (JAR가 있어야 이미지를 만들 수 있기 때문)</li>
<li>홈서버에 SSH 접속</li>
<li>git pull로 최신 코드 땡겨옴</li>
<li>그 상태에서 Docker 빌드 (이미지 생성) → 💥 주로 여기서 OOM 발생 💥</li>
<li>배포</li>
</ol>
<pre><code class="language-yaml">steps:
  - uses: actions/checkout@v4
  - name: Set up JDK 21
    uses: actions/setup-java@v4
  - name: Build JAR
    run: mvn package -DskipTests -q &amp;&amp; cp target/*.jar app.jar

  - name: SCP JAR to server          # JAR를 홈서버로 전송
    uses: appleboy/scp-action@v0.1.7
    with:
      source: &quot;app.jar&quot;
      target: ${{ secrets.DEPLOY_PATH }}

  - name: Deploy via SSH # ssh로 홈서버 접속
    uses: appleboy/ssh-action@v1.0.3
    with:
      script: |
        cd ${{ secrets.DEPLOY_PATH }}
        git pull origin main
        docker compose up -d --build   # 💥 홈서버에서 Docker 빌드 → OOM</code></pre>
<p>이 방식에 문제점이 있다면</p>
<ol>
<li>서버에서 빌드하기 때문에 서버에 부하가 크다 (얼마나 부하가 큰지는 측정을 못해봤음 ㅠ)</li>
</ol>
<p>어쨌든 PR 날릴 때마다 서버가 통으로 멈추는건 아주 크나큰 문제였다. </p>
<h3 id="사실-최초의-코드는-더욱-심각했다">사실 최초의 코드는 더욱 심각했다.</h3>
<pre><code class="language-yml">name: Deploy to Home Server

  on:
    push:
      branches: [main]

  jobs:
    deploy:
      runs-on: ubuntu-latest
      steps:
        - name: Deploy via SSH
          uses: appleboy/ssh-action@v1.0.3
          with:
            host: ${{ secrets.SSH_HOST }}
            port: ${{ secrets.SSH_PORT }}
            username: ${{ secrets.SSH_USER }}
            key: ${{ secrets.SSH_PRIVATE_KEY }}
            script: |
              set -e
              cd ${{ secrets.DEPLOY_PATH }}
              git pull origin main
              ./mvnw package -DskipTests      # 홈서버에서 Maven 빌드
              docker compose up -d --build    # 홈서버에서 Docker 빌드
              echo &quot;✅ 배포 완료&quot;</code></pre>
<ul>
<li>CI 과정은 아예 생략, </li>
<li>곧바로 ssh로 서버 접속해서 pull받고,</li>
<li>maven build, docker build를 둘 다 서버에서 했다.</li>
<li>결국, <strong>부하가 두배로 드는 최악의 코드</strong>였다. </li>
</ul>
<h2 id="해결책">해결책</h2>
<blockquote>
<p><strong>Github Container Registry</strong> (ghcr.io)를 도입하기로 했다.</p>
<p>ghcr.io란? <em>github에서 운영하는 Docker 이미지 저장소</em></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/path__find_er/post/36ee78e8-fc21-4123-a747-2ca842ebd3da/image.png" alt=""></p>
<p>Github Container Registry를 이용해서 도커 이미지를 pull받아 <strong>이미지만 실행시키는 방식</strong>을 사용하도록 했다. </p>
<ol>
<li>Actions VM 실행!</li>
<li>github Repo에서 코드 checkout</li>
<li>maven 빌드 </li>
<li>생성된 jar를 토대로 docker 빌드 </li>
<li>생성된 이미지를 ghcr.io에 업로드 </li>
<li>SSH로 홈서버 접속</li>
<li>git pull로 최신 코드(docker-compose.yml) 땡겨옴</li>
<li>docker compose pull로 이미지 pull 받아오기</li>
<li>그 상태에서 Docker Run</li>
<li>배포</li>
</ol>
<pre><code class="language-yml">steps:
  - uses: actions/checkout@v4
  - name: Set up JDK 21
    uses: actions/setup-java@v4
  - name: Build JAR # maven 빌드
    run: mvn package -DskipTests -q &amp;&amp; cp target/*.jar app.jar
  - name: Set up Docker Buildx # docker build 도구
    uses: docker/setup-buildx-action@v3
  - name: Log in to ghcr.io # ghcr.io 로그인
    uses: docker/login-action@v3
  - name: Build and push Docker image   # VM에서 이미지 빌드 + ghcr.io push
    uses: docker/build-push-action@v5
    with:
      context: .
      push: true
      tags: ghcr.io/0xdf0101/mock_invest:latest

  - name: Deploy via SSH
    uses: appleboy/ssh-action@v1.0.3
    with:
      script: |
        git pull origin main
        docker compose pull             # ghcr.io에서 이미지 pull
        docker compose up -d            # 실행만 (빌드 없음) ✅</code></pre>
<h3 id="이렇게-했을-시-장점">이렇게 했을 시 장점</h3>
<ol>
<li>VM에서 maven과 image 빌드를 다 하고, 서버에서는 이미지 실행만 하기 때문에 서버의 부하가 줄어든다!</li>
<li>ghcr.io에 push 했기 때문에 같은 이미지를 다른 환경에서도 그대로 실행 가능 </li>
<li>이미지가 환경 자체를 포함하기 때문에 일관성 증가</li>
</ol>
<h2 id="결론">결론</h2>
<blockquote>
<p>✅
ghcr.io를 도입함으로써 서버의 부하를 줄이고, OOM을 예방할 수 있었다. 
(두 방식에 대한 메모리 변화량도 기록하고 싶었으나 Grafana를 띄우니까 또 뻗었다.)</p>
</blockquote>
<h2 id="배운-점">배운 점</h2>
<ul>
<li>&#39;한정된 자원&#39;이라는 것을 처음 경험해봤다. 지금까지는 제공되는 빵빵한 서버에 빵빵한 리소스 위에 배포를 했다면, 이제는 정말 <strong>&#39;한정된 자원&#39;에 최적화 하는 경험</strong>을 해본 것 같다.</li>
<li>그저 &#39;동작만 하는 코드&#39;를 만들어내는게 중요한 것이 아니라 <u>구조를 이해하고 설계하는 능력</u>이 필요함을 느꼈다. </li>
<li>사실 이제 막 배워가는 단계라 &#39;최적화&#39;인지는 잘 모르겠슈</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[컴퓨터 구조] CPU - Control Unit]]></title>
            <link>https://velog.io/@path__find_er/%EC%BB%B4%ED%93%A8%ED%84%B0-%EA%B5%AC%EC%A1%B0-CPU-Control-Unit</link>
            <guid>https://velog.io/@path__find_er/%EC%BB%B4%ED%93%A8%ED%84%B0-%EA%B5%AC%EC%A1%B0-CPU-Control-Unit</guid>
            <pubDate>Fri, 29 May 2026 06:06:39 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>컴퓨터 구조를 공부해보자 ! 
컴퓨터공학을 전공했는데 가오 떨어지게 컴퓨터 구조를 모르는건 말이 안된다. 
CS를 찬찬히 공부할건데, 그 중 가장가장가장 기초가 되는 CPU의 작동 방식에 대해서 알아보자.</p>
<h2 id="cpu의-기본-구성">CPU의 기본 구성</h2>
<p>CPU는 3가지 요소로 구성되어 있다. </p>
<ol>
<li>ALU <em>Arithmetic Logic Unit</em> : 이름처럼 <u>실제 연산을 수행하는 장치</u>이다. </li>
<li>CU <em>Control Unit</em> : 명령어를 받아서 해석하고, 실제 <u>제어 신호로 변환하는 역할</u>을 한다. </li>
<li>Register : CPU 내부에 있는 작은 메모리이다. 연산, 제어 시 데이터를 잠시 저장하는 용도로 사용한다. </li>
</ol>
<h2 id="control-unit">Control Unit</h2>
<p>Control Unit은 ..(이하 CU)</p>
<ol>
<li>사용자의 명령어를 읽어와서 (Fetch)</li>
<li>그것을 잘 해석한 다음 (Decode)</li>
<li>제어신호로 바꿔서 (Execute; 연산 자체는 ALU가 하긴 함)
실제 연산을 수행하도록 하는 장치이다.</li>
</ol>
<h3 id="메커니즘">메커니즘</h3>
<blockquote>
<p>📝
사용자의 프로그램, 혹은 명령어는 메모리(RAM)에 올라가 있다. 
성질이 급한 CPU 입장에서 연산을 수행할때마다 메모리에 갔다오는 것을 눈뜨고 봐줄 수 없다. 
메모리는 시스템 버스를 타고 다른 마을로 가야 하는 굉장히 멀리있는 동네이기 때문이다.
(정확히 말하면, <strong>CPU의 속도에 비해 메모리 조회 속도가 너무 느리기 때문에</strong> 매번 CPU가 기다려야만 한다)</p>
<p>그래서 수행할 명령어를 레지스터에 담아놓고, 그 레지스터를 조회해서 명령을 수행한다. </p>
</blockquote>
<p>여기서 두가지 레지스터가 등장한다.</p>
<ol>
<li><strong>PC</strong> <em>Program Counter</em> : 다음에 실행할 명령어의 <u>메모리 주소</u>를 담고 있는 레지스터 </li>
<li><strong>IR</strong> <em>Instruction Register</em> : 지금 실행할 명령어를 담고 있는 레지스터</li>
</ol>
<p><img src="https://velog.velcdn.com/images/path__find_er/post/4639332e-092e-49fc-b239-61c5b4137031/image.png" alt=""></p>
<h4 id="전체적인-과정은-이렇다">전체적인 과정은 이렇다.</h4>
<ol>
<li>PC를 조회해서 다음 명령어가 저장된 <u>메모리 주소</u>를 알아온다. </li>
<li>이때 <u>PC는 자동으로 다음 명령어를 가리킨다</u> (CPU가 순차적으로 실행할 수 있는 이유임)</li>
<li>해당 주소의 메모리의 명령어를 가지고 와서 IR에 저장을 해둔다. </li>
<li>그 IR에 담긴 명령어를 CU가 해석을 하고, 그에 맞는 <strong>제어 신호</strong>를 생성해서 시스템 버스에 태워 보낸다. </li>
<li>그 제어 신호를 받은 ALU가 그 신호에 맞게 연산을 수행한다. </li>
</ol>
<blockquote>
<p>📝 <strong>클럭 신호</strong>란?
<strong>시스템 동기화를 위한 타이밍 신호</strong></p>
<p>똑딱똑딱 이 박자에 맞춰서 장치들이 작동한다. 
마치 심장 박동과 같다! </p>
</blockquote>
<h2 id="배운-점">배운 점</h2>
<ul>
<li>레지스터라는게 와닿지 않았는데 어떤 용도로 어떻게 사용되는 지 알 수 있었다. </li>
<li>어쨌든 내가 키 하나를 누르고 화면 하나를 띄울때마다 이 메커니즘을 기초로 작동한다는 거잖아?</li>
<li>조상님들은 진짜 개쩌는거 같다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security의 인증 과정]]></title>
            <link>https://velog.io/@path__find_er/Spring-Security%EC%9D%98-%EC%9D%B8%EC%A6%9D-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@path__find_er/Spring-Security%EC%9D%98-%EC%9D%B8%EC%A6%9D-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Wed, 06 May 2026 08:00:41 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>Spring Security는 여러가지 필터 덩어리로 이루어져 있다. 
Filter이므로, DispatcherServlet보다도 앞에 서서 (마치 문지기마냥) 요청을 가장 먼저 받아보고, 적절하게 인증, 인가 처리를 하는 아주 야무진 녀석이다. </p>
<p>하지만 IoC에 의해서 작동하기 때문에 내부에서 어떻게 실제 인증, 인가가 이루어지는지 와닿지가 않는다. 
그래서 천천히 뜯어보면서 공부해보았다. </p>
<p>전체적인 흐름은 이러하다
<img src="https://velog.velcdn.com/images/path__find_er/post/d43dd134-e8a0-48b0-bce8-cb973ce44d39/image.png" alt=""></p>
<p>📌 세션 방식을 사용했다.</p>
<h2 id="인증-authentication-vs-인가-authorization">인증 <em>Authentication</em> vs 인가 <em>Authorization</em></h2>
<p>인증이란 <strong>사용자가 누구인지 식별</strong>하는 과정이고, 
인가란 <strong>인증된 사용자가 어디까지 접근하느냐</strong>를 구성하는 것이다. </p>
<p>📌 인증에 실패하면 401을 응답받고, 인가에 실패(권한 없는 접근)하면 403을 응답받는다. </p>
<h2 id="security의-인증-과정">Security의 인증 과정</h2>
<p>세션 방식을 사용했으므로 일단 모든 요청을 보낼 때, 쿠키에 세션ID를 지참해서 보내줘야 한다. 
그럼 그 쿠키가 없는 경우가 바로 <strong>로그인이 안된 경우</strong>(인증이 안된 상황)라고 할 수 있다.</p>
<h3 id="0-securitycontextholderfilter">0. SecurityContextHolderFilter</h3>
<p><img src="https://velog.velcdn.com/images/path__find_er/post/a12c4cbf-e648-47e6-8f5e-4eb2f3a45ba2/image.png" alt=""></p>
<ol>
<li>FilterChain의 가장 앞단에 서서 가장 먼저 지참한 쿠키를 까본다. </li>
<li>쿠키에 적힌 세션ID를 확인하고, 세션 저장소에 해당 세션이 있는지 확인한다. </li>
<li>만약 존재한다? 그럼 그 세션의 Authentication 객체를 SecurityContextHolder에 꽂아넣어준다.</li>
</ol>
<blockquote>
<p>📝 <strong>여기서 잠깐!!</strong>
SecurityContextHolder는 뭘까? 
그건 바로 <strong>처리하는 Thread의 ThreadLocal을 의미</strong>한다. 
다른 Thread가 접근할 수 없는 Thread 내부 독립된 공간에 Auth 객체를 보관한다. 
Controller, Service, Repository등을 신나게 돌아다니다 사용자를 식별해야하는 경우가 생기면 ThreadLocal에서 Auth 객체를 꺼내보는거다.</p>
</blockquote>
<ol start="4">
<li>만약 존재하지 않으면 <code>AuthenticationFilter</code>로 넘겨서 인증 과정(로그인)을 거치도록 한다. </li>
</ol>
<h3 id="1-authenticationfilter">1. AuthenticationFilter</h3>
<p><img src="https://velog.velcdn.com/images/path__find_er/post/b94d45e2-1b3f-4e31-92b9-2203397d404e/image.png" alt=""></p>
<ol>
<li>사용자가 보내온 ID/PW를 받아본다. </li>
<li>그리고 <code>UsernamePasswordAuthenticationToken</code>을 발급해준다. <blockquote>
<p>📝
여기서 <em>UsernamePasswordAuthenticationToken</em>은 말 그대로 Username과 Password를 입력받아 인증하는 고전적인 방식에서 사용되는 토큰이다. 
다른 인증 방식에서는 다른 토큰을 입력받는다. </p>
</blockquote>
</li>
</ol>
<blockquote>
<p>❓그럼 어떻게 알고 <code>UsernamePasswordAuthenticationFilter</code>와 Token을 발급해준걸까?
그건 내가 <u>SecurityConfig에다 설정을 해두었기 때문이다.</u> </p>
</blockquote>
<pre><code class="language-java">// SecurityConfig.java
http  
    .formLogin(form -&gt; form  // &lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt; 이 설정을 켜면 해당 filter가 활성화된다. 
            .loginPage(&quot;/login&quot;)  
            .loginProcessingUrl(&quot;/login&quot;)  
            .usernameParameter(&quot;email&quot;)  
            .passwordParameter(&quot;pa![](https://velog.velcdn.com/images/path__find_er/post/6f297b09-f333-481b-89d9-4fe942b576c6/image.png)
ssword&quot;)  
            .defaultSuccessUrl(&quot;/main&quot;)  
    )  
    .userDetailsService(userDetailService)</code></pre>
<p> 이 토큰이 바로 다름아닌 Authentication 객체이다. 
 단지, 아직 <strong>미인증</strong> 상태일 뿐이다. </p>
<pre><code class="language-java">public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {  
...

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {  
        if (this.postOnly &amp;&amp; !request.getMethod().equals(&quot;POST&quot;)) {  
            throw new AuthenticationServiceException(&quot;Authentication method not supported: &quot; + request.getMethod());  
        } else {  
            String username = this.obtainUsername(request);  
            username = username != null ? username.trim() : &quot;&quot;;  
            String password = this.obtainPassword(request);  
            password = password != null ? password : &quot;&quot;;  
            UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);  
            this.setDetails(request, authRequest);  
            return this.getAuthenticationManager().authenticate(authRequest);  
        }  
    }</code></pre>
<h3 id="2-authenticationmanager-providermanager">2. AuthenticationManager (ProviderManager)</h3>
<p><img src="https://velog.velcdn.com/images/path__find_er/post/5587cc73-1286-4878-84ff-ee6548f3f632/image.png" alt=""></p>
<p>📌 <em>AuthenticationManager</em>는 인터페이스이고, <em>ProviderManger</em>가 그 구현체이다. </p>
<ol>
<li>앞서 말했듯, 인증 방식은 여러가지이다. </li>
<li>그러므로 실제 &#39;인증&#39; 로직을 처리할 여러 <em>AuthenticationProvider</em>들 중에서 적절한 Provider를 찾는 역할을 한다. </li>
<li><code>AuthenticationProvider</code>를 순회하면서 해당 인증을 할 수 있는 Provider를 찾는다. </li>
</ol>
<blockquote>
<p>📝
여기서 사용되는 디자인패턴이 바로 전략 패턴 <em>Strategy Pattern</em>이다. 
Provider들은 <code>supports()</code> 메서드를 갖고 있고, ProviderManager가 해당 메서드를 실행해보면서 
인증 절차를 처리할 수 있는 녀석인지 식별한다. </p>
</blockquote>
<pre><code class="language-java">// ProviderManager.class

@Override  
public Authentication authenticate(Authentication authentication) throws AuthenticationException {  
    Class&lt;? extends Authentication&gt; toTest = authentication.getClass();  
    AuthenticationException lastException = null;  
    AuthenticationException parentException = null;  
    Authentication result = null;  
    Authentication parentResult = null;  
    int currentPosition = 0;  
    int size = this.providers.size();  
    for (AuthenticationProvider provider : getProviders()) {  
       if (!provider.supports(toTest)) {  // &lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt; 갖고있는 provider 순회하면서 supports() 실행
          continue;  
       }  
       if (logger.isTraceEnabled()) {  
          logger.trace(LogMessage.format(&quot;Authenticating request with %s (%d/%d)&quot;,  
                provider.getClass().getSimpleName(), ++currentPosition, size));  
       }  
       try {  
          result = provider.authenticate(authentication);  
          if (result != null) {  
             copyDetails(authentication, result);  
             break;  
          }  
       }
       ...</code></pre>
<h3 id="3-authenticationprovider">3. AuthenticationProvider</h3>
<p><img src="https://velog.velcdn.com/images/path__find_er/post/2d95577f-2248-49e7-9d0d-f6d9df1ff23e/image.png" alt=""></p>
<ol>
<li>이제 <em>얘가 진짜 사용자인지</em> 검증하는 로직을 실행하게 된다. </li>
<li>무엇을 검증하는가? <ul>
<li>요청에 담겨온 ID/PW</li>
<li>내 서비스가 저장하고 있는 ID/PW
이 두개를 비교해서 맞으면 <em>인증된 사용자</em>인 것이다. (당연한 소리)</li>
</ul>
</li>
<li>그래서 <u>내 서비스가 저장하고 있는 ID/PW를 가져와야 한다.</u></li>
<li>하지만 서비스마다 저장, 조회 방식이 다른데 어떡할까?? 하고 스프링은 당황할 수 있다. </li>
<li>여기서부터 개발자가 두두등장 해야한다. </li>
</ol>
<pre><code class="language-java">@Override  
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)  
       throws AuthenticationException {  
    prepareTimingAttackProtection();  
    try {  
       UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);  
       // &lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt; 내가 작성한 UserDetailsService를 호출하는 (건방진) 모습
       if (loadedUser == null) {  
          throw new InternalAuthenticationServiceException(  
                &quot;UserDetailsService returned null, which is an interface contract violation&quot;);  
       }  
       return loadedUser;  
    }</code></pre>
<h3 id="4-userdetailsservice">4. UserDetailsService</h3>
<p><img src="https://velog.velcdn.com/images/path__find_er/post/d5eb7f2c-d8de-405c-b836-47f135aea011/image.png" alt=""></p>
<blockquote>
<p>📝
AuthenticationProvider 까지는 요청만 보내면 스프링이 알아서 일해주는 영역이다. 
그러나 서비스를 설계한 내가 적절하게 저장된 ID와 PW(그니까.. 사용자 정보)넘겨줘야한다. </p>
</blockquote>
<ol>
<li><p>UserDetailsService를 구현한다. </p>
<pre><code class="language-java">@Slf4j  
@RequiredArgsConstructor  
@Service  
public class CustomUserDetailService implements UserDetailsService {  
 private final UserRepository userRepository;  
 @Override  
 public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {  

     User user = userRepository.findByEmail(email).orElseThrow(() -&gt; new EntityNotFoundException(&quot;해당 사용자를 찾을 수 없습니다.&quot;));  

     return new CustomUserDetails(user);  // &lt;&lt;&lt;&lt;&lt;&lt; Provider가 원하는 형태로 넘겨줌
 }  
}</code></pre>
</li>
<li><p><code>AuthenticationProvider</code>는 요청에 담겨온 ID를 <code>UserDetailsService</code>에 넘겨준다. </p>
</li>
<li><p>넘어온 ID를 토대로 내 DB에 저장된 사용자를 찾아서 Provider에게 넘겨준다. </p>
</li>
</ol>
<p>⚠ 검증을 내가 하는게 아니다. 단순히 나는 저장된 사용자 정보를 넘겨주기만 하고, 그 정보를 넘겨받은 Provider가 검증하는 것이다!</p>
<blockquote>
<p>📝
내가 작성한 코드를 스프링이 호출해서 (<code>loadUserByUsername()</code>) 정보를 받아가고 있으니 이게 바로 제어 역전 <em>IoC</em>라 할 수 있겠다!</p>
</blockquote>
<ol start="4">
<li>사용자 정보(<code>UserDetails</code>)를 넘겨받은 AuthenticationProvider는 실제 검증을 한다. </li>
</ol>
<h3 id="5-인증이-완료된-이후">5. 인증이 완료된 이후</h3>
<ol>
<li>인증이 되었다면, 그때 비로소 <code>SecurityContextHolder</code>에 <code>Authentication</code> 객체를 넣어준다! </li>
<li>여기서 Authentication 객체는 아까 발급받은 토큰이고, <code>Authenticated</code> 값이 true가 된다. 즉 인증 완료 되었다는 의미이다. </li>
<li>여기에 넣어줄 뿐 아니라 <strong>세션 저장소</strong>에도 넣어준다. </li>
<li>그리고 클라이언트에게 세션ID를 건네주고, 다음 요청부터는 클라이언트가 쿠키를 싸와야 한다. (HTTP의 무상태성 어쩌구 저쩌구..)</li>
</ol>
<blockquote>
<p>[!warning]
요청이 마무리된 이후 ThreadLocal은 반드시 초기화해주어야 한다. 
요청 하나당 Thread 하나가 배정되어서 일을 하는데, 다음 요청 처리하려고 봤더니 ThreadLocal에 이전의 Auth 정보가 남아있다면? 
⇒ 대참사 </p>
</blockquote>
<h2 id="기타-궁금한-사항">기타 궁금한 사항</h2>
<h3 id="q-authentication-객체는-무엇을-담고-있을까">Q. Authentication 객체는 무엇을 담고 있을까?</h3>
<p><code>Principal</code>: 인증된 사용자의 주체 (user details).<br><code>Credentials</code>: 사용자의 인증 자격 증명 (보통 비밀번호).<br><code>Authorities</code>: 사용자의 권한 (roles or permissions).<br><code>Authenticated</code>: 인증 여부를 나타내는 boolean 값.</p>
<h2 id="배운-점">배운 점</h2>
<ul>
<li>IoC가 이런 거구나, 내가 짠 코드를 스프링이 호출한다는게 이런거구나 </li>
<li><strong>스프링은 진짜 개쩌는구나</strong> 진짜 이걸 설계한 조상님들은 대단하다. </li>
<li>이제 스프링 프레임워크가 제공하는 규격만 지키면 필요한 부분을 적절히 커스텀해서 쓸수 있다. 예를 들면, OAuth같은거 </li>
<li>진짜 개쩐다 스프링</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[View와 API를 분리해야 하는 이유]]></title>
            <link>https://velog.io/@path__find_er/Controller%EC%99%80-RestController%EB%A5%BC-%EB%B6%84%EB%A6%AC%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@path__find_er/Controller%EC%99%80-RestController%EB%A5%BC-%EB%B6%84%EB%A6%AC%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Wed, 22 Apr 2026 06:47:25 GMT</pubDate>
            <description><![CDATA[<h2 id="controller와-restcontroller는-분리해서-관리해야-한다"><code>Controller</code>와 <code>RestController</code>는 분리해서 관리해야 한다.</h2>
<p>View와 Model을 함께 제공하는 Controller,
json 데이터만 딸랑 던져주는 RestController를 지금까지 하나의 클래스에서 혼용했다. </p>
<p>코드를 짜면서도 스스로 짜증났다. 
굳이 굳이 <code>@ResponseBody</code>를 붙여줘야 하는게 너무 귀찮고 짜증났다. </p>
<p>하지만 둘은 <strong>성격이 매우 다르기 때문에</strong> 추후 유지보수를 위해서라도 분리할 필요가 있었다. </p>
<p>단순히 view만 넘겨주는 컨트롤러는 그냥 ViewResolver에게 위임했다. </p>
<pre><code class="language-java">@Configuration  
@RequiredArgsConstructor  
public class WebConfig implements WebMvcConfigurer {  
    @Override  
    public void addViewControllers(ViewControllerRegistry registry) {  
        registry.addViewController(&quot;/&quot;).setViewName(&quot;index&quot;);  
        registry.addViewController(&quot;/main&quot;).setViewName(&quot;main&quot;);  
        registry.addViewController(&quot;/login&quot;).setViewName(&quot;login&quot;);  
        registry.addViewController(&quot;/signup&quot;).setViewName(&quot;signup&quot;);  
        registry.addViewController(&quot;/signup/set-username&quot;).setViewName(&quot;signup/set-username&quot;);  
    }  
}</code></pre>
<p>그리고 json 데이터만 넘겨주는 RestController들(API 담당)만 따로 모아 클래스를 만들었다.</p>
<pre><code class="language-java">@RestController  
@RequiredArgsConstructor  
@Validated
@Slf4j  
public class UserController {  

    private final UserService userService;  

    @GetMapping(&quot;/users/{userId}&quot;)  
    public ResponseEntity&lt;UserInfo&gt; getUser(@PathVariable Long userId) { // &lt;&lt;&lt; 파라미터 검증 필요  
        UserInfo userinfo = userService.getUser(userId);  
        return ResponseEntity.ok().body(userinfo);  
    }  

    /**  
     * 로컬 회원가입 요청 로직 (OAuth X)  
     */   
     @PostMapping(&quot;/api/v1/users&quot;)  
    public ResponseEntity&lt;Void&gt; createUser(@Valid @RequestBody UserCreateRequest request) {  

        userService.signUp(request);  

        log.info(&quot;회원 등록 완료 : {}&quot;, request.username());  
        return ResponseEntity.ok().build();  
    }  

    /**  
     * OAuth로 회원가입 시 username을 따로 입력받는 컨트롤러  
     */  
    @PostMapping(&quot;/api/v1/users/username&quot;)  
    public ResponseEntity&lt;Void&gt; setUsername(@NotBlank @Size(min=4, max=20) @RequestParam(&quot;username&quot;) String username,  
                                            @AuthenticationPrincipal OAuth2User oAuth2User) {  

        log.debug(&quot;입력받은 아이디 : {}&quot;, username);  
        String email = oAuth2User.getAttribute(&quot;email&quot;);  
        // --&gt; 사용자 식별용 이메일  
        userService.updateUsername(email, username);  

        return ResponseEntity.ok().build();  
    }  
}</code></pre>
<h3 id="view와-api를-서로-분리했더니-좋은-점은">View와 API를 서로 분리했더니 좋은 점은?</h3>
<blockquote>
<p><strong>예외처리</strong>가 매우 용이해진다. 
반대로, 분리하지 않으면 예외처리가 참 거지같다. </p>
</blockquote>
<p>애시당초에 분리해야겠다는 생각 자체를 한 것이 예외처리를 하면서였다. 
예외처리를 하는데 어떤 건 String을 반환하고 어떤 건  ResponseEntity를 반환하고 하니 자꾸 꼬였다. </p>
<p>그래서 view 컨트롤러와 API 컨트롤러의 Advice를 각각 나눴다. </p>
<pre><code class="language-java">@Slf4j  
@ControllerAdvice(annotations = Controller.class)  
public class ViewExceptionAdvice {  

    @ExceptionHandler({EntityNotFoundException.class, EmailNotFoundException.class})  
    public String handleNotFoundException(RuntimeException e, Model model) {  
        log.error(&quot;Not found Exception : {}&quot;, e.getMessage(), e);  
        model.addAttribute(&quot;exception&quot;, e.getMessage());  
        return &quot;error&quot;;  
    }  
}</code></pre>
<p>얘는 view만 넘겨준다.</p>
<pre><code class="language-java">
@Slf4j  
@RestControllerAdvice(annotations = RestController.class)  
public class ApiExceptionAdvice {  

    /**  
     * 회원가입시, email이나, username이 중복된 경우 예외 처리  
     */  
    @ExceptionHandler({EmailDuplicateException.class, UsernameDuplicateException.class})  
    public ResponseEntity&lt;List&lt;ErrorResponseDto&gt;&gt; handleDuplicateExceptions(RuntimeException e) {  
        String field = (e instanceof EmailDuplicateException) ? &quot;email&quot; : &quot;username&quot;;  

        // List.of()를 사용해서 딱 하나의 에러만 담긴 리스트 생성  
        List&lt;ErrorResponseDto&gt; errors = List.of(new ErrorResponseDto(field, e.getMessage()));  

        return ResponseEntity.status(HttpStatus.CONFLICT).body(errors);  
    }  

    /**  
     * @Valid에 의해서 검증하다 예외가 터지면 얘가 처리함  
     */  
    @ExceptionHandler(MethodArgumentNotValidException.class)  
    public ResponseEntity&lt;List&lt;ErrorResponseDto&gt;&gt; handleValidationExceptions(MethodArgumentNotValidException e) {  
        List&lt;ErrorResponseDto&gt; errors = e.getBindingResult().getFieldErrors().stream()  
                .map(error -&gt; new ErrorResponseDto(error.getField(), error.getDefaultMessage()))  
                .toList();  

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);  
    }  

    /**  
     * @Valided에 의해서 파라미터만 검증하다 터지는 예외 처리  
     */  
    @ExceptionHandler(ConstraintViolationException.class)  
    public ResponseEntity&lt;List&lt;ErrorResponseDto&gt;&gt; handleConstraintViolationException(ConstraintViolationException e) {  

        List&lt;ErrorResponseDto&gt; errors = e.getConstraintViolations().stream()  
                .map(violation -&gt; {  
                    String propertyPath = violation.getPropertyPath().toString();  
                    String fieldName = propertyPath.substring(propertyPath.lastIndexOf(&#39;.&#39;) + 1);  
                    return new ErrorResponseDto(fieldName, violation.getMessage());  
                })  
                .toList();  

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);  
    }  
}</code></pre>
<p>js에 던져줄 적당한 양식을 맞췄고, 중복 코드도 줄었을 뿐더러 가독성도 높아졌다. 
얘는 이제 js에 던져줄 <code>List&lt;ErrorResponseDto</code>만 잘 포장해서 넘겨주면 된다. </p>
<blockquote>
<p>⚠ 주의할 점
<code>@RestControllerAdvice(annotations = RestController.class)</code> advice에 이런 식으로 못박아버리면
<code>@RestController</code>가 붙은 애한테만 반응한다.
고로, 맨 처음 내가 했던 것처럼 <u>view Controller와 섞인 클래스</u>에는 반응을 안한다!</p>
</blockquote>
<h2 id="결론과-배운-점">결론과 배운 점</h2>
<h3 id="결론">결론</h3>
<blockquote>
<p>응답의 형태(HTML vs JSON)에 따라 컨트롤러를 나눴다. 
결국 고급진 말로 <strong>관심사의 분리</strong>를 실천했다. </p>
<p>관심사 분리를 통해 유지보수성을 높일 수 있었다. 
앞으로도 관심사의 분리를 고려하며 코드를 짜자!</p>
</blockquote>
<h3 id="배운-점">배운 점</h3>
<ol>
<li>무작정 코드를 짜기보단 &#39;<strong>어떻게 구조화를 할 것인지</strong>&#39; 생각할 필요가 있는 것 같다. </li>
<li>차분히 설계하는 시간보다 이렇게 리팩토링 해가는 과정이 더 오래걸리는거 같다(체감 상)</li>
<li><strong>코드 컨벤션</strong>을 맞추는 이유를 알겠다. 왜 API 스펙을 맞추는지도... 
지금이야 혼자 개발하니까 나 혼자 끙끙대지만 다른 사람과의 협업이였으면 한대 맞았을 수 있다는 생각이 든다. </li>
<li>대 AI 에이전트 시대에 결국 필요한건 구현보단 이런 <u>설계 능력이랑 규격을 정하는 능력</u>이지 않을까? (아님 말고)</li>
</ol>
<p><del>애시당초에 SSR과 CSR을 혼합해서 쓰는게 맞나 싶다</del></p>
<h2 id="기타-알게된-점">기타 알게된 점</h2>
<ul>
<li><code>@Valid</code>를 쓰면 객체(DTO)를 검증하는 데 쓴다. 
<code>BindingResult</code> 안쓰면 <code>MethodArgumentNotValidException</code>이 터지니까 얘를 잡아주면 된다. </li>
<li><code>@Validated</code>를 쓰면 파라미터 단위를 검증 할 수 있다.
얘는 <code>ConstraintViolationException</code>이 터지니까 얘를 잡아주면 된다.  </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot Auto Configuration]]></title>
            <link>https://velog.io/@path__find_er/Spring-Boot-Auto-Configuration</link>
            <guid>https://velog.io/@path__find_er/Spring-Boot-Auto-Configuration</guid>
            <pubDate>Tue, 07 Apr 2026 09:04:52 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>Spring을 배우면서 <code>pom.xml</code>에 의존성을 추가하고 라이브러리를 가져다 쓰는 걸 아주 습관적으로 반복했다. 
하지만 생각해보면 어떻게 작동되는지는 궁금하지도 않았고 알 필요도 없다고 생각했다. </p>
<p><img src="https://velog.velcdn.com/images/path__find_er/post/6c9d7180-7e68-44cc-aced-0abe4a14d1ac/image.png" alt="">
<del>노란 밑줄 굉장히 거슬림</del> </p>
<p>그러나 Spring에서의 Auto Configuration을 배우는 과정 중에, 
✅ maven과 Spring이 어떻게 상호작용 하는지, 
✅ 어떻게 외부 라이브러리를 가져와서 사용하는지,
✅ 그로 인해 어떤 편리함이 있는지,
알게 되어서 정리한다.</p>
<h2 id="메커니즘">메커니즘</h2>
<h3 id="1-pomxml에-의존성-추가">1. <code>pom.xml</code>에 의존성 추가</h3>
<p>개발자는 외부 라이브러리의 의존성(spring boot, lombok, mysql 등)을 <code>pom.xml</code>에 추가한다. 
이는 마치 필요한 부품의 목록을 작성하는 것과 같다. </p>
<pre><code class="language-xml">    &lt;dependencies&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
        &lt;/dependency&gt;

        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
            &lt;scope&gt;test&lt;/scope&gt;
        &lt;/dependency&gt;

        &lt;dependency&gt;
            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
            &lt;artifactId&gt;lombok&lt;/artifactId&gt;
            &lt;optional&gt;true&lt;/optional&gt;
        &lt;/dependency&gt;

        &lt;dependency&gt;
            &lt;groupId&gt;com.mysql&lt;/groupId&gt;
            &lt;artifactId&gt;mysql-connector-j&lt;/artifactId&gt;
            &lt;scope&gt;runtime&lt;/scope&gt;
        &lt;/dependency&gt;

      ...</code></pre>
<h3 id="2-의존성-관리">2. 의존성 관리</h3>
<p>빌드 초기 단계(패키징, IDE프로젝트 로딩, complie 등)에서, pom.xml을 참고하여 maven Central Repository(mvn 원격 저장소) 으로부터 실제 코드를 다운받고, 로컬 저장소에 캐싱한다.<br>(그래서 <code>mvn clean package</code>를 처음하면 라이브러리를 다운 받느라 빌드하는데 시간이 오래 걸린다.)</p>
<p>그리고 다운받은 라이브러리 jar 파일은 프로젝트 classpath에 올려진다.
📌 classpath에 올려졌다는 표현은, 애플리케이션 실행시 <u>JVM의 ClassLoader</u>가 JAR 파일을 읽을 수 있는 상태가 된다는 의미이다.</p>
<h3 id="3-enableautoconfiguration에-의한-라이브러리-빈-자동-등록">3. <code>@EnableAutoConfiguration</code>에 의한 라이브러리 빈 자동 등록</h3>
<p>스프링 부트가 실행되고, <code>@EnableAutoConfiguration</code> 어노테이션을 통해 외부 라이브러리 빈 등록이 시작된다. 
<code>@EnableAutoConfiguration</code> 은, classpath에 있는 라이브러리를 확인하고, 필요한 빈을 자동으로 등록해주는 어노테이션이다. </p>
<ol>
<li><p><code>META-INF/spring/*.imports</code> 목록을 쭉 스캔한다. 이때 이 목록에는 제공할 수 있는 모든 <code>AutoConfiguration</code>이 담겨있다.
(Spring 2.7 ~ 이상의 버전에만 해당)</p>
<pre><code>org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration  
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration  
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration  
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration  
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration  
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration  
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
...</code></pre></li>
<li><p><code>/*.imports</code> 목록의 클래스를 탐색하면서 <code>@Conditional</code> 조건을 체크한다. </p>
<pre><code class="language-java">@AutoConfiguration(
 after = {DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class}
)
@ConditionalOnWebApplication(
 type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class}) // ----&gt; 이런 이런 조건들을 classpath의 jar와 비교함
@AutoConfigureOrder(-2147483638)
@ImportRuntimeHints({WebResourcesRuntimeHints.class})
public class WebMvcAutoConfiguration {
 public static final String DEFAULT_PREFIX = &quot;&quot;;
 public static final String DEFAULT_SUFFIX = &quot;&quot;;
 private static final String SERVLET_LOCATION = &quot;/&quot;;

 @Bean
 @ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
 @ConditionalOnProperty(
     prefix = &quot;spring.mvc.hiddenmethod.filter&quot;,
     name = {&quot;enabled&quot;}
 )
 public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
     return new OrderedHiddenHttpMethodFilter();
 }
</code></pre>
</li>
</ol>
<pre><code>3. classpath에 올려진 jar파일을 기준으로 조건을 체크하고, 조건 만족 시 해당 `AutoConfiguration` 클래스의 빈을 생성하고 등록한다. 
   정확히는, **개발자가 직접 등록한 빈이 없을 때**(`ConditionalOnMissingBean`) 작동한다. 그러므로 우리가 직접 설정을 커스텀 할 수 있게 된다. ⭐

## 마무리 
&gt; 결국, 습관적으로 추가했던 의존성이 실제로 어떻게 가져와지며, 어떻게 빈으로 등록이 되는지를 알 수 있게 됐다!
&gt; 
&gt; 그리고, Spring이 없었다면 얼마나 많은 과정을 거쳐 사용해야했는지 생각한다면, Spring의 자동화 로직은 참 편리한 것 같다!

</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[DI Dependency Injection 의존성 주입]]></title>
            <link>https://velog.io/@path__find_er/DI-Dependency-Injection-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85</link>
            <guid>https://velog.io/@path__find_er/DI-Dependency-Injection-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85</guid>
            <pubDate>Fri, 27 Mar 2026 05:27:57 GMT</pubDate>
            <description><![CDATA[<p>📌 Spring 위주로 작성됨</p>
<h2 id="개요">개요</h2>
<blockquote>
<p>[!note]
객체가 필요로 하는 의존성을 외부에서 직접 주입받는 것을 DI 라고 한다</p>
<p>프로그래밍에서 구성 요소간의 의존관계가 소스코드 내부가 아닌 외부의 설정파일 등을 통해 정의되게 하는 디자인 패턴 중의 하나</p>
</blockquote>
<blockquote>
<p>[!note] 의존성이란?
의존성이라는 아주 멋들어지는 말을 써서 약간 헷갈릴 수 있는데, 
A를 실행하기 위해 B를 호출하거나 기능을 빌려써야 한다. == A가 B에 의존한다. 
그냥 <u>필요하다</u>는 말임</p>
</blockquote>
<ul>
<li>IoC 패턴 중 하나 </li>
<li>객체간 의존성을 낮춤[^1]</li>
<li>외부에서 객체를 생성하고 전달 </li>
</ul>
<h3 id="dependency-inversion-principle-의존성-역전-원칙">Dependency Inversion Principle (의존성 역전 원칙)</h3>
<ul>
<li>상위 모듈이 하위 모듈[^2]에 의존관계를 가지지 않도록 구현해야 한다는 원칙.</li>
<li>추상클래스는 그 구현체의 내용에 의존관계를 가지지 않는다.</li>
<li>구현체가 추상클래스에 의존관계를 가질 수 있다.</li>
</ul>
<blockquote>
<p>[!note]</p>
<h2 id="dip-의존성-역전-원칙의-핵심">DIP: 의존성 역전 원칙의 핵심</h2>
<h3 id="1-전통적인-의존-관계의-문제점">1. 전통적인 의존 관계의 문제점</h3>
<ul>
<li>상위 모듈이 하위 모듈의 구체적인 구현에 직접 의존함. [cite: 2026-01-20]</li>
<li>하위 모듈 변경 시 상위 모듈까지 수정해야 하는 <strong>강한 결합</strong>이 발생함. [cite: 2026-01-20]</li>
</ul>
<h3 id="2-역전inversion의-메커니즘">2. 역전(Inversion)의 메커니즘</h3>
<ul>
<li>상위 모듈과 하위 모듈 사이에 <strong>추상화(인터페이스)</strong>를 둠. [cite: 2026-01-20]</li>
<li>하위 모듈이 상위 모듈이 정의한 인터페이스에 의존하게 함으로써 의존 방향을 역전시킴. [cite: 2026-01-20]</li>
</ul>
<h3 id="3-설계의-이점">3. 설계의 이점</h3>
<ul>
<li>상위 모듈은 상세 구현의 변화로부터 <strong>독립</strong>됨. [cite: 2026-01-20]</li>
<li>코드의 재사용성이 높아지고, 테스트 시 가짜 객체(Mock)를 주입하기 쉬워짐. [cite: 2026-01-20]<h3 id="4-요약">4. 요약</h3>
DIP는 상위 모듈이 하위 모듈을 무시하는 것이 아니라, 둘 다 <strong>추상화</strong>에 의존하게 하여 변화에 유연하게 대처하는 원칙임. [cite: 2026-01-20]</li>
</ul>
</blockquote>
<h2 id="주입-방법">주입 방법</h2>
<p>📌 <code>@Autowired</code> : 이 어노테이션을 달고 있다는 말은 &quot;나 객체 필요해요&quot; 하고 손들고 있는거랑 똑같음</p>
<h3 id="종류">종류</h3>
<ul>
<li>Constructor Injection</li>
<li>Setter Injection</li>
<li>Field Injection</li>
<li>@Configuration + @Bean</li>
</ul>
<h3 id="constructor-injection">Constructor Injection</h3>
<pre><code class="language-java">@Component
public class AppStartupRunner implements ApplicationRunner {

    private Greeting greeting;

    @Autowired
    public ApplicationRunner(Greeting greeting) {
        this.greeting = greeting;
    }

    @Override
    public void run(ApplicationArguments args) {
        greeting.sayHello();
    }
}</code></pre>
<ul>
<li>말 그대로 생성자를 통해 사용 </li>
<li><code>lombok</code>이랑 연계해서 많이 사용됨</li>
<li>이걸 권장함</li>
<li>생성자가 하나면, <code>@Autowired</code>가 있다고 간주함</li>
</ul>
<h3 id="setter-injection">Setter Injection</h3>
<pre><code class="language-java">@Component
public class AppStartupRunner implements ApplicationRunner {

    private Greeting greeting;

    @Autowired
    public void setGreeting(Greeting greeting) {
        this.greeting = greeting;
    }

    @Override
    public void run(ApplicationArguments args) {
        greeting.sayHello();
    }
}</code></pre>
<ul>
<li>setter에다가 주입</li>
<li>근데 꼭 set~ 형태가 아니여도 됨, 다른 이름으로 해도 무방함</li>
</ul>
<h3 id="field-injection">Field Injection</h3>
<pre><code class="language-java">@Component
public class AppStartupRunner implements ApplicationRunner {

    @Autowired
    private Greeting greeting;


    @Override
    public void run(ApplicationArguments args) {
        greeting.sayHello();
    }
}</code></pre>
<ul>
<li><p>명시적이지 않아서 그다지 권장하진 않는 듯 </p>
<h3 id="configuration--bean">@Configuration + @Bean</h3>
<pre><code class="language-java">@Configuration
public class GreetingConfig {

 @Bean
 American innerGreeting() {
     return new American();
 }
 // bean 으로 등록된 American 을 파라미터로 넣어준다
 @Bean
 Greeting englishGreeting(American american) {
     return new EnglishGreeting(american);
 }
}</code></pre>
</li>
<li><p>이러면 매개변수로 bean을 넘겨줘야 하니까 뒤지게 귀찮은게 아닌가 싶음</p>
</li>
</ul>
<h2 id="주입할-bean을-좀-더-명확하게-하고-싶다면-primary-qualifier">주입할 Bean을 좀 더 명확하게 하고 싶다면? <code>@Primary</code>, <code>@Qualifier</code></h2>
<h3 id="🎯-동일-타입-빈-충돌-해결-primary--qualifier">🎯 동일 타입 빈 충돌 해결: @Primary &amp; @Qualifier</h3>
<h4 id="1-사용-배경">1. 사용 배경</h4>
<ul>
<li>인터페이스 하나에 여러 구현체가 빈으로 등록된 경우 발생함.</li>
<li>의존성 주입 시 어떤 구현체를 선택할지 스프링에 명시적인 기준을 제공하기 위함임.<h4 id="2-주요-전략">2. 주요 전략</h4>
</li>
<li>@Primary: 해당 빈을 기본 우선순위로 설정함. 주입 받는 곳에서 별도 설정이 없으면 이 빈이 채택됨.</li>
<li>@Qualifier: 빈에 별칭을 부여하고 주입 시 해당 이름을 명시함. 가장 강력하고 구체적인 선택 방법임.<h4 id="3-우선순위-규칙">3. 우선순위 규칙</h4>
</li>
<li>두 어노테이션이 충돌할 경우, 더 구체적인 <strong>@Qualifier</strong>가 @Primary보다 우선권을 가짐.</li>
</ul>
<table>
<thead>
<tr>
<th>구분</th>
<th><strong>@Primary</strong></th>
<th><strong>@Qualifier</strong></th>
</tr>
</thead>
<tbody><tr>
<td>성격</td>
<td>기본값 설정 (우선순위)</td>
<td>특정 빈 지목 (정밀 타격)</td>
</tr>
<tr>
<td>장점</td>
<td>코드가 간결함 (주입 시 추가 어노테이션 불필요)</td>
<td>의도가 명확함 (어떤 빈이 주입되는지 바로 알 수 있음)</td>
</tr>
<tr>
<td>우선순위</td>
<td>낮음</td>
<td><strong>높음</strong> (@Qualifier가 이김)</td>
</tr>
<tr>
<td>구현체에다가 이거 붙여주면 된다!</td>
<td></td>
<td></td>
</tr>
<tr>
<td>#### 4. 요약</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<blockquote>
<p>다형성을 활용한 설계에서 발생하는 <strong>주입 모호성</strong>을 해결하는 도구이며, 유지보수 편의를 위해 @Primary를 기본으로 하되 특수한 상황에 @Qualifier를 병용함.</p>
</blockquote>
<h2 id="🏗️-생성자-주입을-권장하는-이유-di">🏗️ 생성자 주입을 권장하는 이유 (DI)</h2>
<h3 id="1-불변성-확보">1. 불변성 확보</h3>
<ul>
<li><strong>final</strong> 키워드 사용이 가능하여 의존관계가 실행 중 변하지 않음을 보장함.</li>
<li>객체의 안정성이 높아지며 조작의 위험을 방지함.<h3 id="2-테스트-용이성">2. 테스트 용이성</h3>
</li>
<li>스프링 컨테이너 없이도 순수 자바 코드로 의존성을 주입하여 단위 테스트 가능함.</li>
<li>Mock 객체 주입이 간편해짐.<h3 id="3-완전한-객체-상태">3. 완전한 객체 상태</h3>
</li>
<li>객체 생성 시점에 모든 의존성이 주입되어야 하므로 <strong>NPE</strong> 발생을 사전에 차단함.</li>
<li>컴파일 시점에 의존성 누락을 확인할 수 있음.<h3 id="4-순환-참조-탐지">4. 순환 참조 탐지</h3>
</li>
<li>애플리케이션 구동 시점에 순환 참조 문제를 즉시 발견하여 <strong>Fail-fast</strong>가 가능함.<h3 id="5-요약">5. 요약</h3>
<blockquote>
<p>생성자 주입은 객체의 <strong>안전성</strong>과 <strong>테스트 효율성</strong>을 극대화하며, 롬복(@RequiredArgsConstructor)과 결합 시 코드 가독성까지 챙길 수 있는 표준 방식임.</p>
</blockquote>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVC 패턴]]></title>
            <link>https://velog.io/@path__find_er/MVC-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@path__find_er/MVC-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Tue, 10 Feb 2026 07:43:50 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p><img src="https://velog.velcdn.com/images/path__find_er/post/720b57d8-507e-4568-88b7-0608fe03c0b2/image.png" alt=""></p>
<blockquote>
<p>Model - View - Controller 
관심사의 분리 <em>Separation of Concerns, SoC</em>가 주된 목적
즉, 각 객체가 자기가 잘하는 일에 집중하게 해서 <u>코드의 재사용성과 유지보수성을 높임</u></p>
</blockquote>
<ul>
<li><code>Model</code>: <u>비즈니스 로직</u> 및 <u>데이터 처리</u> 담당</li>
<li><code>View</code>: 모델이 처리한 결과 데이터의 화면 생성 담당 ex) JSP</li>
<li><code>Controller</code>: 요청 처리 및 흐름 제어 담당</li>
</ul>
<h2 id="model">Model</h2>
<p>시스템의 비즈니스 데이터와 그 데이터를 처리하는 규칙을 정의한 컴포넌트</p>
<blockquote>
<p>App의 상태 <em>Data</em>와 비즈니스 로직 <em>Logic</em>을 관리하는 부분 </p>
</blockquote>
<h3 id="주요-특징">주요 특징</h3>
<ul>
<li>비즈니스 로직을 다루므로 <strong>&#39;실질적인 일&#39;을 하는 부분</strong>이라고 할 수 있다!</li>
<li>독립성 : View, Controller에 대한 정보를 전혀 몰라야 함 (코드 내에 UI 관련 라이브러리, 컨트롤러 객체 등이 들어가면 안됨!)</li>
<li>상태 저장 : <u>DB와 연동</u>되어 데이터를 CRUD함 </li>
<li><code>Service</code>, <code>Repository</code> 계층을 포함한다! </li>
</ul>
<h2 id="view">View</h2>
<blockquote>
<p>모델이 보유한 데이터를 시각적으로 표현하여 사용자에게 출력하는 컴포넌트</p>
<p>사용자에게 <u>보여지는 결과물</u>을 생성하는 부분 </p>
</blockquote>
<h3 id="주요-특징-1">주요 특징</h3>
<ul>
<li>수동성 : 데이터를 저장하거나 처리하는게 아니라 그냥 <u><strong>전달받은 데이터를 화면에 그리는 역할</strong>만 함</u></li>
<li>로직 최소화 : 출력 로직만 포함되어야 하며, 비즈니스 로직은 포함되지 않음!</li>
<li>독립성 : 모델의 구현은 몰라도 상관 없음. 그냥 전달받은 데이터를 잘 그려내기만 하면 된다!</li>
</ul>
<h2 id="controller">Controller</h2>
<blockquote>
<p>사용자의 요청을 해석하여 적절한 모델 서비스를 호출하고 그 결과에 맞는 뷰를 선택해 응답을 제어하는 컴포넌트</p>
<p>사용자의 입력 <em>Request</em>를 받아 Model과 View를 연결하는 중개자 </p>
</blockquote>
<h3 id="주요-특징-2">주요 특징</h3>
<ul>
<li>흐름 제어 : 전체적인 애플리케이션의 워크플로우를 결정함</li>
<li>가교 : 모델로부터 데이터를 전달받아 뷰로 넘겨주거나, 사용자의 입력을 모델이 이해하도록 변환함. 즉, <strong>Client, Model, View의 가교 역할</strong>을 한다</li>
<li>비즈니스 로직 배제 : 직접적인 <u>비즈니스 로직을 포함하지 않음</u> 그저 모델에게 일을 잘 시키는데 집중함</li>
</ul>
<h2 id="mvc-패턴의-장점">MVC 패턴의 장점</h2>
<h3 id="1-유지보수성이-좋음">1. 유지보수성이 좋음</h3>
<ul>
<li>디자인을 바꿀땐 View만 고치면 되고, 비즈니스 로직을 수정할땐 Model만 고치면 됨</li>
<li>즉, 서로 간섭 없이 유지보수할 수 있다!<h3 id="2-확장성">2. 확장성</h3>
</li>
<li>하나의 모델에 여러 뷰를 붙일 수 있음</li>
<li>웹이나, 모바일 등</li>
</ul>
<h3 id="3-테스트-용이성">3. 테스트 용이성</h3>
<ul>
<li>화면 없이 백엔드 로직만 떼어내서 유닛 테스트 하는데 용이함</li>
</ul>
<h2 id="참조">참조</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[Cookie 쿠키]]></title>
            <link>https://velog.io/@path__find_er/Cookie-%EC%BF%A0%ED%82%A4</link>
            <guid>https://velog.io/@path__find_er/Cookie-%EC%BF%A0%ED%82%A4</guid>
            <pubDate>Fri, 24 Oct 2025 08:54:08 GMT</pubDate>
            <description><![CDATA[<p>#개발용어 #web </p>
<h2 id="개요">개요</h2>
<blockquote>
<p>[!note]
Cookie는 웹사이트를 방문할 때 사용자의 컴퓨터에 저장되는 작은 텍스트 파일이야.
쉽게 말해, <em>브라우저가 사용자의 정보를 저장하는 작은 메모리 조각</em>이지</p>
</blockquote>
<h3 id="쿠키의-필요성">쿠키의 필요성</h3>
<blockquote>
<p>[!note] 
쿠키가 필요한 이유는, HTTP의 무상태성 <em>Statelessness</em> 때문이다. </p>
<p>서버는 매 요청을 새로운 요청으로 간주해서, 네가 방금 전에 로그인을 했는지, 아니면 어떤 페이지를 봤는지 전혀 기억하지 못해. 만약 무상태성만 있다면, 다음 페이지로 넘어갈 때마다 &quot;네가 김유진이야?&quot;라고 다시 물어봐야 하는 문제가 생기지.</p>
</blockquote>
<p>이때 쿠키가 등장해. 서버는 로그인에 성공한 사용자에게 <strong>&#39;너는 김유진이다&#39;</strong> 라는 정보가 담긴 쿠키를 넘겨줘. 브라우저는 이 쿠키를 저장했다가, 같은 서버에 요청을 보낼 때마다 이 <strong>&#39;신분증&#39;</strong> 을 함께 보내줘. 덕분에 서버는 매번 로그인 정보를 받지 않아도 사용자를 식별할 수 있는 거야.</p>
<p>주로 다음과 같은 용도로 사용돼.</p>
<ul>
<li><strong>로그인 상태 유지</strong>: 한 번 로그인하면 다시 로그인할 필요가 없도록.</li>
<li><strong>개인 맞춤 설정</strong>: &quot;오늘 다시 보지 않기&quot; 같은 옵션을 저장.</li>
<li><strong>장바구니 기능</strong>: 로그인하지 않아도 장바구니에 상품을 담아둘 수 있도록.</li>
<li><strong>트래킹 기능</strong>: 사용자의 행동을 기록하고 분석하는 용도 </li>
</ul>
<h3 id="쿠키-종류">쿠키 종류</h3>
<ol>
<li>세션 쿠키 <em>Session Cookie</em><ul>
<li>사용자가 브라우저를 사용하는 동안만 유효함.</li>
<li>브라우저는 사용자가 브라우저를 사용하는 동안 Cookie 정보를 서버로 전달.</li>
<li><code>Expires</code> , <code>Max-Age</code> [^1] 를 설정안함</li>
</ul>
</li>
<li>지속 쿠키 <em>Persistent Cookie</em><ul>
<li>사용자가 브라우저를 종료하더라도 유지되는 쿠키</li>
<li><code>Expires</code> 혹은 <code>Max-Age</code> 가 같이 설정되는 쿠키</li>
</ul>
</li>
</ol>
<h2 id="쿠키-속성">쿠키 속성</h2>
<pre><code>SESSION=61f39beb-0a39-4705-8f8e-8738caf6a162; Domain=.dooray.com; Path=/; Secure; HttpOnly; SameSite=None</code></pre><ul>
<li><code>Name</code> : 세션 키 이름</li>
<li><code>Value</code> : 세션 값</li>
<li><code>Domain</code> : 어느 도메인에서 쿠키를 사용할 수 있는지 지정</li>
<li><code>Path</code> : 어느 경로에서 쿠키를 사용할 수 있는지 지정 <pre><code>Set-Cookie: cart=items; Path=/shop
→ /shop, /shop/cart, /shop/payment에서 사용 가능
→ /profile에서는 사용 불가
</code></pre></li>
</ul>
<p>Set-Cookie: session=abc; Path=/
→ 모든 경로에서 사용 가능</p>
<pre><code>
- `Expaires` / `Max-Age` : 쿠키의 유효(만료)기간[^2]</code></pre><p>Set-Cookie: session=data; Expires=Wed, 31 Jul 2025 23:59:59 GMT
Set-Cookie: session=abc; Max-Age=3600  (1시간 후 만료)
Set-Cookie: remember=yes  (브라우저 종료시 삭제)</p>
<p>```</p>
<ul>
<li><code>HttpOnly</code><ul>
<li>보안을 위해 사용 (XSS 공격 방지)</li>
<li>JavaScrip 에서 쿠키에 접근 불가능하게 만듦</li>
</ul>
</li>
<li><code>SameSite</code><ul>
<li>크로스 사이트(다른 도메인) 요청에서 쿠키 포함여부를 결정</li>
</ul>
</li>
</ul>
<h2 id="참조">참조</h2>
<p><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Guides/Cookies">https://developer.mozilla.org/ko/docs/Web/HTTP/Guides/Cookies</a></p>
<p>[^1]: 만료 시간을 의미
[^2]: <code>Expires</code>는 <u>정확한 날짜</u>를, <code>Max-Age</code>는 <u>남은 시간</u>을 지정함</p>
]]></description>
        </item>
    </channel>
</rss>