<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>soeun2k.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 14 Oct 2025 15:46:46 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>soeun2k.log</title>
            <url>https://velog.velcdn.com/images/carol_ly/profile/d00058ef-5ff5-4bca-a88c-47f4020606b3/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. soeun2k.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/carol_ly" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[커피 예제로 이해하는 데코레이터 패턴]]></title>
            <link>https://velog.io/@carol_ly/%EC%BB%A4%ED%94%BC-%EC%98%88%EC%A0%9C%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@carol_ly/%EC%BB%A4%ED%94%BC-%EC%98%88%EC%A0%9C%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Tue, 14 Oct 2025 15:46:46 GMT</pubDate>
            <description><![CDATA[<h2 id="데코레이터-패턴이란">데코레이터 패턴이란?</h2>
<p>데코레이터(Decorator) 패턴은 객체의 <strong>기능</strong>을 동적으로 <strong>확장</strong>할 수 있도록 하는 디자인 패턴입니다. </p>
<h2 id="커피를-주문해볼까--">커피를 주문해볼까. . .</h2>
<p>여러분은 스타벅스 알바생입니다. 음료를 만드는 데 꽤나 힘이 드니 3가지 음료만 만듭니다. 에스프레소 베이스 메뉴, 말차 베이스 메뉴가 있고, 적절한 토핑과 함께하면 아래와 같이 맛있는 메뉴가 탄생합니다!</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/8127acf0-3238-4b69-861a-97b6bec12590/image.jpeg" alt=""></p>
<p>베이스 2종(에스프레소, 말차)과 3가지 토핑(우유, 글레이즈드 폼, 카라멜 시럽)을 적절히 조합해 커피를 판매해보려고 합니다. 이 때, 3가지 종류의 토핑은 기본 베이스를 <strong>확장</strong>해주는 데코레이터입니다.</p>
<h3 id="요구사항-정리">요구사항 정리</h3>
<ul>
<li>베이스: 에스프레소(4000원), 말차(5000원)</li>
<li>토핑: 우유(500원), 글레이즈드 폼(300원), 카라멜 시럽(400원)</li>
</ul>
<ol>
<li>라떼: 에스프레소 + 우유 (4500원)</li>
<li>카라멜 라떼: 에스프레소 + 우유 + 카라멜 시럽 (4900원)</li>
<li>말차 글레이즈드 라떼: 말차 + 우유 + 글레이즈드 폼 + 카라멜 시럽 (6200원)</li>
</ol>
<br>

<h2 id="에스프레소와-말차-베이스만-있다면-다-만들-수-있어">에스프레소와 말차 베이스만 있다면 다~ 만들 수 있어</h2>
<p>먼저 커피 인터페이스를 만들어줍니다.</p>
<ul>
<li>커피 인터페이스</li>
</ul>
<pre><code class="language-java">public interface Coffee {
    int getPrice();
}</code></pre>
<p>이제 커피 인터페이스를 구현해 베이스 2종을 만들어주겠습니다. 에스프레소 베이스는 4000원, 말차 베이스는 5000원입니다.</p>
<ul>
<li>에스프레소 베이스</li>
</ul>
<pre><code class="language-java">public class Espresso implements Coffee{
    @Override
    public int getPrice() {
        return 4000;
    }
}</code></pre>
<ul>
<li>말차 베이스</li>
</ul>
<pre><code class="language-java">public class Matcha implements Coffee{
    @Override
    public int getPrice() {
        return 5000;
    }
}</code></pre>
<p>베이스 두 개 완성!</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/306d5475-019f-4509-bd94-22c77ad51fc7/image.gif" alt=""></p>
<br>

<h2 id="토핑-3개면-나도-바리스타">토핑 3개면 나도 바리스타</h2>
<p>이번에는 3가지 토핑(우유, 글레이즈드 폼, 카라멜 시럽)을 만들어보겠습니다.</p>
<p>토핑은 토핑만으로 커피가 될 수 없고, 반드시 베이스와 결합되어야 합니다. 그렇기 때문에 내부에 베이스 커피가 포함되어야 하고, 필드로 <code>Coffee</code>를 갖고 있어야 합니다. 또한 기본 생성자가 아닌 <code>Coffee</code>를 파라미터로 받는 생성자를 통해서만 <code>Milk</code> 객체를 만들 수 있습니다. </p>
<p>그리고, 베이스 음료에 우유를 넣게 되면, 가격은 기존 음료에서 500원이 추가됩니다.</p>
<ul>
<li>우유 데코레이터</li>
</ul>
<pre><code class="language-java">public class Milk implements Coffee {

    private Coffee coffee;

    public Milk(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public int getPrice() {
        return coffee.getPrice() + 500;
    }
}
</code></pre>
<p>동일한 방식으로, 글레이즈드 폼과 카라멜 시럽도 만들어보겠습니다. </p>
<ul>
<li>글레이즈드 폼 데코레이터</li>
</ul>
<pre><code class="language-java">public class GlazedFoam implements Coffee {

    private Coffee coffee;

    public GlazedFoam(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public int getPrice() {
        return coffee.getPrice() + 300;
    }
}</code></pre>
<ul>
<li>카라멜 시럽 데코레이터</li>
</ul>
<pre><code class="language-java">public class CaramelSyrup implements Coffee{

    private Coffee coffee;

    public CaramelSyrup(Coffee coffee) {
        this.coffee = coffee;    
    }

    @Override
    public int getPrice() {
        return coffee.getPrice() + 400;
    }
}</code></pre>
<br>

<h2 id="커피-공장을-두두두두">커피 공장을 두두두두</h2>
<p>자! 이제 모든 준비가 끝났습니다. 베이스 2종(에스프레소, 말차)과 3가지 토핑(우유, 글레이즈드 폼, 카라멜 시럽)을 적절히 조합해 커피를 만들어보겠습니다. </p>
<ol>
<li>라떼: 에스프레소 + 우유 (4500원)</li>
<li>카라멜 라떼: 에스프레소 + 우유 + 카라멜 시럽 (4900원)</li>
<li>말차 글레이즈드 라떼: 말차 + 우유 + 글레이즈드 폼 + 카라멜 시럽 (6200원)</li>
</ol>
<p>커피 공장에서 라떼를 만들어보겠습니다. 라떼는 에스프레소 베이스에, 우유를 넣으면 됩니다! 에스프레소 베이스가 4000원, 우유가 500원이기 때문에 라떼의 가격은 4500원입니다.</p>
<ul>
<li>라떼</li>
</ul>
<pre><code class="language-java">public class CoffeeFactory {

    public static Coffee latte() {
        return new Milk(new Espresso());
    }
}</code></pre>
<p>에스프레소 베이스를 우유 데코레이터로 <strong>래핑</strong>시켜주었더니 라떼가 만들어졌습니다! </p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/6b92d868-edf2-433e-ada0-b516769bf17a/image.gif" alt=""></p>
<p>테스트 코드를 통해 4500원이 맞는지 확인해봅니다.</p>
<pre><code class="language-java">class CoffeeFactoryTest {

    @Test
    void makeLatte() {
        Coffee latte = CoffeeFactory.latte();
        assertThat(latte.getPrice()).isEqualTo(4500);
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/6f320e81-8cbc-4428-b7e5-5fe925dbe6fc/image.png" alt=""></p>
<p>마찬가지로 카라멜 라떼, 말차 글레이즈드 라떼도 만들어보겠습니다.</p>
<p>카라멜 라떼는 <strong>에스프레소 베이스</strong>(<code>4000원</code>)에 우유(<code>500원</code>), 카라멜 시럽(<code>400원</code>)을 추가하면 되며, 가격은 4900원입니다. </p>
<p>말차 글레이즈드 라떼는 <strong>말차 베이스</strong>에 우유(<code>500원</code>), 글레이즈드 폼(<code>300원</code>), 카라멜 시럽(<code>400원</code>)을 추가하면 되며, 가격은 6200원입니다.</p>
<pre><code class="language-java">public class CoffeeFactory {

    // 라떼 생략

    public static Coffee caramelLatte() {
        return new CaramelSyrup(new Milk(new Espresso()));
    }

    public static Coffee matchaGlazedLatte() {
        return new GlazedFoam(new CaramelSyrup(new Milk(new Matcha())));
    }
}</code></pre>
<p>코드를 보면, 말차 글레이즈드라떼를 만들기 위해 여러 데코레이터가 재귀적으로 말차 베이스를 래핑하고 있습니다. </p>
<p>이처럼 데코레이터 패턴은 객체의 <strong>기능을 확장하거나 변경해야 할 때, *<em>객체를 *</em>재귀적으로 결합</strong>하여 서브 클래스가 폭발적으로 많아지는 문제를 해결할 수 있는 구조적(Structural) 디자인 패턴입니다.</p>
<p>지금은 세 가지 메뉴 조합만 있지만, 바닐라 시럽, 초코 드리즐, 자바칩, 모카 소스, 휘핑크림, . . . 등 데코레이터가 늘어난다면 만들 수 있는 메뉴도 기하급수적으로 늘어나게 됩니다. </p>
<p>자바칩 초코 프라푸치노는 에스프레소 베이스에 모카 소스, 휘핑크림, 초코 드리즐을 추가하면 만들 수 있습니다. 이 외에 바닐라 라떼, 초코 바닐라 라떼, 자바칩 프라푸치노,초코 라떼 등 수많은 커피 메뉴를 탄생시킬 수 있습니다!</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/c63981e7-fae2-4fa9-a1e6-fc3b49873054/image.png" alt=""></p>
<p>커피 공장에서 카라멜 라떼를 만들어 4900원인지 확인하고, 말차 글레이즈드라떼를 만들어 6200원이 맞는지 테스트 코드를 통해 확인해보겠습니다.</p>
<pre><code class="language-java">class CoffeeFactoryTest {


    // 라떼 테스트 코드 생략

    @Test
    void makeCaramelLatte() {
        Coffee caramelLatte = CoffeeFactory.caramelLatte();
        assertThat(caramelLatte.getPrice()).isEqualTo(4900);
    }

    @Test
    void makeMatchaGlazedLatte() {
        Coffee matchaGlazedLatte = CoffeeFactory.matchaGlazedLatte();
        assertThat(matchaGlazedLatte.getPrice()).isEqualTo(6200);
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/e726b8c5-e6fc-45a8-9603-a9f7a7f65825/image.png" alt=""></p>
<p>모든 테스트가 성공했습니다!</p>
<p>그럼 지금까지 구현한 내용을 클래스 다이어그램으로 그려보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/a4dd5fd2-a0fd-4152-96d3-47c0325295a1/image.png" alt=""></p>
<p>베이스 2종과 토핑 3종 모두 커피 인터페이스를 구현하고 있으며, 토핑 3종(<code>Milk</code>, <code>GlazedFoam</code>, <code>CaramelSyrup</code>)은 베이스 2종(<code>Espresso</code>, <code>Matcha</code>)에 결합될 수 있는 데코레이터로서 내부 필드로 커피 타입을 갖고 있습니다.</p>
<p>그런데 저희가 흔히 보는 데코레이터 패턴의 클래스 다이어그램은 저렇게 생기지 않았던 것 같은데요. 이제 <strong>데코레이터 추상 클래스</strong>를 추가해서 우유, 글레이즈드폼, 카라멜 시럽이 <strong>상속</strong>받도록 변경해 토핑 3종이 데코레이터라는 것을 조금 더 명확하게 드러내보겠습니다.</p>
<br>

<h2 id="추상-클래스를-잘-써보자">추상 클래스를 잘 써보자!</h2>
<p>추상 클래스는 추상 메서드와 변수를 가질 수 있습니다. 위에 구현한 내용을 아래 그림처럼 바꿔보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/100a9e2e-77cc-48cb-818c-31e5b2840373/image.png" alt=""></p>
<p><code>CoffeeDecorator</code>는 추상 클래스로, <code>Coffee</code> 인터페이스를 구현합니다. 이때 <code>Coffee</code> 인터페이스의 <code>getPrice()</code> 메서드의 구현은 <code>Milk</code>, <code>GlazedFoam</code>, <code>CaramelSyrup</code> 클래스에서 하게 됩니다.</p>
<p>커피 토핑은 반드시 베이스가 있어야 하기 때문에, 필드에 <code>Coffee</code>를 선언하고, <code>Coffee</code> 타입을 받는 생성자를 만들어줍니다.</p>
<ul>
<li>커피 데코레이터 추상 클래스</li>
</ul>
<pre><code class="language-java">public abstract class CoffeeDecorator implements Coffee {
    Coffee coffee;

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }
}</code></pre>
<blockquote>
<p>❓ <code>Coffee</code> 인터페이스를 구현했는데 왜 <code>getPrice()</code> 메서드를 오버라이드하라는 에러가 안뜨지</p>
</blockquote>
<p><code>CoffeeDecorator</code>는 추상 클래스이기 때문에 이 자체로 객체를 만들 수 없습니다. 결국 다른 구체(concrete) 클래스가 <code>CoffeeDecorator</code>를 구현해줘야 합니다. 추상 클래스이기 때문에, 추상 메서드를 가질 수 있습니다. <code>CoffeeDecorator</code>가 인터페이스에 있는 메서드를 구현해도 되지만, 그 아래의 구체 클래스에서 구현해도 됩니다.</p>
<p>이제 <code>CoffeeDecorator</code> 추상 클래스를 상속 받는 <code>Milk</code>, <code>GlazedFoam</code>, <code>CaramelSyrup</code>을 만들어보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/7e5f33cc-a9b0-46e6-a05f-3a429fed8af7/image.png" alt=""></p>
<ul>
<li>우유</li>
</ul>
<p><code>CoffeeDecorator</code> 추상클래스를 상속받는 <code>Milk</code> 클래스를 만들고, <code>Coffee</code> 인터페이스에 있던 <code>getPrice()</code> 메서드를 구현해줍니다. </p>
<p>이때 <code>Milk</code>는 상위 클래스인<code>CoffeeDecorator</code>로부터 <code>coffee</code> 인스턴스 필드를 상속 받아 아래와 같이 사용할 수 있습니다.</p>
<pre><code class="language-java">public class Milk extends CoffeeDecorator {

    public Milk(Coffee coffee) {
        super(coffee);
    }

    @Override
    public int getPrice() {
        return coffee.getPrice() + 500;
    }
}
</code></pre>
<p>앗. . 그런데 에러가 났습니다. </p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/26cfb8ab-4b60-43a7-bb4f-d3107a2fb501/image.png" alt=""></p>
<p>패키지 구조를 살펴보겠습니다. </p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/0d6f9794-afe6-4e74-b7ed-87fa21646d75/image.png" alt=""></p>
<p><code>CoffeeDecorator</code>와 <code>Milk</code>는 다른 패키지에 있습니다. <code>CoffeeDecorator</code>의 <code>coffee</code>는 default 접근 제어자로 설정 되어 있기 때문에 다른 패키지에서는 접근할 수 없습니다. 따라서 <strong>protected</strong> 접근제어자로 바꿔줍니다. </p>
<p>protected 접근 제어자를 사용하면, 패키지 위치가 다른 자식 클래스에서 해당 필드에 접근이 가능합니다.</p>
<ul>
<li>커피 데코레이터 추상 클래스 수정</li>
</ul>
<pre><code class="language-java">public abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;

    // 생성자 생략
}</code></pre>
<p>에러가 해결됐습니다!</p>
<p>이제 글레이즈드폼과 카라멜 시럽도 마저 만들어주겠습니다.</p>
<ul>
<li>글레이즈드폼</li>
</ul>
<pre><code class="language-java">public class GlazedFoam extends CoffeeDecorator {

    public GlazedFoam(Coffee coffee) {
        super(coffee);
    }

    @Override
    public int getPrice() {
        return coffee.getPrice() + 300;
    }
}</code></pre>
<ul>
<li>카라멜 시럽</li>
</ul>
<pre><code class="language-java">public class CaramelSyrup extends CoffeeDecorator {

    public CaramelSyrup(Coffee coffee) {
        super(coffee);
    }

    @Override
    public int getPrice() {
        return coffee.getPrice() + 400;
    }
}
</code></pre>
<p>커피 공장, 에스프레소, 말차, 테스트 코드는 그대로입니다. </p>
<p>테스트 코드를 돌려 라떼, 카라멜 라떼, 말차 글레이즈드 라떼가 잘 만들어지는지 확인해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/bb89d5b3-deb3-486a-93d7-d3d3973956bb/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/26d1a1a1-3079-4a7a-8c95-80df7696bee8/image.gif" alt=""></p>
<p>커피를 열심히 만들면서 데코레이터 패턴에 대해 알아봤습니다. 말차 글레이즈드 라떼는 참 맛있습니다.</p>
<p>다음 번에는 데코레이터 패턴과 구조는 동일하지만 용도가 다른 프록시 패턴을 알아보겠습니다람쥐</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/62c5f654-cf93-4f98-9fe1-402612d20207/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[인터페이스와 추상클래스 (feat. 헐리우드 원칙과 템플릿 메서드 패턴)]]></title>
            <link>https://velog.io/@carol_ly/%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EC%99%80-%EC%B6%94%EC%83%81%ED%81%B4%EB%9E%98%EC%8A%A4</link>
            <guid>https://velog.io/@carol_ly/%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EC%99%80-%EC%B6%94%EC%83%81%ED%81%B4%EB%9E%98%EC%8A%A4</guid>
            <pubDate>Wed, 22 Jan 2025 08:19:04 GMT</pubDate>
            <description><![CDATA[<h2 id="✈️-시작하며">✈️ 시작하며</h2>
<p><strong>추상화</strong>는 <strong>객체 지향 프로그래밍</strong>의 핵심적인 특징입니다. 추상화를 통해 간단한 인터페이스를 제공하고, 복잡한 구현은 뒤로 감춰줍니다. </p>
<p>Java에서는 <strong>인터페이스</strong>와 <strong>추상 클래스</strong>를 통해 추상화를 달성합니다.</p>
<p>이 둘은 어떤 차이점이 있을까요?</p>
<p><img src="https://velog.velcdn.com/images/carol0724/post/36bdfc92-abe1-462d-8837-7f8bf8d227bc/image.webp" alt=""></p>
<h2 id="🛫-인터페이스">🛫 인터페이스</h2>
<p>인터페이스에 선언 가능한 것들을 살펴보도록 하겠습니다.</p>
<h3 id="1️⃣-추상-메서드">1️⃣ 추상 메서드</h3>
<pre><code class="language-java">public interface Animal {

    // 추상 메서드
    void makeSound();
    void move();
}</code></pre>
<p><strong>추상 메서드</strong>는 <strong>구현부</strong>(<code>{ }</code>)<strong>가 없는 메서드</strong>입니다. 오직 메서드 이름과 파라미터만 가지며, <strong>abstract 키워드</strong>를 붙여 정의합니다. </p>
<p>인터페이스에 구현부가 없는 메서드를 정의하면, 컴파일러가 abstract 키워드를 자동으로 붙여줍니다.</p>
<p>이때 <strong>추상 메서드의 접근 제어자는 오직 public으로만 선언</strong>할 수 있습니다. </p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/d9b6148a-4d79-4fb4-9873-656700e92ab6/image.png" alt=""></p>
<p>private, protected 등으로 선언 시 위와 같이 컴파일 에러가 발생합니다. </p>
<br>


<h3 id="2️⃣-상수">2️⃣ 상수</h3>
<pre><code class="language-java">public interface Animal {

    // 상수
    int MAX_AGE = 100;

}</code></pre>
<p>인터페이스에서 변수를 선언할 때는 항상 <code>public</code>, <code>static</code>, <code>final</code>로만 선언할 수 있습니다. 즉, 인터페이스에서 선언된 모든 변수는 <strong>상수</strong>로 취급되며, <strong>변경할 수 없습니다.</strong></p>
<h3 id="🤔-왜-인터페이스의-변수는-모두-static-final일까">🤔 왜 인터페이스의 변수는 모두 static final일까?</h3>
<p>자바의 인터페이스는 <strong>인스턴스화(=객체화)할 수 없습니다.</strong> 그렇기에 객체가 생성될 때 만들어지는 인스턴스 변수도 가질 수 없습니다.</p>
<pre><code class="language-java">Animal animal = new Animal(); // 컴파일 에러</code></pre>
<p>따라서, 인터페이스에서는 정적 컨텍스트에서 공유될 수 있는 <strong>상수</strong>만 정의할 수 있습니다. </p>
<pre><code class="language-java">int animalMaxAge = Animal.MAX_AGE;</code></pre>
<p>인터페이스에서 정의한 변수는 인터페이스 이름과 함께 사용할 수 있습니다.</p>
<p>🙋‍♂️ 참고) <code>private</code> <code>static</code> <code>final</code>은 사용 불가</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/fe85fdee-4c4e-49a6-9646-778798c1ad00/image.png" alt=""></p>
<br>

<h3 id="3️⃣-구현-메서드---default-메서드">3️⃣ 구현 메서드 - default 메서드</h3>
<p>Java8 이전에는 인터페이스가 구현 메서드를 가지지 않았습니다. Java8 이후, <strong>인터페이스에 static과 default 메서드가 추가되며 구현 메서드를 제공하고 있습니다.</strong></p>
<h3 id="java8은-왜-default-메서드를-추가했을까">Java8은 왜 default 메서드를 추가했을까?</h3>
<p>앞서 제시한 <code>Animal</code> 인터페이스를 구현한 <code>Cow</code>, <code>Lion</code> 클래스를 만들었습니다. 인터페이스에 선언한 <code>makeSound()</code>, <code>move()</code> 메서드를 각 구현체에서 구현한 모습입니다. </p>
<pre><code class="language-java">public class Cow implements Animal{
    @Override
    public void makeSound() {
        System.out.println(&quot;음메에에에&quot;);
    }

    @Override
    public void move() {
        System.out.println(&quot;아주 느리게 한 칸..&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Lion implements Animal{
    @Override
    public void makeSound() {
        System.out.println(&quot;어흥&quot;);
    }

    @Override
    public void move() {
        System.out.println(&quot;아주 빠르게 열 칸!!!!&quot;);
    }
}</code></pre>
<p>그런데 이런 클래스가 100개가 있다고 가정해보겠습니다. (양, 고양이, 돼지, 말, 얼룩말, ...)</p>
<p>개발 도중에 요구사항이 바뀌어 몇몇 동물들은 이제 소리를 내고, 움직이는 것 외에도 잠을 잘 수 있게 되었습니다!! <code>Animal</code> 인터페이스에 <code>sleep()</code> 추상 메서드를 추가했습니다.</p>
<pre><code class="language-java">public interface Animal {

    // 기존 추상 메서드
    void makeSound();
    void move();

    // 새롭게 추가된 추상 메서드
    void sleep();
}    </code></pre>
<p>그럼 이제 100개의 동물 클래스에서 이런 에러를 만나게 됩니다. 새로 추가된 <code>sleep()</code> 메서드를 구현하라는 내용입니다. </p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/e95190b0-af44-4edc-b9e4-c5892c11a6e2/image.png" alt=""></p>
<p>그런데 대부분의 동물들은 동일한 패턴으로 잠에 듭니다. 따라서 동물별로 <code>sleep()</code> 메서드를 다르게 구현할 필요가 없습니다. 또 하나의 메서드를 추가했을 뿐인데, 100개의 클래스를 일일이 수정하는 것도 번거롭습니다.</p>
<p>이러한 상황을 해결하고자, Java8은 default 메서드를 소개합니다. </p>
<p><strong>기존 인터페이스에 새로운 default 메서드를 추가하더라도, 구현체를 변경할 필요가 없습니다.</strong> 즉, 구현체에서 default 메서드를 별도로 구현하지 않더라도, 인터페이스에 정의된 default 메서드를 곧바로 사용할 수 있습니다.</p>
<blockquote>
<p>💡 Java8은 인터페이스에 왜 default 메서드를 도입했을까?
인터페이스에 새로운 default 메서드를 추가하더라도, <strong>기존의 구현체들에서 이 새로운 메서드를 구현하도록 강제하지 않아도 된다.</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/carol0724/post/ea73d019-7348-438e-aec0-50a384f7d2ec/image.webp" alt=""></p>
<h3 id="✏️-default-메서드-사용하기">✏️ default 메서드 사용하기</h3>
<p>default 메서드는 인터페이스에서 바로 구현하며, <strong>하위 클래스에서 별도로 구현하지 않고 바로 사용할 수 있습니다.</strong> 또한 default 메서드는 하위 클래스에서 선택적으로 재정의할 수 있습니다.</p>
<p>Animal 인터페이스에 <code>sleep()</code> 메서드를 추가했습니다. 이때 <code>default</code> 키워드를 붙여 default 메서드임을 명시해줍니다.</p>
<pre><code class="language-java">public interface Animal {

    // 추상 메서드
    void makeSound();
    void move();

    // default 메서드
    default void sleep() {
        System.out.println(&quot;Zzz... 동물이 잠을 잡니다.&quot;);
    }
}</code></pre>
<p>소(<code>Cow</code>)와 사자(<code>Lion</code>)는 <code>sleep()</code> 메서드에 대한 별다른 구현을 하지 않더라도 <code>sleep()</code>을 호출할 수 있습니다.</p>
<pre><code class="language-java">Animal cow = new Cow();
Animal lion = new Lion();

cow.sleep();
lion.sleep();</code></pre>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/45a7a6bf-b4c1-4d17-937d-79c1bbcf8248/image.png" alt=""></p>
<p>이처럼 <strong>default 메서드는 하위 클래스에서 별도로 구현하지 않고도 사용할 수 있습니다.</strong> 기존의 구현체들을 변경하지 않고 인터페이스에 새로운 기능을 추가할 수 있다는 점이 큰 장점인데요!</p>
<h3 id="✍️-default-메서드는-하위-클래스에서-재정의할-수-있다">✍️ default 메서드는 하위 클래스에서 재정의할 수 있다.</h3>
<p>위의 예시처럼 인터페이스에 정의한 default 메서드를 그대로 사용할 수도 있지만, <strong>하위 클래스에서 재정의</strong>하여 사용할 수도 있습니다.</p>
<p>예를 들어, 거꾸로 매달려서 잠을 자는 박쥐를 생각해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/ffae2e83-7df0-45d6-8522-b292ebb2002d/image.png" alt=""></p>
<p>일반적인 동물들과는 다른 방식으로 잠을 자는 박쥐는 구현체에서 default 메서드(<code>sleep()</code>)를 재정의하려고 합니다.</p>
<pre><code class="language-java">public class Bat implements Animal{
    @Override
    public void makeSound() {
        System.out.println(&quot;대롱대롱&quot;);
    }

    @Override
    public void move() {
        System.out.println(&quot;하늘에서 날기&quot;);
    }

    // default 메서드 재정의 - 선택적
    @Override
    public void sleep() {
        System.out.println(&quot;Zzz... 박쥐는 매달려서 잠을 잡니다.&quot;);
    }
}</code></pre>
<p>소, 사자, 박쥐가 잠을 자는 코드를 실행해보겠습니다.</p>
<pre><code class="language-java">Animal cow = new Cow();
Animal lion = new Lion();
Animal bat = new Bat();

cow.sleep();
lion.sleep();
bat.sleep();</code></pre>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/6c380063-a1c0-4b16-85e3-20fd3e091317/image.png" alt=""></p>
<p>Cow와 Lion은 별도로 default 메서드를 재정의하지 않았기 때문에, 인터페이스에 정의한 내용이 출력됩니다. Bat은 구현체에서 default 메서드를 재정의했기 때문에, 구현체에서 오버라이딩한 내용이 출력됩니다. </p>
<h3 id="참고-다중-인터페이스-상속-시-발생할-수-있는-default-메서드-충돌-문제">참고) 다중 인터페이스 상속 시 발생할 수 있는 Default 메서드 충돌 문제</h3>
<p>자바는 인터페이스의 다중 상속을 허용합니다. 소는 동물이면서, 일을 할 수 있습니다. <code>Workable</code> 인터페이스를 정의하고, 소가 <code>Animal</code>과 <code>Workable</code>을 모두 구현하도록 했습니다. 이때 일을 할 때도 잠시 잠을 자며 휴식할 수 있기 때문에 <code>sleep()</code> default 메서드를 정의했습니다.</p>
<pre><code class="language-java">public interface Workable {
    void work();

    default void sleep() {
        System.out.println(&quot;Zzz... 일하다가 잠시 잠에 듭니다.&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Cow implements Animal, Workable{
    @Override
    public void makeSound() {
        System.out.println(&quot;음메에에에&quot;);
    }

    @Override
    public void move() {
        System.out.println(&quot;아주 느리게 한 칸..&quot;);
    }

    @Override
    public void work() {
        System.out.println(&quot;소는 우유를 만듭니다.&quot;);
    }
}</code></pre>
<p>위 코드에서는 아래와 같은 에러 메시지를 발견할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/22d22708-decf-411f-8a40-9e45bfdf3fcb/image.png" alt=""></p>
<p><code>Cow</code>는 <code>Animal</code>과 <code>Workable</code> 인터페이스의 <code>sleep()</code> 메서드를 둘 다 상속 받기 때문에, 충돌이 발생한 것입니다. 따라서 하위 클래스에서 default 메서드(<code>sleep()</code>)를 재정의하여 <strong>어느 것을 호출해야 할지 명시적으로 정해줘야 합니다.</strong> </p>
<pre><code class="language-java">@Override
public void sleep() {
    Animal.super.sleep();
    /* 별개로 구현해도 됨 */
}</code></pre>
<br>

<h3 id="4️⃣-구현-메서드---static-메서드">4️⃣ 구현 메서드 - static 메서드</h3>
<p>Java8부터 인터페이스에서 default 메서드와 함께 static 메서드도 정의할 수 있게 되었습니다. static 메서드는 특정 인스턴스에 종속되지 않기에, 메서드 이름 앞에 인터페이스 이름을 붙여 호출합니다.</p>
<pre><code class="language-java">public interface Animal {

    /* 생략 */

    // static 메서드
    static String getCode() {
        return &quot;ANIMAL&quot;;
    }
}</code></pre>
<pre><code class="language-java">System.out.println(Animal.getCode()); // ANIMAL 출력</code></pre>
<p>이와 더불어, <strong>static 메서드는 다른 static 메서드 또는 default 메서드에서도 호출될 수 있습니다.</strong> 또한, <code>public</code> 및 <code>private</code> 접근 제어자로 선언할 수 있습니다.</p>
<pre><code class="language-java">public interface Animal {

    /* 생략 */

    default void eat(String food) {
        // default 메서드 내에서 static 메서드 호출
        if (isEdible(food)) {
            System.out.println(&quot;동물이 &quot; + food + &quot;을(를) 먹습니다.&quot;);
        } else {
            System.out.println(&quot;이 음식은 먹을 수 없습니다!&quot;);
        }
    }

    private static boolean isEdible(String food) {
        // 음식이 먹을 수 있는지 검증하는 로직
        return !food.equals(&quot;돌&quot;) &amp;&amp; !food.equals(&quot;흙&quot;);
    }
}</code></pre>
<br>


<h3 id="5️⃣-구현-메서드---private-메서드">5️⃣ 구현 메서드 - private 메서드</h3>
<p>Java9 이후 인터페이스에 private 구현 메서드를 정의할 수 있습니다. Java8에서 추가된 <strong>default 메서드와 static 메서드의 코드를 캡슐화</strong>하기 위해 등장했습니다.</p>
<blockquote>
<p>⭐️ <strong>인터페이스 요약</strong> ⭐️
(1) 인터페이스를 인스턴스화할 수 없다. (구현체가 필요)
(2) 인터페이스에서 정의하는 변수는 모두 public, static, final한 상수이다.
(3) 추상/구현 메서드에서 final 키워드를 사용할 수 없다.
(4) 추상/구현 메서드의 접근 제어자가 protected가 될 수 없다.
(5) 추상 메서드의 접근 제어자는 오직 public이다.
(6) Java8부터 default, static 구현 메서드가 제공된다. default 메서드의 접근 제어자는 public이며, static 메서드는 public/private 둘 다 사용 가능하다.
(7) Java9부터 private 구현 메서드가 제공된다. </p>
</blockquote>
<br>

<h2 id="🌟-인터페이스-vs-추상-클래스">🌟 인터페이스 VS 추상 클래스</h2>
<p><strong>Java8부터 인터페이스도 구현 메서드를 제공</strong>하게 되면서 인터페이스와 추상 클래스의 경계가 조금 흐릿해졌습니다. </p>
<p>그럼에도 불구하고 여전히 유의미한 차이가 존재하며, 그 중 <strong>객체 지향적 관점에서 추상 클래스가 필요한 이유</strong>를 설명해보려고 합니다.</p>
<p><img src="https://velog.velcdn.com/images/carol0724/post/3cf41a22-3431-4087-b2e4-225d5a4b67a1/image.webp" alt=""></p>
<h3 id="🐰-추상-클래스는-public이-아닌-추상-메서드를-가질-수-있다">🐰 추상 클래스는 public이 아닌 추상 메서드를 가질 수 있다.</h3>
<p><strong>인터페이스</strong>에 <strong>추상 메서드</strong>를 선언 시 <strong>오직 public 접근 제어자</strong>만 사용할 수 있습니다. </p>
<p>반면, <strong>추상 클래스</strong>의 경우 public과 더불어 <strong>protected 추상 메서드를 가질 수 있습니다.</strong></p>
<p>이것이 왜 유효한 차이인지 <strong>헐리우드 원칙</strong>과 <strong>템플릿 메서드 패턴</strong>을 소개하며 설명을 이어가보도록 하겠습니다.</p>
<br>

<h3 id="🎥-헐리우드-원칙">🎥 헐리우드 원칙</h3>
<blockquote>
<p>📞 <strong>&quot;Don&#39;t Call Us, We&#39;ll Call You&quot;</strong></p>
</blockquote>
<p>헐리우드 원칙의 핵심은 &quot;자꾸 연락하지마 내가 연락할게&quot;입니다. 여기서 <strong>연락을 하는 주체는 고수준 컴포넌트</strong>이며, <strong>연락을 받는 대상은 저수준 컴포넌트</strong>입니다.</p>
<blockquote>
<p>☝️ <em><strong>Low-Level</strong> Components Are <strong>Passive</strong></em></p>
</blockquote>
<p>저수준 컴포넌트는 고수준 컴포넌트를 호출하지 않습니다. 저수준 컴포넌트는 <strong>수동적</strong>인 존재로, <strong>고수준 컴포넌트의 호출이 있을 때까지 기다립니다.</strong></p>
<blockquote>
<p>✌️ <em><strong>High-Level</strong> Components <strong>Control the Flow</strong></em></p>
</blockquote>
<p>고수준 컴포넌트는 애플리케이션 실행 흐름에 대한 <strong>제어권</strong>을 갖고 있습니다. 저수준 컴포넌트가 언제, 어떻게 행동해야 할지를 결정합니다. 즉 <strong>고수준 컴포넌트가 저수준 컴포넌트르 호출</strong>하여, 특정 변화 또는 이벤트가 있을 때 저수준 컴포넌트가 적절하게 반응할 수 있도록 <strong>시스템의 흐름을 제어</strong>합니다.</p>
<p>헐리우드 원칙은 관찰자 패턴(Observer Pattern), 템플릿 메서드 패턴(Template Method Pattern), 이벤트 기반 아키텍처(Event Driven Architecture) 등에서 사용됩니다.</p>
<br>

<h3 id="🔧-헐리우드-원칙을-준수하여-추상-클래스-설계하기">🔧 헐리우드 원칙을 준수하여 추상 클래스 설계하기</h3>
<p>아래와 같은 요구사항을 떠올리며 Animal 추상 클래스를 정의해보겠습니다. </p>
<p>(1) 동물들은 밥을 먹습니다.
(2) 밥을 먹으려면 사냥을 해야 합니다. 
(3) 사냥 방법은 동물마다 다릅니다. 
(4) 그 외의 밥 먹는 순서는 동물마다 동일합니다.</p>
<p><img src="https://velog.velcdn.com/images/carol0724/post/c9bf4329-9f73-4959-84d1-31b7aad36fcf/image.gif" alt=""></p>
<p>Animal 추상 클래스 안에 <code>eat()</code> 메서드를 선언하고, 구현부를 정의했습니다. 이때 <code>eat()</code> 메서드 내부에서 추상 메서드인 <code>hunt()</code>를 호출하고 있습니다. </p>
<pre><code class="language-java">public abstract class Animal {

    public final void eat() {
        System.out.println(&quot;사냥을 해야 밥을 먹을 수 있어요.&quot;);
        hunt();
        System.out.println(&quot;잡아온 밥을 먹어요.&quot;);
    }

    protected abstract void hunt();
}</code></pre>
<p>이제 Animal 클래스를 상속 받은 고양이(<code>Cat</code>)와 사자(<code>Lion</code>) 클래스를 만들어보겠습니다. 각 클래스에서 <code>hunt()</code> 메서드를 구현합니다.</p>
<pre><code class="language-java">public class Cat extends Animal {
    @Override
    protected void hunt() {
        System.out.println(&quot;냐옹~ 쥐를 잡았어요.&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Lion extends Animal {
    @Override
    protected void hunt() {
        System.out.println(&quot;어흥! 토끼를 잡았어요.&quot;);
    }
}</code></pre>
<p>이제 두 가지 동물 객체를 만들고, <code>eat()</code> 메서드를 실행해보도록 하겠습니다.</p>
<pre><code class="language-java">Animal cat = new Cat();
Animal lion = new Lion();

cat.eat();
lion.eat();</code></pre>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/2b37e720-35e5-4d80-9742-ed062dd28750/image.png" alt=""></p>
<p>고양이와 사자가 각각 다른 방법으로 사냥을 해서 밥을 먹습니다. 주목할 점은, 외부에서 <code>hunt()</code>를 따로 호출하지 않았지만, 자식 클래스에서 재정의한 <code>hunt()</code> 메서드도 함께 호출되었다는 점입니다.</p>
<p><img src="https://velog.velcdn.com/images/carol0724/post/8bf3a0f9-c595-48f1-bd21-3261ef94e9c9/image.gif" alt=""></p>
<br>

<h3 id="⺁-추상-클래스는-고수준-컴포넌트-구체-클래스는-저수준-컴포넌트">⺁ 추상 클래스는 고수준 컴포넌트, 구체 클래스는 저수준 컴포넌트</h3>
<p>Animal은 추상 클래스, Cat과 Lion은 Animal을 상속받은 구체 클래스입니다. 이때 <strong>추상 클래스</strong>인 Animal은 <strong>상위</strong>(고수준) 컴포넌트라 불리며, <strong>구체 클래스</strong>인 Cat과 Lion은 <strong>하위</strong>(저수준) 컴포넌트라 불립니다.</p>
<blockquote>
<p>추상 클래스 → 상위(고수준) 컴포넌트
구체 클래스 → 하위(저수준) 컴포넌트</p>
</blockquote>
<p><code>eat()</code> 메서드가 동작할 때, 각 구현체에서 정의한 <code>hunt()</code> 메서드가 호출됩니다. </p>
<p>구체 클래스에서 구현한 <code>hunt()</code> 메서드는 추상 클래스의 <code>eat()</code> 메서드가 호출할 때까지 수동적으로 기다립니다.</p>
<p>즉, <strong>추상 클래스</strong>(고수준)<strong>가 구체 클래스</strong>(저수준)<strong>를 호출하여 프로그램의 흐름을 제어</strong>하고, 반대로 구체 클래스는 추상 클래스에 있는 메서드를 호출하지 않습니다. 또한 구체 클래스의 <code>hunt()</code> 메서드는 본인이 언제 호출되는지를 모르며, 수동적인 태도로 호출되기를 기다립니다. </p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/0abb3f88-a876-41bf-a2f2-e787cfd8a9b8/image.png" alt=""></p>
<br>


<h3 id="📐-템플릿-메서드-패턴">📐 템플릿 메서드 패턴</h3>
<p>템플릿 메서드(Template Method) 패턴은 헐리우드 원칙을 준수하는 디자인 패턴입니다. </p>
<p>여러 클래스에서 <strong>공통</strong>으로 사용하는 메서드를 템플릿화하여 <strong>상위</strong> 클래스에서 정의하고, <strong>하위</strong> 클래스마다 <strong>세부 동작 사항을 다르게</strong> 구현합니다.</p>
<p>즉, <strong>변하지 않는 기능</strong>(템플릿)은 <strong>상위</strong> 클래스에 만들어두고 <strong>자주 변경되며 확장</strong>할 기능은 <strong>하위</strong> 클래스에서 만들도록 하여, <strong>세부 실행 내용을 다양화</strong>할 수 있습니다. </p>
<blockquote>
<p>💡 Tip
디자인 패턴에서의 <strong>템플릿</strong>은 <strong>변하지 않는 것</strong>을 의미한다.</p>
</blockquote>
<pre><code class="language-java">public abstract class Animal {

    public final void eat() {
        System.out.println(&quot;사냥을 해야 밥을 먹을 수 있어요.&quot;); // 1
        hunt(); // 2
        System.out.println(&quot;잡아온 밥을 먹어요.&quot;); // 3
    }
    protected abstract void hunt();
}</code></pre>
<p>Animal 추상 클래스의 eat() 메서드에서 1과 3의 내용은 모든 구현체에서 변하지 않는 내용입니다. 반면 2의 경우 하위 클래스에서 오버라이딩하여 구현체마다 변경될 수 있는 부분입니다. </p>
<p>이렇게 <strong>상위</strong> 클래스에서는 <strong>뼈대</strong>를 만들어두고, 템플릿 메서드에 포함된 메서드들의 <strong>구체적인 구현</strong>은 <strong>하위</strong> 클래스가 담당하게 하는 것을 <strong>템플릿 메서드 패턴</strong>이라고 합니다. </p>
<br>

<h3 id="🏝️-템플릿-메서드-패턴과-추상-클래스">🏝️ 템플릿 메서드 패턴과 추상 클래스</h3>
<pre><code class="language-java">public abstract class Animal {

    public final void eat() {
        System.out.println(&quot;사냥을 해야 밥을 먹을 수 있어요.&quot;);
        hunt();
        System.out.println(&quot;잡아온 밥을 먹어요.&quot;);
    }

    protected abstract void hunt();
}</code></pre>
<p>Animal 추상 클래스를 다시 살펴보면 두 가지 특징이 있습니다.</p>
<blockquote>
<p>1️⃣ 템플릿 메서드(<code>eat()</code>)의 <strong>final</strong> 키워드</p>
</blockquote>
<p>템플릿 메서드 <code>eat()</code>은 <code>final</code>로 선언되어 있어 <strong>자식 클래스에서 오버라이드할 수 없습니다.</strong> </p>
<p>→ 🌟 추상 클래스 속 템플릿 메서드는 <code>final</code>로 선언되어 오버라이드되지 않아야 합니다.</p>
<blockquote>
<p>2️⃣ 추상 메서드(<code>hunt()</code>)의 <strong>protected</strong> 접근 제어자</p>
</blockquote>
<p>추상 메서드 <code>hunt()</code>의 접근제어자를 <code>protected</code>로 선언함으로써 <strong>캡슐화를 보장</strong>하고, <strong>템플릿 메서드 패턴의 의도</strong>를 지킬 수 있습니다. </p>
<p>아래 세 가지의 목적을 달성하기 위해 protected 추상 메서드가 필요합니다 ❗️</p>
<br>

<p>💊 <strong>캡슐화</strong></p>
<p><code>hunt()</code>는 <code>eat()</code>의 내부적인 동작이므로 외부에서 <code>hunt()</code>를 직접 호출할 일이 없습니다. 만약 public으로 선언 시 불필요하게 구현 세부사항이 노출됩니다. </p>
<p>🎥 <strong>헐리우드 원칙 준수</strong></p>
<p>고수준인 추상 클래스 속 <code>eat()</code> 메서드가 저수준인 구체 클래스 속 <code>hunt()</code>가 언제 호출될지를 결정합니다. </p>
<p>만약 public으로 선언 시 바깥에서 <code>hunt()</code>를 직접 호출할 수 있게 됩니다. 그럼 &quot;먼저 연락하지 마세요. 저희가 연락 드리겠습니다&quot;는 의미를 갖는 헐리우드 원칙을 깨뜨리게 됩니다.</p>
<p>✍️ <strong>명시적으로 템플릿 메서드 패턴의 의도를 드러냄</strong></p>
<p><code>hunt()</code> 추상 메서드를 <code>protected</code> 접근 제어자로 선언함으로써, <code>hunt()</code>는 외부에서 호출 가능한 public API가 아니라, 하위 클래스에서 구현만 하면 된다는 점을 명시합니다.</p>
<p><img src="https://velog.velcdn.com/images/carol0724/post/de44bb8e-d3b8-4ba0-9e74-976aa938de0c/image.gif" alt=""></p>
<p>Animal 추상 클래스의 이 두 가지 특징은 인터페이스에서는 볼 수 없는 특징입니다!</p>
<p>*<em>인터페이스는 메서드 앞에 <code>final</code> 키워드를 붙일 수 없으며, <code>protected</code> 접근 제어자도 사용할 수 없습니다. *</em></p>
<br>

<h3 id="🆚-템플릿-메서드-패턴과-인터페이스-추상-클래스">🆚 템플릿 메서드 패턴과 인터페이스, 추상 클래스</h3>
<p>Java8 이후 인터페이스도 구현 메서드를 제공할 수 있게 되면서, 위와 비슷하게 코드를 작성할 수 있게 되었습니다. default 메서드 속에서 추상 메서드를 호출하고, 추상 메서드는 각 구현체에서 정의합니다.</p>
<pre><code class="language-java">public interface Animal {
    default void eat() {
        System.out.println(&quot;사냥을 해야 밥을 먹을 수 있어요.&quot;);
        hunt();
        System.out.println(&quot;잡아온 밥을 먹어요.&quot;);
    }

    abstract void hunt();
}</code></pre>
<p>그러나, <code>default</code> 메서드의 경우 <strong>하위 클래스에서 재정의가 가능합니다.</strong> (🚨→ 캡슐화 위반) 또한, <code>hunt()</code> 추상 메서드의 접근 제어자는 <code>public</code>만 사용할 수 있기 때문에 <strong>외부에서의 호출을 막을 수 없습니다.</strong> (🚨→ 헐리우드 원칙 위반)</p>
<p><img src="https://velog.velcdn.com/images/carol0724/post/a493f02c-2832-4416-91f9-20cad4b8059b/image.gif" alt=""></p>
<p>이러한 이유로, 보통 <strong>템플릿 메서드 패턴은 추상 클래스를 이용해 구현합니다.</strong> 인터페이스로 구현할 수 있는 대표적인 디자인 패턴으로는 전략 패턴이 있습니다.</p>
<br>

<h3 id="참고-헐리우드-원칙과-템플릿-메서드-패턴-그리고-spring-프레임워크">참고) 헐리우드 원칙과 템플릿 메서드 패턴, 그리고 Spring 프레임워크</h3>
<p>헐리우드 원칙의 핵심은 &quot;자꾸 연락하지마 내가 연락할게&quot;입니다. 이는 프레임워크에서 자주 사용되는 개념입니다.</p>
<p>스프링 프레임워크와 개발자가 작성하는 애플리케이션을 생각해보면, 개발자는 프레임워크의 동작에 대해 아무것도 알지 못하고 참견하지도 못합니다. 코드를 작성하기만 하면, 스프링 프레임워크는 개발자가 작성한 코드를 호출합니다.</p>
<p>웹에서 <code>/velog</code>로 HTTP 요청이 들어오는 경우를 생각해보겠습니다. 개발자는 컨트롤러에 <code>@GetMapping(/velog)</code> 어노테이션이 붙은 메서드를 작성합니다. 다만, 개발자는 이 메서드가 언제 어디서 호출되는지 알지 않습니다. 그 결정은 고수준 컴포넌트인 스프링이 알아서 해주고 있습니다.</p>
<p>모든 웹 요청의 진입점 역할을 하는 <code>DispatcherServlet</code> 클래스를 살펴보겠습니다. <code>DispatcherServlet</code>은 <code>FrameworkServlet</code> 추상 클래스를 상속 받고 있습니다. <code>processRequest()</code>라는 메서드는 HTTP 요청을 처리하는 역할을 수행하며, 템플릿 메서드 패턴으로 구현되어 있습니다.</p>
<pre><code class="language-java">public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {

    protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            this.doService(request, response);
        } /* 생략 */
    }    

    protected abstract void doService(HttpServletRequest request, HttpServletResponse response) throws Exception;
    /* 생략 */    
}</code></pre>
<p><code>FrameworkServlet</code>의 <code>processRequest()</code>라는 메서드는 템플릿 메서드입니다. 변하지 않는다는 의미를 final 키워드를 통해 확인할 수 있습니다. <code>processRequest()</code>는 웹에서 요청이 들어왔을 때 처리해야 하는 작업들의 뼈대를 정의하고 있으며, 추상 메서드인 <code>doService()</code>를 내부에서 호출하여 세부적인 내용은 구현체가 만들도록 합니다.</p>
<p><code>doService()</code>는 하위 클래스인 <code>DispatcherServlet</code>에서 아래와 같이 정의하고 있습니다.</p>
<pre><code class="language-java">public class DispatcherServlet extends FrameworkServlet {
    protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    /* 생략 */
    }
}</code></pre>
<blockquote>
<p><strong>🌟 요약 🌟</strong>
(1) 추상 클래스는 public이 아닌 추상 메서드를 가질 수 있다. (인터페이스는 only <code>public</code>)
(2) 헐리우드 원칙을 준수한 설계를 하기 위해 protected 추상 메서드가 필요하다.
(3) 헐리우드 원칙: &quot;자꾸 연락하지마 내가 연락할게&quot;
(4) 템플릿 메서드 패턴은 헐리우드 원칙을 준수하는 대표적인 디자인 패턴이며, 프레임워크에서 주로 사용된다.
(5) 인터페이스는 <code>protected</code> 접근 제어자 및 <code>final</code> 키워드를 사용할 수 없다. 템플릿 메서드 패턴을 구현 시 이 두 가지가 필요하다.</p>
</blockquote>
<br>

<h2 id="🎐-그-밖의-차이점-총정리">🎐 그 밖의 차이점 총정리</h2>
<br>

<table>
<thead>
<tr>
<th>종류</th>
<th>추상클래스</th>
<th>인터페이스</th>
</tr>
</thead>
<tbody><tr>
<td>사용 키워드</td>
<td>abstract</td>
<td>interface</td>
</tr>
<tr>
<td>사용 가능 변수</td>
<td>제한 없음</td>
<td>public static final (상수)</td>
</tr>
<tr>
<td>추상 메서드 접근 제어자</td>
<td>제한 없음 (public, private, protected, default)</td>
<td>public</td>
</tr>
<tr>
<td>구현 메서드 접근 제어자</td>
<td>제한 없음 (public, private, protected, default)</td>
<td>public, private (default 메서드는 public으로만 선언 가능)</td>
</tr>
<tr>
<td>사용 가능 메소드</td>
<td>제한 없음</td>
<td>abstract method, default method, static method, private method</td>
</tr>
<tr>
<td>상속 키워드</td>
<td>extends</td>
<td>implements</td>
</tr>
<tr>
<td>다중 상속 가능 여부</td>
<td>불가능</td>
<td>가능 (클래스에 다중 구현, 인터페이스끼리 다중 상속)</td>
</tr>
</tbody></table>
<p>기억해볼 만한 내용은, <strong>인터페이스</strong>에는 오직 <strong>상수만</strong> 정의할 수 있지만 추상 클래스에는 모든 종류의 변수를 선언할 수 있다는 것입니다. </p>
<p>또한, <strong>다중 상속</strong>이 필요한 경우 <strong>인터페이스</strong>를 고려해야 합니다.</p>
<br>


<h2 id="🐋-마치며">🐋 마치며</h2>
<p>인터페이스와 추상 클래스를 정리하며, 그 둘의 차이점을 알아봤습니다. 인터페이스와 추상 클래스는 객체 지향 프로그래밍에서 추상화를 달성하기 위해 Java에서 제공하는 주요 메커니즘입니다. 이 두 가지를 적절히 활용하면 코드의 유연성과 재사용성을 높일 수 있습니다. 템플릿 메서드 패턴에서 protected 추상 메서드가 필요하다는 점을 통해 인터페이스와 추상 클래스의 차이점을 설명해보았습니다. 긴 글 읽어주셔서 감사합니다!</p>
<p><img src="https://velog.velcdn.com/images/carol0724/post/1672538e-4163-41b8-84e8-0480d43b8bed/image.gif" alt=""></p>
<h2 id="참고자료">참고자료</h2>
<p><a href="https://stackoverflow.com/questions/2430756/why-are-interface-variables-static-and-final-by-default">Why are Interface Variables Static and Final - StackOverflow</a>
<a href="https://www.baeldung.com/java-static-final-variables">Static Final Variables - Baeldung</a>
<a href="https://www.baeldung.com/jvm-static-storage">Jvm Static Storage - Baeldung</a>
<a href="https://www.baeldung.com/java-static-default-methods">Static and Default Methods in Interfaces - Baeldung</a>
<a href="https://medium.com/@sandy619g/hollywood-principle-in-software-engineering-5d68f679b524">Hollywood principal - Medium</a>
<a href="https://refactoring.guru/design-patterns/template-method">Template Method - Refactoring.Guru</a>
<a href="https://docs.oracle.com/javase/tutorial/java/IandI/abstract.html">Abstract Methods and Classes - Oracle</a>
<a href="https://dzone.com/articles/design-patterns-template-method">Template Method Pattern - dzone</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[헷갈리는 자바 문자열 구석구석 이해하기]]></title>
            <link>https://velog.io/@carol_ly/%ED%97%B7%EA%B0%88%EB%A6%AC%EB%8A%94-%EC%9E%90%EB%B0%94-%EB%AC%B8%EC%9E%90%EC%97%B4-%EA%B5%AC%EC%84%9D%EA%B5%AC%EC%84%9D-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@carol_ly/%ED%97%B7%EA%B0%88%EB%A6%AC%EB%8A%94-%EC%9E%90%EB%B0%94-%EB%AC%B8%EC%9E%90%EC%97%B4-%EA%B5%AC%EC%84%9D%EA%B5%AC%EC%84%9D-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 20 Jan 2025 13:35:47 GMT</pubDate>
            <description><![CDATA[<h2 id="🐣-시작하며">🐣 시작하며</h2>
<p>안녕하세요! 오늘은 자바 문자열을 주제로 포스팅해보려고 합니다. 먼저 아래 문제들을 살펴보도록 하겠습니다. ✈️🌝</p>
<h3 id="문제-1">문제 1</h3>
<pre><code class="language-java">String str1 = &quot;abc&quot;;
String str2 = &quot;abc&quot;;

// 출력 결과는? 
System.out.println(str1==str2);
System.out.println(str1.equals(str2));
System.out.println(str1.hashCode()==str2.hashCode());</code></pre>
<h3 id="문제-2">문제 2</h3>
<pre><code class="language-java">String str3 = new String(&quot;abc&quot;);
String str4 = new String(&quot;abc&quot;);

// 출력 결과는?
System.out.println(str3==str4);
System.out.println(str3.equals(str4));
System.out.println(str3.hashCode()==str4.hashCode());</code></pre>
<p>정답은, 차례대로 </p>
<pre><code class="language-Plain">## 문제 1
true
true
true

## 문제 2
false
true
true</code></pre>
<p>입니다.</p>
<br>

<h2 id="🐰-자바에서-문자열을-만드는-방법">🐰 자바에서 문자열을 만드는 방법</h2>
<p>자바에서는 문자열을 크게 두 가지 방식으로 만들 수 있습니다. </p>
<h3 id="1️⃣-string-literal">1️⃣ String Literal</h3>
<pre><code class="language-java">String usingLiteral = &quot;velog&quot;;</code></pre>
<p>두 개의 큰따옴표(&quot; &quot;)로 묶인 텍스트를 String Literal이라고 합니다. </p>
<h3 id="2️⃣-new-연산자">2️⃣ new 연산자</h3>
<pre><code class="language-java">String usingNew = new String(&quot;velog&quot;);</code></pre>
<p>new 연산자를 사용해 새로운 String 객체를 생성하는 방법입니다.</p>
<br>

<h2 id="🏊-string-literal-방식과-java-string-pool">🏊 String Literal 방식과 Java String Pool</h2>
<p>먼저, String Literal 방식부터 살펴보겠습니다. </p>
<p>자바의 힙 메모리 영역에는 <strong>Java String Pool</strong>(또는 String Constant Pool)이라는 공간이 존재합니다. JVM이 문자열을 효율적으로 관리하기 위해 사용하는 장소인데요!</p>
<p>JVM은 String 변수를 선언하고 String Literal 방식으로 값을 할당할 때, <strong>동일한 값을 가진 문자열이 이미 스트링 풀에 존재</strong>하는지 탐색합니다. </p>
<pre><code class="language-java">String str1 = &quot;abc&quot;;
String str2 = &quot;abc&quot;;</code></pre>
<p>만약 스트링 풀에 &quot;abc&quot;라는 문자열이 이미 존재한다면, 추가적인 메모리 할당 없이 기존 문자열에 대한 참조값을 반환합니다. </p>
<p>반대로 &quot;abc&quot;라는 문자열이 스트링 풀에 없다면, 이를 새로 추가한 뒤 참조값을 반환합니다.</p>
<p>위의 예시에서 str2에 값을 할당할 때에는 스트링 풀에 이미 &quot;abc&quot;가 존재합니다. 따라서 str2는 str1과 <strong>스트링 풀 속 동일한 공간을 가리키게 됩니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/009f276d-4895-41b2-b9a3-78c6937d60eb/image.png" alt=""></p>
<pre><code class="language-java">System.out.println(str1==str2); // true
System.out.println(str1.equals(str2)); // true
System.out.println(str1.hashCode()==str2.hashCode()); // true
}</code></pre>
<ol>
<li><code>str1==str2</code></li>
</ol>
<p>== 연산자의 경우 두 개의 인스턴스가 <strong>메모리 상에서 같은 공간을 참조</strong>하고 있는지를 비교합니다. 위의 그림을 통해 확인했듯이, <strong>str1과 str2는 스트링 풀 속 동일한 공간을 가리키고 있습니다.</strong> 따라서 true가 출력됩니다.</p>
<ol start="2">
<li><code>str1.equals(str2)</code></li>
</ol>
<p>String은 equals() 메서드를 재정의해 <strong>동등 비교</strong>가 가능합니다. str1과 str2는 모두 &quot;abc&quot;라는 <strong>논리적으로 동일한 문자열</strong>을 지칭합니다. 즉, <strong>str1과 str2는 동등</strong>하다고 판단되기에 true가 출력됩니다.</p>
<ol start="3">
<li><code>str1.hashCode()==str2.hashCode()</code></li>
</ol>
<p>String은 equals()와 함께 hashCode()도 재정의하고 있습니다. <strong>논리적으로 동일한 인스턴스는 같은 해시 코드를 반환</strong>합니다. 따라서 true가 출력됩니다.</p>
<h3 id="참고-string-interning">참고) String Interning</h3>
<p>스트링 풀에 이미 문자열이 존재하는지 확인하고, 재사용하는 작업을 <strong>String Interning</strong>이라고 합니다. 이를 통해 <strong>논리적으로 내용이 같은 문자열은 동일한 메모리 공간에서 공유</strong>될 수 있도록 합니다. </p>
<p>String Literal 방식을 사용하면, 내부적으로 Interning을 수행합니다. 이밖에 String.intern() 메서드를 직접 사용할 수도 있습니다.</p>
<pre><code class="language-java">String str1 = new String(&quot;abc&quot;);
String str2 = str1.intern();
System.out.println(str1.equals(str2)); // true
System.out.println(str1.hashCode()==str2.hashCode()); // true</code></pre>
<br>

<h2 id="🆕-new-연산자-방식">🆕 new 연산자 방식</h2>
<p>new String() 방식은 <strong>언제나 새로운 String 객체를 생성</strong>합니다.</p>
<p>new 연산자를 사용해 String 객체를 생성 시, 자바 컴파일러는 <strong>새로운 객체를 생성해 힙 메모리에 저장</strong>합니다. 이렇게 생성되는 객체는 항상 서로 다른 메모리 영역에 위치하게 됩니다.</p>
<pre><code class="language-java">String str1 = new String(&quot;abc&quot;);
String str2 = new String(&quot;abc&quot;);</code></pre>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/0a908856-3672-4c22-89ba-8847fc1e1d18/image.png" alt=""></p>
<p><strong>객체의 주소값을 비교하는 == 연산</strong>을 통해 str1과 str2의 메모리 주소가 같은지, 다른지 확인해보도록 하겠습니다!</p>
<pre><code class="language-java">System.out.println(str3 == str4); // false</code></pre>
<p>false가 출력되었고, 같은 값을 갖더라도 new 연산자로 String 객체를 만들면 다른 메모리 주소를 참조한다는 것을 확인할 수 있습니다.</p>
<br>

<h2 id="🙅-string은-불변-객체">🙅 String은 불변 객체</h2>
<p><a href="https://docs.oracle.com/javase/8/docs/api/java/lang/String.html">Oracle 공식 문서</a>에 따르면,</p>
<blockquote>
<p>Strings are constant; their values cannot be changed after they are created. </p>
</blockquote>
<p>String 객체는 <strong>불변</strong>합니다.</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/13334444-3456-404e-872b-27edbda110c1/image.png" alt=""></p>
<p>String 클래스는 <strong>final로 선언</strong>되어 있으며, value 및 coder, hash 필드 등으로 구성되어 있습니다.</p>
<p>그 중 value 필드는 byte[] 타입의 배열로, 문자열의 실제 문자 데이터를 저장합니다. 이 필드는 <strong>final로 선언</strong>되어 있기 때문에, String *<em>객체가 한 번 생성된 이후에는 변경할 수 없습니다. *</em></p>
<p>객체의 <strong>실제 메모리 위치</strong>(참조)에 따라 해시코드를 반환하는 <code>System.identityHashCode</code>를 사용해 <strong>String의 불변성</strong>을 직접 확인해보도록 하겠습니다.</p>
<pre><code class="language-java">String hello = &quot;hello&quot;;
System.out.println(System.identityHashCode(hello)); //2060468723
hello = hello + &quot; velog&quot;;
System.out.println(System.identityHashCode(hello)); //1933863327</code></pre>
<ol>
<li><code>String hello = &quot;hello&quot;;</code></li>
</ol>
<p>&quot;hello&quot;라는 문자열 리터럴은 <strong>스트링 풀</strong>에 저장됩니다. hello는 문자열 풀에 있는 &quot;hello&quot; 객체를 참조합니다. </p>
<ol start="2">
<li><code>&quot; velog&quot;</code></li>
</ol>
<p>&quot; velog&quot;도 문자열 리터럴이므로 <strong>스트링 풀</strong>에 저장됩니다. </p>
<ol start="3">
<li><code>hello + &quot; velog&quot;</code></li>
</ol>
<p>문자열 연결 연산자(+)를 통해 새로운 문자열 <strong>객체가 힙 메모리에 생성</strong>됩니다. 이때는 new 연산자를 사용한 것과 같이 스트링 풀에 저장되지 않습니다.</p>
<p>따라서 위의 코드는 아래와 같이 세 개의 문자열 객체를 생성합니다. 즉, 기존 문자열이 변경되는 것이 아니라 <strong>새로운 객체를 만들어 참조</strong>하게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/095fb449-57e9-482e-9817-69adf45b6f7f/image.png" alt=""></p>
<br>

<h2 id="🟰-equals-and-hashcode">🟰 Equals And HashCode</h2>
<p>기본적으로, Object 클래스는 equals() 및 hashCode() 메서드를 정의하고 있습니다. 따라서 자바의 모든 클래스는 이 두 메서드를 사용할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/c45346c0-2f7a-4c56-b979-2a2cf53a4419/image.png" alt=""></p>
<p>Object 클래스가 디폴트로 정의한 equals() 메서드는 객체의 <strong>동일성</strong>을 비교합니다. 즉, <strong>같은 메모리 공간을 참조</strong>하고 있는지를 확인합니다. 이를 <strong>물리적 동일성</strong>(인스턴스의 메모리 주소가 같음)이라고 합니다.</p>
<p>그런데 서로 다른 주소 값을 가지더라도, 내용물이 같아 같은 인스턴스라고 정의하고 싶은 경우가 있습니다. 이를 <strong>논리적 동일성</strong>(논리적으로 두 인스턴스가 같음)이라고 하며, <strong>동등성</strong>이라고도 합니다.</p>
<p>만약 equals() 메서드를 통해 객체의 <strong>동등성</strong>을 비교하고 싶다면, 해당 메서드를 오버라이드 해주어야 합니다. (<a href="https://stackoverflow.com/questions/1692863/what-is-the-difference-between-identity-and-equality-in-oop">동등성 vs 동일성</a>)</p>
<p>String 클래스는 아래처럼 equals() 메서드를 재정의하여 <strong>동등성 비교</strong>가 가능합니다.</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/71f871b3-adaf-4816-9420-da14de680f24/image.png" alt=""></p>
<p>먼저, 비교하려는 객체가 자기 자신과 동일하다면 true를 반환합니다. 두 번째로, 객체가 String 타입인지 확인해 아니라면 false를 반환합니다. 이후 두 문자열의 실제 문자 내용을 비교하여 내용이 같다면 true를 반환합니다.</p>
<p>String 타입의 객체를 동등 비교해보도록 하겠습니다. new 연산자를 사용해 같은 값을 갖는 두 개의 객체를 만들었습니다. </p>
<pre><code class="language-java">String str3 = new String(&quot;abc&quot;);
String str4 = new String(&quot;abc&quot;);

System.out.println(str3.equals(str4)); // true
System.out.println(str3.hashCode()==str4.hashCode()); // true</code></pre>
<p>str3과 str4는 서로 다른 메모리 공간을 참조하고 있지만, 문자열 내용이 같기 때문에 equals() 비교 결과 동등하다고 판단합니다. 즉, <strong>값으로 비교가 가능</strong>해집니다.</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/fcde884c-361b-4778-9a00-aab89cfa5f65/image.png" alt=""></p>
<p>자바에서 <strong>equals()를 재정의하면, hashCode()도 함께 재정의</strong>해주는 것이 관례입니다. 논리적으로 동일한 두 인스턴스는 <strong>같은 해시 코드 값을 반환</strong>하도록 해야 합니다. 또한 HashMap, HashSet과 같은 <strong>Hash 기반의 자료 구조</strong>를 사용할 때 hashCode()를 재정의하지 않으면 문제가 발생할 수 있습니다. 따라서 동등성 비교를 위해서는 equals()를 재정의해야 하며, 이때 hashCode()도 함께 재정의하는 것이 권장됩니다.</p>
<br>

<h2 id="🏡-stringbuilder와--연산자의-차이점은">🏡 StringBuilder와 + 연산자의 차이점은?</h2>
<p>StringBuilder 클래스는 내부에 <strong>변경 가능</strong>한 byte <strong>배열</strong>을 가집니다. StringBuilder 클래스는 AbstractStringBuilder 추상클래스를 상속받습니다.</p>
<p>이때 AbstractStringBuilder 추상 클래스는 String 클래스와 달리 byte <strong>배열을 final로 선언하지 않습니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/4053963b-7a49-4970-b0ef-cd07137bd9b6/image.png" alt=""></p>
<p>따라서 StringBuilder 클래스는 String 클래스와 달리 <strong>변경이 가능한 데이터 구조</strong>를 가집니다. 문자열을 추가 및 삭제 또는 삽입 시 <strong>새로운 객체를 만들지 않고</strong> 기존 객체를 활용합니다. 따라서 문자열에 대한 작업을 훨씬 효율적으로 처리합니다.</p>
<p>문자열을 두 가지 방법으로 합쳐보며 테스트를 진행해보도록 하겠습니다. </p>
<p>먼저, StringBuilder를 활용한 예제입니다.</p>
<pre><code class="language-java">StringBuilder sb = new StringBuilder(&quot;hello&quot;);
System.out.println(System.identityHashCode(sb));
sb.append(&quot; velog&quot;);
System.out.println(System.identityHashCode(sb));</code></pre>
<p>append 연산 전과 후의 StringBuilder 객체는 같은 메모리 공간을 참조하고 있는 것을 확인할 수 있습니다. 즉, <strong>새로운 StringBuilder 객체를 생성하지 않고 내부의 값을 변경했습니다.</strong></p>
<p><strong>실행 결과</strong>
<img src="https://velog.velcdn.com/images/carol_ly/post/5df1b91d-1341-4ce3-b503-049d32a91c9b/image.png" alt=""></p>
<p>이와 반면, + 연산자를 사용해 두 개의 문자열을 합치게 되면 <strong>새로운 String 객체가 생성됩니다.</strong> 연산 전과 후 참조하고 있는 메모리 주소가 다른 것을 확인할 수 있습니다.</p>
<pre><code class="language-java">String str = new String(&quot;hello&quot;);
System.out.println(System.identityHashCode(str));
str += &quot; velog&quot;;
System.out.println(System.identityHashCode(str));</code></pre>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/4f7741e9-3cef-45e0-beea-5ac18ea22c1d/image.png" alt=""></p>
<p>문자열에 대한 연산을 할 때마다 <strong>새로운 객체를 생성</strong>하게 되면 <strong>커다란 GC 오버헤드 문제</strong>가 발생할 수 있습니다. 이럴 때는 StringBuilder를 사용하는 것을 고려해볼 수 있습니다.</p>
<h2 id="🍊-마치며">🍊 마치며</h2>
<p>자바 공부를 다시 하면서 놓치고 있었던 String과 관련한 개념들을 정리해봤습니다. 자바에서 제공해주는 클래스들을 사용할 때 한 번씩 타고 들어가 살펴보는 습관을 기르려고 합니다 🐰🥕</p>
<h2 id="참고-자료">참고 자료</h2>
<p><a href="https://docs.oracle.com/javase/8/docs/api/java/lang/String.html">String - Oracle</a>
<a href="https://www.baeldung.com/java-string-pool">Java String Pool - Baeldung</a>
<a href="https://www.geeksforgeeks.org/interning-of-string/">Interning of String in Java - GeeksForGeeks</a>
<a href="https://stackoverflow.com/questions/10578984/what-is-java-string-interning">String Interning - StackOverFlow</a>
<a href="https://www.geeksforgeeks.org/java-string-is-immutable-what-exactly-is-the-meaning/">String is Immutable - GeeksForGeeks</a>
<a href="https://www.baeldung.com/java-equals-hashcode-contracts">Equals and HashCode - Baeldung</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[객체를 생성하는 다양한 방법 (feat. 생성자 vs builder)]]></title>
            <link>https://velog.io/@carol_ly/%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EC%83%9D%EC%84%B1%ED%95%98%EB%8A%94-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%B0%A9%EB%B2%95-feat.-%EC%83%9D%EC%84%B1%EC%9E%90-vs-builder</link>
            <guid>https://velog.io/@carol_ly/%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EC%83%9D%EC%84%B1%ED%95%98%EB%8A%94-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%B0%A9%EB%B2%95-feat.-%EC%83%9D%EC%84%B1%EC%9E%90-vs-builder</guid>
            <pubDate>Wed, 01 Jan 2025 18:09:37 GMT</pubDate>
            <description><![CDATA[<h2 id="🐥-시작하며">🐥 시작하며</h2>
<p>프로젝트를 진행하며 아래처럼 생긴 클래스를 자주 만들었던 기억이 있습니다.</p>
<pre><code class="language-java">@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Member {
    private Long id;
    /* 
    * 생략 
    */</code></pre>
<p>@AllArgsConstructor, @RequiredArgsConstructor, @NoArgsConstructor, @Builder, . . 등등 생성자와 관련된 어노테이션을 별다른 고민 없이 사용했었던 것 같습니다.</p>
<p>또 <strong>builder와 생성자 방식</strong>을 비교하는 많은 글을 통해 builder가 대세라는 것도 알 수 있었습니다.</p>
<p>이밖에 <strong>정적 팩토리 메서드</strong>도 객체를 생성하는 유용한 방법입니다.</p>
<p>이번 글에서는 다양한 방법으로 객체를 만들어보려고 합니다!</p>
<h2 id="1️⃣-생성자">1️⃣ 생성자</h2>
<p>먼저, 생성자를 통해서 회원 객체를 만들어 보겠습니다. </p>
<p>회원은 이름, 이메일, 휴대폰번호, 나이를 필드로 가지고 있습니다. 아래 코드에는 모든 필드를 파라미터로 갖는 생성자가 있습니다.</p>
<pre><code class="language-java">public class Member {
    private String name;
    private String email;
    private String phoneNumber;
    private int age;


    public Member(String name, String email, String phoneNumber, int age) {
        this.name = name;
        this.email = email;
        this.phoneNumber = phoneNumber;
        this.age = age;
    }
}</code></pre>
<p>new 키워드를 사용해 회원 객체를 만들어보겠습니다.</p>
<pre><code class="language-java">Member member = new Member(&quot;소은&quot;, &quot;soeun1234@vmail.com&quot;, &quot;010-1212-3434&quot;, 100);</code></pre>
<p>회원 객체가 만들어졌습니다 !</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/d61b320e-1642-4500-9513-1c74eaad0a47/image.webp" alt=""></p>
<h3 id="lombok-allargsconstructor">Lombok @AllArgsConstructor</h3>
<p><a href="https://projectlombok.org/api/lombok/AllArgsConstructor">lombok의 @AllArgsConstructor 어노테이션</a>을 사용해보도록 하겠습니다.</p>
<p>아래와 같이 lombok 의존성을 추가합니다.</p>
<ul>
<li>build.gradle<pre><code class="language-java">compileOnly &#39;org.projectlombok:lombok:1.18.36&#39;
annotationProcessor &#39;org.projectlombok:lombok:1.18.36&#39;</code></pre>
</li>
</ul>
<p>회원 클래스에서 직접 구현한 생성자를 제거하고, <code>@AllArgsConstructor</code>를 붙입니다.</p>
<pre><code class="language-java">@AllArgsConstructor
public class Member {
    private Long id;
    private String name;
    private String email;
    private String phoneNumber;
    private int age;
}</code></pre>
<p><code>@AllArgsConstructor</code> 어노테이션은 앞서 직접 구현했던 것과 같이 <strong>모든 필드를 파라미터로 갖는 생성자</strong>를 만듭니다.</p>
<br>


<h2 id="🤔-생성자-방식의-단점">🤔 생성자 방식의 단점</h2>
<h3 id="타입-안전성">타입 안전성</h3>
<pre><code class="language-java">Member wrongMember = new Member(&quot;소은&quot;, &quot;010-1212-3434&quot;,&quot;soeun1234@vmail.com&quot;, 100);</code></pre>
<p>실수로 파라미터의 순서를 바꿔 잘못된 회원 객체를 만들었다고 가정해보겠습니다. 이메일과 휴대폰 번호가 잘못 전달되었고, 이는 유효하지 않은 회원 객체입니다. </p>
<p>그러나, 두 가지 모두 <code>String</code> 타입이기 때문에 파라미터의 순서가 잘못되어도 컴파일러는 이를 인식하지 못합니다. </p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/824a72b6-b615-4197-bf14-87111536faea/image.png" alt=""></p>
<p>이렇게 잘못 생성된 회원 객체는 런타임에 예기치 못한 오류를 발생시킬 수 있습니다.</p>
<h3 id="너무-많은-파라미터로-인해-가독성-저하">너무 많은 파라미터로 인해 가독성 저하</h3>
<p>파라미터의 개수가 이보다 많아지게 된다면, 가독성이 떨어질 수 있습니다. 또한 개발자의 실수가 발생할 가능성이 높아집니다.</p>
<br>


<h2 id="2️⃣-빌더-패턴">2️⃣ 빌더 패턴</h2>
<p>위의 두 가지 문제점에 대한 해결책으로 <strong>빌더 패턴</strong>이 흔히 제시됩니다.</p>
<p><a href="https://www.baeldung.com/java-builder-pattern">Builder Pattern in Java - Baeldung</a>에 따르면 빌더 패턴의 장점은 다음과 같습니다.</p>
<ul>
<li><p><strong>유연성</strong>: 실제 객체의 표현으로부터 생성 프로세스를 분리함으로써, 다양한 구성으로 객체를 생성할 수 있습니다. 여러 개의 생성자 또는 Setter 성격의 메서드를 둘 필요가 없어집니다. </p>
</li>
<li><p><strong>가독성</strong>: 복잡한 객체의 생성 과정을 한 눈에 이해할 수 있도록 돕습니다. </p>
</li>
<li><p><strong>불변성</strong>: 불변 객체를 생성해 스레드 안전성을 보장하고, 의도치 않은 수정을 방지합니다. </p>
</li>
</ul>
<p><strong>빌더 패턴</strong>을 사용하면 각 필드에 대해 <strong>명시적인 메서드</strong>를 호출하여 값을 설정할 수 있기 때문에, 앞서 살펴봤던 <strong>파라미터의 순서가 뒤바뀌는 실수를 방지</strong>할 수 있습니다.</p>
<p>특히 <strong>타입이 동일한</strong> 필드가 많을 때 장점이 드러난다고 느꼈습니다.</p>
<h3 id="lombook-builder">Lombook @Builder</h3>
<p>@Builder 어노테이션을 사용하면, Lombok이 빌더 클래스를 제공해줍니다. </p>
<pre><code class="language-java">@Builder
public class Member {
    private String name;
    private String email;
    private String phoneNumber;
    private int age;
}</code></pre>
<p>회원 클래스 위에 @Builder 어노테이션을 추가했습니다. 이제 아래와 같이 객체를 생성할 수 있습니다.</p>
<pre><code class="language-java">Member jiyeon = Member.builder()
            .name(&quot;지연&quot;)
            .email(&quot;soeun1234@vmail.com&quot;)
            .phoneNumber(&quot;010-2323-3434&quot;)
            .age(200)
            .build();</code></pre>
<p>이제 이메일과 휴대폰번호를 전달할 때 *<em>파라미터의 순서를 고려하지 않아도 됩니다. *</em></p>
<h3 id="참고-noargsconstructor와-builder를-함께-사용-시">참고) @NoArgsConstructor와 @Builder를 함께 사용 시</h3>
<p>@Builder 어노테이션은 클래스, 생성자, 메서드 위에 붙일 수 있습니다. </p>
<p><a href="https://projectlombok.org/features/Builder">lombok 공식문서</a>에 따르면, 클래스 레벨에 @Builder 어노테이션을 붙일 경우 Lombok이 자동으로 <strong>모든 필드를 파라미터로 받는 package-private 생성자</strong>를 만들어줍니다.</p>
<p>즉, 아래 두가지는 동일하게 동작합니다. </p>
<ul>
<li><p>클래스 레벨에 @Builder 추가</p>
<pre><code class="language-java">@Builder
public class Member {
  private String name;
  private String email;
  private String phoneNumber;
  private int age;
}</code></pre>
</li>
<li><p>이때 Lombok이 자동으로 추가해주는 생성자는 @AllArgsConstructor(access = AccessLevel.PACKAGE)를 붙이는 것과 동일</p>
</li>
</ul>
<pre><code class="language-java">@AllArgsConstructor(access = AccessLevel.PACKAGE)
@Builder
public class Member {
    private String name;
    private String email;
    private String phoneNumber;
    private int age;
}</code></pre>
<p>다만, @NoArgsConstructor와 @Builder를 함께 사용하는 경우에는 위와 같이 동작하지 않습니다. </p>
<pre><code class="language-java">@NoArgsConstructor
@Builder
public class Member {
    private String name;
    private String email;
    private String phoneNumber;
    private int age;
}</code></pre>
<p>@NoArgsConstructor는 파라미터가 없는 기본 생성자를 제공합니다. </p>
<pre><code class="language-java">public Member() {        
}</code></pre>
<p>Lombok의 @Builder를 클래스 레벨에 붙이게 되면, 전체 파라미터를 받는 생성자가 반드시 필요한데 이 기본 생성자와 충돌이 발생하는 것입니다.</p>
<p>그래서 이럴때는 아래와 같이 @AllArgsConstructor를 추가합니다.</p>
<pre><code class="language-java">@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Member {
    private String name;
    private String email;
    private String phoneNumber;
    private int age;
}</code></pre>
<p>위에 제시했던 lombok 어노테이션 <del>3종 세트</del>를 가진 클래스가 만들어졌습니다.  </p>
<h3 id="참고-builder를-클래스-레벨-vs-생성자-위에">참고) @Builder를 클래스 레벨 vs 생성자 위에</h3>
<p>위의 문제는 @Builder를 클래스 레벨이 아닌, 생성자 위에 붙여 해결할 수도 있습니다.</p>
<p>id 필드가 추가되었다고 가정해보겠습니다. id는 파라미터로 전달받는 것이 아닌 내부적으로 랜덤하게 생성되는 값입니다. </p>
<p>따라서 @AllArgsConstructor로 전체 파라미터를 받는 생성자를 추가하지 않고, 필요한 파라미터만 포함해 생성자를 만들고 그 위에 @Builder를 붙입니다. </p>
<pre><code class="language-java">@NoArgsConstructor
public class Member {
    private String id;
    private String name;
    private String email;
    private String phoneNumber;
    private int age;

    @Builder
    public Member(String name, String email, String phoneNumber, int age) {
        this.id = UUID.randomUUID().toString();
        this.name = name;
        this.email = email;
        this.phoneNumber = phoneNumber;
        this.age = age;
    }
}</code></pre>
<p>클래스 레벨에서 @Builder를 사용할 때와는 다르게 필요한 파라미터만 노출된다는 장점이 있습니다. </p>
<h2 id="🤔-builder의-단점">🤔 @Builder의 단점</h2>
<p>빌더 패턴의 장점으로 언급되는 &quot;<strong>유연성</strong>&quot;에는 아래와 같은 단점이 따릅니다.</p>
<h3 id="원자적-객체-생성">원자적 객체 생성</h3>
<p>@Builder를 사용하게 될 경우, <strong>완전한 객체생성의 원자성</strong>을 보장하기 어렵습니다. </p>
<p>회원 객체는 아래와 같이 유연하게 생성될 수 있습니다.</p>
<pre><code class="language-java">/* 휴대전화번호와 나이만 갖는 회원 */
Member member1 = Member.builder()
    .phoneNumber(&quot;010-1212-3434&quot;)
    .age(200)
    .build();

/* 휴대전화번호만 갖는 회원 */
Member member2 = Member.builder()
    .phoneNumber(&quot;010-2323-4545&quot;)
    .build();  

/* 이름과 나이만 갖는 회원 */    
Member member3 = Member.builder()
    .name(&quot;회원3&quot;)
    .age(200)
    .build();    </code></pre>
<p>이를 장점으로 보기도 하지만, <strong>필수적으로 포함해야 하는 파라미터를 빠뜨리는</strong> 치명적인 실수를 하게 될 가능성이 있습니다.</p>
<p>회원의 이름, 이메일, 전화번호, 나이가 필수 값이라고 가정해보겠습니다. 위와 같이 *<em>유효하지 않은 객체를 생성하더라도 컴파일 타임에 이를 체크하기 어렵습니다. *</em></p>
<p>반면, <strong>생성자 방식</strong>의 경우 <strong>꼭 포함해야 하는 파라미터가 빠졌을 때 컴파일 타임에 알아차릴 수 있습니다.</strong></p>
<h3 id="참고-커스텀-빌더-구현으로-해결">참고) 커스텀 빌더 구현으로 해결!?</h3>
<blockquote>
<p>롬복이 만들어주는 빌더말고 직접 빌더를 만들어 사용하더라도 런타임 에러를 낼 수 있을뿐 컴파일 타임에 체크하기는 어렵다.
<a href="https://multifrontgarden.tistory.com/290">출처</a></p>
</blockquote>
<p>Lombok이 제공하는 @Builder를 사용하지 않고 직접 빌더 패턴을 구현해보겠습니다.</p>
<pre><code class="language-java">public class Member {
    private String name;
    private String email;
    private String phoneNumber;
    private int age;

    public Member(String name, String email, String phoneNumber, int age) {
        this.name = name;
        this.email = email;
        this.phoneNumber = phoneNumber;
        this.age = age;
    }

    public static class Builder {
        private String name;
        private String email;
        private String phoneNumber;
        private int age;

        public Builder() {

        }

        public Builder(Member member) {
            this.name = member.name;
            this.email = member.email;
            this.phoneNumber = member.phoneNumber;
            this.age = member.age;
        }

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder email(String email) {
            this.email = email;
            return this;
        }

        public Builder phoneNumber(String phoneNumber) {
            this.phoneNumber = phoneNumber;
            return this;
        }

        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public Member build() {
        /* int 타입 age 필드의 경우 초기화되었는지 확인이 어렵*/
            if (name == null || email == null || phoneNumber == null) {
                throw new IllegalStateException(&quot;필수 파라미터가 빠졌습니다.&quot;);
            }
            return new Member(name, email, phoneNumber, age);
        }
    }
}</code></pre>
<p>build() 메서드에서 null인 필드를 확인하고, 예외를 발생시키고 있습니다. 다만, 여전히 아래와 같이 <strong>컴파일 타임에 유효하지 않은 객체를 만드는 것을 막지 못합니다.</strong></p>
<pre><code class="language-java">/* 컴파일 에러 발생 X */
Member jiyeon = new Member.Builder()
    .name(&quot;지연&quot;)
    .build();</code></pre>
<p>실수로 유효하지 않은 객체를 생성했을 때, 이를 컴파일 타임이 아닌 런타임에 알아차릴 수 있습니다. 여전히 원자적 객체 생성에 대한 고민이 해결되지 않았습니다.</p>
<br>


<h2 id="📚-타입-안전성을-보장하면서-원자적인-객체-생성을-할-수-있는-방법은-없을까">📚 타입 안전성을 보장하면서, 원자적인 객체 생성을 할 수 있는 방법은 없을까?</h2>
<p>생성자 방식에서 문제가 가장 문제가 되었던 점은 <strong>타입 안전성</strong>이 보장되지 않는다는 것이었습니다. </p>
<p>타입 안전성을 보장하기 위해 builder pattern을 고려하게 되었지만, <strong>완전한 객체생성의 원자성을 보장하기 어렵다</strong>는 단점을 마주하게 되었습니다. </p>
<h3 id="타입을-다-다르게-만들면-되지-않을까">타입을 다 다르게 만들면 되지 않을까?</h3>
<p><code>@Builder</code> 를 사용하는 것이 의미가 있는 경우는 <strong>동일한 타입의 필드가 많을 때</strong>였습니다.</p>
<p>만약 동일한 타입의 필드가 많지 않다면, <strong>원자적인 객체 생성이 가능한 생성자 방식</strong>을 택하는 것이 낫다고 판단했습니다. </p>
<p>그렇다면, 필드의 타입을 모두 다르게 하려면 어떻게 하면 좋을까요?</p>
<blockquote>
<p><code>String</code> 타입을 <code>Name</code>, <code>Email</code>, <code>PhoneNumber</code>로 바꾸면 되지 않을까?</p>
</blockquote>
<p><strong>→ VO</strong>(<code>Value Object</code>)를 사용해서 타입 안전성을 보장하면 된다 ❗ </p>
<p>그리고, 이렇게 타입 안전성이 보장된다면 <strong>생성자 방식</strong>을 택하는 것이 <del>두 마리 토끼</del>(타입 안전성, 원자적 객체 생성)를 모두 잡는 방법이라고 생각했습니다. </p>
<h2 id="🧚-vo로-타입-안전성을-보장하기">🧚 VO로 타입 안전성을 보장하기</h2>
<h3 id="email-vo-구현">Email VO 구현</h3>
<p>Email VO를 만들어보겠습니다.</p>
<pre><code class="language-java">@Getter
@ToString
// 3. 값으로 동등성이 비교됨
@EqualsAndHashCode
public class Email {

    // 1. 불변성 (final 키워드, setter 성격의 메서드 없음)
    private final String emailValue;

    private static final String EMAIL_REGEX = &quot;^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$&quot;;
    private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX);

    public Email(String emailValue) {
        // 2. 자가 유효성 검사 (유효한 값으로만 객체가 생성됨)
        validateEmail(emailValue);
        this.emailValue = emailValue;
    }

    private void validateEmail(String emailValue) {
        if (!EMAIL_PATTERN.matcher(emailValue).matches()) {
            throw new InvalidEmailException(emailValue);
        }
    }
}</code></pre>
<p>VO는 세 가지 특징을 갖고 있습니다.</p>
<blockquote>
<ol>
<li>불변 객체이다.</li>
<li>자가 유효성 검사를 포함한다.</li>
<li>값으로 비교된다. </li>
</ol>
</blockquote>
<p>이 세 가지 특징은 위의 코드에서 확인할 수 있습니다. VO와 관련된 더 자세한 내용은 추후 작성해보도록 하겠습니다. </p>
<h3 id="생성자--vo-적용-전후">생성자 + VO 적용 전/후</h3>
<ul>
<li>Member<pre><code class="language-java">@AllArgsConstructor
public class Member {
  private Name name;
  private Email email;
  private PhoneNumber phoneNumber;
  private int age;
}</code></pre>
</li>
</ul>
<h4 id="before">Before</h4>
<pre><code class="language-java">Member member = new Member(&quot;소은&quot;, &quot;soeun1234@vmail.com&quot;, &quot;010-1212-3434&quot;, 100);</code></pre>
<p>이전에는 이름, 이메일, 휴대폰번호의 타입이 모두 <code>String</code>으로 같아 파라미터의 순서를 바꾸어 전달해도 컴파일 타임에 이를 체크할 수 없었습니다. </p>
<h4 id="after">After</h4>
<pre><code class="language-java">Member member = new Member(new Name(&quot;소은&quot;), new Email(&quot;soeun1234@vmail.com&quot;), new PhoneNumber(&quot;010-1212-3434&quot;), 100);</code></pre>
<p>이제 타입이 모두 다르기 때문에, 생성자 방식을 사용하더라도 타입에 대한 안전성이 보장됩니다. 또한 필드중 하나라도 누락시 컴파일 타임에 에러를 만나게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/e7b333fb-aae6-4e8b-8c2b-c5e471f979f2/image.png" alt=""></p>
<h2 id="📕-builder-vs-생성자">📕 Builder vs 생성자</h2>
<p>builder의 장점은 <strong>필드들을 명확히 할당</strong>할 수 있고, <strong>객체의 크기가 크더라도 필드들을 헷갈리지 않고 할당</strong>할 수 있다는 것입니다. </p>
<p>그러나, <strong>유효하지 않은 객체를 생성할 수 있다</strong>는 위험성이 있습니다. 실제로 필드를 새로 추가하면서 객체를 생성하는 모든 코드를 꼼꼼히 수정하지 않아 치명적인 문제가 발생했던 적이 있습니다. builder로 객체를 생성할 때 새로 추가한 필드를 포함하지 않아도 <strong>컴파일 타임에 이를 확인할 수 없었기 때문</strong>입니다.  </p>
<p>모든 선택에는 trade-off가 따르기 때문에 builder의 장점이 이를 상쇄한다고 생각할 수도 있습니다. </p>
<p>☝️ 다만, 앞서 이야기했듯이 빌더를 사용해야 하는 이유가 <strong>동일 타입이 많아서였다면, VO를 고려</strong>해볼 수 있습니다.</p>
<p>✌️ 또한 <strong>큰 객체의 생성은 객체의 분리를 고민해보아야 할 시점</strong>일 수 있습니다. 작은 객체를 만들게 되면, 빌더의 장점이 많이 사라지게 됩니다.</p>
<p>하지만 모든 타입을 VO로 구현할 수도 없고, 큰 객체를 만들어야만 하는 상황도 있습니다. 이럴 때는 builder나 아래에 소개될 정적 팩토리 메서드를 고민해볼 수 있습니다. </p>
<p><del>이쯤되면 어떡하자고..?ㅎㅎ</del></p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/b0f71489-0b14-4ebc-b3ce-0e021897ab92/image.webp" alt=""></p>
<p>예전에는 builder가 무조건 정답이라고 알고 있었습니다. 다만 사용해보니 장단점이 있었고, 좋은 해결 방법도 찾을 수 있었습니다. 생성자 방식을 주된 객체 생성 방법으로 택했지만, 상황에 따라 builder를 사용하는 것이 더 적절한 경우도 있었습니다.</p>
<p>따라서 <strong>각 방식의 장단점을 알고, 요구사항에 맞게 적절한 판단</strong>을 하는 것이 중요하다고 생각합니다❗</p>
<br>

<h2 id="3️⃣-정적-팩토리-메서드">3️⃣ 정적 팩토리 메서드</h2>
<p>이펙티브 자바라는 책에 따르면, &quot;<strong>생성자 대신 정적 팩토리 메서드를 고려하라</strong>&quot;고 합니다.</p>
<p>정적 팩토리 메서드는 객체 생성을 캡슐화하는 방법입니다. 객체의 생성에 대한 책임을 static 메서드가 대리합니다.</p>
<p>아래 DatabaseConnection.java는 데이터베이스 연결을 위한 클래스입니다.</p>
<h4 id="생성자를-사용했을-때">생성자를 사용했을 때</h4>
<pre><code class="language-java">public class DatabaseConnection {

    private final String connectionString;
    private Integer poolSize = 8;

    public DatabaseConnection(String connectionString) {
        this.connectionString = connectionString;
    }

    public DatabaseConnection(String connectionString, Integer poolSize) {
        this.connectionString = connectionString;
        this.poolSize = poolSize;
    }

    public void connect() {
        System.out.println(&quot;---&gt; connected successfully! \n connection-string: &quot; + connectionString + &quot;\n poolSize: &quot; + poolSize);

    }

}</code></pre>
<p>두 가지 생성자가 있습니다. </p>
<p>하나는, 데이터베이스 연결 문자열만 받고 기본 풀 크기를 사용해 데이터베이스 연결을 수립합니다. 다른 하나는, 연결 문자열과 풀 크기를 모두 지정 가능합니다.</p>
<p>이 두 가지 생성자를 제공함으로써, 기본 환경을 사용하거나 커스터마이징 하고 싶을 때를 구분해 유연하게 데이터베이스 연결을 수립할 수 있습니다. </p>
<h4 id="정적-팩토리-메서드를-사용했을-때">정적 팩토리 메서드를 사용했을 때</h4>
<pre><code class="language-java">public class DatabaseConnection {

    private final String connectionString;
    private Integer poolSize = 8;

    private static DatabaseConnection databaseConnection = null;

    private DatabaseConnection(String connectionString) {
        this.connectionString = connectionString;
    }

    private DatabaseConnection(String connectionString, Integer poolSize) {
        this.connectionString = connectionString;
        this.poolSize = poolSize;
    }

    public void connect() {
        System.out.println(&quot;---&gt; connected successfully! \n connection-string: &quot; + connectionString + &quot;\n poolSize: &quot; + poolSize);
    }

    /**
     * This method provide singleton instance
     * @param connectionString
     * @return
     */
    public static DatabaseConnection getInstance(String connectionString) {
        if (Objects.isNull(databaseConnection)) {
            databaseConnection = new DatabaseConnection(connectionString);
        }
        return databaseConnection;
    }

    public static DatabaseConnection getInstanceWithPoolSize(String connectionString, Integer poolSize) {
        return new DatabaseConnection(connectionString, poolSize);
    }

}</code></pre>
<p>정적 팩토리 메서드의 특징과 함께 위의 코드를 살펴보도록 하겠습니다.</p>
<blockquote>
<p><strong>1. 이름을 가진다.</strong></p>
</blockquote>
<p>생성자와 달리, 정적 팩토리 메서드는 getInstance(), getInstanceWithPoolSize()와 같이 <strong>이름을 가질 수 있습니다.</strong> </p>
<p>PoolSize를 직접 지정하고 싶을 때 getInstanceWithPoolSize()를 선택해야 한다는 사실을 알 수 있습니다. </p>
<blockquote>
<p>*<em>2. 하나의 인스턴스만 생성하고 재사용할 수 있다. *</em></p>
</blockquote>
<p>즉, 정적 팩토리 메서드를 통해 싱글톤 패턴을 구현할 수 있습니다. </p>
<p>getInstance() 메서드를 보면, DatabaseConnection이 null일 때만 새로운 객체를 생성하고, 그렇지 않을 때는 이미 만들어진 객체를 재사용합니다.</p>
<p>또한 생성자의 접근 제한자를 private으로 설정했기 때문에, 정적 팩토리 메서드를 통해서만 객체를 생성할 수 있습니다. 이를 통해 <strong>불필요한 중복 객체를 방지</strong>하고, <strong>싱글톤</strong>으로 사용할 수 있습니다. </p>
<blockquote>
<p><strong>3. 하위 타입을 리턴할 수 있다.</strong></p>
</blockquote>
<p>DatabaseConnection을 상속받은 MySqlConnection, PostgresConnection, OracleConnection이 있다고 가정해보겠습니다. </p>
<p>다음과 같이 하위 타입의 객체를 리턴할 수 있습니다. </p>
<pre><code class="language-java">public static DatabaseConnection getInstance(String connectionString) {
        if (connectionString.startsWith(&quot;mysql&quot;)) {
            return new MySqlConnection(connectionString);
        } else if (connectionString.startsWith(&quot;postgresql&quot;)) {
            return new PostgresConnection(connectionString);
        } else if (connectionString.startsWith(&quot;oracle&quot;)) {
            return new OracleConnection(connectionString);
        }
        throw new IllegalArgumentException(&quot;Unsupported database type&quot;);
    }</code></pre>
<blockquote>
<p><strong>4. 예외를 던질 수 있다.</strong></p>
</blockquote>
<p>객체를 생성하는 과정에서 예외가 발생할 수 있다면, 정적 팩토리 메서드를 고려해볼 수 있습니다. </p>
<p>위의 예시에서처럼 connectionString이 잘못되었을 때 예외를 던지는 등의 validation을 추가할 수 있습니다. </p>
<br>


<h2 id="⛳️-마치며">⛳️ 마치며</h2>
<p>객체를 생성하는 세 가지 방법을 알아봤습니다. </p>
<p>이제 습관처럼 어노테이션을 붙이지 않고, 다양한 객체 생성 방법을 고민해 적절한 선택을 할 수 있게 되었습니다.</p>
<p>긴 글 읽어주셔서 감사합니다. 새해복 많이 받으세요!</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/06a2bd48-6ca9-4606-99fd-29c6ff338ddf/image.webp" alt=""></p>
<br>

<h2 id="📖-참고자료">📖 참고자료</h2>
<p><a href="https://projectlombok.org/features/constructor">@AllArgsConstructor - lombok</a>
<a href="https://www.baeldung.com/java-builder-pattern">Builder - baldung</a>
<a href="https://stackoverflow.com/questions/29881135/difference-between-builder-pattern-and-constructor">Builder pattern vs Constructor - stackoverflow</a>
<a href="https://projectlombok.org/features/Builder">@Builder on Constructor - lombok</a>
<a href="https://stackoverflow.com/questions/40264/how-many-constructor-arguments-is-too-many">how many constructor arguments is too many - stackoverflow</a>
<a href="https://multifrontgarden.tistory.com/290">builder pattern에 대한 고찰</a>
<a href="https://www.devnips.com/2021/05/adding-custom-validation-in-lombok.html">adding custom validation in lombok</a>
<a href="https://www.svlada.com/step-builder-pattern/">Step Builder</a>
<a href="https://beratyesbek.medium.com/item-1-effective-java-consider-static-factory-methods-instead-of-constructors-7ff773804aaa">Effective Java Consider Static Factory Methods instead of Constructors</a>
<a href="https://stackoverflow.com/questions/929021/what-are-static-factory-methods">What are static factory methods? - stackoverflow</a>
<a href="https://www.quora.com/What-are-the-disadvantages-of-Java-constructors">What are the disadvantages of Java constructors?</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[동시성 이슈를 곁들인 조회수 기능 개선하기]]></title>
            <link>https://velog.io/@carol_ly/%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88%EB%A5%BC-%EA%B3%81%EB%93%A4%EC%9D%B8-%EC%A1%B0%ED%9A%8C%EC%88%98-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@carol_ly/%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88%EB%A5%BC-%EA%B3%81%EB%93%A4%EC%9D%B8-%EC%A1%B0%ED%9A%8C%EC%88%98-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 26 Dec 2024 12:20:42 GMT</pubDate>
            <description><![CDATA[<h2 id="📚-시작하며">📚 시작하며</h2>
<p>지난번 <a href="https://velog.io/@carol_ly/Event-Publisher-%EC%82%AC%EC%9A%A9%ED%95%B4-%EC%A1%B0%ED%9A%8C%EC%88%98-%EB%A1%9C%EC%A7%81-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0-Async-%EB%B9%A0%EB%9C%A8%EB%A0%B8%EC%9D%84-%EB%95%8C-%EC%9D%BC%EC%96%B4%EB%82%98%EB%8A%94-%EC%9D%BC">Event Publisher를 사용하여 비동기적으로 조회수 이벤트를 처리한 글</a>을 작성한 이후, 아래와 같은 질문을 받았습니다. </p>
<blockquote>
<p>&quot;10만 명의 사용자가 동호회를 접속했을 때, 조회수가 10만 만큼 올라가나요?&quot;</p>
</blockquote>
<p>조회수 증가 로직이 실패하는 경우가 언제일까 고민해보다가 <strong>동시성 이슈</strong>가 가장 먼저 떠올랐습니다.</p>
<p>그래서 이번 글에서는 아래의 질문에 대해 고민하고 해결해나가는 과정을 담아보려고 합니다! 😄</p>
<blockquote>
<p><strong>다수의 사용자</strong>가 하나의 동호회를 <strong>동시에 조회</strong>했을 때, 조회수가 정상적으로 올라갈까?</p>
</blockquote>
<br>

<h2 id="🖍️-javautilconcurrent">🖍️ java.util.concurrent</h2>
<p>동시성 문제를 어떻게 경험하고, 테스트 할 수 있을까요?</p>
<p>자바의 <code>java.util.concurrent</code> 패키지를 살펴보도록 하겠습니다. 해당 패키지에는 <code>ExecutorService</code> 및 <code>CountDownLatch</code>와 같은 여러 동기화 장치가 포함되어 있습니다. </p>
<blockquote>
<p>ExecutorService란?</p>
</blockquote>
<p><strong>스레드풀을 제공</strong>하며, <strong>각각의 스레드에 작업을 할당</strong>할 수 있습니다. </p>
<pre><code class="language-java">
Future&lt;?&gt; submit(Runnable task);</code></pre>
<p>주어진 작업(task)을 실행하고, 모든 작업이 완료되었을 때 작업 상태와 결과를 보유한 Future 리스트를 반환합니다. </p>
<pre><code class="language-java">executorService.shutdown();</code></pre>
<p>스레드 풀을 종료하여 리소스를 해제합니다. </p>
<blockquote>
<p>CountDownLatch란?</p>
</blockquote>
<p>CountDownLatch를 사용해 다른 스레드들이 <strong>주어진 일을 완료할 때까지 특정 스레드를 대기 상태</strong>에 들게 할 수 있습니다. 이는 각각의 스레드가 작업을 종료하면 <strong>1씩 감소하는 counter field</strong>입니다.</p>
<pre><code class="language-java">CountDownLatch latch = new CountDownLatch(numberOfThreads);</code></pre>
<p>생성 시 초기 카운트 값(정수)을 설정합니다. 이때 numberOfThreads는 작업할 스레드의 개수와 동일한 값으로 설정합니다.</p>
<pre><code class="language-java">latch.countDown();</code></pre>
<p>latch의 카운트 값을 감소시킵니다. 만약 현재 카운트 값이 0보다 크다면, 1 줄어듭니다. </p>
<pre><code class="language-java">latch.getCount();</code></pre>
<p>현재 카운트 값을 반환합니다(long 타입). 디버깅 또는 테스트 용도로 활용할 수 있습니다. </p>
<pre><code class="language-java">latch.await();</code></pre>
<p><strong>카운트 값이 0이 될 때까지 스레드를 대기 상태로 유지</strong>합니다. 모든 스레드가 작업을 완료하여 카운트가 0이 될 때까지 메인 스레드가 대기합니다. <strong>카운트 값이 0이 되면, 대기 중이던 모든 스레드가 깨어나고 대기 상태에서 해제</strong>됩니다.</p>
<br>


<h2 id="✍️-테스트-코드-작성">✍️ 테스트 코드 작성</h2>
<p>자, 이제 테스트 코드를 작성해보도록 합시다!</p>
<ul>
<li>테스트 시나리오
100명의 사용자가 동시에 UUID가 club_token_1인 동호회를 조회하는 상황을 가정해보겠습니다. 해당 동호회에 대한 조회수는 100만큼 올라야 합니다. </li>
</ul>
<p><code>@SpringBootTest</code>를 사용하여 통합 테스트를 진행하였습니다. </p>
<pre><code class="language-java">@SpringBootTest(classes = BadmintonApplication.class)
@ActiveProfiles(&quot;test&quot;)
@Transactional
public class ClubReadConcurrencyTest {

    @Autowired
    private ClubStatisticsRepository clubStatisticsRepository;

    @Autowired
    private ClubStatisticsService clubStatisticsService;

    @Test
    void 동호회_조회_동시성_테스트() throws InterruptedException {
        ///given
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        CountDownLatch latch = new CountDownLatch(100);

        //when
        for (int i = 0; i &lt; 100; i++) {
            executorService.submit(() -&gt; {
                try {
                    clubStatisticsService.increaseVisitedClubCount(&quot;club_token_1&quot;);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            });
        }
        latch.await();
        executorService.shutdown();

        //then
        int visitedCount = clubStatisticsRepository.findByClubClubToken(&quot;club_token_1&quot;).getVisitedCount();
        assertThat(visitedCount).isEqualTo(100);
    }
}</code></pre>
<blockquote>
<p>😵‍💫 CountDownLatch, latch.await() 대체 뭐야?</p>
</blockquote>
<p>이 개념이 저에게는 다소 어려웠기 때문에, 설명을 하고 넘어가려고 합니다. 이미 알고 계시다면 내용이 조금 길기 때문에 넘어가주세요 ...  🙏</p>
<h3 id="테스트-코드를-실행하는-test-worker-스레드-비동기적으로-실행되는-20개의-스레드">테스트 코드를 실행하는 Test Worker 스레드, 비동기적으로 실행되는 20개의 스레드</h3>
<pre><code class="language-java">System.out.println(&quot;스레드 이름: &quot; + Thread.currentThread().getName());</code></pre>
<p>중간 중간에 스레드의 이름을 찍어 보면 아래와 같이 출력됩니다. </p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/256624e0-0b0a-42bb-9c9e-85ab50efe4f5/image.png" alt=""></p>
<p><strong>Test Worker</strong>는 테스트 코드 실행을 위해 만들어진 스레드입니다. 이 스레드는 JUnit과 같은 테스트 프레임워크가 <strong>테스트를 실행할 때 기본적으로 사용하는 스레드</strong> 이름입니다.</p>
<p>그럼 그 아래 pool-3-thread로 시작하는 스레드는 무엇일까요?</p>
<p>Test Worker 스레드는 아래의 코드를 실행합니다.</p>
<pre><code class="language-java">ExecutorService executorService = Executors.newFixedThreadPool(20);</code></pre>
<p>ExecutorService는 <strong>크기가 20인 스레드 풀을 생성</strong>합니다. 즉, 최대 20개의 스레드가 동시에 작업을 처리할 수 있습니다.</p>
<p>이제 <code>submit()</code> 메서드를 통해 <strong>스레드풀에 제출된 100개의 작업</strong>은 <strong>비동기</strong>적으로 처리됩니다.</p>
<h3 id="latchawait가-없다면">latch.await()가 없다면?</h3>
<p>Test Worker 스레드는 <strong>100개의 비동기 작업이 완료될 때까지 기다려주지 않습니다</strong>.</p>
<p>스레드풀에 제출된 100개의 작업이 아직 실행되고 있더라도, Test Worker 스레드는 <del>아래로 쭉쭉</del> 내려갑니다. </p>
<p>만약 latch.await()가 없었다면 Test Worker 스레드는 그 아래 executorService.shutdown()을 실행할 것입니다. 그럼 스레드풀이 종료되고 리소스가 해제되기 때문에 위에서 할당해준 작업들 또한 중단됩니다. </p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/13011910-a26e-4515-9146-29bbc550f2c4/image.png" alt=""></p>
<p>위의 그림을 통해 Thread1~20이 아직 작업을 처리 중인데, <strong>Test Worker 스레드는 실행을 마치고 종료된 모습</strong>을 확인할 수 있습니다. </p>
<p>즉, latch.await()가 없다면 동시성을 제어했더라도 테스트가 실패할 것입니다. Test Worker 스레드는 20개의 Sub 스레드가 수행하는 100개의 작업이 끝나기도 전에 종료되기 때문이죠. </p>
<h3 id="이래서-countdownlatch를-사용하는구나">이래서 CountDownLatch를 사용하는구나!</h3>
<p>저희가 원하는 것은 Thread1~20이 100개의 작업을 모두 끝마치고 난 뒤의 결과값을 확인하는 것입니다.</p>
<p>따라서 <strong>Test Worker 스레드는 잠시 실행을 멈추고 20개의 Sub 스레드가 100개의 작업을 모두 마칠 때까지 기다려</strong>줘야 합니다. 이때 CountDownLatch라는 <strong>걸쇠</strong>를 사용할 수 있습니다.</p>
<pre><code class="language-java">CountDownLatch latch = new CountDownLatch(100);</code></pre>
<p>카운트값을 100으로 설정해 CountDownLatch를 만들었습니다. </p>
<pre><code class="language-java">finally {
    latch.countDown();
}</code></pre>
<p>하나의 작업이 완료될 때마다 finally 구문 안에 있는 <code>latch.countDown();</code>가 실행되어 카운트값이 <strong>1씩 감소</strong>합니다.</p>
<p><strong>100개의 작업이 모두 완료되면, 카운트값이 0</strong>이 됩니다.</p>
<pre><code class="language-java">latch.await();</code></pre>
<p>Test Worker 스레드가 위의 코드를 만나면, CountDownLatch의 카운트값을 확인합니다. 만약, <strong>0보다 크다면 대기 상태</strong>에 머무릅니다. <strong>0이라면, 대기 상태에서 해제</strong>됩니다. </p>
<p>위에서 확인했듯이, 스레드풀에 제출된 100개의 작업이 모두 완료되면 카운트값이 0이 되며, 이때 <strong>Test Worker 스레드가 대기 상태에서 해제</strong>됩니다!</p>
<p>즉, CountDownLatch는 Test Worker 스레드가 100개의 작업이 모두 완료될 때까지 <strong>대기 상태에 머무르도록</strong> 하는 역할을 합니다. </p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/8392d76f-ba45-4ee2-a074-a5e99c12b7c7/image.png" alt=""></p>
<p>위의 그림을 통해 더이상 Test Worker 스레드가 비동기 작업이 끝나기 전에 종료되지 않고, <strong>스레드풀에 제출된 작업이 모두 완료될 때까지 기다려주는 것</strong>을 확인할 수 있습니다. </p>
<h3 id="threadsleep-쓰면-되지-않아">Thread.sleep() 쓰면 되지 않아?</h3>
<p>Thread.sleep()은 <strong>지정된 시간 동안 현재 스레드를 중단</strong>합니다. 다만, 의도한 이벤트 발생 시점에 맞춰 작업을 재개할 수 없습니다. <strong>작업이 끝나는 정확한 시간을 예측하기 어렵</strong>기 때문에 테스트 수행 시 <strong>불필요한 대기 시간이 발생</strong>할 확률이 높습니다.</p>
<p>이와 반면, CountDownLatch 사용 시 <strong>정확한 이벤트 기반 동작</strong>이 가능합니다!</p>
<br>

<h2 id="🤔-왜-이런-일이">🤔 왜 이런 일이?</h2>
<p>테스트한 결과, 평균 10% 정도의 성공률을 보였습니다. 즉 100명의 사용자가 동시에 조회했을 때, 조회수가 100으로 오르지 않고, 10에 웃도는 모습을 확인할 수 있었습니다.</p>
<p>예상했던 것보다도 너무 낮은 성공률을 보여서, 조회수를 올리는 로직을 다시 살펴보았습니다.</p>
<ul>
<li>ClubStatisticsService</li>
</ul>
<pre><code class="language-java">@Transactional
public void increaseVisitedClubCount(String clubToken) {
        ClubStatistics clubStatistics = clubStatisticsReader.readClubStatistics(clubToken);
        clubStatistics.increaseVisitedCount();
        clubStatisticsStore.store(clubStatistics);
}</code></pre>
<ul>
<li>ClubStatisticsEntity</li>
</ul>
<pre><code class="language-java">public void increaseVisitedCount() {
    this.visitedCount++;
}</code></pre>
<p>위와 같이 Entity 내부에 필드를 1씩 증가시키는 코드가 있습니다. </p>
<h3 id="jpa-dirty-checking">JPA Dirty Checking</h3>
<p>JPA는 dirty checking으로 엔티티의 상태 변경을 감지하고, 트랜잭션이 커밋될 때 UPDATE 쿼리를 날립니다. 이때 Race Condition이 발생할 수 있습니다. </p>
<h3 id="race-condition">Race Condition</h3>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/c0404ff9-f016-4791-a6e3-44fcdc33aaed/image.png" alt=""></p>
<p>그림과 같이 Thread 1이 read()를 수행합니다. 이때 visitedCount 값은 0입니다. Thread 1이 visitedCount를 1 증가하여 DB에 반영하기 이전에, Thread 2가 read()를 수행합니다. 아직 visitedCount는 0입니다. 두 개의 스레드가 조회수를 올리고 싶었으나, 최종적으로 DB에 반영되는 값은 2가 아니라 1이 됩니다.</p>
<br>

<h2 id="🧚-직접-update-query-작성하기">🧚 직접 Update Query 작성하기</h2>
<p>JPA를 통한 업데이트를 할 때 락을 통해 동시성을 제어하지 않게 되면, 위와 같은 문제가 발생할 수 있습니다. </p>
<p>Update 쿼리를 직접 날려 간단하게 해결해보도록 하겠습니다. querydsl을 사용하여 DB의 row에 직접 +1을 수행합니다. </p>
<pre><code class="language-java">@Override
@Transactional
public void increaseClubVisitCount(String clubToken) {
        queryFactory.update(clubStatistics)
            .set(clubStatistics.visitedCount, clubStatistics.visitedCount.add(1))
            .where(clubStatistics.club.clubToken.eq(clubToken))
            .execute();
}</code></pre>
<p>영속성 컨텍스트에서 객체를 읽어와 값을 1 증가시키고 JPA의 Dirty Checking을 통해 업데이트하는 이전 방식과 다르게, DB의 해당 행(row)에 직접 +1 연산을 수행하기 때문에 자동으로 락이 걸리며 동시성 문제가 해소됩니다. 
<img src="https://velog.velcdn.com/images/carol_ly/post/af977c81-ae13-4683-a4fb-9cd32a9776d5/image.png" alt=""></p>
<br>

<h2 id="🔓-락을-걸어보자">🔓 락을 걸어보자!</h2>
<p>위의 방식에는 어떤 문제점이 있을까요? DB Connection Pool 소진, Deadlock, DB 부하 증가 등의 문제가 발생할 수 있습니다.</p>
<p>다만, 기존 프로젝트에서는 위와 같이 UPDATE 쿼리를 직접 작성하는 것으로 동시성 문제를 해결하기로 했습니다. <strong>데이터 정합성이 거의 100% 지켜졌고</strong>, 하나의 동호회를 조회하는데 <strong>높은 트래픽이 몰릴 일이 적어</strong> 큰 문제점을 느끼지 못했기 때문입니다. </p>
<p>그래도 . . . 아쉬우니 여러 가지 종류의 락을 걸어보면서 동시성 문제를 조금 더 깊게 공부해보기로 했습니다. </p>
<br>


<h2 id="🚨-락을-걸-때-주의할-점---transactional">🚨 락을 걸 때 주의할 점 - @Transactional</h2>
<pre><code class="language-java">@Override
@Transactional
public void increaseVisitedClubCount(String clubToken) {
        synchronized (this) {
            increaseClubVisitCount(clubToken);
        }
    }

private void increaseClubVisitCount(String clubToken) {
        ClubStatistics clubStatistics = clubStatisticsReader.readClubStatistics(clubToken);
        clubStatistics.increaseVisitedCount();
        clubStatisticsStore.store(clubStatistics);
}</code></pre>
<p>synchronized 키워드를 붙여봤습니다. 위 코드에서는 어떠한 문제가 있을까요?</p>
<p><strong>트랜잭션 내부에서 락을 거는 행위는 주의</strong>해야 합니다. 아래와 같은 문제가 발생할 수 있습니다. </p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/aabc4b27-c524-4692-9cd7-c5381d00e5e9/image.png" alt=""></p>
<p>1번 요청이 작업을 다 수행한 다음 lock을 반납했습니다. 그러나 아직 트랜잭션이 커밋되기 이전이라고 가정해보겠습니다. </p>
<p>그 사이에 2번 요청은 lock을 획득하고, visitCount를 읽습니다. 이때 읽은 visitCount값은 1이 아닌 0입니다.</p>
<p>1번 요청이 +1 증가시킨 조회수가 아직 DB에 반영되기 이전이기 때문입니다. 그런데 두 번째 요청이 1번 <strong>tx가 커밋되기 이전에 lock을 획득</strong>해버렸고, 이때 읽어들인 조회수는 아직 0입니다. </p>
<p>이는 트랜잭션 내부에 락을 걸었기 때문인데요, <strong>커밋하지 않은 데이터를 다른 곳에서 조회</strong>하게 되면 <strong>데이터 정합성에 큰 문제</strong>가 발생합니다.</p>
<p>어떻게 해결할 수 있을까요?</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/384380ae-8775-4511-b6bc-b464f9f1743f/image.gif" alt=""></p>
<p><strong>락 내부에 @Transactional을 걸면</strong> 위의 문제가 해결됩니다! </p>
<pre><code class="language-java">@Override
public void increaseVisitedClubCount(String clubToken) {
        // 1. 락을 건다.
        synchronized (this) {
            increaseClubVisitCount(clubToken);
        }
    }

// 2. 트랜잭션이 시작된다.
@Transactional
public void increaseClubVisitCount(String clubToken) {
        ClubStatistics clubStatistics = clubStatisticsReader.readClubStatistics(clubToken);
        clubStatistics.increaseVisitedCount();
        clubStatisticsStore.store(clubStatistics);
}</code></pre>
<p>앞선 코드와 다르게, <strong>락을 획득하고 나서 트랜잭션이 시작</strong>됩니다. 작업을 수행한 다음, <strong>커밋까지 완료되어야 락을 반납</strong>합니다. </p>
<p>즉, <strong>커밋되지 않은 공유 자원의 경우 아직 락이 해제되지 않았기</strong> 때문에, *<em>다른 스레드가 더이상 접근하지 못합니다. *</em></p>
<p>이를 통해 항상 커밋된 내용을 읽는 것을 보장할 수 있습니다. </p>
<blockquote>
<p>🔎 요약
락과 @Transactional을 함께 사용할 때는 데이터 정합성을 위해 <strong>락 내부에서 트랜잭션이 시작</strong>되도록 합니다.</p>
</blockquote>
<br>

<h2 id="😕-synchronized의-문제점">😕 Synchronized의 문제점</h2>
<p>다만, Synchronized 키워드는 <strong>한 프로세스 내에서만 스레드의 접근 제한을 보장</strong>합니다. 따라서 여러 프로세스에서 데이터 정합성을 보장해주지 않습니다. 또한 <strong>자바 애플리케이션에 종속</strong>되기 때문에, 여러 서버로 확장되었을 때 사용하기 어렵습니다. </p>
<br>


<h2 id="🔏-비관적-락">🔏 비관적 락</h2>
<p>비관적락은 mysql에서 제공하는 레코드락입니다.</p>
<pre><code class="language-sql"># user1 요청
start transaction;
select * from clubStatistics where clubToken = 1 for update; # visitCount = 0
/*
user 1 조회수 증가
- update visitCount # visitCount = 1
- insert club_statistics
*/
commit; # 커밋이 되는 순간, 락이 해제된다.ㅡㅡㅡㅡ</code></pre>
<p>ClubStatistics 테이블에서 조회수를 조회할 때 락을 걸면, 커밋되기 전까지 읽거나 쓸 수 없습니다. 쿼리의 끝에 <code>for update</code>를 붙여 해당하는 레코드에 락을 걸 수 있습니다. 레코드락은 중첩해서 걸 수 없으며, 락이 해제될 때까지 기다려야 합니다. <strong>커밋이 되면, 락이 해제</strong>됩니다. </p>
<pre><code class="language-sql"># user2 요청
start transaction;
select * from clubStatistics where clubtoken = 1 for update; # 락이 해제되기를 기다림, 락을 건다. 
/*
user 2 조회수 증가
- update visitCount # visitCount = 2
- insert club_statistics
*/
commit;</code></pre>
<p>user2는 <strong>락이 해제될 때까지 기다렸다가</strong>, user1의 요청이 <strong>커밋되는 순간 락을 획득</strong>합니다. 이때 읽어들인 visitCount는 1입니다. </p>
<p>아래와 같이 어노테이션을 통해 비관적락을 구현할 수 있습니다. 조회 시 락을 걸어, Transaction 범위 내에서 해당 레코드에 대한 접근 제한이 보장됩니다. </p>
<pre><code class="language-java">@Lock(LockModeType.PESSIMISTIC_WRITE)
ClubStatistics findByClubClubToken(String clubToken);</code></pre>
<p>다만, 비관적 락의 경우 mysql에서 제공하는 락으로 <strong>DB에 병목이 생기면, 처리량에 문제</strong>가 생길 수 있습니다. 즉, <strong>DB에 부하</strong>가 생기며 성능 이슈가 발생할 가능성이 있습니다. </p>
<br>

<h2 id="🧣-레디스락">🧣 레디스락</h2>
<p>레디스락은 분산락입니다. 즉, <strong>분산 환경에서도 안정적으로 락을 걸고 해제</strong>할 수 있습니다. Java의 Redis client인 <strong>Redisson</strong>을 사용했습니다. Redisson은 Lock, Semaphore, CountDownLatch 등의 잠금 및 동기화 장치를 제공합니다.</p>
<p>기본적으로 Redis는 <strong>인메모리 DB</strong>이기에 <strong>더 빠르게 락을 획득 및 해제</strong>할 수 있습니다. 분산락에 대한 부하를 Redis 서버가 감당하기 때문에, 앞서 살펴봤던 DB 성능 문제를 방지할 수 있습니다. </p>
<p>build.gradle에 아래와 같이 의존성을 추가합니다. </p>
<ul>
<li>build.gradle</li>
</ul>
<pre><code class="language-java">implementation &#39;org.redisson:redisson-spring-boot-starter:3.27.0&#39;</code></pre>
<ul>
<li>Executor </li>
</ul>
<pre><code class="language-java">package org.badminton.infrastructure.statistics;

@Component
@Slf4j
@RequiredArgsConstructor
public class DistributeLockProcessorExecutorImpl implements DistributedLockProcessor {

    private final RedissonClient redissonClient;

    @Override
    public void execute(String lockName, long waitMilliSecond, long releaseMilliSecond, Runnable runnable) {
        RLock lock = redissonClient.getLock(lockName);
        try {
            boolean isLocked = lock.tryLock(waitMilliSecond, releaseMilliSecond, TimeUnit.MILLISECONDS);
            if (!isLocked) {
                throw new IllegalArgumentException(&quot;[&quot; + lockName + &quot;] lock 획득 실패&quot;);
            }
            runnable.run();
        } catch (InterruptedException e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException(e);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}</code></pre>
<ul>
<li>ClubStatisticsService</li>
</ul>
<pre><code class="language-java">@Override
public void increaseVisitedClubCount(String clubToken) {
    String lockName = &quot;CLUB_VISIT_LOCK_&quot; + clubToken;
    distributedLockProcessor.execute(lockName, 10000, 10000,
            () -&gt; increaseClubVisitCountWithNamedLock(clubToken)
    );
}

@Transactional
public void increaseClubVisitCountWithNamedLock(String clubToken) {
    ClubStatistics clubStatistics = clubStatisticsReader.readClubStatistics(clubToken);
    clubStatistics.increaseVisitedCount();
    clubStatisticsStore.store(clubStatistics);
}</code></pre>
<br> 

<h2 id="🕰️-세-가지-락-비교">🕰️ 세 가지 락 비교</h2>
<p>100개 데이터를 테스트한 결과, 아래와 같은 결과를 확인했습니다.</p>
<ol>
<li>동시성 적용 X<ul>
<li>0.322s (322.807ms)</li>
</ul>
</li>
<li>synchronized<ul>
<li>1.144s (1143.671ms)</li>
</ul>
</li>
<li>레디스락<ul>
<li>1.21s (1206.73ms)</li>
</ul>
</li>
<li>mysql 락(비관적 락)<ul>
<li>0.634s (634.514ms)</li>
</ul>
</li>
</ol>
<p>당연히 동시성을 고려하지 않았을 때, 락을 걸지 않아 가장 빨랐습니다. 다만, 세 가지 락이 보여준 시간이 예상 밖이었는데, mysql 락이 가장 빨랐고 레디스락이 가장 느렸습니다. 이는 추가적으로 알아보려고 합니다. </p>
<br>

<h2 id="⛳️-조회수와-동시성">⛳️ 조회수와 동시성</h2>
<p>동시성에서 중요한 건, <strong>&quot;공유 자원인가? 공유 자원이 아닌가?&quot;</strong>를 판단하는 것입니다. 만약 공유 자원이라면, <strong>얼마나 공유되고 있는지</strong>를 고민해보아야 합니다. 조회수의 경우, <strong>동시성의 경합 조건이 실제로는 훨씬 덜할 것</strong>이라고 예상됩니다. </p>
<p>또한 조회수는 엄격하게 가져갈 것이 아니기 때문에, <strong>&quot;조회수를 올리는데 별도의 락까지 써야 할까?&quot;</strong>라고 생각했습니다. 어쩌다가 한 번씩 실패하는 경우에는 감내할 수 있기 때문입니다. </p>
<p>따라서, 최종적으로 처음에 소개했던 UPDATE 쿼리를 직접 날리는 방식을 택하게 되었습니다. </p>
<br>

<h2 id="🌳-마치며">🌳 마치며</h2>
<p>프로젝트에서 조회수 기능을 구현하며 동시성 문제를 고민해본 내용을 담아봤습니다. 아주 간단할거라 생각했는데, 고민해볼 만한 부분이 많아 재미있게 구현할 수 있었습니다! </p>
<p>긴 글 읽어주셔서 감사합니다 😄</p>
<br>

<h2 id="📘-참고자료">📘 참고자료</h2>
<p><a href="https://www.baeldung.com/java-countdown-latch">CountDownLatch - baeldung</a>
<a href="https://www.baeldung.com/java-executor-service-tutorial">ExecutorService - baeldung</a>
<a href="https://www.baeldung.com/jpa-pessimistic-locking">비관적락 - baeldung</a>
<a href="https://www.baeldung.com/redis-redisson">Redisson - baeldung</a>
<a href="https://redis.io/docs/latest/develop/use/patterns/distributed-locks/">Distributed Locks with Redis</a>
<a href="https://stackoverflow.com/questions/41767860/spring-transactional-with-synchronized-keyword-doesnt-work">Transactional with synchronized doesn&#39;t work - stackoverflow</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Event Publisher로 조회수 비동기적으로 처리하기 (+ @Async를 빠뜨리면?)]]></title>
            <link>https://velog.io/@carol_ly/Event-Publisher-%EC%82%AC%EC%9A%A9%ED%95%B4-%EC%A1%B0%ED%9A%8C%EC%88%98-%EB%A1%9C%EC%A7%81-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0-Async-%EB%B9%A0%EB%9C%A8%EB%A0%B8%EC%9D%84-%EB%95%8C-%EC%9D%BC%EC%96%B4%EB%82%98%EB%8A%94-%EC%9D%BC</link>
            <guid>https://velog.io/@carol_ly/Event-Publisher-%EC%82%AC%EC%9A%A9%ED%95%B4-%EC%A1%B0%ED%9A%8C%EC%88%98-%EB%A1%9C%EC%A7%81-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0-Async-%EB%B9%A0%EB%9C%A8%EB%A0%B8%EC%9D%84-%EB%95%8C-%EC%9D%BC%EC%96%B4%EB%82%98%EB%8A%94-%EC%9D%BC</guid>
            <pubDate>Tue, 03 Dec 2024 15:54:57 GMT</pubDate>
            <description><![CDATA[<h2 id="📗-시작하며">📗 시작하며</h2>
<p>프로젝트를 진행하며 <strong>조회수 업데이트</strong>를 <strong>비동기</strong>적으로 처리하게 되며 <strong>Event Publisher</strong>를 공부하게 되었습니다. </p>
<p>해당 글은 <strong>Event Publisher를 사용하게 된 이유</strong>와 <strong>기본적인 사용법</strong>을 코드 예시와 함께 설명하고 있습니다. </p>
<p>또한 <strong>@TransactionalEventListener</strong>와 <strong>@EventListener</strong> 동작의 차이점을 테스트하던 중 깨달음을 얻은 내용을 담고 있습니다. </p>
<p>바로 코드를 통해 확인해보겠습니다!</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class ClubFacade {
    private final ClubService clubService;
    private final ClubStatisticsService clubStatisticsService;

    @Transactional
    public ClubInfo readClub(String clubToken) {
        var clubInfo = clubService.readClub(clubToken);
        clubStatisticsService.increaseClubVisitCount(clubToken);
        return clubInfo;
    }
    /* 
  * 생략
  */
}</code></pre>
<p>동호회를 조회하는 서비스 코드 일부입니다. 동호회를 조회한 다음, 동호회 통계 서비스의 <code>increaseClubVisitCount</code> 메서드를 실행해 조회수를 1 올리고 있습니다. </p>
<h2 id="📍--조회수의-중요성">📍 +) 조회수의 중요성</h2>
<p>참고로, 저희 서비스의 경우 조회수를 사용자에게 직접적으로 보여주지 않고 있습니다. <strong>인기 있는 동호회를 정렬할 때 내부적으로 사용하는 지표</strong>인데요. </p>
<p>이러한 이유로, 조회수의 정합성이 아주 중요하다고 판단하지 않았습니다. 즉, 10만 명의 사용자가 동호회를 조회했지만, 조회수가 10만보다 조금 적더라도, 이는 서비스에 큰 영향을 미치지 않습니다. </p>
<p>따라서 동호회 조회 시 <strong>조회수가 일부 올라가는데 실패하더라도</strong>, <strong>사용자가 동호회를 정상적으로 조회</strong>할 수 있는 것이 더 중요한 요구사항으로 작용했습니다. </p>
<p>개발 중인 서비스에서 <strong>조회수가 어떤 역할</strong>을 하고 있는지, 또한 <strong>실시간성</strong>과 <strong>데이터 정합성</strong>이 어느 정도로 보장되어야 하는지를 고민해보면 좋을 것 같습니다!</p>
<h2 id="🤔-문제점">🤔 문제점</h2>
<h3 id="1-강결합">1. 강결합</h3>
<p>지금은 동호회와 관련된 서비스가 동호회 통계 서비스와 강하게 결합이 되어있습니다. 동호회를 조회할 때 <code>ClubStatisticsService</code>에 꼭 의존해야 할까요?</p>
<h3 id="2-조회수-쓰기-작업-실패-시-동호회-조회도-실패">2. 조회수 쓰기 작업 실패 시 동호회 조회도 실패</h3>
<p>조회수를 올리는 기능이 동기적으로 처리되고 있으며, 동호회 조회와 조회수 쓰기 작업이 하나의 트랜잭션으로 묶여 있습니다. 아래의 상황을 생각해보겠습니다.</p>
<blockquote>
<p>조회수를 올리는 로직에서 예외가 발생한다면?</p>
</blockquote>
<p>동호회 조회에는 성공했지만, 조회수를 올리는데 실패했다고 가정해보겠습니다. 이를 실패로 간주해야 할까요?
현재 동호회 조회와 조회수 쓰기 작업이 @Transactional로 묶여 있기 때문에 하나가 실패하면 나머지 작업도 실패한 것으로 간주됩니다.<br>조회수를 올리는데 실패했다고 동호회 조회 API가 에러를 응답해야 할까요?</p>
<p>앞서 이야기했듯이, 저희 서비스의 경우 <strong>조회수가 100% 정확하지 않더라도 사용자 경험에 큰 영향을 미치지 않습니다.</strong> 따라서 조회수가 실제와는 조금 다를 수도 있다는 점은 감수할 수 있다고 판단했습니다. </p>
<h2 id="📮-application-event-publisher">📮 Application Event Publisher</h2>
<p>스프링에서 제공하는 <strong>Event</strong> 기능을 사용해 조회수를 올리는 작업을 <strong>비동기</strong>적으로 처리해 위의 두 가지 문제점을 해결할 수 있습니다. </p>
<h3 id="1-event">1. Event</h3>
<p>이벤트와 관련된 데이터를 저장하는 Placeholder, 또는 DTO 객체를 아래와 같이 작성합니다. 동호회 조회 이벤트의 경우, 동호회 UUID를 필요로 합니다. </p>
<p>** 참고로, Event는 아래와 같이 일반 객체로 만들어도 무방합니다. Spring Framework 4.2 이전에는 이벤트 객체가 반드시 ApplicationEvent를 상속해야 했습니다. 4.2 이후부터 <code>publishEvent(Object event)</code> 메서드가 오버로드되어 일반 객체를 이벤트 객체로 사용할 수 있습니다.</p>
<pre><code class="language-java">@Getter
@RequiredArgsConstructor
public class ReadClubEvent {

    private final String clubToken;
}</code></pre>
<h3 id="2-listener">2. Listener</h3>
<p>리스너는 @EventListener 어노테이션을 붙이거나 ApplicationListener 인터페이스를 정의해 구현할 수 있습니다. 아래 예시에서는 간단하게 어노테이션을 붙여 Event Listener를 구현했습니다.</p>
<p>파라미터에 정의한 타입의 이벤트가 발생하면 @EventListener 어노테이션을 붙인 퍼블릭 메서드가 동작합니다.</p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class ReadClubEventHandler {

    private final ClubStatisticsService clubStatisticsService;

    @EventListener
    public void readClubEventListener(ReadClubEvent event) {
        clubStatisticsService.increaseVisitedClubCount(event.getClubToken());
    }
}</code></pre>
<p>이때 한 가지 기억해야 할 점은, 스프링은 default일 때 이벤트를 <strong>동기적(Synchronous)</strong>으로 처리합니다. </p>
<h3 id="3-publisher">3. Publisher</h3>
<p>이벤트와 이벤트 리스너가 준비되었으니, 이제 이벤트를 발행할 차례입니다. </p>
<h4 id="31-applicationeventpublisher">3.1. ApplicationEventPublisher</h4>
<p><code>ApplicationEventPublisher</code>라는 함수형 인터페이스를 사용해 이벤트를 발행할 수 있는데요, 아래와 같이 <code>publishEvent</code>라는 추상 메서드를 포함하고 있습니다.</p>
<pre><code class="language-java">@FunctionalInterface
public interface ApplicationEventPublisher {
    default void publishEvent(ApplicationEvent event) {
        this.publishEvent((Object)event);
    }

    void publishEvent(Object event);
}</code></pre>
<p><code>publishEvent</code> 메서드를 통해 이벤트가 발행되면, Spring은 Application Context에 등록된 리스너 중 해당 이벤트와 매칭되는 리스너를 찾아 호출합니다.</p>
<p>해당 함수형 인터페이스는 <code>ApplicationContext</code> 를 구현한 <code>AbstractApplicationContext</code> 클래스에 구현되어 있습니다. </p>
<pre><code class="language-java">public interface ApplicationContext extends ApplicationEventPublisher /*, ... 생략*/ {</code></pre>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/5b8cb2fb-7f51-49bb-88db-c3ab07ddfc16/image.png" alt=""></p>
<p>따라서 별도로 구현하지 않고, 간단하게 의존성을 주입받아 <code>publishEvent</code>메서드를 사용할 수 있습니다.</p>
<h4 id="32-코드에-적용">3.2. 코드에 적용</h4>
<p><code>ApplicationEventPublisher</code>에 대한 의존성을 추가하고, <code>publishEvent</code> 메서드를 호출해 이벤트를 발행합니다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class ClubFacade {
    private final ClubService clubService;
    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public ClubInfo readClub(String clubToken) {
        var clubInfo = clubService.readClub(clubToken);
        eventPublisher.publishEvent(new ReadClubEvent(clubToken));
        return clubInfo;
    }
    /* 
  * 생략
  */
}</code></pre>
<h2 id="👛-before--after">👛 Before &amp; After</h2>
<h3 id="before">Before</h3>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class ClubFacade {
    private final ClubService clubService;
    private final ClubStatisticsService clubStatisticsService;

    @Transactional
    public ClubInfo readClub(String clubToken) {
        var clubInfo = clubService.readClub(clubToken);
        clubStatisticsService.increaseClubVisitCount(clubToken);
        return clubInfo;
    }
    /* 
  * 생략
  */
}</code></pre>
<h3 id="after">After</h3>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class ClubFacade {
    private final ClubService clubService;
    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public ClubInfo readClub(String clubToken) {
        var clubInfo = clubService.readClub(clubToken);
        eventPublisher.publishEvent(new ReadClubEvent(clubToken));
        return clubInfo;
    }
    /* 
  * 생략
  */
}</code></pre>
<p>먼저, 더이상 <code>ClubStatisticsService</code>에 의존하고 있지 않습니다. 동호회 서비스와 동호회 통계 서비스 간의 <strong>결합도가 낮아졌습니다</strong>.</p>
<p>또한, 동호회 읽기 작업은 동호회 조회수를 올리는 쓰기 작업과 무관하게 동작합니다. <strong>동호회 조회수를 올릴 때 예외가 발생</strong>하더라도, <strong>동호회 읽기 API는 성공적으로 요청을 처리</strong>합니다. </p>
<h2 id="☔️-테스트-어라라">☔️ 테스트.. 어라라?!</h2>
<p>두 가지 테스트를 진행해보려고 합니다.</p>
<blockquote>
<p><strong>예상한 시나리오 1</strong>
이벤트 핸들러에서 예외가 발생하더라도, 동호회 조회 API는 정상 응답할 것이다.</p>
</blockquote>
<p><strong>동호회 조회수를 올릴 때 문제가 발생</strong>하더라도, <strong>동호회 조회는 성공해야 한다</strong>는 요구사항이 있습니다. 따라서 Event Listener 내부에 예외를 발생하는 코드를 작성했습니다. 이벤트를 처리하다가 예외가 발생해도, 동호회 조회를 하는 API는 성공해야 합니다. </p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class ReadClubEventHandler {

    private final ClubStatisticsService clubStatisticsService;

    @EventListener
    public void readClubEventListener(ReadClubEvent event) {
        clubStatisticsService.increaseVisitedClubCount(event.getClubToken());
        // 예외를 발생시킨다.
        throw new BadmintonException(ErrorCode.CLUB_NOT_EXIST);
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/a2f4cc37-9b61-401d-b223-5786903b373c/image.png" alt=""></p>
<p>그러나, 예상했던 것과는 다르게 에러를 응답하고 있습니다.</p>
<blockquote>
<p><strong>예상한 시나리오 2</strong>
@EventListener를 사용했기 때문에 트랜잭션 실패에도 불구하고 이벤트가 실행되어 조회수가 늘어나 있을 것이다.</p>
</blockquote>
<p>@TransactionalEventListener를 붙인 이벤트 리스너는 <strong>기본적으로 트랜잭션 커밋 후 실행</strong>됩니다. </p>
<pre><code class="language-java">@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)  // Default 옵션</code></pre>
<p>트랜잭션 커밋 이후 리스너가 실행되며, 만약 트랜잭션이 실패하면 리스너는 실행되지 않습니다. 즉, <strong>@Transactional 내 모든 작업이 성공해야 이벤트 리스너가 실행</strong>됩니다. </p>
<p>이 둘의 차이점을 느껴보고자 먼저 @EventListener를 사용해 테스트를 진행했습니다. 아래와 같이 이벤트를 발행한 다음 예외를 발생하는 코드를 추가했습니다.</p>
<pre><code class="language-java">@Transactional
    public ClubCreateInfo createClub(ClubCreateCommand createCommand, String memberToken) {
        ClubCreateInfo clubCreateInfo = clubService.createClub(createCommand);
        clubMemberService.clubMemberOwner(memberToken, clubCreateInfo);
        eventPublisher.publishEvent(new CreateClubEvent(clubCreateInfo));
        // 예외를 발생시킨다. 
        throw new BadmintonException(ErrorCode.CLUB_NOT_EXIST);
    }</code></pre>
<p>예상했던 것과 같이 트랜잭션이 종료되기 전이지만, 이벤트 리스너 내부의 코드로 진입하는 것을 확인했습니다. </p>
<p>다만, 최종적으로 <strong>롤백되어 조회수가 늘어나지 않았습니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/b00b4333-7333-4329-9118-6b908217c443/image.png" alt=""></p>
<p>(기존 조회수 110, API 호출 이후 조회수 여전히 110)</p>
<h2 id="️-async를-안-붙이면-spring은-이벤트를-동기적으로-처리">‼️ @Async를 안 붙이면 Spring은 이벤트를 동기적으로 처리</h2>
<p>EventHandler에 @Async 어노테이션을 붙이지 않아 조회수를 증가하는 로직이 <strong>동기적으로 처리</strong>되고 있었습니다. </p>
<p>스프링은 <strong>default 옵션에서 이벤트를 동기적으로 처리</strong>하며, 이때 이벤트 리스너는 <strong>동일한 스레드에서 이벤트를 처리</strong>합니다. 
즉, 동호회를 조회하고, 조회수를 올리는 두 가지 로직이 동일 스레드에서 실행됩니다.</p>
<p>따라서 <strong>동일 스레드 내에 예외가 발생</strong>했을 때 (1) 이를 실패로 간주해 <strong>에러를 응답</strong>하고, (2) <strong>조회수 쓰기 작업은 롤백</strong>된 것입니다.</p>
<p>아래는<code>SimpleApplicationEventMulticaster</code> 클래스의 코드 일부입니다. 이벤트가 발생하면 이를 적합한 리스너에게 전달하며, 비동기 처리를 위해 TaskExecutor를 사용합니다.</p>
<pre><code class="language-java">public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
        ResolvableType type = eventType != null ? eventType : ResolvableType.forInstance(event);
        Executor executor = this.getTaskExecutor();
        Iterator var5 = this.getApplicationListeners(event, type).iterator();

        while(true) {
            while(var5.hasNext()) {
                ApplicationListener&lt;?&gt; listener = (ApplicationListener)var5.next();
                /* Executor가 설정되어 있고 리스너가 비동기를 지원하면 */
                if (executor != null &amp;&amp; listener.supportsAsyncExecution()) {
                    try {
                        executor.execute(() -&gt; {
                        /* 이벤트를 리스너에게 전달 */
                            this.invokeListener(listener, event);
                        });
                    } catch (RejectedExecutionException var8) {
                        this.invokeListener(listener, event);
                    }
                } else {
                /* 설정된 Executor가 없다면 동기적으로 실행된다 */
                    this.invokeListener(listener, event);
                }
            }

            return;
        }
    }
        /* 생략 */</code></pre>
<p>Executor가 없으면, 이벤트는 <strong>동기적</strong>으로 실행됩니다. 즉, <strong>이벤트 리스너가 현재 스레드에서 호출</strong>됩니다. </p>
<p>반면, Executor가 설정되어 있다면, 이벤트는 <strong>비동기적</strong>으로 실행됩니다. 즉, <strong>이벤트 리스너가 별도의 스레드에서 호출</strong>됩니다.</p>
<p>따라서, @Async 어노테이션을 붙여야 이벤트가 비동기적으로 처리됩니다. 이때 <strong>리스너는 이벤트를 별개의 스레드에서 처리</strong>합니다.</p>
<p>지금은 이벤트가 <strong>동기적</strong>으로 실행되고 있고 @Transactional로 묶여 있기 때문에 <strong>트랜잭션에 참여한 스레드에서 예외가 발생할 때 전부 롤백</strong>합니다. 그래서 조회수 증가 로직이 실행되었음에도 불구하고, 롤백이 된 것이죠. </p>
<h2 id="🧵-동기적으로-실행-시-이벤트-리스너가-동일-스레드에서-동작">🧵 동기적으로 실행 시 이벤트 리스너가 동일 스레드에서 동작</h2>
<p>앞서 말했듯이, <strong>이벤트 리스너가 동일 스레드</strong>에서 돌고 있으며, <strong>동일 스레드 내에서 예외가 발생했으니 롤백</strong>시키고 있습니다.</p>
<p>동기적으로 이벤트를 실행할 때 정말 동일 스레드인지 직접 확인해보도록 하겠습니다.</p>
<p>먼저 아래와 같이 @Async 어노테이션 없이 기존처럼 이벤트를 동기적으로 처리해보도록 하겠습니다. </p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class ReadClubEventHandler {

    private final ClubStatisticsService clubStatisticsService;

    @EventListener
    public void readClubEventListener(ReadClubEvent event) {
        System.out.println(&quot;**** 이벤트 리스너 스레드 이름: &quot; + Thread.currentThread().getName());
        clubStatisticsService.increaseVisitedClubCount(event.getClubToken());
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/a1c98e35-e63e-439e-901e-d2ce0e655791/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/20c18fb0-2555-4347-9a0e-45281fe3d8fe/image.png" alt=""></p>
<p>이벤트 리스너가 동호회를 조회하는 스레드와 동일한 스레드임을 확인했습니다.</p>
<p>이제 이벤트 리스너에 @Async 어노테이션을 붙여 이벤트를 비동기적으로 처리해보도록 하겠습니다. </p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class ReadClubEventHandler {

    private final ClubStatisticsService clubStatisticsService;

    @Async
    @EventListener
    public void readClubEventListener(ReadClubEvent event) {
        System.out.println(&quot;**** 이벤트 리스너 스레드 이름: &quot; + Thread.currentThread().getName());
        clubStatisticsService.increaseVisitedClubCount(event.getClubToken());
    }
}</code></pre>
<pre><code class="language-java">@SpringBootApplication
@EnableAsync
public class BadmintonApplication {
    public static void main(String[] args) {
        SpringApplication.run(BadmintonApplication.class, args);
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/fe5b1808-5d64-4e13-b44b-bc655e479135/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/36558737-e75b-42ae-b70d-64d33b731da8/image.png" alt=""></p>
<p><strong>이벤트 리스너가 별개의 스레드에서 호출</strong>되는 것을 확인할 수 있었습니다. </p>
<p>이제 이벤트가 <strong>비동기적</strong>으로 처리되고 있으니, 앞서 예상했던 두 가지 시나리오를 다시 확인해보도록 하겠습니다.</p>
<blockquote>
<p><strong>예상한 시나리오 1</strong>
이벤트 핸들러에서 예외가 발생하더라도, 동호회 조회 API는 정상 응답할 것이다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/700e8815-7769-496a-b3e7-42ce26f799b8/image.png" alt=""></p>
<p>동호회 조회수를 올리는 로직에서 예외가 발생하지만, 이는 <strong>비동기적으로 처리되며 별도의 스레드에서 동작</strong>합니다. 따라서 동호회를 조회하는 API는 이벤트 리스너의 동작과는 무관하게 성공합니다.</p>
<blockquote>
<p><strong>예상한 시나리오 2</strong>
@EventListener를 사용했기 때문에 트랜잭션 실패에도 불구하고 이벤트가 실행되어 조회수가 늘어나 있을 것이다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/2a5daae2-2169-41b1-a898-9ccac7a711d4/image.png" alt=""></p>
<p>@TransactionalEventListener가 아닌 @EventListener를 사용했기 때문에, <strong>트랜잭션이 실패했음에도 불구하고 이벤트가 처리</strong>되는 것을 확인했습니다. (기존에 조회수가 110이었고, 현재 1 증가한 상황)</p>
<h2 id="🛤️-transactionaleventlistener">🛤️ @TransactionalEventListener</h2>
<p>위에서 @EventListener를 사용하면, 트랜잭션 실패 시에도 이와 무관하게 이벤트가 처리되는 것을 확인할 수 있었습니다. </p>
<p>다만, <strong>조회수 읽기에 실패하면 조회수 또한 올라가지 않아야 한다</strong>는 요구사항이 있습니다. 이를 보장하려면, @TransactionalEventListener를 사용할 수 있습니다!</p>
<p>@EventListener 대신 @TransactionalEventListener를 사용하면 <strong>트랜잭션이 커밋되고 난 이후</strong>에 이벤트가 처리됩니다.</p>
<p>트랜잭션을 커밋하기 전에 예외를 발생시켜 이벤트가 정말 실행되지 않는지 확인해보도록 하겠습니다. </p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class ReadClubEventHandler {

    private final ClubStatisticsService clubStatisticsService;

    @Async
    @TransactionalEventListener
    public void readClubEventListener(ReadClubEvent event) {
        System.out.println(&quot;**** 이벤트 리스너 스레드 이름: &quot; + Thread.currentThread().getName());
        clubStatisticsService.increaseVisitedClubCount(event.getClubToken());
    }
}</code></pre>
<p>동호회 조회와 @Transactional로 함께 묶여 있는 블록 내에 예외를 던지는 코드를 작성했습니다. </p>
<pre><code class="language-java">@Transactional
    public ClubDetailsInfo readClub(String clubToken) {
        clubService.readClub(clubToken);
        eventPublisher.publishEvent(new ReadClubEvent(clubToken));
        throw new BadmintonException(ErrorCode.CLUB_NOT_EXIST);
    }</code></pre>
<p>위에서 @EventListener를 사용했을 때는, <strong>트랜잭션 커밋 여부와 상관없이 이벤트가 처리</strong>되었습니다.  </p>
<p>반면, @TransactionalEventListener의 경우, 이벤트가 발행은 되지만, 트랜잭션 실패 시 이벤트가 처리되지 않는 것을 확인할 수 있었습니다. </p>
<p>이벤트가 발행은 되었으나, <strong>트랜잭션이 실패했기 때문에 이벤트를 처리하지 않았습니다.</strong> 따라서 아래와 같이 조회수는 여전히 110입니다. 
<img src="https://velog.velcdn.com/images/carol_ly/post/664398f8-bdcc-4074-8949-09f0ccca460a/image.png" alt=""></p>
<p>이제 동호회 조회에 실패하면, 동호회 조회수는 올라가지 않습니다! 😁</p>
<h2 id="👀-헷갈렸던-부분">👀 헷갈렸던 부분</h2>
<p>처음에는 @TransactionalEventListener 사용 시 트랜잭션이 실패했을 때 이벤트 처리가 롤백되는 것이라고 이해했습니다. 이는 틀린 말이며, 트랜잭션이 실패하면 이벤트 리스너가 아예 이벤트를 처리하지 않습니다. (혹시 저처럼 헷갈리신 분이 계실까봐 . . . 😅)</p>
<h2 id="🔮-개선할-점">🔮 개선할 점</h2>
<p>지금은 동호회 GET 요청이 들어왔을 때, 동일 사용자인지 등을 확인하지 않고 조회수를 올리고 있습니다. <strong>동일 사용자</strong>에 대해서는 조회수를 1만 올리도록 제한하거나, <strong>세션 정보</strong>를 활용해 개선할 예정입니다. </p>
<p>또한 <strong>레디스</strong>를 활용해 조회가 있을 때마다 DB에 업데이트를 하지 않고, <strong>특정 시간이 지나거나 어느 정도 데이터가 쌓였을 때 DB에 업데이트</strong>를 하도록 수정하려고 합니다. 서비스에서 <strong>조회수의 실시간성</strong>이 아주 중요하다고 판단하지 않았기 때문입니다.</p>
<h2 id="🎄-마치며">🎄 마치며</h2>
<p>조회수를 비동기적으로 올리기 위해 Event Publisher를 공부하며 스레드에 대해서도 추가적인 공부를 할 수 있었습니다. 이제 조회수를 올리는 작업은 동호회 조회와는 별개의 스레드에서 비동기적으로 처리됩니다!</p>
<p>다만, 스프링에서 지원하는 <strong>Event는 동일 프로세스 내에서만 동작</strong>합니다. 스프링 이벤트 시스템이 <strong>ApplicationContext를</strong> 기반으로 동작하기 때문인데, 이러한 이유로 <strong>분산 환경에서는 별도의 메시징 시스템이 필요</strong>합니다. 카프카, RabbitMQ 등이 있으며 아직 공부해보지 못해서 추후에 적용해볼 계획입니다!</p>
<p>모두 해피 연말 보내세요 😁</p>
<h2 id="📚-참고-자료">📚 참고 자료</h2>
<blockquote>
<p><a href="https://www.baeldung.com/spring-events">https://www.baeldung.com/spring-events</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[잘 만든 커스텀 예외]]></title>
            <link>https://velog.io/@carol_ly/%EC%9E%98-%EB%A7%8C%EB%93%A0-%EC%BB%A4%EC%8A%A4%ED%85%80-%EC%98%88%EC%99%B8</link>
            <guid>https://velog.io/@carol_ly/%EC%9E%98-%EB%A7%8C%EB%93%A0-%EC%BB%A4%EC%8A%A4%ED%85%80-%EC%98%88%EC%99%B8</guid>
            <pubDate>Tue, 05 Nov 2024 15:31:29 GMT</pubDate>
            <description><![CDATA[<h1 id="📗-시작하며">📗 시작하며</h1>
<p>이 글은 <strong>커스텀 예외 처리</strong>를 어떻게 할지 고민하며 점진적으로 개선해나가는 과정을 담고 있습니다. <strong>처음 커스텀 예외 클래스를 작성</strong>하시는 분부터, <strong>더 좋은 예외 처리</strong>를 위해 고민하고 계시는 분들 모두에게 도움이 되는 글을 작성하고자 했습니다! </p>
<h2 id="🙋♂️커스텀-예외를-왜-만들어야-하나요">🙋‍♂️커스텀 예외를 왜 만들어야 하나요?</h2>
<p>로또 번호는 <code>1</code>~<code>45</code> 사이의 숫자입니다. <code>validateLottoNumber()</code> 메서드는 이를 확인하고 범위를 벗어난 숫자에 대해 <code>IllegalArgumentException</code>을 던집니다. </p>
<pre><code class="language-java">public class LottoNumber implements Comparable&lt;LottoNumber&gt; {

    private final int lottoNumber;

    public LottoNumber(int lottoNumber) {
        validateLottoNumber(lottoNumber);
        this.lottoNumber = lottoNumber;
    }

    private void validateLottoNumber(int lottoNumber) {
        if(lottoNumber &lt; 1 || lottoNumber &gt; 45) {
            throw new IllegalArgumentException(lottoNumber);
        }
    }
}</code></pre>
<p>메서드 파라미터에 잘못된 값이 입력되면 아래와 같이 에러가 발생합니다. </p>
<pre><code>Exception in thread &quot;main&quot; java.lang.IllegalArgumentException
    at lotto.domain.lotto.LottoNumber.validateLottoNumber(LottoNumber.java:18)
    at lotto.domain.lotto.LottoNumber.&lt;init&gt;(LottoNumber.java:12)
    /* 생략 */</code></pre><blockquote>
<p>❓🙋‍이러한 에러 처리 방식에는 어떠한 <strong>문제점</strong>들이 있을까요? </p>
</blockquote>
<ol>
<li><p>먼저, *<em>예외의 원인을 분명하게 파악하기 어렵습니다. *</em>
에러 메시지를 통해 validateLottoNumber 메서드에 문제가 발생했다는 것은 알 수 있습니다. 하지만 정확히 해당 메서드에 무슨 문제가 발생했는지 알 수 없습니다. </p>
</li>
<li><p>또한, <strong>어떤 값이 문제를 일으켰는지 알기 어렵습니다.</strong> 개발자는 로그를 통해 에러의 상세 내용을 확인할 수 있어야 합니다. 사용자가 잘못된 값을 입력했다면, 이를 로그로 남겨 개발자가 확인할 수 있어야 합니다. </p>
</li>
</ol>
<p>단순히 <code>IllegalArgumentException</code>을 던지고, 아무런 메시지도 남기지 않는다면 예외가 발생할 때마다 디버깅을 반복적으로 해야겠죠!</p>
<p>위와 같은 이유로, <strong>커스텀 예외</strong>를 만들고 <strong>예외가 발생한 원인을 구체적</strong>으로 남기는 것을 선호합니다.</p>
<br>

<h2 id="🌱-어떻게-만드나요">🌱 어떻게 만드나요?</h2>
<p>아주 간단합니다! 이번 3주차 미션의 요구사항에는 IllegalArgumentException을 던지는 것으로 되어 있었습니다. 그래서 CustomException 클래스를 만들고, IllegalArgumentException을 상속받도록 구현하면 됩니다.</p>
<pre><code class="language-java">public class InvalidLottoNumberException extends IllegalArgumentException{

    private static final String ERROR_MESSAGE = &quot;[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.&quot;;

    public InvalidLottoNumberException() {
        super(ERROR_MESSAGE);
    }
}</code></pre>
<p>이제 잘못된 값이 전달되면 아래와 같이 직접 정의한 메시지를 함께 확인할 수 있습니다.</p>
<pre><code>Exception in thread &quot;main&quot; lotto.common.exception.InvalidLottoNumberException: [ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.
    at lotto.domain.lotto.LottoNumber.validateLottoNumber(LottoNumber.java:18)
    at lotto.domain.lotto.LottoNumber.&lt;init&gt;(LottoNumber.java:12)
    at
    /* 생략 */</code></pre><blockquote>
<p>❓🙋‍ super()는 왜 하는거에요?</p>
</blockquote>
<p><code>super(ERROR_MESSAGE)</code>를 통해 부모 생성자가 호출됩니다. IllegalArgumentException를 잠깐 살펴보면 아래와 같은 생성자가 있습니다.</p>
<pre><code class="language-java">public IllegalArgumentException(String s) {
    super(s);
}</code></pre>
<p>여기서도 부모 생성자를 호출하는데요, 이를 타고 들어가다 보면 최상위 Throwable이 나옵니다.</p>
<pre><code class="language-java">public Throwable(String message) {
    fillInStackTrace();
    detailMessage = message;
}</code></pre>
<p><code>fillInStackTrace()</code>는 예외가 발생한 시점의 스택 트레이스를 캡처합니다. 이를 통해 예외가 발생한 위치와 호출 경로를 추적할 수 있습니다. </p>
<p><code>detailMessage</code>는 <code>getMessage()</code> 메서드를 통해 외부에서 조회할 수 있습니다. 따라서, InvalidLottoNumberException에서 전달한 메시지는 최상위 Throwable 클래스의 <code>detailMessage</code> 필드에 저장됩니다.</p>
<br>


<h1 id="🐉-커스텀-예외-잘-만들기">🐉 커스텀 예외 잘 만들기</h1>
<p>이제부터 앞서 만든 커스텀 예외를 하나씩 고쳐보도록 하겠습니다!</p>
<h2 id="1️⃣-동적-메시지를-제공한다">1️⃣ 동적 메시지를 제공한다.</h2>
<pre><code>&quot;[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.&quot;</code></pre><p>해당 에러 메시지로는 <strong>어떤 로또 번호가 예외를 발생시켰는지 알기 어렵</strong>습니다.
<code>-1</code>이었을 수도 있고, <code>Integer.MAX_VALUE</code> 정말 큰 수였을 수도 있죠. </p>
<p>개발자는 이슈가 생겼을 때 <strong>로그로 문제를 쉽게 파악</strong>할 수 있어야 합니다. <strong>어떤 파라미터가 문제를 일으킨건지 빠르게 확인</strong>할 수 있어야 하는데요.</p>
<p>이때, <strong>동적 메시지를 활용</strong>할 수 있습니다. 즉, <strong>예외를 발생시킨 값</strong>을 메시지에 함께 제공하면 예외를 발생시킨 상황에 대한 파악을 더 빠르게 할 수 있습니다.</p>
<pre><code class="language-java">public class InvalidLottoNumberException extends IllegalArgumentException{

private static final String ERROR_MESSAGE = &quot;[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.&quot;;

    public InvalidLottoNumberException(int invalidLottoNumber) {
        super(ERROR_MESSAGE + &quot;(잘못된 로또 번호 : &quot; + invalidLottoNumber + &quot;)&quot;);
    }
}</code></pre>
<p>커스텀 예외 클래스의 생성자에서 <code>int</code> 타입의 <code>invalidLottoNumber</code>를 전달받고 있습니다. 예외를 던지는 쪽에서는 아래와 같이 값을 전달합니다.</p>
<pre><code class="language-java">private void validateLottoNumber(int lottoNumber) {
    if(lottoNumber &lt; 1 || lottoNumber &gt; 45) {
        throw new InvalidLottoNumberException(lottoNumber);
    }
}</code></pre>
<p><strong>예외를 발생시킨 값</strong>을 에러 메시지를 통해 확인하고, <strong>구체적인 상황을 파악</strong>하기가 쉬워졌습니다!</p>
<pre><code>[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다. (잘못된 로또 번호 : 90)</code></pre><br>

<h2 id="2️⃣-예외-체이닝을-걸어준다">2️⃣ 예외 체이닝을 걸어준다.</h2>
<p>예외 클래스를 만들땐 <strong>꼭 기존 예외랑 체이닝</strong>을 시켜줘야 합니다.</p>
<pre><code class="language-java">public LottoNumber readBonusNumber() {
    int bonusNumber;
    String input = Console.readLine();
    try {
        bonusNumber = Integer.parseInt(input);
    } catch (NumberFormatException e) {
        throw new InvalidLottoNumberException(input);
    }
    return new LottoNumber(bonusNumber);
}</code></pre>
<p>보너스 번호를 사용자에게 입력받고, 숫자가 아니라면 InvalidLottoNumberException을 던지고 있습니다. <code>String</code> 변수를 <code>int</code> 타입으로 변환하는 과정에서 숫자가 아니라면 NumberFormatException이 발생합니다. 이를 <code>try-catch</code>로 잡아 커스텀 예외를 다시 던지고 있습니다. </p>
<p>이제 잘못된 보너스 번호를 입력해보겠습니다.</p>
<pre><code>Exception in thread &quot;main&quot; lotto.common.exception.InvalidLottoNumberException: [ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다. (잘못된 로또 번호 : invalid)
    at lotto.interfaces.input.InputHandler.readBonusNumber(InputHandler.java:53)
    at lotto.interfaces.lotto.LottoController.getWinningLotto(LottoController.java:62)
    at lotto.interfaces.lotto.LottoController.lottoGameStart(LottoController.java:33)
    at lotto.Application.main(Application.java:10)</code></pre><p>보시다시피 <strong>스택 트레이스에 NumberFormatException에 대한 정보가 어디에도 없습니다.</strong> 원천 예외가 있을 때는, 이를 cause에 담아야 스택 트레이스에 남습니다. 최상위 Throwable에서는 아래와 같이 cause를 받고 있습니다. </p>
<pre><code class="language-java">public Throwable(String message, Throwable cause) {
    fillInStackTrace();
    detailMessage = message;
    this.cause = cause;
}</code></pre>
<p>여기에 <strong>cause를 전달</strong>하기 위해 커스텀 예외 클래스를 아래와 같이 수정할 수 있습니다. </p>
<pre><code class="language-java">public class InvalidLottoNumberException extends IllegalArgumentException{

    private static final String ERROR_MESSAGE = &quot;[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.&quot;;

    /* 생략 */

    // 1번: 원천 예외가 없을 때
    public InvalidLottoNumberException(String invalidLottoNumber) {
        super(ERROR_MESSAGE + &quot;(잘못된 로또 번호 : &quot; + invalidLottoNumber+ &quot;)&quot;);
    }

    // 2번: 원천 예외가 있을 때
    public InvalidLottoNumberException(String invalidLottoNumber, Exception e) {
        super(ERROR_MESSAGE + &quot;(잘못된 로또 번호 : &quot; + invalidLottoNumber+ &quot;)&quot;, e);
    }
}</code></pre>
<p>원천 예외를 받아, 이를 부모 생성자로 넘기고 있습니다. </p>
<p><strong>원천 예외가 없을 때</strong>는 위쪽에 있는 생성자가 호출되고, <strong>NumberFormatException와 같이 원천 예외가 있을 때</strong>는 아래쪽에 있는 생성자가 호출됩니다.</p>
<p>따라서 커스텀 예외 클래스를 만들 때 위와 같이 <strong>두 가지 생성자를 모두 만들어야</strong> 합니다.</p>
<p>예외를 던지는 쪽에서는  아래와 같이 Exception을 함께 전달해주면 됩니다.</p>
<pre><code class="language-java">public LottoNumber readBonusNumber() {
    int bonusNumber;
    String input = Console.readLine();
    try {
        bonusNumber = Integer.parseInt(input);
    } catch (NumberFormatException e) {
        throw new InvalidLottoNumberException(input, e);
    }
    return new LottoNumber(bonusNumber);
}</code></pre>
<p>이제 잘못된 보너스 번호 입력 시 아래와 같이 NumberFormatException도 함께 확인할 수 있습니다. </p>
<pre><code>Exception in thread &quot;main&quot; lotto.common.exception.InvalidLottoNumberException: [ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.(잘못된 로또 번호 : invalid)
    at lotto.interfaces.input.InputHandler.readBonusNumber(InputHandler.java:53)
    at lotto.interfaces.lotto.LottoController.getWinningLotto(LottoController.java:62)
    at lotto.interfaces.lotto.LottoController.lottoGameStart(LottoController.java:33)
    at lotto.Application.main(Application.java:10)
Caused by: java.lang.NumberFormatException: For input string: &quot;invalid&quot;
    at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
    at java.base/java.lang.Integer.parseInt(Integer.java:662)
    at java.base/java.lang.Integer.parseInt(Integer.java:778)
    at lotto.interfaces.input.InputHandler.readBonusNumber(InputHandler.java:51)
    ... 3 more</code></pre><p><strong>예외를 체이닝</strong>하면 <strong>예외의 원래 원인에 대한 자세한 정보를 제공</strong>할 수 있으며, 
<strong>오류 발생 지점을 더 명확하게 파악</strong>하는데 필수적입니다.</p>
<br>

<h2 id="3️⃣-정의한-예외-vs-예기치-못한-예외-구분의-중요성">3️⃣ 정의한 예외 vs 예기치 못한 예외: 구분의 중요성</h2>
<p>개발을 하다보면, 종종 예외 상황을 마주하게 됩니다. 이때 커스텀 예외를 하나씩 만들며 <strong>예상치 못한 예외</strong>들을 줄여나가는데요! <strong>이 둘을 구분</strong>하기 위해서 <strong>커스텀 예외가 상속받는 공통적인 예외 클래스</strong>를 추가적으로 하나 더 선언합니다.</p>
<p>바로 코드로 확인해보겠습니다!</p>
<pre><code class="language-java">public class LottoException extends IllegalArgumentException {

    private static final String ERROR_MESSAGE_HEADER = &quot;[ERROR] &quot;;
    private final String errorMessage;

    public LottoException(String message) {
        super(ERROR_MESSAGE_HEADER + message);
        this.errorMessage = ERROR_MESSAGE_HEADER + message;
    }

    public LottoException(String message, Exception e) {
        super(ERROR_MESSAGE_HEADER + message, e);
        this.errorMessage = ERROR_MESSAGE_HEADER + message;
    }
}</code></pre>
<p>요구사항에 맞춰 IllegalArgumentException을 상속받은 LottoException 클래스를 하나 만들었습니다. 이는 앞으로 정의할 <strong>모든 커스텀 예외 클래스들이 상속받을 중추적인 예외 클래스</strong>인데요! </p>
<p>각 커스텀 예외에서 공통적으로 필요한 errorMessage를 갖고 있습니다. 또, <code>[ERROR]</code>를 메시지에 추가해주는 작업도 이쪽에서 수행합니다. </p>
<p>그럼, 다시 InvalidLottoNumberException을 수정해보겠습니다.</p>
<pre><code class="language-java">public class InvalidLottoNumberException extends LottoException{

    private static final String ERROR_MESSAGE = &quot;로또 번호는 1부터 45 사이의 숫자여야 합니다.&quot;;

    public InvalidLottoNumberException(int invalidLottoNumber) {
        super(ERROR_MESSAGE + &quot;(잘못된 로또 번호 : &quot; + invalidLottoNumber+ &quot;)&quot;);
    }

    public InvalidLottoNumberException(int invalidLottoNumber, Exception e) {
        super(ERROR_MESSAGE + &quot;(잘못된 로또 번호 : &quot; + invalidLottoNumber+ &quot;)&quot;, e);
    }

    public InvalidLottoNumberException(String invalidLottoNumber) {
        super(ERROR_MESSAGE + &quot;(잘못된 로또 번호 : &quot; + invalidLottoNumber+ &quot;)&quot;);
    }

    public InvalidLottoNumberException(String invalidLottoNumber, Exception e) {
        super(ERROR_MESSAGE + &quot;(잘못된 로또 번호 : &quot; + invalidLottoNumber+ &quot;)&quot;, e);
    }
}</code></pre>
<p>커스텀 예외 클래스가 상속받는 부모 클래스가 IllegalArgumentException에서 LottoException으로 변경되었습니다!</p>
<p>이렇게 했을 때의 장점은, <strong>개발자가 정의한 예외와 그렇지 못한 예외를 구분할 수 있다</strong>는 점인데요!</p>
<p>예외를 처리하는 곳을 한 번 살펴보겠습니다.</p>
<pre><code class="language-java">private LottoMoney getLottoMoney() {
    while (true) {
        try {
            return inputHandler.readPurchaseAmount();
        } catch (LottoException e) {
            System.out.println(e.getMessage());
        } catch (Exception e) {
            System.out.println(&quot;예상치 못한 예외가 발생했습니다.&quot;);
    }
}</code></pre>
<p>먼저 try-catch 블록에서 LottoException을 잡아 이를 처리합니다. 만약 LottoException이 아닌 다른 예외가 발생할 경우에는 하위 catch 블록이 실행되어 별도의 메시지를 출력하도록 했습니다. </p>
<blockquote>
<p>❓🙋‍♂️ 이렇게 했을 때 <strong>장점</strong>이 무엇인가요?</p>
</blockquote>
<h3 id="😊-개발자가-정의된-예외와-예상치-못한-예외를-분리하여-처리할-수-있다">😊 <strong>개발자가 정의된 예외와 예상치 못한 예외</strong>를 <strong>분리하여 처리</strong>할 수 있다.</h3>
<pre><code class="language-java">private LottoMoney getLottoMoney() {
    while (true) {
        try {
            return inputHandler.readPurchaseAmount();
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        } 
    }
}</code></pre>
<p>위의 코드에서는 <strong>개발자가 예상할 수 있는 예외</strong>(커스텀 예외)와 <strong>예상하지 못한 예외</strong>가 <strong>한곳에서 함께 처리</strong>되고 있습니다.</p>
<p>개발자가 미리 정의한 커스텀 예외는 특정 에러 메시지를 출력하고, 사용자로부터 다시 입력을 받도록 설계되어 있습니다. 그러나 <strong>예상치 못한 예외에 대해서는 다른 방식의 처리가 필요</strong>할 수 있습니다. 예를 들어, 에러 메시지를 띄운 후 시스템을 종료하거나, 별도의 에러 처리 로직을 실행할 수도 있습니다. </p>
<p>이럴 때 중추적인 예외를 만들어 처리하게 만들면, <strong>개발자가 정의된 예외와 예상치 못한 예외</strong>를 <strong>분리하여 처리</strong>할 수 있습니다.</p>
<h3 id="😊-예상치-못한-예외를-빠르게-파악하고-그-수를-줄일-수-있다">😊 예상치 못한 예외를 빠르게 파악하고, 그 수를 줄일 수 있다.</h3>
<p>시스템에서 발생할 수 있는 예외를 미리 예측하고 대처하기 위해 <strong>예상치 못한 예외를 줄이고, 커스텀 예외를 정의</strong>하게 되는데요,</p>
<p>이때 try-catch에서 잡히지 못하는 예외는 모두 예상치 못한 예외로 간주되어, <strong>미처 파악하지 못했던 예외 상황을 더 빠르게 구체화</strong>하는 데 도움을 줍니다.</p>
<p>이러한 이유로, LottoException 처럼 중추적인 예외를 하나 선언하고, 각 커스텀 예외 클래스가 이를 상속받도록 합니다. 예외를 처리하는 쪽에서 이 둘을 구분하여 핸들링해주면 얻을 수 있는 이점을 말씀드렸습니다. </p>
<h1 id="🌳-마치며">🌳 마치며</h1>
<h2 id="before--after">Before &amp; After</h2>
<h3 id="😈-before">😈 Before</h3>
<pre><code>Exception in thread &quot;main&quot; java.lang.IllegalArgumentException
    at lotto.domain.lotto.LottoNumber.validateLottoNumber(LottoNumber.java:18)
    at lotto.domain.lotto.LottoNumber.&lt;init&gt;(LottoNumber.java:12)
    /* 생략 */</code></pre><pre><code class="language-java">private void validateLottoNumber(int lottoNumber) {
    if(lottoNumber &lt; 1 || lottoNumber &gt; 45) {
        throw new IllegalArgumentException(lottoNumber);
    }
}</code></pre>
<h3 id="😁-after">😁 After</h3>
<pre><code>Exception in thread &quot;main&quot; lotto.common.exception.InvalidLottoNumberException: [ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다. (잘못된 로또 번호 : invalid)
    at lotto.interfaces.input.InputHandler.readBonusNumber(InputHandler.java:53)
    at lotto.interfaces.lotto.LottoController.getWinningLotto(LottoController.java:62)
    at lotto.interfaces.lotto.LottoController.lottoGameStart(LottoController.java:33)
    at lotto.Application.main(Application.java:10)
Caused by: java.lang.NumberFormatException: For input string: &quot;invalid&quot;
    at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
    at java.base/java.lang.Integer.parseInt(Integer.java:662)
    at java.base/java.lang.Integer.parseInt(Integer.java:778)
    at lotto.interfaces.input.InputHandler.readBonusNumber(InputHandler.java:51)
    ... 3 more</code></pre><pre><code class="language-java">public class InvalidLottoNumberException extends LottoException{

    private static final String ERROR_MESSAGE = &quot;[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.&quot;;

    public InvalidLottoNumberException(int invalidLottoNumber) {
        super(ERROR_MESSAGE + &quot;(잘못된 로또 번호 : &quot; + invalidLottoNumber+ &quot;)&quot;);
    }

    public InvalidLottoNumberException(int invalidLottoNumber, Exception e) {
        super(ERROR_MESSAGE + &quot;(잘못된 로또 번호 : &quot; + invalidLottoNumber+ &quot;)&quot;, e);
    }

    public InvalidLottoNumberException(String invalidLottoNumber) {
        super(ERROR_MESSAGE + &quot;(잘못된 로또 번호 : &quot; + invalidLottoNumber+ &quot;)&quot;);
    }

    public InvalidLottoNumberException(String invalidLottoNumber, Exception e) {
        super(ERROR_MESSAGE + &quot;(잘못된 로또 번호 : &quot; + invalidLottoNumber+ &quot;)&quot;, e);
    }
}</code></pre>
<blockquote>
</blockquote>
<p>🌙 <strong>동적 메시지</strong>를 제공한다. 
🌙 <strong>예외 체이닝</strong>을 걸어준다.
🌙 <strong>정의한 예외</strong>와 <strong>예상치 못한 예외</strong>를 <strong>구분</strong>한다.</p>
<p>이 세 가지를 적용해 커스텀 예외 클래스를 만들며 예외 처리 로직을 개선해보았습니다! <strong>이슈가 생겼을 때 문제를 파악</strong>하기 쉽게 하려면, <strong>어떤 파라미터가 문제</strong>를 일으킨 건지, <strong>원천 예외</strong>가 있다면 무엇인지를 메시지에 잘 적어주는 것이 필요하다고 생각합니다.</p>
<br>


<p>아직 글에 담지 못한 내용이 많은데, 이는 2탄에서 이어가도록 하겠습니다..! ✨</p>
<p><strong>2탄에서는, 아래와 같은 내용을 담아보려고 합니다!</strong></p>
<p>_1. 에러 코드, 에러 메시지 관리하기
2. 커스텀 예외 클래스를 어디까지 나눠야 하지? 너무 많아지는거 아니야..?
3. 스프링에서 예외 처리
4. 자바의 표준 예외 활용하기?!?!
5. 로그도 IO야 _</p>
<p>1탄 내용에 부족한 점이 있거나, 2탄에서 듣고 싶은 이야기가 있으시다면 댓글로 많이 남겨주세요! 😁💚</p>
<br>

<blockquote>
<p>참고자료</p>
</blockquote>
<p>예외 체이닝 관련
<a href="https://www.baeldung.com/java-chained-exceptions">https://www.baeldung.com/java-chained-exceptions</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프리코스 1주차 회고]]></title>
            <link>https://velog.io/@carol_ly/%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@carol_ly/%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 01 Nov 2024 13:12:35 GMT</pubDate>
            <description><![CDATA[<h2 id="🌱-시작하며">🌱 시작하며</h2>
<p>프리코스 1주차 미션을 수행하며 느낀 점들을 글로 남기고자 합니다. 48개의 커밋과 110개의 코드 리뷰를 주고 받으며 스스로 성장한 내용을 담고 있습니다. </p>
<br>

<h2 id="👀-내-목표는-무엇이었을까">👀 내 목표는 무엇이었을까?</h2>
<p>목표를 설정하고, 이를 달성했을 때의 성취감을 좋아합니다. <del>성취감 중독으로 굴러가는 개발 일상</del> 저에게는 성취감이 항상 다음 성장을 위한 동력이 되어 줍니다. 
그러기 위해서는 목표를 설정하고, 이를 달성하기 위한 작은 계획들을 세우고 있습니다. 또 짧게라도 회고를 매일 작성하고 있는데요! 노션에서 아래와 같이 일정을 관리하고 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/4b619455-f02b-4bff-a25e-0fc5364b5495/image.png" alt=""></p>
<p>지원서에 기재한 프리코스 목표는 아래와 같습니다.</p>
<ol>
<li>프레임워크 없이 객체 지향 설계를 처음부터 연습</li>
<li>테스트 코드 작성</li>
<li>좋은 영향력을 끼치기 위한 노력</li>
</ol>
<p>이를 위해 세운 세부적인 계획은 아래와 같습니다. </p>
<p>✅ SOLID 원칙 준수하기
✅ 다양한 디자인 패턴 코드에 녹여보기
✅ TDD 첫 도전
✅ 채널에 일주일에 적어도 2개씩 글 올리기
✅ 코드 리뷰 열심히 참여하기</p>
<p>목표를 세웠다면, 이를 의식적으로 챙기면서 열심히 하루하루 굴러갑니다. ( •_•) ○ ) ○ )</p>
<br>


<h2 id="📚-학습-과정">📚 학습 과정</h2>
<p>1주차를 어떻게 보냈는지 간단하게 되돌아보겠습니다. </p>
<h3 id="🌙-1일차">🌙 1일차</h3>
<p>기능 구현 목록을 정리하고, 문제 요구사항을 구체화했습니다. 간단한 요구사항이라고 생각했지만, 생각할 부분과 추가해야 하는 검증이 매우 많았습니다. 최대한 여러 가지 경우의 수를 생각해보며 어떻게 설계할지 고민했습니다.</p>
<h3 id="🌙-2일차">🌙 2일차</h3>
<p>TDD를 도전하기로 목표를 잡았습니다. Red - Green Cycle을 경험하며 작은 단위로 개발을 시작했습니다. 테스트 코드를 먼저 작성하고 개발을 하다 보니 자연스럽게 한 가지 일만 하는 메서드를 만들 수 있게 되었습니다. <del>벌써 첫 번째 계획 일부 달성!?</del> 
또 작은 커밋 단위를 지킬 수 있게 되었습니다. 기존에 프로젝트를 진행할 때 몇 백줄의 코드를 한 번에 커밋을 하던 행동을 깊게 반성하게 되었습니다.. 이때까지만 해도 TDD 최고!! 를 외쳤던 것 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/259fe108-63ac-4198-a5cd-1d2aee481a81/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/e0494b1d-1402-4ec6-a5eb-dac3c3144cfb/image.png" alt=""></p>
<h3 id="🌙-3일차">🌙 3일차</h3>
<p>어랏 !? 생각보다 할 일이 너무 많다는 것을 느꼈습니다. 벌써 3일차인데 20개 남짓의 체크 박스 중에 3개밖에 완료하지 못했습니다. 
이때부터 TDD의 스텝을 놓치기 시작했습니다. 큰 기능 단위로 구현을 했고 변경의 범위가 넓어지게 되었습니다.</p>
<h3 id="🌙-4-5일차">🌙 4, 5일차</h3>
<p>프로젝트 일정이 바빠 프리코스를 수행하지 못했습니다.</p>
<h3 id="🌙-6일차">🌙 6일차</h3>
<p>객체를 다시 정의했습니다. 요구사항을 다시 분석하며 Parser, Number, Separator, Calculator 등의 객체를 만들었고, 각각의 객체가 가질 상태와 행위, 의존성을 고민했습니다. </p>
<div align="center">
<img src="https://velog.velcdn.com/images/carol_ly/post/d475f39b-6ad9-428e-b527-58606faaa0d1/image.png" width="300px">
</div>

<p>Parser는 Separator를 기준으로 문자열을 자르고, Calculator는 Number들을 계산합니다. </p>
<p><del>객체는 반드시 처음에 정의해야지</del></p>
<h3 id="🌙-7일차">🌙 7일차</h3>
<p>기능 구현을 완료했고, 세부적인 리팩토링을 진행했습니다. 변수명과 메서드명이 적절한지, 객체가 올바른 책임을 갖고 있는지 등을 고민했습니다. </p>
<br>

<h2 id="📮-코드-리뷰">📮 코드 리뷰</h2>
<h3 id="내가-작성한-리뷰-📖">내가 작성한 리뷰 📖</h3>
<p>총 13분의 코드를 리뷰하며 좋은 리뷰를 할 수 있도록 노력했고, 결과적으로 제 성장에도 많은 도움을 얻었습니다.</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/72452706-a8ce-4c8e-a808-af5914a2518a/image.png" alt=""></p>
<p>코드 리뷰를 할 때, 아래의 내용을 생각하며 좋은 리뷰를 하기 위해 노력하고 있습니다. </p>
<p>✅ 추상적으로 말하지 않기. 
의견이 갈리는 것 같지만 개인적으로는, 구체적인 해결책까지 제시하는 걸 선호합니다. 서로의 의도를 이해하지 못해 두 번 질문이 오가는 것이 불편할 때가 있습니다. </p>
<p>✅ 상황에 적합한 리뷰를 하기. 
너무 현실성이 떨어지는 리뷰는 지양하고 있습니다. 리뷰를 하시는 분의 코드 스타일과 상황을 존중하는 리뷰를 해야 한다고 생각합니다.</p>
<p>✅ 이렇게 해주세요 ❌ 이렇게 하는 거에 대해 어떻게 생각하세요? ⭕
물론 상황마다 더 적합한 선택이 존재하지만, 기존에 내가 익숙한 방식이 틀릴 수도 있다는 것을 항상 전제에 두고 있습니다. 그렇기에 내가 작성한 코멘트가 무조건 정답이라고 강요하는 듯한 말투는 지양하고 있습니다. </p>
<p>이 외에도 세세하게 신경 쓰는 포인트가 많고, 앞으로도 좋은 리뷰를 하기 위해 많이 노력하려고 합니다!</p>
<h3 id="내가-받은-소중한-리뷰들-🎁">내가 받은 소중한 리뷰들 🎁</h3>
<p><a href="https://github.com/woowacourse-precourse/java-calculator-7/pull/1037">https://github.com/woowacourse-precourse/java-calculator-7/pull/1037</a></p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/e0b53970-4ed8-45d1-8202-addf7947d3aa/image.png" alt=""></p>
<p>감사하게도 총 11분께서 좋은 리뷰들을 남겨주셨습니다. 생각지도 못했던 부분을 짚어주시기도 했고, 더 좋은 방향성을 제시하거나 사소한 실수들을 바로잡아 주신 분도 계셨습니다. </p>
<p>시간을 들여 코드를 읽어주시고, 또 리뷰를 작성해주신 분들에 대한 기본적인 예의로 꼼꼼하게 읽어본 뒤, 댓글과 이모지를 열심히 달았습니다. 👀 🎉</p>
<p>많은 리뷰에서 공통된 몇 가지 사항에 있어 아래에 정리해보았습니다. </p>
<br>

<h2 id="✍️-코드리뷰를-통해-발견한-개선할-점들">✍️ 코드리뷰를 통해 발견한 개선할 점들</h2>
<h3 id="1️⃣-적절하지-않은-네이밍">1️⃣ 적절하지 않은 네이밍</h3>
<p><strong>네이밍이 굉장히 중요하다는 것</strong>을 이번 코드 리뷰를 통해 알게 되었습니다.</p>
<p>처음 제 코드를 보시는 분들은 클래스 이름을 통해 &quot;아, 여기 이러한 기능들이 있겠구나&quot; 생각하시고, 메서드 이름을 통해 &quot;이 메서드는 이러한 역할을 수행하겠지?&quot; 추측하시게 됩니다. </p>
<p>그런데 Calculator 라는 클래스에 들어가봤더니 정작 하는 기능은 문자열을 자르는 기능이라면 <strong>코드를 파악하는데 더 오랜 시간</strong>이 걸리겠죠. </p>
<p>이처럼 <strong>네이밍은 제 코드에 대한 첫인상</strong>이며, <strong>가독성을 위한 중요한 역할</strong>을 수행하기에 더 신경을 써야 겠다는 다짐을 하게 되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/5eb1291b-83b4-4d2e-ad96-a70e3c56b2fd/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/b14773c6-7e3d-4c9a-966d-e87bcce09324/image.png" alt=""></p>
<h3 id="2️⃣-변경에-대비해-상수-처리는-기본">2️⃣ 변경에 대비해 상수 처리는 기본!</h3>
<p>이번 과제에서 전반적으로 상수 처리가 미흡했던 것 같습니다. 특히 단일 문자로 구성된 Character 타입은 상수 처리를 할 생각을 하지 않고 하드코딩을 하게 되었습니다. </p>
<pre><code class="language-java">public Separators() {
        separators = new HashSet&lt;&gt;(Arrays.asList(&#39;:&#39;, &#39;,&#39;));</code></pre>
<p>기본 구분자는 언제든지 콜론(<code>:</code>)에서 쌍따옴표(<code>&quot;</code>) 등으로 변경이 될 가능성이 있기 때문에, 이를 상수 처리하는 것은 변경에 대비하는데 큰 도움이 됩니다. </p>
<p>작다고 무시하지 말고, 꼼꼼하게 상수 처리를 해야겠습니다!</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/aaf551d8-2a85-48a9-9135-a601e2a9fcd6/image.png" alt=""></p>
<h3 id="3️⃣-불필요한-주석은-지양하자">3️⃣ 불필요한 주석은 지양하자</h3>
<p>코드 리뷰를 받을 것에 대비해 메서드마다 주석을 달게 되었습니다. 보시는 분들의 편의성을 생각하고, 가독성을 높이기 위함이었는데요. </p>
<pre><code class="language-java">/*
* 리스트의 각 요소를 검증하고, Number 타입으로 변환하여 반환한다.
*/</code></pre>
<p>다만, 코드 <strong>가독성</strong>은 <strong>주석이 아니라 네이밍, 클린 코드 등을 통해 보장</strong>되어야 합니다. 가독성이 충분히 지켜지지 않을 것 같다는 걱정에 주석을 달게 되었는데, 코드만 읽어도 술술 읽혀야 하겠네요 ..! 정말 많은 노력이 필요한 부분 같습니다.</p>
<br>


<h2 id="💫-목표를-달성했는가">💫 목표를 달성했는가?</h2>
<p>✅ SOLID 원칙 준수하기
✅ 다양한 디자인 패턴 코드에 녹여보기
✅ TDD 첫 도전
✅ 채널에 일주일에 적어도 2개씩 글 올리기
✅ 코드 리뷰 열심히 참여하기</p>
<p>1주차 목표를 모두 달성했습니다! </p>
<p>함께 나누기 채널에 Commit Message 를 편하게 작성할 수 있는 plugin 관련 글, Intellij Ultimate 학생 계정으로 무료 이용하는 방법을 공유했습니다!</p>
<div align="center">
  <img src="https://velog.velcdn.com/images/carol_ly/post/9f0704b2-6f66-4179-9cd5-3fc7361fe1e7/image.png" width="400px">
</div>


<br>

<h2 id="✨-칭찬할-점">✨ 칭찬할 점</h2>
<p>1주차에 세운 목표를 모두 달성했습니다! 코드 리뷰도 기존에 생각했던 것보다 훨씬 많은 양을 주고 받으며 많은 깨달음과 배움을 얻었습니다. 커뮤니티의 글들을 열심히 읽어보고, 또 배움을 직접 공유하기도 하며, 함께 성장하는 기분을 느끼고 있습니다! 진정한 몰입의 길을 걷는 것 같아 뿌듯했습니다. </p>
<br>


<h2 id="🤔-아쉬운-점-그리고-다음-목표를-세우자">🤔 아쉬운 점, 그리고 다음 목표를 세우자!</h2>
<h3 id="1️⃣-tdd--더-잘하고-싶어">1️⃣ TDD .. 더 잘하고 싶어</h3>
<p>비록 중간에 <strong>큰 단위의 리팩토링</strong>을 하며 TDD의 스텝을 <del>꽈당</del> 놓치게 되었지만, 처음으로 TDD에 도전해본 결과 많은 장점을 느꼈습니다. 
리팩토링을 하더라도, 안전 장치가 존재해서 기존에 구현한 기능이 정상적으로 작동하는지 테스트할 수 있습니다. 또, 꼼꼼한 설계를 위한 지름길이 되어주기도 했습니다.
다만, 완전한 TDD를 하기에는 시간도, 실력도 부족함이 많다는 것을 느꼈습니다. TDD를 왜 실패했는지, 그 원인을 아래와 같이 정리했습니다. </p>
<p><strong>🌨️ 원인 1</strong>
public 으로 선언한 메서드의 <strong>접근 제어자를 private으로 바꾸며</strong> 기존에 작성한 테스트를 사용할 수 없게 되었습니다.  </p>
<p><strong>🌨️ 원인 2</strong>
TDD를 실천하려고 하니 <strong>평소 개발하던 속도의 3배</strong> 정도가 걸렸습니다. 처음이라 익숙하지 않아 시간이 오래 걸렸고, 답답한 마음에 테스트 코드 작성을 후순위로 미루게 되었습니다. </p>
<p><strong>🌨️ 원인 3</strong>
객체 지향 설계를 연습하자는 목표와 맞지 않게 <strong>초반에 객체에 대한 고민 없이 기능만 작성하여 개발</strong>을 시작했습니다. 중간에 추가적인 객체의 필요성을 느끼며 설계가 한 번 크게 변경되었습니다. 이때 <strong>큰 범위의 변경</strong>이 발생하며 TDD의 스텝을 놓쳤습니다. </p>
<h3 id="2️⃣-스트림-사용">2️⃣ 스트림 사용</h3>
<p><del>정말 정말</del> 부끄럽지만, 프로젝트를 할 때 스트림 사용에 익숙하지 않아 Gpt 에 많이 의존했던 것 같습니다. 스트림 사용에 더 자유로워질 수 있도록 많은 연습이 필요할 것 같습니다. 프리코스 미션을 수행하는 동안 Gpt를 사용하지 않고 있기 때문에, 스트림 사용에 더 자유로워질 수 있도록 많은 연습을 할 수 있을 것 같습니다!</p>
<h3 id="3️⃣-객체의-책임과-협력-관계에-대한-아쉬움">3️⃣ 객체의 책임과 협력 관계에 대한 아쉬움</h3>
<p>객체가 어떠한 책임을 갖고 어떠한 역할을 수행하는지에 대한 고민이 부족했던 것 같습니다. 또 <strong>객체를 정의하고, 어떤 식으로 협력하며 기능을 동작</strong>시킬지 고민해보는 시간이 부족했습니다. 
다음 주차 미션부터는, 이러한 부분을 많이 고민하며 객체 설계부터 더 꼼꼼히 해보려고 합니다!</p>
<br>

<h2 id="🐋-마치며">🐋 마치며</h2>
<p>정말 많은 고민과 연습, 그리고 깨달음이 있었던 1주차였습니다. 특히 코드 리뷰를 통해 많은 도움을 얻었습니다. 함께 성장하는 경험을 하게 해준 모든 분들께 감사합니다! 이제 시작이기 때문에, 앞으로의 성장이 무척이나 기대됩니다! 
읽어주셔서 감사합니다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[static 메서드는 언제 사용해야 할까?]]></title>
            <link>https://velog.io/@carol_ly/static-%EB%A9%94%EC%84%9C%EB%93%9C%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@carol_ly/static-%EB%A9%94%EC%84%9C%EB%93%9C%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Sat, 26 Oct 2024 06:45:27 GMT</pubDate>
            <description><![CDATA[<h2 id="📗-시작하며">📗 시작하며</h2>
<blockquote>
<p>❓🙋‍♂️ 이 객체는 <strong>상태</strong>를 가지지 않는데, <strong>static 메서드</strong>로 선언해보는 것은 어떨까요? </p>
</blockquote>
<p>우아한 테크코스 7기 프리코스를 수행하며 위와 같은 리뷰를 받게 되었습니다. 이를 보며 고민한 과정을 글로 남기고자 합니다.</p>
<p>아래의 Parser는 문자열에서 커스텀 구분자가 있는지 확인하여 구분자 리스트를 반환하고, 이를 기준으로 문자열을 자르는 등의 기능을 수행합니다. <strong>static 메서드</strong>가 아닌 <strong>인스턴스 메서드</strong>로 존재합니다.</p>
<pre><code class="language-java">public class Parser {

    private static final int SEPARATOR_LOCATION_INDEX = 2;

    public Separators getSeparatorList(boolean hasCustomSeparator, String inputStr) {
        Separators separators = new Separators();
        if (hasCustomSeparator) {
            separators.addCustomSeparator(inputStr.charAt(SEPARATOR_LOCATION_INDEX));
        }
        return separators;
    }

    public String[] splitStrBySeparator(Separators separators, String slicedStr) {
        String delimiter = separators.getSeparatorsRegex();
        String[] splitStr = slicedStr.split(delimiter);
        return Arrays.stream(splitStr)
                .filter(str -&gt; !str.trim().isEmpty())
                .toArray(String[]::new);
    }

    /* 생략 */
</code></pre>
<p>&lt;객체지향의 사실과 오해&gt;라는 책을 보면 객체는 <strong>상태</strong>와 <strong>행위</strong>로 이루어져 있고, 행위로 상태가 변경됩니다. </p>
<p>1주차 미션에서 Number, Separator와 같은 객체는 <strong>상태</strong>와 <strong>행위</strong>를 가지고 있습니다.</p>
<p>이와 반면, Parser는 내부 상태를 가지지 않으며 행위만 존재합니다. 이렇게 <strong>상태가 없는 객체</strong>의 메서드는 <strong>static</strong>으로 선언해야 할까요?</p>
<p>아래는 제가 받은 리뷰의 일부입니다.</p>
<pre><code>객체는 상태와 행위로 이루어져 있습니다.
그런데 Parser의 경우 상태 없이 행위만 정의하고 있습니다.
이렇게 상태가 없는 객체의 메서드는 static으로 선언하는게 어떨까요?
그럼 객체를 직접 생성할 필요도 없고, 기능만 수행하는 객체로 유지할 수 있을 것 같습니다. </code></pre><p>🙋‍♂️ 지금부터 이 질문에 대해 제가 고민을 한 과정을 공유해보려고 합니다! </p>
<br>


<h2 id="🌱--함수와-메서드의-차이">🌱  함수와 메서드의 차이</h2>
<p>먼저 함수와 메서드의 차이점에 대해 짚고 넘어가려고 합니다. 자바에서는 함수라는 표현을 잘 사용하지 않고 주로 <strong>메서드</strong>라고 부릅니다. 이 둘의 차이점은 뭐가 있을까요?</p>
<h3 id="1️⃣-함수">1️⃣ 함수</h3>
<p><strong>함수</strong>는 전달 받은 입력(<strong>파라미터</strong>)에 의해서만 결과가 바뀝니다.</p>
<pre><code class="language-java">public static String[] splitStrBySeparator(Separators separators, String slicedStr) {
    String delimiter = separators.getDelimiter();
    String[] splitStr = slicedStr.split(delimiter);
    return Arrays.stream(splitStr)
        .filter(str -&gt; !str.trim().isEmpty())
        .toArray(String[]::new);
}</code></pre>
<p>이는 구분자를 기준으로 문자열을 자르는 함수입니다. 파라미터로 받은 Separators, slicedStr에 따라 결과가 달라집니다. 즉, splitStrBySeparator라는 함수는 *<em>오직 파라미터에 의해서만 결과가 바뀝니다. *</em></p>
<p>함수는 객체 지향 언어인 Java 보다는 절차 지향 언어에서 주로 정의됩니다. 절차 지향 언어에서는 클래스를 사용하지 않고, 외부에서 <strong>함수를 전역적으로 선언</strong>할 수 있습니다. 이를 통해 함수는 특정 클래스에 속하지 않고 코드 <strong>어디에서든 접근 가능</strong>해집니다. </p>
<p>그러나 자바에서는 모든 함수(메서드)가 반드시 클래스에 속해야 하기 때문에 이를 클래스 내부에 <strong>static 메서드</strong>의 형태로 정의하게 됩니다. 이렇게 정의된 <strong>static 메서드는 함수</strong>라고 부를 수 있습니다.</p>
<h3 id="2️⃣-메서드">2️⃣ 메서드</h3>
<p>그럼 메서드는 어떨까요?
메서드는 파라미터에 의해서도 결과가 달라지지만, <strong>내부 상태</strong>에 의해서도 변화합니다. </p>
<pre><code class="language-java">public class Separators {

    private final Set&lt;Character&gt; separators;
    private String separatorsRegex;

    public Separators() {
        separators = new HashSet&lt;&gt;(Arrays.asList(&#39;:&#39;, &#39;,&#39;));
    }

    public void addCustomSeparator(Character customSeparator) {
        validateSeparator(customSeparator);
        separators.add(customSeparator);
    }

    public String getSeparatorsRegex() {
        if (separatorsRegex == null) {
            makeDelimiter();
        }
        return this.separatorsRegex;
    }

    private void makeDelimiter() {
        StringBuilder delimiter = new StringBuilder();
        delimiter.append(&quot;[&quot;);
        for (Character separator : this.separators) {
            if (separator == &#39;-&#39; || separator == &#39;[&#39; || separator == &#39;]&#39;) {
                delimiter.append(&quot;\\&quot;);
            }
            delimiter.append(separator);
        }
        delimiter.append(&quot;]&quot;);
        this.separatorsRegex = delimiter.toString();
    }

    private void validateSeparator(Character separator) {
        if (Character.isDigit(separator)) {
            throw new InvalidSeparatorException(separator);
        }
    }
}</code></pre>
<p>separators라는 <strong>내부 상태가 변경</strong>되면서 getSeparatorsRegex 메서드의 반환 값도 달라집니다. 이처럼 <strong>메서드는 객체 안에 정의된 함수</strong>이며, 파라미터 뿐 아니라 <strong>객체의 내부 상태</strong>에 의해서도 변경될 수 있습니다. </p>
<p>또한 메서드는 메서드가 속해있는 클래스의 인스턴스를 통해서만 호출될 수 있습니다. <strong>메서드</strong>가 해당 객체의 <strong>상태에 접근</strong>할 수 있고, <strong>객체의 상태를 변경</strong>할 수 있기 때문이죠. </p>
<blockquote>
<p>정리하자면, 함수는 파라미터에 의해서만 결과가 바뀌며, 메서드는 내부 상태에 의해서도 바뀝니다. 객체 안에 있는 함수를 메서드라고 부르며, 모든 메서드는 함수입니다. 반대로, 모든 함수가 메서드가 되는 것은 아닙니다. </p>
</blockquote>
<br>


<h2 id="📮-static-method는-객체의-장점을-활용할-수-없다">📮 static method는 객체의 장점을 활용할 수 없다.</h2>
<p>앞서 이야기한 흐름에 따르면, <strong>static 메서드</strong>는 <strong>함수</strong>와 같습니다. static 메서드는 오직 파라미터에 의해서만 결과가 바뀌며, 객체의 상태를 접근하거나 수정할 수 없습니다.</p>
<p>상태가 없는 객체의 메서드는 static method로 선언해도 인스턴스 메서드로 사용할 때와 큰 차이점은 없습니다.</p>
<p>다만, 클래스 내부의 모든 메서드를 <strong>static</strong>으로 선언했다면(주로 ~Util 등으로 이름 지은), 이는 <strong>객체라고 부를 수 없습니다</strong>. 따라서 객체의 장점을 이용할 수 없게 됩니다. </p>
<h3 id="1️⃣-다형성">1️⃣ 다형성</h3>
<p>제일 먼저, <strong>다형성</strong>을 활용할 수 없습니다. 예를 들어 Parser 라는 인터페이스를 두고, StringParser, NumberParser 처럼 다양한 구현체를 만든다고 가정해보겠습니다. </p>
<h4 id="🌙-parser">🌙 Parser</h4>
<pre><code class="language-java">public interface Parser {
    String[] parse(String input);
}</code></pre>
<h4 id="🌙-commaparser">🌙 CommaParser</h4>
<pre><code class="language-java">public class CommaParser implements Parser {
    @Override
    public String[] parse(String input) {
        return input.split(&quot;,&quot;);
    }
}</code></pre>
<h4 id="🌙-colonparser">🌙 ColonParser</h4>
<pre><code class="language-java">public class ColonParser implements Parser {
    @Override
    public String[] parse(String input) {
        return input.split(&quot;:&quot;);
    }
}</code></pre>
<p>Parser를 <strong>인터페이스</strong>로 선언하고, 내부에 문자열을 자르는 행위를 나타내는 parse 메서드를 두었습니다.</p>
<p>Parser를 구현한 <code>CommaParser</code>, <code>ColonParser</code>는 각각 쉼표(<code>,</code>), 콜론(<code>:</code>)을 기준으로 문자열을 잘라서 반환합니다. &quot;자른다&quot;는 <strong>행위를 재사용</strong>할 수 있게 되었습니다. </p>
<p>이와 같이 parser 메서드를 static으로 선언하지 않고, 인스턴스 메서드로 선언함으로써 *<em>Parser는 객체로 활용될 수 있습니다. *</em></p>
<br>


<p><strong>반면, parse 메서드를 static으로 선언한다면,</strong> 위처럼 *<em>다형성이라는 장점을 활용할 수 없게 됩니다. *</em></p>
<pre><code class="language-java">public class ParserStatic {

    public static String[] parse(String input) {
        return input.split(&quot;,&quot;);
    }
}</code></pre>
<p>ParserStatic 클래스는 <strong>객체라고 말하기 어렵습니다.</strong> 여러 가지 ParserStatic을 만들 수도 없고, parse라는 메서드를 재사용할 수도 없습니다. 앞의 예시처럼 쉼표(<code>,</code>), 콜론(<code>:</code>) 등 구분자를 기준으로 다양한 parse 메서드를 구현할 수 없어졌습니다.</p>
<br>


<p>1주차 프리코스 미션에서 지금 당장은 Parser가 확장될 일이 없어 보이지만, <strong>만약 객체의 다형성이 필요한 시점</strong>이 왔을 때, static 메서드로만 선언한 클래스는 <strong>확장하여 사용할 수 없습니다.</strong> 확장에 닫혀 있다는 것은 좋지 않은 설계입니다.</p>
<p>또한 객체 지향 프로그래밍에서 다형성은 중요한 개념이기 때문에 다형성을 활용할 수 없다는 것은 큰 단점이라고 생각했습니다.</p>
<br>


<h3 id="2️⃣-독립적-단위-테스트의-어려움">2️⃣ 독립적 단위 테스트의 어려움</h3>
<p>두 번째로, 클래스 내부의 메서드를 모두 static으로 선언하게 된다면, 이를 <strong>모킹하여 테스트하는 일이 굉장히 어려워집니다.</strong></p>
<p>먼저 <strong>모킹을 하는 이유</strong>는, 의존하는 클래스에 대한 의존성을 잠시 끊고 독립적으로 단위 테스트하기 위함입니다.</p>
<p>그러나 static 메서드는 Parser를 모킹해 테스트하고 싶을 때, 이를 불가능하게 만듭니다.</p>
<p>parse 메서드를 static으로 선언하지 않고, Parser를 사용하는 StringWrapper라는 클래스를 테스트하는 예시를 살펴 보도록 하겠습니다. </p>
<p>StringWrapper 클래스에 문자열을 자르고 이를 인덱싱하고 감싸서 반환하는 기능을 하는 메서드를 선언했습니다.</p>
<pre><code class="language-java">public class StringWrapper {

    private final Parser parser;

    public StringWrapper(Parser parser) {
        this.parser = parser;
    }

    public String wrap(String input) {
        String[] splitInput = parser.parse(input);
        StringBuilder result = new StringBuilder();
        for (int i = 0; i &lt; splitInput.length; i++) {
            result.append(i);
            result.append(splitInput[i]);
        }
        return String.valueOf(result);
    }
}</code></pre>
<p>StringWrapper는 Parser 인터페이스에 의존하며, 다양한 Parser 구현체를 주입받습니다.</p>
<p>이제 StringWrapper를 독립적으로 단위 테스트해보도록 하겠습니다. FakeParser를 만들고 이를 StringWrapper에 모킹해 테스트하려고 합니다.</p>
<p>StringWrapper를 단위 테스트하는 코드는 아래와 같습니다. </p>
<pre><code class="language-java">@Test
void fakeParserTest() {
    FakeParser fakeParser = new FakeParser();
    StringWrapper stringWrapper = new StringWrapper(fakeParser);
    assertThat(stringWrapper.wrap(&quot;a,b:c&quot;)).isEqualTo(&quot;0fake1parser2for3test&quot;);
}</code></pre>
<pre><code class="language-java">public class FakeParser implements Parser {
    @Override
    public String[] parse(String input) {
        return new String[] {&quot;fake&quot;, &quot;parser&quot;, &quot;for&quot;, &quot;test&quot;};
    }
}</code></pre>
<p>FakeParser는 input과는 무관하게 항상 동일한 결과를 리턴하는 <strong>가짜 목 객체</strong>입니다. 이로써 StringWrapper를 독립적으로 단위테스트 할 수 있게 됩니다. Parser와는 무관하게 StringWrapper 만을 테스트할 수 있게 되는 것이죠.</p>
<br>


<p>이제 parse 메서드를 <strong>static 메서드</strong>로 선언하고, 위의 과정을 다시 살펴보도록 하겠습니다. </p>
<pre><code class="language-java">public class StringWrapper {

    public String wrap(String input) {
        String[] splitInput = ParserStatic.parse(input);
        StringBuilder result = new StringBuilder();
        for (int i = 0; i &lt; splitInput.length; i++) {
            result.append(i);
            result.append(splitInput[i]);
        }
        return String.valueOf(result);
    }
}</code></pre>
<p>StringWrapper에서 <code>ParserStatic.parse</code>(클래스 이름.메서드명)로 ParserStatic 클래스의 parse 메서드를 사용하고 있습니다. </p>
<p>StringWrapper에 대한 테스트 코드는 아래와 같습니다. </p>
<pre><code class="language-java">@Test
void staticParserTest() {
    StringWrapper stringWrapper = new StringWrapper();
    assertThat(stringWrapper.wrap(&quot;a,b:c&quot;)).isEqualTo(&quot;0a1b:c&quot;);
}</code></pre>
<p>그런데 이때, ParserStatic 클래스를 수정하다가 아래처럼 문제가 발생한다면 어떻게 될까요? </p>
<pre><code class="language-java">public class ParserStatic {

    public static String[] parse(String input) {
        throw new IllegalArgumentException(&quot;ParserStatic 클래스에 문제 발생&quot;);
    }
}
</code></pre>
<p>parse 메서드에서 예외를 발생하기 때문에 StringWrapper에 대한 단위테스트도 실패하게 됩니다. </p>
<p>StringWrapper가 의존하고 있는 ParserStatic에 문제가 발생했을 때 이 영향이 StringWrapper에 미치고 있기 때문에, 이러한 의존성을 끊고 목 객체를 생성하고 싶습니다. </p>
<p>그러나 static 메서드의 경우 앞선 에시처럼 가짜 목 객체를 생성해 테스트에 활용할 수가 없습니다. 이로써 static 메서드를 포함한 클래스를 모킹하여 테스트하기가 어려워집니다. </p>
<br>


<h2 id="📚-static-메서드로-선언한다는-것은">📚 static 메서드로 선언한다는 것은</h2>
<p>static 메서드를 사용할 경우, 위와 같은 객체의 장점을 잃어버리기 때문에 잘 생각해보고 사용해야 할 것 같습니다. </p>
<p>단순히 상태가 없다면 static 메서드를 사용해야 한다는 것은 틀린 말입니다. <strong>객체는 상태가 있을 수도 있고, 없을 수도 있습니다.</strong> 중요한 것은 static 메서드가 나열된 클래스는 객체로 활용될 수가 없기 때문에, <strong>해당 클래스를 객체로 볼 것인지 아닌지</strong>를 먼저 고려해보아야 합니다. 만약 Parser를 객체로 보지 않는다면, static 메서드로 선언할 수도 있습니다. 그러나 단순히 <strong>상태가 없다고 static으로 만들면 안됩니다.</strong></p>
<p>그 예시로, Spring 프로젝트를 할 때 Service, Controller 등 @Component가 붙어 빈에 등록되는 객체들은 모두 내부에 상태(인스턴스 변수)를 가질 수 없습니다. 이들은 <strong>싱글톤 빈에서 관리</strong>되기 때문에 애플리케이션 전체에서 <strong>하나의 인스턴스만 생성되어 여러 요청에서 공유</strong>됩니다. 즉, 멀티스레드에서 공유되는 객체기 때문에 변경이 가능한 <strong>상태를 뒀을 때 동시성 문제</strong>가 발생할 수 있습니다. 이들은 <strong>상태를 가지지는 않지만 명백한 객체</strong>입니다.</p>
<br>


<h2 id="🤔-그럼-static-method는-쓰지-말아야-하는가">🤔 그럼 static method는 쓰지 말아야 하는가?</h2>
<p>엘레강트 오브젝트라는 책에서 static은 객체가 아니니까 절대 쓰지 말라고 합니다. 그러나 <strong>코드나 설계에는 항상 트레이드 오프가 존재</strong>하기 때문에 적절한 시점에 사용할 줄 알아야 한다고 생각합니다. static을 통해 구현하는 것도 방법 중의 하나이며, static 메서드를 사용할 때가 더 적합한 상황도 분명히 존재합니다.</p>
<p>그렇기 때문에 <strong>static method로 선언할 때와 객체의 인스턴스 메서드로 선언할 때의 차이점을 분명히 알고</strong>, 필요할 때 적절한 방식을 잘 선택할 줄 아는 것이 중요합니다.</p>
<br>


<h2 id="🌳-결론">🌳 결론</h2>
<p>1주차 미션에서 제가 정의한 Parser의 경우 구체적인 클래스입니다. 또한 객체 내부에 상태를 가지고 있지 않기 때문에 static 메서드로 선언해도, 동작하는 데에 큰 차이점은 없습니다. 
다만, 요구사항이 변경되어 Parser를 인터페이스로 만들고, CommaParser, ColonParser 등 다양한 구현체를 만들게 된다면, Parser는 반드시 객체로 만들어야 합니다. static 메서드로만 동작하는 클래스는 객체라고 부를 수 없습니다. 
따라서 객체가 상태를 가지지 않으면 static 메서드를 사용하는 것이 아니라, 해당 클래스를 객체로 볼 것인지를 먼저 고민해볼 필요가 있습니다. 저의 경우 Parser를 객체로 바라봤습니다. 
static 메서드 또한 분명 필요한 경우가 있기 때문에 이에 대한 공부가 더 필요할 것 같습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Security] OAuth와 일반 로그인 동시에 구현하기]]></title>
            <link>https://velog.io/@carol_ly/OAuth%EC%99%80-%EC%9D%BC%EB%B0%98-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%8F%99%EC%8B%9C%EC%97%90-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@carol_ly/OAuth%EC%99%80-%EC%9D%BC%EB%B0%98-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%8F%99%EC%8B%9C%EC%97%90-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 08 Oct 2024 15:28:36 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 진행하며 OAuth 로그인과 일반 로그인을 동시에 구현하게 되었다. Spring Security의 formLogin과 oauth2Login 기능을 동시에 사용하는 방법은 아래와 같다. </p>
<h2 id="1-spring-security-설정">1. Spring Security 설정</h2>
<pre><code class="language-java">package org.example.catch_line.config;

@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록된다.
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true)
// secured 어노테이션 사용 가능(특정 메서드에 간단하게 걸고 싶을 때 사용) &quot;ROLE_ADMIN&quot;
// @PreAuthorize 어노테이션 사용 가능 &quot;hasRole(&#39;ROLE_MANAGER&#39;) or hasRole(&#39;ROLE_ADMIN&#39;)&quot; 함수가 실행되기 전에 권한을 검사
@Configuration
@RequiredArgsConstructor
public class SecurityConfig{

    private final PrincipleOAuth2DetailsService principleOAuth2DetailsService;
    private final PrincipalDetailsService principalDetailsService;

    @Bean
    public static BCryptPasswordEncoder bCryptPasswordEncoder() {   // for hash encrypt
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, PrincipleOAuth2DetailsService principleOAuth2DetailsService) throws Exception {
        http
                .cors(cors -&gt; cors.configurationSource(corsConfigurationSource()))  // CORS 설정을 최신 방식으로 변경
                .csrf(AbstractHttpConfigurer::disable)
                .headers(headers -&gt; headers.frameOptions(frameOptions -&gt; frameOptions.disable()))  // frameOptions 설정
                .authorizeHttpRequests(requests -&gt;requests
                        .requestMatchers(&quot;/**&quot;, &quot;/static/**&quot;, &quot;/images/**&quot;, &quot;/signup&quot;, &quot;/login&quot;, &quot;/restaurants/**&quot;, &quot;/owner&quot;).permitAll()
                        .requestMatchers(&quot;/members&quot;, &quot;/history&quot;).authenticated() // 인증이 필요
                        .anyRequest().permitAll()
                )
                // 일반 로그인
                .formLogin(formLogin -&gt; formLogin
                        .loginPage(&quot;/login&quot;)  // 로그인 페이지 URL
                        .loginProcessingUrl(&quot;/loginProcess&quot;)  // 로그인 처리 URL (여기에 POST 요청이 와야 함)
                        .defaultSuccessUrl(&quot;/&quot;)
                        .failureUrl(&quot;/login?error=true&quot;)
                        .permitAll()
                )
                        // Oauth 로그인
                .oauth2Login(login -&gt; login
                        .loginPage(&quot;/login/oauth&quot;)
                        .defaultSuccessUrl(&quot;/loginSuccess&quot;)
                        .userInfoEndpoint()
                        .userService(principleOAuth2DetailsService) // OAuth 사용자 로그인 처리
                )
                .userDetailsService(principalDetailsService);  // 일반 사용자 로그인 처리

//                .sessionManagement(session -&gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList(&quot;http://localhost:8080&quot;, &quot;http://localhost:8081&quot;));
        configuration.setAllowedMethods(Arrays.asList(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;, &quot;OPTIONS&quot;));
        configuration.setAllowedHeaders(Arrays.asList(&quot;*&quot;));
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration(&quot;/**&quot;, configuration);
        return source;
    }
}</code></pre>
<br>

<h2 id="2-authentication-객체에-저장되는-principaldetail-→-userdetails-oauth2user-둘-다-구현">2. Authentication 객체에 저장되는 PrincipalDetail → UserDetails, OAuth2User 둘 다 구현</h2>
<p>두 개의 생성자를 통해 일반 로그인 사용자와 OAuth 로그인 사용자를 구분했다. 일반 로그인을 한 경우 UserDetails가 생기며, OAuth로 로그인을 할 경우 OAuth2User가 생성된다. </p>
<pre><code class="language-java">package org.example.catch_line.config.auth;

import lombok.Data;
import org.apache.logging.log4j.util.Strings;
import org.example.catch_line.common.constant.Role;
import org.example.catch_line.user.member.model.entity.MemberEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

@Data
public class PrincipalDetail implements UserDetails, OAuth2User {

    private MemberEntity member;

    private Map&lt;String,Object&gt; attributes;

    // 일반 로그인 생성자
    public PrincipalDetail(MemberEntity member) {
        this.member = member;
    }

    // Oauth 로그인 생성자
    public PrincipalDetail(MemberEntity member, Map&lt;String, Object&gt; attributes) {
        this.member = member;
        this.attributes = attributes;
    }

    @Override
    public Map&lt;String, Object&gt; getAttributes() {
        return attributes;
    }

    // 잘 사용하지 않는다.
    @Override
    public String getName() {
        return Strings.EMPTY;
    }

    @Override
    public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
        Collection&lt;GrantedAuthority&gt; collect = new ArrayList&lt;&gt;();
        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return Role.USER.getDescription();
            }
        });
        return collect;
    }

    @Override
    public String getPassword() {
        return member.getPassword() != null ? member.getPassword().getEncodedPassword() : &quot;&quot;;
    }

    @Override
    public String getUsername() {
        return member.getEmail().getEmailValue();
    }

}
</code></pre>
<h2 id="일반-로그인과-oauth-로그인-각각-서비스-구현">일반 로그인과 OAuth 로그인 각각 서비스 구현</h2>
<br>

<h3 id="1-일반-로그인-서비스-구현-principaldetailsservice">1. 일반 로그인 서비스 구현 <code>PrincipalDetailsService</code></h3>
<pre><code class="language-java">package org.example.catch_line.config.auth;

@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {

    // 시큐리티 session (내부 Authentication (내부 UserDetails ))
    private final MemberDataProvider memberDataProvider;

    // 함수 종료 시 @AuthenticationPrincipal 어노테이션이 만들어진다.
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MemberEntity member = memberDataProvider.provideMemberByEmail(new Email(username));
        return new PrincipalDetail(member);

    }
}
</code></pre>
<h3 id="2-oauth2-로그인-서비스-구현">2. OAuth2 로그인 서비스 구현</h3>
<pre><code class="language-java">package org.example.catch_line.config.auth;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.catch_line.common.constant.Role;
import org.example.catch_line.common.model.vo.Email;
import org.example.catch_line.user.member.model.entity.MemberEntity;
import org.example.catch_line.user.member.model.provider.MemberDataProvider;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.thymeleaf.util.StringUtils;

import java.util.List;
import java.util.Map;

@Service
@RequiredArgsConstructor
@Slf4j
public class PrincipleOAuth2DetailsService extends DefaultOAuth2UserService {

    private final MemberDataProvider memberDataProvider;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        OAuth2User oAuth2User = super.loadUser(userRequest);

        // Role generate
        List&lt;GrantedAuthority&gt; authorities = AuthorityUtils.createAuthorityList(Role.USER.getDescription());

        Map&lt;String, Object&gt; attributes = oAuth2User.getAttributes();
        log.info(&quot;attributes: {}&quot;, attributes);

        Map&lt;String, Object&gt; properties = (Map&lt;String, Object&gt;) attributes.get(&quot;properties&quot;);
        String name = (String) properties.get(&quot;nickname&quot;); // properties에서 닉네임 가져오기

        Map&lt;String, Object&gt; kakaoAccount = (Map&lt;String, Object&gt;) attributes.get(&quot;kakao_account&quot;);
        String email = (String) kakaoAccount.get(&quot;email&quot;); // kakao_account에서 이메일 가져오기

        String provider = userRequest.getClientRegistration().getRegistrationId(); // kakao
        Long providerId = ((Number) attributes.get(&quot;id&quot;)).longValue();
        String nickname = provider + &quot;_&quot; + StringUtils.substring(providerId, 0, 7); // kakao_이름

        if (memberDataProvider.isNotDuplicateKakaoMember(providerId, new Email(email))) {
            MemberEntity member = MemberEntity.builder()
                    .name(name)
                    .nickname(nickname)
                    .email(new Email(email))
                    .kakaoMemberId(providerId)
                    .build();
            memberDataProvider.saveMember(member);
        }

        MemberEntity member = memberDataProvider.provideMemberByKakaoMemberId(providerId);

        // 어떤 OAuth2 공급자를 통해 로그인하는지, 해당 공급자에서 사용자의 고유 식별자를 나타내는 필드명이 무엇인지를 반환한다.
        // 지금 kakao login만 사용하기 때문에 필요없지만, 추후 구현 위해 남겨 놓는다.
        String userNameAttributeName = userRequest.getClientRegistration()
                .getProviderDetails()
                .getUserInfoEndpoint()
                .getUserNameAttributeName();

        return new PrincipalDetail(member, oAuth2User.getAttributes());
    }
}
</code></pre>
<h2 id="마치며">마치며</h2>
<p>일반 로그인과 OAuth2 로그인을 동시에 사용하기 위해 Security 설정을 어떻게 할 수 있는지 알아봤다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Security] Authorization Filter와 White List 처리]]></title>
            <link>https://velog.io/@carol_ly/Authorization-Filter%EC%99%80-White-List-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@carol_ly/Authorization-Filter%EC%99%80-White-List-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Tue, 08 Oct 2024 15:20:00 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-상황">문제 상황</h3>
<p>필터는 모든 요청에 대해 동작한다. 특정 url에서만 필터가 동작하도록 하기 위해 White List를 리스트 형태로 만들었다. 
다만 아래와 같은 상황에서 어떻게 구현할지 고민이 되었다. </p>
<p><code>/restaurants</code>는 통과, <code>/restaurants/{restaurant-id}/waitings</code>는 통과하면 안될 때 필터 조건을 어떻게 줄 수 있을까?</p>
<h3 id="1-화이트-리스트-설정에서-restaurants는-제외">1. 화이트 리스트 설정에서 /restaurants는 제외</h3>
<pre><code class="language-java">private static final List&lt;String&gt; WHITELIST_URLS = Arrays.asList(
    &quot;/login&quot;,
    &quot;/logout&quot;,
    &quot;/signup&quot;,
    &quot;/static&quot;,
    &quot;/css&quot;,
    &quot;/js&quot;,
    &quot;/owner&quot;
);</code></pre>
<h3 id="2-인증이-필요한-restaurants-하위-경로를-블랙-리스트에-추가">2. 인증이 필요한 /restaurants 하위 경로를 블랙 리스트에 추가</h3>
<pre><code class="language-java">private static final List&lt;String&gt; BLACKLIST_URLS = Arrays.asList(
    &quot;/reviews/create&quot;,
    &quot;/scraps&quot;,
    &quot;/waiting&quot;,
    &quot;/reservation&quot;
);</code></pre>
<h3 id="3-shouldnotfilter에서는-white-list를-처리">3. shouldNotFilter에서는 white list를 처리</h3>
<pre><code class="language-java">@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
    String requestURI = request.getRequestURI();
    return WHITELIST_URLS.stream().anyMatch(requestURI::startsWith);
}</code></pre>
<h3 id="4-dofilterinternal-내부에서-restaurants로-시작하면서-블랙-리스트에-해당되지-않는-uri는-필터-통과">4. doFilterInternal 내부에서 /restaurants로 시작하면서 블랙 리스트에 해당되지 않는 URI는 필터 통과</h3>
<pre><code class="language-java">
if (requestURI.startsWith(&quot;/restaurants&quot;) &amp;&amp; !isBlacklisted(requestURI)) {
        chain.doFilter(request, response);
        return;
}</code></pre>
<h2 id="onceperrequestfilter의-shouldnotfilter">OncePerRequestFilter의 <strong>shouldNotFilter</strong></h2>
<p>BasicAuthenticationFilter는 OncePerRequestFilter를 상속받는다. OncePerRequestFilter는 아래와 같은 메서드를 재정의하여 사용할 수 있다. </p>
<pre><code class="language-java">*protected boolean* shouldNotFilter(*HttpServletRequest* request) *throws* ServletException {    
            *return false*;
}</code></pre>
<p>아래와 같은 화이트리스트가 있다고 가정하자.</p>
<pre><code class="language-java">private static final List&lt;String&gt; WHITELIST_URLS = Arrays.asList(
            &quot;/owner/login&quot;,
            &quot;/owner/signup&quot;,
            &quot;/templates&quot;,
            &quot;/static&quot;,
            &quot;/css&quot;,
            &quot;/js&quot;
);</code></pre>
<p>기존에는 아래와 같은 메서드를 만들어 WHITELIST_URLS로 시작하는 경로는 모두 필터를 통과시켰다. </p>
<pre><code class="language-java">private boolean isWhitelisted(String requestURI) {
        return WHITELIST_URLS.stream().anyMatch(url -&gt;  requestURI.startsWith(url));
}</code></pre>
<p>doFilterInternal 메서드 안에는 아래와 같이 isWhitelisted를 동작시켜 검증하는 코드가 존재했다. </p>
<pre><code class="language-java">if (isWhitelisted(requestURI)) {
        chain.doFilter(request, response);
        return;
}</code></pre>
<p>shouldNotFilter 메서드는 <code>true</code>를 반환할 경우 필터가 적용되지 않으며, <code>false</code>를 반환할 경우 필터가 적용된다. </p>
<pre><code class="language-java">@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        String requestURI = request.getRequestURI();
    return WHITELIST_URLS.stream().anyMatch(requestURI::startsWith);
}</code></pre>
<h3 id="마치며">마치며</h3>
<p>간단하게 화이트리스트와 블랙리스트를 구현하여 url을 상세하게 나누어 필터 조건을 주었다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Security] 2개의 Authentication Manager를 Bean에 등록하는 방법]]></title>
            <link>https://velog.io/@carol_ly/Authentication-Manager-Bean%EC%97%90-2%EA%B0%9C-%EB%93%B1%EB%A1%9D%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@carol_ly/Authentication-Manager-Bean%EC%97%90-2%EA%B0%9C-%EB%93%B1%EB%A1%9D%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 08 Oct 2024 15:15:50 GMT</pubDate>
            <description><![CDATA[<ol>
<li>Spring이 Bean에 등록할 때 이름을 따로 지정해주지 않으면 메서드명으로 등록된다. </li>
<li>두 AuthenticationManager의 이름을 다르게 지정한다.</li>
<li>각자 사용하는 UserDetailsService가 다르니, 이를 각각 설정해준다.</li>
</ol>
<pre><code class="language-java">@Bean
// Primary 지정해버리면 다른 config 파일이더라도 무조건 이것만 사용된다. -&gt; Bean에 이름 지정 필요
@Primary
public AuthenticationManager memberAuthenticationManager(HttpSecurity http) throws Exception {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(memberDefaultLoginService);
        provider.setPasswordEncoder(bCryptPasswordEncoder());
        return new ProviderManager(provider);
}

@Bean
public AuthenticationManager ownerAuthenticationManager(HttpSecurity http) throws Exception {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(ownerLoginService);
        provider.setPasswordEncoder(bCryptPasswordEncoder());
        return new ProviderManager(provider);
}</code></pre>
<ol>
<li><p><code>@Qualifier</code>를 통해 사용할 AuthenticationManager의 이름을 지정해준다.</p>
</li>
<li><p>FilterChain에 <code>AuthenticationManager authenticationManager</code>만 파라미터로 넣어주면 <code>@Primary</code>로 지정한 AuthenticationManager만 들어간다. 다른 Security Config 파일을 사용해도 마찬가지다. 그래서 반드시 이름으로 둘을 구분하고, 하나의 config에서 둘 다 사용할 경우 둘 다 넣어줘야 한다. </p>
</li>
<li><p>AuthenticationFilter의 경우 아래와 같이 authenticationManager의 authenticate을 통해 UserDetailsService를 호출하여 로그인 요청을 처리한다. </p>
<pre><code class="language-java"> *Authentication* authentication = authenticationManager.authenticate(authenticationToken);</code></pre>
</li>
<li><p>AuthorizationFilter의 경우 BasicAuthenticationFilter를 상속받는다. 이 필터가 내부에 AuthenticationManager를 갖고 있기 때문에 필요하다. → 🌟<strong><code>OncePerRequestFilter로 변경 가능할지?</code></strong> </p>
</li>
<li><p>위와 같은 이유로 두 개의 필터에서 모두 AuthenticationManager를 필요로 한다.</p>
</li>
<li><p>Authentication Manager를 잘 맞게 넣어준다. </p>
</li>
</ol>
<pre><code class="language-java">@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
                @Qualifier(&quot;memberAuthenticationManager&quot;) AuthenticationManager memberAuthenticationManager,
        @Qualifier(&quot;ownerAuthenticationManager&quot;) AuthenticationManager ownerAuthenticationManager) throws Exception {

// form Login disable -&gt; 해당 필터 사용할 수 있도록 추가, AuthenticationManager 넣어줘야 한다.

        MemberJwtAuthenticationFilter memberJwtAuthenticationFilter = new MemberJwtAuthenticationFilter(memberAuthenticationManager);
        MemberJwtAuthorizationFilter memberJwtAuthorizationFilter = new MemberJwtAuthorizationFilter(memberAuthenticationManager);

        OwnerJwtAuthenticationFilter ownerJwtAuthenticationFilter = new OwnerJwtAuthenticationFilter(ownerAuthenticationManager);
        OwnerJwtAuthorizationFilter ownerJwtAuthorizationFilter = new OwnerJwtAuthorizationFilter(ownerAuthenticationManager);
     /*
     * 생략 
     */
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[패스트캠퍼스  Kernel360 백엔드 부트캠프 2기] 기술세미나]]></title>
            <link>https://velog.io/@carol_ly/%ED%8C%A8%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%8D%BC%EC%8A%A4-Kernel360-%EB%B0%B1%EC%97%94%EB%93%9C-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-2%EA%B8%B0-%EA%B8%B0%EC%88%A0%EC%84%B8%EB%AF%B8%EB%82%98</link>
            <guid>https://velog.io/@carol_ly/%ED%8C%A8%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%8D%BC%EC%8A%A4-Kernel360-%EB%B0%B1%EC%97%94%EB%93%9C-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-2%EA%B8%B0-%EA%B8%B0%EC%88%A0%EC%84%B8%EB%AF%B8%EB%82%98</guid>
            <pubDate>Sat, 21 Sep 2024 18:06:26 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 저는 Kernel 360 백엔드 2기 크루 박소은이라고 합니다. 오늘은 기술세미나에 관한 이야기를 적어보려고 합니다!</p>
<blockquote>
<p><strong>Kernel 360</strong>의 <strong>기술세미나</strong>란?</p>
</blockquote>
<p>프로젝트를 하며 했던 기술적인 공부, 고민들의 해결 과정 등을 크루들 앞에서 발표하는 시간입니다!</p>
<p>저의 경우에는 VO(Value Object)를 주제로 발표를 진행했습니다!</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/22b005e0-4e8d-416a-aef9-a0038ff5b27d/image.jpeg" alt=""></p>
<h3 id="주제-선정-🧘">주제 선정 🧘</h3>
<p>제가 주제를 선정한 기준은 아래와 같았습니다!</p>
<ol>
<li>프로젝트를 하며 학습한 내용일 것!</li>
<li>많은 고민을 거쳐 문제를 해결한 과정을 담을 수 있을 것!</li>
<li>크루들의 공감이 갈 수 있는 주제일 것!</li>
</ol>
<p>저희 팀에서 E2E 프로젝트를 진행하며 Builder 패턴에 대한 고민을 하며 VO를 적용하여 해결한 과정을 크루들과 공유하고 싶었습니다. </p>
<h3 id="발표에-대한-두려움-😧">발표에 대한 두려움 😧</h3>
<p>사실 저는 20학번입니다 !! 두둥</p>
<p>코로나 시기에 비대면으로 학교를 다녀서 발표에 점점 자신이 없어지게 되었습니다 😭 Kernel에 들어와서 발표할 기회가 많이 있었고, 이를 최대한 활용하려고 했습니다!</p>
<p>아이디어 발표, 프로젝트 설계 발표, 프로젝트 중간 발표, 최종 발표, 기술 세미나 등 생각보다 많은 기회가 있습니다.</p>
<p>두 달 동안 위의 발표를 모두 해볼 수 있었습니다! 점점 발표에 두려움은 없어졌고, 어떻게 하면 이 내용을 잘 전달할 수 있는가 등의 실용적인 고민들을 하게 되었습니다 ㅎㅎ</p>
<h3 id="목차-정하기-🍬">목차 정하기 🍬</h3>
<p>하고 싶은 이야기를 정했으니, 목차를 정하기 시작했습니다. 
목차를 정할 때는 최종적으로 어떤 이야기를 전달하고 싶은지를 계속 생각하면서 시나리오를 잘 만들어야 하는 것 같습니다. </p>
<p>다른 사람이 하는 이야기가 잘 와닿기 위해서는 이야기들의 최종 종착지가 뚜렷해야 하고, 그 흐름이 납득이 가야 하는 것 같습니다! </p>
<p>VO에 대해 하고 싶은 이야기들을 효율적으로 전달하려면 발표가 어떤 구성으로 이루어져야 하는지를 생각해봤습니다.</p>
<p>저희 팀이 VO를 사용하게 된 이유는 생성자 방식에서 타입 안전성을 보장하기 위함이었습니다! 왜 빌더 패턴 대신 생성자 방식을 사용하는지, VO를 사용하면 어떻게 달라지는지를 시작으로 VO의 특징을 코드와 함께 제시하기로 했습니다. </p>
<p>이렇게 정해진 목차는 아래와 같습니다 ✨</p>
<blockquote>
</blockquote>
<p>✅ VO 개념
💵 실생활 속 VO 예시
❓ VO를 사용하는 이유 </p>
<ol>
<li>타입 안전성</li>
<li>원자적 객체 생성
🚼 VO 특징 1.  불변성<ul>
<li>만약 내부 상태 변경이 필요하다면?</li>
<li>대표적인 불변 클래스 - String</li>
<li>잦은 객체 생성에 대한 고민?
🍋‍ VO 특징 2. 생성 시 값을 검증
🟰 VO 특징 3. 동등성</li>
<li>Equals, Hashcode</li>
</ul>
</li>
</ol>
<h3 id="발표-준비-🙋♀️">발표 준비 🙋‍♀️</h3>
<p>여러분들은 발표를 어떻게 준비하시나요? 저희 크루들은 대본을 작성해서 외우기도 하고, 내용을 머릿속에 잘 정리해 막힘없이 줄줄 이야기하기도 합니다!</p>
<p>저는 대본을 작성해도 결국 입에 붙는 말로 발표를 하게 되는 것 같아서 발표할 내용을 처음부터 끝까지 입으로 말하는 연습을 합니다!</p>
<p>발표 내용을 구성하고 장표를 준비하는 동안 이미 발표 내용은 머리에 다 들어있습니다. 다만 이를 매끄럽게 말하려면 연습이 필요한 것 같습니다 ㅎㅎ</p>
<p>발표 시간도 계산해보고, 매끄럽지 못한 부분도 알 수 있게 됩니다!</p>
<h3 id="발표-그리고-그-이후-💭">발표, 그리고 그 이후 💭</h3>
<p>기술 세미나를 통해 VO와 관련하여 하고 싶었던 이야기를 잘 전달할 수 있었습니다! 퀴즈 시간에 발표를 크루들이 잘 이해했는지 확인할 수 있었습니다. 
<img src="https://velog.velcdn.com/images/carol_ly/post/db83d5ee-9733-4343-95c4-976bea48cd90/image.png" alt=""></p>
<p>또, Q&amp;A 시간에 크루분들의 질문을 받으며 저도 조금 더 공부해야 할 점들을 알게 되었고, 멘토님과 소통하며 이를 보완할 수 있었습니다!</p>
<p>기술 세미나를 통해 VO를 누구에게나 잘 설명할 수 있게 되었습니다. <strong>&quot;공부라는 것이 이런게 아닐까?&quot;</strong>라는 생각도 들었고, &quot;무언가를 제대로 <strong>알고 있다</strong>고 이야기하려면 이만큼은 공부해야 하는 거구나...&quot;라는 생각도 했습니다.</p>
<p>발표를 준비하면서, 프로젝트 기간 동안 공부했던 내용을 정리할 수 있었고, 더 밀도가 높은 지식을 쌓을 수 있게 되었습니다. 이를 다른 사람들 앞에서 <strong>발표하고 지식을 공유</strong>하는 일이 얼마나 중요한지 알 수 있었던 시간이었던 것 같습니다. 또 발표에서 끝나지 않고, 추가적인 <strong>Q&amp;A</strong>를 통해 <strong>생각을 확장</strong>할 수 있었던 점도 좋았습니다.</p>
<p>이후에 기술 세미나의 내용을 블로그에 정리하며 VO와 관련한 모든 고민들과 생각의 과정, 발표 내용, Q&amp;A 등을 한꺼번에 실을 수 있었습니다! </p>
<p>혹시.. 궁금하시다면 아래의 링크를 통해 확인해주세요 😄</p>
<p><a href="https://kernel360.github.io/blog/VO">기술세미나 블로그 - VO</a></p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/8ea10632-df3d-49ac-8f4c-3563088d5c9c/image.png" alt=""></p>
<h3 id="마치며">마치며</h3>
<p>기술 세미나는 어떤 기술에 대해 본인이 무엇을 모르고, 무엇을 아는지를 명확하게 알고 부족한 점을 채워 내 것으로 만들 수 있는 시간입니다. 이를 사람들 앞에서 설명하며 내가 알고 있는 것을 스스로 증명할 수 있는 기회가 되기도 하는 것 같습니다. 앞으로도 기술을 배울 때 다른 사람 앞에서 설명할 수 있을 정도의 공부를 하려고 다짐했습니다. </p>
<p>긴 글 읽어주셔서 감사합니다🍀 (모두 기술 세미나하세요~~!)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[패스트캠퍼스  Kernel360 백엔드 부트캠프 2기] Kernel 360 크루의 하루]]></title>
            <link>https://velog.io/@carol_ly/%ED%8C%A8%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%8D%BC%EC%8A%A4-Kernel360-%EB%B0%B1%EC%97%94%EB%93%9C-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-2%EA%B8%B0-Kernel-360-%ED%81%AC%EB%A3%A8%EC%9D%98-%ED%95%98%EB%A3%A8</link>
            <guid>https://velog.io/@carol_ly/%ED%8C%A8%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%8D%BC%EC%8A%A4-Kernel360-%EB%B0%B1%EC%97%94%EB%93%9C-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-2%EA%B8%B0-Kernel-360-%ED%81%AC%EB%A3%A8%EC%9D%98-%ED%95%98%EB%A3%A8</guid>
            <pubDate>Sat, 21 Sep 2024 16:58:49 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 저는 Kernel 360 백엔드 2기 크루 박소은이라고 합니다. OT날을 담은 지난 이야기에 뒤이어 이번에는 Kernel 360 크루의 하루를 담아보려고 합니다. </p>
<p>Kernel 360 2기의 경우, 오전 10시에 출근하여 오후 7시에 퇴근합니다! 출퇴근 시간에서 살짝 벗어나 있어서 좋은 것 같습니다😄</p>
<h3 id="스크럼">스크럼</h3>
<p>Kernel 360 교육 과정은 팀 프로젝트 위주로 이루어지기 때문에 주로 10시에는 스크럼을 하게 됩니다!</p>
<p><strong>마일스톤</strong>을 보며 <strong>오늘의 계획</strong>을 세우고, 팀원들과 공유해야 할 이야기들을 합니다. 상황에 따라 짧게는 10분에서 길게는 2시간까지도 하는 것 같습니다!</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/9c953537-326a-4ca3-a225-18ece671d55e/image.png" alt=""></p>
<p>지하의 회의실에서 진행한 스크럼 사진인데요! <strong>멘토링</strong>을 받기 위해 질문할 내용들을 정리해야 하는 날이었나 봅니다 ㅎㅎ</p>
<p>팀원들의 진행 상황을 공유하고, 어노테이션에 관한 이야기를 나눈 것 같습니다. </p>
<h3 id="업무-집중-시간">업무 집중 시간</h3>
<p>몇 주가 지나면 하루를 함께하는 팀원들과 많이 친해지게 되는데요! 모르는 내용을 옆자리 팀원에게 질문하기도 하고, 개발을 하며 생기는 고민들을 공유하기도 합니다. 그러다 보면 가끔씩은 업무의 흐름이 끊길 때도 있습니다. 이를 방지하기 위해 <strong>업무 집중 시간</strong>이 있습니다! 회사에서도 업무 집중 시간이 있다고 해요!</p>
<p>이 시간 동안에는 최대한 대화를 삼가고, 본인의 업무에 집중해야 합니다! 하고 싶은 말이 있거나 질문하고 싶은 점이 있어도 온라인으로 소통하거나 조금 기다리는 것이 권장됩니다! </p>
<h3 id="점심-시간-🍤🍱🍝">점심 시간 🍤🍱🍝</h3>
<p>오전 동안 열심히 개발을 하다 보면 점심 시간이 됩니다!! 교육 장소가 봉은사역에 위치해 있기 때문에 크루들은 코엑스에서 식사를 하기도 하고, 주변 맛집들을 탐방하기도 합니다.</p>
<p>맛집들을 살짝.. 소개하자면!! </p>
<ol>
<li>한식이 먹고 싶다면..? 솥밥집!!</li>
</ol>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/ea851ea3-de50-46c2-9191-e4244b9aeede/image.jpeg" alt=""></p>
<p>여긴 가지 솥밥이 정말 맛있습니다 🤤 새벽 두시인데 지금도 먹고 싶네요 . . </p>
<ol start="2">
<li>항상 사람이 많고 모든 메뉴가 맛있는 쌀국수집!</li>
</ol>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/73f2c0b6-0a22-4eee-b8f8-ee135e8ca1cf/image.jpeg" alt=""></p>
<p>여긴 항상 인근 직장인 분들로 붐비는 것 같습니다. 저희 식사 시간이 1시에서 2시라 한산할 때 먹을 수가 있어서 좋아요 ㅎㅎ</p>
<ol start="3">
<li>코엑스 햄버거 맛집!</li>
</ol>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/40d73551-44d6-4a1a-9f0c-cc9a3753640c/image.jpeg" alt=""></p>
<p>어떤 크루 분은 여길 일주일에 2~3번도 가시는 것 같더라구요 🍔 느끼하지 않고 산뜻한 수제 버거라 완전 제 취향입니다! 너무 맛있어 보여요. . . 🥹</p>
<p>점심 시간 이야기가 역시 신나네요 😝</p>
<h3 id="특강">특강</h3>
<p>적어도 2주에 한 번씩은 아주 좋은 특강들이 있습니다! 이날에는 Git 브랜치 전략과 Github 활용법에 대한 특강을 진행해주셨는데요!</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/5b4da8b5-b203-4cee-bc50-1eb4ac47ce3a/image.jpeg" alt=""></p>
<p>Git 같은 경우 능숙하게 사용할 줄 알아야 프로젝트를 진행함에 무리가 없기 때문에 열심히 들었던 것 같습니다. </p>
<p>현업에서 어떻게 브랜치 관리를 하시는지, 커밋 메시지와 이슈는 어떻게 작성하시는지를 아주 자세하게 설명해주셔서 프로젝트를 진행하며 정말 큰 도움이 되었던 특강이었습니다.</p>
<p>디렉터님께서 아주 인맥이 넓으시기 때문에 정말 대단하신 분들께서 엄청난 특강을 많이 해주시는 것 같습니다! 이것도 Kernel 360의 특장점이라고 생각합니다 😄</p>
<h3 id="개발">개발</h3>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/eb28f8d2-bec7-44f8-abd4-9c18a8a1002e/image.jpeg" alt=""></p>
<p>다시 자리에 앉아서 열심히 개발을 합니다!! (<del>책상이 조금 더러운데, 평소에는 깨끗합니다..ㅎㅎ</del>)</p>
<h3 id="멘토링">멘토링</h3>
<p>제가 아주 좋아하는 시간입니다!! 멘토링을 하고 나면 몰랐던 점들도 해소되고, 응원에 힘입어 더 열심히 하게 되는 것 같습니다! 한 주 동안 개발하며 마주한 기술적인 어려움과 고민들을 멘토님과 공유하면서 아주 알찬 시간을 보낼 수 있습니다. </p>
<p>멘토님께서 정말 적극적으로 도움을 주려고 하시고, 저희의 수준에 맞게 아주 쉽게 설명을 해주시는 것 같아 항상 큰 도움을 받고 있는 것 같습니다! </p>
<p>꼭 Kernel 360에 들어와서 멘토님들을 만나보세요 .. 😇</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/449f3691-560d-4ef3-a37f-0c8c54d9f9f7/image.jpeg" alt=""></p>
<h3 id="마치며">마치며</h3>
<p>저의 하루 일과를 소개해드린 것 같아요! 정말 시간이 순식간에 흐르기 때문에 항상 열심히 하려고 노력하고 있습니다! 오늘은 평범한 하루의 일과를 소개했지만, 기술 세미나, 프로젝트 발표, 설계 회의 등 재미난 일들이 아주 많습니다! 위에 소개한 일과는 정말 극히 일부에 불과하니까요 ㅎㅎ</p>
<p>Kernel에서의 하루는 힘들기도 하지만 정말 재밌습니다. 다들 잠을 4~7시간 사이로만 주무시는데도 정말 몰입해서 개발을 하고 있는 모습을 보면 저도 덩달아 열심히 하게 됩니다! 또 회의하고 특강을 듣고 멘토링을 받는 그 모든 과정이 재밌기 때문에 모두 들어오셔서 경험해보셨으면 좋겠습니다. </p>
<p>긴 글 읽어주셔서 감사합니다🍀</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[패스트캠퍼스  Kernel360 백엔드 부트캠프 2기] OT 솔직 후기]]></title>
            <link>https://velog.io/@carol_ly/%ED%8C%A8%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%8D%BC%EC%8A%A4-Kernel360-%EB%B0%B1%EC%97%94%EB%93%9C-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-2%EA%B8%B0-OT-%EC%86%94%EC%A7%81-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@carol_ly/%ED%8C%A8%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%8D%BC%EC%8A%A4-Kernel360-%EB%B0%B1%EC%97%94%EB%93%9C-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-2%EA%B8%B0-OT-%EC%86%94%EC%A7%81-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sat, 21 Sep 2024 15:57:54 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요, 저는 Kernel 360 백엔드 2기 크루 박소은이라고 합니다. 7월 중순 Kernel에 들어와 어느덧 시간이 흘러 10월을 바라보고 있는데요! 시간이 쑥 쑥 흐르는 kernel에서의 이야기를 해보려고 합니다. </p>
<p>최종 합격 발표 이후 떨리는 마음으로 봉은사역 패스트 파이브 건물로 향했습니다! 교육 장소는 봉은사역 6번 출구에서 4분 거리에 위치해 있습니다. </p>
<p><a href="https://naver.me/5B1ktFtA">패스트파이브 삼성 4호점</a></p>
<p>깨끗한 신축 건물에 기분이 좋아졌던 기억이 납니다 ㅎㅎ 백엔드와 프론트엔드 크루분들, 디렉터님, 운영 매니저님들께서 한 곳에 모여 오리엔테이션이 진행되었습니다 !</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/a10dcefa-9d0d-456f-b284-51731edd856e/image.jpg" alt=""></p>
<h2 id="kernel-360의-교육-방식">kernel 360의 교육 방식</h2>
<p>먼저 디렉터님들께서 kernel 360 교육의 특징을 말씀해주셨습니다. 지난 하반기부터 백엔드 부트캠프를 알아보다가 kernel에 오게 된 이유가 바로 여기 있는데요 .. !!</p>
<p>일단 kernel 360 교육 과정에는 공식적인 강사님이 계시지 않습니다! 오직 디렉터님, 운영 매니저님, 멘토님, 그리고 크루가 있는데요!</p>
<p>&quot;그럼 어떻게 공부를 하지?&quot; 라고 생각하실 수도 있을 것 같습니다!</p>
<p>kernel 360의 교육은 프로젝트, 멘토링, 동료 학습으로 이루어져 있습니다. 크루들은 프로젝트를 통해 함께 학습하며 서로에게 멘토가 되기도, 멘티가 되기도 합니다. 직접 부딪히고 실패를 통해 학습하는 경험은 아주 커다란 효과가 있는데요!</p>
<h3 id="나와-맞지-않는-강의식-교육">나와 맞지 않는 강의식 교육</h3>
<p>&quot;Spring 완전 정복!&quot;이라는 강의를 다 들으면 정말 Spring을 이용해 백엔드 개발을 잘 할 수 있게 될까요? 저의 경우에는 아니었습니다😢</p>
<p>처음 강의를 결제할 때는 “아, 이 44시간 짜리 강의를 다 들으면 난 스프링으로 프로그램을 만들 수 있겠지?!”라는 기대감으로 강의를 수강하기 시작합니다. </p>
<p>그렇지만 이 44시간 짜리 강의는 완강하기도 어려웠고, 다 듣는다고 해서 강의의 내용이 완전하게 내 것이 되기도 어렵습니다.</p>
<p>학습의 주도권이 강사님에게 있고, 강사님이 정해준 목차를 따라가게 됩니다. &quot;지금 내가 필요한 공부는 이거야!&quot;
&quot;다음주에는 이걸 만들기 위해서 이런걸 공부하고 싶어!&quot;
이런 마음을 가지고 스스로 공부를 하는 것이 아니라 수동적으로 학습에 임하게 됩니다. </p>
<p>저의 경우에는 하고 싶은 공부와 공부를 통해 얻고 싶은 것이 명확할 때 몰입하여 학습하고, 그 속도도 훨씬 빠릅니다. </p>
<p>다만 강의를 들을 때는 졸리기도 하고, 다음날 공부할 주제는 강사님이 정해주시기 때문에 많은 생각을 하지 않고 수강합니다. 
그러다 보니 듣고, 쓰고, 따라 치기 바쁠 때도 많았습니다. 가끔은 지금 하고 있는 공부가 왜 필요한지 파악하지도 못했던 것 같습니다😭</p>
<h3 id="kernel-만의-특별한-교육">kernel 만의 특별한 교육</h3>
<p>kernel 360에서 크루들은 주로 프로젝트, 동료 학습, 멘토링을 통해 성장합니다. 크루들의 성장 속도가 매우 빠른 이유는 여기에 있다고 생각합니다!</p>
<blockquote>
<p><strong>학습의 주도권</strong>은 그 누구도 아닌 <strong>내가</strong> 가지고 있기!</p>
</blockquote>
<p>kernel 360에서는 어떤 공부가 언제 필요한지를 스스로 깨닫게 됩니다. 공부를 했다면 그 성과가 있어야 할 텐데요! 과정 내내 프로젝트가 진행되기 때문에 학습한 결과를 곧장 적용하여 내 것으로 만들 수 있습니다.</p>
<p><strong>학습 동기</strong>가 쉽게 생기기에, <strong>학습 목표가 뚜렷</strong>하고 <strong>단기간에 빠른 성장</strong>을 하게 됩니다.</p>
<p>또한 동료 학습을 통해 서로의 지식들을 흡수하게 됩니다. 하루에 적게는 9시간, 많게는 15시간까지 크루들과 함께 있게 되면 정말 많은 지식을 서로에게 배울 수 있게 되는 것 같습니다!</p>
<p><strong>학습에 대한 동기</strong>와 <strong>동료 학습</strong>이 주는 효과는 정말 매우 컸습니다! 저의 경우 혼자서는 강의로 6개월 동안 할 공부를 kernel에서는 한 달만에 했던 것 같습니다.</p>
<p>디렉터님께서 개발 분야에서 학습이라는 것은 지식을 많이 기억하고 있다기보다 <strong>&quot;만들 수 있는가?&quot;</strong>에 대한 것이라고 말씀해주셨습니다.</p>
<p>강의를 아무리 열심히 듣고 따라 치고 필기를 하더라도 그 강의의 코드를 혼자 처음부터 끝까지 만들 수 없다면 제대로 학습했다고 말하기는 어렵습니다. 제 것이 아니라 그 강의의 코드이니까요.</p>
<p>그런데 <strong>kernel에서는 처음부터 스스로 만듭니다</strong>. 정말 많은 어려움을 마주하지만, 디렉터님과 운영 매니저님들, 멘토님들, 그리고 크루분들께 많은 도움을 받아 직접 만들 수 있게 됩니다. </p>
<p>처음에는 방법을 몰라 헤매고 많이 어렵기도 하지만 결국에 만들 줄 알게 되는 경험은 아주 크다고 생각합니다. 강의가 만들어준 것이 아니라 내가 실패하며 직접 만들어낸 것이기 때문이죠.</p>
<p>초중고, 대학까지 강의 위주로 진행되는 주입식 교육에 지치셨다면, 꼭 kernel에서 공부해보셨으면 좋겠습니다! ㅎㅎ</p>
<h2 id="ice-breaking-시간">Ice Breaking 시간</h2>
<p>여러분들의 MBTI는 무엇인가요? 앞글자만 밝히자면 저는 E입니다.. ㅎㅎ 그런데 혼자 공부하는 몇개월동안 정말 낯도 많이 가리고 점점 I가 되어 가는 느낌이었습니다 🤣 처음 보는 사람들과 대화하는 것이 역시 편하지는 않은데요.. !</p>
<p>Kernel 360에서는 이러한 크루들의 걱정을 미리 알고 있었던 것 같습니다! 개발 부트캠프의 아이스 브레이킹에서는 무엇을 할까요?</p>
<p>저는 딱딱한 자기소개를 하고 자바를 어디까지 아는지.. Spring은 쓸 줄 아는지.. 이런 이야기가 오가는 어색한 담소 시간일거라고 생각했습니다.ㅎㅎ</p>
<p>그런데 전혀 아니었습니다! 무려 젓가락으로 높이 높이 탑 쌓기 게임을 했습니다🙄</p>
<p>어색함도 잠시, 모든 크루들이 열심히 힘을 모아 젓가락 탑을 쌓기 시작했습니다. 이런저런 전략을 고민하며 고무줄로 나무 젓가락을 엮어 높은 탑을 만들었습니다!</p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/31e5d8ef-9430-4c6e-b998-9558ceb6dcc3/image.jpg" alt=""></p>
<p>정말 열심히 만들고 있네요! ㅎㅎ 놀랍게도 저희 팀이 1등을 했습니다!! 역시 언변 능력은 아주 중요한 것 같습니다..</p>
<p>탑을 만들고 돌아가며 탑에 대한 소개를 했는데, 저희 팀원분이 정말 말씀을 잘해주셔서 1등을 했던 것 같습니다. 😄 </p>
<p>정말 특별한 Kernel 360 만의 아이스 브레이킹 시간이었던 것 같습니다!</p>
<h2 id="마치며">마치며</h2>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/4ca18df3-0c88-467e-9cd0-2ab34ba6b507/image.jpeg" alt=""></p>
<p>돌아오는 길에는 이렇게 예쁜 삼성동의 하늘을 볼 수 있었습니다. 처음 모든 분들을 뵙고 인사하는 자리이기도 했기 때문에 많이 긴장이 되었는지 무지 피곤하더라고요 ㅎㅎ 그렇지만 아주 좋은 시작의 기운을 받았던 것 같습니다! 앞으로의 6개월을 알차게 채워나가보려고 합니다. </p>
<p>긴 글 읽어주셔서 감사합니다 🍀</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git, Github 특강]]></title>
            <link>https://velog.io/@carol_ly/Git-Github-%ED%8A%B9%EA%B0%95</link>
            <guid>https://velog.io/@carol_ly/Git-Github-%ED%8A%B9%EA%B0%95</guid>
            <pubDate>Mon, 22 Jul 2024 01:39:00 GMT</pubDate>
            <description><![CDATA[<h2 id="협업과-코드관리">협업과 코드관리</h2>
<ul>
<li>command 이용해 사용하는 것을 추천</li>
</ul>
<p><a href="https://daunje0.tistory.com/6">https://daunje0.tistory.com/6</a></p>
<blockquote>
<p>github의 wiki나 다양한 기능을 활용하자!</p>
</blockquote>
<blockquote>
<p>코드리뷰</p>
</blockquote>
<ul>
<li><p>코드리뷰를 요청한 개발자가 미처 발견하지 못한 버그가 숨어있는 경우는 분쟁의 소지가 없음</p>
</li>
<li><p>더 좋은 코드에 대해 논의할 때는 조심스러운 상황이 발생하기도 함</p>
</li>
<li><p>&quot;더 좋은 코드&quot;가 개발자간에 다를 수 있기 때문</p>
</li>
<li><p>코드리뷰의 유형</p>
<ul>
<li>질의응답</li>
<li>칭찬 ^^ 선칭찬 후요구</li>
<li>이해한 내용 확인하기</li>
</ul>
</li>
<li><p>코드리뷰 요청자는 작업 내용을 최대한 작게 가져간다</p>
</li>
</ul>
<ul>
<li><p>Comment: 버그가 있는 코드는 아니지만 가급적 수정해줬으면 좋겠다는 의도 전달</p>
</li>
<li><p>Approve: 코멘트는 전달하지만 문제가 있는 코드는 아니므로 수용 여부는 개발자가 결정</p>
</li>
<li><p>Request Changes: 반드시 수정해야 하는 코멘트를 전달(버그를 유발하는 코드)</p>
<ul>
<li>거의 안씀</li>
</ul>
</li>
</ul>
<br>

<h2 id="브랜치-관리-전략">브랜치 관리 전략</h2>
<ul>
<li>git에서 브랜치는 개발자간의 독립적인 공간을 제공</li>
<li>브랜치 명만 보고도 해당 작업이 어떤 작업인지 유추 가능</li>
<li>원격에 브랜치가 쌓이는 것은 좋지 않다. merge 이후 삭제?</li>
</ul>
<blockquote>
<p>GitFlow</p>
</blockquote>
<p>master branch는 언제든지 문제없이 빌드되어야 하고 배포될 수 있는 고결한 상태를 유지한다.</p>
<h4 id="새로운-배포">새로운 배포</h4>
<ul>
<li><p>master -&gt; develop -&gt; release</p>
</li>
<li><p>develop branch에는 여러 feature branch가 모인다. 버그 발생, 빌드 안됨 등이 발생 가능하다. 런타임 동안 알 수 있기 때문에 release에서 QA가 진행되며 바로 바로 수정하고 commit이 쌓일 것이다.</p>
</li>
<li><p>작업이 완료되면 master branch에 올라간다.</p>
</li>
</ul>
<h4 id="branch-종류">branch 종류</h4>
<ul>
<li>feature
  기능개발을 위해 develop으로부터 생성하는 브랜치</li>
<li>develop
  완료된 feature 브랜치들이 머지되는 브랜치, 배포 간격 사이에 추가 기능들이 머지되어 있음</li>
<li>hotfix
  이미 배포되어 있는 상태에서 급히 수정이 필요한 버그가 있을 경우 master로부터 생성되는 브랜치, 수정 이후 master와 develop으로 머지</li>
<li>release
  배포 시점이 다가오면 develop으로부터 생성</li>
</ul>
<p>정기배포가 있는 환경에 적합하며 수시배포 환경에선 사용되는 브랜치가 많음</p>
<br>

<h2 id="실무에서-개발-시-작업-흐름">실무에서 개발 시 작업 흐름</h2>
<blockquote>
<p>이슈 관리 도구(jira)</p>
</blockquote>
<ul>
<li><p>머지된 기능은 테스트서버에 배포되어 테스트 진행</p>
</li>
<li><p>테스트 완료 후 프로덕션 수준까지 배포가 되면 이슈관리도구의 티켓 종료</p>
</li>
<li><p>Task &gt; Sub Task &gt; 건별로 PR이 갈 수가 있도록</p>
</li>
<li><p>요령이 없이 작게 올리는 것의 폐해</p>
<ul>
<li><p>&quot;작업 단위&quot;를 작게 쪼갠다는 단위</p>
</li>
<li><p>작업을 완료하지 않고 중간에 올리라는 말이 아니다. &quot;이거 아직 안되서 그래요.&quot; 이런 Comment가 오고 간다면 작업 단위를 잘못 나눈 것이다. </p>
</li>
</ul>
</li>
</ul>
<ol>
<li>CI(지속적인 통합) 관점: 잘게 나누고 master에 merge되고 배포되는 것</li>
<li>현실적인 방안
master에서 중간 branch를 하나 딴다. 거기서 개발자들이 브랜치를 각각 또 판다. feature branch에서 중간 branch에 merge한 이후 master로 들어갈 때는 큰 commit이 발생.</li>
</ol>
<br>

<h4 id="feature-브랜치">feature 브랜치</h4>
<ul>
<li>feature/configure-json</li>
<li>개인당 하나씩 브랜치를 따라. </li>
<li>merge되면 사라지는 branch 이름보다는 commit 메시지가 더 중요하지 않을까?</li>
</ul>
<br>

<h4 id="commit-메시지와-merge-전략">commit 메시지와 merge 전략</h4>
<p><a href="https://meetup.nhncloud.com/posts/122">https://meetup.nhncloud.com/posts/122</a></p>
<ul>
<li>시간이 지난 후 해당 코드변경이 왜 발생했는지를 나타내는 중요한 메시지</li>
<li>어떻게, 무엇에 대한 커밋인지보다는 왜 발생한 커밋인지를 적는게 중요</li>
</ul>
<ul>
<li>작업 완료 이후 브랜치를 머지할때 3가지 방식 존재</li>
<li>merge: feature 브랜치에서 작업한 커밋 히스토리가 그대로 main 브랜치에 포함됨</li>
</ul>
<ol>
<li><p>일반 Merge
<img src="https://velog.velcdn.com/images/carol_ly/post/568586eb-1af6-499b-ad26-f1d46865989f/image.png" alt=""></p>
</li>
<li><p>Squash Merge</p>
</li>
</ol>
<ul>
<li>feature 브랜치에서 작업한 내용을 하나의 커밋으로 합침.</li>
<li>새로운 커밋 메시지 필요</li>
<li>Issue Id도 여기서 넣으면 된다.</li>
<li>Squash Merge를 할 때 커밋 메시지를 제대로 작성하면 매번 커밋할 때마다 메시지 고민 하지 않아도 된다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/f405d4a8-9ee4-4022-aaa8-495d5986be5d/image.png" alt=""></p>
<ul>
<li>퉁쳐질 때 커밋 메시지만 잘 쓰면 됨</li>
<li>일반 Merge를 하면 처음부터 커밋 메시지에 신경을 많이 써야 한다. Main 브랜치에 들어가니까!</li>
</ul>
<ol start="3">
<li>Rebase Merge</li>
</ol>
<ul>
<li>main 브랜치에 머지 시 각 커밋들이 재배치됨</li>
<li>좋은데, 충돌나기 쉬운 방법
<img src="https://velog.velcdn.com/images/carol_ly/post/486fe30c-78a0-455a-8ee5-cb2417e72726/image.png" alt=""></li>
</ul>
<br>

<h4 id="테스트용-브랜치">테스트용 브랜치</h4>
<ul>
<li>동시에 QA</li>
<li>feature -&gt; test</li>
<li>feature -&gt; master</li>
</ul>
<br>

<h2 id="git-명령어">Git 명령어</h2>
<blockquote>
<p>Git pull</p>
</blockquote>
<p>다른 팀원이 Pull Request, Merge 완료 -&gt; pull</p>
<br>

<h4 id="git-pull의-동작-방식">git pull의 동작 방식</h4>
<ul>
<li>Fetch: 원격 저장소의 최신 변경 사항을 로컬 저장소로 가져옵니다.</li>
<li>Merge: 가져온 변경 사항을 현재 로컬 브랜치에 병합합니다.</li>
</ul>
<br>

<h4 id="코드가-지워질-수-있는-경우">코드가 지워질 수 있는 경우</h4>
<p>git pull은 단순히 원격 저장소의 변경 사항을 로컬로 가져와 병합하는 과정이기 때문에 직접적으로 코드를 삭제하지 않는다. 하지만 다음과 같은 경우 충돌이 발생할 수 있다.</p>
<ul>
<li>동일한 파일의 동일한 부분을 수정한 경우: 로컬과 원격 저장소에서 동일한 파일의 동일한 부분을 수정한 경우 충돌이 발생한다. 이 경우 Git은 자동으로 병합할 수 없기 때문에 수동으로 충돌을 해결해야 한다.</li>
<li>삭제된 파일을 수정한 경우: 원격 저장소에서 삭제된 파일을 로컬에서 수정한 경우에도 충돌이 발생할 수 있다.</li>
</ul>
<blockquote>
<p>Git Fetch</p>
</blockquote>
<p>❗ fetch 와 pull 의 차이점</p>
<p>pull 을 실행하면, 원격 저장소의 내용을 가져와 자동으로 병합 작업을 실행하게 된다.
그러나 단순히 원격 저장소의 내용을 확인만 하고 로컬 데이터와 병합은 하고 싶지 않은 경우에는 fetch 명령어를 사용하면 된다.</p>
<p>fetch 를 실행하면, 원격 저장소의 최신 이력을 확인할 수 있다. 이 때 가져온 최신 커밋 이력은 이름 없는 브랜치로 로컬에 가져오게 된다.</p>
<p>다른 작업자들의 작업물을 바로 내 로컬에 반영하려면 fetch는 잊고 pull만 하면 된다(fetch + 내 파일에 반영이 pull 이므로)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Course 3] 웹 개발 입문과 데이터베이스 Ch01. Web과 HTTP 통신에 대해서 알아보기]]></title>
            <link>https://velog.io/@carol_ly/Course-3-%EC%9B%B9-%EA%B0%9C%EB%B0%9C-%EC%9E%85%EB%AC%B8%EA%B3%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-Ch01.-Web%EA%B3%BC-HTTP-%ED%86%B5%EC%8B%A0%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@carol_ly/Course-3-%EC%9B%B9-%EA%B0%9C%EB%B0%9C-%EC%9E%85%EB%AC%B8%EA%B3%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-Ch01.-Web%EA%B3%BC-HTTP-%ED%86%B5%EC%8B%A0%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 21 Jul 2024 13:56:34 GMT</pubDate>
            <description><![CDATA[<h1 id="web-개론">WEB 개론</h1>
<blockquote>
<p>Web 이란?</p>
</blockquote>
<p>(World Wide Web, WWW, W3) 인터넷에 연결된 컴퓨터를 통해 사람들이 정보를 공유할 수 있는 전 세계적인 정보 공간을 말합니다. 
Web은 크게 세 가지로 구성됩니다. 첫 번째, URI입니다. URI는 리소스 식별자로서 특정 사이트, 특정 쇼핑 목록, 동영상 목록 등 웹 상의 모든 정보에 접근할 수 있는 주소입니다. 특정 리소스에 대해서는 유니크한 주소가 한 개만 존재합니다. 
두 번째는 HTML입니다. 이는 XML을 바탕으로한 범용 문서 포맷입니다. 서버에 특정 리소스를 요청하게 되면 HTML로 구성된 문자열 형태가 내려오게 되며 Chrome, Safari, Explorer에서 사용자가 알아보기 쉬운 형태로 표현해줍니다. 
세 번째로 HTTP입니다. 이는 어플리케이션을 컨트롤하는 프로토콜입니다. 프로토콜이란, 약속입니다. 데이터를 주고 받을 때 서로 약속한 형태를 프로토콜이라고 합니다. GET, POST, PUT, DELETE 등의 여러 Method가 존재합니다. </p>
<blockquote>
<p>HTTP란?</p>
</blockquote>
<p>HTTP란 Web에서 데이터를 주고 받는 프로토콜(약속)입니다. 실제로는 HTML, XML, JSON, Image, Voice, Video, Javascript, PDF 등 다양한 컴퓨터에서 다룰 수 있는 것은 모두 전송할 수 있습니다. HTTP는 TCP를 기반으로 한 REST의 특징을 모두 구현하고 있는 Web 기반의 프로토콜입니다. </p>
<ul>
<li>특정한 데이터를 상대방에게 보내는 행위를 통신이라고 합니다.</li>
</ul>
<blockquote>
<p>통신을 간략하게 설명</p>
</blockquote>
<p>데이터를 요청하는 쪽에서 어떠한 데이터를 받고 싶다는 요청 메시지를 작성을 합니다. 이는 주소 형태일 수도 있고, 메시지 바디가 채워져 있을 수도 있습니다. 서버의 HTTP 통신을 이용하여 특정 데이터에 대한 요청 메시지를 전송합니다. 이때 어디로 요청을 하겠다는 URI가 필요합니다. 서버에서는 항상 리슨하고 있다가 요청 메시지를 수신합니다. 이를 해석하고 애플리케이션을 할당하여 로직을 통해 HTML, JSON 형태의 데이터로 응답 메시지를 송신합니다. 클라이언트는 응답 메시지를 수신하고 해석하여 브라우저로 데이터를 표시하고 처리합니다.</p>
<ul>
<li><p>멱등성
동일한 연산을 여러 번 수행해도 결과가 달라지지 않는 성질</p>
</li>
<li><p>안정성
호출해도 리소스가 변경되지 않는 성질
GET 메서드는 단순히 데이터를 조회하는 기능을 수행하기 때문에 리소스를 변경 및 수정하지 않으니 안전한 HTTP 메서드이다.
반면에 POST PUT PATCH DELETE와 같은 메서드들은 호출할 경우 데이터에 변경이 발생하거나, 서버에서 삭제되기 때문에 안전하지 않는 HTTP 메서드라고 볼 수 있다. </p>
</li>
</ul>
<blockquote>
<p>GET 메서드</p>
</blockquote>
<p>리소스를 취득할 때 사용하는 HTTP 메서드입니다. (R) 멱등성, 안정성을 보장합니다. URI의 주소에 대해서 Path Variable(ex. /naver.com/mail/)을 가질 수 있으며 Query Parameter(ex. ?name=&#39;&#39;)를 통해서 조작할 수 있습니다. http 바디에 데이터를 실을 수 없습니다. GET은 리소스를 취득할 때 쓰고 데이터 바디를 쓰지 않기로 약속했습니다. </p>
<blockquote>
<p>POST 메서드</p>
</blockquote>
<p>리소스를 생성하고 추가할 때 사용하는 HTTP 메서드입니다. (C) 요청을 할 때마다 서버에 새로운 데이터가 생기기 때문에 멱등하지 않습니다. 서버에 데이터를 요청할 때마다 서버의 데이터 상태가 바뀌기 때문에 안정성을 가지지 않습니다. 주소에 Path Variable을 가질 수 있습니다. Query Parameter의 용도는 특정 리소스를 찾기 위해 필터를 걸 때 사용을 하는 것이기 때문에 POST 메서드에서는 사용하지 않습니다. HTTP Data Body에 어떤 리소스를 생성할 것인지 작성할 수 있습니다.</p>
<blockquote>
<p>PUT 메서드</p>
</blockquote>
<p>리소스를 갱신하거나 생성할 때 사용합니다. (C/U) 데이터가 없으면 생성하고 있다면 갱신합니다. 멱등하지만 안정성은 없습니다. 요청을 할 때마다 리소스를 계속해서 변경하기 때문입니다. Path Variable을 가지며 Query Parameter는 사용하지 않습니다. Data Body는 가집니다.</p>
<blockquote>
<p>DELETE 메서드</p>
</blockquote>
<p>리소스를 삭제할 때 사용합니다. (D) 멱등하지만 안정성은 없습니다. Path Variable을 가지며 Query Parameter를 사용할 수 있씁니다. Data Body는 사용하지 않습니다.</p>
<blockquote>
<p>HTTP 상태 코드</p>
</blockquote>
<p>클라이언트가 서버에 요청을 했을 때 응답에 대한 코드입니다. </p>
<ul>
<li>100번대: 처리가 계속되고 있는 상태입니다. 클라이언트는 요청을 계속 하거나 서버의 지시에 따라서 재요청합니다. </li>
<li>200번대: 요청이 성공한 상태입니다.  <ul>
<li>PUT 메서드의 상태 코드</li>
</ul>
</li>
<li>200: 데이터가 수정됨 201: 데이터가 생성됨</li>
<li>300번대: 다른 리소스로 리다이렉트합니다. 자동으로 Response의 새로운 주소(리소스)로 다시 요청합니다.</li>
<li>400번대: 클라이언트의 요청에 에러가 있는 상태입니다. 재전송을 하여도 에러가 해결되지 않습니다.</li>
<li>500번대: 서버 처리 중 에러가 발생한 상태입니다. 재전송 시 에러가 해결될 수도 있습니다. </li>
</ul>
<h1 id="rest-api-개론">REST API 개론</h1>
<blockquote>
<p>REST란?</p>
</blockquote>
<p>네트워크 아키텍처 원리 중 하나입니다. </p>
<ol>
<li><p>Client, Server
클라이언트와 서버가 독립적으로 분리되어져 있는 상태여야 합니다. 떨어져 있는 상태에서 서로의 자원의 상태를 주고받습니다.</p>
</li>
<li><p>Stateless
요청에 대해서 클라이언트의 상태를 서버에 저장하지 않습니다. </p>
</li>
<li><p>캐시
클라이언트는 서버의 응답을 캐시할 수 있어야 합니다. 클라이언트가 캐시를 통해서 응답을 재사용할 수 있어야 하며, 이를 통해서 서버의 부하를 낮춥니다.</p>
</li>
<li><p>계층화
서버와 클라이언트 사이에 방화벽, 게이트웨이, Proxy 등 다계층 형태를 구성할 수 있어야 하며, 확장할 수 있어야 합니다.</p>
</li>
<li><p>인터페이스 일관성
아키텍처를 단순화시키고 작은 단위로 분리하여서, 클라이언트, 서버가 독립적으로 개선될 수 있어야 합니다.</p>
</li>
</ol>
<ul>
<li><p>자원 식별
웹 기반의 REST에서는 URI를 사용하여 리소스에 접근합니다. </p>
</li>
<li><p>메시지를 통한 리소스 조작
Web에서는 다양한 방식으로 데이터를 전송할 수 있습니다. 그 중에는 HTML, XML, JSON, TEXT 등 다양한 방법이 있습니다. 이 중에서 리소스의 타입을 알려주기 위해서 header 부분에 content-type(헤더)를 통해서 어떠한 타입인지를 지정할 수 있습니다. </p>
</li>
<li><p>자기 서술적 메시지
요청하는 데이터가 어떻게 처리되어야 하는지 충분한 데이터를 포함할 수 있어야 합니다. HTTP 기반의 REST 에서는 HTTP Method와 Header의 정보로 이를 표현할 수 있습니다.</p>
</li>
<li><p>애플리케이션 상태에 대한 엔진으로서 하이퍼미디어
REST API를 개발할 때에도 단순히 Client 요청에 대한 데이터만 내리는 것이 아닌 관련된 리소스에 대한 Link 정보까지 같이 포함되어야 합니다. </p>
</li>
</ul>
<ol start="6">
<li>Code On Demand</li>
</ol>
<blockquote>
<p>URI란?</p>
</blockquote>
<p>Uniform Resource Identifier의 약자로 인터넷에서 특정 자원을 나타내는 주소값이며 해당 값은 유일합니다.</p>
<blockquote>
<p>URL이란?</p>
</blockquote>
<p>Uniform Resource Loader의 약자로  인터넷 상에서의 자원, 특정 파일이 어디에 위치하는지 식별하는 주소입니다. </p>
<blockquote>
<p>URI와 URL의 차이는?</p>
</blockquote>
<p>URL은 URI의 하위 개념입니다. URL은 리소스의 위치를 지정하는 반면, URI는 리소스를 식별합니다. URL은 실제 위치를 가리키는 역할을 하며 URI는 리소스를 찾는 것에 중점을 둡니다.</p>
<p>URI = 식별자, URL = 식별자 + 위치</p>
<ul>
<li>naver.com은 URI입니다. 리소스의 이름만 나타내기 때문입니다.</li>
<li><a href="https://naver.com%EC%9D%80">https://naver.com은</a> URL입니다. 이름과 더불어, 어떻게 도달할 수 있는지 위치까지 함께 나타내기 때문입니다. (프로토콜 &#39;https&#39; 포함)</li>
</ul>
<p>URL은 어떻게 위치를 찾고 도달할 수 있는지까지 포함되어야 하기 때문에 URL은 프로토콜 + 이름(또는 번호)의 형태여야 합니다. </p>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/8dd78c1e-5a44-4c7b-b27f-c66d6d7fc179/image.png" alt=""></p>
<ul>
<li>Scheme: 리소스에 접근하는 데 사용할 프로토콜. 웹에서는 http 또는 https를 사용</li>
<li>Host: 접근할 대상(서버)의 호스트 명</li>
<li>Path: 접근할 대상(서버)의 경로에 대한 상세 정보</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 부트의 정석] Part4. JPA]]></title>
            <link>https://velog.io/@carol_ly/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%9D%98-%EC%A0%95%EC%84%9D-Part4.-JPA</link>
            <guid>https://velog.io/@carol_ly/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%9D%98-%EC%A0%95%EC%84%9D-Part4.-JPA</guid>
            <pubDate>Sun, 21 Jul 2024 13:43:51 GMT</pubDate>
            <description><![CDATA[<h2 id="jpa의-개요와-설정">JPA의 개요와 설정</h2>
<blockquote>
<p>JPA(Java Persistence API)란?</p>
</blockquote>
<p>ORM을 위한 Java 표준 API</p>
<ul>
<li>ORM: 객체, 관계형 데이터베이스를 맵핑</li>
<li>인터페이스 집합(JDBC)</li>
</ul>
<p>JPA 구현체: HIBRNATE, OpenJpa, EclipseLink, DataNucleus, ...</p>
<p>Persistence(영속성): 애플리케이션 종료 후에도 객체(데이터)가 유지되는 것</p>
<blockquote>
<p>ORM(Object/Relation Mapping) Framework - HIBERNATE</p>
</blockquote>
<ul>
<li>JPA API 구현</li>
<li>객체와 DB 테이블 간의 연결을 해주는 프레임워크</li>
<li>객체 모델(object model)과 관계형 모델(relational model)의 차이를 해소</li>
</ul>
<ul>
<li><p>객체는 참조로 연결된 형태</p>
</li>
<li><p>RDB는 고정되어 있는 2차원 테이블</p>
</li>
<li><p>이 두 모델 간의 변환을 HIBERNATE(Orm Framework)이다.</p>
</li>
</ul>
<blockquote>
<p>객체 모델과 관계형 모델의 비교</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/2fc8af65-8c77-4455-9a6f-b79ee800489f/image.png" alt=""></p>
<blockquote>
<p>JPA의 개요와 설정
SQL Mapper(MyBatis) vs ORM framework(HIBERNATE)</p>
</blockquote>
<ol>
<li>SQL Mapper
DB 중심 개발</li>
</ol>
<ul>
<li>SQL 직접 작성</li>
<li>데이터 저장할 클래스 작성</li>
<li>MyBatis</li>
<li>큰 회사들이 많다.</li>
</ul>
<p>DB에 변경 사항 발생 시 고쳐야 할 것이 많다.</p>
<ol start="2">
<li>ORM framework(HIBERNATE)</li>
</ol>
<ul>
<li><p>자바 클래스 작성</p>
<ul>
<li>변경 사항 발생 시 Entity만 변경하면 HIBERNATE ORM이 자동 반영</li>
</ul>
</li>
<li><p>HIBERNATE ORM이 테이블 자동 생성</p>
<ul>
<li>SQL문 자동 생성</li>
<li>유지보수에 유리</li>
<li>복잡한 일처리, 성능 면에서 문제</li>
</ul>
</li>
</ul>
<p>둘 다 장단점이 존재한다. 오래된 기업들은 RDB가 중요하고 실무에서 복잡한 일처리하기 위해서는 JPA로 하기 어렵다. DB 중심 개발을 바꾸기가 쉽지 않다.</p>
<blockquote>
<p>Spring Data
저장소 종류가 달라도 일관된 데이터 처리 방법을 제공</p>
</blockquote>
<br>

<h2 id="엔터티-매니저-팩토리와-엔터티-메니저">엔터티 매니저 팩토리와 엔터티 메니저</h2>
<blockquote>
<p>EntityManager와 EntityManagerFactory</p>
</blockquote>
<p>EntityManagerFactory: EntityManager를 생성. 애플리케이션에 하나
EntityManager: Entity를 관리하는 객체. Entity를 저장 관리. 사용자당 하나. 공유 불가</p>
<p>사용자는 직접 DB에 명령내리는 대신, EntityManager로만 작업</p>
<p>저장은 persist(), 조회는 find(), 삭제는 remove(), 변경은 Entity의 setter를 이용</p>
<blockquote>
<p>Entity 클래스의 작성</p>
</blockquote>
<p>Entity 클래스: DB 테이블의 한 행(row)을 정의한 것.</p>
<ul>
<li>클래스에 @Entity를 붙인다.</li>
<li>키(PK)로 사용할 속성에 @Id를 붙인다.</li>
</ul>
<p>*** 순서는 보장되지 않는다.</p>
<p>database에서는 대소문자 구분하지 않는다.</p>
<blockquote>
<p>Entity 클래스의 작성을 위한 애너테이션</p>
</blockquote>
<p>@Generated
자동 번호 생성을 적용할 속성에 사용</p>
<p>GenerationType.TABLE
GenerationType.SEQUENCE
GenerationType.IDENTITY
GenerationType.AUTO</p>
<blockquote>
<p>EntityTransaction</p>
</blockquote>
<pre><code class="language-java">EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction tx = entityManager.getTransaction(); //트랜잭션 얻기

User user = new User(); // User 객체를 생성하고 초기화
user.setId(&quot;aaa&quot;);
user.setPassword(&quot;1234&quot;);

tx.begin(); // 트랜잭션을 시작
entityManager.persist(user); // User 객체를 저장
tx.commit(); // 트랜잭션을 종료(작업 내용을 DB에 반영)</code></pre>
<p>Hibernate가 SQL을 자동 생성한다. </p>
<blockquote>
<p>PreparedStatement</p>
</blockquote>
<ol>
<li>성능</li>
<li>보안</li>
</ol>
<blockquote>
<p>Transaction이란?
더 이상 나눌 수 없는 작업의 단위</p>
</blockquote>
<ul>
<li>모두 성공하지 않으면 실패</li>
<li>하나 이상의 작업을 묶은 것이 Transaction이다.</li>
</ul>
<blockquote>
<p>Commit과 Rollback</p>
</blockquote>
<ol>
<li>커밋: 작업 내용을 DB에 영구적으로 저장</li>
<li>롤백: 최근 변경사항을 취소(마지막 커밋으로 복귀)</li>
</ol>
<blockquote>
<p>PersistenceContext = PC = Entity 저장공간</p>
</blockquote>
<p>*** AC = Application Context = 빈(bean) 저장소</p>
<ul>
<li>entity를 저장하는 공간</li>
<li>EntityManager마다 PersistenceContext를 하나씩 가지고 있다.</li>
</ul>
<h4 id="비영속-상태와-영속-상태">비영속 상태와 영속 상태</h4>
<ul>
<li><p>처음 entity를 생성 시 비영속 상태이다.     </p>
<ul>
<li>PersistenctContext에 저장되지 않았다.</li>
</ul>
</li>
<li><p><code>em.persist(entity)</code> : 비영속 상태의 entity가 PersistenceContext에 저장되고 엔터티를 영속 상태로 바꾸고, SQL 저장소에 INSERT문을 저장한다.</p>
</li>
<li><p><code>em.remove(entity)</code> : Entity Manager가 PersistenceContext에서 해당 Entity를 제거</p>
</li>
<li><p><code>em.flush()</code> : db에 sql이 전송된다. SQL 저장소에 누적된 SQL문을 DB로 전달. <code>tx.rollback()</code> 가능</p>
</li>
</ul>
<h4 id="persistencecontext의-캐시">PersistenceContext의 캐시</h4>
<ul>
<li>실제 Entity가 저장되는 공간(Map). Entity의 @Id컬럼을 Key로 사용</li>
<li>key와 value</li>
<li><code>em.find(User.class, &quot;a&quot;)</code> : 캐시에서 Entity를 먼저 조회. 캐시에 없을 때만 DB에서 조회한다.</li>
<li><code>em.clear()</code>를 통해 캐시를 비운다. </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redirect와 Forward]]></title>
            <link>https://velog.io/@carol_ly/Redirect%EC%99%80-Forward</link>
            <guid>https://velog.io/@carol_ly/Redirect%EC%99%80-Forward</guid>
            <pubDate>Sat, 20 Jul 2024 10:37:36 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>redirect와 forward의 처리 과정 비교</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/ccbcf992-7f7b-433c-835e-88817a0d998c/image.png" alt=""></p>
<ul>
<li>2번 요청, 2번 응답</li>
</ul>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/55ecd282-b282-4ac3-a1ac-23beaa029831/image.png" alt=""></p>
<ul>
<li>요청을 받아서 (request 객체) 다른 프로그램으로 그대로 전달</li>
<li>1번 요청, 1번 응답</li>
</ul>
<blockquote>
<p>RedirectView</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/b045237b-d7fb-494a-929b-c4bfdff263a9/image.png" alt=""></p>
<blockquote>
<p>HTTP 요청과 요청 방법</p>
</blockquote>
<ol>
<li>URL 직접 입력으로 요청(GET)</li>
<li>링크 <code>&lt;a&gt;</code>로 요청(GET)</li>
<li>폼 <code>&lt;form&gt;</code>으로 요청(GET, POST)</li>
<li>redirect: 다른 URL로 이동(GET). 자동 재요청. 브라우저 URL 변경됨.</li>
<li>forward: 요청(GET, POST)을 다른 URL로 전달. 브라우저 URL 변경 안됨.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 부트의 정석] Part2. 스프링 부트 시작하기]]></title>
            <link>https://velog.io/@carol_ly/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%9D%98-%EC%A0%95%EC%84%9D-Part2.-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@carol_ly/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%9D%98-%EC%A0%95%EC%84%9D-Part2.-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 19 Jul 2024 16:22:58 GMT</pubDate>
            <description><![CDATA[<p>static 메서드는 객체 생성 없이 호출 가능하다. 그렇다면 원격 프로그램은 어떻게 실행할 수 있을까?</p>
<blockquote>
<p>원격 프로그램의 실행</p>
</blockquote>
<p>웹 브라우저 + WAS(톰캣) -&gt; 원격 프로그램의 실행</p>
<ol>
<li>프로그램 등록
@Controller 을 통해 프로그램 등록</li>
<li>URL과 프로그램을 연결
이 URL을 연결했을 때 실행될 메서드를 연결해준다.</li>
</ol>
<pre><code class="language-java">@Controller // 1. 프로그램 등록
public class Hello {
    @RequestMapping(&quot;/hello&quot;) // 2. URL과 main()을 연결
    public void main() {
        System.out.println(&quot;Hello&quot;);
    }
}</code></pre>
<p>원래 static이 없다면 객체를 생성해야 한다. 다만, @Controller 어노테이션이 붙은 프로그램은 스프링이 자동으로 객체를 생성해주기 때문에 인스턴스 메서드(static이 안 붙은 메서드)가 URL이 연결되어 다른 컴퓨터에서 호출될 수 있다.</p>
<p>** 인텔리제이 단축키
패키지를 선택한 상태에서 Command + N 
해당 패키지에 파일 추가</p>
<p>** github</p>
<ul>
<li>git add .: 현재 디렉토리와 그 하위 디렉토리에서 변경된 파일들만 스테이징한다. 새로운 파일과 변경된 파일을 스테이징하지만, 이미 삭제된 파일은 스테이징하지 않는다. </li>
<li>git add -A: 리포지토리 전체에서 모든 변경 사항을 스테이징한다. 새로운 파일, 변경될 파일뿐만 아니라 삭제된 파일도 스테이징한다.</li>
</ul>
<p>따라서, 프로젝트 전체에서 변경 사항을 커밋하려는 경우 &#39;git add -A&#39;를 사용하는 것이 더 안전하며, 특정 디렉토리에서만 작업하는 경우 &#39;git add .&#39;를 사용할 수 있다.</p>
<h2 id="클라이언트와-서버">클라이언트와 서버</h2>
<p>클라이언트와 서버는 역할로 구분한다. </p>
<blockquote>
<p>서버의 종류</p>
</blockquote>
<ul>
<li>Email을 서비스하면 Email Server</li>
<li>Web을 서비스하면 Web Server</li>
</ul>
<p>브라우저를 통해 사용하는 서비스들은 Web Server를 통해 이루어진다.</p>
<p>한 대의 컴퓨터에 서버 프로그램이 여러 개 설치되어 있다면 같은 IP 내에 서비스가 존재하기 때문에 포트가 필요하다. 서버 프로그램은 특정 포트와 연결되어 있다. Email Server는 25번 포트, File Server는 22번 포트, Web Server는 80번 포트를 기본적으로 사용한다. </p>
<blockquote>
<p>WAS란?</p>
</blockquote>
<p>Web Application을 서비스하는 서버이다.</p>
<ul>
<li>Application = Server</li>
</ul>
<p>클라이언트가 애플리케이션을 사용할 수 있게 해준다. 서버에 있는 프로그램을 클라이언트에게 서비스해주는 프로그램이다.</p>
<blockquote>
<p>HTTP 요청과 응답</p>
</blockquote>
<ol>
<li>DNS에 문의</li>
</ol>
<p>사용자는 브라우저에 URL을 입력하게 된다. 이때 DNS 서버가 도메인 이름(fastcampus.co.kr)이 IP 주소(111.222.33.44)를 알려준다. 해당 IP 주소가 있는 웹 서버에 요청이 간다. </p>
<ol start="2">
<li>TCP 연결(3-way handshake)</li>
</ol>
<ul>
<li>SYN / ACK + SYN / ACK</li>
<li>연결을 하면 두개의 Stream이 생긴다. I/O Stream</li>
</ul>
<ol start="3">
<li><p>HTTP 요청</p>
</li>
<li><p>HTTP 응답</p>
</li>
</ol>
<ul>
<li>헤더 + 바디(html)</li>
</ul>
<ol start="5">
<li>응답을 수신하고 연결을 종료 (재사용)</li>
</ol>
<h2 id="원격-프로그램에-데이터-전달하기">원격 프로그램에 데이터 전달하기</h2>
<blockquote>
<p>HttpServletRequest</p>
</blockquote>
<ol>
<li>HttpServletRequest </li>
</ol>
<ul>
<li>클래스</li>
<li>http 요청 정보를 제공</li>
<li>URL로 블라우저에서 클라이언트가 요청을 한다. Tomcat이 요청 정보를 받아서 HttpServletRequest 객체를 생성한다. 이 객체에 요청 정보를 받는 것이다. </li>
<li>메서드의 매개 변수에 <code>HttpServletRequest request</code>를 적어주면 메서드 안에서 해당 객체의 메서드를 사용할 수가 있다.</li>
</ul>
<ol start="2">
<li>HttpServletRequest의 메서드
<img src="https://velog.velcdn.com/images/carol_ly/post/c0d73211-2ad7-4b33-8847-5c1ca1855c5b/image.png" alt=""></li>
</ol>
<ul>
<li>getScheme()</li>
<li>getServerName()</li>
<li>getServerPort()</li>
<li>getContextPath(): Web Application 시작 루트 경로이다. 루트로 설정 시 없을 수도 있다.</li>
<li>getRequestURI(): 서버 Name과 Port를 제외한 부분</li>
<li>getQueryString(): ?부터 끝까지. 원격 프로그램에 data를 전달할 때 사용한다. &amp;는 구분자이다. 값의 name과 value를 구분자(&amp;)를 통해 원격 프로그램에 전달한다.</li>
<li>getRequestURL()</li>
</ul>
<p>위의 메서드를 통해 원하는 정보를 얻을 수 있다. URL을 잘라서 원하는 것을 얻을 필요가 없다.</p>
<pre><code class="language-java">String year = request.getParameter(&quot;year&quot;);
String month = request.getParameter(&quot;month&quot;);
String day = request.getParameter(&quot;day&quot;);

// Enumeration은 Iterator의 old version이다.
Enueration enum = request.getParameterNames();
Map paramMap = request.getParameterMap();

// 같은 name이 많을 때 배열로 반환한다. 
// 만약 getParameter()를 사용 시 첫 번째 value만 반환된다.
/* ex) 체크박스의 경우 
hobby=computer&amp;hobby=drawing&amp;hobby=music 
이런 식으로 들어오게 된다. */

String[] yearArr = request.getParameterValues(&quot;year&quot;);</code></pre>
<blockquote>
<p>GET과 POST의 차이점</p>
</blockquote>
<h4 id="get">GET</h4>
<p>입력한 내용이 query string으로 URL뒤에 붙는다.</p>
<ul>
<li>서버로부터 리소스를 얻어오기(=읽기)</li>
<li>요청에 body가 없다.</li>
<li>URL에 전송 데이터가 있다.</li>
<li>Query String을 통해 데이터를 전달(소용량)</li>
<li>URL에 데이터 노출되므로 보안에 취약</li>
<li>데이터 공유에 유리
ex. 검색 엔진에서 검색단어 전송에 이용</li>
</ul>
<h4 id="post">POST</h4>
<p>URL 뒤에 query string이 붙지 않고 별도로 전송이 된다.</p>
<ul>
<li>글 쓰기</li>
<li>전송할 데이터가 body에 있어서 URL에 붙이지 않는다.</li>
<li>서버에 데이터를 올리기 위해 설계됨.</li>
<li>전송 데이터 크기의 제한이 없음(대용량)</li>
<li>보안에 유리, 데이터 공유에는 불리
ex. 게시판에 글쓰기, 로그인, 회원가입</li>
</ul>
<blockquote>
<p>HTTP 요청 방법</p>
</blockquote>
<ol>
<li>URL 직접 입력 - GET</li>
<li><code>&lt;a&gt;</code> - GET</li>
<li><code>&lt;form&gt;</code> - GET이 default, POST</li>
</ol>
<h2 id="http-요청과-응답">HTTP 요청과 응답</h2>
<blockquote>
<p>프로토콜이란?</p>
</blockquote>
<p>서로 간의 통신을 위한 규칙
주고 받을 데이터에 대한 형식을 정의한 것</p>
<blockquote>
<p>HTTP</p>
</blockquote>
<p>Hyper Text Transfer Protocol
단순하고 <strong>읽기 쉽다</strong>.
<strong>텍스트 기반</strong>의 프로토콜</p>
<p>클라이언트가 브라우저에 URL을 입력하면 HTTP 요청 메시지를 만들어 서버에 요청 메시지를 보낸다. 서버는 이를 보고 HTTP 응답 메시지를 만들어 클라이언트에게 보여준다. </p>
<p>상태를 유지하지 않는다(<strong>stateless</strong>)
클라이언트 정보를 저장하지 않는다. 서버에 부담이 없어진다.</p>
<p>확장 가능하다. 
커스텀 헤더 추가 가능</p>
<blockquote>
<p>상태 코드</p>
</blockquote>
<ul>
<li>1XX: Informational(정보 제공)
임시 응답으로 현재 클라이언트의 요청까지는 처리되었으니 계속 진행하라는 의미입니다. HTTP 1.1 버전부터 추가되었습니다.</li>
<li>2XX: Success(성공)
클라이언트의 요청이 서버에서 성공적으로 처리되었다는 의미입니다.</li>
<li>3XX: Redirection(리다이렉션)
완전한 처리를 위해서 추가 동작이 필요한 경우입니다. 주로 서버의 주소 또는 요청한 URI의 웹 문서가 이동되었으니 그 주소로 다시 시도하라는 의미입니다.</li>
<li>4XX: Client Error(클라이언트 에러)
없는 페이지를 요청하는 등 클라이언트의 요청 메시지 내용이 잘못된 경우를 의미합니다.</li>
<li>5XX: Server Error(서버 에러)
서버 사정으로 메시지 처리에 문제가 발생한 경우입니다. 서버의 부하, DB 처리 과정 오류, 서버에서 익셉션이 발생하는 경우를 의미합니다.</li>
</ul>
<blockquote>
<p>텍스트 파일과 바이너리 파일</p>
</blockquote>
<ul>
<li>바이너리 파일: 문자와 숫자가 저장되어 있는 파일</li>
<li>텍스트 파일: 문자만 있는 저장되어 있는 파일</li>
</ul>
<h2 id="원격-프로그램으로-응답하기">원격 프로그램으로 응답하기</h2>
<blockquote>
<p>정적 리소스와 동적 리소스</p>
</blockquote>
<ul>
<li>정적 리소스
메서드가 호출 될 때마다 다른 결과가 반환된다.
<img src="https://velog.velcdn.com/images/carol_ly/post/e78b73a2-2ad8-4fed-9f88-4afb3b54b4b9/image.png" alt=""></li>
</ul>
<h3 id="static-폴더의-indexhtml-정적-리소스">static 폴더의 index.html (정적 리소스)</h3>
<p>src/main/resources/static 폴더에 위치한 파일들은 정적 리소스로 취급된다.
이 파일들은 서버 측 처리 없이 그대로 클라이언트에 제공된다.
따라서 localhost:8080/index.html로 직접 접근할 수 있다.
<img src="https://velog.velcdn.com/images/carol_ly/post/dabb1b72-c2c4-4c72-825b-9847061ae261/image.png" alt=""></p>
<h3 id="templates-폴더의-index2html-동적-리소스">templates 폴더의 index2.html (동적 리소스):</h3>
<p>src/main/resources/templates 폴더에 위치한 파일들은 동적 리소스로 취급된다.
이 파일들은 주로 템플릿 엔진(예: Thymeleaf)을 통해 처리되어야 한다.
<strong>직접 URL로 접근할 수 없으며, 컨트롤러를 통해 접근해야 한다.</strong></p>
<p>templates 폴더의 index2.html에 접근하려면 컨트롤러를 만들어야 한다.</p>
<pre><code class="language-java">@Controller
public class HomeController {
    @GetMapping(&quot;/index2&quot;)
    public String showIndex2() {
        return &quot;index2&quot;;
    }
}</code></pre>
<p>이후 <strong>localhost:8080/index2</strong>로 접근하면 index2.html이 보일 것입니다.</p>
<p>만약, localhost:8080/index2.html로 접근하면 error 페이지가 나온다.</p>
<p>Spring Boot는 templates 폴더의 파일을 <strong>직접 URL로 접근할 수 있는 정적 리소스</strong>로 취급하지 않는다.
따라서 해당 URL에 맞는 컨트롤러 매핑이 없으면 404 에러가 발생하고, 기본 에러 페이지가 표시된다.</p>
<h3 id="요약">요약</h3>
<p>static 폴더 </p>
<ul>
<li>정적 파일 직접 제공 (CSS, JavaScript, 이미지 등)</li>
</ul>
<p>templates 폴더</p>
<ul>
<li>동적 콘텐츠를 위한 템플릿 파일 (컨트롤러를 통해 접근)</li>
</ul>
<p>이러한 구조는 보안과 유연성을 위해 설계되었다. 동적 콘텐츠는 서버 측 로직을 통해 제어되어야 하므로, 직접 접근을 제한하고 컨트롤러를 통해 접근하도록 한다.</p>
<blockquote>
<p>HttpServletResponse로 응답하기</p>
</blockquote>
<p>Tomcat이 response 객체를 생성해서 준다. 요청 시 사용할 수 있는 객체, 응답에 사용할 수 있는 객체가 있다.</p>
<p>응답에 필요한 출력 스트림을 담아서 준다. </p>
<p><code>response.getWriter()</code></p>
<ul>
<li>출력 스트림 얻어온다.</li>
<li>응답을 얻을 수 있다.</li>
</ul>
<pre><code class="language-java">response.setStatus(200);
response.setContentType(&quot;text/html&quot;);

PrintWriter out = response.getWriter();
out.println(&quot;&lt;html&gt;&quot;);
out.println(&quot;&lt;head&gt;&lt;/head&gt;&quot;);
out.println(&quot;&lt;body&gt;Monday&lt;/body&gt;);
out.println(&quot;&lt;/html&gt;&quot;);
out.close();</code></pre>
<p>톰캣은 응답의 헤더를 기본적으로 만들어준다. 헤더는 변경과 확장이 가능하다. </p>
<blockquote>
<p>HttpServletResponse의 메서드</p>
</blockquote>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>void addCookie(Cookie cookie)</td>
<td>응답에 쿠키를 추가</td>
</tr>
<tr>
<td>void addHeader(String name, String value)</td>
<td>응답 헤더를 추가</td>
</tr>
<tr>
<td>String getHeader(String name)<br> void setHeader(String name, String value)</td>
<td>지정된 이름의 헤더의 값을 반환<br>지정된 이름의 헤더의 값을 변경</td>
</tr>
<tr>
<td>void sendRedirect(String location)</td>
<td>지정된 위치(URL)로 요청하라는 응답(3xx)를 전송</td>
</tr>
<tr>
<td>ServletOutputStream getOutputStream() <br> PrintWriter getWriter()</td>
<td>클라이언트(브라우저)와 연결된 출력 스트림을 반환</td>
</tr>
</tbody></table>
<blockquote>
<p>코드의 분리</p>
</blockquote>
<ol>
<li>관심사</li>
<li>변하는 것과 변하지 않는 것</li>
<li>중복 코드 - 별도의 메서드로 만들어 제거해야 함.</li>
</ol>
<p>OOP - 변경에 대비</p>
<blockquote>
<p>객체 지향 설계 5대 원칙 </p>
</blockquote>
<ol>
<li>단일 책임 원칙(SRP)
하나의 메서드는 하나의 책임(Concern=관심사=작업)만 가진다.</li>
<li>개방 폐쇄 원칙(OCP)
상속 Open, 변경 Closed</li>
<li>리스코프 치환 원칙(LSP)</li>
<li>인터페이스 분리 원칙(ISP)</li>
<li>의존관계 역전 원칙(DIP)</li>
</ol>
<ul>
<li>코드가 너무 구체적이면 변경에 불리</li>
<li>&quot;추상화&quot; 통해 변경에 유리</li>
</ul>
<blockquote>
<p>Spring의 Reflection API</p>
</blockquote>
<pre><code class="language-java">@Controller
public void date(@HttpservletRequest request) {

    request.getParameter(&quot;year&quot;);
    request.getParameter(&quot;month&quot;);
    request.getParameter(&quot;day&quot;);
}</code></pre>
<p>위의 코드를 아래처럼 바꿀 수 있다.</p>
<pre><code class="language-java">@Controller
public void date(int year, int month, int day) {
}</code></pre>
<p>각 매개변수에는 @RequestParam이 생략되어 있다.</p>
<blockquote>
<p>DispatcherServlet</p>
</blockquote>
<ol>
<li>입력 &amp; 변환</li>
</ol>
<ul>
<li><code>request.getParameter()</code></li>
<li>String -&gt; int</li>
</ul>
<ol start="2">
<li>모델 생성</li>
</ol>
<ul>
<li>Controller 메서드에 Model을 선언하면 DispatcherServlet이 모델 객체를 생성해서 넘겨줌. (객체의 주소를 줌) </li>
<li>뷰에 Model이 넘어오면 이를 사용할 수 있다.</li>
<li>작업 결과를 보여줄 View의 이름을 반환</li>
<li>매개변수가 참조형일 경우 원본 객체를 수정할 수가 있다.</li>
</ul>
<blockquote>
<p>컨트롤러 메서드의 반환타입</p>
</blockquote>
<ol>
<li><p>String
뷰 이름을 반환</p>
<pre><code class="language-java">@GetMapping(&quot;/getYoil&quot;)
pubic String main(int year, int month, int day) {

 return &quot;yoil&quot; // resources/templates/yoil.html
}</code></pre>
</li>
<li><p>void
맵핑된 url의 끝단어가 뷰 이름</p>
</li>
</ol>
<pre><code class="language-java">@GetMapping(&quot;/yoil&quot;) //resources/templates/yoil.html
public void main(int year, int month, int day) {
}</code></pre>
<ul>
<li>JSP 방식</li>
</ul>
<blockquote>
<p>@RestContoller와 @Controller의 차이점은?</p>
</blockquote>
<p>@RestController 어노테이션은 메소드의 반환값을 HTTP 응답 본문으로 직접 전송한다. Thymeleaf 템플릿을 사용하려면 @Controller 어노테이션을 사용해야 한다.</p>
<h2 id="requestparam과-modelattribute">@RequestParam과 @ModelAttribute</h2>
<blockquote>
<p>@ModelAttribute</p>
</blockquote>
<ul>
<li>적용 대상을 Model의 속성으로 자동 추가해준다.</li>
<li>반환 타입 또는 컨트롤러 메서드의 매개변수에 적용 가능</li>
<li>Controller 메서드 파라미터에서 참조형 앞에 사용할 경우 어노테이션 생략 가능</li>
</ul>
<ol>
<li><p>참조형에만 사용 가능하다.
ex. int, char 등 사용 불가하다.</p>
</li>
<li><p>메서드 앞에 사용</p>
<pre><code class="language-java">@ModelAttribute(&quot;yoil&quot;)
 private char getYoil(MyDate myDate) {
     /*** 생략 ***/
     char yoil = &#39;일&#39;;
     return yoil;
 }</code></pre>
</li>
</ol>
<ul>
<li>이 메서드의 반환값(<code>yoil</code>)을 &quot;yoil&quot;이라는 이름으로 <strong>모델에 자동으로 추가</strong>한다.</li>
<li>모든 요청 처리 전에 이 메서드가 실행된다. 따라서,
<code>char yoil = getYoil(myDate);</code>을 추가할 필요가 없다. 또한, main 메서드가 실행될 때 모델에는 이미 &quot;yoil&quot; 속성이 추가되어 있다.</li>
</ul>
<ol start="3">
<li><p>파라미터 앞에 사용</p>
<pre><code class="language-java">@Controller
public class YoilTeller {
 @RequestMapping(&quot;/yoil&quot;)
 public String getYoil(@ModelAttribute MyDate myDate, Model model) {

     return &quot;yoil&quot;;
}
</code></pre>
</li>
</ol>
<pre><code>
- Spring은 요청 파라미터를 MyDate 객체에 자동으로 바인딩한다.
예) &quot;/yoil?year=2023&amp;month=7&amp;day=20&quot; 요청이 오면, myDate 객체의 각 필드에 값이 설정된다.
- 생략되는 코드 
```java
model.addAttribute(&quot;myDate&quot;, date);</code></pre><ul>
<li><code>Model model</code>
뷰에 전달할 데이터를 담는 객체
*** Model은 <strong>스프링이 자동으로 주입하는 객체</strong>이므로 @ModelAttribute <strong>어노테이션을 붙일 필요가 없다</strong>.</li>
</ul>
<pre><code class="language-java">@Controller
public class YoilTeller {
    @RequestMapping(&quot;/yoil&quot;)
    public String main(@ModelAttribute MyDate date, Model model) throws IOException {

        return &quot;yoil&quot;;
    }

    @ModelAttribute(&quot;yoil&quot;)
    private char getYoil(MyDate date) {
        Calendar cal = Calendar.getInstance(); // 현재 날짜와 시간을 갖는 cal
        cal.clear(); // cal의 모든 필드를 초기화
        cal.set(date.getYear(), date.getMonth() - 1, date.getDay());

        int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
        char yoil = &quot;일월화수목금토&quot;.charAt(dayOfWeek - 1);
        return yoil;
    }
}
</code></pre>
<p>** 이때, 컨트롤러 메서드의 매개변수가 기본형일 경우 <code>@RequestParam</code>이 생략된 것이며, 참조형일 경우 <code>@ModelAttribute</code>가 생략된 것이다.</p>
<blockquote>
<p>WebDataBinder의 역할</p>
</blockquote>
<pre><code class="language-java">@RequestMapping(&quot;getYoil&quot;)
public String main(@ModelAttribute MyDate date, BindingResult result) {
}</code></pre>
<p>URL -&gt; getYoil?<strong>year=2023&amp;month=1&amp;day=2</strong></p>
<h4 id="mapstring-string">Map&lt;String, String&gt;</h4>
<table>
<thead>
<tr>
<th>name</th>
<th>value</th>
</tr>
</thead>
<tbody><tr>
<td>&quot;year&quot;</td>
<td>&quot;2023&quot;</td>
</tr>
<tr>
<td>&quot;month&quot;</td>
<td>&quot;1&quot;</td>
</tr>
<tr>
<td>&quot;day&quot;</td>
<td>&quot;2&quot;</td>
</tr>
</tbody></table>
<ol>
<li>타입 변환</li>
</ol>
<ul>
<li>string -&gt; int</li>
</ul>
<ol start="2">
<li>데이터 검증(validator)</li>
</ol>
<h4 id="mydate">MyDate</h4>
<table>
<thead>
<tr>
<th>year</th>
<th>month</th>
<th>day</th>
</tr>
</thead>
<tbody><tr>
<td>2023</td>
<td>1</td>
<td>2</td>
</tr>
</tbody></table>
<blockquote>
<p>@RequestParam</p>
</blockquote>
<ul>
<li>요청의 파라미터를 연결할 때 매개변수에 붙이는 애너테이션</li>
</ul>
<ol>
<li>HttpServletRequest</li>
</ol>
<p>서블릿(Servlet)은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 HTTP 메시지를 대신 파싱한다.
이렇게 파싱된 메시지를 HttpServletRequest 객체에 담아서 제공하는 것이다.
즉, HttpServletRequest는 서블릿이 HTTP 요청 메시지를 파싱한 결과를 담은 객체이다.
HttpServletRequest를 사용하면 HTTP 요청 메시지를 편리하게 조회할 수 있게 된다.</p>
<pre><code class="language-java">@RequestMapping(&quot;/requestParam&quot;)
public void main(HttpServletRequest request) {
    String year = request.getParameter(&quot;year&quot;);
}</code></pre>
<ol start="2">
<li>Reflection API</li>
</ol>
<pre><code class="language-java">@RequestMapping(&quot;/requestParam&quot;)
public void main(@RequestParam String year) {
}</code></pre>
<ul>
<li><p><code>request.getParameter()</code> 필요 없어짐.</p>
</li>
<li><p>여기서 더해, @RequestParam도 생략하는 것이 좋다.</p>
<ul>
<li><code>@RequestParam(name=&quot;year&quot;, required=true)</code></li>
<li>year이 필수값이 된다.</li>
</ul>
</li>
<li><p>@RequestParam이 붙었다면 반드시 client에서 year 정보가 넘어와야 한다.</p>
</li>
<li><p><a href="http://localhost/requestParam">http://localhost/requestParam</a></p>
<ul>
<li>400 Bad Request</li>
</ul>
</li>
<li><p><a href="http://localhost/requestParam?year">http://localhost/requestParam?year</a></p>
<ul>
<li>빈 문자열로 처리되어 값이 넘어온 것으로 본다. </li>
</ul>
</li>
</ul>
<ol start="3">
<li>WebDataBinder 덕분에 .. @RequestParam도 생략 가능</li>
</ol>
<pre><code class="language-java">@RequestMapping(&quot;/requestParam&quot;)
public void main(String year) {
}</code></pre>
<ul>
<li><p><code>required = false</code></p>
<ul>
<li>year이 넘어오지 않아도 null로 처리되며 에러가 발생하지 않는다. </li>
</ul>
</li>
<li><p><a href="http://localhost/requestParam">http://localhost/requestParam</a></p>
</li>
<li><p><a href="http://localhost/requestParam?year">http://localhost/requestParam?year</a></p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/carol_ly/post/08099eab-3767-4be7-8236-e5e1f40f3a24/image.png" alt=""></p>
<p>브라우저를 통해서 요청받은 값이 실제 서버의 객체에 바인딩 될 때, 중간 역할을 한다.</p>
<ol>
<li>타입변환</li>
<li>데이터 검증</li>
</ol>
<p>➡️ 변환 결과나 에러는 BindingResult에 저장</p>
<ol start="4">
<li>WebDataBinder의 타입 변환 </li>
</ol>
<pre><code class="language-java">@RequestMapping(&quot;/requestParam&quot;)
public void main(int year) {
}</code></pre>
<ul>
<li><p><a href="http://localhost/requestParam">http://localhost/requestParam</a></p>
<ul>
<li><p>500 error(server)</p>
</li>
<li><p><code>required=false</code>이기에 client는 잘못한 것이 없다.</p>
</li>
<li><p>year = null</p>
</li>
<li><p>null을 int로 타입 변환하려고 하니까 error 발생</p>
</li>
</ul>
</li>
<li><p><a href="http://localhost/requestParam?year">http://localhost/requestParam?year</a></p>
<ul>
<li>400 error(client)</li>
<li>year = &quot;&quot;</li>
<li>빈 문자열은 int로 변환될 수 없다.</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@RequestMapping(&quot;/requestParam&quot;)
public void main(@RequestParam(required=false, defaultValue=&quot;1&quot;) int year) {
}</code></pre>
<ul>
<li>default value를 설정함으로써 사용 가능</li>
</ul>
<h2 id="로그인-화면-만들기">로그인 화면 만들기</h2>
<blockquote>
<p>@RequestMapping, @GetMapping, @PostMapping</p>
</blockquote>
<ul>
<li>@RequestMapping의 default method는 GET이다.</li>
</ul>
<pre><code class="language-java">@Controller
@RequestMapping
public class LoginController {

    @RequestMapping(&quot;/login/login&quot;, method = RequestMethod.GET)
    public String login(){
        return &quot;login&quot;;

}</code></pre>
<pre><code class="language-java">@Controller
@RequestMapping
public class LoginController {

    @GetMapping(&quot;/login/login&quot;)
    public String login(){
        return &quot;login&quot;;

}</code></pre>
<p>두 코드는 동일하다. URL이 같아도 method가 다르면 서버가 요청을 구별할 수 있다.</p>
<p>만약, 하나의 메서드로 GET, POST를 둘 다 처리하는 경우에는 @RequestMapping으로 사용해야 한다. </p>
<pre><code class="language-java">@Controller
@RequestMapping
public class LoginController {

    @RequestMapping(&quot;/login/login&quot;, method = {RequestMethod.GET, RequestMethod.POST})
    public String login(){
        return &quot;login&quot;;

}</code></pre>
<blockquote>
<p>클래스에 붙이는 @RequestMapping
맵핑될 URL의 공통 부분을 @RequestMapping으로 클래스에 적용</p>
</blockquote>
<pre><code class="language-java">@Controller
@RequestMapping(&quot;/login&quot;)
public class LoginController {

    @PostMapping(&quot;/login&quot;)
    public String login(){
        return &quot;login&quot;;

}</code></pre>
<blockquote>
<p>URL Encoding
URL에 포함된 non-ASCII 문자를 문자 코드(16진수) 문자열로 변환</p>
</blockquote>
<pre><code class="language-java">String msg = URLEncoder.encode(&quot;id 또는 pwd가 일치하지 않습니다&quot;, &quot;utf-8&quot;);</code></pre>
<blockquote>
<p>Redirect</p>
</blockquote>
<p>@GetMapping</p>
<pre><code class="language-java">return &quot;redirect:/login/login&quot;;
</code></pre>
<blockquote>
<p> url 다시쓰기</p>
</blockquote>
<ul>
<li>URL을 변경하는 것.</li>
<li>주로 URL에 쿼리 스트링을 추가</li>
</ul>
<pre><code class="language-java">String msg = &quot;id 또는 pwd가 일치하지 않습니다&quot;;
return &quot;redirect:/login/login?msg=&quot;+msg;
</code></pre>
<ul>
<li>queryString으로 다른 정보를 추가</li>
<li>이때 한글이 포함되어 있다면 URL 인코딩을 직접 처리</li>
<li>브라우저를 통해 입력한 경우 ASCII가 아닌 문자가 포함되어 있다면 수동으로 처리해야 한다.</li>
</ul>
<blockquote>
<p>ThymeLeaf 엔진</p>
</blockquote>
<ol>
<li>model</li>
</ol>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot; xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;title&gt;Title&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1 th:text=&quot;|id=${id}|&quot;&gt;asdf&lt;/h1&gt;
&lt;h1 th:text=&quot;|pwd=${pwd}|&quot;&gt;1234&lt;/h1&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>json 이외의 형식이 들어가려면 <code>| |</code>로 감싸주어야 한다.</p>
<ol start="2">
<li>query parameter<pre><code class="language-java">&lt;h1 th:text=&quot;${param.msg}&quot;&gt;&lt;/h1&gt;</code></pre>
</li>
</ol>
]]></description>
        </item>
    </channel>
</rss>