<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Dev 달팽이 @_''</title>
    <link>https://dabonee.tistory.com/</link>
    <description>달팽이처럼 느리더라도 꾸준히 ..
https://github.com/Daboni</description>
    <language>ko</language>
    <pubDate>Tue, 7 Apr 2026 15:40:28 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>다본죽</managingEditor>
    <image>
      <title>Dev 달팽이 @_''</title>
      <url>https://tistory1.daumcdn.net/tistory/3295402/attach/a371116ea1d0415e8f72751f99481ab3</url>
      <link>https://dabonee.tistory.com</link>
    </image>
    <item>
      <title>[객체 지향] 구조 패턴 1 - Adapter, Bridge Pattern</title>
      <link>https://dabonee.tistory.com/108</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;구조 패턴 1 - Adapter, Bridge Pattern&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조 패턴의 대표적인 패턴으로 Adapter, Bridge 패턴에 대해 Deep-Dive하려고 한다. 코틀린은 사용해보진 않았지만, 이번 디자인패턴은 코틀린으로 한번 진행해보려고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Adapter Pattern&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 호환되지 않는 인터페이스를 가진 클래스들을 함께 동작하게 해주는 패턴. 가장 흔히 하는 비유가 한국-일본의 전기 플러그 어댑터이다. 일본에 여행갈 때 필수 준비물로 챙겨야하는 물건 중 하나가 돼지코 어댑터이다. 돼지코의 어댑터의 용도를 생각해보면 한국 규격의 콘센트를 일본 전압에 맞게 변환해준다. Adapter Pattern이 바로 그 역할이다. Adapter Pattern의 역할은&amp;nbsp;&lt;b&gt;기존 클래스를 수정하지 않고도 클라이언트와 호환되도록 인터페이스를 &quot;변환&quot;해준다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구조(UML)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;199&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCLTEB/btsOms1bih5/3lJs9b1iG17swS45WrxiEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCLTEB/btsOms1bih5/3lJs9b1iG17swS45WrxiEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCLTEB/btsOms1bih5/3lJs9b1iG17swS45WrxiEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCLTEB%2FbtsOms1bih5%2F3lJs9b1iG17swS45WrxiEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;199&quot; height=&quot;342&quot; data-origin-width=&quot;199&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Target: 클라이언트가 기대하는 인터페이스&lt;/li&gt;
&lt;li&gt;Adaptee: 이미 존재하는 클래스(인터페이스가 다름)&lt;/li&gt;
&lt;li&gt;Adapter: Target 인터페이스를 구현하고 내부에서 Adaptee를 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용 예제&lt;/h3&gt;
&lt;h4 data-end=&quot;127&quot; data-start=&quot;116&quot; data-ke-size=&quot;size20&quot;&gt;시나리오&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;315&quot; data-start=&quot;128&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;190&quot; data-start=&quot;128&quot;&gt;기존 시스템에 있는 OldPrinter 클래스는 printOld(String msg) 메서드만 있음.&lt;/li&gt;
&lt;li data-end=&quot;254&quot; data-start=&quot;191&quot;&gt;새 시스템은 Printable 인터페이스를 통해 print(String msg) 메서드를 사용해야 함.&lt;/li&gt;
&lt;li data-end=&quot;315&quot; data-start=&quot;255&quot;&gt;기존 OldPrinter를 수정하지 않고 새 시스템에 맞게 연결해야 함 &amp;rarr; &lt;b&gt;Adapter 패턴&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748766851997&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Target
interface Printable {
    fun print(message: String)
}

// Adaptee(기존 클래스)
class OldPrinter {
    fun printOld(message: String) {
        println(&quot;Old printed $message&quot;)
    }
}

// Adpater: Printable을 구현하고 OldPrinter를 위임
class PrinterAdapter (private val oldPrinter: OldPrinter) : Printable {
    override fun print(message: String) {
        oldPrinter.printOld(message);
    }
}

