<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ahn_s.log</title>
        <link>https://velog.io/</link>
        <description>하루하루 의미있고 행복하게! (Yesterday is History, Tomorrow is a mystery, But today is a gift)</description>
        <lastBuildDate>Fri, 30 Jan 2026 08:30:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ahn_s.log</title>
            <url>https://velog.velcdn.com/images/ahn_s/profile/02b48a47-3c0c-4b6a-9b27-0ad1935845e1/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ahn_s.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ahn_s" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[객체지향과 SOLID]]></title>
            <link>https://velog.io/@ahn_s/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EA%B3%BC-SOLID</link>
            <guid>https://velog.io/@ahn_s/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EA%B3%BC-SOLID</guid>
            <pubDate>Fri, 30 Jan 2026 08:30:03 GMT</pubDate>
            <description><![CDATA[<p>면접에서 받은 질문 2편입니다! (저 CS 질문 답변 잘 못했는데 1차합격했어요! 😆 이래서 면까몰인가.. 암튼! 2차면접도 화이팅 해보겠슴돠) 이번에는 객체지향과 SOLID에 대해서 한번 알아보겠습니다.</p>
<h2 id="객체지향">객체지향?</h2>
<p>Oracle 공식문서에서는 아래와 같이 정의했습니다.</p>
<blockquote>
<p>&quot;Object-oriented programming is a method of implementation in which programs are organized as cooperative collections of objects, each of which represents an instance of some class, and whose classes are all members of a hierarchy of classes united via inheritance relationships.&quot;</p>
</blockquote>
<p><strong>&quot;객체지향 프로그래밍은 여러 객체들이 서로 협력해서 프로그램을 만드는 방식인데, 각 객체는 클래스라는 설계도로 만들어지고, 이 클래스들은 부모-자식 상속 관계로 계층적으로 연결되어 있다.&quot;</strong></p>
<hr>
<h2 id="객체지향하면-solid-아니야">객체지향하면 SOLID 아니야?</h2>
<p>객체지향과 SOLID의 관계는 밀접하게 연결되어 있지만 객체지향 = SOLID 라는건 아닙니다.</p>
<p>객체지향 (패러다임)
    ↓
좋은 객체지향 설계가 필요
    ↓
SOLID 원칙 (가이드라인)</p>
<p>위 계층처럼 <code>객체지향</code>은 <strong>프로그래밍 패러다임으로 &quot;어떻게 프로그램을 구성할 것인가&quot;</strong>에 대한 근본적인 방식이며, <code>SOLID 원칙</code>은 <strong>좋은 객체지향적인 설계를 위한 가이드라인</strong>입니다.
<del>(저는 객체지향 = SOLID라고 머릿속에 박혀있어서 면접답변에서도 SOLID를 이야기를 해버린..)</del></p>
<hr>
<h2 id="빠질수-없는-solid-원칙">빠질수 없는 SOLID 원칙</h2>
<p>SOLID는 로버트 마틴(Uncle Bob)이 정리한 객체지향 설계 5원칙입니다.</p>
<h3 id="👀-한눈에-보기">👀 한눈에 보기</h3>
<table>
<thead>
<tr>
<th>원칙</th>
<th>한 줄 요약</th>
<th>해결하는 문제</th>
<th>핵심 키워드</th>
</tr>
</thead>
<tbody><tr>
<td><strong>SRP</strong></td>
<td>한 클래스는 한 가지 책임만</td>
<td>변경 영향 최소화</td>
<td>책임 분리</td>
</tr>
<tr>
<td><strong>OCP</strong></td>
<td>확장에 열려있고 수정에 닫혀있게</td>
<td>기존 코드 수정 최소화</td>
<td>확장 가능</td>
</tr>
<tr>
<td><strong>LSP</strong></td>
<td>부모를 자식으로 치환 가능</td>
<td>잘못된 상속 방지</td>
<td>올바른 상속</td>
</tr>
<tr>
<td><strong>ISP</strong></td>
<td>사용 안 하는 메서드에 의존X</td>
<td>불필요한 구현 방지</td>
<td>인터페이스 분리</td>
</tr>
<tr>
<td><strong>DIP</strong></td>
<td>구체가 아닌 추상에 의존</td>
<td>강한 결합 방지</td>
<td>추상화 의존</td>
</tr>
</tbody></table>
<hr>
<h3 id="각-원칙별-예시를-한번-보면-이해가-쉽겠죠-가봅시다">각 원칙별 예시를 한번 보면 이해가 쉽겠죠? 가봅시다!</h3>
<h3 id="1️⃣-srp-single-responsibility-principle">1️⃣ SRP (Single Responsibility Principle)</h3>
<h3 id="단일-책임-원칙">단일 책임 원칙</h3>
<h3 id="🔴-나쁜-예시">🔴 나쁜 예시</h3>
<pre><code class="language-java">public class User {
    private String name;
    private String email;

    public void sendEmail(String message) { ... }      // 책임 1: 이메일 발송
    public void saveToDatabase() { ... }               // 책임 2: DB 저장
    public void generateReport() { ... }               // 책임 3: 보고서 생성
}</code></pre>
<p><strong>문제:</strong> 이메일, DB, 보고서 중 하나만 바뀌어도 User 클래스를 수정해야 함!</p>
<h3 id="🟢-좋은-예시">🟢 좋은 예시</h3>
<pre><code class="language-java">public class User {
    private String name;
    private String email;
    // 사용자 정보만 관리
}

public class EmailService {
    public void send(String email, String message) { ... }
}

public class UserRepository {
    public void save(User user) { ... }
}

public class ReportGenerator {
    public void generate(User user) { ... }
}</code></pre>
<p><strong>장점:</strong> 각 클래스가 하나의 책임만! 변경 영향 최소화</p>
<hr>
<h3 id="2️⃣-ocp-open-closed-principle">2️⃣ OCP (Open-Closed Principle)</h3>
<h3 id="개방-폐쇄-원칙">개방-폐쇄 원칙</h3>
<h3 id="🔴-나쁜-예시-1">🔴 나쁜 예시</h3>
<pre><code class="language-java">public class DiscountCalculator {
    public int calculate(String type, int price) {
        if (type.equals(&quot;Regular&quot;)) return price;
        else if (type.equals(&quot;VIP&quot;)) return price * 90 / 100;    // 10% 할인
        else if (type.equals(&quot;VVIP&quot;)) return price * 80 / 100;   // 20% 할인
        return price;
    }
}</code></pre>
<p><strong>문제:</strong> 새 등급(SVIP) 추가 시 → DiscountCalculator 수정 필요!</p>
<h3 id="🟢-좋은-예시-1">🟢 좋은 예시</h3>
<pre><code class="language-java">public interface DiscountPolicy {
    int discount(int price);
}

public class RegularDiscount implements DiscountPolicy {
    public int discount(int price) { return price; }
}

public class VIPDiscount implements DiscountPolicy {
    public int discount(int price) { return price * 90 / 100; }
}

// 새 등급 추가 - 기존 코드 수정 없이!
public class SVIPDiscount implements DiscountPolicy {
    public int discount(int price) { return price * 70 / 100; }
}</code></pre>
<p><strong>장점:</strong> 새 등급 추가해도 기존 코드 수정 불필요!</p>
<hr>
<h3 id="3️⃣-lsp-liskov-substitution-principle">3️⃣ LSP (Liskov Substitution Principle)</h3>
<h3 id="리스코프-치환-원칙">리스코프 치환 원칙</h3>
<h3 id="🔴-나쁜-예시-2">🔴 나쁜 예시</h3>
<pre><code class="language-java">public class Rectangle {
    protected int width, height;
    public void setWidth(int w) { width = w; }
    public void setHeight(int h) { height = h; }
    public int getArea() { return width * height; }
}

public class Square extends Rectangle {
    @Override
    public void setWidth(int w) { width = height = w; }  // 정사각형은 가로=세로
    @Override
    public void setHeight(int h) { width = height = h; }
}

// 테스트
Rectangle rect = new Rectangle();
rect.setWidth(5);
rect.setHeight(4);
System.out.println(rect.getArea());  // 20 ✅

Rectangle square = new Square();
square.setWidth(5);
square.setHeight(4);
System.out.println(square.getArea());  // 16 ❌ (기대: 20)</code></pre>
<p><strong>문제:</strong> 부모(Rectangle)를 자식(Square)으로 치환하니 예상과 다르게 동작!</p>
<h3 id="🟢-좋은-예시-2">🟢 좋은 예시</h3>
<pre><code class="language-java">public interface Shape {
    int getArea();
}

public class Rectangle implements Shape {
    private int width, height;
    public Rectangle(int w, int h) { width = w; height = h; }
    public int getArea() { return width * height; }
}

public class Square implements Shape {
    private int side;
    public Square(int s) { side = s; }
    public int getArea() { return side * side; }
}

// 테스트
Shape rect = new Rectangle(5, 4);
Shape square = new Square(4);
System.out.println(rect.getArea());    // 20 ✅
System.out.println(square.getArea());  // 16 ✅</code></pre>
<p><strong>장점:</strong> 상속 대신 인터페이스! 치환해도 문제없음</p>
<hr>
<h3 id="4️⃣-isp-interface-segregation-principle">4️⃣ ISP (Interface Segregation Principle)</h3>
<h3 id="인터페이스-분리-원칙">인터페이스 분리 원칙</h3>
<h3 id="🔴-나쁜-예시-3">🔴 나쁜 예시</h3>
<pre><code class="language-java">public interface SmartDevice {
    void print();
    void scan();
    void fax();
}

public class MultiFunctionPrinter implements SmartDevice {
    public void print() { ... }
    public void scan() { ... }
    public void fax() { ... }
}

public class SimplePrinter implements SmartDevice {
    public void print() { ... }
    public void scan() { throw new UnsupportedOperationException(); }  // 억지 구현!
    public void fax() { throw new UnsupportedOperationException(); }   // 억지 구현!
}</code></pre>
<p><strong>문제:</strong> SimplePrinter는 print만 필요한데 scan, fax도 구현해야 함!</p>
<h3 id="🟢-좋은-예시-3">🟢 좋은 예시</h3>
<pre><code class="language-java">public interface Printer {
    void print();
}

public interface Scanner {
    void scan();
}

public interface Fax {
    void fax();
}

public class SimplePrinter implements Printer {
    public void print() { ... }  // print만 구현!
}

public class MultiFunctionPrinter implements Printer, Scanner, Fax {
    public void print() { ... }
    public void scan() { ... }
    public void fax() { ... }
}</code></pre>
<p><strong>장점:</strong> 필요한 인터페이스만 구현! 불필요한 메서드 구현 강제 안 됨</p>
<hr>
<h3 id="5️⃣-dip-dependency-inversion-principle">5️⃣ DIP (Dependency Inversion Principle)</h3>
<h3 id="의존성-역전-원칙">의존성 역전 원칙</h3>
<h3 id="🔴-나쁜-예시-4">🔴 나쁜 예시</h3>
<pre><code class="language-java">public class MySQLDatabase {
    public void save(String data) { ... }
}

public class UserService {
    private MySQLDatabase database;  // 구체 클래스에 직접 의존!

    public UserService() {
        this.database = new MySQLDatabase();
    }

    public void saveUser(String userName) {
        database.save(userName);
    }
}</code></pre>
<p><strong>문제:</strong> MySQL → PostgreSQL 변경 시 UserService 수정 필요!</p>
<h3 id="🟢-좋은-예시-4">🟢 좋은 예시</h3>
<pre><code class="language-java">public interface Database {
    void save(String data);
}

public class MySQLDatabase implements Database {
    public void save(String data) { ... }
}

public class PostgreSQLDatabase implements Database {
    public void save(String data) { ... }
}

public class UserService {
    private Database database;  // 인터페이스에 의존!

    public UserService(Database database) {  // 의존성 주입
        this.database = database;
    }

    public void saveUser(String userName) {
        database.save(userName);
    }
}

// 사용
Database mysql = new MySQLDatabase();
UserService service = new UserService(mysql);  // DB 교체 쉬움!</code></pre>
<p><strong>장점:</strong> DB 교체 시 UserService 수정 불필요! 테스트용 Mock도 쉽게 생성</p>
<hr>
<h3 id="이해하면-좋겠지만-까먹잖아요-쉽게-외워볼까요">이해하면 좋겠지만 까먹잖아요..? 쉽게 외워볼까요</h3>
<p>다들 코딩하다보면 아래와 같은 상황들을 아주 수 많이 느낄텐데요 (특히 내가 짠 코드..)
SOLID를 어긴 코드는 요런 느낌을 풀풀 풍기니 확인 한번 해보면 좋겠어요!</p>
<pre><code>S 위반 ➜ &quot;이 클래스 왜 이렇게 길어?&quot;
O 위반 ➜ &quot;if-else가 왜 이렇게 많아?&quot;
L 위반 ➜ &quot;상속받았는데 이상하게 동작하네?&quot;
I 위반 ➜ &quot;왜 안 쓰는 메서드까지 구현해야 해?&quot;
D 위반 ➜ &quot;DB 바꾸려면 코드 다 뜯어고쳐야 하네?&quot;</code></pre><p>이렇게 외우면 쉬울까요..? 단(일책임)개(방폐쇄)이(리스코프)분(리)역(전)
<code>이번역은 단개이분역 입니다.</code> (죄송합니다.. 혹시라도 쉽게 외우는방법이 생각나면 수정해볼게요)</p>
<hr>
<h1 id="마무리">마무리</h1>
<p>일단 마지막에 분위기 싸하게 만들어서 죄송하다는 말씀 먼저 드리겠습니다. 그냥 해보고 싶었어요! (어차피 아무도 안보잖아요..)</p>
<p>무튼! 객체지향과 SOLID에 대해 잘 알아가고 다음엔 당당하게 말할 수 있었으면 좋겠습니다!</p>
<p>다음 포스팅은 <code>클래스, 추상클래스, 인터페이스</code>에 대해 공부해볼거에요!</p>
<h3 id="끝까지-읽어주신-분들께-감사드리며-오늘도-행복한-하루-되셨으면-좋겠습니다">끝까지 읽어주신 분들께 감사드리며 오늘도 행복한 하루 되셨으면 좋겠습니다!</h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java에 대하여]]></title>
            <link>https://velog.io/@ahn_s/Java%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@ahn_s/Java%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Mon, 19 Jan 2026 08:35:51 GMT</pubDate>
            <description><![CDATA[<p>오늘은 면접을 보고 왔지만 기초가 부족하다는걸 느껴 작성하게 되었습니다. 
(Like 말하는 감자.. 아무래도 답변을 잘 못해서 떨어졌겠지만 다음엔 딴딴하게 간다!) 😭</p>
<h2 id="java란-어떤-언어인가">Java란 어떤 언어인가?</h2>
<p>내가 알기로는 자바는 컴파일과 인터프리터의 특성을 모두 가진 하이브리드 언어로 알고 있는데, 맞을까?</p>
<p>일단, Java는 컴파일과 인터프리터를 모두 사용하는 <code>하이브리드 언어</code>가 맞다!</p>
<blockquote>
<p><code>Java의 실행 과정</code></p>
</blockquote>
<ol>
<li>컴파일 단계: .java 소스코드 → .class 바이트코드 (javac 컴파일러)</li>
<li>클래스 로더: 바이트코드 → 메모리 영역 로딩</li>
<li>인터프리터/JIT 단계: 바이트코드 → 기계어 (JVM이 실행)<blockquote>
</blockquote>
EX) Hello.java → javac → Hello.class → JVM → 실행</li>
</ol>
<p>Java는 컴파일 단계에서 바이트코드로 변환하고 JVM이 바이트코드를 기계어로 변환하는 실행과정을 거치기에 하이브리드 언어라고 할 수 있다고 합니다..! </p>
<h3 id="﹖-근데-컴파일-언어-하면-java가-떠오르고-인터프리터-언어-하면-python이-떠오르는데-뭐가-어째-된겁니까">﹖ 근데 컴파일 언어 하면 Java가 떠오르고 인터프리터 언어 하면 Python이 떠오르는데 뭐가 어째 된겁니까?!</h3>
<p>(사실 여기서 제가 잘 모른다는걸 느꼈습니다.
&#39;자바가 인터프리터의 역할을 수행한다고요?&#39;라는 질문을 받았을 때 답변에 확.신을 가지지 못한거죠 ㅠㅠ)</p>
<p>그래서 바로 GPT와 토론을 해보았죠.</p>
<p>Q. 왜 대중적으로 Java는 컴파일언어 / Python은 인터프리터 언어라고 할까? 사실은 하이브리드 언어라는데
A. &quot;Java는 컴파일 언어, Python은 인터프리터 언어다!&quot; 이렇게 알고 계신 분들 많으시죠? 저도 처음엔 그렇게 배웠어요. (핳 지선생님도 이렇게 알고 있었데요. 나만 이상한거 아녔어)</p>
<p>일단 두괄식으로 갑시다. 결론으로는 Java, Python <code>모두 하이브리드</code> 언어 라는것!</p>
<blockquote>
<p>💡 왜 &quot;Java = 컴파일 언어&quot;로 알려졌을까?</p>
</blockquote>
<pre><code># 1. 컴파일 (명시적으로 해야 함)
javac Hello.java

# 2. 실행
java Hello</code></pre><p>이처럼 Java는 컴파일 하는 과정이 눈으로 명시적으로 보이고, Hello.class 파일도 직접 생성되니까 컴파일 언어처럼 느껴지니 <code>대중적으로</code> 컴파일 언어이다라고 생각이 드는거였습니다.</p>
<blockquote>
<p>💡 그럼 Python은?</p>
</blockquote>
<pre><code>python hello.py</code></pre><p>파이썬은 CLI에서 이렇게 실행하죠? 하지만, 내부적으로는 컴파일이 자동으로 일어나서 사용자는 의식하지 못합니다. 그래서 인터프리터 언어처럼 느껴지는것이고 사실 Python도 _<em>pycache_</em> 폴더에 .pyc 바이트코드를 만들기 때문에 하이브리드 언어라는 것이죠!</p>
<pre><code>실행 과정 : 소스코드 → 바이트코드로 컴파일 → 가상머신이 실행 (하이브리드)

차이점:
Java: 컴파일이 눈에 보임 (javac 명령 필요, .class 파일 생성)
Python: 컴파일이 자동/숨겨짐 (한 번에 실행, .pyc는 뒤에서 생성)

결론:
둘 다 같은 방식인데, 사용자 경험이 달라서 Java는 &quot;컴파일 언어&quot;,
Python은 &quot;인터프리터 언어&quot;처럼 느껴진다!</code></pre><hr>
<br>
다음! 일단 저희의 큰 주제가 Java죠?? Java에 대해서 좀 더 가봅시다!



<h2 id="1-jvm이-뭐야">1. JVM이 뭐야??</h2>
<p>Java 프로그램을 실행시키는 가상 머신으로, 자바 소스 코드를 컴파일하여 생성된 바이트코드(.class 파일)를 운영체제(OS) 환경에 맞게 해석하고 실행하는 역할을 수행합니다.</p>
<blockquote>
<p>이를 한번 게임기에 비유해볼까요?</p>
</blockquote>
<p>다들 닌텐도 아시죠? (저도 포켓몬이랑 리듬세상 엄청했었는데..) 닌텐도를 즐기려면 기기말구 전용 카트리지가 꼭 필요해요!
<code>게임 카트리지(바이트코드)를 게임기(JVM)에 꽂으면 → 화면에 게임(프로그램)이 실행되는 것처럼!</code> 바이트코드는 JVM만 있으면 어디서든 실행 가능합니다. </p>
<p>🤔 왜 JVM이 필요할까?
JVM 없이 프로그램을 실행한다면?</p>
<pre><code>Windows용 프로그램.exe  → ❌ Mac에서 실행 안 됨
Mac용 프로그램.app      → ❌ Linux에서 실행 안 됨
운영체제마다 따로따로 만들어야 해요. 😭</code></pre><p>JVM을 사용하면?</p>
<pre><code>Hello.class (바이트코드)
    ↓
Windows JVM → ✅ Windows에서 실행
Mac JVM     → ✅ Mac에서 실행  
Linux JVM   → ✅ Linux에서 실행
한 번 작성하면 어디서든 실행! (Write Once, Run Anywhere)</code></pre><p>이게 바로 JVM의 힘입니다! 근데 이거말고 JVM은 메모리 관리까지 같이 해줍니다! </p>
<hr>
<h3 id="jvm-내부-구조">JVM 내부 구조</h3>
<p>JVM은 3개 영역으로 구성 되어 있습니다.</p>
<pre><code>Class Loader → 메모리 영역 → 실행 엔진
   (로딩)        (저장)       (실행)</code></pre><blockquote>
<p>  Q. 각각 뭐 하는 건데?</p>
</blockquote>
<h3 id="📁-class-loader">📁 Class Loader</h3>
<ul>
<li>.class 파일을 메모리로 불러오는 역할</li>
</ul>
<h3 id="💾-메모리-영역-runtime-data-area">💾 메모리 영역 (Runtime Data Area)</h3>
<p><code>공유 영역 (모든 스레드가 함께 사용, GC 대상)</code></p>
<ul>
<li>Method Area: 클래스 정보, static 변수 저장</li>
<li>Heap: 객체(new로 생성한 것)가 저장되는 곳</li>
</ul>
<p><code>스레드별 개별 영역</code></p>
<ul>
<li>Stack: 메서드 호출 시 지역변수, 매개변수 저장 (메서드 끝나면 자동 삭제)</li>
<li>PC Register: 현재 실행 중인 명령어 위치 기록</li>
<li>Native Method Stack: C/C++ 같은 네이티브 코드 실행 정보 저장</li>
</ul>
<h3 id="⚙️-실행-엔진">⚙️ 실행 엔진</h3>
<p>바이트코드를 기계어로 변환하고 실행합니다.</p>
<ul>
<li>인터프리터: 바이트코드를 한 줄씩 해석해서 실행</li>
<li>JIT 컴파일러: 자주 쓰는 코드를 미리 기계어로 변환해서 캐시에 저장 → 성능 향상!</li>
</ul>
<p>JIT 컴파일러 = 실행 중에 코드를 최적화하는 컴파일러</p>
<pre><code>Q. JIT는 뭘 해주는데?
1. 자주 실행되는 코드 발견 👀
2. 기계어로 미리 변환해서 캐시에 저장 💾
3. 다음번엔 캐시에서 바로 꺼내 씀 ⚡
A. 반복되는 코드 실행 속도가 엄청 빨라짐!</code></pre><hr>
<p>** 📌 이제 이렇게 답변해봅시다! **</p>
<pre><code>Q. Java는 컴파일 언어인가요, 인터프리터 언어인가요?
A. Java는 두 가지 방식을 모두 사용하는 하이브리드 언어입니다.
소스코드를 javac로 바이트코드로 컴파일한 후, JVM이 인터프리터와 JIT 컴파일러를 통해 
실행합니다.</code></pre><pre><code>Q. JVM이 뭔가요?
A. JVM은 자바 바이트코드를 실행하는 가상 머신으로, 
운영체제에 독립적으로 Java 프로그램을 실행할 수 있게 해주며,
메모리 관리(GC)도 자동으로 처리합니다.</code></pre><pre><code>Q. JIT 컴파일러가 뭔가요?
A. 런타임 중에 자주 실행되는 코드를 기계어로 미리 컴파일해서 캐싱하는 기술로, 
Java의 실행 속도를 크게 향상시킵니다.</code></pre><hr>
<h3 id="마무리">마무리</h3>
<p>자바는 객체지향 언어라는거 다들 알고 계시져? 다음에는 <code>객체지향</code>이 무엇인지와 <code>클래스, 추상클래스, 인터페이스</code>란 무엇인지에 대해 포스팅을 작성하고자 합니다.</p>
<h3 id="끝까지-읽어주신-분들께-감사드리며-오늘도-행복한-하루-되셨으면-좋겠습니다">끝까지 읽어주신 분들께 감사드리며 오늘도 행복한 하루 되셨으면 좋겠습니다!</h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[Trie (With.매일메일)]]></title>
            <link>https://velog.io/@ahn_s/Trie-With.%EB%A7%A4%EC%9D%BC%EB%A9%94%EC%9D%BC</link>
            <guid>https://velog.io/@ahn_s/Trie-With.%EB%A7%A4%EC%9D%BC%EB%A9%94%EC%9D%BC</guid>
            <pubDate>Mon, 12 Jan 2026 11:14:29 GMT</pubDate>
            <description><![CDATA[<p>GPT와 토론한 내용을 바탕으로 작성한 포스트 입니다. 틀린부분이 있다면 피드백 마구마구 주시면 감사드리겠습니다.</p>
<h2 id="trie-자료구조란">Trie 자료구조란?</h2>
<p>문자열을 저장하고 효율적으로 탐색하기 위한 트리 형태의 자료 구조입니다. 트라이는 문자열을 탐색할 때 단순히 비교하는 것에 비해서 효율적으로 찾을 수 있지만, 각 정점이 자식에 대한 링크를 모두 가지고 있기 때문에 저장 공간을 더욱 많이 사용한다는 특징이 있습니다.</p>
<p>주로 검색어 자동완성, 사전찾기 기능을 구현할 때 사용합니다.</p>
<h2 id="어떻게-동작하는거지">어떻게 동작하는거지?</h2>
<p><img src="https://velog.velcdn.com/images/ahn_s/post/aa47719f-55a4-410f-abcd-a543ad093d1e/image.png" alt="">
위 그림처럼 Trie는 단어를 한 글자씩 나눠서 나무처럼 저장하는 방식입니다!
<code>단어 저장</code></p>
<pre><code>1. Root에서 시작: 모든 단어는 최상단에서 출발하며 Root는 빈 문자열이 들어있습니다.

2. 한 글자씩 따라가기: 각 노드는 하나의 문자를 의미
CAR,CAR,DOG라는 문자를 저장할 때에는 Root에서 가지를 뻗어 저장합니다.
그림에서는 총 2갈래 &#39;C&#39;와 &#39;D&#39;로 분리됩니다.

3. C 경로 저장하기
&#39;C&#39;로 시작하는 단어들(CAT, CAR)은 같은 경로를 공유합니다:

- C 노드를 생성합니다
- 다음 글자인 &#39;A&#39;로 이동합니다
- A 노드에서 다시 2갈래로 분리됩니다:
  - &#39;T&#39;로 가는 경로 → CAT 완성
  - &#39;R&#39;로 가는 경로 → CAR 완성

4. D 경로 저장하기
&#39;D&#39;로 시작하는 단어(DOG)는 별도 경로를 만듭니다:

- D 노드를 생성합니다
- &#39;O&#39; 노드로 이동합니다
- &#39;G&#39; 노드로 이동합니다 → DOG 완성</code></pre><blockquote>
<p>Trie 저장 장점</p>
</blockquote>
<ul>
<li>공통 접두사 공유: CAT과 CAR은 &#39;CA&#39;를 공유하여 메모리 절약</li>
<li>검색 속도: 단어 길이만큼만 탐색 (3글자 단어는 3단계)<ul>
<li>시간 복잡도
검색 시간 = O(n) (n = 단어의 길이)
&quot;CAR&quot; 검색 = 3단계 (C→A→R)
&quot;DOG&quot; 검색 = 3단계 (D→O→G)</li>
</ul>
</li>
</ul>
<p><code>단어 검색</code></p>
<pre><code>예시: &quot;CAR&quot;을 검색

- 1단계: Root에서 시작
Root 노드(빈 문자열)에서 출발합니다

- 2단계: 첫 글자 &#39;C&#39; 찾기
Root의 자식 노드들을 확인합니다.&#39;C&#39;노드가 있는지 확인하고 있다면 &#39;C&#39;노드로 이동합니다.

- 3단계: 두 번째 글자 &#39;A&#39; 찾기
현재 노드(&#39;C&#39;)의 자식 노드들을 확인합니다. &#39;A&#39;노드가 있는지 확인하고 있다면 &#39;A&#39;노드로 이동합니다.

- 4단계: 세 번째 글자 &#39;R&#39; 찾기
현재 노드(&#39;A&#39;)의 자식 노드들을 확인합니다. &#39;R&#39;노드가 있는지 확인하고 있다면 &#39;R&#39;노드로 이동합니다.</code></pre><p>이처럼 Trie 자료구조는 데이터베이스의 Index와 비슷하게 단어의 색인을 찾아나가는 방식을 통해 시간복잡도 O(n)으로 원하는 단어를 빠르게 찾을 수 있습니다.</p>
<p>개발을 하다보면 검색기능을 많이 사용합니다. 그러면 검색기능을 만들면서 항상 떠오르는게 무엇일까요?
.
.
.
그쵸..! 자동완성입니다. (저는 검색 부분 구현하면 떠오르던데..) 그래서 이 자동완성 기능을 구현할 때 Trie 자료구조를 사용한다면 빠르게 기능을 완성할 수 있을 겁니다.</p>
<p>이제 Trie를 사용한 문제를 풀어보며 실제로 Java 코드로는 어떻게 구현해야 하는지 알아봅시다!</p>
<h2 id="백준-14426---접두사-찾기">백준 14426 - 접두사 찾기</h2>
<p>문제 및 조건 : </p>
<pre><code>문제

문자열 S의 접두사란 S의 가장 앞에서부터 부분 문자열을 의미한다. 예를 들어, S = &quot;codeplus&quot;의 접두사는 
&quot;code&quot;, &quot;co&quot;, &quot;codepl&quot;, &quot;codeplus&quot;가 있고, &quot;plus&quot;, &quot;s&quot;, &quot;cude&quot;, &quot;crud&quot;는 접두사가 아니다.

총 N개의 문자열로 이루어진 집합 S가 주어진다.

입력으로 주어지는 M개의 문자열 중에서 집합 S에 포함되어 있는 문자열 중 적어도 하나의 접두사인 것의 개수를 구하는 프로그램을 작성하시오.

조건

시간 제한 1초
N과 M (1 ≤ N ≤ 10,000, 1 ≤ M ≤ 10,000)
입력으로 주어지는 문자열은 알파벳 소문자로만 이루어져 있으며, 길이는 500을 넘지 않는다.</code></pre><p>이거를 브루트포스로 푼다면 <code>N(10,000) * M(10,000) \* 500 = 50억</code>으로 시간초과가 발생합니다!!</p>
<p>그렇다면 Trie 자료구조로 문제를 푼다면?  <code>O(N) + O(M) + 500 으로 20,500</code>밖에 걸리지 않겠죠?
좋습니다 한번 가봅시다.</p>
<p>일단 문제 풀이를 한번 작성해봅시다.</p>
<pre><code>0. Trie 자료구조 생성

1. N개의 집합을 Trie에 삽입

2. M 개의 문자열을 돌며 문자열이 접두사인지 확인

2-1. 접두사가 맞다면 count + 1

2-2. 중간에 노드가 없다면 (접두사가 아니라면) 종료

3. 접두사의 개수(count) 출력</code></pre><p><code>0. N개의 집합을 Trie에 삽입</code></p>
<ul>
<li>자식 노드들을 저장하는 TrieNode 클래스</li>
</ul>
<pre><code>class TrieNode {
    // 자식 노드들을 저장할 Map
    Map&lt;Character, TrieNode&gt; children;

    public TrieNode() {
        children = new HashMap&lt;&gt;();
    }
}</code></pre><blockquote>
<ul>
<li>왜 HashMap을 사용하나요?</li>
</ul>
</blockquote>
<p>알파벳 소문자만 있다면 TrieNode[] children = new TrieNode[26] 배열도 가능
하지만 HashMap이 더 유연하고 메모리 효율적으로 있는 문자만 저장하면 됨!</p>
<ul>
<li>실제 트리를 관리하는 Trie 클래스</li>
</ul>
<pre><code>class Trie {
    TrieNode root;

    public Trie() {
        root = new TrieNode();
    }

    // 문자열을 Trie에 삽입하는 메서드
    public void insert(String word) {
        TrieNode node = root;  // 루트에서 시작

        // 문자열을 한 글자씩 순회
        for (int i = 0; i &lt; word.length(); i++) {
            char ch = word.charAt(i);

            // 해당 문자의 노드가 없으면 생성
            if (!node.children.containsKey(ch)) {
                node.children.put(ch, new TrieNode());
            }

            // 다음 노드로 이동
            node = node.children.get(ch);
        }
    }

    // 문자열이 Trie에 저장된 문자들의 접두사인지 검색
    public boolean search(String word) {
        TrieNode_14426 node = root;

        // 문자열을 한 글자씩 순회
        for (int i = 0; i &lt; word.length(); i++) {
            char c = word.charAt(i);

            // 해당 문자의 노드가 없다면 접두사 X
            if (node.children.get(c) == null) {
                return false;
            }

            // 다음 노드(글자)로 이동
            node = node.children.get(c);
        }

        // 문자열을 모두 돌았다면 접두사 O
        return true;
    }
}</code></pre><p><code>1. N개의 집합을 Trie에 삽입</code></p>
<pre><code>// Trie 인스턴스 생성
Trie_14426 trie = new Trie_14426();

// N개의 문자 삽입
for (int i = 0; i &lt; N; i++) {
    String word = br.readLine();
    trie.insert(word);
}</code></pre><p><code>2. M개의 M 개의 문자열을 돌며 문자열이 접두사인지 확인</code></p>
<pre><code>// 접두사인 문자열 개수 카운트하는 변수
int count = 0;

// M개의 문자열 순회
for (int i = 0; i &lt; M; i++) {
    String word = br.readLine();
    // 만약 Trie에 저장된 문자중에 M이 접두사라면 count 증가
    if(trie.search(word)) {
        count++;
    }
}</code></pre><p><code>전체코드</code></p>
<pre><code>class TrieNode_14426 {
    Map&lt;Character, TrieNode_14426&gt; children;
    boolean isEnd;

    public TrieNode_14426(boolean isEnd) {
        children = new HashMap&lt;&gt;();
        this.isEnd = isEnd;
    }
}

class Trie_14426 {
    TrieNode_14426 root;

    public Trie_14426() {
        root = new TrieNode_14426(false);
    }

    public void insert(String word) {
        TrieNode_14426 node = root;

        for (int i = 0; i &lt; word.length(); i++) {
            char c = word.charAt(i);

            if (!node.children.containsKey(c)) {
                boolean isEnd = i == word.length() - 1;

                node.children.put(c, new TrieNode_14426(isEnd));
            }

            node = node.children.get(c);
        }
    }

    public boolean search(String word) {
        TrieNode_14426 node = root;

        for (int i = 0; i &lt; word.length(); i++) {
            char c = word.charAt(i);
            if (node.children.get(c) == null) {
                return false;
            }
            node = node.children.get(c);
        }

        return true;
    }
}

public class BOJ_14426 {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        int N = Integer.parseInt(st.nextToken());
        int M = Integer.parseInt(st.nextToken());

        // Trie 인스턴스 생성
        Trie_14426 trie = new Trie_14426();

        // N개의 문자 삽입
        for (int i = 0; i &lt; N; i++) {
            String word = br.readLine();
            trie.insert(word);
        }

        int count = 0;

        for (int i = 0; i &lt; M; i++) {
            String word = br.readLine();
            if(trie.search(word)) {
                count++;
            }
        }

        System.out.println(count);
    }
}
</code></pre><hr>
<p>여기까지 Trie 자료구조에 대해서 설명했지만, 저는 웹 개발을 지망하고 있기 때문에, 한가지 궁금한 점이 생겼습니다.</p>
<h3 id="그런데-그냥-elasticsearch-사용하면-되지-않아">그런데... 그냥 ElasticSearch 사용하면 되지 않아..?</h3>
<p>GPT의 답변은...</p>
<pre><code>결론부터 말하면
대부분의 웹 개발에서는 ElasticSearch가 검색 분야의 끝판왕 같은 존재이기에 ES를 주로 사용합니다!</code></pre><p>예...? 이건 그냥 코테를 위한 용도였던 것 인 걸까?! 그래도 Trie와 ElasticSearch를 어떨때 사용하면 좋을지 알려주더군요..!</p>
<ol>
<li>Elasticsearch와 Trie의 결정적 차이
Elasticsearch는 내부적으로 Inverted Index(역색인) 구조를 사용하며, 그 안에서 문자열 검색을 최적화하기 위해 Trie의 변형인 FST(Finite State Transducer) 등을 사용합니다. 즉, ES를 쓴다는 건 이미 누군가 잘 만들어놓은 Trie 계열의 최적화된 엔진을 쓰는 것과 같습니다.</li>
</ol>
<p>하지만 직접 Trie를 사용해야 하거나 고려하게 되는 이유는 다음과 같습니다.</p>
<ol start="2">
<li>왜 Trie를 써야 할까? (Elasticsearch와의 차이점)
극강의 가벼움과 속도 (In-Memory 최적화): ES는 별도의 서버를 띄워야 하고 네트워크 통신(REST API)이 필요합니다. 반면, Trie는 내 애플리케이션의 <strong>메모리 안(In-Memory)</strong>에 바로 올릴 수 있습니다. 네트워크 오버헤드가 없기 때문에 단순한 &#39;자동 완성&#39;이나 &#39;금칙어 필터링&#39; 같은 기능은 Trie가 훨씬 빠릅니다.</li>
</ol>
<ul>
<li><p>비용과 리소스: ES는 실행하는 데 상당한 CPU와 RAM 리소스가 필요합니다. 단순한 단어 사전 확인이나 아주 작은 규모의 자동 완성 기능을 위해 ES 서버를 구축하고 유지하는 것은 &quot;닭 잡는 데 소 잡는 칼&quot;을 쓰는 격일 수 있습니다.</p>
</li>
<li><p>프론트엔드/엣지 컴퓨팅: 서버까지 가지 않고 사용자의 브라우저(JavaScript) 단에서 즉시 자동 완성을 보여주고 싶을 때 Trie를 구현해서 넣으면 서버 부하를 제로로 만들면서 즉각적인 반응 속도를 줄 수 있습니다.</p>
</li>
</ul>
<blockquote>
<p>요약하자면
Elasticsearch는 &quot;구글 같은 검색 기능&quot;이나 &quot;복합적인 필터링&quot;이 필요할 때 사용하는 완제품입니다.</p>
</blockquote>
<p>Trie는 그 완제품을 만드는 핵심 부품 중 하나이며, 특정 기능을 아주 가볍고 빠르게 구현하고 싶을 때 직접 꺼내 쓰는 도구입니다.</p>
<blockquote>
</blockquote>
<p>실무에서는 보통 전체 검색은 ES에 맡기되, 아주 빈번하게 일어나는 초성 검색이나 실시간 자동 완성 같은 부분은 Redis 같은 메모리 DB 상에서 Trie 구조를 응용해 별도로 처리하기도 합니다.</p>
<p>....🤯🤯🤯 어우 어려워 좀 더 쉽게 알아가 봅시다.</p>
<p>일단 비유로 이해해 봅시다. (요리도구 vs 밀키트)</p>
<ul>
<li><p>Trie (자료구조): 이건 잘 드는 <strong>&#39;칼&#39;이나 &#39;도마&#39;</strong> 같은 도구예요. 내가 직접 재료를 썰고 요리해서 내놓아야 하지만, 손에 익으면 아주 빠르고 가볍죠.</p>
</li>
<li><p>Elasticsearch (ES): 이건 모든 요리가 다 준비되어 있는 <strong>&#39;대형 뷔페 레스토랑&#39;</strong>이에요. 주문만 하면 다 나오지만, 레스토랑을 차리는 데 돈도 많이 들고 관리인원도 필요하겠죠?</p>
</li>
</ul>
<p>그렇기 때문에 ES가 있는데도 Trie를 쓰는 이유는 무엇이냐?!</p>
<p><code>① 서버 안에 바로 있다.</code>
ES를 사용하면 외부에 &quot;너&quot;로 시작하는 단어 뭐야? 라고 물어보고 답변을 기다려야 하겠죠? 하지만 우리의 Trie는 메모리에 바로 저장해둔 것과 같이 물어볼 필요 없이 즉시 답변을 얻을 수 있다는 장점이 있습니다.</p>
<p><code>② 가성비가 끝내줘요</code>
단순히 자동 완성이나 비속어 필터링 하나 만들려고 거대한 ES 서버를 따로 띄우는건 낭비입니다 낭비!
그렇다면 우리의 Trie는 코드 몇줄로 아주 적은 용량만 차지하며 빠르게 처리를 해줍니다.</p>
<h3 id="그래서-결론-언제-어떤걸-사용할까요">그래서 결론. 언제 어떤걸 사용할까요?</h3>
<p>Trie: 앱 안에서 아주 빠르게 단어 몇 개만 자동 완성해주고 싶어. 서버 늘리기는 싫어! (적은 데이터 - 수십, 수만)</p>
<p>Elasticsearch: 수만 개의 게시물 중에서 오타도 좀 봐주고, 유사한 내용도 찾아주는 진짜 &#39;검색 엔진&#39;이 필요해! (대용량 데이터 - 수백만)</p>
<hr>
<p>이렇듯 ES도 내부적으로는 Trie 자료구조를 사용하고 있다는 GPT 피셜이 있었기때문에 Trie의 개념은 알아두도록 합시다! 기초는 알아두면 백익무해! (백해무익 순서만 바꿔봤어요.. ㅎ 그리고 면접에서도 Trie에 대해서 종종 질문을 하신다고 하더라구요..!)</p>
<p>👍 <code>아무튼 이렇듯 저렇듯 기초가 중요하다는거!!</code></p>
<hr>
<h3 id="끝까지-읽어주신-분들께-감사드리며-오늘도-행복한-하루-되셨으면-좋겠습니다">끝까지 읽어주신 분들께 감사드리며 오늘도 행복한 하루 되셨으면 좋겠습니다!</h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[Keep Alive (with. 매일메일)]]></title>
            <link>https://velog.io/@ahn_s/Keep-Alive-with.-%EB%A7%A4%EC%9D%BC%EB%A9%94%EC%9D%BC</link>
            <guid>https://velog.io/@ahn_s/Keep-Alive-with.-%EB%A7%A4%EC%9D%BC%EB%A9%94%EC%9D%BC</guid>
            <pubDate>Wed, 07 Jan 2026 07:38:07 GMT</pubDate>
            <description><![CDATA[<p>GPT와 토론한 내용을 바탕으로 작성한 포스트 입니다. 틀린부분이 있다면 피드백 마구마구 주시면 감사드리겠습니다.</p>
<h3 id="keep-alive-란">Keep Alive 란?</h3>
<p>네트워크 또는 시스템에서 커넥션을 지속해서 유지하기 위해 사용되는 기술이나 설정을 의미합니다. TCP 연결 자체를 재사용하기 위한 메커니즘이라고 이해하면 될 것 같습니다.</p>
<h3 id="keep-alive의-장점과-단점은-무엇이-있을까요">Keep Alive의 장점과 단점은 무엇이 있을까요?</h3>
<p>일단 Keep Alive의 장단점 먼저 알아봐야 겠죠?</p>
<p><code>장점</code></p>
<ul>
<li>커넥션을 재사용하여 네트워크 비용을 절감할 수 있습니다.</li>
<li>handshake에 필요한 RTT(Round Trip Time)가 감소하여 네트워크 지연 시간(Latency)을 줄일 수 있습니다.</li>
<li>handshake 과정에서 발생하는 CPU, 메모리 등의 리소스 소비를 줄일 수 있습니다.</li>
</ul>
<p><code>단점</code></p>
<ul>
<li>유휴 상태일 때에도 커넥션을 점유하고 있기 때문에 서버의 소켓이 부족해질 수 있습니다.</li>
<li>DoS 공격으로 다수의 연결을 길게 유지하여 서버를 과부하시킬 수 있습니다.</li>
<li>타임아웃 설정이 적절하지 않으면 커넥션 리소스가 낭비될 수 있습니다.</li>
</ul>
<h3 id="어디에-속해있고-어떻게-동작하지">어디에 속해있고 어떻게 동작하지?</h3>
<p>HTTP/1.1부터는 Keep-Alive가 Header에 기본값으로 등록되어 있습니다. 헤더에 <code>Connection: keep-alive</code>가 명시되어 있거나, 또는 아무것도 없으면 자동으로 유지됩니다. 끊고 싶다면 <code>Connection: close</code>를 사용하여 끊을 수 있습니다.</p>
<pre><code>Connection: keep-alive
Keep-Alive: timeout=5, max=100

- timeout: 유휴 연결이 계속 열려 있어야 하는 최소한의 시간(초 단위)을 가르킵니다. 
keep-alive TCP 메시지가 전송 계층에 설정되지 않는다면 TCP 타임아웃 이상의 타임아웃은 
무시된다는 것을 알아두시기 바랍니다.

- max: 연결이 닫히기 이전에 전송될 수 있는 최대 요청 수를 가리킵니다.
만약 0이 아니라면, 해당 값은 다음 응답 내에서 다른 요청이 전송될 것이므로 
비-파이프라인 연결의 경우 무시됩니다. 
HTTP 파이프라인은 파이프라이닝을 제한하는 용도로 해당 값을 사용할 수 있습니다.</code></pre><ul>
<li><p><code>Keep-Alive 없을 때</code>
클라이언트 연결 들어옴 → 요청 처리 → 응답 보냄 → 연결 종료 → 리소스 정리
새 연결 들어옴 → 요청 처리 → 응답 보냄 → 연결 종료 → 리소스 정리
새 연결 들어옴 → ... (반복)</p>
</li>
<li><p><code>Keep-Alive 있을 때</code>
클라이언트 연결 들어옴 → 요청 처리 → 응답 보냄 → 연결 유지 → 대기
같은 연결에서 새 요청 → 처리 → 응답 → 연결 유지 → 대기
같은 연결에서 새 요청 → 처리 → 응답 → timeout 후 종료</p>
</li>
</ul>
<p><strong>즉, TCP 연결을 time-out 동안 유지시켜 3-way-handshake의 사용을 줄여 TCP 연결에 대한 생성/종료 오버헤드 감소가 된다는 것이죠!</strong></p>
<h3 id="http-keep-alive--tcp-keep-alive">HTTP Keep-Alive / TCP Keep-Alive</h3>
<p>Keep-Alive도 HTTP 프로토콜과 TCP 프로토콜로 나뉘어 집니다.</p>
<blockquote>
<p><code>HTTP Keep-Alive</code>
여러 HTTP 요청을 하나의 TCP 연결로 처리합니다.
클라이언트에서 일정 시간 동안 요청이 없으면 타임아웃만큼 커넥션을 유지하고, 타임아웃이 지나면 커넥션이 끊어집니다.
<br>
HTTP Keep-Alive는 짧은 시간(초 단위) 동안 연결을 재사용하여 성능을 최적화하는 것이 목적입니다.</p>
<p>제가 위에 설명한게 HTTP 프로토콜에서의 Keep-Alive 동작 방식입니다.</p>
</blockquote>
<blockquote>
<p><code>TCP Keep-Alive</code>
장시간 유휴 상태인 TCP 연결이 실제로 살아있는지 확인하기 위한 메커니즘입니다.
일정 시간(기본 2시간) 동안 데이터 전송이 없으면, OS가 작은 probe 패킷을 보내 상대방이 응답하는지 확인합니다.</p>
<p>TCP Keep-Alive는 긴 시간(시간 단위) 동안 죽은 연결을 감지하고, NAT/방화벽 타임아웃을 방지하는 것이 목적입니다.</p>
</blockquote>
<pre><code>클라이언트 ↔ 서버: DB 커넥션 연결 (데이터베이스 연결 풀)
[2시간 동안 쿼리 없음]
OS: &quot;연결 살아있나?&quot; (probe 패킷 전송)
서버: &quot;살아있어!&quot; (ACK 응답)
→ 연결 유지

만약 응답 없으면:
OS: 재시도 (보통 9번)
계속 응답 없음 → 연결 종료</code></pre><p><code>정리 테이블</code></p>
<table>
<thead>
<tr>
<th>구분</th>
<th>HTTP Keep-Alive</th>
<th>TCP Keep-Alive</th>
</tr>
</thead>
<tbody><tr>
<td><strong>레벨</strong></td>
<td>HTTP (애플리케이션)</td>
<td>TCP (OS)</td>
</tr>
<tr>
<td><strong>목적</strong></td>
<td>연결 재사용으로 성능 향상</td>
<td>죽은 연결 감지</td>
</tr>
<tr>
<td><strong>타임아웃</strong></td>
<td>짧음 (5-15초)</td>
<td>김 (2시간)</td>
</tr>
<tr>
<td><strong>사용 예시</strong></td>
<td>웹 페이지 리소스 로딩</td>
<td>DB 커넥션, WebSocket</td>
</tr>
</tbody></table>
<p>** HTTP Keep-Alive는 성능 최적화, TCP Keep-Alive는 연결 상태 확인이 목적입니다.
이름만 비슷할 뿐 완전히 다른 레벨에서 작동하는 독립적인 기술이라고 생각하시면 될 것 같습니다.**</p>
<hr>
<h3 id="keep-alive-시간이-길면-길수록-좋을까-❌">Keep-Alive 시간이 길면 길수록 좋을까? ❌</h3>
<ol>
<li>Socket 제한 문제
서버가 열 수 있는 Socket은 유한해요.<pre><code>서버 Socket 제한: 1024개 (OS 레벨)
</code></pre></li>
</ol>
<p>Keep-Alive 짧음 (5초):
사용자 A → 요청 → Socket 생성 → 5초 후 Socket 종료
→ 사용자 B가 그 Socket 사용 가능 ✅</p>
<p>Keep-Alive 김 (1시간):
사용자 A → 요청 → Socket 생성 → 1시간 동안 점유
→ 실제로 안 쓰는데 Socket만 차지
→ Socket 1024개 다 차면 사용자 B 연결 불가 ❌</p>
<pre><code>
2. DoS 공격 대응
Dos 공격에도 취약해 질 수 있어요 time-out 시간이 길다면 Dos Bot이 모든 Connection을 점유할 수 있어요.
</code></pre><p>DoS 공격 (봇 100개):</p>
<p>Keep-Alive 짧음 (10초):
봇 100개 접속 → 10초 후 자동 정리
→ 정상 사용자 연결 가능 ✅</p>
<p>Keep-Alive 김 (1시간):
봇 100개 접속 → 1시간 동안 점유
→ Socket 고갈
→ 정상 사용자 접속 불가 ❌</p>
<pre><code>
---
## 🧹 정리

**HTTP Keep-Alive vs TCP Keep-Alive**

| 구분 | HTTP Keep-Alive | TCP Keep-Alive |
|------|----------------|----------------|
| **목적** | 연결 재사용 (성능) | 죽은 연결 감지 |
| **타임아웃** | 5-15초 (짧음) | 2시간 (김) |
| **레벨** | 애플리케이션 | OS |

### 적절한 타임아웃 설정</code></pre><p>✅ 웹사이트: 5-15초
✅ API 서버: 30-60초
✅ 모바일 앱: 15-30초
✅ DB 연결: TCP Keep-Alive 사용 (60초)</p>
<pre><code>
### 주의사항

**Keep-Alive가 너무 길면:**
- ❌ 서버 Socket 고갈
- ❌ DoS 공격에 취약
- ❌ 리소스 낭비

**Keep-Alive가 너무 짧으면:**
- ❌ 잦은 재연결로 성능 저하
- ❌ handshake 비용 증가

---

**Q: Keep-Alive가 뭔가요?**</code></pre><p>A: TCP 연결을 재사용해서 여러 HTTP 요청을 
처리하는 기술입니다. 3-way handshake 반복을 
줄여 성능을 향상시킵니다.</p>
<pre><code>
**Q: HTTP Keep-Alive와 TCP Keep-Alive 차이는?**</code></pre><p>A: HTTP Keep-Alive는 짧은 시간(초 단위) 동안 
연결을 재사용하는 성능 최적화 기술이고,
TCP Keep-Alive는 긴 시간(시간 단위) 동안 
죽은 연결을 감지하는 OS 레벨 기능입니다.</p>
<pre><code>
**Q: Keep-Alive를 너무 길게 설정하면 왜 안 좋나요?**</code></pre><p>A: 서버의 Socket이 고갈될 수 있습니다.
사용하지 않는 연결이 Socket을 계속 점유하면
새로운 사용자가 접속할 수 없고, DoS 공격에도 
취약해집니다.</p>
<h2 id="">```</h2>
<p><strong>한 줄:</strong>
Keep-Alive는 &quot;적절한 길이&quot;가 중요합니다. 무조건 길다고 좋은 것이 아니라, 상황에 맞게 설정하도록 고민해보아야 합니다.</p>
<hr>
<h3 id="끝까지-읽어주신-분들께-감사드리며-오늘도-행복한-하루-되셨으면-좋겠습니다">끝까지 읽어주신 분들께 감사드리며 오늘도 행복한 하루 되셨으면 좋겠습니다!</h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[처음해보는 성능테스트 기본 정리]]></title>
            <link>https://velog.io/@ahn_s/%EC%B2%98%EC%9D%8C%ED%95%B4%EB%B3%B4%EB%8A%94-%EC%84%B1%EB%8A%A5%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B8%B0%EB%B3%B8-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@ahn_s/%EC%B2%98%EC%9D%8C%ED%95%B4%EB%B3%B4%EB%8A%94-%EC%84%B1%EB%8A%A5%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B8%B0%EB%B3%B8-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 10 Dec 2025 10:35:30 GMT</pubDate>
            <description><![CDATA[<p>백엔드 개발을 하고 서버를 구축하다보면 내가 구축한 서버가 어느정도의 트래픽을 감당할 수 있는지가 궁금할 수 밖에(?) 없습니다. 그래서 실제로 측정을 해봐야 하는데 이러한 측정은 성능테스트를 통해 이루어집니다.
일반적으로 특정 작업 부하 상태에서 응답성과 안정성 측면에서 시스템이 어떻게 작동하는지를 확인하기 위해 수행하는 테스트 관행으로 <code>응답시간</code>, <code>처리량</code>, <code>자원 사용량(CPU, 메모리, etc...)</code>을 주로 측정합니다.</p>
<h2 id="성능테스트-종류">성능테스트 종류</h2>
<blockquote>
</blockquote>
<ol>
<li>부하 테스트
특정한 예상 부하에서 시스템이 어떻게 동작하는지 확인하며 이 테스트를 통해 주요 기능의 응답시간과 처리량 등의 성능 지표를 확인할 수 있고, 병목을 파악하는데 도움이 됩니다.<br></li>
<li>스트레스 테스트
시스템의 최대 성능을 확인하기 위한 테스트이며 예상을 뛰어넘는 부하가 발생했을 때, 시스템이 어디까지 성능을 낼 수 있는지를 확인하는 테스트 입니다.<br></li>
<li>지속 부하 테스트
시스템이 지속적인 부하를 견딜 수 있는지를 검증하며 장시간 동안 일정 수준의 부하를 주어 성능 저하가 발생하는지 확인하며 메모리 누수도 탐지할 수 있습니다.<br></li>
<li>스파이크 테스트
급격하게 트래픽이 변화할 때 시스템의 반응성과 안정성을 검증하는 테스트이며 순간적으로 트래픽이 급증했을 때 성능 저하나 실패가 발생하는지를 확인합니다.</li>
</ol>
<h3 id="그래서-어떻게-진행하면-되는-건데요">그래서 어떻게 진행하면 되는 건데요..?</h3>
<p>성능테스트를 진행하는 일반적인 방식은 낮은 부하부터 시작해서 점진적으로 부하를 높이면서 테스트합니다. 예를들어 동시 사용자 수를 100명부터 시작해서 200명, 300명으로 단계적으로 부하를 높여가며 성능을 측정하면 됩니다.</p>
<p>이렇게 부하를 증가시키다보면 초기에는 처리량도 함께 증가하다가 일정 부하 구간에 도달하면 처리량의 증가 폭이 줄어들기 시작하고, 어느 시점부터는 처리량과 응답 시간이 급격히 저하되는데, 이때 어느 순간에 더 이상 증가하지 않고 그래프가 꺽이게 되는 곳. 즉,<strong>성능이 저하되기 전의 최대 처리량</strong>을 <code>포화점</code>이라고 하고, <strong>포화점을 지나 성능이 꺾이기 시작하는 구간</strong>을 <code>버클존</code>이라고 합니다. 
<img src="https://velog.velcdn.com/images/ahn_s/post/b69a7e1f-2682-4ae7-90e2-c25ec5d0ad8d/image.jpeg" alt=""></p>
<p>포화점이 목표로 한 성능보다 높다면 만족하면서 성능 테스트를 마치면 됩니다. 하지만.. 포화점이 목표치보다 낮거나 같다면 병목 지점을 찾아 제거해야 합니다. 일반적으로 웹 서버의 병목 지점은 호출 비중이 높으면서도 응답 시간이 긴 기능과 관련되어 있는데 DB연동, 외부 연동 시간, 트래픽 대비 부족한 커넥션 풀 크기를 확인하며 어떤 지점에서 병목이 발생하는지 확인하고 해결하도록 합시다. 그렇게 한다면 포화점을 높일 수 있을 것 입니다.</p>
<h3 id="어떤걸-중점으로-측정하는게-좋을까요">어떤걸 중점으로 측정하는게 좋을까요?</h3>
<p>주요 측정 지표로는 <code>응답시간 (평균, 최대, 최소, 중앙, 백분위)</code>, <code>TPS(처리량)</code>, <code>에러율</code>, <code>CPU 사용율</code>을 중점적으로 확인해보면 좋습니다.</p>
<h3 id="설계-고려사항">설계 고려사항</h3>
<p>성능테스트를 설계할 때는 다음 사항들을 고려해보세요.</p>
<ul>
<li>시스템의 트래픽 패턴</li>
<li>동시 요청 사용자 수 / 트래픽 규모</li>
<li>기능별 요청 비율</li>
<li>데이터 크기</li>
<li>워밍업</li>
<li>적절한 목표치 설정</li>
</ul>
<p>시스템은 보통 일정한 트래픽 패턴을 가지지만 일부 서비스는 시간에 따라 트래픽 편차가 클 수 있습니다. 따라서 서비스에 맞게 테스트를 진행해주시면 될 것 같습니다.</p>
<h3 id="성능-테스트-도구">성능 테스트 도구</h3>
<ul>
<li><p>nGrinder -&gt; naver에서 개발한 성능 테스트 도구로, grinder를 사용한다. nGrinder는 1개의 컨트롤러와 다수의 에이전트로 구성되어있고, 웹 UI를 제공하고있어 사용하기 쉽습니다.
nGrinder는 Groovy나 Jython을 이용하여 스크립트를 작성합니다.</p>
</li>
<li><p>k6 -&gt; k6는 Grafana Labs에서 개발한 부하 테스트 도구로 Go언어로 개발됐지만 javaScript를 사용하여 스크립트를 작성합니다. 
k6는 CLI로 실행하기에 CI/CD환경에 통합이 용이하다는 장점이 있습니다.</p>
</li>
</ul>
<h3 id="주의사항">주의사항</h3>
<p>마지막으로 성능 테스트를 진행할 때 흔히 하는 실수 중 하나가 테스트 대상 시스템과 부하기를 한 서버에서 사용하는 것입니다. 부하 생성은 그 자체로 많은 자원을 사용하기 때문에 부하기와 테스트 서버는 다른 환경에서 동작해야 정확한 테스트를 진행할 수 있으니 꼭! 분리해서 테스트를 진행합시다!</p>
<p>또한..! nginx에 처리율 제한을 설정했다면 이것도 테스트에 맞게 수정하도록 합시다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[외부 연동이 문제일 때 살펴보아야 할 것들]]></title>
            <link>https://velog.io/@ahn_s/%EC%99%B8%EB%B6%80-%EC%97%B0%EB%8F%99%EC%9D%B4-%EB%AC%B8%EC%A0%9C%EC%9D%BC-%EB%95%8C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EC%95%84%EC%95%BC-%ED%95%A0-%EA%B2%83%EB%93%A4</link>
            <guid>https://velog.io/@ahn_s/%EC%99%B8%EB%B6%80-%EC%97%B0%EB%8F%99%EC%9D%B4-%EB%AC%B8%EC%A0%9C%EC%9D%BC-%EB%95%8C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EC%95%84%EC%95%BC-%ED%95%A0-%EA%B2%83%EB%93%A4</guid>
            <pubDate>Fri, 21 Nov 2025 09:06:08 GMT</pubDate>
            <description><![CDATA[<p>이 포스팅은 최범균 저자님의 &#39;주니어 백엔드 개발자가 반드시 알아야 할 실무 지식&#39; 책을 읽고 기록해 두기 위해 작성하였습니다.</p>
<p>개발을 하다보면 결제가 필요하면 PortOne이나 카카오페이, 토스페이 등 각자의 외부 연동이 필요합니다. 이 때 살펴보아야 할 것들은 무엇이 있을까요?</p>
<h3 id="0-우리는-문제가-없는데">0. 우리는 문제가 없는데...?</h3>
<p>개발자가 아무리 대규모 트래픽 경험이 있고 트래픽 처리 경험이 뛰어나더라도 외부 서비스와 연동을 하며 외부 서비스가 몰려드는 트래픽이 감당하지 못하면서 장애가 발생할 수 있습니다.
외부 서비스를 줄이면 좋겠지만 필수인 경우도 있기 때문에 연동 서비스의 문제로 인한 영향을 줄이는 방법을 찾아보겠습니다.</p>
<h3 id="타임아웃-connection-timeout-read-timeout">타임아웃 (Connection Timeout, Read Timeout)</h3>
<p>외부 연동에서 가장 중요한 설정 중 하나는 타임아웃입니다.타임아웃은 응답시간과 깊이 관련되어 있습니다. 연동 서비스를 호출할 때 타임아웃을 적절히 설정하지 않으면, 연동 서비스에 장애가 발생했을 때 전체 서비스의 품질이 급격히 나빠질 수 있습니다.</p>
<p>예를 들어, A서비스는 톰캣을 사용하고 있으며 스레드 풀 크기는 200이라면 A서비스는 동시에 200개의 요청을 처리할 수 있다고 했을 때,</p>
<p>A서비스가 B서비스를 호출했지만 B서비스에 문제가 생겨 응답 시간이 1분을 넘기기 시작했다면 동시에 200개의 요청을 처리할 수 있더라도 B서비스의 응답대기로 인해 처리량이 급격하게 떨어질 수 있습니다. 또한 이후의 요청이 B서비스와 연관이 없는 요청이라도 이 또한 처리하지 못하게 됩니다.</p>
<blockquote>
<p>사용자 입장에서는 어떻게 될까? </p>
</blockquote>
<p>사용자는 응답을 기다리면서 무작정 기다릴 수 밖에 없습니다. 하지만 사용자는 오래 기다려주지 않습니다.
구글 리서치 자료에 따르면, 아래와 같은 이탈률을 보인다고 나와있습니다.</p>
<pre><code>3초 이상: 이탈률 32% 증가
5초 이상: 이탈률 90% 증가
6초 이상: 이탈률 106% 증가
10초 이상: 이탈률 123% 증가</code></pre><p>그렇다면 사용자를 덜 화나게(화를 안낼 수는 없겠죠..?)하는 방법은..! 에러를 보여주는 것 입니다. 반응없는 무한 대기 보다 에러 화면이라도 보여주는것이 더 낫습니다. 또한 서버는 사용자 요청에 대해 자원이 포화되기 전에 응답하게 되므로, 연동 서비스 문제가 다른 기능에 주는 여향을 줄일 수 있다는 장점도 있습니다.</p>
<h3 id="재시도">재시도</h3>
<p>네트워크 통신 과정에서 간헐적으로 연결에 실패하거나 일시적으로 응답이 느려지는 경우가 있습니다. 이럴 때는 재시도를 통해 연동 실패를 성공으로 바꿀 수 있습니다.</p>
<blockquote>
<p>재시도 가능 조건</p>
</blockquote>
<p>재시도를 통해 연동 실패를 줄일 수 있지만, 항상 재시도를 할 수 있는 것은 아닙니다. 조건을 확인해 봐야죠..!</p>
<pre><code>재시도 조건 3가지

1. 단순 조회 기능
2. 연결 타임아웃
3. 멱등성(Idempotent)을 가진 변경 기능</code></pre><p><code>단순 조회</code> 기능은 재시도를 통해 성공 확률을 높일 수 있습니다. 예를들어 상용자 포인트 내역 조회같은 기능은 다시 호출해도 포인트 중복 차감같은 데이터 문제가 생기지 않기때문에 일시적인 문제였다면 다시 조회할 경우 정상적으로 처리될 가능성이 높습니다.</p>
<p><code>연결 타임아웃</code>도 마찬가지로 연동 서비스가 요청을 처리하고 있지 않은 상태이므로, 순간적인 네트워크 문제였다면 재시도를 통해 연결이 성공할 가능성이 있습니다.</p>
<p><code>읽기 타임아웃</code>은 재시도 할 때 <strong>주의</strong>해야 합니다.이 경우는 이미 연동 서비스가 요청을 처리하고 있는 중이기 때문에 읽기 타임아웃이 발생한 상황에서 재시도를 하면 문제가 생길 수 있습니다.</p>
<p><code>멱등성</code> = 연산을 여러번 적용해도 결과가 달라지지 않는 성질을 말합니다.
데이터 변경이 이루어지는 재시도 때에는 멱등성을 가지는 변경인지 확인하여야 합니다.</p>
<blockquote>
<p>재시도 결정 조건</p>
</blockquote>
<pre><code>1. 재시도 횟수
2. 재시도 간격</code></pre><p>재시도는 무한정 할 수는 없습니다. 재시도 횟수만큼 응답시간도 함께 증가하기 때문에 대부분의 경우 1~2번 정도의 재시도가 적당합니다. 모든 재시도가 실패하였다면 간헐적인 오류보다는 다른 근본적인 문제일 가능성이 높아 다시 재시도해도 실패할 확률이 높습니다.</p>
<p>또한 재시도 간격도 중요합니다. 만약 네트워크 연결 상태가 6초간 좋지 않은 상황을 가정해봤을 때, 바로 재시도 하면 같은 네트워크 문제로 인해 다시 연결 타임아웃이 발생할 수 있습니다. 그렇기에 재시도 간격도 생각해보면 좋겠습니다.</p>
<p><code>재시도를 통해 성공 가능성을 높일 수 있지만, 반대로 연동 서비스에는 더 큰 부하를 줄 수 있으니, 재시도를 검토할 때는 연동 서비스의 성능 상황도 함께 고려해보면 좋겠습니다.</code></p>
<h3 id="서킷-브레이커">서킷 브레이커</h3>
<p>연동 서비스에 과부하가 발생해 응답을 제대로 주지 못하고 있는 상황이라고 해봅시다. 연동 서비스가 정상화되기 전까지는 요청을 보내도 계속 에러만 발생합니다. 또한, 읽기 타임아웃이 발생할 때까지 대기하느라 응답 시간도 길어질 것입니다. 즉, 연동 서비스가 장애일 때 요청을 보내도 에러만 발생합니다.</p>
<p>이러한 상황일 때에는, B서비스에 요청을 보내지 않고 바로 에러를 응답하는 것이 좋습니다. 사용자 입장에서도 수 초를 대기하다가 에러화면을 보는 것 보다 빠르게 에러화면을 보는 편이 낫겠죠?</p>
<p>이렇듯 연동 서비스가 장애 상황일 때는 연동 대신 바로 에러를 응답하고, 정상화되었을 때 연동을 재개하면 연동 서비스의 장애가 주는 영향을 줄일 수 있습니다.</p>
<p><code>서킷브레이커</code>는 누전 차단기와 비슷하게 생각하면 좋을 것 같습니다. 과전류가 흐르면 차단기가 내려가 전기를 끊는 것처럼, 과도한 오류가 발생하면 연동을 중지시키고 바로 에러를 응답합니다.
서킷브레이커는 닫힘, 열림, 반 열림 3가지 상태를 가집니다.</p>
<ul>
<li>닫힘 (Closed)
서킷 브레이커는 닫힘 상태로 시작합니다. 이 상태는 모든 요청을 연동 서비스에 전달합니다.</li>
<li>열림 (Open)
연동요청을 수행하지 않고, 바로 에러 응답을 보냅니다. 열림 상태는 지정된 시간 동안 유지되며, 이 시간이 지나면 반 열림 상태로 전환됩니다.</li>
<li>반 열림 (Half-Open)
일부 요청에 한해 연동을 시도합니다. 일정 개수 또는 일정 시간 동안 반 열림 상태를 유지하며, 이 기간동안 연동에 성공하면 닫힘 상태로 복귀합니다. 반대로 실패하면 열림 상태로 전환되어 연동을 차단합니다.</li>
</ul>
<hr>
<p>이외에도 외부서비스로 인한 성능 감소시 고려해야할 다양한 것들(<code>HTTP 커넥션 풀</code>, <code>연동 서비스 이중화</code> , ...)이 많이 있으니 서비스 상황에 맞게 경험해보고 적용하면서 적절한 방법을 찾아나가 보면 좋겠습니다!! </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ubuntu에서 DB DUMP 하기 (+ NCP Storage)]]></title>
            <link>https://velog.io/@ahn_s/AWS%EC%97%90%EC%84%9C-DB-DUMP-%ED%95%98%EA%B8%B0-NCP-Storage</link>
            <guid>https://velog.io/@ahn_s/AWS%EC%97%90%EC%84%9C-DB-DUMP-%ED%95%98%EA%B8%B0-NCP-Storage</guid>
            <pubDate>Tue, 29 Jul 2025 06:55:52 GMT</pubDate>
            <description><![CDATA[<p>나비 프로젝트에서 개발하면서 데이터베이스 백업을 해두지 않아 난감한 경우가 생겨 최신의 데이터로 복구하기 위해서 매일 DB 데이터를 DUMP해야할 필요가 있다고 생각했습니다. (아직은 출시전이라서 크게 타격은 없었지만 실제 운영서버였으면 큰일이났을텐데.. 참 다행입니다 ㅠ)</p>
<p>그리하여 매일 오전 0시 0분에 Ubuntu cron을 통해 DB를 Dump 하고 NCP Storage에 업로드를 시키는 스크립트를 작성하게 되었습니다.</p>
<h3 id="aws---스크립트">AWS - 스크립트</h3>
<pre><code class="language-jsx"># DB
DATE=$(date +%Y.%m.%d)
BACKUP_DIR=&quot;/home/ubuntu/dump/&quot;
DUMP_COMMAND=&quot;/usr/bin/mysqldump&quot;

# nCloud Storage
S3_BUCKET_NAME=&quot;codenear-storage&quot;
NCLOUD_ENDPOINT=&quot;https://kr.object.ncloudstorage.com&quot;
S3_KEY=&quot;db_backups/&quot;

# 백업 디렉토리가 없으면 생성
mkdir -p &quot;$BACKUP_DIR&quot;

# DB DUMP
echo &quot;데이터베이스 백업 시작...&quot;
docker exec mysql $DUMP_COMMAND codenear &gt; &quot;${BACKUP_DIR}${DATE}.sql&quot;

if [ $? -eq 0 ]; then
    echo &quot;데이터베이스 백업이 성공적으로 완료되었습니다: ${BACKUP_DIR}${DATE}.sql&quot;

    # 백업 파일이 실제로 생성되었는지 확인
    if [ ! -f &quot;${BACKUP_DIR}${DATE}.sql&quot; ] || [ ! -s &quot;${BACKUP_DIR}${DATE}.sql&quot; ]; then
        echo &quot;오류: 백업 파일이 제대로 생성되지 않았습니다.&quot;
        exit 1
    fi

    echo &quot;NCloud Object Storage에 백업 파일 업로드 중...&quot;

    # NCloud Storage에 업로드
    aws --endpoint-url=&quot;$NCLOUD_ENDPOINT&quot; s3 cp &quot;${BACKUP_DIR}${DATE}.sql&quot; &quot;s3://$S3_BUCKET_NAME/$S3_KEY&quot; --acl public-read

    if [ $? -eq 0 ]; then
        echo &quot;NCloud Object Storage 업로드가 성공적으로 완료되었습니다: s3://$S3_BUCKET_NAME/$S3_KEY&quot;

        # 로컬 백업 파일 삭제
        rm &quot;${BACKUP_DIR}${DATE}.sql&quot;
        echo &quot;로컬 백업 파일 삭제 완료: ${BACKUP_DIR}${DATE}.sql&quot;
  else
    echo &quot;오류: NCloud Object Storage 업로드에 실패했습니다.&quot;
    exit 1
  fi
else
  echo &quot;데이터베이스 백업에 실패했습니다.&quot;
fi
</code></pre>
<blockquote>
<p>주의할점</p>
<p>현재 이상태로 스크립트를 실행시키면 PutObject Access Denied가 발생할 것이다. 이 문제에 대하여 문의를 보냈고 네이버측에서 아래와 같은 답변을 받을 수 있었다.</p>
<p>“AWS CLI 및 SDK 특정 버전(예: AWS CLI v2.23.0, boto3 1.36) 이후 버전에서는 Object Storage에서 지원하지 않는 새로운 체크섬 알고리즘이 기본적으로 활성화되기 때문에 요청에 실패할 수 있습니다.”</p>
<p>즉, CheckSum 알고리즘을 비활성화 하는 설정이 필요하다.</p>
<p><code>CheckSum 비활성화</code></p>
<pre><code class="language-jsx">~/.aws/config

[default]
request_checksum_calculation = WHEN_REQUIRED
response_checksum_validation = WHEN_REQUIRED</code></pre>
</blockquote>
<h2 id="aws---스케줄링-cron">AWS - 스케줄링 (Cron)</h2>
<h3 id="cron">cron</h3>
<pre><code class="language-jsx">$ crontab -e

0 0 * * * /home/ubuntu/db_dump.sh &gt;&gt; /var/log/db_backup.log 2&gt;&amp;1

매일 0시 0분에 해당 스크립트를 실행시키고 /var/log/db_backup.log 파일에 로그를 남긴다

** 스케줄링이 실행이 안된다고 생각되면 TimeZone 확인하기 
sudo timedatectl set-timezone Asia/Seoul # 타임존 변경 (Asia/Seoul)</code></pre>
<h3 id="logrotate">logrotate</h3>
<pre><code class="language-jsx">$ sudo nano /etc/logrotate.d/db_backup

/var/log/db_backup.log {  # 이 로그 파일에 대한 설정 시작
    daily                 # 매일 로테이트
    missingok             # 파일 없어도 오류 아님
    rotate 7              # 7개 보관
    compress              # 압축
    delaycompress         # 지연 압축
    notifempty            # 비어있으면 로테이트 안 함
    create 0640 root adm  # 새 파일 생성 권한
    sharedscripts         # 스크립트 공유
    postrotate            # 로테이트 후 실행
    endscript
}                         # 이 로그 파일에 대한 설정 끝</code></pre>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/ahn_s/post/8b92f103-2550-4224-9a87-05e5b76b2f11/image.png" alt=""></p>
<p>정상적으로 업로드 완료!
++ 이제는 오전 0시0분 부터 하루 사이에 이슈가 발생했을 때 유실 되는 데이터를 복구할 수 있는 방법을 고민하기!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[캐시와 데이터베이스간 데이터 불일치에 대한 고민 (Mysql 이벤트 스케줄러)]]></title>
            <link>https://velog.io/@ahn_s/%EC%BA%90%EC%8B%9C%EC%99%80-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4%EA%B0%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B6%88%EC%9D%BC%EC%B9%98%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EB%AF%BC-Mysql-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%9F%AC</link>
            <guid>https://velog.io/@ahn_s/%EC%BA%90%EC%8B%9C%EC%99%80-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4%EA%B0%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B6%88%EC%9D%BC%EC%B9%98%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EB%AF%BC-Mysql-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%9F%AC</guid>
            <pubDate>Fri, 04 Jul 2025 17:12:36 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>나비 프로젝트에서 MYSQL 이벤트 스케줄러를 사용하여 사용자의 등급을 변경하는 이벤트를 만들었습니다.
여기서 문제는 사용자 정보를 캐싱하고 있는데, MYSQL 이벤트 스케줄러에서 변경이 발생하면 유저 캐시는 반영을 하지 못해 캐시와 데이터베이스간 데이터 불일치가 발생하는 문제가 생겨 데이터 정합성에 대한 고민들을 기록하기 위해 이 글을 작성하게 되었습니다.</p>
<h2 id="mysql-이벤트-스케줄러-프로시저">MYSQL 이벤트 스케줄러 프로시저</h2>
<pre><code>CREATE TEMPORARY TABLE temp_member_orders AS
    SELECT 
        od.member_id, 
        SUM(od.total) AS total_amount
    FROM 
        order_details od
    WHERE
        od.created_at &gt;= DATE_SUB(CURRENT_DATE(), INTERVAL 4 MONTH)
        AND od.order_status != &#39;CANCELED&#39;
    GROUP BY 
        od.member_id;

    UPDATE member m
    LEFT JOIN temp_member_orders tmo ON m.id = tmo.member_id
    SET m.grade = &#39;EGG&#39;
    WHERE m.grade IS NULL OR tmo.member_id IS NULL;



    UPDATE member m
    INNER JOIN temp_member_orders tmo ON m.id = tmo.member_id
    SET m.grade = &#39;LARBA&#39;
    WHERE tmo.total_amount &lt; 100000;


    UPDATE member m
    INNER JOIN temp_member_orders tmo ON m.id = tmo.member_id
    SET m.grade = &#39;PUPA&#39;
    WHERE tmo.total_amount &gt;= 100000 AND tmo.total_amount &lt; 300000;


    UPDATE member m
    INNER JOIN temp_member_orders tmo ON m.id = tmo.member_id
    SET m.grade = &#39;BUTTERFLY&#39;
    WHERE tmo.total_amount &gt;= 300000;</code></pre><blockquote>
<p>방법 1. (가장 빠르게 구현 가능)</p>
<p>어플리케이션 내에서 스케줄러로 매달 1일에 user에 대한 캐시를 모두 지움 (캐시 초기화)</p>
<h3 id="장점">장점</h3>
<ol>
<li>가장 정확하고, 빠르게 구현 가능</li>
<li>스케줄러가 실행된 시점 이후로는 캐시가 비워지므로, 다음 요청 시에는 반드시 최신 데이터를 DB에서 가져올 수 있음.</li>
</ol>
<h3 id="단점">단점</h3>
<ol>
<li><p>변경되지 않은 유저에 대한 캐시도 삭제하기 때문에 삭제 작업이 많음 (불필요한 작업)</p>
</li>
<li><p>성능 저하의 가능성</p>
<p> Redis는 Read의 성능이 좋지 Write의 성능이 좋지 않음. 때문에 유저에 대한 캐시가 많으면 삭제에 대한 작이 무거워짐</p>
</li>
<li><p>캐시를 사용하는 가장 큰 목적은 DB 부하를 줄이고 응답 속도를 높이는 것인데, 주기적으로 캐시를 통째로 비워버리면 캐시의 본래 이점을 상당 부분 상실.</p>
</li>
</ol>
</blockquote>
<blockquote>
<p>방법 2.</p>
<p>Mysql 프로시저에서 변경된 memberId를 body에 담아 http요청 </p>
<p>→ 서버에서 해당 유저 캐시 삭제 또는 업데이트</p>
<h3 id="장점-1">장점</h3>
<ol>
<li>mysql 프로시저에 api요청만 추가하면 되고, 서버에서도 api를 하나만 두면 되어 로직이 간단.</li>
</ol>
<h3 id="단점-1">단점</h3>
<ol>
<li><p>Mysql은 기본적으로 http 요청을 보내는 설정이 되어있지 않음. UDF(User Defined Function - 사용자 정의 함수)를 추가적으로 설정해야 하는 번거로움</p>
</li>
<li><p>UDF 설정을 위해 Python 또는 C / C++을 이용하여 사용자 정의 라이브러리를 작성해야 함.</p>
</li>
<li><p>MySQL의 본질적인 역할에서 벗어남</p>
<p> 데이터베이스는 데이터를 저장하고 관리하는 것이 주된 역할. 외부 HTTP 요청을 보내는 것은 애플리케이션 계층의 책임이며, 이를 DB 내부에서 처리하는 것은 아키텍처적으로 바람직하지 않음.</p>
</li>
<li><p>AI에게 물어봤을 때 UDF사용은 보안 취약점을 야기한다고 함.</p>
</li>
<li><p>디버깅 및 관리가 어려움</p>
</li>
</ol>
</blockquote>
<blockquote>
<p>방법 3.</p>
<p>Mysql 프로시저에서 변경된 memberId들을 파일로 저장 (Binlog) </p>
<p>→ 서버 내에서 Debezium 이용하여 파일 변경 감지 </p>
<p>→ 파일 내에 변경된 memberId들을 rabbitMQ 메세지 큐 발행 (Publish)</p>
<p>→ Consumer를 통해 해당 유저 캐시 삭제 또는 업데이트</p>
<h3 id="장점-2">장점</h3>
<ol>
<li><p>Debezium 라이브러리를 Dependency에 추가하기만 하면 사용 가능</p>
</li>
<li><p>아키텍처 분리 및 책임 명확. </p>
<p> MySQL은 데이터 변경에 집중하고, Debezium이 변경 감지를, Spring Boot가 캐시 무효화를 담당하여 각 컴포넌트의 역할이 명확해짐.</p>
</li>
<li><p>디버깅이 가능하여 관리가 1번보다는 쉽다고 생각.</p>
</li>
</ol>
<h3 id="단점-2">단점</h3>
<ol>
<li>Debezium 환경설정과 추가 로직이 많음 (+ 처음 사용해봐서 학습비용 고려)</li>
<li>데이터베이스 변경사항(INSERT,UPDATE,DELETE)을 모두 감지 하기 때문에 해당 파일만 감지하도록 변경해야 함</li>
<li>파일(로그) 관리</li>
</ol>
</blockquote>
<h3 id="결론">결론</h3>
<p>현재로서는 1번 방법이 가장 쉽고, 초기에는 사용자가 많지 않을 것이라고 생각하기 때문에 명확한 성능 저하가 올 가능성이 적기도 하지만, 이후에 서비스가 커진다고 가정했을 때 2번 또는 3번(,etc…)으로 로직 변경이 필요합니다.</p>
<p>개인적인 생각으로는 확장성 측면에서 Debesium이 더 확장성이 높다고 생각합니다. 현재는 Mysql 이벤트 스케줄러 내에서 변경된 사항들에 대해 유저 캐시가 변경되지 않아 발생하는 문제이지만, 이후에 데이터분석을 위한 로깅이나 추가적인 데이터베이스 변경 감지가 필요한 상황이 생겼을 때를 고려해봤을 때 3번을 잘 구축해두면 좋을 것 같다는 생각입니다.</p>
<hr>
<h1 id="spring-boot-debezium-설정하기">Spring Boot Debezium 설정하기</h1>
<p><code>build.gradle</code></p>
<ul>
<li>Debezium 의존성 설정</li>
</ul>
<pre><code class="language-jsx">    // Debezium
    implementation &#39;io.debezium:debezium-api:2.7.0.Final&#39;
    implementation &#39;io.debezium:debezium-embedded:2.7.0.Final&#39;
    implementation &#39;io.debezium:debezium-connector-mysql:2.7.0.Final&#39;</code></pre>
<p><code>DebeziumConfig</code></p>
<pre><code class="language-jsx">import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DebeziumConfig {
    @Value(&quot;${debezium.database-hostname}&quot;)
    private String databaseHostname;
    @Value(&quot;${debezium.database-user}&quot;)
    private String databaseUser;
    @Value(&quot;${debezium.database-password}&quot;)
    private String databasePassword;
    @Value(&quot;${debezium.database-port}&quot;)
    private String databasePort;
    @Value(&quot;${debezium.name}&quot;)
    private String debeziumConnectorName;
    @Value(&quot;${debezium.database-server-id}&quot;)
    private String databaseServerId;
    @Value(&quot;${debezium.database-server-name}&quot;)
    private String databaseServerName;
    @Value(&quot;${debezium.database-include-list}&quot;)
    private String databaseIncludeList;
    @Value(&quot;${debezium.table-include-list}&quot;)
    private String tableIncludeList;
    @Value(&quot;${debezium.offset-filename}&quot;)
    private String offsetFilename;
    @Value(&quot;${debezium.history-filename}&quot;)
    private String historyFilename;

    @Bean
    public io.debezium.config.Configuration debeziumConnectorConfig() {
        return io.debezium.config.Configuration.create()
                /* Debezium 엔진 설정 */
                .with(&quot;name&quot;, debeziumConnectorName) // 커넥터 이름
                .with(&quot;connector.class&quot;, &quot;io.debezium.connector.mysql.MySqlConnector&quot;) // 사용할 커넥터 클래스
                .with(&quot;offset.storage&quot;, &quot;org.apache.kafka.connect.storage.FileOffsetBackingStore&quot;) // offset 저장 방식 (파일)
                .with(&quot;offset.storage.file.filename&quot;, offsetFilename) // offset 저장 파일 경로
                .with(&quot;offset.flush.interval.ms&quot;, &quot;30000&quot;) // offset 파일 저장 주기 (ms)

                /* 데이터베이스 연결 정보 */
                .with(&quot;database.hostname&quot;, databaseHostname) // DB 호스트
                .with(&quot;database.port&quot;, databasePort) // DB 포트
                .with(&quot;database.user&quot;, databaseUser) // DB 사용자
                .with(&quot;database.password&quot;, databasePassword) // DB 비밀번호
                .with(&quot;database.server.name&quot;, databaseServerName) // DB 서버의 논리적 이름 (고유해야 함)
                .with(&quot;topic.prefix&quot;, databaseServerName) // topic.prefix
                .with(&quot;database.include.list&quot;, databaseIncludeList) // 모니터링할 데이터베이스(스키마) 이름
                .with(&quot;database.server.id&quot;, databaseServerId) // DB 서버 아이디
                .with(&quot;table.include.list&quot;, tableIncludeList) // 특정 테이블만 모니터링할 경우

                /* 스키마 변경 이력 관리 */
                .with(&quot;schema.history.internal&quot;, &quot;io.debezium.storage.file.history.FileSchemaHistory&quot;)
                .with(&quot;schema.history.internal.file.filename&quot;, historyFilename)
                .build();
    }
}</code></pre>
<p><code>DebeziumListener</code></p>
<pre><code class="language-jsx">@Component
@Slf4j
public class DebeziumListener {
    private final DebeziumEngine&lt;ChangeEvent&lt;String, String&gt;&gt; debeziumEngine;
    private final ExecutorService executor;
    private final ObjectMapper objectMapper;
    private final ApplicationEventPublisher eventPublisher;

    @Autowired
    public DebeziumListener(Configuration debeziumConnectorConfig, ObjectMapper objectMapper, ApplicationEventPublisher eventPublisher) {
        this.debeziumEngine = DebeziumEngine.create(Json.class)
                .using(debeziumConnectorConfig.asProperties())
                .notifying(this::handleChangeEvent)
                .build();
        this.objectMapper = objectMapper;
        this.executor = Executors.newSingleThreadExecutor();
        this.eventPublisher = eventPublisher;
    }

    /**
     * Debezium으로부터 변경 이벤트를 받아 처리
     *
     * @param changeEvent Debezium에서 발생한 변경 이벤트 (문자열 형태의 JSON 값 포함)
     */
    private void handleChangeEvent(ChangeEvent&lt;String, String&gt; changeEvent) {
        try {
            JsonNode payloadNode = validateAndGetPayload(changeEvent);
            if (payloadNode == null) {
                return;
            }

            // op필드 = DML (INSERT, UPDATE, DELETE) = &#39;c&#39;,&#39;u&#39;,&#39;d&#39;
            JsonNode operationNode = payloadNode.get(&quot;op&quot;);

            // op필드에 대한 변경이 일어나면, 즉 INSERT,UPDATE,DELETE의 로직이 수행되면 (DDL 제외)
            if (operationNode != null) {
                handleDmlEvent(operationNode.asText(), payloadNode, changeEvent.destination(), changeEvent.value());
            }
        } catch (Exception e) {
            log.error(&quot;변경 이벤트 처리 중 오류 발생. 값: {}&quot;, changeEvent.value(), e);
        }
    }

    /**
     * Debezium 이벤트의 유효성을 검사하고 페이로드 JsonNode를 반환
     * 이벤트 값이나 페이로드가 유효하지 않으면 null을 반환 및 로그 발행
     *
     * @param changeEvent Debezium 변경 이벤트
     * @return 유효한 경우 페이로드 JsonNode, 그렇지 않으면 null
     * @throws IOException JSON 파싱 중 오류 발생 시
     */
    private JsonNode validateAndGetPayload(ChangeEvent&lt;String, String&gt; changeEvent) throws IOException {
        String value = changeEvent.value();
        if (value == null) {
            log.warn(&quot;값이 null인 변경 이벤트를 수신했습니다. 이벤트를 건너뜜니다.&quot;);
            return null;
        }

        JsonNode jsonNode = objectMapper.readTree(value);
        JsonNode payloadNode = jsonNode.get(&quot;payload&quot;);

        if (payloadNode == null) {
            log.warn(&quot;페이로드가 null인 변경 이벤트를 수신했습니다: {}&quot;, value);
            return null;
        }
        return payloadNode;
    }

    /**
     * DML(INSERT, UPDATE, DELETE, READ) 이벤트를 처리하는 메서드.
     */
    private void handleDmlEvent(String operation, JsonNode payloadNode, String topic, String rawValue) {
        String table = topic.substring(topic.lastIndexOf(&#39;.&#39;) + 1);

        JsonNode dataPayload;

        // before : 변경 또는 삭제 되기 전의 데이터 / after : 변경 또는 삭제 된 후의 데이터
        dataPayload = operation.equals(&quot;d&quot;) ? payloadNode.get(&quot;before&quot;) : payloadNode.get(&quot;after&quot;);

        if (dataPayload == null) {
            log.warn(&quot;작업 &#39;{}&#39; 테이블 &#39;{}&#39;에 대한 데이터 페이로드(before/after)가 null입니다. 이벤트 값: {}&quot;, operation, table, rawValue);
            return;
        }

        log.debug(&quot;DML 변경 감지됨: 작업={}, 테이블={}, 데이터={}&quot;, operation, table, dataPayload);
        eventPublisher.publishEvent(new DebeziumChangeEventDTO(this, operation, table, dataPayload, payloadNode));
    }

    /**
     * 스프링 컴포넌트 초기화 시 Debezium 엔진을 시작
     */
    @PostConstruct
    public void start() {
        executor.execute(debeziumEngine);
    }

    /**
     * 스프링 컴포넌트 종료 시 Debezium 엔진을 중지하고 리소스를 해제합니다.
     */
    @PreDestroy
    public void stop() {
        if (debeziumEngine != null) {
            try {
                debeziumEngine.close();
            } catch (IOException e) {
                log.error(&quot;Error while stopping Debezium engine&quot;, e);
            }
        }
        executor.shutdown();
    }
}</code></pre>
<p><code>DebeziumChangeEventDTO</code></p>
<pre><code>@Getter
public class DebeziumChangeEventDTO extends ApplicationEvent {
    private final String operationType;
    private final String tableName;
    private final JsonNode dataPayload;
    private final JsonNode fullPayload;

    public DebeziumChangeEventDTO(Object source, String operationType, String tableName, JsonNode dataPayload, JsonNode fullPayload) {
        super(source);
        this.operationType = operationType;
        this.tableName = tableName;
        this.dataPayload = dataPayload;
        this.fullPayload = fullPayload;
    }
}</code></pre><p><code>MemberDebeziumEventListener</code></p>
<pre><code>package com.codenear.butterfly.member.util;

import com.codenear.butterfly.global.dto.DebeziumChangeEventDTO;
import com.codenear.butterfly.member.application.MemberService;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
@Slf4j
public class MemberDebeziumEventListener {
    private final MemberService memberService;

    /**
     * &#39;member&#39; 테이블에 대한 DmlChangeEvent만 수신하여 처리
     *
     * @param event 발생한 DML 변경 이벤트
     */
    @EventListener(condition = &quot;#event.tableName == &#39;member&#39;&quot;)
    public void handleMemberDmlChange(DebeziumChangeEventDTO event) {
        String operationType = event.getOperationType();
        JsonNode dataPayload = event.getDataPayload();

        log.info(&quot;[회원 테이블] DML 변경 이벤트 수신 : 작업={}, 데이터={}&quot;, operationType, dataPayload);

        switch (operationType) {
            case &quot;c&quot; -&gt; handleInsert(event.getTableName(), dataPayload);
            case &quot;u&quot; -&gt; handleUpdate(event.getTableName(), dataPayload);
            case &quot;d&quot; -&gt; handleDelete(event.getTableName(), dataPayload);
            default -&gt; log.warn(&quot;처리되지 않은 DML 작업 유형: {}&quot;, operationType);
        }
    }

    /**
     * INSERT(생성) 이벤트를 처리하는 메서드.
     *
     * @param table       변경이 발생한 테이블 이름
     * @param dataPayload 삽입된 레코드 데이터
     */
    private void handleInsert(String table, JsonNode dataPayload) {
        log.debug(&quot;테이블 &#39;{}&#39;에 새 레코드 삽입됨: {}&quot;, table, dataPayload);
        // TODO: INSERT에 대한 비즈니스 로직
    }

    /**
     * UPDATE(수정) 이벤트를 처리하는 메서드.
     *
     * @param table       변경이 발생한 테이블 이름
     * @param dataPayload 수정된 레코드 데이터
     */
    private void handleUpdate(String table, JsonNode dataPayload) {
        log.debug(&quot;테이블 &#39;{}&#39;의 레코드 업데이트됨: {}&quot;, table, dataPayload);

        Long memberId = dataPayload.get(&quot;id&quot;).asLong();
        memberService.evictMemberCache(memberId);
    }

    /**
     * DELETE(삭제) 이벤트를 처리하는 메서드.
     *
     * @param table       변경이 발생한 테이블 이름
     * @param dataPayload 삭제된 레코드 데이터 (삭제 전 데이터)
     */
    private void handleDelete(String table, JsonNode dataPayload) {
        log.debug(&quot;테이블 &#39;{}&#39;의 레코드 삭제됨: {}&quot;, table, dataPayload);
        // TODO: DELETE에 대한비즈니스 로직
    }
}
</code></pre><blockquote>
<p>Mysql 설정도 추가를 해주어야 한다.</p>
<p><code>mysql - ./conf.d/{custom}.cnf</code></p>
<pre><code class="language-jsx">[mysqld]
log_bin = mysql-bin
binlog_format = ROW
binlog_row_image = FULL
server_id = {server_id} ex)123456

이후 mysql restart</code></pre>
</blockquote>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/ahn_s/post/9a64d4db-29d7-4f4a-b988-3757c8f9f876/image.png" alt=""></p>
<p>Mysql 이벤트 스케줄러로 변경된 데이터들이 감지 성공!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 빈 생명주기 콜백]]></title>
            <link>https://velog.io/@ahn_s/Spring-%EB%B9%88-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0-%EC%BD%9C%EB%B0%B1</link>
            <guid>https://velog.io/@ahn_s/Spring-%EB%B9%88-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0-%EC%BD%9C%EB%B0%B1</guid>
            <pubDate>Thu, 06 Apr 2023 13:18:22 GMT</pubDate>
            <description><![CDATA[<p>★ Reference - <a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard">https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard</a></p>
<h2 id="빈-생명주기-콜백">빈 생명주기 콜백</h2>
<ul>
<li><p>데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다.</p>
</li>
<li><p>스프링 빈은 간단하게 다음과 같은 라이프사이클을 가진다.</p>
<ul>
<li>객체 생성 -&gt; 의존관계 주입</li>
<li>스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료된다. 따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 한다.</li>
</ul>
</li>
<li><p>스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다. 또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다.</p>
</li>
</ul>
<blockquote>
<h3 id="스프링-빈의-이벤트-라이프사이클">스프링 빈의 이벤트 라이프사이클</h3>
</blockquote>
<ul>
<li>스프링 컨테이너 생성 -&gt; 스프링 빈 생성 -&gt; 의존관계 주입 -&gt; 초기화 콜백 사용 -&gt;
소멸전 콜백 -&gt; 스프링 종료  <br>
- 초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
- 소멸전 콜백: 빈이 소멸되기 직전에 호출

</li>
</ul>
<h3 id="🎯객체의-생성과-초기화를-분리하자">🎯객체의 생성과 초기화를 분리하자.</h3>
<ul>
<li>생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다. </li>
<li>초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는등 무거운 동작을 수행한다.</li>
</ul>
<p>따라서 생성자 안에서 무거운 초기화 작업을 함께 하는 것 보다는 객체를 생성하는 부분과 초기화 하는부분을 명확하게 나누는 것이 유지보수 관점에서 좋다.</p>
<hr>
<h2 id="빈-생명주기-콜백-3가지">빈 생명주기 콜백 3가지</h2>
<ul>
<li>스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원한다.<ul>
<li>인터페이스(InitializingBean, DisposableBean)</li>
<li>설정 정보에 초기화 메서드, 종료 메서드 지정</li>
<li>@PostConstruct, @PreDestroy 애노테이션 지원</li>
</ul>
</li>
</ul>
<ul>
<li><h3 id="인터페이스initializingbean-disposablebean">인터페이스(InitializingBean, DisposableBean)</h3>
</li>
</ul>
<pre><code class="language-java">public class NetworkClient implements InitializingBean, DisposableBean {
    private String url;

    public NetworkClient() {
        System.out.println(&quot;생성자 호출, url = &quot; + url);
        connect();
        call(&quot;초기화 연결 메세지&quot;);
    }

    public void setUrl(String url) {
        this.url = url;
    }

    // 서비스 시작시 호출
    public void connect() {
        System.out.println(&quot;connect : &quot; + url);
    }

    public void call(String massage) {
        System.out.println(&quot;call : &quot; + url + &quot; massage = &quot; + massage);
    }

    // 서비스 종료시 호출
    public void disconnect() {
        System.out.println(&quot;close : &quot; + url);
    }

    // 의존관계 주입이 끝나면 호출해 주겠다 (InitializingBean)
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println(&quot;NetworkClient.afterPropertiesSet&quot;);
        connect();
        call(&quot;초기화 연결 메세지&quot;);
    }

    // Disconnect를 호출해주겠다 (DisposableBean)
    @Override
    public void destroy() throws Exception {
        System.out.println(&quot;NetworkClient.destroy&quot;);
        disconnect();
    }
}</code></pre>
<pre><code class="language-java">생성자 호출, url = null
NetworkClient.afterPropertiesSet
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
13:24:49.043 [main] DEBUGorg.springframework.context.annotation.AnnotationConfigApplicationContext - Closing NetworkClient.destroy
close + http://hello-spring.dev</code></pre>
<blockquote>
<p>초기화, 소멸 인터페이스 단점</p>
</blockquote>
<ul>
<li>이 인터페이스는 스프링 전용 인터페이스다. 해당 코드가 스프링 전용 인터페이스에 의존한다.
초기화, 소멸 메서드의 이름을 변경할 수 없다. 즉, 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.</li>
</ul>
<p>** ❗인터페이스를 사용하는 초기화, 종료 방법은 스프링 초창기에 나온 방법들이고, 지금은 다음의 더 나은 방법들이 있어서 거의 사용하지 않는다.**</p>
<ul>
<li><h3 id="설정-정보에-초기화-메서드-종료-메서드-지정">설정 정보에 초기화 메서드, 종료 메서드 지정</h3>
</li>
</ul>
<pre><code class="language-java">
    public void init() {
        System.out.println(&quot;NetworkClient.init&quot;);
        connect();
        call(&quot;초기화 연결 메세지&quot;);
    }


    public void close() {
        System.out.println(&quot;NetworkClient.close&quot;);
        disconnect();
    }
</code></pre>
<pre><code class="language-java">@Configuration
    static class LifeCycleConfig {
        @Bean(initMethod = &quot;init&quot;, destroyMethod = &quot;close&quot;)
        public NetworkClient networkClient() {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl(&quot;http://hello-spring.dev&quot;);
            return networkClient;
        }
    }</code></pre>
<pre><code class="language-java">생성자 호출, url = null
NetworkClient.init
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
13:33:10.029 [main] DEBUGorg.springframework.context.annotation.AnnotationConfigApplicationContext -Closing NetworkClient.close
close + http://hello-spring.dev</code></pre>
<blockquote>
<p>설정 정보 사용 특징</p>
</blockquote>
<ul>
<li><p>메서드 이름을 자유롭게 줄 수 있다.</p>
</li>
<li><p>스프링 빈이 스프링 코드에 의존하지 않는다.</p>
</li>
<li><p><strong>외부 라이브러리에도 초기화, 종료메서드를 적용할 수 있다.</strong> ★</p>
</li>
<li><h3 id="postconstruct-predestroy-애노테이션-지원-★">@PostConstruct, @PreDestroy 애노테이션 지원 ★</h3>
</li>
</ul>
<pre><code class="language-java">    @PostConstruct
    public void init() {
        System.out.println(&quot;NetworkClient.init&quot;);
        connect();
        call(&quot;초기화 연결 메세지&quot;);
    }

    @PreDestroy
    public void close() {
        System.out.println(&quot;NetworkClient.close&quot;);
        disconnect();
    }</code></pre>
<pre><code class="language-java">@Configuration
    static class LifeCycleConfig {
        @Bean
        public NetworkClient networkClient() {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl(&quot;http://hello-spring.dev&quot;);
            return networkClient;
        }
    }</code></pre>
<pre><code class="language-java">생성자 호출, url = null
NetworkClient.init
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
13:33:10.029 [main] DEBUGorg.springframework.context.annotation.AnnotationConfigApplicationContext -Closing NetworkClient.close
close + http://hello-spring.dev</code></pre>
<blockquote>
<p>@PostConstruct, @PreDestroy 애노테이션 특징</p>
</blockquote>
<ul>
<li>최신 스프링에서 가장 권장하는 방법이다.</li>
<li>유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용하자.</li>
</ul>
<hr>
<h3 id="🎯정리">🎯정리</h3>
<ul>
<li>@PostConstruct, @PreDestroy 애노테이션을 사용하자</li>
<li>코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야 하면 @Bean 의 initMethod , destroyMethod를 사용하자.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 의존관계 자동 주입]]></title>
            <link>https://velog.io/@ahn_s/Spring-%EC%9D%98%EC%A1%B4%EA%B4%80%EA%B3%84-%EC%9E%90%EB%8F%99-%EC%A3%BC%EC%9E%85</link>
            <guid>https://velog.io/@ahn_s/Spring-%EC%9D%98%EC%A1%B4%EA%B4%80%EA%B3%84-%EC%9E%90%EB%8F%99-%EC%A3%BC%EC%9E%85</guid>
            <pubDate>Mon, 03 Apr 2023 08:16:19 GMT</pubDate>
            <description><![CDATA[<p>★ Reference - <a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard">https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard</a></p>
<h2 id="생성자-주입">생성자 주입</h2>
<ul>
<li>이름 그대로 생성자를 통해서 의존 관계를 주입 받는 방법이다.<ul>
<li>지금까지 우리가 진행했던 방법이 바로 생성자 주입이다.</li>
</ul>
</li>
<li>특징<ul>
<li>생성자 호출시점에 딱 1번만 호출되는 것이 보장된다. </li>
<li>불변, 필수 의존관계에 사용</li>
</ul>
</li>
<li>생성자가 한 개이면 @Autowired 생략해도 자동 주입 된다. (스프링 빈일 경우에만)</li>
</ul>
<pre><code class="language-java">@Component
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    // 생성자가 한개이기 대문에 자동 주입된다. (@Autowired 생략)
    //@Authwired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}</code></pre>
<hr>
<h2 id="수정자-주입setter-주입">수정자 주입(setter 주입)</h2>
<ul>
<li>setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.</li>
<li>특징<ul>
<li>선택, 변경 가능성이 있는 의존관계에 사용</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Component
public class OrderServiceImpl implements OrderService {
    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {

        this.discountPolicy = discountPolicy;
    }
}</code></pre>
<blockquote>
<p>❗ @Autowired 의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false) 로 지정하면 된다.</p>
</blockquote>
<hr>
<h2 id="필드주입">필드주입</h2>
<ul>
<li>이름 그대로 필드에 바로 주입하는 방법이다.</li>
<li>특징<ul>
<li>외부에서 변경이 불가능해서 테스트 하기 힘들다는 치명적인 단점이 있다.</li>
<li>애플리케이션의 실제 코드와 관계 없는 테스트 코드를 제외하고는 사용하지 말자!</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Component
public class OrderServiceImpl implements OrderService {
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private DiscountPolicy discountPolicy;
}</code></pre>
<hr>
<h2 id="일반메서드-주입">일반메서드 주입</h2>
<ul>
<li>일반 메서드를 통해서 주입 받을 수 있다.</li>
<li>특징<ul>
<li>한번에 여러 필드를 주입 받을 수 있다.</li>
<li>일반적으로 잘 사용하지 않는다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="옵션-처리">옵션 처리</h2>
<pre><code class="language-java">public class AutowiredTest {

    @Test
    void AutowiredOption() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
    }

    static class TestBean {
        // @Autowired 만 사용하면 required 옵션의 기본값이 true 로 되어 있어서 자동 주입 대상이없으면 오류가 발생한다.
        @Autowired(required = false)
        public void setNoBean1(Member noBean1){
            System.out.println(&quot;noBean1 = &quot; + noBean1);
        }

        @Autowired
        public void setNoBean2(@Nullable Member noBean2){
            System.out.println(&quot;noBean2 = &quot; + noBean2);
        }

        @Autowired
        public void setNoBean3(Optional&lt;Member&gt; noBean3){
            System.out.println(&quot;noBean3 = &quot; + noBean3);
        }
    }
}</code></pre>
<blockquote>
<ul>
<li>@Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨</li>
</ul>
</blockquote>
<ul>
<li>org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.</li>
<li>ptional&lt;&gt; : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.</li>
</ul>
<h2 id="생성자-주입을-사용해라">생성자 주입을 사용해라!</h2>
<ul>
<li><p>과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다.</p>
</li>
<li><p>이유</p>
<ul>
<li>장점<ul>
<li>불변<ul>
<li>대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다.</li>
<li>누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.</li>
<li>생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.</li>
</ul>
</li>
<li>누락<ul>
<li>생성자 주입은 주입 데이터를 누락했을 때 컴파일 오류가 발생한다. 누락 실수를 막는다.</li>
</ul>
</li>
<li>final 키워드 사용 가능<ul>
<li>생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다.</li>
<li>생성자에서 혹시라도 값이설정되지 않는 오류를 컴파일 시점에 막아준다</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>** 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이기도 하다.**</p>
<hr>
<h2 id="롬복과-최신-트랜드">롬복과 최신 트랜드</h2>
<ul>
<li>Lombok 적용 전</li>
</ul>
<pre><code class="language-java">@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}</code></pre>
<ul>
<li>Lombok 적용 후<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
  private final MemberRepository memberRepository;
  private final DiscountPolicy discountPolicy;
}</code></pre>
</li>
<li>@RequiredArgsConstructor: 필수 값인 final 붙은 필드를 파라미터로 받는 생성자를 자동으로 만들어 준다.</li>
</ul>
<p><strong>즉, 적용 전 코드와 적용 후의 코드의 기능은 동일하다</strong></p>
<hr>
<h2 id="조회대상-빈이-2개-이상일-때">조회대상 빈이 2개 이상일 때</h2>
<ul>
<li>해결방법<ul>
<li>@Autowired 필드 명 매칭</li>
<li>@Qualifier 끼리 매칭 -&gt; 빈 이름 매칭</li>
<li>@Primary 사용</li>
</ul>
</li>
</ul>
<h3 id="autowired-필드-명-매칭">@Autowired 필드 명 매칭</h3>
<pre><code class="language-java">@Autowired
private DiscountPolicy discountPolicy</code></pre>
<p>위에 코드에서 필드 명을 빈 이름으로 변경</p>
<pre><code class="language-java">@Autowired
private DiscountPolicy rateDiscountPolicy</code></pre>
<h3 id="qualifier">@Qualifier</h3>
<ul>
<li>빈 등록시 @Qualifier를 붙여준다<pre><code class="language-java">@Component
@Qualifier(&quot;mainDiscountPolicy&quot;)
public class RateDiscountPolicy implements DiscountPolicy {}</code></pre>
</li>
</ul>
<hr>
<p>@Component
@Qualifier(&quot;fixDiscountPolicy&quot;)
public class FixDiscountPolicy implements DiscountPolicy {}</p>
<pre><code>
이후 생성자 주입 시
```java
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier(&quot;mainDiscountPolicy&quot;) DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}</code></pre><blockquote>
<p>@Qualifier 정리</p>
</blockquote>
<ol>
<li>@Qualifier끼리 매칭</li>
<li>빈 이름 매칭</li>
<li>NoSuchBeanDefinitionException 예외 발생</li>
</ol>
<p>** @Qualifier 는 @Qualifier 를 찾는 용도로만 사용하는게 명확하고 좋다. **</p>
<h3 id="primary">@Primary</h3>
<ul>
<li>@Primary 는 우선순위를 정하는 방법이다. @Autowired 시에 여러 빈이 매칭되면 @Primary 가 우선권을 가진다.<pre><code class="language-java">@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}</code></pre>
But!! ** 우선 순위는 수동인 Qualifier가 자동인 Primary보다 높다. **</li>
</ul>
<hr>
<h2 id="애노테이션-직접-만들기">애노테이션 직접 만들기</h2>
<pre><code class="language-java">@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier(&quot;mainDiscountPolicy&quot;)
public @interface MainDiscountPolicy {

}
---

@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}

---

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
  this.memberRepository = memberRepository;
  this.discountPolicy = discountPolicy;
}
</code></pre>
<hr>
<h2 id="조회한-빈이-모두-필요할-때">조회한 빈이 모두 필요할 때</h2>
<ul>
<li>List,Map
동적으로 빈을 선택해야 할 때 사용하면 좋다 (상황에 맞는 빈을 사용할 때)</li>
</ul>
<pre><code class="language-java">public class AllBeanTest {
    @Test
    void findAllBean() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
        DiscountService discountService = ac.getBean(DiscountService.class);
        Member member = new Member(1L,&quot;userA&quot;, Grade.VIP);
        int discountPrice = discountService.discount(member,10000,&quot;fixDiscountPolicy&quot;);

        assertThat(discountService).isInstanceOf(DiscountService.class);
        assertThat(discountPrice).isEqualTo(1000);

        int rateDiscountPolicy = discountService.discount(member, 20000, &quot;rateDiscountPolicy&quot;);
        assertThat(rateDiscountPolicy).isEqualTo(2000);
    }

    static class DiscountService {
        private final Map&lt;String, DiscountPolicy&gt; policyMap;
        private final List&lt;DiscountPolicy&gt; policies;

        public DiscountService(Map&lt;String, DiscountPolicy&gt; policyMap, List&lt;DiscountPolicy&gt; policies) {
            this.policyMap = policyMap;
            this.policies = policies;
            System.out.println(&quot;policyMap = &quot; + policyMap);
            System.out.println(&quot;policies = &quot; + policies);
        }

        public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member,price);
        }
    }
}</code></pre>
<hr>
<h2 id="자동-빈-수동-빈-등록은-어떤-상황에-사용하면-좋을까">자동 빈 수동 빈 등록은 어떤 상황에 사용하면 좋을까?</h2>
<blockquote>
<p>업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리등이 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.</p>
</blockquote>
<blockquote>
<p>기술 지원 빈: 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.</p>
</blockquote>
<ul>
<li><p>업무 로직은 숫자도 매우 많고, 한번 개발해야 하면 컨트롤러, 서비스, 리포지토리 처럼 어느정도 유사한 패턴이 있다. 이런 경우 자동 기능을 적극 사용하는 것이 좋다. (보통 문제가 발생해도 어떤 곳에서 문제가 발생했는지 명확하게 파악하기 쉬울 때.)</p>
</li>
<li><p>기술 지원 로직은 업무 로직과 비교해서 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다.또한 기술 지원 로직은 적용이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다. 그래서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 드러내는 것이 좋다.</p>
</li>
</ul>
<p>** 애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 딱! 설정 정보에 바로 나타나게 하는 것이 유지보수 하기 좋다. ** </p>
<p>But - ** 예외 : 비즈니스 로직 중에서 다형성을 적극 활용할 때 ** </p>
<ul>
<li>스프링 부트가 아니라 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 수동으로 등록해서 명확하게 드러내는 것이 좋다.</li>
</ul>
<p>** 정리</p>
<ol>
<li>편리한 자동 기능을 기본으로 사용하자</li>
<li>직접 등록하는 기술 지원 객체는 수동 등록</li>
<li>다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자 **</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 컴포넌트 스캔]]></title>
            <link>https://velog.io/@ahn_s/Spring-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%8A%A4%EC%BA%94</link>
            <guid>https://velog.io/@ahn_s/Spring-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%8A%A4%EC%BA%94</guid>
            <pubDate>Sat, 01 Apr 2023 03:15:53 GMT</pubDate>
            <description><![CDATA[<p>★ Reference - <a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard">https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard</a></p>
<h2 id="컴포넌트-스캔과-의존관계-자동-주입">컴포넌트 스캔과 의존관계 자동 주입</h2>
<ul>
<li><p>@ComponentScan : @Component 어노테이션이 붙은 클래스를 찾아서 자동으로 스프링 빈으로 등록을 시켜준다.</p>
<ul>
<li>이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.
(ex) MemberServiceImpl클래스 -&gt; memberServiceImpl</li>
</ul>
</li>
<li><p>@Autowired : 생성자에 @Autowired를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.</p>
<ul>
<li>ac.getBean(MemberService.class)와 동일</li>
</ul>
</li>
</ul>
<hr>
<h2 id="탐색-위치와-기본-스캔-대상">탐색 위치와 기본 스캔 대상</h2>
<ul>
<li>컴포넌트 스캔이 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.<pre><code class="language-java">@ComponentScan(
basePackages = &quot;hello.core&quot;,
)</code></pre>
</li>
<li>basePackages 의 하위 패키지를 모두 탐색한다.<ul>
<li>ex) &quot;hello.core.member&quot; = member 패키지 부터 하위패키지들을 찾는다. (member만 컴포넌트 스캔에 대상이 된다.)</li>
</ul>
</li>
<li>만약 지정하지 않으면 @ComponentScan 이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.<ul>
<li>권장 : 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것이다.<ul>
<li>프로젝트 시작 루트, 여기에 AppConfig 같은 메인 설정 정보를 두고, @ComponentScan 애노테이션을 붙이고, basePackages 지정은 생략한다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<blockquote>
<ul>
<li>컴포넌트 스캔의 기본 대상</li>
</ul>
</blockquote>
<ul>
<li>@Component : 컴포넌트 스캔에서 사용</li>
<li>@Controlller : 스프링 MVC 컨트롤러에서 사용</li>
<li>@Service : 스프링 비즈니스 로직에서 사용</li>
<li>@Repository : 스프링 데이터 접근 계층에서 사용</li>
<li>@Configuration : 스프링 설정 정보에서 사용</li>
</ul>
<hr>
<h2 id="필터">필터</h2>
<ul>
<li><p>includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.</p>
</li>
<li><p>excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.</p>
</li>
<li><p>includeFilters</p>
<pre><code class="language-java">@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}</code></pre>
</li>
<li><p>excludeFilters</p>
<pre><code class="language-java">@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}</code></pre>
</li>
</ul>
<pre><code class="language-java">@ComponentScan(
includeFilters = @Filter(type = FilterType.ANNOTATION, classes =
MyIncludeComponent.class),
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes =
MyExcludeComponent.class)
)</code></pre>
<blockquote>
<p>❗Component 면 충분하기 때문에, includeFilters 를 사용할 일은 거의 없다. excludeFilters 는 여러가지 이유로 간혹 사용할 때가 있지만 많지는 않다. 옵션을 변경하면서 사용하기 보다는 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장한다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 싱글톤 컨테이너]]></title>
            <link>https://velog.io/@ahn_s/Spring-%EC%8B%B1%EA%B8%80%ED%86%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88</link>
            <guid>https://velog.io/@ahn_s/Spring-%EC%8B%B1%EA%B8%80%ED%86%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88</guid>
            <pubDate>Thu, 30 Mar 2023 17:08:42 GMT</pubDate>
            <description><![CDATA[<p>★ Reference - <a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard">https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard</a></p>
<h2 id="웹-애플리케이션과-싱글톤">웹 애플리케이션과 싱글톤</h2>
<ul>
<li><p>웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다.</p>
<pre><code class="language-java">public class SingletonTest {

  /*
  * 호출할 때 마다 새로운 객체를 생성하기 때문에 효율적이지 않다.
  * ex) TPS 가 50000이면 초당 50000개의 새로운 객체를 생성한다.
  */
  @Test
  @DisplayName(&quot;스프링 없는 순수한 DI 컨테이너&quot;)
  void pureContainer() {
      AppConfig appconfig = new AppConfig();
      // 1. 조회 : 호출할 때 마다 객체를 생성
      MemberService memberService1 = appconfig.memberService();;

      // 2. 조회 : 호출할 때 마다 객체를 생성
      MemberService memberService2 = appconfig.memberService();;

      //참조값이 다른 것을 확인
      System.out.println(&quot;memberService1 = &quot; + memberService1);
      System.out.println(&quot;memberService1 = &quot; + memberService2);

      // memberService1 != memberService2
      Assertions.assertThat(memberService1).isNotSameAs(memberService2);
  }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/ahn_s/post/04eeb701-3efa-4c9d-9341-0ad36adf9003/image.png" alt=""></p>
</li>
</ul>
<ul>
<li>위 코드는 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다. 이때 문제점으로는 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸되기 때문에 메모리 낭비가 심하다.</li>
</ul>
<p>🎯 <strong>해결방안</strong> : 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다. (싱글톤 패턴)</p>
<hr>
<h2 id="싱글톤-패턴">싱글톤 패턴</h2>
<blockquote>
<p>싱글톤 패턴</p>
</blockquote>
<ul>
<li>클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
∴ 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.<ul>
<li>private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.</li>
</ul>
</li>
</ul>
<pre><code class="language-java">public class SingletonService {

    // 1. static 영역에 객체를 딱 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();

    // 2. public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용한다.
    public static SingletonService getInstance() {
        return instance;
    }

    // 3. 생성자를 private로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
    private SingletonService() {}

    public void logic() {
        System.out.println(&quot;싱글톤 객체 로직 호출&quot;);
    }
}</code></pre>
<blockquote>
<p>싱글톤의 문제점</p>
</blockquote>
<ul>
<li>싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.</li>
<li>의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.</li>
<li>클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.</li>
<li>테스트하기 어렵다.</li>
<li>내부 속성을 변경하거나 초기화 하기 어렵다.</li>
<li>private 생성자로 자식 클래스를 만들기 어렵다.</li>
<li>결론적으로 유연성이 떨어진다.</li>
<li>안티패턴으로 불리기도 한다.</li>
<li>싱글톤 컨테이너</li>
</ul>
<hr>
<h2 id="싱글톤-컨테이너">싱글톤 컨테이너</h2>
<p>스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로관리한다.
즉, 지금까지 우리가 학습한 스프링 빈이 바로 싱글톤으로 관리되는 빈이다.</p>
<pre><code class="language-java">@Test
    @DisplayName(&quot;스프링 컨테이너와 싱글톤&quot;)
    void springContainer() {
//        AppConfig appconfig = new AppConfig();
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberService memberService1 = ac.getBean(&quot;memberService&quot;, MemberService.class);
        MemberService memberService2 = ac.getBean(&quot;memberService&quot;, MemberService.class);


        System.out.println(&quot;memberService1 = &quot; + memberService1);
        System.out.println(&quot;memberService1 = &quot; + memberService2);

        // memberService1 != memberService2
        Assertions.assertThat(memberService1).isSameAs(memberService2);
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/ahn_s/post/5b7b7642-4227-40c8-9ba0-3886c14ad1d1/image.png" alt=""></p>
<hr>
<h2 id="싱글톤-방식의-주의점">싱글톤 방식의 주의점</h2>
<ul>
<li>싱글톤은 무상태(stateless)로 설계해야 한다.<ul>
<li>특정 클라이언트에 의존적인 필드가 있으면 안된다.</li>
<li>특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.</li>
<li>가급적 읽기만 가능해야 한다.</li>
<li>필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.</li>
</ul>
</li>
</ul>
<ul>
<li>statefulService</li>
</ul>
<pre><code class="language-java">public class StatefulService {
    private int price; // 상태를 유지하는 필드 10000 -&gt; 20000

    public void order(String name, int price){
        System.out.println(&quot;name = &quot; + name + &quot; price = &quot; + price);
        this.price = price; // 여기가 문제!!
    }

    public int getPrice() {
        return price;
    }
}</code></pre>
<ul>
<li><p>statefulServiceTest</p>
<pre><code class="language-java">class StatefulServiceTest {
  @Test
  void statefulServiceSingleton() {
      ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
      StatefulService statefulService1 = ac.getBean(StatefulService.class);
      StatefulService statefulService2 = ac.getBean(StatefulService.class);

      // ThreadA : A사용자 10000원 주문
      statefulService1.order(&quot;userA&quot;,10000);
      // ThreadB : B사용자 20000원 주문
      statefulService2.order(&quot;userB&quot;,20000);

      // ThreadA : 사용자A 주문 금액 조회
      int price = statefulService1.getPrice();
      System.out.println(&quot;price = &quot; + price);

      Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
  }

  static class TestConfig {
      @Bean
      public StatefulService statefulService() {
          return new StatefulService();
      }
  }
}</code></pre>
</li>
<li><p>ThreadA가 사용자A 코드를 호출하고 ThreadB가 사용자B 코드를 호출한다 가정하자.
StatefulService 의 price 필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경한다면, 사용자A의 주문금액은 10000원이 되어야 하는데, 20000원이라는 결과가 나왔다. (문제)</p>
</li>
</ul>
<p>*<em>🎯 해결 *</em></p>
<ul>
<li><p>statefulService</p>
<pre><code class="language-java">public class StatefulService {
//    private int price; // 상태를 유지하는 필드 10000 -&gt; 20000

  public int order(String name, int price){
      System.out.println(&quot;name = &quot; + name + &quot; price = &quot; + price);
//        this.price = price; // 여기가 문제!!
      return price; // return 값으로 변경
  }
}</code></pre>
</li>
<li><p>statefulServiceTest</p>
<pre><code class="language-java">@Test
  void statefulServiceSingleton() {
      ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
      StatefulService statefulService1 = ac.getBean(StatefulService.class);
      StatefulService statefulService2 = ac.getBean(StatefulService.class);

      // ThreadA : A사용자 10000원 주문
      int userAprice = statefulService1.order(&quot;userA&quot;,10000); // 지역변수로 만든다.
      // ThreadB : B사용자 20000원 주문
      int userBprice = statefulService2.order(&quot;userB&quot;,20000); // 지역변수로 만든다.

      // ThreadA : 사용자A 주문 금액 조회
      System.out.println(&quot;price = &quot; + userAprice);
</code></pre>
</li>
</ul>
<p>//        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }</p>
<pre><code>
#### ❗진짜 공유필드는 조심해야 한다! 스프링 빈은 항상 무상태(stateless)로 설계하자.

---
## Configuration과 싱글톤

```java
@Configuration // AppConfig를 설정정보로 사용
public class AppConfig {
/* @Configuration을 사용하지 않고, @Bean만 사용하였을 때
    * call AppConfig.memberService
    * call AppConfig.memberRepository
    * call AppConfig.orderService
    * call AppConfig.memberRepository
    * call AppConfig.memberRepository
    * 
*/

    /* @Configuration을 사용했을 때
     * call AppConfig.memberService
     * call AppConfig.memberRepository
     * call AppConfig.orderService
    * */

    @Bean // 스프링 컨테이너에 등록
    public MemberService memberService() {
        System.out.println(&quot;call AppConfig.memberService&quot;);
        // 생성자 주입
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        System.out.println(&quot;call AppConfig.memberRepository&quot;);
        return new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService() {
        System.out.println(&quot;call AppConfig.orderService&quot;);
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        /*할인 정책을 변경할 때에는 return 값만 바꿔주면 된다.*/
//        return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
 }</code></pre><p>위 주석에서 @Configuration을 사용했을 때와 사용하지 않았을 때의 차이점을 알아보자.</p>
<p>@Configuration을 적용한 스프링 빈을 조회해서 클래스 정보를 출력해보면 </p>
<pre><code>bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70</code></pre><p>클래스 명에 xxxCGLIB가 붙으면서 상당히 복잡해진 것을 볼 수 있다. 
이것은 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다!
즉, @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.</p>
<p><strong>그 덕분에 싱글톤이 보장되는 것이다.</strong></p>
<p>따라서 </p>
<blockquote>
</blockquote>
<ul>
<li>@Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
memberRepository() 처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지않는다.</li>
</ul>
<h4 id="🎯-크게-고민할-것이-없다-스프링-설정-정보는-항상-configuration-을-사용하자">🎯 크게 고민할 것이 없다. 스프링 설정 정보는 항상 @Configuration 을 사용하자.</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 스프링 컨테이너]]></title>
            <link>https://velog.io/@ahn_s/Spring-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88</link>
            <guid>https://velog.io/@ahn_s/Spring-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88</guid>
            <pubDate>Thu, 30 Mar 2023 15:06:53 GMT</pubDate>
            <description><![CDATA[<p>★ Reference - <a href="https://www.inflearn.com/course/lecture?courseSlug=%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&amp;unitId=55352&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&amp;unitId=55352&amp;tab=curriculum</a></p>
<h2 id="스프링-컨테이너-생성">스프링 컨테이너 생성</h2>
<pre><code class="language-java">//스프링 컨테이너 생성
ApplicationContext applicationContext =
        new AnnotationConfigApplicationContext(AppConfig.class);</code></pre>
<blockquote>
<ul>
<li>ApplicationContext 를 스프링 컨테이너 이며 인터페이스이다.</li>
</ul>
</blockquote>
<ul>
<li>스프링 컨테이너는 XML을 기반으로 만들 수 있고, 애노테이션 기반의 자바 설정 클래스로 만들 수 있다.</li>
<li>new AnnotationConfigApplicationContext(AppConfig.class) 클래스는 ApplicationContext 인터페이스의 구현체이다.</li>
</ul>
<p>+더 정확히는 스프링 컨테이너를 부를 때 BeanFactory , ApplicationContext 로 구분해서
이야기하는데, BeanFactory 를 직접 사용하는 경우는 거의 없으므로 일반적으로 ApplicationContext 를 스프링 컨테이너라 부른다.</p>
<p><img src="https://velog.velcdn.com/images/ahn_s/post/3bd3478c-b75b-4584-94d1-4cfdd4fd8197/image.png" alt=""></p>
<p><strong>❗❗주의!</strong> : 빈 이름은 항상 다른 이름을 부여해야 한다. 같은 이름을 부여하면, 다른 빈이 무시되거나, 기존 빈을 덮어버리거나 설정에 따라 오류가 발생한다.</p>
<hr>
<h2 id="스프링-컨테이너에서-데이터-조회">스프링 컨테이너에서 데이터 조회</h2>
<ul>
<li><p>컨테이너에 등록된 모든 빈 조회하기</p>
<pre><code class="language-java">public class ApplicationContextInfoTest {

  AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

  /*
  * ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회한다.
  * ac.getBean() : 빈 이름으로 빈 객체(인스턴스)를 조회한다.
  */

  @Test
  @DisplayName(&quot;모든 빈 출력하기&quot;)
  void findAllBean() {

      String[] beanDefinitionNames = ac.getBeanDefinitionNames();
      for (String beanDefinitionName : beanDefinitionNames) {
          Object bean = ac.getBean(beanDefinitionName);
          System.out.println(&quot;name=&quot; + beanDefinitionName + &quot; object=&quot; + bean);
      }
  }

  @Test
  @DisplayName(&quot;애플리케이션 빈 출력하기&quot;)
  void findApplicationBean() {
      String[] beanDefinitionNames = ac.getBeanDefinitionNames();
      for (String beanDefinitionName : beanDefinitionNames) {
          BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

          //Role ROLE_APPLICATION : 직접 등록한 애플리케이션 빈
          //Role ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈

          if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
              Object bean = ac.getBean(beanDefinitionName);
              System.out.println(&quot;name=&quot; + beanDefinitionName + &quot; object=&quot; + bean);
          }

      }
  }
}</code></pre>
</li>
</ul>
<hr>
<ul>
<li>스프링 빈 조회 - 기본
스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회 방법<ul>
<li>ac.getBean(빈이름, 타입)</li>
<li>ac.getBean(타입)</li>
</ul>
</li>
</ul>
<pre><code class="language-java">import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

public class ApplicationContextBasicFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName(&quot;빈 이름으로 조회&quot;)
    void findBeanByName() {
        MemberService memberService = ac.getBean(&quot;memberService&quot;, MemberService.class);
        //memberService가 MemberServiceImpl의 인스턴스 이면 성공
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName(&quot;이름 없이 타입으로만 조회&quot;)
    void findBeanByType() {
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    // 구체타입으로 조회하면 유연성이 떨어진다.
    @Test
    @DisplayName(&quot;구체 타입으로 조회&quot;)
    void findBeanByName2() {
        MemberService memberService = ac.getBean(&quot;memberService&quot;, MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName(&quot;빈 이름으로 조회 실패&quot;)
    void findBeanByNameFail() {
        //ac.getBean(&quot;xxxx&quot;,MemberService.class);
        assertThrows(NoSuchBeanDefinitionException.class,
                () -&gt; ac.getBean(&quot;xxxx&quot;, MemberService.class));

    }
}</code></pre>
<hr>
<ul>
<li>스프링 빈 조회 - 동일한 타입이 둘 이상<ul>
<li>타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생한다. 이때는 빈 이름을 지정하자.</li>
<li>ac.getBeansOfType() 을 사용하면 해당 타입의 모든 빈을 조회할 수 있다.</li>
</ul>
</li>
</ul>
<pre><code class="language-java">import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ApplicationContextSameBeanFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName(&quot;타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다.&quot;)
    void findBeanByTypeDuplicate() {
        assertThrows(NoUniqueBeanDefinitionException.class,
                () -&gt; ac.getBean(MemberRepository.class));
    }

    @Test
    @DisplayName(&quot;타입으로 조회시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다.&quot;)
    void findBeanByName() {
        MemberRepository memberRepository = ac.getBean(&quot;memberRepository1&quot;,MemberRepository.class);
        assertThat(memberRepository).isInstanceOf(MemberRepository.class);
    }

    @Test
    @DisplayName(&quot;특정 타입을 모두 조회하기&quot;)
    void findAllBeanByType() {
        Map&lt;String, MemberRepository&gt; beansOfType = ac.getBeansOfType(MemberRepository.class);

        for (String key : beansOfType.keySet()) {
            System.out.println(&quot;key = &quot; + key + &quot; value = &quot; + beansOfType.get(key));
        }
        System.out.println(&quot;beansOfType = &quot; + beansOfType);
        assertThat(beansOfType.size()).isEqualTo(2);
    }

    @Configuration
    static class SameBeanConfig {
        @Bean
        public MemberRepository memberRepository1() {
            return new MemoryMemberRepository();
        }

        @Bean
        public MemberRepository memberRepository2() {
            return new MemoryMemberRepository();
        }
    }
}
</code></pre>
<hr>
<ul>
<li>스프링 빈 조회 - 상속관계<ul>
<li>부모 타입으로 조회하면, 자식 타입도 함께 조회한다.</li>
<li>그래서 모든 자바 객체의 최고 부모인 Object 타입으로 조회하면, 모든 스프링 빈을 조회한다.
<img src="https://velog.velcdn.com/images/ahn_s/post/2029933e-32a9-4ece-924f-fd3827669de4/image.png" alt=""><pre><code class="language-java">import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
</code></pre>
</li>
</ul>
</li>
</ul>
<p>public class ApplicationContextExtendsFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);</p>
<pre><code>@Test
@DisplayName(&quot;부모 타입으로 조회시, 자식이 둘 이상 있으면, 중복 오류가 발생한다.&quot;)
void findBeanByParentTypeDuplicate() {
    assertThrows(NoUniqueBeanDefinitionException.class,
            () -&gt; ac.getBean(DiscountPolicy.class));
}

@Test
@DisplayName(&quot;부모 타입으로 조회시, 자식이 둘 이상 있으면, 빈 이름을 지정하면 된다.&quot;)
void findBeanByParentTypeName() {
    DiscountPolicy rateDiscountPolicy = ac.getBean(&quot;rateDiscountPolicy&quot;, DiscountPolicy.class);
    assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
}

@Test
@DisplayName(&quot;특정 하위 타입으로 조회&quot;)
void findBeanBySubType() {
    RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
    assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
}

@Test
@DisplayName(&quot;부모 타입으로 모두 조회하기&quot;)
void findAllBeanByParentType() {
    Map&lt;String, DiscountPolicy&gt; beansOfType = ac.getBeansOfType(DiscountPolicy.class);
    for (String key : beansOfType.keySet()) {
        System.out.println(&quot;key = &quot; + key + &quot; value = &quot; + beansOfType.get(key));
    }
}

@Test
@DisplayName(&quot;부모 타입으로 모두 조회하기 - Object&quot;)
void findAllBeanByObjectType() {
    Map&lt;String, Object&gt; beansOfType = ac.getBeansOfType(Object.class);
    for (String key : beansOfType.keySet()) {
        System.out.println(&quot;key = &quot; + key + &quot; value = &quot; + beansOfType.get(key));
    }
}
@Configuration
static class TestConfig {
    @Bean
    public DiscountPolicy rateDiscountPolicy() {
        return new RateDiscountPolicy();
    }
    @Bean
    public DiscountPolicy fixDiscountPolicy() {
        return new FixDiscountPolicy();
    }
}</code></pre><p>}</p>
<pre><code>---
## BeanFactory와 ApplicationContext

&gt; &quot;BeanFactory&quot;
- 스프링 컨테이너의 최상위 인터페이스로 빈을 관리하고 조회하는 역할을 담당한다

&gt; &quot;ApplicationContext
- BeanFactory 기능을 모두 상속받아서 제공한다.

![](https://velog.velcdn.com/images/ahn_s/post/1aee564a-cc06-460f-a9bf-628928259fa2/image.png)

&gt; - 메시지소스를 활용한 국제화 기능
   - 예를 들어서 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력
- 환경변수
   - 로컬, 개발, 운영등을 구분해서 처리
- 애플리케이션 이벤트
   - 이벤트를 발행하고 구독하는 모델을 편리하게 지원
- 편리한 리소스 조회
   - 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회


</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 객체 지향 원리 적용]]></title>
            <link>https://velog.io/@ahn_s/Java-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%9B%90%EB%A6%AC-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@ahn_s/Java-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%9B%90%EB%A6%AC-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Wed, 29 Mar 2023 16:08:26 GMT</pubDate>
            <description><![CDATA[<p>★ Reference : <a href="https://www.inflearn.com/course/lecture?courseSlug=%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&amp;unitId=55343&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8&amp;unitId=55343&amp;tab=curriculum</a></p>
<h1 id="새로운-할인-정책-적용과-문제점">새로운 할인 정책 적용과 문제점</h1>
<pre><code>//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();</code></pre><p>위 코드에서의 문제점으로는 정액 할인 정책에서 정률 할인 정책으로 변경시, 
추상화(인터페이스), 구체클래스(할인 정책 구현 클래스) 모두 의존 한다. </p>
<p>실제 의존관계
<img src="https://velog.velcdn.com/images/ahn_s/post/d90f81e1-80e5-4798-bee2-d15fd58b55f2/image.png" alt=""></p>
<p>잘보면 클라이언트인 OrderServiceImpl 이 DiscountPolicy 인터페이스 뿐만 아니라
FixDiscountPolicy 인 구체 클래스도 함께 의존하고 있다. 실제 코드를 보면 의존하고 있다! DIP 위반사항이다.</p>
<p><img src="https://velog.velcdn.com/images/ahn_s/post/aead71a6-b613-47f0-af3f-79a83aa02b58/image.png" alt=""></p>
<p>중요!: 그래서 FixDiscountPolicy 를 RateDiscountPolicy 로 변경하는 순간 OrderServiceImpl 의 소스 코드도 함께 변경해야 한다! OCP 위반하였다.</p>
<p>예를 들어 보자면,
공연을 예로 들어 로미오 역할(인터페이스)을 하는 디카프리오라는 남자 배우(구현체)가 줄리엣 역할(인터페이스)을 하는 여자 주인공(구현체)를 초빙하는 것과 같으며 디카프리오는 공연도하고 여자 주인공도 초빙하는 다양한 책임을 가지게 된다.
역할에 맞는 배우를 지정하는 책임을 담당하는 별도의 공연 기획자가 필요하고, 배우와 공연 기획자의 책임을 확실히 분리해야함. (관심사 분리)</p>
<hr>
<blockquote>
<p>해결방안 (관심사 분리)</p>
</blockquote>
<p>애플리케이션의 전체 동작 방식을 구성(config)하기 위해, <strong>&#39;구현 객체를 생성&#39;</strong> 하고, <strong>&#39;연결&#39;</strong> 하는 책임을 가지는 별도의 설정 클래스를 만들기</p>
<h3 id="appconfig-객체의-생성과-연결을-담당">AppConfig (객체의 생성과 연결을 담당)</h3>
<pre><code>public class AppConfig {

    public MemberService memberService() {
        // 생성자 주입
        return new MemberServiceImpl(memberRepository());
    }
    private MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    private DiscountPolicy discountPolicy() {
    /*할인 정책을 변경할 때에는 return 값만 바꿔주면 된다.*/
//        return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}</code></pre><p>애플리케이션의 실제 동작에 필요한 &#39;구현 객체를 생성&#39; 한다.</p>
<ul>
<li>MemberServiceImpl</li>
<li>MemoryMemberRepository</li>
<li>OrderServiceImpl</li>
<li>FixDiscountPolicy</li>
</ul>
<p>AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다.</p>
<ul>
<li>MemberServiceImpl MemoryMemberRepository</li>
<li>OrderServiceImpl MemoryMemberRepository , FixDiscountPolicy</li>
</ul>
<p>이렇게 된다면 MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.</p>
<p>즉, DIP 완성 - MemberServiceImpl 은 MemberRepository 인 추상에만 의존하면 된다. 이제 구체 클래스를
몰라도 된다.</p>
<hr>
<h3 id="좋은-객체-지향-설계의-5가지-원칙의-적용">좋은 객체 지향 설계의 5가지 원칙의 적용</h3>
<p>여기서는 3가지 SRP , DIP , OCP 적용됨.</p>
<blockquote>
<ol>
<li>SRP 단일 책임 원칙 : 한 클래스는 하나의 책임만 가져야 한다.</li>
</ol>
</blockquote>
<ul>
<li>SRP 단일 책임 원칙을 따르면서 관심사 분리</li>
<li>AppConfig: 구현 객체를 생성하고 연결하는 책임</li>
<li>클라이언트 객체: 실행하는 책임</li>
</ul>
<blockquote>
<ol start="2">
<li>DIP 의존관계 역전 원칙 : 프로그래머는 &quot;추상화에 의존해야지, 구체화에 의존하면 안된다.&quot; 의존성 주입은 이 원칙을 따르는 방법 중 하나다.</li>
</ol>
</blockquote>
<ul>
<li>클라이언트 코드가 추상화 인터페이스, 구체화 구현 클래스 함께 의존 했었지만, AppConfig가 구체화 구현 클래스 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입함으로써 클라이언트 코드는 추상화 인터페이스만 의존할 수 있게 되어 DIP 원칙을 지켰다.</li>
</ul>
<blockquote>
<ol start="3">
<li>OCP 개방-폐쇄 원칙 : 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.</li>
</ol>
</blockquote>
<ul>
<li>AppConfig가 의존관계인 할인 정책을 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 된다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[For Me] 애자일]]></title>
            <link>https://velog.io/@ahn_s/%EC%95%A0%EC%9E%90%EC%9D%BC</link>
            <guid>https://velog.io/@ahn_s/%EC%95%A0%EC%9E%90%EC%9D%BC</guid>
            <pubDate>Wed, 29 Mar 2023 14:43:22 GMT</pubDate>
            <description><![CDATA[<h1 id="애자일-방법론">애자일 방법론</h1>
<p>애자일은 신속한 반복 작업을 통해 실제 작동 가능한 소프트웨어를 개발하여 지속적으로 제공하기 위한 소프트웨어 개발 방식입니다. 
구체적으로 말하자면, 애자일 소프트웨어 개발 방법론의 핵심은 작동하는 소프트웨어의 작은 구성 요소를 신속하게 제공하여 고객의 만족도를 개선하는 것입니다. 이러한 방법은 적응형 접근 방식과 팀워크를 활용한 지속적인 개발에 중점을 두고 있습니다. 일반적으로, 애자일 소프트웨어 개발은 소프트웨어 개발자와 비즈니스 담당자가 자체적으로 조직한 소규모 팀으로 이루어지며, 이들은 소프트웨어 개발 라이프사이클 전체에 걸쳐 정기적으로 직접 만나 협업합니다. 애자일 개발은 소프트웨어 도큐멘테이션에 대한 경량화 방식을 선호하며 라이프사이클의 모든 단계에서 변화를 적극 수용합니다.</p>
<h1 id="애자일-소프트웨어-개발-선언문-4대선언">애자일 소프트웨어 개발 선언문 (4대선언)</h1>
<p>애자일 소프트웨어 개발 선언</p>
<p>우리는 <strong>소프트웨어를 개발</strong>하고, 또 <strong>다른 사람의 개발을
도와주면서</strong> 소프트웨어 개발의 <strong>더 나은 방법들을 찾아가고</strong>
있다. 이 작업을 통해 우리는 다음을 가치 있게 여기게 되었다:</p>
<h2 id="공정과-도구보다-개인과-상호작용을">공정과 도구보다 개인과 상호작용을</h2>
<h2 id="포괄적인-문서보다-작동하는-소프트웨어를">포괄적인 문서보다 작동하는 소프트웨어를</h2>
<h2 id="계약-협상보다-고객과의-협력을">계약 협상보다 고객과의 협력을</h2>
<h2 id="계획을-따르기보다-변화에-대응하기를">계획을 따르기보다 변화에 대응하기를</h2>
<p>가치 있게 여긴다. 이 말은, 왼쪽에 있는 것들도 가치가 있지만,
우리는 오른쪽에 있는 것들에 더 높은 가치를 둔다는 것이다.</p>
<ul>
<li><a href="https://agilemanifesto.org/iso/ko/manifesto.html">https://agilemanifesto.org/iso/ko/manifesto.html</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 객체 지향 프로그래밍]]></title>
            <link>https://velog.io/@ahn_s/Java-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D</link>
            <guid>https://velog.io/@ahn_s/Java-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D</guid>
            <pubDate>Mon, 20 Mar 2023 08:38:52 GMT</pubDate>
            <description><![CDATA[<p>★ reference
<a href="https://www.inflearn.com/course/lecture?courseSlug=%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8">https://www.inflearn.com/course/lecture?courseSlug=%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8</a></p>
<h2 id="객체지향-프로그래밍의-특징">객체지향 프로그래밍의 특징</h2>
<ul>
<li>&quot;객체&quot; 들의 모임으로 파악하고자 하는 것으로 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있습니다. (협력)</li>
<li>프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용됩니다.</li>
</ul>
<blockquote>
<h4 id="★-유연하고-변경이-용이하다">★ 유연하고, 변경이 용이하다</h4>
</blockquote>
<ul>
<li>레고 블럭 조립하듯이</li>
<li>키보드, 마우스 갈아 끼우듯이</li>
<li>컴퓨터 부품 갈아 끼우듯이</li>
<li>컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법</li>
</ul>
<p>즉, 객체 지향 프로그래밍의 핵심은 <strong>다형성!!</strong></p>
<hr>
<h3 id="역할과-구현을-분리">역할과 구현을 분리</h3>
<p>역할과 구현으로 구분하면 세상이 단순해지고, 유연해지며 변경도 편리해진다.</p>
<blockquote>
<p>ex) 운전자 (역할)이 있는상태, 자동차는 어떤 종류(구현)여도 자동차의 역할을 한다.
자동차가 바뀌어도(구현) 운전자는 운전할 수 있다. (역할)</p>
</blockquote>
<p>역할 = 인터페이스 / 구현 = 인터페이스를 구현한 클래스, 구현 객체</p>
<p>따라서 클라이언트는 대상의 역할(인터페이스)만 알면 되며, 내부구조나 대상 자체를 변경하여도 영향을 받지 않는다. </p>
<p><strong>핵심❗객체를 설계할 때 역할과 구현을 명확히 분리하여 역할을 먼저 부여하고, 그 역할을 수행하는 구현 객체 만들기</strong></p>
<h1 id="★-정리">★ 정리</h1>
<ul>
<li>실시계의 역할과 구현이라는 편리한 컨셉을 다형성을 통해 객체 세상으로 가져올 수 있다.</li>
<li>유연하고, 변경이 용이</li>
<li>확장 가능한 설계</li>
<li>클라이언트에 영향을 주지 않는 변경 가능</li>
<li><strong>인터페이스를 안정적으로 잘 설계하는 것이 중요</strong> ★</li>
</ul>
<p>But 한계로는</p>
<ul>
<li>역할(인터페이스) 자체가 변하면, 클라이언트, 서버 모두에 큰 변경 필요<ul>
<li>ex) 자동차를 비행기로 변경한다면? / 대본 자체가 변경된다면?</li>
</ul>
</li>
</ul>
<p><strong>🎯즉, 인터페이스를 안정적으로 잘 설계하는 것이 중요하다!!</strong></p>
<hr>
<h1 id="solid">SOLID</h1>
<h3 id="좋은-객체-지향-설계의-5가지-원칙">좋은 객체 지향 설계의 5가지 원칙</h3>
<blockquote>
<p>SRP (단일 책임 원칙) : 하나의 클래스는 하나의 책임만 져야한다.
∴ 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것이다.</p>
</blockquote>
<blockquote>
<p>OCP (개방-폐쇄 운칙) : 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
∴ 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현 
<strong>❗즉, 다형성을 활용!!</strong></p>
</blockquote>
<blockquote>
<p>LSP (리스코프 치환 원칙) : 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
∴ 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것, 다형성을 지원하기 위한 원칙, 인터페이스를 구현한 구현체를 믿고 사용하려면 이 원칙이 필요</p>
</blockquote>
<blockquote>
<p>ISP (인터페이스 분리 원칙) : 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
∴ 인터페이스가 명확해지고, 대체 가능성이 높아진다.</p>
</blockquote>
<blockquote>
<p>DIP (의존관계 역전 원칙) : 프로그래머는 &quot;<strong>추상화에 의존해야지, 구체화에 의존하면 안된다.</strong>&quot; 의존성 주입은 이 원칙을 따르는 방법 중 하나이다.
∴ 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻
❗ 즉, <strong>역할</strong> 에 의존해야지 <strong>구현</strong>에 의존하면 안된다. 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다.</p>
</blockquote>
<hr>
<h2 id="★-정리-1">★ 정리</h2>
<ul>
<li>객체 지향의 핵심은 다형성</li>
<li>다형성 만으로는 쉽게 부품을 갈아 끼우듯이 개발할 수 없다.</li>
<li>다형성 만으로는 구현 객체를 변경할 때 클라이언트 코드도 함께 변경된다.</li>
<li>다형성 만으로는 OCP, DIP를 지킬 수 없다.
∴ 뭔가 더 필요하다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 스프링이란?]]></title>
            <link>https://velog.io/@ahn_s/Spring-%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@ahn_s/Spring-%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Mon, 20 Mar 2023 08:11:55 GMT</pubDate>
            <description><![CDATA[<p>★ refrence 
<a href="https://www.inflearn.com/course/lecture?courseSlug=%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8">https://www.inflearn.com/course/lecture?courseSlug=%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8</a></p>
<ul>
<li><p>핵심기술</p>
<ul>
<li>스프링 DI 컨테이너</li>
<li>AOP</li>
<li>이벤트</li>
<li>기타</li>
</ul>
</li>
<li><p>웹 기술</p>
<ul>
<li>웹 기술 : 스프링 MVC , 스프링 WebFlux</li>
<li>데이터 접근 기술 : 트랜잭션, JDBC, ORM 지원 , XML 지운</li>
</ul>
</li>
<li><p>기술 통합</p>
<ul>
<li>캐시</li>
<li>이메일</li>
<li>원격접근</li>
<li>스케줄링</li>
</ul>
</li>
<li><p>테스트</p>
<ul>
<li>스프링 기반 테스트 지원</li>
</ul>
</li>
<li><p>언어</p>
<ul>
<li>코틀린, 그루비</li>
</ul>
</li>
<li><p>스프링 부트</p>
<ul>
<li>스프링을 편리하게 사용할 수 있도록 지원, 최근에는 기본으로 사용</li>
<li>단독으로 실행할 수 있는 스프링 어플리케이션을 쉽게 생성</li>
<li>Tomcat 같은 웹 서버를 내장해서 별도의 웹 서버를 설치하지 않아도 됨</li>
<li>손쉬운 빌드 구성을 위한 starter 종속성 제공</li>
<li>스프링과 3rd parth(외부) 라이브러리 자동 구성</li>
<li>매트릭, 상태확인, 외부 구성같은 프로덕션 준비 기능</li>
<li>관례에 의한 간결한 설정</li>
</ul>
</li>
</ul>
<hr>
<p>★ 스프링의 핵심 기능</p>
<ul>
<li><p>자바 언어 기반의 프레임워크</p>
</li>
<li><p>자바 언어의 가장 큰 특징 - 객체 지향 언어</p>
</li>
<li><p>객체 지향 언어가 가진 강력한 특징을 살려내는 프레임워크</p>
</li>
<li><p>좋은 객체 지향 어플리케이션을 개발할 수 있게 도와주는 프레임워크</p>
<br>
** 즉, 스프링이 제대로된 객체지향 프로그래밍을 할 수 있도록 도와주는 도구

<hr>
</li>
</ul>
<h2 id="★-스프링과-객체-지향">★ 스프링과 객체 지향</h2>
<ul>
<li>다형성이 가장 중요!!</li>
<li>스프링은 다형성을 극대화해서 이용할 수 있게 도와준다</li>
<li>스프링에서 이야기하는 제어의 역전(IoC), 의존관계 주입(DI)은 다형성을 활용해서 역할과 구현을 편리하게 다룰 수 있도록 지원한다.</li>
<li>스프링을 사용하면 마치 <strong>&quot;레고 블럭 조립하듯이!&quot;</strong>, <strong>&quot;공연 부대의 배우를 선택하듯이!&quot;</strong> 구현을 편리하게 변경할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] AOP]]></title>
            <link>https://velog.io/@ahn_s/Spring-AOP</link>
            <guid>https://velog.io/@ahn_s/Spring-AOP</guid>
            <pubDate>Thu, 16 Mar 2023 16:56:57 GMT</pubDate>
            <description><![CDATA[<ul>
<li>김영한님 &lt;스프링 입문&gt; 강의
<a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8/dashboard">https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8/dashboard</a></li>
</ul>
<h2 id="aop가-필요한-상황">AOP가 필요한 상황</h2>
<ul>
<li>모든 메소드의 호출 시간을 측정하고 싶다면?</li>
<li>공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern)</li>
<li>회원 가입 시간, 회원 조회 시간을 측정하고 싶다면?</li>
</ul>
<pre><code>@Transactional
public class MemberService {
 public Long join(Member member) {
        long start = System.currentTimeMillis();
        try {
            validateDuplicateMember(member);

            memberRepository.save(member);
            return member.getId();
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println(&quot;join = &quot; + timeMs + &quot;ms&quot;);
        }

    }

    public void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName()).ifPresent(m -&gt; {
            throw new IllegalArgumentException(&quot;이미 존재하는 회원입니다.&quot;);
        });
    }

    /**
    * 전체 회원 조회
    */
    public List&lt;Member&gt; findMembers() {
        long start = System.currentTimeMillis();
        try {
            return memberRepository.findAll();
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println(&quot;findMembers = &quot; + timeMs + &quot;ms&quot;);
        }
    }
}
</code></pre><p>★ 문제</p>
<ul>
<li>회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항이 아니다.</li>
<li>시간을 측정하는 로직은 공통 관심 사항이다.</li>
<li>시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여서 유지보수가 어렵다.</li>
<li>시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어렵다.</li>
<li>시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야 한다</li>
</ul>
<h2 id="aop-적용">AOP 적용</h2>
<ul>
<li>AOP: Aspect Oriented Programming</li>
<li>공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern) 분리</li>
</ul>
<pre><code>@Aspect
@Component
public class TimeTraceAop {

    @Around(&quot;execution(* hello.hellospring..*(..))&quot;) // AOP 를 적용할 곳을 설정 (모든 패키지에 적용)
//    @Around(&quot;execution(* hello.hellospring.service..*(..))&quot;) // AOP 를 적용할 곳을 설정 (서비스에만 적용)
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println(&quot;START : &quot; + joinPoint.toLongString());
        try {
            return joinPoint.proceed();
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println(&quot;END : &quot; + joinPoint.toLongString() + &quot; &quot; + timeMs + &quot;ms&quot;);
        }

    }
}</code></pre><p><img src="https://velog.velcdn.com/images/ahn_s/post/dd101434-57a8-4d71-97ad-dc4ba4ea98a9/image.png" alt=""></p>
<p>★ 해결</p>
<ul>
<li>회원가입, 회원 조회등 핵심 관심사항과 시간을 측정하는 공통 관심 사항을 분리한다.</li>
<li>시간을 측정하는 로직을 별도의 공통 로직으로 만들었다.</li>
<li>핵심 관심 사항을 깔끔하게 유지할 수 있다.</li>
<li>변경이 필요하면 이 로직만 변경하면 된다.</li>
<li>원하는 적용 대상을 선택할 수 있다</li>
</ul>
<h1 id="스프링의-aop-동작-방식-설명">스프링의 AOP 동작 방식 설명</h1>
<ul>
<li><p>AOP 적용 전 의존관계
<img src="https://velog.velcdn.com/images/ahn_s/post/522fbb8f-d849-4536-8162-2c0cbe3ba760/image.png" alt=""></p>
</li>
<li><p>AOP 적용 전 전체 관계
<img src="https://velog.velcdn.com/images/ahn_s/post/5fd9c44b-87d7-4370-b360-6e4a3c0b71fd/image.png" alt=""></p>
</li>
<li><p>AOP 적용 후 의존관계
<img src="https://velog.velcdn.com/images/ahn_s/post/cf26f82e-1da9-4870-88e3-be190265598e/image.png" alt=""></p>
</li>
<li><p>AOP 적용 후 전체 관계
<img src="https://velog.velcdn.com/images/ahn_s/post/646285b0-a5b8-4dd3-9092-9d75d7ac5dc5/image.png" alt=""></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 라이브러리]]></title>
            <link>https://velog.io/@ahn_s/Spring-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC</link>
            <guid>https://velog.io/@ahn_s/Spring-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC</guid>
            <pubDate>Thu, 16 Mar 2023 07:09:05 GMT</pubDate>
            <description><![CDATA[<p>&quot;스프링 부트 라이브러리&quot;</p>
<ul>
<li><p>spring-boot-starter-web</p>
<ul>
<li>spring-boot-starter-tomcat : 톰캣 (웹서버)<ul>
<li>spring-webmvc : 스프링 웹 mvc</li>
</ul>
</li>
</ul>
</li>
<li><p>spring-boot-starter-thymeleaf : 타임리프 템플릿 엔진</p>
</li>
<li><p>spring-boot-starter(공통) : 스프링 부트 + 스프링 코어 + 로깅</p>
<ul>
<li><p>spring-boot
+</p>
</li>
<li><p>spring-boot-start-logging</p>
<ul>
<li>logback,slf4j</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>&quot;테스트 라이브러리&quot;</p>
<ul>
<li>spring-boot-start-test<ul>
<li>junit : 테스트 프레임워크</li>
<li>mockito : 목 라이브러리</li>
<li>assertj : 테스트 코드를 좀 더 편하게 작성하게 도와주는 라이브러리</li>
<li>spring-test : 스프링 통합 테스트 지원</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Algo] LinkedHashSet]]></title>
            <link>https://velog.io/@ahn_s/Algo-LinkedHashSet</link>
            <guid>https://velog.io/@ahn_s/Algo-LinkedHashSet</guid>
            <pubDate>Thu, 02 Mar 2023 13:29:26 GMT</pubDate>
            <description><![CDATA[<p>BOJ 13414 번을 해결하는 중 Hash의 순서 고려를 어떻게 해야 하나 찾아보다가 LinkedHashSet이라는 함수가 있어 손쉽게 해결 할 수 있다는 것을 알게되어 작성하였습니다.</p>
<h1 id="linkedhashset이란">LinkedHashSet이란?</h1>
<p>HashSet과 동일한 구조를 가지지만 HashSet은 순서를 관리하지 않아 값을 출력할 때마다 다른 순서대로 출력이 됩니다</p>
<p>하지만 LinkedHashSet은 삽입된 순서대로 반복합니다</p>
<p>HashSet과 동일한 특징들이 있는데 마찬가지로 중복 값을 허용하지 않습니다</p>
<p>기본적인 메서드 들은 set과 동일하기 때문에 순서를 고려한 set이 필요할 때 사용하면 좋을 것 같습니다.</p>
<ul>
<li>BOJ 13414 수강신청</li>
</ul>
<pre><code>import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        int K = Integer.parseInt(st.nextToken());
        int L = Integer.parseInt(st.nextToken());

        LinkedHashSet&lt;String&gt; set = new LinkedHashSet&lt;&gt;();

        for(int i=0;i&lt;L;i++){
            String student = br.readLine();

            if(set.contains(student))
                set.remove(student);
            set.add(student);
        }

        int count = 0;

        for(String next : set){
            count++;
            System.out.println(next);
            if(count == K) break;
        }
    }
}</code></pre><ul>
<li><a href="https://www.acmicpc.net/problem/13414">https://www.acmicpc.net/problem/13414</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>