fun main() {
    val oldPrinter = OldPrinter()
    val printer : Printable = PrinterAdapter(oldPrinter)

    printer.print(&quot;Print Adapter&quot;)

}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인터페이스를 구현해 기존 클래스에 맞춤: PrintAdapter는 Printable을 구현함으로써 클라이언트와 호환&lt;/li&gt;
&lt;li&gt;기존 코드 수정 없음: OldPrinter는 그대로 유지&lt;/li&gt;
&lt;li&gt;유연성 향상: 새로운 시스템은 기존 시스템과 분리된 채로 개발 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적용 고려 시점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 클래스를 수정할 수 없는 경우&lt;/li&gt;
&lt;li&gt;새로운 시스템이이 레거시 시스템과 통합될 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Adapter Pattern VS Port-Adpater Pattern&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 시스템이 MSA향으로 가면서 Port-Adapter Pattern(헥사고널 아키텍처)를 많이 고려하게 된다. Port-Adapter Pattern의 Adpater와 Adapter Pattern에서의 Adapter가 같은 역할을 한다고 생각하기 쉽다.(본인이 그렇게 생각을 하고 있었다...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 말하면, GoF의 Adapter Pattern과&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Port-Adapter Pattern&lt;/span&gt;의 Adapter는 &lt;b&gt;개념적으로 유사하지만, 쓰임새와 목적이 다르다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Port-Adapter Pattern의 Adapter&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Port-Adapter Pattern의 Port는 내부 도메인이 기대하는 인터페이스로 도메인으로 가거나 도메인에서 외부로 나갈 수 있게 해주는 인터페이스이다. &lt;b&gt;Adapter는 Port구현하거나 호출하는 실제 구현체 (외부 시스템과 연결)이다.&lt;/b&gt; 예를 들어, UserRegisterUseCase를 호출하는 구현체 UserController는 Inbound-Adapter이다.&lt;/p&gt;
&lt;pre id=&quot;code_1748768635423&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Port (도메인이 기대하는 인터페이스)
interface UserRegisterUseCase {
    fun registerUser(user: User)
}

// Adapter (외부에서 도메인 호출)
@RestController
class UserController(private val userRegisterUseCase: UserRegisterUseCase) {
    @PostMapping(&quot;/users&quot;)
    fun createUser(@RequestBody user: User) {
        userRegisterUseCase.registerUser(user)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Adapter Pattern과 Port-Adpater Pattern 비교&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 148px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;항목&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Adapter Pattern&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;Port-Adapter Pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;소속&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;디자인 패턴 (GoF)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;아키텍처 스타일 (Hexagonal)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;목적&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;호환되지 않는 인터페이스 연결&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;시스템 내부(core)와 외부(world) 연결&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;초점&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;객체 간&lt;/b&gt;의 인터페이스 변환&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;계층 간&lt;/b&gt;의 역할 구분 및 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;위치&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;클래스 또는 객체 수준&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;아키텍처 상의 경계지점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;방향성&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;클라이언트가 기대하는 인터페이스로 &quot;변환&quot;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;내부 도메인 &amp;rarr; 외부 or 외부 &amp;rarr; 내부 흐름 중계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;예시&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;PrinterAdapter가 OldPrinter 감쌈&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Controller가 포트를 구현하여 Service 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Bridge Pattern&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Adapter 패턴이 &quot;호환&quot;에 초점을 맞춘 패턴이라면, Bridge Pattern은 확장성과 유연성을 극대화하기 위한 패턴. 추상화와 구현을 분리하여 독립적으로 확장할 수 있도록 한다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구조(UML)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;513&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NOKLQ/btsOl8oljBu/z33IG0gF5Zdm6ee92hIyA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NOKLQ/btsOl8oljBu/z33IG0gF5Zdm6ee92hIyA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NOKLQ/btsOl8oljBu/z33IG0gF5Zdm6ee92hIyA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNOKLQ%2FbtsOl8oljBu%2Fz33IG0gF5Zdm6ee92hIyA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;513&quot; height=&quot;342&quot; data-origin-width=&quot;513&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Abstraction: 클라이언트가 사용하는 인터페이스. 구현체를 가짐&lt;/li&gt;
&lt;li&gt;Implementor: 구현을 위한 인터페이스&lt;/li&gt;
&lt;li&gt;ConcreteImplementor: 실제 구현&lt;/li&gt;
&lt;li&gt;RefinedAbstraction: Abstraction의 확장(기능 추가)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용 예제&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시나리오&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;885&quot; data-start=&quot;851&quot;&gt;RemoteControl은 TV를 제어하는 추상화 계층&lt;/li&gt;
&lt;li data-end=&quot;915&quot; data-start=&quot;886&quot;&gt;SamsungTV, LGTV는 TV의 실제 구현체&lt;/li&gt;
&lt;li data-end=&quot;955&quot; data-start=&quot;916&quot;&gt;리모컨 종류와 TV 브랜드를 &lt;b&gt;독립적으로 확장&lt;/b&gt;할 수 있어야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748771159065&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Implementor: 구현 인터페이스
interface TV {
    fun turnOn()
    fun turnOff()
    fun setChannel(channel: Int)
}

// ConcreteImplementor: 구체 구현체
class SamsungTV : TV {
    override fun turnOn() = println(&quot;(Samsung)TV turned on&quot;)

    override fun turnOff() = println(&quot;(Samsung)TV turned off&quot;)

    override fun setChannel(channel: Int) = println(&quot;(Samsung)TV set channel $channel&quot;)
}

// ConcreteImplementor: 구체 구현체
class LgTV : TV {
    override fun turnOn() = println(&quot;(LG)TV turned on&quot;)

    override fun turnOff() = println(&quot;(LG)TV turned off&quot;)

    override fun setChannel(channel: Int) = println(&quot;(LG)TV set channel $channel&quot;)

}

// Abstraction: 추상 계층
abstract class RemoteController (protected val tv : TV) {
    abstract fun turnOn()
    abstract fun turnOff()
    abstract fun setChannel(channel : Int)
}

// RefinedAbstraction: 구체 추상화
class BasicRemoteController(tv: TV) : RemoteController(tv){
    override fun turnOn() = tv.turnOn()

    override fun turnOff() = tv.turnOff()

    override fun setChannel(channel: Int) = tv.setChannel(channel)
}

fun main() {
    val samsungTV = SamsungTV()
    val lgTV = LgTV()

    val remote1 = BasicRemoteController(samsungTV)
    val remote2 = BasicRemoteController(lgTV)

    remote1.turnOn()
    remote1.turnOff()
    remote1.setChannel(7)

    remote2.turnOn()
    remote2.turnOff()
    remote2.setChannel(9)

}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;188&quot; data-start=&quot;148&quot;&gt;&lt;b&gt;&lt;/b&gt;RemoteControl은 TV를 제어하는 &lt;b&gt;추상화 계층&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;188&quot; data-start=&quot;148&quot;&gt;SamsungTV, LgTV는 실제 동작을 구현한 &lt;b&gt;구현 계층&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;188&quot; data-start=&quot;148&quot;&gt;이 둘은 서로 &lt;b&gt;독립적으로 확장 가능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;적용 고려 시점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2468&quot; data-start=&quot;2437&quot;&gt;&lt;b&gt;구현과 인터페이스를 독립적으로 확장&lt;/b&gt;해야 할 경우&lt;/li&gt;
&lt;li data-end=&quot;2510&quot; data-start=&quot;2469&quot;&gt;기능 계층과 플랫폼 계층이 &lt;b&gt;조합 가능한 형태로 분리&lt;/b&gt;되어야 할 경우&lt;/li&gt;
&lt;li data-end=&quot;2556&quot; data-start=&quot;2511&quot;&gt;상속 대신 &lt;b&gt;구성(Composition)&lt;/b&gt; 으로 다형성을 확보하고 싶을 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Adapter Pattern VS Bridge Pattern&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 95px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;항목&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Adapter&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Bridge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;목적&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;호환되지 않는 인터페이스 연결&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;추상화와 구현을 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;시점&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;통합 단계 (사후에 연결)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;설계 단계 (처음부터 구조 분리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;유연성&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;낮음 (Adaptee 고정)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;높음 (양쪽 독립적 확장 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;관계&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;1개의 계층 (클래스 &amp;harr; 클래스)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;2개의 계층 (추상 &amp;harr; 구현)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java&amp;amp;Spring/기타</category>
      <category>GOF</category>
      <category>객체지향</category>
      <category>디자인 패턴</category>
      <author>다본죽</author>
      <guid isPermaLink="true">https://dabonee.tistory.com/108</guid>
      <comments>https://dabonee.tistory.com/108#entry108comment</comments>
      <pubDate>Sun, 1 Jun 2025 18:52:43 +0900</pubDate>
    </item>
    <item>
      <title>[객체 지향] 디자인 패턴의 분류</title>
      <link>https://dabonee.tistory.com/107</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;디자인 패턴의 분류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격적으로 디자인 패턴에 Deep-Dive 하기 전에 디자인 패턴의 분류에 대한 이해를 다지고 가려고 한다.(예열 느낌으로...)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;디자인 패턴의 세 가지 분류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 패턴의 분류는 패턴을 기능적 목적에 따라 나눈 것이다.&amp;nbsp; 첫번째는 &lt;b&gt;생성(Creational)&lt;/b&gt; 패턴, 두번째는 &lt;b&gt;구조(Structural)&lt;/b&gt; 패턴, 마지막으로 &lt;b&gt;행위(Behavioral)&lt;/b&gt; 패턴이 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생성 패턴 (Creational Patterns)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체의 생성 과정을 추상화하여 유연하고 재사용 가능한 방식으로 객체를 생성하는 목적의 패턴이다. 객체 생성 로직을 캡슐화하여 코드 의존성을 낮추고, 런타임 시점의 유연한 객체 결정이 가능하도록 하는 것이 특징이다. 대표적 패턴으로 Singleton, Factory Method, Abstract Factory, Builder, Prototype 등이 있다. 실무에서 자주 사용하는 Lombok의 @Builder가 생성 패턴의 대표적인 예이다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구조 패턴 (Structual Patterns)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스나 객체를 조합하여 더 큰 구조를 만들되, 그 구조를 유연하게 유지하는 패턴이다. 객체들 간의 관계(연결/조합 방식)를 정의하고 인터페이스 호환성을 확보하면서 기능을 확장하는 것이 특징이다. 대표적 패턴으로 Adapter, Bridge, Composite, Decorator, Facade, Proxy 등이 있다. Spring을 사용하는 현업자라면 Proxy를 많이 들었을텐데, Proxy가 대표적인 구조 패턴이다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;행위 패턴 (Behavioral Patterns)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 간의 상호작용 및 책임 분산 방식을 정의하는 패턴이다. 책임의 위임과 메시지 흐름을 명확하게 하고, 실행 알고리즘을 동적으로 바꾸기 쉽게하는 것이 특징이다. 대표적 패턴으로 Observer, Strategy, Template Method, State, Command, Mediator, Iterator가 있다. 가장 많이 접해 봤을 법한 예로 Spring의 JdbcTemplate이나 RestTemplate(deprecated 됐지만..)이 있다. Spring의 @EventListener도 행위 패턴의 대표적인 예이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;각 패턴의 핵심 역할 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분류 핵심 역할 왜 중요한가? 실제 예 (실무에서 자주 보이는 모습)&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 99.7678%; height: 218px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 10.4651%;&quot;&gt;분류&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 21.6279%;&quot;&gt;핵심 역할&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 48.6047%;&quot;&gt;중요성&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 18.9599%;&quot;&gt;실무 예&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 57px;&quot;&gt;
&lt;td style=&quot;height: 57px; width: 10.4651%;&quot;&gt;&lt;b&gt;생성(Creational)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 57px; width: 21.6279%;&quot;&gt;객체를 &lt;b&gt;어떻게 만들 것인가&lt;/b&gt;에 대한 전략&lt;/td&gt;
&lt;td style=&quot;height: 57px; width: 48.6047%;&quot;&gt;잘못된 객체 생성 방식은 유지보수와 테스트를 어렵게 만든다. 생성 로직을 분리하면 의존성 주입, 테스트가 쉬워진다.&lt;/td&gt;
&lt;td style=&quot;height: 57px; width: 18.9599%;&quot;&gt;- DI/IoC 컨테이너 (Spring, Guice) &lt;br /&gt;- Builder 패턴으로 복잡한 DTO 생성 &lt;br /&gt;- 싱글턴 서비스 인스턴스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 68px;&quot;&gt;
&lt;td style=&quot;height: 68px; width: 10.4651%;&quot;&gt;&lt;b&gt;구조(Structural)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 68px; width: 21.6279%;&quot;&gt;클래스/객체를 &lt;b&gt;어떻게 조합할 것인가&lt;/b&gt;에 대한 전략&lt;/td&gt;
&lt;td style=&quot;height: 68px; width: 48.6047%;&quot;&gt;변경에 유연한 구조는 장기 프로젝트에서 필수. 클래스 간 관계를 느슨하게 유지해야 확장이 가능해진다.&lt;/td&gt;
&lt;td style=&quot;height: 68px; width: 18.9599%;&quot;&gt;- Adapter로 외부 API 래핑 &lt;br /&gt;- Decorator로 요청/응답 가공 &lt;br /&gt;- Facade로 하위 모듈 캡슐화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 57px;&quot;&gt;
&lt;td style=&quot;height: 57px; width: 10.4651%;&quot;&gt;&lt;b&gt;행위(Behavioral)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 57px; width: 21.6279%;&quot;&gt;객체 간 &lt;b&gt;책임과 커뮤니케이션 방식&lt;/b&gt;에 대한 전략&lt;/td&gt;
&lt;td style=&quot;height: 57px; width: 48.6047%;&quot;&gt;객체 간 협력이 많아질수록 &quot;누가 무엇을 담당하느냐&quot;가 핵심이다. 책임 분산 없이는 코드가 뒤엉킨다.&lt;/td&gt;
&lt;td style=&quot;height: 57px; width: 18.9599%;&quot;&gt;- Strategy로 다양한 정책 선택 &lt;br /&gt;- Observer로 이벤트 처리 &lt;br /&gt;- Template Method로 공통 로직 재사용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생성 패턴 - &quot;객체는 아무데서나 만들지 않는다&quot;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 클래스에서 new 키워드 남용 시 테스트가 어려워지고, 의존성이 강해짐&lt;/li&gt;
&lt;li&gt;패턴을 통해 의존성 주입, 객체 재사용, 책임 분리가 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구조 패턴 - &quot;바꿔 끼울 수 있는 유연한 코드 구조를 만들자&quot;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 클래스를 손대지 않고도 기능 확장 가능&lt;/li&gt;
&lt;li&gt;변화에 강한 아키텍처 구성 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;행위 패턴 - &quot;객체 간 역할을 명확히 나누면 시스템이 견고해진다&quot;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;협력 관계, 이벤트 처리, 조건 분기 등 로직 흐름 제어에 핵심&lt;/li&gt;
&lt;li&gt;유지보수/기능 추가 시 가장 자주 문제가 생기는 영역이므로 설계가 중요&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java&amp;amp;Spring/기타</category>
      <category>객체지향</category>
      <category>디자인패턴</category>
      <author>다본죽</author>
      <guid isPermaLink="true">https://dabonee.tistory.com/107</guid>
      <comments>https://dabonee.tistory.com/107#entry107comment</comments>
      <pubDate>Wed, 28 May 2025 17:39:05 +0900</pubDate>
    </item>
    <item>
      <title>[객체 지향] 객체지향원칙 - SOLID</title>
      <link>https://dabonee.tistory.com/106</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;객체지향원칙 - SOLID&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 아키텍트 업무를 맡게 되면서(아직 이르지만..) DDD, 헥사고널 아키텍처에 대해 좀 더 깊이 있는 이해가 필요하게 되었다. 기본부터 다시 잡는 다는 생각으로 OOP, 디자인패턴부터 다시 다져보려 한다...&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;객체 지향 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향원칙을 지키면서 설계하는 건 단순 &quot;깔끔한 코드&quot; 작성을 위해서는 아니다. 객체 지향 설계를 한다는 것은 확장이 쉽고, 높은 응집도와 낮은 결함도로 인해 변경이 전체 시스템에 미치는 영향을 줄이고, 리팩토링과 유지보수를 쉽게 설계한다는 것이다. SOLID는 이러한 설계를 할 수 있도록 도움을 주는 지침이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SOLID란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향 프로그래밍(OOP)의 5가지 핵심 설계 원칙, 유지보수성과 확장성이 뛰어난 소프트웨어를 설계하기 위한 개념&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;S - Single Responsibility Principle (단일 책임 원칙)&lt;/li&gt;
&lt;li&gt;O - Open/Closed Principle (개방/폐쇄 원칙)&lt;/li&gt;
&lt;li&gt;L - Liskov Substitution Principle (리스코프 치환 원칙)&lt;/li&gt;
&lt;li&gt;I - Interface Segregation Principle (인터페이스 분리 원칙)&lt;/li&gt;
&lt;li&gt;D - Dependency Inversion Principle (의존 역전 원칙)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Single Responsibility Principle (SRP)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 클래스는 하나의 책임만 가져야 한다는 원칙. 즉, 클래스가 변경되는 이유가 하나뿐이어야 한다. 책임이 여러 개인 클래스는 여러 이유로 변경될 수 있으므로 유지보수가 어려워지고 테스트가 힘들어진다. 여기서 의미하는 책임의 범위는 개개인에 따라 다르다. 책임을 단순 기능(세부 동작)로 오해할 수 있다.(내가 그랬었다..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, Java의 Scanner는 단일 책임 원칙을 지킨 좋은 예이다. Sanner는 입력에 대한 책임만 가지고, 입력 후 데이터 처리는 별도 클래스가 담당한다. Scanner는 입력 -&amp;gt; 파싱 -&amp;gt; 반환의 단일 흐름 책임만 수행한다. 여기서 Scanner는 단일 책임 원칙을 잘 지킨 예라고 했는데 입력, 파싱, 반환 3가지 기능을 하고 있는 걸 볼 수 있다. 하지만 Scanner는 &quot;입력 처리&quot; 라는 단일 책임을 담당하고 있다. 즉, 책임의 범위를 단순 작업의 단위가 아니라 &quot;클래스가 왜 바뀌는가?&quot;를 기준으로 판단해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Open/Closed Principle (OCP)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확장에는 열려있고, 수정에는 닫혀 있어야 한다는 원칙. 새로운 기능이 필요할 때 기존 코드는 수정하지 않고, 확장해서 대응할 수 있어야 한다. 추상 클래스와 인터페이스를 사용하여 새로운 기능을 구현하는 것을 의미한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, Java의 Writer가 전형적인 예시이다. Writer는 추상 클래스로 되어 있다. 이를 FileWriter, BufferedWriter, StringWriter 등 다양한 구현체가 존재하며, 새로운 출력 방식이 필요하면 하위 클래스를 확장하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1748330327613&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Writer.java
public abstract class Writer implements Appendable, Closeable, Flushable {
	//...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Liskov Substitution Principle (LSP)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다는 원칙. 부모 타입을 사용하는 코드에서, 자식 타입으로 대체해도 기능에 문제가 없어야 한다. 즉, LSP는 다형성에 대한 원칙이다. LSP를 지키지 않으면 상속을 썼는데도 다형성이 깨져 부모 타입으로 코딩하는 게 의미가 없어진다. 이를 지키기 위해선 자식 클래스가 부모 클래스의 계약(contract)를 지키도록 설계해 치환 가능성을 유지해야한다. 이 말은 LSP는 단순히 &quot;상속이 잘 되도록&quot; 설계하는 것이 아닌 &quot;상위 타입으로 안전하게 추론할 수 있도록&quot; 설계를 해야 한다는 메시지를 담고 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;*Contract란?&lt;br /&gt;부모 클래스(또는 인터페이스)가 외부(클라이언트)에게 약속하는 동작과 규칙.&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;- 메서드 시그니처와 시맨틱&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;- 입력에 따른 기대 출력&lt;br /&gt;- 예외&lt;br /&gt;- 상태 변화 규칙&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java의 Collection 인터페이스는 LSP의 가장 대표적인 예이다.&lt;/p&gt;
&lt;pre id=&quot;code_1748332792227&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; arrayList = new ArrayList&amp;lt;&amp;gt;();
List&amp;lt;String&amp;gt; linkedList = new LinkedList&amp;lt;&amp;gt;();

arrayList.add(&quot;1&quot;);
linkedList.add(&quot;1&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ArrayList, LinkedList는 모두 List 인터페이스 구현체이다. 하위 클래스(구현체)를 상위 타입(List)로 치환해도 기능에 문제가 없다. 또한, Collections.sort(List) 어떤 List 구현체도 동등하게 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식이 부모의&amp;nbsp; 계약을 지키기 위해선 아래가 지켜져야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부모가 가능한 입력을 자식도 전부 받아들여야 함&lt;/li&gt;
&lt;li&gt;부모가 반환하는 값이나 상태 변화와 동등하거나 더 넓은 결과를 보장해야 함&lt;/li&gt;
&lt;li&gt;예외 처리나 부작용도 예상 가능한 수준 내에서 일어나야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Interface Segregation Principle (ISP)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙. 큰 인터페이스 하나를 만드는 것보다, 작고 역할별로 분리된 인터페이스를 만들어야 한다. 즉, &quot;인터페이스는 작게, 목적에 맞게&quot; 설계 해야한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ISP 위반 Case&lt;/h3&gt;
&lt;pre id=&quot;code_1748394698624&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Machine {
    void print();
    void scan();
    void fax();
}

public class SimplePrinter implements Machine {
    @Override
    public void print() {
        System.out.println(&quot;Print&quot;);
    }

    @Override
    public void scan() {
        System.out.println(&quot;Not Supported&quot;);}

    @Override
    public void fax() {
        System.out.println(&quot;Not Supported&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISP가 위반된 케이스를 보면 불필요한 메서드를 구현체에서 구현하게 된다. 이렇게 되면 유지보수가 어려워지고 코드에 대한 이해가 어려워진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ISP를 지킨 개선된 Case&lt;/h3&gt;
&lt;pre id=&quot;code_1748394954535&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Printer {
    void print();


public interface Scanner {
    void scan();
}

public interface Fax {
    void fax();
}

public class SimplePrinter implements Printer {
    @Override
    public void print() {
        System.out.println(&quot;Print&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선된 케이스를 보면 필요한 기능만 구현하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISP의 예제를 보면 단일 책임 원칙(SRP)처럼 단일 책임을 가진 인터페이스를 만드는 것처럼 보인다. 그래서 이 둘의 개념이 혼동의 여지가 있다. 하지만 둘은 적용 대상과 맥락이 완전히 다르다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SRP vs ISP&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 129px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;SRP (단일 책임 원칙)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;ISP (인터페이스 분리 원칙)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;초점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;클래스(또는 모듈)가 &lt;b&gt;하나의 책임만 가져야 함&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;클라이언트가 &lt;b&gt;사용하지 않는 인터페이스에 의존하지 않도록&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;적용 대상&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;클래스나 모듈 전체&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;인터페이스&lt;/b&gt; (혹은 API 계약)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;목표&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;변경 이유를 하나로 제한 (수정 시 영향을 최소화)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;강제로 쓸데없는 메서드를 구현하지 않게 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;철학&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&amp;ldquo;이 클래스는 왜 바뀌는가?&amp;rdquo;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&amp;ldquo;이 인터페이스를 구현하는 입장에서, 다 써야 하는가?&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;위반 예&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;DB 저장 + PDF 출력 + 알림 전송을 한 클래스가 다 함&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;MultiFunctionPrinter가 scan(), fax() 등 안 쓰는 기능까지 구현&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dependency Inversion Principle (DIP)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다는 원칙. 비즈니스 로직은(고수준)은 구체적인 구현(저수준)에 의존 하면 안된다. 즉, 인터페이스(추상화)를 통해 의존 구조를 역전 시켜야 한다. DIP는 대부분 실무에서 많이 접하고 자연스럽게 체득하는 원칙이 아닌가 싶다. (특히, JPA를 보더라도...)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DIP 구조로 보는 JPA vs Hibernate&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;고수준:&amp;nbsp;비즈니스&amp;nbsp;로직&amp;nbsp;(Service,&amp;nbsp;Repository)&lt;br /&gt;&amp;nbsp;&amp;darr;&lt;br /&gt;추상화:&amp;nbsp;JPA&amp;nbsp;(javax.persistence)&lt;br /&gt;&amp;nbsp;&amp;darr;&lt;br /&gt;저수준:&amp;nbsp;Hibernate,&amp;nbsp;EclipseLink,&amp;nbsp;OpenJPA&amp;nbsp;(구현체)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고수준 모듈인 비즈니스 로직은 jPA 인터페이스에만 의존하게 된다. JPA의 실제 동작은 구현체인 Hibernate가 수행한다. 비즈니스 로직은 Hibernate를 몰라도 작동이 가능하다. 또한, JPA의 구현체를 Hibernate가 아닌 다른 구현체를 사용하고 싶으면 구현체만 바꿔주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, DIP를 통해 유연한 변경 구조가 가능하게 된다.(Loose Coupling)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SOLID는 &quot;변화에 강한 설계&quot;를 위한 철학이다. SRP는 관심사를 분리해서 변경이유를 단일화, OCP는 기능 확장을 유연하게, LSP는 상속 관계에서의 신뢰성 확보, ISP는 클라이언트 인터페이스 최소화, DIP는 유연한 의존성과 테스트 용이성 확보에 초점을 두고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무를 하다 보면 어떤 원칙은 자연스럽게 체득하고 고려하지만 어떤 원칙은 아직은 어렵게 느껴지는 거 같다.. 무조건 이 모두를 지켜야한다는 것은 아니지만, 적어도 적용할지 말지 고려하면서 설계하는 습관을 들여야겠다..&lt;/p&gt;</description>
      <category>Java&amp;amp;Spring/기타</category>
      <category>java</category>
      <category>Solid</category>
      <category>객체지향</category>
      <category>디자인패턴</category>
      <author>다본죽</author>
      <guid isPermaLink="true">https://dabonee.tistory.com/106</guid>
      <comments>https://dabonee.tistory.com/106#entry106comment</comments>
      <pubDate>Tue, 27 May 2025 18:25:41 +0900</pubDate>
    </item>
    <item>
      <title>Redis Lettuce를 통한 동시성 제어</title>
      <link>https://dabonee.tistory.com/105</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis Lettuce를 통한 동시성 제어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;들어가기 전에..&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구축 프로젝트 진행 중, 기존 레거시 시스템의 문제(동시 예약)를 해결 해야하는 상황에 직면하게 됐다. 이 상황을 Redis Lettuce를 통해 해결하게 되었고, 이 경험에 대한 내용이다. 안그래도 실무에서 Redis를 경험해보고 싶었는데, 드디어 경험해 볼 기회가 생기게 되었다.(물론, Redis를 써서 해결할 수 있을 거라고 생각은 못했지만..)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;직면한 문제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 수요일 00시에 주말에 이용할 수 있는 특정 차량 20대에 대한 예약이 풀린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 이를 예약하기 위해, 많은 이용자들이 23시59분 부터 대기를 하고 있다가 예약을 시도한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;u&gt;&lt;b&gt;같은 차량, 같은 날에 대해 동시에 예약이 된다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이를 해결하기 위한 방법 모색(분산 Lock)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 Lock을 위한 방법으로 크게 3가지 방법을 찾았다. 첫번째는 DB에서 직접 처리, 두번째는 Java단에서 Transaction을 통해 처리, 세번째는 Redis를 이용한 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. MySQL Named Lock&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;get_lock&lt;/li&gt;
&lt;li&gt;get_release&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Java transaction&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Transactional&lt;/li&gt;
&lt;li&gt;JTA(Java Transaction APIs)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Redis&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Lettuce&lt;/li&gt;
&lt;li&gt;Redisson&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Redis를 선택한 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3가지 방법 중 Redis를 선택한 이유는 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. MySQL Named Lock&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Named Lock을 관리할 별도의 Connection Pool 관리가 필요하다.&lt;/li&gt;
&lt;li&gt;예약하기 하나의 버튼에서 실제 예약에 대한 Row만 저장되는 것이 아닌 여러 DB가 서비스 Logic(다른 MSA 영역 호출 포함)이 얽혀있으며, 마지막에 예약 데이터가 저장된다. (RollBack에 대한 부담)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Java transaction&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시 예약 간단한 처리 하나 처리하는 것에 비해 너무 많은 시간과 작업이 필요함&lt;/li&gt;
&lt;li&gt;Named Lock을 사용할 때와 같이 RollBack에 대한 부담&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Redis&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Token 검증을 위한 Redis 서버가 이미 존재&lt;/li&gt;
&lt;li&gt;실제 예약을 위한 서비스 Logic에 진입하기 전에 확인하여 RollBack에 대한 부담이 없음&lt;/li&gt;
&lt;li&gt;In-memory 방식이라 사용자에게 빠른 응답이 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Lettuce vs Redisson&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산락을 위해 Redis를 사용하기로 정하였고, 이제 어떤 라이브러리를 사용하냐 고민에 있어서는 크게 어려움은 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson이 더 분산락을 위해 지원하고 있는 것이 많고, Lettuce보다 분산락에 더 특화되어 있는 것이 맞으나, 현재 프로젝트에서 Lettuce를 사용하고 있었으며, 동시 예약에 대한 처리를 위해서만 사용하기 때문에(Retry가 필요 없음) Redisson이 지원하는 기능이 크게 필요가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Lettuce를 사용할 때 주의할 점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lettuce는 Spin Lock 방식이기 때문에, Redis 등록에 실패하면 계속해서 등록을 시도한다.(Redisson은 Pub/Sub 방식)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 동시 예약에 대한 문제는 등록이 실패되면 예약 실패를 응답으로 주면 되기 때문에, Expired Time을 꼭 줘야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Logic 정리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 예약 진입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Redis에 예약 정보가(lock) 없으면 예약 Logic 진입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Redis에 예약 정보가(lock) 있으면 예약 실패 응답&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;적용 Flow&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1902&quot; data-origin-height=&quot;646&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFK0L4/btsH2V2QXZ7/rvqz3wakeEYkRaKQfe2bAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFK0L4/btsH2V2QXZ7/rvqz3wakeEYkRaKQfe2bAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFK0L4/btsH2V2QXZ7/rvqz3wakeEYkRaKQfe2bAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFK0L4%2FbtsH2V2QXZ7%2Frvqz3wakeEYkRaKQfe2bAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1902&quot; height=&quot;646&quot; data-origin-width=&quot;1902&quot; data-origin-height=&quot;646&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마무리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 문제에 직면했을 때, Redis를 사용해서 해결 할 수 있을 거라는 생각은 하지 못했다.. 기존에 Redis에 대한 개념 학습을 했을 때는 화면과 서버 간의 캐싱에 대해서만 학습했는데, 역시 개념 학습과 실무에 실제 적용은 좀 다르다는 생각이 들었다.&lt;/p&gt;</description>
      <category>기타</category>
      <category>Lettuce</category>
      <category>redis</category>
      <category>동시성</category>
      <category>분산락</category>
      <author>다본죽</author>
      <guid isPermaLink="true">https://dabonee.tistory.com/105</guid>
      <comments>https://dabonee.tistory.com/105#entry105comment</comments>
      <pubDate>Mon, 17 Jun 2024 23:37:19 +0900</pubDate>
    </item>
    <item>
      <title>[이펙티브 자바] 2장 - 아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라.</title>
      <link>https://dabonee.tistory.com/104</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;[이펙티브 자바] 2장 - 아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 팩터리와 생성자에는 매개변수가 많을 때, 적절히 대응하기 어렵다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 실무에서 Legacy 코드를 보다보면, 멤버변수가 많은 클래스 내부에 생성자, Getter, Setter가 덕지덕지 붙어 있는 경우를 심심찮게 볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[참고] 멤버 변수가 8개인 User 클래스&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1704892993705&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class User {
   String id
   String pwd;
   String nickname;
   
   String name;
   Integer age;
   String addr;
   String email;
   
   LocalDateTime lastLoginDtm;
   
   public User(String id, String pwd, String nickname) {
        this.id = id;
        this.pwd = pwd;
        this.nickname = nickname;
    }

    public User(String id, String pwd, String nickname, String name) {
        this.id = id;
        this.pwd = pwd;
        this.nickname = nickname;
        this.name = name;
    }

    public User(String id, String pwd, String nickname, String name, Integer age, String addr, String email, LocalDateTime lastLoginDtm) {
        this.id = id;
        this.pwd = pwd;
        this.nickname = nickname;
        this.name = name;
        this.age = age;
        this.addr = addr;
        this.email = email;
        this.lastLoginDtm = lastLoginDtm;
    }
    
    // ... 중략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 이렇게 많은 멤버변수를 가지고 있는 클래스에 여러 생성자를 가지고 있다면, 어떤 멤버변수에 값을 넣어주고 있는지 파악하기가 힘든 경험이 다들 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;빌더 패턴&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 안정성과 가독성을 겸비한 빌더 패턴(Builder pattern)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌더 패턴은 클라이언트가 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다. 그런 다음 빌더가 객체가 제공하는 일종의 Setter 메소드들로 원하는 선택 매개변수들을 설정한다. 마지막으로 매개변수가 없는 build 메서드를 호출하여 필요한 객체를 얻게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히, Spring에서 @Builder를 통해 빌더 패턴을 사용하곤 한다. 하지만, 실제로 Builder가 어떻게 구성되는지, 어떻게 동작하는지 알지 못하고 습관에 의해 사용하는 경우가 있다. 빌더가 어떻게 동작하는지 코드로 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1704893679549&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class User {
   String id
   String pwd;
   String nickname;
   
   String name;
   Integer age;
   String addr;
   String email;
   
   LocalDateTime lastLoginDtm;
   
   public static class Builder {
      private final String id;
      private final String pwd;
      private final String nickname;
      
      private String name;
      private Integer age;
      private String addr;
      private String email;
      
      public Builder(String id, String pwd, String nickname) {
         this.id = id;
         this.pwd = pwd;
         this.nickname = nickname;
      }
      
      public Builder name(String name) {
         name = name;
         return this;
      }
      
      public Builder age(Integer age) {
         age = age;
         return this;
      }
      
      // ...중략
      
      public User build() {
         return new User(this)
      }
      
   private User(Builder builder) {
      id = builder.id;
      pwd = builder.pwd;
      nickname = builder.nickname;
      
      name = builder.name;
      age = builder.age;
      addr = builder.addr;
      email = builder.email;
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1704894270263&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;User user = User.builder(&quot;dabon1234&quot;,&quot;daboni1234!@&quot;,&quot;dabonee&quot;).name(&quot;dabon&quot;).age(77).build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User 클래스는 불변이 되었으며, 빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적 호출이 가능하다. 에런 방식을 플루언트 API(fluent API) 혹은 메서드 연쇄(method chaining)이라고 한다. 빌더 패턴으로 인하여 직관적으로 어떤 멤버 변수를 넣었는지 확인이 가능하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 계층적으로 설계된 클래스와 함께 쓰기에 좋다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상 클래스는 추상 빌더를, 구체 클래스는 구체 빌더를 갖게한다. 다음은 메이플스토리의 다양한 직업을 표현하는 계층구조의 루트에 놓은 추상 클래스를 만들었다.&lt;/p&gt;
&lt;pre id=&quot;code_1704896515638&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 초보자
public abstract class Beginner {
    public enum Stat{STR, DEX, INT, LCK}
    private Set&amp;lt;Stat&amp;gt; stats;

    abstract static class Builder&amp;lt;T extends Builder&amp;lt;T&amp;gt;&amp;gt; {
        EnumSet&amp;lt;Stat&amp;gt; stats = EnumSet.noneOf(Stat.class);
        public T plusStat(Stat stat) {
            stats.add(Objects.requireNonNull(stat));
            return self();
        }

        abstract Beginner build();

        protected abstract T self();
    }

    Beginner(Builder&amp;lt;?&amp;gt; builder) {
        stats = builder.stats.clone();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드에서 추상 메서드 self는 하위 클래스에서 형변환하지 않고도 메서드 연쇄를 할 수 있도록 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하위 클래스로 1차 전직하여 직업을 선택하도록 하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1704896656883&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Intermediate extends Beginner{
    public enum Job {WARRIOR, ARCHER, MAGICIAN, ROGUE}
    private final Job job;

    public static class Builder extends Beginner.Builder&amp;lt;Builder&amp;gt; {
        private final Job job;

        public Builder(Job job) {
            this.job = Objects.requireNonNull(job);
        }

        @Override public Intermediate build() {
            return new Intermediate(this);
        }

        @Override protected Builder self() {return this;}
    }

    private Intermediate(Builder builder) {
        super(builder);
        job = builder.job;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하위 클래스의 build는 해당하는 구체 하위 클래스를 반환하도록 한다. 하위 클래스의 메서드가 상의 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능을 &lt;b&gt;공변 반환 타이핑(convariant return typing)&lt;/b&gt;이라 한다. 이 기능으로 인해 클라이언트는 형변환에 신경쓰지 않고도 빌더를 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1704897297506&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Intermediate warrior = new Intermediate.Builder(WARRIOR).plusStat(STR).build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;생성자나 정적 팩터리가 처리해야 할 매개변수가 만하면 빌더 패턴을 선택하는 게 더 효과적이다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>Java&amp;amp;Spring/Effective Java</category>
      <category>Effective Java</category>
      <category>이펙티브 자바</category>
      <author>다본죽</author>
      <guid isPermaLink="true">https://dabonee.tistory.com/104</guid>
      <comments>https://dabonee.tistory.com/104#entry104comment</comments>
      <pubDate>Wed, 10 Jan 2024 23:36:52 +0900</pubDate>
    </item>
    <item>
      <title>[이펙티브 자바] 2장 - 아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라.</title>
      <link>https://dabonee.tistory.com/103</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;[이펙티브 자바] 2장 - 아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스는 클라이언트에 public 생성자 대신 정적 팩터리 메서드를 제공할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;정적 팩터리 메서드의 장점&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 이름을 가질 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자에 넘기는 매개변수와 생성자 자체 만으로는 &lt;b&gt;반환될 객체의 특성을 제대로 설명하지 못한다.&lt;/b&gt; 반면에, 정적 팩터리 메서드는 이름만 잘 지으면 &lt;b&gt;반환될 객체의 특성을 쉽게 파악할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1704719066171&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Human {
   private String gender;
   private Integer age;
    
   private Human(String gender,Integer age) {
      this.gender = gender;
      this.age = age;
   }
   
   // 정적 팩터리 메서드 - 남자 생성
   public static Human maleFrom(Integer age) {
      return new Human(&quot;male&quot;,age);
   }
   
   // 정적 팩터리 메서드 - 여자 생성
   public static Human femaleFrom(Integer age) {
      return new Human(&quot;female&quot;,age);
   }
   
   // 정적 팩터리 메서드 - 지정
   public static Human of(String gender, Integer age) {
      return new Human(gender,age)
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1704719731032&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
    // 20살 남자 생성 
    Human man = Human.maleFrom(20);

    // 30 여자 생성
    Human woman = Human.femaleFrom(30);
    
    // 40살 남자 생성
    Human manFourty = Human.of(40,gender);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의&amp;nbsp; 예와 같이, 정적 팩터리 메서드인 maleFrom과 femaleFrom을 통하여 어떤 성별을 가진 객체가 반환될 것인지 쉽게 묘사가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불변 클래스(immutable class)는 인스턴스를 미리 만들어 놓고나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다. 이는 같은 객체가 자주 요청되는 상황에서 성능 향상을 기대할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1704720929816&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class DefaultDataBody {

   private static DefaultDataBody instance = new DefaultDataBody(0,new byte[0]);
   
   private Integer byteLength;
   private byte[] byteData;
   
   private DefaultDataBody(Integer byteLength, byte[] byteData) {
      this.byteLength = byteLength;
      this.byteData = byteData;
   }
   
   public static DefaultDataBody getInstance() {
      return instance;
   }
   
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1704721253154&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private void sendDefaultData(DataBlockHead head) {
   send(head, DefaultDataBody.getInstance());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 이전 프로젝트를 참고하여 간략화한 Legacy 코드이다. body가 비어있는 응답을 보낼 경우, 계속해서 객체를 생성하는 것이 아닌 정적 팩터리 메서드(getInstance)를 통해 하나의 인스턴스를 재활용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼, 반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 통제할 수 있다. 이를 인스턴스 통제(Instance-Controlled) 클래스라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스를 통제하면 좋은 점은&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;싱글턴(singleton)으로 만들 수 있다.&lt;/li&gt;
&lt;li&gt;인스턴스화 불가(noninstantiable)로 만들 수 있다.&lt;/li&gt;
&lt;li&gt;불변 값 클래스에서 동치인 인스턴스가 하나뿐임을 보장할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는, Flyweight Pattern의 근간이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 '엄청난 유연성'을 제공한다.&lt;/p&gt;
&lt;pre id=&quot;code_1704722989339&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface HmgBrand {
	
   public static HmgBrand getHyunDai(){
      return new HyunDai();
   }
    
   public static HmgBrand getKia(){
      return new Kia();
   }

   public static HmgBrand getGenesis(){
      return new Genesis();
   }
}

public class HyunDai implements HmgBrand {
}

public class Kia implements HmgBrand {
}

public class Genesis implements HmgBrand {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 8 전에는 인터페이스에 정적 메서드를 선언할 수 없어 java.util.Collections와 같이 인스턴스화 불가인 동반 클래스(Companion class)를 만들어 정의하는 것이 관례였다. 하지만, 자바 8 부터는 인터페이스가 정적 메서드를 가질 수 있어 인터페이스 만으로 하위 타입의 객체를 반환할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환 할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관 없다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704723904036&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Human {
   public static Human getHuman(String gender, Integer age) {
      return age&amp;lt;20 ? new Child(gender,age) : new Adult(gender, age);
   }
}

public class Child implements Human {
   String gender;
   String age;
   
   public Child(String gender, String age) {
      this.gender = gender;
      this.age = age;
   }
}

public class Adult implements Human {
   String gender;
   String age;
   
   public Adult(String gender, String age) {
      this.gender = gender;
      this.age = age;
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 서비스 제공자 프레임워크(service provider framework)를 만드는 근간이 된다. 대표적인 서비스 제공자 프레임워크로 JDBC(Java Database Connectivity)가 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 제공자 프레임워크에서 제공자(provider)는 서비스 구현체이다. 그리고 이 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트를 구현체로부터 분리해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 제공자 프레임워크는 3개의 핵심 컴포넌트로 이루워진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구현체의 동작을 정의하는 서비스 인터페이스(service interface) - JDBC에서 Connection&lt;/li&gt;
&lt;li&gt;제공자가 구현체를 등록할 때 사용하는 제공자 등록 API(provider registration API) - DriverManager.registerDriver&lt;/li&gt;
&lt;li&gt;클라이언트가 서비스의 인스턴스를 얻을 때 사용하는 서비스 접근 API(service access API) - DriverManager.getConnection&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1704727319563&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 서비스 인터페이스
public interface Connection {
   // SQL Statements Function ...
}

// 서비스 제공자 인터페이스
public interface Driver {
    String getDriverName();
    Connection getConnection();
}

public class DriverManager {

    static final Map&amp;lt;String,Driver&amp;gt; registeredDriver = new HashMap&amp;lt;&amp;gt;();
    
    // 제공자 등록 API
    public static void registerDriver(Driver driver) {
        registeredDriver.put(driver.getDriverName(), driver);
    }

	// 서비스 접근 API
    public static Connection getConnection(String driverName) {
        Driver driver = registeredDriver.get(driverName);
        return driver.getConnection();
    }
}

public static void main(String[] args) {
   Connection conn = DriverManager.getConnection(&quot;mySql&quot;);
   conn.deleteQuery(&quot;DELETE FROM table&quot;);
   conn.commit();

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java&amp;amp;Spring/Effective Java</category>
      <category>Effective Java</category>
      <category>이펙티브 자바</category>
      <author>다본죽</author>
      <guid isPermaLink="true">https://dabonee.tistory.com/103</guid>
      <comments>https://dabonee.tistory.com/103#entry103comment</comments>
      <pubDate>Tue, 9 Jan 2024 00:26:37 +0900</pubDate>
    </item>
    <item>
      <title>[Redis] Redis Cluster</title>
      <link>https://dabonee.tistory.com/102</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;[Redis] Redis Cluster&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가기 전에..&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 Redis에 대해 정리를 하다보니 Redis Cluster에 대한 정리까지 하면 기본적인 이론은 마무리할 수 있을 거 같아 하는김에 Redis Cluster까지 정리하기로 했다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis Cluster&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;1116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buGZHI/btso8dgqvgH/RrhjqLbqoOhf4HksfMPtzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buGZHI/btso8dgqvgH/RrhjqLbqoOhf4HksfMPtzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buGZHI/btso8dgqvgH/RrhjqLbqoOhf4HksfMPtzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuGZHI%2Fbtso8dgqvgH%2FRrhjqLbqoOhf4HksfMPtzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1194&quot; height=&quot;1116&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;1116&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cluster란 여러 대의 서버를 하나로 묶어 1개의 시스템처럼 동작하게 만드는 것이다. 그래서 여러 대의 서버에 데이터를 분산하여 저장하기 때문에 1대의 서버 부하를 여러 대로 분산시키므로 더 빠른 속도로 사용자에게 서비스를 제공할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 특정 서버가 장애가 발생하면 다른 서버로 연결하여 서비스 중단과 데이터의 손실 없이 계속해서 사용자에게 서비스를 제공할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis Sharding(샤딩)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 Single Thread이기 때문에 데이터의 양이 많아지면 심각한 장애가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해 1개의 테이블의 데이터를 물리적으로 여러 파티션으로 나누어 저장하여 조회,저장,삭제 등 성능을 크게 향상 시키는 방법을 &lt;b&gt;수평적 파티셔닝(Horizontal Partitioning)&lt;/b&gt; 또는 &lt;b&gt;샤딩(Sharding)&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DBpzS/btso6pu4fa1/BKg804KoQvCCUzVNxIExUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DBpzS/btso6pu4fa1/BKg804KoQvCCUzVNxIExUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DBpzS/btso6pu4fa1/BKg804KoQvCCUzVNxIExUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDBpzS%2Fbtso6pu4fa1%2FBKg804KoQvCCUzVNxIExUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;818&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis Cluster의 구성 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. Master로만 구성된 Cluster&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;1190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s54Bl/btso6LEMpCX/gtTEepHr7PTRiJ4wqSb9B0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s54Bl/btso6LEMpCX/gtTEepHr7PTRiJ4wqSb9B0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s54Bl/btso6LEMpCX/gtTEepHr7PTRiJ4wqSb9B0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs54Bl%2Fbtso6LEMpCX%2FgtTEepHr7PTRiJ4wqSb9B0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1456&quot; height=&quot;1190&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;1190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림과 같이 3개의 Master로 구성하면 데이터가 저장될 때 마다 M1,M2,M3으로 순차적으로 변경되어 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. Master와 Slave로 구성되어 있는 Cluster&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;748&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w55h7/btso6aZggTi/lE1M3LOGXxi1AR5bKd55uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w55h7/btso6aZggTi/lE1M3LOGXxi1AR5bKd55uk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w55h7/btso6aZggTi/lE1M3LOGXxi1AR5bKd55uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw55h7%2Fbtso6aZggTi%2FlE1M3LOGXxi1AR5bKd55uk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1282&quot; height=&quot;748&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;748&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Master로만 구성된 클러스터는 Master 중 1대의 Node만이라도 장애가 발생하면 서비스 중단과 데이터 손실이 일어날 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, Master와 Slave를 같이 구축해야 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Master와 Slave 서버로 구성되어 있는 클러스로 구성하면 Master Node에 있는 데이터를 복제하여 가지고 있으므로 Master Node가 장애가 발생하더라도 Slave Node가 Master Node로 승격하여 중단없이 사용자에게 서비스를 제공할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis Cluster는 Master를 여러 개 두어 분상 저장이 가능(Sharding), Scale Out이 가능하다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Redis Cluster는 고성능과 확장성을 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;하나의 Master에 여러 Slave를 둘 수 있다.&lt;/li&gt;
&lt;li&gt;Master 1,2,3이 있다면 데이터는 그 중 하나에 저장되며, Client가 데이터 읽기를 요청 시 저장된 곳이 아닌 다른 Master에 요청했다면 저장된 Master의 정보를 알려주며, Client는 전달받은 Master 정보에 다시 요청해서 데이터를 받아와야 한다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타</category>
      <category>Cache</category>
      <category>redis</category>
      <category>Redis Cluster</category>
      <category>레디스</category>
      <category>캐시</category>
      <author>다본죽</author>
      <guid isPermaLink="true">https://dabonee.tistory.com/102</guid>
      <comments>https://dabonee.tistory.com/102#entry102comment</comments>
      <pubDate>Thu, 27 Jul 2023 10:52:59 +0900</pubDate>
    </item>
    <item>
      <title>[배포] 무중단 배포와 배포 전략</title>
      <link>https://dabonee.tistory.com/101</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;무중단 배포와 배포 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가기 앞서..&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 쿠버네티스 기초 다지기 3/e 책을 읽기 시작했다. 분명 책이 기초 다지기인데.. 너무 어렵다...(왜 쿠베 생태계를 섭렵한 백엔드 개발자가 몸값이 높은지를 알겠다..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 책에 대한 내용 정리는 따로 안하려고 했지만.. 너무 내용이 어렵고 모르는 용어도 많이 나와서 예외로 모르는 용어들에 대한 정리나 쿠버네티스에 관한 정리도 따로 해야겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 실무에서 쿠버네티스를 사용하고 있고(내가 관리하고 있지는 않지만..) 처음에는 완벽한 무중단 배포가 아니어서 배포할 때마다 트래픽이 가라앉는 걸 경험했다. 지금은 헬스 체크와 쿠버네티스 pod 교체 시간을 좀 늘리니 트래픽이 가라앉는 양상은 보이질 않고 있지만 내가 배포를 하는 것이 아니니 배포 방식에 대해 알 수가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 쿠버네티스 책을 보면서 무중단 배포와 배포 전략에 대해서는 계속해서 나올 거 같아서 정리 한 번 하는게 좋겠다고 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중단 배포 방식과 다운타임&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2310&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/df7ItL/btsoU6O2Gk5/wUYK2XjzwUkDj51MYw5XQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/df7ItL/btsoU6O2Gk5/wUYK2XjzwUkDj51MYw5XQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/df7ItL/btsoU6O2Gk5/wUYK2XjzwUkDj51MYw5XQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdf7ItL%2FbtsoU6O2Gk5%2FwUYK2XjzwUkDj51MYw5XQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2310&quot; height=&quot;440&quot; data-origin-width=&quot;2310&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 한대로 서비스를 운영하고 있다고 가정하면, 현재 서버에는 V1 버전이 실행되고 있다. 그리고 이번에 새로운 기능들과 V1에서 발견된 버그/결함을 수정한 V2 버전을 개발 완료하였다. 이제 사용자들이 새로운 버전인 V2를 사용하도록 배포해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포를 하려면 어떤 과정을 거치게 될까? 먼저, 새로운 V2 버전 빌드를 서버에서 다운로드를 받아야 한다. V1과 V2는 같은 Port를 사용하므로 V2 버전을 실행시키기 전에 V1 프로세스를 종료시켜야 한다. 이 시점 부터 사용자들은 서비스를 이용할 수 없게 된다. 이제 사용자가 사용할 수 있는 V2 빌드를 실행하고 로딩 과정을 거쳐 안정적으로 실행되면 다시 사용자가 정상적으로 서비스를 이용할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 말한 배포방식을 중단 배포라고 하며, V1 버전이 종료되고 V2 버전이 실행되는 사이(사용자가 서비스를 사용할 수 없는 시간)을 &lt;b&gt;다운타임(Downtime)&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무중단 배포(Zero-downtime Deployment)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무중단 배포는 말 그대로 서비스가 중단되지 않은 상태로 새로운 버전을 사용자에게 배포하는 것을 의미한다. 무중단 배포를 위해서는 최소 서버가 2개 이상 확보되어야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;롤링 배포(Rolling)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rolling 배포는 서버를 한 대씩 구버전에서 새 버전으로 교체해가는 전략이다. 서비스 중인 서버 한 대를 제외시키고 그 자리에 새 버전의 서버를 추가한다. 이렇게 구 버전에서 새 버전으로 트래픽을 점진적으로 전환한다. 이와 같은 방식은 서버 수의 제약이 있을 경우 유용하나 배포 중 인스턴스의 수가 감소되므로 서버 처리 용량을 미리 고려해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1606&quot; data-origin-height=&quot;1388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLnwbf/btsoTmdC8sX/MBX9zmIVdTcq5UJqe2NG6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLnwbf/btsoTmdC8sX/MBX9zmIVdTcq5UJqe2NG6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLnwbf/btsoTmdC8sX/MBX9zmIVdTcq5UJqe2NG6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLnwbf%2FbtsoTmdC8sX%2FMBX9zmIVdTcq5UJqe2NG6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1606&quot; height=&quot;1388&quot; data-origin-width=&quot;1606&quot; data-origin-height=&quot;1388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;롤링 방식은 k8s, elastic beanstalk과 같은 &lt;b&gt;많은 오케스트레이션 도구에서 지원&lt;/b&gt;하여 간편하다. 또한, &lt;b&gt;많은 서버 자원 확보하지 않아도 무중단 배포&lt;/b&gt;가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점진적으로 새로운 버전이 출시되므로, &lt;b&gt;배포로 인한 위험성이 다소 줄어들&lt;/b&gt; 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배포 도중 서비스 중인 인스턴스의 수가 줄어들어 각각의 서버가 부담하는 트래픽의 양이 늘어날&lt;/b&gt; 수 있다. 따라서 전체적인 트래픽 양과 단일 서버가 처리할 수 있는 트래픽의 양을 잘 파악하여 배포를 진행해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 구 버전과 신 버전의 어플리케이션이 동시에 서비스되기 때문에 &lt;b&gt;호환성 문제가 발생&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;블루/그린 배포(Blue/Green)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blue/Green 배포는 구 버전에서 신 버전으로 일제히 전환하는 전략이다. 구 번의 서버와 신 버전의 서버들을 동시에 나란히 구성하고 배포 시점이 되면 트래픽을 일제히 전환시킨다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1890&quot; data-origin-height=&quot;1166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nXgmB/btsoNVBBBV7/JbXfLJUFLr6zleqykGB8Ik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nXgmB/btsoNVBBBV7/JbXfLJUFLr6zleqykGB8Ik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nXgmB/btsoNVBBBV7/JbXfLJUFLr6zleqykGB8Ik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnXgmB%2FbtsoNVBBBV7%2FJbXfLJUFLr6zleqykGB8Ik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1890&quot; height=&quot;1166&quot; data-origin-width=&quot;1890&quot; data-origin-height=&quot;1166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;롤링 배포와 달리 한번에 트래픽을 모두 새로운 버전으로 옮기기 때문에 &lt;b&gt;호환성 문제가 발생하지 않는다&lt;/b&gt;. 또한, &lt;b&gt;빠른 롤백이 가능&lt;/b&gt;하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 운영에 필요한 서&lt;b&gt;버 리소스 대비 2배의 리소스를 확보&lt;/b&gt;해야한다. 클라우드 환경에서 운영한다면 필요없는 인스턴스는 제거하면 그만이지만, 온프레미스 방식으로 서비스를 운영했다면 &lt;b&gt;비용 부담이 크다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;카나리 배포(Canary)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Canary 배포는 위험을 빠르게 감지할 수 있는 배포 방법이다. 점진적으로 구 버전에 대한 트래픽을 새 버전으로 옮기는 것은 롤링 방식이랑 비슷하나 Canary 배포의 핵심은 새로운 버전에 대한 오류를 조기에 감지하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소수 인원에 대해서만 트래픽을 새로운 버전으로 옮겨둔 상태에서 서비스를 운영하고 새로운 버전에 이상이 없다고 판단하였을 경우에 모든 트래픽을 신규 버전으로 옮긴다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;1430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/llp2a/btsoQuqKXVT/JzuOdnoRdRseYZzaXSFaBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/llp2a/btsoQuqKXVT/JzuOdnoRdRseYZzaXSFaBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/llp2a/btsoQuqKXVT/JzuOdnoRdRseYZzaXSFaBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fllp2a%2FbtsoQuqKXVT%2FJzuOdnoRdRseYZzaXSFaBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1516&quot; height=&quot;1430&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;1430&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;새로운 버전으로 인한 위험을 최소화&lt;/b&gt; 할 수 있다. 또한, &lt;b&gt;오류율 및 성능 모니터링에 유용&lt;/b&gt;하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;롤링 배포와 마찬가지로 구 버전과 신 버전의 어플리케이션이 동시에 존재하므로 &lt;b&gt;호환성 문제&lt;/b&gt;가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며 ..&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 실무에서 사용하는 방식은 롤링 배포 전략인 거 같다. pod가 총 10개이며 헬스 체크가 끝나면 텀을 두고 구 버전 pod와 신 버전 pod를 하나씩 교체하면서 배포를 진행하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 시 마다 모든 Pod가 교체되기 전에 롤링 방식의 단점인 구 버전과 신 버전의 호환성 문제가 발생하여 정확한 분석은 모든 pod교체 된후(배포 작업이 끝난 후)에 진행하고 있는데 그 이유도 이제는 알게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[참조]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onlywis.tistory.com/10&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://onlywis.tistory.com/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hudi.blog/zero-downtime-deployment/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://hudi.blog/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://may9noy.tistory.com/678&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://may9noy.tistory.com/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기타</category>
      <category>롤링</category>
      <category>무중단</category>
      <category>배포</category>
      <category>배포전략</category>
      <category>블루그린</category>
      <category>카나리</category>
      <author>다본죽</author>
      <guid isPermaLink="true">https://dabonee.tistory.com/101</guid>
      <comments>https://dabonee.tistory.com/101#entry101comment</comments>
      <pubDate>Tue, 25 Jul 2023 13:57:50 +0900</pubDate>
    </item>
    <item>
      <title>[Redis] 캐시(Cache) 설계 전략</title>
      <link>https://dabonee.tistory.com/100</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis 캐시(Cache) 설계 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;본 포스트에 앞서..&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스트에서 Redis에 대한 정리를 하는 이유를 다시 복기하자면,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;`유저가 많아진 게시판 웹페이지의 첫페이지가 항상 느린데 이유가 무엇인가? 성능 개선 방법은 무엇인가?`&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 대한 대답을 찾기 위해서였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스트에서 Redis 설계 전략을 알아보고 저 질문에 대한 내 생각을 적어보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis - 캐시(Cache) 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐싱 전략은 웹 서비스 환경에서 시스템 성능 향상을 기대할 수 있는 중요한 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐싱 전략을 구현할 때 고려해야 할 점 중 하나는 캐시 데이터의 수명이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시는 메모리를 사용하기 때문에 RDM보다 훨씬 빠르다. 하지만 용량이 작아&amp;nbsp; 모든 데이터를 지우지 않고 캐시 저장소에 저장하면 용량 부족 현상으로 인해 시스템이 다운될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러므로 캐시 만료 정책을 적절하게 설정하고 사용할 것인지가 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;캐싱 전략 패턴 종류&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시를 이용하게 되면 반드시 닥쳐오는 문제점이 바로 데이터 정합성이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 정합성 문제란, 캐시 저장소(Redis)에 있는 데이터와 DB에 있는 데이터가 다른 경우이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시를 사용하기 전에는 DB에서만 데이터 조회/수정을 했기 때문에 데이터 정합성 문제가 나타나지 않지만, 캐시를 사용하면 또 다른 데이터 저장소가 생기기 때문에 주 저장소에 저장된 값이 서로 다를 수 있는 현상이 일어날수 밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 적절한 캐시 일기 전략(Read Cache Strategy)과 캐시 쓰기 전략(Write Cache Strategy)을 통해, 캐시와 DB 간의 데이터 불일치 문제를 극복하면서도 빠른 성능을 잃지 않도록 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐시 읽기 전략(Read Cache Strategy)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Look Aside 패턴&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cache Aside 패턴이라고도 불림.&lt;/li&gt;
&lt;li&gt;데이터를 찾을 때 우선 캐시에 저장된 데이터가 있는지 우선적으로 확인하는 전략.&lt;/li&gt;
&lt;li&gt;캐시가 없다면 DB에서 조회.&lt;/li&gt;
&lt;li&gt;반복적인 읽기가 많을 때 적합.&lt;/li&gt;
&lt;li&gt;캐시와 DB가 분리되어 가용.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;원하는 데이터만 별도 구성하여 캐시에 저장.&lt;/li&gt;
&lt;li&gt;캐시 서버가 장애나도 DB에서 데이터를 가져올 수 있어  캐시 장애 대비 구성이 되어있음.&lt;/li&gt;
&lt;li&gt;단, 캐시에 붙어있는 Connection이 많았다면 캐시 서버 장애 시 다운된 순간 DB로 몰려 부하 발생.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;722&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XC1tF/btsoz81Mu8T/PDNbsyzsbuvzK8o9VvAPaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XC1tF/btsoz81Mu8T/PDNbsyzsbuvzK8o9VvAPaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XC1tF/btsoz81Mu8T/PDNbsyzsbuvzK8o9VvAPaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXC1tF%2Fbtsoz81Mu8T%2FPDNbsyzsbuvzK8o9VvAPaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1360&quot; height=&quot;722&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;722&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;1. Client에서 서버에 데이터 요청&lt;br /&gt;2. Server는 Cache에 해당 데이터가 있는지 확인&lt;br /&gt;3. 데이터가 존재하면 서버에 전달 (Cache Hit)&lt;br /&gt;4. 데이터가 없으면 DB에서 조회 (Cache Miss)&lt;br /&gt;6. Cache에 DB에서 조회해 온 데이터 업데이트&lt;br /&gt;7. Client에게 데이터 응답&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Look Aside 패턴은 애플리케이션에서 일반적으로 사용되는 기본적인 캐시 전략.&lt;/li&gt;
&lt;li&gt;Cache Store와 DB 간의 정합성 유지가 발생 할 수 있으며, 초기 조회 시 무조건 DB를 호출해야 하므로 단건 호출 빈도가 높은 서비스에서는 적합하지 않음.&lt;/li&gt;
&lt;li&gt;대신, 반복적인 동일 쿼리를 수행하는 서비스에 적합한 아키텍쳐.&lt;/li&gt;
&lt;li&gt;이런 경우, DB에서 캐시로 데이터를 미리 넣어주는 작업을 하기도 하는데 이를 Cache Warming이라고 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 34px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 17px;&quot;&gt;[Cache Warming]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 17px;&quot;&gt;Cache Warming은 미리 Cache로 데이터를 넣어두는 작업을 의미한다.&lt;br /&gt;이 작업을 수행하지 않으면 서비스 초기 트래픽 급증시 대량의 cache miss가 발생하여 DB의 부하가 급증할 수 있다(Thundering Herd).&lt;br /&gt;다만, 캐시는 메모리를 사용하여 용량이 매우 작으므로 데이터를 무한정으로 가지고 있을 수 없어 일정시간이 지나면 expire가 되어 다시 Thundergin Herd가 발생 할 수 있다.&lt;br /&gt;따라서, TTL을 잘 조정해야 할 필요가 있다.(뒤에서 설명)&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Thundering Herd는 모든 지점에서 발생하는 것은 아니고, 서비스 첫 페이지와 같은 대부분 조회가 몰리는 지점에서 주로 발생한다.&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Read Through 패턴&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt; 캐시에서만 데이터를 읽어오는 전략(inline cache).&lt;/li&gt;
&lt;li&gt;Look Aside와 비슷하지만 데이터 동기화를 라이브러리 또는 캐시 제공자에게 위임하는 방식이라는 차이가 있음.&lt;/li&gt;
&lt;li&gt;따라서 데이터 조회에 있어 전체적으로 속도가 느림.&lt;/li&gt;
&lt;li&gt;데이터 조회를 전적으로 캐시에만 의존하므로, 캐시 서버가 죽으면 서비스에 문제가 생길 수 있음.&lt;/li&gt;
&lt;li&gt;캐시와 DB 간 데이터 동기화가 지속적으로 이루어지므로 데이터 정합성 문제에서는 벗어날 수 있음.&lt;/li&gt;
&lt;li&gt;읽기가 많은 워크로드에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfPKdi/btsoIOad40C/5eVGmF4jSka17eUpjhZ2k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfPKdi/btsoIOad40C/5eVGmF4jSka17eUpjhZ2k0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfPKdi/btsoIOad40C/5eVGmF4jSka17eUpjhZ2k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfPKdi%2FbtsoIOad40C%2F5eVGmF4jSka17eUpjhZ2k0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1328&quot; height=&quot;660&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 17px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 17px;&quot;&gt;1. Client에서 서버에 데이터 요청&lt;br /&gt;2. Server는 Cache에 해당 데이터가 있는지 확인, 데이터가 있으면 서버에 데이터 전달(Cache Hit)&lt;br /&gt;3,4. 데이터가 없으면 &lt;b&gt;캐시에서 DB에 조회 후 자체 업데이트&lt;/b&gt;(Cache Miss)&lt;br /&gt;5. 데이터를 서버에 전달&lt;br /&gt;6. Client에게 데이터 응답&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Look Aside 패턴과 차이점은 캐시에 저장하는 주체가 서버가 아닌 DB 자체임.&lt;/li&gt;
&lt;li&gt;직접적인 DB 접근은 최소화하고 Read에 대한 소모되는 자원을 최소화 할 수 있음.&lt;/li&gt;
&lt;li&gt;캐시 서버가 죽으면 서비스 전체가 영향이 있으므로 Replication 또는 Cluster로 구성하여 가용성을 높여야 함.&lt;/li&gt;
&lt;li&gt;이 방식 또한, Cache Warming을 해주는 것이 좋음.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐시 쓰기 전략(Write Cache Strategy)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Write Back 패턴&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Write behind 패턴이라고도 불림.&lt;/li&gt;
&lt;li&gt;캐시와 DB 동기화를 비동기하기 때문에 동기화 과정이 생략.&lt;/li&gt;
&lt;li&gt;데이터를 저장할 때 DB에 바로 쿼리하지 않고, 캐시에 모아서 일정 주기로 배치 작업을 통해 DB에 반영.&lt;/li&gt;
&lt;li&gt;캐시에 모아놨다가 DB에 쓰기 때문에 쓰기 쿼리 회수 비용과 부하를 줄일 수 있음.&lt;/li&gt;
&lt;li&gt;Write가 빈번하면서 Read를 하는데 많은 양의 Resource가 소모되는 서비스에 적합&lt;/li&gt;
&lt;li&gt;데이터 정합성 확보&lt;/li&gt;
&lt;li&gt;자주 사용되지 않는 불필요한 리소스 저장.&lt;/li&gt;
&lt;li&gt;캐시에서 오류가 발생하면 데이터 영구 손실.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmNOso/btsoGfF943y/4TlWvGjypNL993ttvQuM10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmNOso/btsoGfF943y/4TlWvGjypNL993ttvQuM10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmNOso/btsoGfF943y/4TlWvGjypNL993ttvQuM10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmNOso%2FbtsoGfF943y%2F4TlWvGjypNL993ttvQuM10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;980&quot; height=&quot;480&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;1. Client가 데이터 업데이트 요청을 한다.&lt;br /&gt;2. 모든 데이터를 Cache에 저장한다.&lt;br /&gt;3. 일정 시간 뒤 DB에 모두 업데이트한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Write Back 방식은 데이터를 저장할 때, DB가 아닌 먼저 캐시에 저장&lt;/li&gt;
&lt;li&gt;캐시에 모인 데이터를 특정 시점마다 DB로 쓰는 방식&lt;/li&gt;
&lt;li&gt;Cache가 일종의 Queue 역할.&lt;/li&gt;
&lt;li&gt;캐시에 데이터를 모았다가 한 번에 DB에 저장하기 때문에 DB 쓰기 횟수 비용과 부하를 줄일 수 있음.&lt;/li&gt;
&lt;li&gt;데이터를 옮기기 전 Cache 서버가 죽으면 데이터가 유실되지만 반대로 DB가 죽어도 지속적인 서비스 제공 가능.&lt;/li&gt;
&lt;li&gt;캐시에 Replication이나 Cluster 구조를 적용함으로써 캐시 서비스의 가용성을 높이는 것이 좋으며, 캐시 읽기 전략인 Read-Through와 결합하면 가장 최근에 업데이트된 데이터를 항상 캐시에서 사용할 수 있는 혼합 워크로드에 적합.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Write Through 패턴&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스와 Cache에 동시에 데이터를 저장하는 전략.&lt;/li&gt;
&lt;li&gt;데이터를 저장할 때 먼저 캐시에 저장한 다음 DB에 저장(모아놓았다 저장하는 것이 아닌 바로 저장).&lt;/li&gt;
&lt;li&gt;Read Through와 마찬가지로 DB 동기화 작업을 Cache에 위임.&lt;/li&gt;
&lt;li&gt;DB와 캐시가 항상 동기화 되어있어, 캐시의 데이터는 항상 최신의 상태로 유지.&lt;/li&gt;
&lt;li&gt;캐시와 백업 저장소에 업데이트를 같이 하여 데이터 일관성을 유지할 수 있어서 안정적.&lt;/li&gt;
&lt;li&gt;데이터 유실이 발생하면 안되는 상황에 적합.&lt;/li&gt;
&lt;li&gt;자주 사용되지 않은 불필요한 리소스 저장.&lt;/li&gt;
&lt;li&gt;매 요청마다 두번의 Write가 발생하게 됨으로써 빈번한 생성/수정이 발생하는 서비스에서는 성능 이슈 발생.&lt;/li&gt;
&lt;li&gt;기억장치 속도가 느릴 경우, 데이터를 기록할 때 CPU가 대기하는 시간이 필요하기 때문에 성능 감소.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cilnGr/btsoNWlPKWr/Nv5dE7vlgJzAMbupT8elkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cilnGr/btsoNWlPKWr/Nv5dE7vlgJzAMbupT8elkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cilnGr/btsoNWlPKWr/Nv5dE7vlgJzAMbupT8elkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcilnGr%2FbtsoNWlPKWr%2FNv5dE7vlgJzAMbupT8elkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1316&quot; height=&quot;628&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;628&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Write Through 패턴은 Cache에도 반영하고 DB에도 동시에 반영하는 방식.&lt;/li&gt;
&lt;li&gt;그래서 항상 동기화되어 있는 최신의 버전을 가지고 있음.&lt;/li&gt;
&lt;li&gt;하지만, 결국 2단계 과정을 거쳐야하기 때문에 상대적으로 느리고, 무조건 일단 캐시에 저장하기 때문에 캐시에 넣은 데이터를 저장만하고 사용하지 않을 가능성이 있어 리소스 낭비 가능성이 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;Write Through와 Write Back 패턴 모두 둘 다 자주 사용되지 않은 데이터가 저장되어 리소스 낭비가 발생하는 문제점이 있다. 이를 해결하기 위해 TTL을 꼭 사용하여 사용되지 않는 데이터는 반드시 삭제해줘야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Write Around 패턴&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Write Through보다 훨씬 빠름.&lt;/li&gt;
&lt;li&gt;모든 데이터는 DB에 저장(캐시를 갱신하지 않음).&lt;/li&gt;
&lt;li&gt;Cache Miss가 발생하는 경우에만 DB와 캐시에도 데이터를 저장.&lt;/li&gt;
&lt;li&gt;따라서 캐시와 DB 내의 데이터가 다를 수 있음(데이터 불일치).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;634&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xTuEM/btsoRfZwND0/88zUEZu8p4XG6Iab76WW7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xTuEM/btsoRfZwND0/88zUEZu8p4XG6Iab76WW7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xTuEM/btsoRfZwND0/88zUEZu8p4XG6Iab76WW7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxTuEM%2FbtsoRfZwND0%2F88zUEZu8p4XG6Iab76WW7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1300&quot; height=&quot;634&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;634&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;1. Client에서 데이터 업데이트를 요청한다.&lt;br /&gt;2. 모든 데이터는 DB에 저장한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Write Around 패턴은 속도가 빠르지만, Cache Miss가 발생하기 전 DB에 저장된 데이터가 수정되었을 경우, 사용자가 조회하는 캐시와 DB 간의 데이터 불일치가 발생&lt;/li&gt;
&lt;li&gt;따라서, 저장된 데이터가 수정/삭제될 때 마다, 캐시 또한 삭제하거나 변경해야 하며, 캐시의 expire를 짧게 조정하는 식으로 대처해야 함.&lt;/li&gt;
&lt;li&gt;Write Around 패턴은 주로 Look Aside, Read Through와 결해서 사용하며, 데이터가 한 번 쓰여지고, 덜 자주 읽히거나 읽지 않는 상황에서 좋은 성능을 제공함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐시 읽기&amp;nbsp; +&amp;nbsp; 쓰기 전략 조합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Look Aside + Write Around 조합&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 일반적으로 자주 쓰이는 조합&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1292&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8wCBk/btsoRzKkbhW/iqBDIdQvQlZ4uzeEzWCkkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8wCBk/btsoRzKkbhW/iqBDIdQvQlZ4uzeEzWCkkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8wCBk/btsoRzKkbhW/iqBDIdQvQlZ4uzeEzWCkkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8wCBk%2FbtsoRzKkbhW%2FiqBDIdQvQlZ4uzeEzWCkkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1292&quot; height=&quot;666&quot; data-origin-width=&quot;1292&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Read Through + Write Around 조합&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;항상 DB에 쓰고, 캐시에서 읽을 때 항상 DB에서 먼저 읽어오므로 데이터 정합성 이슈에 대한 완벽한 안전 장치를 구성할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1274&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBKDR3/btsoGhjMi8N/GiRmg2rTRQ9DVpAo55AphK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBKDR3/btsoGhjMi8N/GiRmg2rTRQ9DVpAo55AphK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBKDR3/btsoGhjMi8N/GiRmg2rTRQ9DVpAo55AphK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBKDR3%2FbtsoGhjMi8N%2FGiRmg2rTRQ9DVpAo55AphK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1274&quot; height=&quot;630&quot; data-origin-width=&quot;1274&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Read Through + Write Through 조합&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 쓸 때 항상 캐시에 먼저 쓰므로, 읽어올 때 최신 캐시 데이터 보장&lt;/li&gt;
&lt;li&gt;데이터를 쓸 때 항상 캐시에서 DB로 보내므로, 데이터 정합성 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sNDsY/btsoQKr6Nxa/RYuO7g9rkeOdhl06ksJsPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sNDsY/btsoQKr6Nxa/RYuO7g9rkeOdhl06ksJsPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sNDsY/btsoQKr6Nxa/RYuO7g9rkeOdhl06ksJsPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsNDsY%2FbtsoQKr6Nxa%2FRYuO7g9rkeOdhl06ksJsPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1632&quot; height=&quot;804&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TTL(Time To Live)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TTL은 Key가 자연스럽게 만료되어 없어지게 하는 시간(Redis의 경우 Integer 당 1ms)이다. 각 전략에 TTL을 추가하면 이득을 취할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Look Aside : &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;자연스럽게 값이 삭제되어 캐시 미스가 발생하고 최신의 값을 유지 가능.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;Write Through : &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;업데이트가 되지 않은 데이터를 자동 삭제하여 메모리 이득을 볼 수 있음.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cache Stampede 현상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 트래픽 환경에서 TTL 값이 너무 작게 설정되면 Cache Stampede 현상이 발생할 가능성이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Look-aside 패턴에서 redis에 데이터가 없다는 응답을 받은 서버(Cache Miss)가 직접 DB로 데이터 요청한 뒤, 다시 redis에 저장하는 과정을 거친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 위 상황에서 key가 만료되는 순간 많은 서버에서 이 key를 같이 보고 있었다면 모든 어플리케이션 서버에서 DB로 가서 찾게 되는 duplicate read&amp;nbsp;가 발생한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;또 읽어온 값을 각 각 redis에 쓰는 duplicate write 도 발생되어, 처리량도 다 같이 느려질 뿐 아니라 불필요한 작업이 굉장히 늘어나 요청양 폭주로 장애로 이어질 가능성 도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1910&quot; data-origin-height=&quot;1182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qYbrz/btsoMHJvYoV/wIevSsON6GtvkwvOpp1fak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qYbrz/btsoMHJvYoV/wIevSsON6GtvkwvOpp1fak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qYbrz/btsoMHJvYoV/wIevSsON6GtvkwvOpp1fak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqYbrz%2FbtsoMHJvYoV%2FwIevSsON6GtvkwvOpp1fak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1910&quot; height=&quot;1182&quot; data-origin-width=&quot;1910&quot; data-origin-height=&quot;1182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐시 공유 방식 지침&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시는 어플리케이션의 여러 인스턴스에서 공유하도록 설계 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 각 어플리케이션 인스턴스가 캐시에서 데이터를 읽고 수정할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 한 어플리케이션이 캐시에 보유하고 있는 데이터를 수정하는 경우, 다른 어플리케이션의 인스턴스가 변경을 덮어쓰지 않도록 해야한다. 그렇지 않으면 데이터 정합성 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터의 충돌을 방지하기 위해 다음과 같은 어플리케이션 개발 방식을 취해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시 데이터를 변경하기 직전에 데이터가 검색된 이후 변경되지 않았는지 일일히 확인하는 방법
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;변경하지 않았다면 즉시 업데이트&lt;/li&gt;
&lt;li&gt;변경되었다면 업데이트 여부를 어플리케이션 레벨에서 결정&lt;/li&gt;
&lt;li&gt;업데이트가 드믈고 충돌이 발생하지 않는 상황에서 적용하기 용이&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;캐시 데이터를 업데이트 하기 전 Lock을 잡는 방법
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;조회성 업무를 처리하는 서비스에 Lock으로 인한 대기현상이 발생.&lt;/li&gt;
&lt;li&gt;데이터의 사이즈가 작아 빠르게 업데이트가 가능한 업무와 빈번한 업데이트가 발생하는 상황에 적용하기 용이&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며..&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 캐시에 대한 정리를 마치며 마지막으로&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;`유저가 많아진 게시판 웹페이지의 첫페이지가 항상 느린데 이유가 무엇인가? 성능 개선 방법은 무엇인가?`&amp;nbsp;&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;/i&gt;에 대한 나의 대답을 정리한 Redis, 캐시 개념을 가지고 내려보면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 유저가 많아진 게시판 웹페이지의 '첫'페이지가 항상 느린 이유에 대한 대답은 &lt;b&gt;Thundering Herd&lt;/b&gt; 때문이다. Thundering Herd는 대부분의 조회가 이루어지는 서비스 첫 페이지 같은 곳에서 발생한다. 이를 해결하기 위해 &lt;b&gt;Cache Warming&lt;/b&gt;을 해주어야 한다. Cache Warming이란, 초기에 미리 DB에 있는 데이터를 캐시에 넣어주는 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 빈번하게 업데이트가 되는 게시물의 '좋아요' 등은 캐시 쓰기 전략의 &lt;b&gt;Write Back&lt;/b&gt; 패턴을 사용하여 캐시에 데이터를 저장해놓았다가&amp;nbsp; 일정 시간이 되면 한번에 DB에 저장하는 방식을 사용하면 성능 개선을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론, 이 질문의 대한 나의 대답이 틀릴 수도 있다. 혹은 성능 개선에 대한 더 좋은 방법이 있을 수도 있다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;향후에 내가 실제로 실무에서 Redis를 사용하게 되어 같은 상황을 마주하고 다른 방식으로 해결한다면 그 때 어떻게 해결했는지 포스팅을 해봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://inpa.tistory.com/entry/REDIS-%F0%9F%93%9A-%EC%BA%90%EC%8B%9CCache-%EC%84%A4%EA%B3%84-%EC%A0%84%EB%9E%B5-%EC%A7%80%EC%B9%A8-%EC%B4%9D%EC%A0%95%EB%A6%AC#recentEntries&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://inpa.tistory.com/entry/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@jinmin2216/%EB%A9%94%EC%8B%9C%EC%A7%95-%EC%8B%9C%EC%8A%A4%ED%85%9C-Kafka-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://velog.io/@jinmin2216/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sabarada.tistory.com/142#google_vignette&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://sabarada.tistory.com/&lt;/a&gt;&lt;/p&gt;</description>
      <category>기타</category>
      <category>Cache</category>
      <category>redis</category>
      <category>캐시</category>
      <author>다본죽</author>
      <guid isPermaLink="true">https://dabonee.tistory.com/100</guid>
      <comments>https://dabonee.tistory.com/100#entry100comment</comments>
      <pubDate>Mon, 24 Jul 2023 17:10:30 +0900</pubDate>
    </item>
    <item>
      <title>[리뷰] 테스트 주도 개발 시작하기</title>
      <link>https://dabonee.tistory.com/99</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;[리뷰] 테스트 주도 개발 시작하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;x9788980783052.jpeg&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;303&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WyUq8/btsol8mjyoq/tKWAGRf93mfv56WAheFX40/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WyUq8/btsol8mjyoq/tKWAGRf93mfv56WAheFX40/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WyUq8/btsol8mjyoq/tKWAGRf93mfv56WAheFX40/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWyUq8%2Fbtsol8mjyoq%2FtKWAGRf93mfv56WAheFX40%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;458&quot; height=&quot;303&quot; data-filename=&quot;x9788980783052.jpeg&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;303&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 개발을 하다보니 테스트가 중요한 지를 깨달았다. 특히 이번 프로젝트처럼 규모가 어느정도 되는 시스템일 경우는 더더욱 중요한 걸 느꼈다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 맡은 서버가 단말 중개 서버(Bridge)였기 대부분의 인입이 프로토콜의 형태로 들어왔고, 비즈니스 로직도 커서 개발하면서 테스트를 진행하는 건 꿈도 못꾸고 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다 보니 유닛 테스트 부터 통합 테스트까지 제대로 검증을 못한 채로 필드 테스트를 진행했다. 결과는 역시나 .. 여기 저기서 에러가 뻥뻥 터지고 수정하고 터지고 수정하고 오픈까지 이 일의 연속이었다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 때 난 이런 생각을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;`이런 시스템은 테스트를 어떻게 하지? 개발하면서 테스트를 충분히 했다면 좀 더 수월하지 않았을까? 내가 다시 돌아간다면 어떻게 테스트를 할 수 있을까?`&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 프로젝트를 오픈하고 가장 먼저 집어 든 책이 바로 이 책이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리뷰&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서론이 길었다. 본격적으로 이 책을 리뷰하자면, 먼저 나는 나같은 주니어에게는 `강추`이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 TDD에 대해서 모르는 사람은 없을 것이다. 그러나 어떻게 하는 건지 아는 사람은 많이 없을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 그랬다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;'TDD는 테스트 주도 개발로 테스트 코드를 먼저 짜고 기능을 개발하는 거야! 나도 요즘 트렌드에 맞게 TDD 개발 해봐야겠다!!(IntelliJ를 키고 스프링 부트 프로젝트를 만든 다음 테스트 폴더에 클래스를 하나 일단 만든다.) 음.. 근데 그걸 어떻게 하지? 기능이 없는데 테스트를? 이게 뭔 소리지?'&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 나의 모습이었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책은 나에게 시작도 못하던 TDD를 시작할 수 있게 해주는 가이드를 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 TDD를 시작하지 못했던, 만들어 놓은 코드가 없는데 테스트를 하는 지, 어떤 거 부터 시작하는지 자세하게 알려준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 이 책에서 말하는 TDD 절차(방법)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TDD 절차&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;테스트할 대상과 케이스 선택&lt;/li&gt;
&lt;li&gt;해당 테스트에 대한 테스트 케이스 코드 작성&lt;/li&gt;
&lt;li&gt;컴파일 오류를 없애는데 필요한 클래스와 메소드 등을 추가한다.&lt;/li&gt;
&lt;li&gt;테스트가 통과되도록 구현한다.(처음엔 하드 코딩 등)&lt;/li&gt;
&lt;li&gt;4번에서 구현한(하드 코딩 등) 목록을 일반화(구체적으로)하여 구현한다.&lt;/li&gt;
&lt;li&gt;테스트를 통과했다면 5번의 코드를 리팩토링 요소를 찾아 리팩토링을 한다.&lt;/li&gt;
&lt;li&gt;모든 테스트가 통과하는지 확인한다.&lt;/li&gt;
&lt;li&gt;새로운 케이스를 선정 후 반복하여 점진적으로 코드를 완성해 나간다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 보면 정말 간단하고 쉽게 시작이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 내가 이걸 보면서 생각하는 게 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;'아 시작은 이제 어떻게 하는지 알겠는데.. 내가 진행한 프로젝트는 DB작업도 많고, 외부 연동도 많고, 얽히고 설킨게 많은데 이게 가능한거야? 실제 실무에서 사용하려면 어떻게 해야하는거지?'&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신기하게도 이런 생각하면서 의심 반, 걱정 반 하며 읽을 때 쯤 필자가 먼저 얘기를 꺼낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;`실제로 실무에서는 외부 연동이나 DB Connection 등 테스트하기 힘들게 만드는 외부 요소들이 존재한다. 이럴 때는 '대역'을 사용한다`&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대역에는 스텁(Stub), 가짜(Fake), 스파이(Spy), 모의(Mock)이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 사용하여 테스트가 힘든 외부 요인이나 결과를 대체한다. 하지만, 대역을 사용할 때 주의해야 하는 사항들이 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모의 객체를 과하게 사용하면 오히려 테스트 코드가 복잡해질 수 있다.&lt;/li&gt;
&lt;li&gt;결과 값을 확인하는 수단으로 모의 객체를 사용하면 결과 검증 코드가 길어지고 복잡해진다.&lt;/li&gt;
&lt;li&gt;모의 객체는 기본적으로 메서드 호출 여부를 검증하는 수단이므로, 테스트 대상과 모의 객체 간의 상호 작용이 조금만 바뀌어도 테스트가 깨지기 쉽다.&lt;/li&gt;
&lt;li&gt;DAO나 리포지토리와 같은 저장소에 대한 대역은 모의 객체보다 메모리를 이용한 가짜 구현을 사용하는 편이 코드의 간결성과 유지보수성에서 더 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지는 TDD의 방법을 주로 다뤘으면 다음 장 부터는 테스트를 위한 설계 관점을 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 위한 설계를 하기 위해서는 꽤 많은 부분을 신경써야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트가 가능 설계는 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;하드 코딩된 상수를 생성자나 메서드 파라미터로&lt;span&gt;&amp;nbsp;&lt;/span&gt;받는다.&lt;/li&gt;
&lt;li&gt;의존 대상을 주입&lt;span&gt;&amp;nbsp;&lt;/span&gt;받는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의존 대상은 생성자 주입으로 교체할 수 있도록 만든다.&lt;/li&gt;
&lt;li&gt;의존 대상을 교체할 수 있게 되면 실제 구현 대신에 대역을 사용할 수 있어 테스트가 수월해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;테스트하고 싶은 코드를 분리한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기능의 일부만 테스트하고 싶은 경우, 해당 코드를 별도 기능으로 분리해서 테스트를 진행 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;시간이나 임의 값 생성 기능을 분리한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 대상이 사용하는 시간이나 임의 값을 제공하는 기능을 별도로 분리해서 테스트 가능성을 높일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;외부 라이브러리는 직접 사용하지 말고 감싸서 사용한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 테스트 코드도 유지보수의 대상이다. 테스트 코드의 유지보수성을 높히기 위한 방법에 대해 소개한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책을 읽고 &lt;b&gt;`그럼 이제 TDD 방법은 알았으니까 이제 실무에서 TDD를 적용해서 개발 할 수 있을까?`&lt;/b&gt; 라는 생각을 해보았을 때, 내 대답은 &lt;b&gt;`흠.. 글쎄.. 간단한 거 정도는 해볼 수 있지 않을까?`&amp;nbsp;&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 생각을 한 나에게 이 책의 필자는 마지막에 이런 말을 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;`물론 100% TDD로 개발할 수는 없겠지만 할 수 있는 한 많은 범위를 TDD로 개발하는 것이 중요하다.`&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;`처음엔 쉬운 것을 TDD로 시도해서 TDD에 익숙해지고 이후에 점진적으로 TDD 적용 범위를 늘려나가자.`&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자의 말 처럼 모든 걸 TDD를 할 필요는 없다. 내가 가능한 간단한 거 정도로 부터 시작하면 언젠간 TDD가 주는 효과를 느낄 수 있지 않을까 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책에 대한 내용과 내 생각은 이 글 하나에 담기엔 역부족하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용이 어렵지 않으니 나같은 주니어라면 기회되면 한번은 읽으면 좋을 거 같다.&lt;/p&gt;</description>
      <category> 리뷰&amp;amp;후기</category>
      <category>java</category>
      <category>Spring</category>
      <category>TDD</category>
      <category>test</category>
      <category>테스트</category>
      <category>테스트 주도 개발 시작하기</category>
      <author>다본죽</author>
      <guid isPermaLink="true">https://dabonee.tistory.com/99</guid>
      <comments>https://dabonee.tistory.com/99#entry99comment</comments>
      <pubDate>Thu, 20 Jul 2023 21:38:48 +0900</pubDate>
    </item>
  </channel>
</rss>