<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>심심한 개발 블로그</title>
    <link>https://simsim231.tistory.com/</link>
    <description>개발 공부 블로그 입니다~</description>
    <language>ko</language>
    <pubDate>Wed, 8 Jul 2026 12:34:59 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>심심231</managingEditor>
    <image>
      <title>심심한 개발 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/3238770/attach/bf81d319c3464c0c8111c3b40f5cd544</url>
      <link>https://simsim231.tistory.com</link>
    </image>
    <item>
      <title>자바 개발자를 위한 코틀린 입문 강의 정리</title>
      <link>https://simsim231.tistory.com/298</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;https://www.inflearn.com/course/java-to-kotlin/dashboard 를 수강하고 학습한 내용을 간단하게 정리한다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1강. 코틀린에서 변수를 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;var과 val의 차이점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;var: 바꿀 수 있는 변수 (ex) var number1 = 10L&lt;/li&gt;
&lt;li&gt;val: 바꿀 수 없는 변수 (ex) val number2 = 10L&lt;/li&gt;
&lt;li&gt;코틀린에서는 모든 변수에 수정 가능 여부(var/val)를 명시해주어야 한다.&lt;/li&gt;
&lt;li&gt;코틀린에서는 타입을 의무적으로 작성하지 않아도 된다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하지만 작성이 가능함 (ex) var number1: Long = 10L&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;var number1 -&amp;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;var number: Int&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;val 변수에 초기값을 지정해주지 않는 경우 처음에 한하여 초기값을 넣어줄 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;val number2:&amp;nbsp; Int&lt;br /&gt;number2 = 10L&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;val인 경우에도 컬렉션에 원소 추가가 가능하다. (Java 와 동일)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Kotlin에서의 Primitive Type&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java에서는 long은 primitive type, Long은 reference type&lt;/li&gt;
&lt;li&gt;Kotlin에서는 구분하고 있지 않음. Long으로 표현해도 Kotlin에서 필요하다면 알아서 내부적으로 primitive type으로 변경해서 처리해줌&lt;/li&gt;
&lt;li&gt;즉, 프로그래머가 boxing/unboxing을 고려하지 않아도 되도록 kotlin이 알아서 처리해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Kotlin에서의 nullable 변수&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 모든 변수는 null이 들어갈 수 없음&lt;/li&gt;
&lt;li&gt;var number3: Long? = null -&amp;gt; 이와 같이 ?를 붙여주는게 nullable 이라는 의미&lt;/li&gt;
&lt;li&gt;Kotlin에서는 nullable 변수면 ?를 붙여줘야함&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Kotlin에서의 객체 인스턴스화&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kotlin에서는 객체 인스턴스화를 할 때 new를 붙이지 않는다.&lt;/li&gt;
&lt;li&gt;val person = Person(&quot;홍길동&quot;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 코틀린에서 null을 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Kotlin에서의 null 체크&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Java에서의 코드&lt;/p&gt;
&lt;pre id=&quot;code_1747230872020&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public boolean startsWithA1(String str) {
    if (str == null) {
    	throw new IllegalArgumentException(&quot;null이 들어왔습니다.&quot;);
    }
	return str.startsWith(&quot;A&quot;);
}&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;2. Kotlin에서의 코드&lt;/p&gt;
&lt;pre id=&quot;code_1747231374770&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun startsWithA1(str: String?): Boolean {
    if (str == null) {
        throw IllegalArgumentException(&quot;null이 들어왔습니다.&quot;)
    }
    return str.startsWith(&quot;A&quot;)
}

// 아래와 같이 null 일 수 있는 값에 대해 함수 호출하려고 하면 오류남
fun startsWithA2(str: String?): Boolean {
    return str.startsWith(&quot;A&quot;) // 오류
}

// 이렇게 위에서 null 체크를 해주면 허용
fun startsWithA2(str: String?): Boolean {
    if (str == null) {
        return false
    }
    return str.startsWith(&quot;A&quot;) // 오류
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Safe Call과 Elvis 연산자&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;null이 가능한 타입만을 위한 기능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Safe Call: null이지 않을 때만 호출. null인 경우 null로 리턴&lt;/p&gt;
&lt;pre id=&quot;code_1747841798601&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val str: String? = &quot;ABC&quot;
str.length // 불가능
str?.length // 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Elvis 연산자: 앞의 값이 null이면 뒤의 값을 사용&lt;/p&gt;
&lt;pre id=&quot;code_1747841885574&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val str: String? = &quot;ABC&quot;
str?.length ?: 0 // null이면 0&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;그래서 위의 코드를 kotlin스럽게 바꾸면?&lt;/p&gt;
&lt;pre id=&quot;code_1747841998058&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun startsWithA1(str: String?): Boolean {
    return str?.startsWith(&quot;A&quot;)
      ?: throw IllegalArgumentException(&quot;null이 들어왔습니다&quot;)
}&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;이것도 kotlin으로 바꿀 수 있음&lt;/p&gt;
&lt;pre id=&quot;code_1747842149089&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Java
public long calculate(Long number) {
    if (number == null) return 0;
}

// kotlin
fun calculate(number: Long?): Long {
    number ?: return 0
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;널 아님 단언!!&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nullable type이지만, 절대 null이 될 수 없는 경우&lt;/p&gt;
&lt;pre id=&quot;code_1747842219434&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun startsWithA1(str: String?): Boolean {
    return str!!.startsWith(&quot;A&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 근데 만약 null이 들어오면? NPE가 남&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;플랫폼 타입&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin에서 Java 코드를 가져다 사용할 때 어떻게 처리될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 Java 상에서 Nullable이면 person.name 못넣음. NotNull 이면 넣을 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 Java의 Annotation을 사용하면 Kotlin에서는 이를 이해할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 annotation이 없다면? Kotlin에서는 알 수 없음. 이런 타입을 &quot;플랫폼 타입&quot;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류도 나지 않고 runtime에 NPE가 난다.&lt;/p&gt;
&lt;pre id=&quot;code_1747842341955&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Person {
    private final String name;
    
    public Person(String name) {
        this.name = name;
    }
    
    @Nullable
    public String getName() {
        return name;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1747842422316&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    val person = Person(&quot;AAA&quot;)
    startsWithA(person.name)
}

fun startsWithA(str:String): Boolean {
    return str.startsWith(&quot;A&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 코틀린에서 Type을 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기본 타입&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java와 동일 Byte, Short, Int, Long, Float, Double&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. 코틀린에서는 선언된 기본값을 보고 타입을 추론한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748176543242&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val num1 = 2 // int
val num2 = 2L // long&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;2. 코틀린에서는 기본 타입간의 변환은 명시적으로 이루어져야 한다. &amp;lt;-&amp;gt; Java에서는 암시적 변환&lt;/p&gt;
&lt;pre id=&quot;code_1748176620260&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val num1 = 4

val num2: Long = num1 // Type mismatch compile error
val num2: Long = num1.toLong() // success&lt;/code&gt;&lt;/pre&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;table style=&quot;border-collapse: collapse; width: 100%; height: 115px;&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: 21px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 21px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 21px;&quot;&gt;value가 Type이면&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 21px;&quot;&gt;value가 Type이 아니면&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;value is Type&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;return true&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;return false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;value !is Type&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;return false&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;return true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 21px;&quot;&gt;value as Type&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 21px;&quot;&gt;Type으로 타입 캐스팅&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 21px;&quot;&gt;예외 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 39px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 39px;&quot;&gt;value as? Type&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 39px;&quot;&gt;Type으로 타입 캐스팅&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 39px;&quot;&gt;null&lt;br /&gt;(+ value가 null이면 null)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre id=&quot;code_1748177390109&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// java
public void printAgeIfPerson(Object obj) {
    if (obj instanceof Person) {
        Person person = (Person) obj;
        System.out.println(person.getAge());
    }
}

// kotlin
fun printAgeIfPerson(obj: Any) {
    if (obj is Person) {
        // as Person은 생략 가능 
        // 스마트 캐스트: 컨택스트를 분석해서 Person인지 체크를 해줌 -&amp;gt; 타입 인지
        val person = obj as Person 
        println(person.age)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Kotlin의 3가지 특이한 타입&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Any&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java의 Object 역할 (모든 객체의 최상위 타입)&lt;/li&gt;
&lt;li&gt;모든 Primitive Type의 최상위 타입도 Any이다.&lt;/li&gt;
&lt;li&gt;Any 자체로는 null을 포함할 수 없어 null을 포함하고 싶다면, Any?로 표현&lt;/li&gt;
&lt;li&gt;Any에 equals / hashCode / toString 존재&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unit&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java의 void와 동일한 역할&lt;/li&gt;
&lt;li&gt;void와 다르게 Unit은 그 자체로 타입 인자로 사용 가능하다. (void는 Void 클래스 사용 필요)&lt;/li&gt;
&lt;li&gt;함수형 프로그래밍에서 Unit은 단 하나의 인스턴스만 갖는 타입을 의미. 즉, 코틀린의 Unit은 실제 존재하는 타입이라는 것을 표현.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nothing&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;/ul&gt;
&lt;pre id=&quot;code_1748180801925&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun fail(message: String): Nothing {
    throw IllegalArgumentException(message)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;String Interpolation, String indexing&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열과 변수 연결&lt;/p&gt;
&lt;pre id=&quot;code_1748180988151&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val person = Person(&quot;홍길동&quot;, 100)
println(&quot;이름 : ${person.name}&quot;)&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;pre id=&quot;code_1748181093934&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val withoutIndent = 
   &quot;&quot;&quot;
      ABC
      123
      234
  &quot;&quot;&quot;.trimIndent() // trimIndent는 앞에 공백 제거용
println(withoutIndent)&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;pre id=&quot;code_1748181185062&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val str = &quot;ABCDE&quot;
val ch = str[1] // java에서는 charAt&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 코틀린에서 연산자를 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단항 연산자 / 산술 연산자&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java와 동일&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, Java와 다르게 객체를 비교할 때 비교 연산자를 사용하면 자동으로 compareTo를 호출&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;비교 연산자와 동등성, 동일성&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일성 Identity: 완전히 동일한 객체인가. 즉, 주소가 같은가&lt;/li&gt;
&lt;li&gt;동등성 Equality: 두 객체의 값이 같은가&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; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;동일성&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;동등성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Java&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;==&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;equals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Kotlin&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;===&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;== (간접적으로 equals를 호출)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java와 동일&lt;/li&gt;
&lt;li&gt;Java처럼 Lazy 연산을 수행 (if (fun1() || fun2()) 일 때 fun1이 true면 fun2는 실행X)&lt;/li&gt;
&lt;/ul&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;in / !in : 컬렉션이나 범위에 포함되어 있다. 포함되어 있지 않다&lt;/li&gt;
&lt;li&gt;a..b: a부터 b까지의 범위 객체를 생성&lt;/li&gt;
&lt;li&gt;a[i]: a에서 특정 index i로 값을 가져온다&lt;/li&gt;
&lt;li&gt;a[i] = b: a의 특정 index i에 b를 넣는다&lt;/li&gt;
&lt;/ul&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;pre id=&quot;code_1748188685911&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val money1 = Money(1_000L)
val money2 = Money(2_000L)

println(money1 + money2) // 3000 -&amp;gt; toString이 구현되어 있기 때문에 가능&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 코틀린에서 제어문을 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;if문&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java에서 if-else는 Statement인데, Kotlin에서는 Expression이다.&lt;/li&gt;
&lt;li&gt;Statement: 프로그램의 문장, 하나의 값으로 도출되지 않는다&lt;/li&gt;
&lt;li&gt;Expression: 하나의 값으로 도출되는 문장 (ex. Java의 3항 연산자)&lt;/li&gt;
&lt;li&gt;Kotlin에서는 if-else를 expression으로 사용할 수 있기 때문에 3항 연산자가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748268859856&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun validateScoreIsNotNegative(score: Int) { // Unit(=void) 리턴 생략
    if (score &amp;lt; 0) {
        throw IllegalArgumentException(&quot;${score}는 0보다 작을 수 없습니다&quot;) // new 없음
    }
}

// kotlin에서 if-else는 expression이기 때문에 그대로 return이 가능하다
fun getPassOrFail(sroce: Int): String {
    return if (score &amp;gt;= 50) {
        &quot;P&quot;
    } else {
       &quot;F&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Expression과 Statement&lt;/h4&gt;
&lt;pre id=&quot;code_1748269734609&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// java
if (0 &amp;lt;= score &amp;amp;&amp;amp; score &amp;lt;= 100)

// kotlin
if (score in 0..100)
if (score !in 0..100)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;switch와 when&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin에서는 switch문이 없고 when문이 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1748273606527&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// kotlin에서는 swith가 없고 when을 사용한다.
fun getGradeWithSwitch(score: Int): String {
    return when (score) { // when도 expression
        in 90..99 -&amp;gt; &quot;A&quot;
        in 80..89 -&amp;gt; &quot;B&quot;
        in 70..79 -&amp;gt; &quot;C&quot;
        else -&amp;gt; &quot;D&quot;
    }
}

// 조건부에는 어떤 expression도 들어갈 수 있다.
fun startsWithA(obj: Any): Boolean {
  return when (obj) {
    is String -&amp;gt; obj.startsWith(&quot;A&quot;)
    else -&amp;gt; false
  }
}

// 조건부에는 여러개의 조건을 동시에 검사할 수 있다.
fun judgeNumber(number: Int) {
  when (number) {
    1, 0, -1 -&amp;gt; println(&quot;정답입니다.&quot;)
    else -&amp;gt; println(&quot;오답입니다.&quot;)
  }
}

// when에 들어오는 값은 없을 수도 있다. early return처럼 동작
fun judgeNumber2(number: Int) {
  when {
    number == 0 -&amp;gt; println(&quot;주어진 숫자는 0입니다&quot;)
    number % 2 == 0 -&amp;gt; println(&quot;주어진 숫자는 짝수입니다&quot;)
    else -&amp;gt; println(&quot;주어진 숫자는 홀수입니다&quot;)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 코틀린에서 반복문을 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;for-each문&lt;/h4&gt;
&lt;pre id=&quot;code_1748274199325&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
  val numbers = listOf(1L, 2L, 3L)
  for (number in numbers) { // java에서는 :, kotlin에서는 in
    println(number)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;전통적인 for문 &amp;amp; Progression과 Range&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;downTo, step도 함수 (중위 호출 함수)&lt;/li&gt;
&lt;li&gt;Kotlin에서 전통적인 for문은 등차수열을 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748274226312&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1, 2, 3
fun main() {
  for (i in 1..3) {
    println(i)
  }
}

// 3, 2, 1
fun main() {
  for (i in 3 downTo 1) {
    println(i)
  }
}

// 두 칸씩 올라가는 경우
fun main() {
  for (i in 1..5 step 2) {
    println(i)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;while문&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;while, do-while문은 모두 java와 동일&lt;/p&gt;
&lt;pre id=&quot;code_1748274506125&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var i = 1
while (i &amp;lt;= 3) {
  println(i)
  i++
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. Kotlin에서 예외를 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;try catch finally 구문&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;java와 문법적으로 동일&lt;/p&gt;
&lt;pre id=&quot;code_1748274691265&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun parseIntOrThrow(str: String): Int {
  try {
    return str.toInt()
  } catch (e: NumberFormatException) {
    throw IllegalArgumentException(&quot;주어진 ${str}는 숫자가 아닙니다&quot;)
  }
}

// 실패 시 null 반환
fun parseIntOrThrowV2(str: String): Int {
  return try { // try-catch 문도 expression으로 취급
    str.toInt()
  } catch (e: NumberFormatException) {
    return null
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Checked Exception과 Unchecked Exception&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서는 IOException이 던져질 수 있다는 것을 표시해줘야 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin에서는 아래에서 빨간줄 없음 -&amp;gt; Kotlin에서는 checked exception과 unchecked exception을 구분하지 않는다. 모두 Unchecked exception이다.&lt;/p&gt;
&lt;pre id=&quot;code_1748275004579&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class FilePrinter {
  fun readFile() {
    val currentFile = File(&quot;.&quot;)
    val file = File(currentFile.absolutePath + &quot;/a.txt&quot;)
    val reader = BufferReader(FileReader(file))
    println(reader.readLine())
    reader.close()
  }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;try with resources&lt;/h4&gt;
&lt;pre id=&quot;code_1748275184054&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Java
public void readFile(String path) throws IOException {
  try (BufferedReader reader = new BufferedReader(new FileReader(path))) { // 자동으로 외부자원 닫아줌
    System.out.println(reader.readLine())
  }
}

// Kotlin
// try with resources 대신 use라는 inline 확장 함수 사용
fun readFile(path: String) {
  BufferedReader(FileReader(path)).use { reader -&amp;gt;
    println(reader.readLin())
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 코틀린에서 함수를 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;함수 선언 문법&lt;/h4&gt;
&lt;pre id=&quot;code_1748357321112&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Step1 두 정수를 받아 더 큰 정수를 반환하는 예제
fun max(a: Int, b: Int): Int {
  return if (a &amp;gt; b) {
    a
  } else {
    b
  }
}

// Step2 함수가 하나의 결과값이면 block 대신 = 사용 가능
fun max(a: Int, b: Int): Int =
  if (a &amp;gt; b) {
    a
  } else {
    b
  }
  
// Step3 함수를 쓸 때 중괄호 대신 =을 썼기 때문에 타입 추론이 가능해 return 타입 생략 가능
// block {}을 사용하는 경우에는 반환 타입이 Unit이 아니면 명시적으로 작성해주어야 함
fun max(a: Int, b: Int) = if (a &amp;gt; b) a else b&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;파일 최상단에 있을 수도 있고&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;default parameter&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;밖에서 파라미터를 넣어주지 않으면 기본값을 사용하도록 해준다. (Java의 오버로드 기능을 대체. Kotlin에서 오버로드 있음)&lt;/p&gt;
&lt;pre id=&quot;code_1748358565108&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 주어진 문자열을 N번 출력
fun repeat(
  str: String,
  num: Int = 3,
  useNewLine: Boolean = true
) {
  for (i in 1..num) {
    if (useNewLine) {
      println(str)
    } else {
      println(str)
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;named argument (parameter)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점: builder를 직접 만들지 않고 builder의 장점을 가지게 된다. 어떤 param에 어떤 값을 넣어주는지 명확해짐&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: Kotlin에서 Java 함수를 가져다 사용할 대는 named argument를 사용할 수 없다. (JVM상에서 Java가 바이트 코드로 변환됐을 때 parameter 이름을 보존하고 있지 않기 때문)&lt;/p&gt;
&lt;pre id=&quot;code_1748358810226&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
  // 셋 모두 같은 결과
  repeat(&quot;Hello World&quot;)
  repeat(&quot;Hello World&quot;, 3, false)
  repeat(&quot;Hello World&quot;, useNewLine = false) // named argument
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;같은 타입의 여러 파라미터 받기 (가변인자)&lt;/h4&gt;
&lt;pre id=&quot;code_1748359954581&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 문자열을 N개 받아 출력
fun printAll(vararg strings: String) { // vararg
  for (str in strings) {
    println(str)
  }
}

fun main() {
  // string 여러개 넣어줄 때
  printAll(&quot;A&quot;, &quot;B&quot;)
  
  // 배열로 넣어줄 때 * (spread 연산자: 배열 안에 있는 것들을 꺼내줌)
  var array = arrayOf(&quot;A&quot;, &quot;B&quot;)
  printAll(*array)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 코틀린에서 클래스를 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;클래스와 프로퍼티&lt;/h4&gt;
&lt;pre id=&quot;code_1748360466026&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// public이 default이기 때문에 생략 가능
// constructor에서 타입 선언되어 있기 때문에 필드에서 타입 생략 가능
// 코틀린에서는 필드만 만들면 getter, setter를 자동으로 만들어 줌 (프로퍼티 = getter + setter)
class Person constructor(name: String, age: Int) {
 val name
 var age
}

// constructor 생략 가능
class Person (name: String, age: Int) {
 val name
 var age
}

// 생성자에서 프로퍼티를 만들어 줄 수 있음
// body는 아무것도 없기 때문에 {} 생략 가능
class Person(
  val name: String, 
  var age: Int
  )
  
// 코틀린에선 .필드를 통해 getter와 setter를 바로 호출한다
// java 클래스에 대해서도 .필드로 getter, setter를 사용
fun main() {
  val person = Person(&quot;홍길동&quot;, 100)
  println(person.name)
  person.age = 10
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;생성자와 init&lt;/h4&gt;
&lt;pre id=&quot;code_1748959705472&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Person(
  val name: String,
  val age: Int
) {
  // init 블록: 생성자가 호출되는 시점에 호출
  init {
    if (age &amp;lt;= 0) {
      throw IllegalArgumentException(&quot;ㅇㅇㅇ&quot;)
    }
    println(&quot;초기화 블록&quot;)
  }
  
  // 주생성자(primary constructor)는 반드시 존재해야 한다. 단, 파라미터가 하나도 없다면 생략 가능
  // 2번째 이후의 부생성자는 아래와 같이 만들어짐
  // 부생성자는 최종적으로 주생성자를 this로 호출해야 한다.
  constructor(name: String): this(name, 1) {
    println(&quot;첫 번째 부생성자&quot;)
  }
  
  // 부생성자는 body를 가질 수 있다.
  constructor(): this(&quot;홍길동&quot;, 1) {
    println(&quot;두 번째 부생성자&quot;)
  }
}&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;pre id=&quot;code_1748960074439&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 두 번째 부생성자로 객체를 생성한 경우 출력
초기화 블록
첫 번째 부생성자
두 번째 부생성자&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;다만, 추가 constructor를 만드는 것보다 default parameter를 kotlin에서는 권장한다.&lt;/p&gt;
&lt;pre id=&quot;code_1748960677858&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Person(
  val name: String = &quot;홍길동&quot;,
  var age: Int = 1,
)&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;Converting과 같은 경우 부생성자를 사용할 수 있지만, 그보다는 정적 팩토리 메소드를 추천한다.&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;커스텀 getter, setter&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 getter&lt;/p&gt;
&lt;pre id=&quot;code_1748961286579&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Person(
  val name: String,
  val age: Int
) {
  init {
    if (age &amp;lt;= 0) {
      throw IllegalArgumentException(&quot;ㅇㅇㅇ&quot;)
    }
    println(&quot;초기화 블록&quot;)
  }
  
  fun isAdult(): Boolean {
    return this.age &amp;gt;= 20
  }
  
  // custom getter: 위 함수를 아래와 같이 만들 수 있다.
  val isAdult: Boolean
    get() = this.age &amp;gt;= 20
  
  // 이름을 대문자로 바꿔서 반환
  val name: String = name // 주생성자에서 받은 name을 불변 프로퍼티 name에 바로 대입
    // name으로 하면 무한루프 발생 -&amp;gt; backing field 자기 자신을 가리키는 예약어
    get() = field.uppercase()
    
  // backing field 사용해서 custom getter 만들일 거의 없음
  val upperCaseName: String
    get() = this.name.uppercase()
}&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;커스텀 setter&lt;/p&gt;
&lt;pre id=&quot;code_1748963414730&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Person(
  val name: String,
  val age: Int
) {
  init {
    if (age &amp;lt;= 0) {
      throw IllegalArgumentException(&quot;ㅇㅇㅇ&quot;)
    }
    println(&quot;초기화 블록&quot;)
  }
  
  var name = name
    set(value) {
      field = value.uppercase()
    }
  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 코틀린에서 상속을 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;추상 클래스&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java, Kotlin 모두 추상 클래스는 인스턴스화 할 수 없다.&lt;/p&gt;
&lt;pre id=&quot;code_1749275000638&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 추상클래스
abstract class Animal(
  protected val species: String,
  // 프로퍼티를 override할 땐 추상 프로퍼티가 아니라면 open 붙여줘야 함
  protected open val legCount: Int,
) {
  abstract fun move()
}

// 상속받는다는 의미의 ':' (타입표시도 동일하지만 상속은 보통 앞에 띄어쓰기를 해줌)
class Cat(
  species: String
) : Animal(species, 4) { // 상위 클래스의 생성자에 값을 넣어줌
  
  // java는 annotation이었지만 Kotlin은 키워드
  override fun move() {
    println(&quot;고양이가 걸어간다&quot;)
  }
}

class Penguin(
  species: String
) : Animal(species, 2) {
  
  private val wingCount: Int = 2
  
  override fun move() {
    println(&quot;펭귄이 움직인다&quot;)
  }
  
  // getter를 Override하기
  override val legCount: Int
    // super: 상위 클래스에 접근
    get() = super.legCount + this.wingCount

}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인터페이스&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java. Kotlin 모두 인터페이스를 인스턴스화 할 수 없다.&lt;/li&gt;
&lt;li&gt;Kotlin에서는 backing field가 없는 프로퍼티를 Interface에 만들 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1749275832038&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Flyable {
  
  // default 키워드 없이 메소드 구현이 가능
  fun act() {
    println(&quot;파닥파닥&quot;)
  }
  
}

interface Swimable {

  // Kotlin에서는 backing field가 없는 프로퍼티를 Interface에 만들 수 있다.
  val swimAbility: Int
  
  fun act() {
    println(&quot;어푸어푸&quot;)
  }
  
}

class Penguin(
  species: String
) : Animal(species, 2), Swimable, Flyable { // interface 구현도 ':'를 사용
  
  private val wingCount: Int = 2
  
  override fun move() {
    println(&quot;펭귄이 움직인다&quot;)
  }
  
  override val legCount: Int
    get() = super.legCount + this.wingCount

  override fun act() {
    // 중복되는 인터페이스를 특정할 때 &quot;super&amp;lt;타입&amp;gt;.함수&quot;를 사용
    super&amp;lt;Swimable&amp;gt;.act()
    super&amp;lt;Flyable&amp;gt;.act()
  }
  
  override val swimAbility: Int
    get() = 3
}&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;클래스를 상속할 때 주의할 점&lt;/h4&gt;
&lt;pre id=&quot;code_1749276326437&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;open class Base(
  open val number: Int = 100
) {
  init {
    println(&quot;Base Class&quot;)
    println(number)
  }
}

class Derived(
  override val number: Int
) : Base(number) {
  init {
    println(&quot;Derived Class&quot;)
  }
}&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;Derived(300) 하면?&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Base Class&lt;br /&gt;0&lt;br /&gt;Derived Class&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;왜 0일까?&lt;/li&gt;
&lt;li&gt;Intellij 경고: Acessing non-final property 'number' in constructor&lt;/li&gt;
&lt;li&gt;Derived를 인스턴스화하면 상위클래스인 Base의 생성자를 실행하는데, 이때 하위클래스인 Derived의 number를 호출하지만 초기화가 되어있지 않음&lt;/li&gt;
&lt;li&gt;즉, 상위 클래스를 설계할 때 생성자 또는 초기화 블록에 사용되는 프로퍼티에는 open을 피해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상속 관련 지시어 정리&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;final: override를 할 수 없게 한다. default로 보이지 않게 존재한다.&lt;/li&gt;
&lt;li&gt;open: override를 열어 준다.&lt;/li&gt;
&lt;li&gt;abstract: 반드시 override 해야 한다.&lt;/li&gt;
&lt;li&gt;override: 상위 타입을 오버라이드 하고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11. 코틀린에서 접근 제어를 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;자바와 코틀린의 가시성 제어&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java의 기본 접근 지시어는 default&lt;br /&gt;Kotlin의 기본 접근 지시어는 public&lt;/li&gt;
&lt;li&gt;Kotlin에서는 패키지라는 개념을 접근 제어에 사용하지 않기 때문에 모듈을 접근 제어하는 기능이 생겼다.&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 146px;&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;width: 11.5892%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 34.4574%; height: 17px;&quot;&gt;Java&lt;/td&gt;
&lt;td style=&quot;width: 11.0852%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; height: 17px;&quot;&gt;Kotlin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 11.5892%; height: 18px;&quot;&gt;public&lt;/td&gt;
&lt;td style=&quot;width: 34.4574%; height: 18px;&quot;&gt;모든 곳에서 접근 가능&lt;/td&gt;
&lt;td style=&quot;width: 11.0852%;&quot;&gt;public&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; height: 18px;&quot;&gt;모든 곳에서 접근 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 75px;&quot;&gt;
&lt;td style=&quot;width: 11.5892%; height: 75px;&quot;&gt;protected&lt;/td&gt;
&lt;td style=&quot;width: 34.4574%; height: 75px;&quot;&gt;같은 패키지 또는 하위 클래스에서만 접근 가능&lt;/td&gt;
&lt;td style=&quot;width: 11.0852%;&quot;&gt;protected&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; height: 75px;&quot;&gt;&lt;b&gt;선언된 클래스&lt;/b&gt; 또는 하위 클래스에서만 접근 가능&lt;br /&gt;-&amp;gt; Kotlin에서는 Java와 다르게 패키지를 namespace를 관리하기 위한 용도로만 사용. 가시성 제어에는 사용되지 않기 때문.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 11.5892%; height: 18px;&quot;&gt;default&lt;/td&gt;
&lt;td style=&quot;width: 34.4574%; height: 18px;&quot;&gt;같은 패키지에서만 접근 가능&lt;/td&gt;
&lt;td style=&quot;width: 11.0852%;&quot;&gt;internal&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; height: 18px;&quot;&gt;같은 모듈에서만 접근 가능&lt;br /&gt;-&amp;gt; 모듈: 한 번에 컴파일 되는 kotlin 코드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 11.5892%; height: 18px;&quot;&gt;private&lt;/td&gt;
&lt;td style=&quot;width: 34.4574%; height: 18px;&quot;&gt;선언된 클래스 내에서만 접근 가능&lt;/td&gt;
&lt;td style=&quot;width: 11.0852%;&quot;&gt;private&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;선언된 클래스 내에서만 접근 가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코틀린 파일의 접근 제어&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린은 .kt 파일에 변수, 함수, 클래스 여러개를 바로 만들 수 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 125px;&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;width: 100%; height: 17px; text-align: center;&quot; colspan=&quot;2&quot;&gt;&lt;b&gt;파일에서의 접근 제어&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;public&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;default값. 어디서든 접근할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 56px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 56px;&quot;&gt;protected&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 56px;&quot;&gt;파일(최상단)에는 사용 불가능&lt;br /&gt;-&amp;gt; protected가 선언된 클래스 또는 하위 클래스에서 사용되는 지시어이기 때문&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;internal&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;같은 모듈에서만 접근 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;private&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;같은 파일에서만 접근 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;다양한 구성요소의 접근 제어&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 125px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&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; text-align: center;&quot; colspan=&quot;2&quot;&gt;&lt;b&gt;클래스에서의 접근 제어&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;public&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;default값. 어디서든 접근할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 56px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 56px;&quot;&gt;protected&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 56px;&quot;&gt;선언된 클래스 또는 하위 클래스에서만 접근 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;internal&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;같은 모듈에서만 접근 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;private&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;선언된 클래스 내에서만 접근 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 125px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&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; text-align: center;&quot; colspan=&quot;2&quot;&gt;&lt;b&gt;생성자에서의 접근 제어&lt;br /&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;public&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;default값. 어디서든 접근할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 56px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 56px;&quot;&gt;protected&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 56px;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;선언된 클래스 또는 하위 클래스에서만 접근 가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;internal&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;같은 모듈에서만 접근 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;private&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;같은 파일에서만 접근 가능&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;생성자에 접근 지시어를 붙이려면 constructor를 써줘야 함&lt;br /&gt;ex) class Cat internal constructor (...)&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 125px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&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; text-align: center;&quot; colspan=&quot;2&quot;&gt;&lt;b&gt;프로퍼티에서의 접근 제어&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;public&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;default값. 어디서든 접근할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 56px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 56px;&quot;&gt;protected&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 56px;&quot;&gt;파일(최상단)에는 사용 불가능&lt;br /&gt;-&amp;gt; protected가 선언된 클래스 또는 하위 클래스에서 사용되는 지시어이기 때문&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;internal&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;같은 모듈에서만 접근 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;private&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;같은 파일에서만 접근 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Java와 Kotlin을 함께 사용할 경우 주의할 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Internal은 바이트 코드 상 public이 된다. 때문에 Java 코드에서는 Kotlin 모듈의 internal 코드를 가져올 수 있다.&lt;/li&gt;
&lt;li&gt;Kotlin의 protected와 java의 protected는 다르다. java는 같은 패키지의 Kotlin protected 멤버에 접근할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12. 코틀린에서 object 키워드를 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;static 함수와 변수&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;companion object&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;static과 유사한 역할. 클래스와 동행하는 유일한 오브젝트&lt;/li&gt;
&lt;li&gt;companion object도 하나의 객체로 간주. 따라서 이름을 붙일 수도 있고, interface를 구현할 수도 있다. (이름을 안쓴다면 'Companion' 이라는 이름이 생략된 것)&lt;/li&gt;
&lt;li&gt;companion object에 유틸성 함수를 넣어도 되지만, 최상단 파일을 활용하는 것을 추천.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1749280828388&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Person private constructor(
  var name: String,
  var age: Int,
) {

  // kotlin에는 static이 없음 -&amp;gt; companion object
  companion object Factory {
  
    // const가 없으면 런타임 시에 0이 할당되지만
    // const를 붙이면 컴파일 시에 0이 할당됨
    // 즉, 진짜 상수를 표현하고 기본타입과 string에만 붙여줄 수 있다.
    private const val MIN_AGE = 0
    fun newBaby(name: String): Person {
      return Person(name, MIN_AGE)
    }
    
    // Java에서 호출할 때 
    // Person.Companion.secondBaby(&quot;abc&quot;);
    // Person.secondBaby(&quot;abc&quot;); // JvmStatic 붙이면 다음과 같이 Java에서 호출할 수 있다.
    @JvmStatic
    fun secondBaby(name: String): Person {
      return Person(name, MIN_AGE + 1)
    }
  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;싱글톤&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin에서는 싱글톤 클래스를 만들기 위해 object만 붙여주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1749282316097&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {

  println(Singleton.a)
  Singleton.a += 10
  
}

object Singleton {
  var a: Int = 0
}&lt;/code&gt;&lt;/pre&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;pre id=&quot;code_1749284166072&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Movable이라는 interface가 있다고 할 때
fun main() {
  moveSomething(object : Movable {
    override fun move() {
      println(&quot;move&quot;)
    }
    
    override fun fly() {
      println(&quot;fly&quot;)
    }
  })
}

private fun moveSomething(movable: Movable) {
  movable.move()
  movable.fly()
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13. 코틀린에서 중첩 클래스를 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;중첩 클래스의 종류&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서?&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Static을 사용하는 중첩 클래스&lt;/b&gt;: 클래스 안에 static을 붙인 클래스&lt;/li&gt;
&lt;li&gt;Static을 사용하지 않는 중첩 클래스
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;내부 클래스&lt;/b&gt;: 클래스 안의 클래스&lt;/li&gt;
&lt;li&gt;지역 클래스: 메소드 안의 클래스&lt;/li&gt;
&lt;li&gt;익명 클래스: 일회성 클래스&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;코틀린의 중첩 클래스와 내부 클래스&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Static을 사용하는 중첩 클래스: 바깥 클래스와 연결안됨. OuterClass.this.field1 이렇게 불러올 수 없음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Static을 사용하지 않는 중첩 클래스 - 내부 클래스: 바깥 클래스와 연결. OuterClass.this.field1 이렇게 불러올 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; Static을 사용하는 내부 클래스를 사용하라&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;1. 내부 클래스는 숨겨진 외부 클래스 정보를 가지고 있어, 참조를 해지하지 못하는 경우 메모리 누수가 생길 수 있고, 이를 디버깅 하기 어려움&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;그래서 Kotlin에서는 이 규칙을 따르고 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1749474401904&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 그냥 내부 클래스 쓰면 연결되지 않은 중첩 클래스가 만들어짐
class JavaHouse(
  prviate val address: String,
  private val livingRoom: LivingRoom
) {
  class LivingRoom(
    private var area: Double,
  )
}

// inner를 붙이면 연결된 중첩 클래스가 만들어짐 (권장되지 않는 클래스 안의 클래스)
class House(
  prviate val address: String,
  private val livingRoom: LivingRoom
) {
  inner class LivingRoom(
    private var area: Double,
  ) {
    val address: String
      get() = this@House.address // 외부 클래스 접근
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14. 코틀린에서 다양한 클래스를 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Data Class&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계층간의 데이터를 전달하기 위한 DTO(Data Transfer Object)&lt;/p&gt;
&lt;pre id=&quot;code_1749475748607&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// data를 붙이면 자동으로 equals, hashCode, toString 을 만들어줌
data class PersonDto(
  val name: String,
  val age: Int,
)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Enum Class&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적인 클래스를 상속받을 수 없다. 인터페이스는 구현할 수 있으며, 각 코드가 싱글톤이다.&lt;/p&gt;
&lt;pre id=&quot;code_1749478885374&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enum class Country(
  private val code: String,
) {
  KOREA(&quot;KO&quot;),
  AMERICA(&quot;US&quot;)
  ;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Enum + When 을 함께 썼을 경우&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;컴파일러가 country의 모든 타입을 알고 있어 다른 타입에 대한 로직(else)을 작성하지 않아도 된다.&lt;/li&gt;
&lt;li&gt;Enum에 변화가 있으면 IDE단에서 warning을 준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1749480367555&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun handleCountry(country: Country) {
  when (country) {
    Country.KOREA -&amp;gt; TODO()
    Country.AMERICA -&amp;gt; TODO()
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Sealed Class, Sealed Interface&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상속이 가능하도록 추상클래스를 만들고 싶은데, 외부에서는 이 클래스를 상속받지 않았으면 좋겠을 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 하위 클래스를 봉인(seal)하자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;하위 클래스는 sealed class와 같은 패키지에 있어야 한다.&lt;/li&gt;
&lt;li&gt;Enum과 다른점: 클래스를 상속받을 수 있고 하위 클래스는 멀티 인스턴스가 가능하다. (&amp;lt;-&amp;gt; 싱글톤)&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1749481510867&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sealed class Car(
  val name: String,
  val price: Long
)

class Avante : Car(&quot;아반떼&quot;, 1_000L)
class Sonata : Car(&quot;쏘나타&quot;, 1_000L)

// enum과 유사해서 when과 함께 주로 사용된다.
private fun handleCar(car: Car) {
  when (car) {
    is Avante -&amp;gt; TODO()
    is Sonata -&amp;gt; TODO()
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;15. 코틀린에서 배열과 컬렉션을 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;배열&lt;/h4&gt;
&lt;pre id=&quot;code_1750080367841&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
  val array = arrayOf(100, 200)
  
  for (i in array.indices) {
    println(&quot;${i} ${array[i]}&quot;)
  }
  
  for ((idx, value) in array.withIndex()) {
    println(&quot;$idx $value&quot;)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코틀린에서의 Collection - List, Set, Map&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가변(Mutable) 컬렉션: 컬렉션에 element를 추가, 삭제할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MutableList, MutableSet, MutableMap&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;불변 컬렉션: 컬렉션에 element를 추가, 삭제할 수 없다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;List, Set, Map&lt;/li&gt;
&lt;li&gt;불변 컬렉션이라 하더라도 reference type인 element의 필드는 바꿀 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1750081805321&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// list
fun main() {
  val numbers = listOf(100, 200)
  val emptyList = emptyList&amp;lt;Int&amp;gt;() // 타입추론이 가능하면 &amp;lt;&amp;gt;를 생략 가능
 
  println(numbers[0])
  
  for (number in numbers) {
    println(number)
  }
  
  for ((idx, number) in numbers.withIndex()) {
    println(&quot;$index $number&quot;)
  }
  
  // 가변List
  // 기본 구현체는 arrayList
  val mutableNumbers = mutableListOf(100, 200)
  mutableNumbers.add(300)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1750082163692&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Set
fun main() {
  val numbers = setOf(100, 200)
  
  for (number in numbers) {
    println(number)
  }
  
  for ((index, number) in numbers.withIndex()) {
    println(&quot;$index $number&quot;)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1750082430007&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Map
fun main() {
  val map = mutableMapOf&amp;lt;Int, String&amp;gt;()
  map[1] = &quot;MONDAY&quot;
  map.put(2, &quot;TUESDAY&quot;)
  
  mapOf(1 to &quot;MONDAY&quot;, 2 to &quot;TUESDAY&quot;)
  
  for (key in map.keys) {
    println(key)
    println(map[key])
  }
  
  for ((key, value) in map.entries) {
    println(key)
    println(value)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;컬렉션의 null 가능성, Java와 함께 사용하기&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;List&amp;lt;Int?&amp;gt;: 리스트에 null이 들어갈 수 있지만, 리스트는 절대 null이 아님&lt;/li&gt;
&lt;li&gt;List&amp;lt;Int&amp;gt;?: 리스트에는 null이 들어갈 수 없지만, 리스트는 null일 수 있음&lt;/li&gt;
&lt;li&gt;List&amp;lt;Int?&amp;gt;?: 리스트에 null이 들어갈 수도 있고, 리스트가 null일 수도 있음&lt;/li&gt;
&lt;li&gt;Kotlin쪽의 컬렉션이 Java에서 호출되면 컬렉션 내용이 변할 수 있음을 감안해야 한다.&lt;/li&gt;
&lt;li&gt;혹은, 코틀린 쪽에서 Java에도 있는 기능인 Collections.unmodifiableXXX()를 활용하면 변경 자체를 막을 수는 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;16. 코틀린에서 다양한 함수를 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;확장함수&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java로 만들어진 라이브러리를 유지보수, 확장할 때 Kotlin 코드를 덧붙이고 싶은 니즈&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 어떤 클래스안에 있는 메소드처럼 호출할 수 있지만, 함수는 밖에 만들 수 있게 하자&lt;/p&gt;
&lt;pre id=&quot;code_1756910481825&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// String 클래스를 확장
fun String.lastChar(): Char {
  return this[this.length - 1]
}

// 사용할 때는 원래 String에 있는 멤버함수 처럼 사용할 수 있다.
val str: String = &quot;ABC&quot;
str.lastChar()&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;확장함수는 클래스에 있는 private 또는 protected 멤버를 가져올 수 없다.&lt;/li&gt;
&lt;li&gt;멤버함수와 확장함수의 시그니처가 같다면 멤버 함수를 우선적으로 호출한다.&lt;/li&gt;
&lt;li&gt;확장함수는 현재 타입을 기준으로 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;infix 함수&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수.함수이름 대신 '변수 함수이름 argument' 이렇게 호출해 주는 것&lt;/p&gt;
&lt;pre id=&quot;code_1756910916328&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 중위함수
infix fun Int.add2(other: Int): Int {
  return this + other
}

// 중위함수 호출 시 
3 add2 4&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;inline 함수&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수가 호출되는 대신, 함수를 호출한 지점에 함수 본문을 그대로 복사하는 것&lt;/p&gt;
&lt;pre id=&quot;code_1756910982591&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;inline fun Int.add(other: Int): Int {
  return this + other
}&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;함수를 파라미터로 전달할 때에 오버헤드를 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;하지만 inline 함수의 사용은 성능 측정과 함께 신중하게 사용되어야 한다.&lt;/li&gt;
&lt;/ul&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;pre id=&quot;code_1756911089736&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun createPerson(..): Person {
  fun validateName(name: String, fieldName: String) {
    if (...)
  }
  
  validateName(&quot;name&quot;, &quot;fieldname&quot;)
  
  return Person()
}&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;함수로 추출하면 좋을 것 같은데, 이 함수를 지금 함수 내에서만 사용하고 싶을 때&lt;/li&gt;
&lt;li&gt;depth가 깊어지기도 하고, 코드가 그렇게 깔끔하지는 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;17. 코틀린에서 람다를 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코틀린에서의 람다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서는 Java와 다르게 함수가 그 자체로 값이 될 수 있다. 변수에 할당할수도, 파라미터로 넘길 수도 있다&lt;/p&gt;
&lt;pre id=&quot;code_1756911733391&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 함수 이름이 없음 - 람다 (타입은 생략 가능)
val isApple: (Fruit) -&amp;gt; Boolean = fun(fruit: Fruit): Boolean {
  return fruit.name == &quot;사과&quot;
}

val isApple2 = { fruit: Fruit -&amp;gt; fruit.name == &quot;사과&quot; }

// 호출하는 방법
isApple(fruits[0])
isApple.invoke(fruit[0])

// parameter로 넘겨주는 경우
filterFruits(fruits, { fruit: Fruit -&amp;gt; fruit.name == &quot;사과&quot; })
// 가장 마지막 파라미터로 람다가 들어가는 경우 밖으로 빼줄 수도 있음
filterFruits(fruits) { fruit: Fruit -&amp;gt; fruit.name == &quot;사과&quot; }
// 타입 생략 가능
filterFruits(fruits) { fruit -&amp;gt; fruit.name == &quot;사과&quot; }
// parameter가 1개인 경우 it으로 사용 가능
filterFruits(fruits) { it.name == &quot;사과&quot; }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Closure&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서&lt;/p&gt;
&lt;pre id=&quot;code_1756912240170&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String targetFruitName = &quot;바나나&quot;;
targetFruitName = &quot;수박&quot;;
filterFruits(fruits, (fruit) -&amp;gt; targetFruitName.equals(fruit.getName()));

// Variable used in lambda expression should be final or effectively final 오류&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;Kotlin에서&lt;/p&gt;
&lt;pre id=&quot;code_1756912289066&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var targetFruitName = &quot;바나나&quot;
targetFruitName = &quot;수박&quot;
filterFruits(fruits) { it.name == targetFruitname }&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;코틀린은 람다가 시작하는 지점에 참조하고 있는 변수들을 모두 포획하여 그 정보를 가지고 있다.&lt;/li&gt;
&lt;li&gt;이러한 데이터 구조를 Closure라고 부른다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;다시 try with resources&lt;/h4&gt;
&lt;pre id=&quot;code_1756912383237&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun readFile(path: String) {
  BufferedReader(FileReader(path)).use { reader -&amp;gt;
    println(reader.readLine())
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;18. 코틀린에서 컬렉션을 함수형으로 다루는 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;필터와 맵&lt;/h4&gt;
&lt;pre id=&quot;code_1756912677334&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val apples = fruits.filter { fruit -&amp;gt; fruit.name == &quot;사과&quot; }
val apples = fruits.filterIndexed { idx, fruit -&amp;gt;
  println(idx)
  fruit.name == &quot;사과&quot;
}
val applePrices = fruits.filter { fruit -&amp;gt; fruit.name == &quot;사과&quot; }
  .map { fruit -&amp;gt; fruit.price }
val applePrices = fruits.filter { fruit -&amp;gt; fruit.name == &quot;사과&quot; }
  .mapIndexed { idx, fruit -&amp;gt;
    println(idx)
    fruit.currentPrice
  }
val values = fruits.filter { fruit -&amp;gt; fruit.name == &quot;사과&quot; }
  .mapNotNull { fruit -&amp;gt; fruit.nullOrValue() }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;다양한 컬렉션 처리 기능&lt;/h4&gt;
&lt;pre id=&quot;code_1756912774182&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 조건 모두 만족시 true
val isAllApple = fruits.all { fruit -&amp;gt; fruit.name == &quot;사과&quot; }
// 조건을 모두 불만족시 true
val isAllApple = fruits.none { fruit -&amp;gt; fruit.name == &quot;사과&quot; }
// 조건을 하나라도 만족하면 true
val isAllApple = fruits.any { fruit -&amp;gt; fruit.name == &quot;사과&quot; }

fruits.count()
fruits.sortedBy { fruit -&amp;gt; fruit.currentPrice }
fruits.sortedByDescending { fruit -&amp;gt; fruit.currentPrice }
fruits.distinctBy { fruit -&amp;gt; fruit.name }.map { fruit -&amp;gt; fruit.name }

fruits.first() // 첫번째 값을 가져오는데 null이면 안됨 (exception)
fruits.firstOrNull() // 첫번째 값을 가져오는데 없으면 null
fruits.last()
fruits.lastOrNull()&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;List를 Map으로&lt;/h4&gt;
&lt;pre id=&quot;code_1756913132729&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 과일이름 -&amp;gt; List&amp;lt;과일&amp;gt;
val map: Map&amp;lt;String, List&amp;lt;Fruit&amp;gt;&amp;gt; = fruits.groupBy { fruit -&amp;gt; fruit.name }
// id -&amp;gt; 과일
val map: Map&amp;lt;Long, Fruit&amp;gt; = fruits.associatedBy { fruit -&amp;gt; fruit.id }
// 과일이름 -&amp;gt; List&amp;lt;출고가&amp;gt;
val map: Map&amp;lt;String, List&amp;lt;Long&amp;gt;&amp;gt; = fruits
  .groupBy({ fruit -&amp;gt; fruit.name }, { fruit -&amp;gt; fruit.factoryPrice })
// id -&amp;gt; 출고가
val map: Map&amp;lt;Long, Long&amp;gt; = fruits
  .associateBy({ fruit -&amp;gt; fruit.id }, { fruit -&amp;gt; fruit.factoryPrice })&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;중첩된 컬렉션 처리&lt;/h4&gt;
&lt;pre id=&quot;code_1756913212408&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fruitsInList.flatMap { list -&amp;gt; 
  list.filter { fruit -&amp;gt; fruit.factoryPrice == fruit.currentPrice }
}
// List&amp;lt;List&amp;lt;Fruit&amp;gt;&amp;gt; 을 List&amp;lt;Fruit&amp;gt;로 변경
fruitsInList.flatten()&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;19. 코틀린의 이모저모&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Type Alias와 as import&lt;/h4&gt;
&lt;pre id=&quot;code_1757002673998&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;typealias FruitFilter = (Fruit) -&amp;gt; Boolean
fun filterFruits(fruits: List&amp;lt;Fruit&amp;gt;, filter: FruitFilter) {}

data class UltraSuperGuardianTribe(val name: String)
typealias USGTMap = Map&amp;lt;String, UltraSuperGuardianTribe&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1757002751205&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 다른 패키지의 같은 이름 함수를 동시에 가져오고 싶은 경우
import com.a.printlnHelloWorld as aPrintlnHelloWorld
import com.b.printlnHelloWorld as bPrintlnHelloWorld

fun main() {
  aPrintHelloWorld()
  bPrintHelloWorld()
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구조분해와 componentN 함수&lt;/h4&gt;
&lt;pre id=&quot;code_1757002971886&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 구조분해: 복합적인 값을 분해하여 여러 변수를 한 번에 초기화하는 것
// data class는 componentN을 자동으로 만들어주기 때문에 별도 설정없이 이렇게 사용이 가능
val person = Person(&quot;가나다&quot;, 100)
val (name, age) = person

// 위와 같은 코드
val person = Person(&quot;가나다&quot;, 100)
val name = person.component1()
val age = person.component2()

// data class가 아닌데 구조분해를 사용하고 싶은 경우: componentN을 구현
class Person(
  val name: String,
  val age: Int
) {
  operator fun component1(): String {
    return this.name
  }
  
  operator fun component2(): Int {
    return this.age
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Jump와 Label&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;return: 기본적으로 가장 가까운 enclosing function 또는 익명함수로 값이 반환된다&lt;/li&gt;
&lt;li&gt;break: 가장 가까운 루프가 제거된다&lt;/li&gt;
&lt;li&gt;continue: 가장 가까운 루프를 다음 step으로 보낸다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1757003377604&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// forEach문에서는 continue, break를 쓸 수 없다.
// break를 꼭 쓰고 싶다면 다음과 같이 작성해야 한다.
run {
  numbers.forEach { number -&amp;gt;
    if (number == 2) {
      return@run
    }
  }
}

// continue를 꼭 쓰고 싶다면 다음과 같이 작성해야 한다.
number.forEach { number -&amp;gt;
  if (number == 2) {
    return@forEach
  }
}&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;Label: 특정 expression에서 라벨이름@을 붙여 하나의 라벨로 간주하고 break, continue, return 등을 사용하는 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1757003607595&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// break문이 가장 가까운 for문이 아닌 loop가 붙은 가장 상위의 for문을 멈춤
loop@ for (i in 1..100) {
  for (j in 1..100) {
    if (j == 2) {
      break@loop
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;TakeIf와 TakeUnless&lt;/h4&gt;
&lt;pre id=&quot;code_1757003824674&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun getNumberOrNull(): Int? {
  return if (number &amp;lt;= 0) {
    null
  } else {
    number
  }
}

// takeIf: 주어진 조건을 만족하면 그 값이, 그렇지 않으면 null이 반환된다.
fun getNumberOrNull(): Int? {
  return number.takeIf { it &amp;gt; 0 }
}

// takeUnless: 주어진 조건을 만족하지 않으면 그 값이 그렇지 않으면 null이 반환된다.
fun getNumberOrNull(): Int? {
  return number.takeUnless { it &amp;lt;= 0 }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;20. 코틀린의 scope function&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;scope function이란 무엇인가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scope function: 람다를 사용해 일시적인 영역을 형성하는 함수. 코드를 더 간결하게 만들거나 emthos chaning에 활용하는 함수를 scope function이라고 한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757004379913&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// let: 확장함수. 람다를 받아, 람다 결과를 반환한다.
fun printPerson(person: Person?) {
  person?.let {
    println(it.name)
    println(it.age)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;scope function의 분류&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;let, run, also, apply, with&lt;/p&gt;
&lt;pre id=&quot;code_1757004627175&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 결과: age, it 사용
person.let { it.age }
// 결과: age, this 사용
person.run { this.age }
// 결과: person, it 사용
person.also { it.age }
// 결과: person, this 사용
person.apply { this.age }

// this: 생략이 가능한 대신, 다른 이름을 붙일 수 없다.
person.run { age }
// it: 생략이 불가능한 대신, 다른 이름을 붙일 수 있다.
person.let { p -&amp;gt; p.age }

// with: this 사용
with(person) {
  println(name)
  println(this.age)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;언제 어떤 scope function을 사용해야 할까&lt;/h4&gt;
&lt;pre id=&quot;code_1757004923997&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. let: 하나 이상의 함수를 call chain 결과로 호출 할 때
val strings = listOf(&quot;a&quot;, &quot;b&quot;)
strings.map { it.length }
  .filter { it &amp;gt; 3 }
  .let(::println)
  
// 2. let: non-null 값에 대해서만 code block을 실행시킬 때
val length = str?.let {
  println(it.uppercase())
  it.length
}

// 3. let: 일회성으로 제한된 영역에 지역 변수를 만들 때
val numbers = listOf(&quot;a&quot;, &quot;b&quot;)
val modifiedFirstItem = numbers.first()
  .let { firstItem -&amp;gt; if (firstItem.length &amp;gt;= 5) firstItem else '.' }.uppercase()&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1757004973599&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// run: 객체 초기화와 반환 값의 계산을 동시에 해야 할 때
val person = Person(&quot;a&quot;, 100).run(personRepository::save)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1757005142531&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// apply: Test Fixture를 만들 때
fun createPerson(name: String, age: Int, hobby: String): Person {
  return Person(
    name = name,
    age = age,
  ).apply {
    this.hobby = hobby
  }
}

// apply: call chain을 수정할 때
person.apply { this.growOld() }.let { println(it) }&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1757005192671&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// also: 객체를 수정하는 로직이 call chain 중간에 필요할 때
mutableListOf(&quot;a&quot;, &quot;b&quot;)
  .also { println(it) }
  .add(&quot;four&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1757005243074&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// with: 특정 객체를 다른 객체로 변환해야 하는데, 모듈 간의 의존성에 의해 정적 팩토리 혹은 toClass 함수를 만들기 어려울 때
return with(person) {
  PersonDto(
    name = name,
    age = age,
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;</description>
      <category>Java &amp;amp; Spring</category>
      <category>Kotlin</category>
      <author>심심231</author>
      <guid isPermaLink="true">https://simsim231.tistory.com/298</guid>
      <comments>https://simsim231.tistory.com/298#entry298comment</comments>
      <pubDate>Fri, 5 Sep 2025 02:01:28 +0900</pubDate>
    </item>
    <item>
      <title>DB Connection Pool과 관련 아티클 (Dead Lock, HikariCP 설정 값으로 최적화하기)</title>
      <link>https://simsim231.tistory.com/297</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;DB Connection Pool&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEu9zZ/btsMJMAEqKX/e3ZdMHvWUxE0WmPPqleuMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEu9zZ/btsMJMAEqKX/e3ZdMHvWUxE0WmPPqleuMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEu9zZ/btsMJMAEqKX/e3ZdMHvWUxE0WmPPqleuMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEu9zZ%2FbtsMJMAEqKX%2Fe3ZdMHvWUxE0WmPPqleuMK%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;594&quot; height=&quot;187&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 DB Connection을 맺는데는 비용이 들기 때문에 이를 효율적으로 관리하기 위해 Pool을 만들어놓고 미리 Connection을 맺어놓는다. 그리고 필요할때마다 꺼내서 쓰는데 이를&lt;b&gt; DB connection Pool(DBCP)&lt;/b&gt;이라고 한다.&amp;nbsp;&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;1280&quot; data-origin-height=&quot;710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caJEaR/btsMJaoPeTO/vUrAkqeBSsCWlPM8Bp0p70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caJEaR/btsMJaoPeTO/vUrAkqeBSsCWlPM8Bp0p70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caJEaR/btsMJaoPeTO/vUrAkqeBSsCWlPM8Bp0p70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaJEaR%2FbtsMJaoPeTO%2FvUrAkqeBSsCWlPM8Bp0p70%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;416&quot; height=&quot;231&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;710&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서는 2.0 버전부터 기본적으로 Connection pooling을 &lt;b&gt;HikariCP&lt;/b&gt;로 제공한다. 그 전에는 Tomcat JDBC Connection Pool를 Default로 사용하였다.&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;(HikariCP가 성능이 월등히 좋다고 함. Git에 benchmark 돌렸다고 표도 있다)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;HikariCP 깃헙 주소&lt;/b&gt;&lt;br /&gt;https://github.com/brettwooldridge/HikariCP&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1079&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TYJbO/btsMLMVxZ1T/sNLi41w7cj3EiiKKg9UmP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TYJbO/btsMLMVxZ1T/sNLi41w7cj3EiiKKg9UmP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TYJbO/btsMLMVxZ1T/sNLi41w7cj3EiiKKg9UmP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTYJbO%2FbtsMLMVxZ1T%2FsNLi41w7cj3EiiKKg9UmP1%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;676&quot; height=&quot;282&quot; data-origin-width=&quot;1079&quot; data-origin-height=&quot;450&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;blockquote data-ke-style=&quot;style2&quot;&gt;HikariCP는 JDBC DataSource의 구현체이고 HikariCP에서 JDBC를 사용해서 데이터베이스와 연결하고 그 연결을 관리한다.&lt;/blockquote&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;그렇다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;DataSource&lt;/b&gt;란?&lt;/h4&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;자바에서 데이터소스는 javax.sql.DataSource 인터페이스를 구현함.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;데이터 소스를 통해 얻을 수 있는 것: 커넥션 객체, 커넥션 풀에서 사용가능한 커넥션, 분산 트랜잭션과 커넥션 풀에서 사용가능한 커넥션&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래와 같이 설정할 수 있다. 물리적 DB와 연결을 위한 기본 정보를 설정하게 된다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;469&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8s9z9/btsMJNfkpxE/7PDV1V5d83YjHJrbBe6Sbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8s9z9/btsMJNfkpxE/7PDV1V5d83YjHJrbBe6Sbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8s9z9/btsMJNfkpxE/7PDV1V5d83YjHJrbBe6Sbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8s9z9%2FbtsMJNfkpxE%2F7PDV1V5d83YjHJrbBe6Sbk%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;469&quot; height=&quot;182&quot; data-origin-width=&quot;469&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;그렇다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;JDBC&lt;/b&gt;란?&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 관계형 데이터베이스에 접근하는 방법을 정의한 어플리케이션 프로그래밍 API&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터베이스와 커넥션&lt;/b&gt;을 만들고, SQL 작업을 위한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;인터페이스를 제공&lt;/b&gt;하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;결과를 처리&lt;/b&gt;함&lt;/li&gt;
&lt;li&gt;JDBC API는 데이터베이스와 연결하기 위해 JDBC 드라이버를 사용함&lt;/li&gt;
&lt;li&gt;JDBC API는 쿼리 실행 이전과 이후에 많은 코드를 작성(커넥션, statement, 연결 및 해제)해야 하는 단점이 있음&lt;br /&gt;-&amp;gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;JdbcTemplate&lt;/b&gt;을&amp;nbsp;사용함으로써 간략하게 작성할 수 있게 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;b&gt;JDBC로 했을 때&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742116885883&quot; class=&quot;gradle&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import java.sql.*;

public class Example {
    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;

        try {
            // 1. JDBC driver 로드
            Class.forName(&quot;com.mysql.cj.jdbc.Driver&quot;);

            // 2. Connection 객체 생성
            conn = DriverManager.getConnection(&quot;jdbc:mysql://localhost:3306/databaseName&quot;, &quot;username&quot;, &quot;password&quot;);

            // 3. Statement 객체 생성
            stmt = conn.createStatement();

            // 4. Query 수행
            rs = stmt.executeQuery(&quot;SELECT * FROM tableName&quot;);

            // 5. ResultSet 객체로부터 데이터 저장
            while(rs.next()) {
                // Retrieve by column name
                int id  = rs.getInt(&quot;id&quot;);
                String name = rs.getString(&quot;name&quot;);
                // Display values
                System.out.println(&quot;ID: &quot; + id + &quot;, Name: &quot; + name);
            }
        } catch(SQLException se) {
            se.printStackTrace();
        } catch(Exception e) {
            e.printStackTrace();
        } finally {
            // 6. 리소스 해제 
            try {
                if(rs != null) rs.close();
                if(stmt != null) stmt.close();
                if(conn != null) conn.close();
            } catch(SQLException se) {
                se.printStackTrace();
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;JDBC Template으로 했을 때&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742116885892&quot; class=&quot;actionscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public class UserRepository {
    private final JdbcTemplate jdbcTemplate;

    public UserRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public List&amp;lt;User&amp;gt; findAllUsers() {
        String sql = &quot;SELECT * FROM User&quot;;
        return jdbcTemplate.query(sql, (resultSet, rowNum) -&amp;gt; new User(
            resultSet.getLong(&quot;id&quot;),
            resultSet.getString(&quot;name&quot;),
            resultSet.getString(&quot;email&quot;)
        ));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 요즘은 더 개선된&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Mybatis, JPA&lt;/b&gt;을 많이 쓴다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;HikariCP 설정값으로 Connection Pool 관리하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection Pool을 관리할 수 있는 HikariCP 설정값에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(출처: &lt;a href=&quot;https://cheese10yun.github.io/mysql-connection-pool-timeout-1/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://cheese10yun.github.io/mysql-connection-pool-timeout-1/&lt;/a&gt;)&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #5d686f; text-align: left; border-collapse: collapse; width: 100%; height: 520px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;설정&lt;span&gt;&amp;nbsp;항목&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;설명&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;Default값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;maximum-pool-size&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;커넥션 풀에서 유지할 수 있는 최대 커넥션 수입니다. 이 수치를 초과하는 요청은 대기 상태로 들어갑니다.&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;minimum-idle&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;풀에서 유지할 유휴 커넥션의 최소 개수입니다. 유휴 커넥션이 이 수치 이하로 떨어지면 새로운 커넥션이 생성됩니다.&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;maximum-pool-size&lt;span&gt;&amp;nbsp;&lt;/span&gt;값과 동일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;connection-timeout&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;커넥션을 가져오기 위해 스레드가 대기할 수 있는 최대 시간입니다. 이 시간이 초과되면 예외가 발생합니다.&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;30,000ms (30초)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;max-lifetime&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;커넥션이 유지될 수 있는 최대 시간입니다. 이 시간이 지나면 커넥션은 폐기되고 새 커넥션으로 교체됩니다.&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;1,800,000ms (30분)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;idle-timeout&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;유휴 상태의 커넥션이 풀에서 유지될 수 있는 최대 시간입니다. 이 시간이 지나면 유휴 커넥션이 풀에서 제거됩니다.&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;600,000ms (10분)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;leak-detection-threshold&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;지정된 시간(밀리초) 동안 사용되지 않은 커넥션을 감지하는 데 사용됩니다. 이 시간이 지나면 커넥션 리크(leak)를 의심하고 경고를 남깁니다.&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;0 (비활성화)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;pool-name&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;커넥션 풀의 이름을 지정합니다. 기본적으로 HikariCP는 자동으로 이름을 생성하지만, 필요에 따라 지정할 수 있습니다.&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;자동 생성된 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;auto-commit&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;새 커넥션이 자동 커밋 모드로 시작할지를 결정합니다. 각 쿼리 후 자동으로 커밋됩니다.&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;validation-timeout&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;커넥션이 유효한지 검증할 때 사용할 최대 시간입니다. 이 시간이 초과되면 커넥션은 유효하지 않다고 판단하고 폐기됩니다.&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;5,000ms (5초)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;read-only&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;커넥션이 읽기 전용 모드에서 작동할지를 결정합니다.&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;isolate-internal-queries&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;내부 쿼리(예: 커넥션 풀의 유지 관리 쿼리)가 애플리케이션의 쿼리와 격리되는지를 설정합니다.&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;allow-pool-suspension&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;커넥션 풀의 일시 정지 기능을 활성화합니다. 이 설정이 활성화되면 풀을 일시 정지하거나 다시 시작할 수 있습니다.&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;initialization-fail-timeout&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;풀을 시작할 때 초기화에 실패하는 경우를 대비한 타임아웃 시간입니다. 이 시간이 지나면 예외가 발생합니다.&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;1초 (1,000ms)&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;application.yml에 아래와 같이 작성해주게 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;spring: &lt;br /&gt;&amp;nbsp; &amp;nbsp; datasource: &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; url: url...&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; username: root &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; password: pw&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; hikari: &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; maximum-pool-size:100 #최대 pool 크기 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; minimum-idle: 10 #최소 유휴 pool 크기 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; idle-timeout: 600000 #연결위한 최대 유휴 시간 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; max-lifetime: 1800000 #반납된 커넥션의 최대 수명&lt;/blockquote&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;그렇다면&lt;b&gt; 적절한 Connection Pool의 크기&lt;/b&gt;는 뭘까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;널리 사용하고 있는 Connection Pool 사이즈 공식&lt;/a&gt;을 확인해보자. 아래 공식에서 시작해서 각자 application 상황에 맞게 테스트를 통해 pool size를 최적화 시켜야 한다고 말하고 있다.&lt;/p&gt;
&lt;figure id=&quot;og_1742141363135&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;About Pool Sizing&quot; data-og-description=&quot;光 HikariCP・A solid, high-performance, JDBC connection pool at last. - brettwooldridge/HikariCP&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&quot; data-og-url=&quot;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cHCHR5/hyYrRQ2pvn/bsX54NFBU1RdCEljS7VfC1/img.png?width=1200&amp;amp;height=600&amp;amp;face=943_133_1051_279,https://scrap.kakaocdn.net/dn/Sf18C/hyYqdmNgk6/gcQwrrqKEGMcTqVy2vkN40/img.png?width=1200&amp;amp;height=600&amp;amp;face=943_133_1051_279&quot;&gt;&lt;a href=&quot;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cHCHR5/hyYrRQ2pvn/bsX54NFBU1RdCEljS7VfC1/img.png?width=1200&amp;amp;height=600&amp;amp;face=943_133_1051_279,https://scrap.kakaocdn.net/dn/Sf18C/hyYqdmNgk6/gcQwrrqKEGMcTqVy2vkN40/img.png?width=1200&amp;amp;height=600&amp;amp;face=943_133_1051_279');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;About Pool Sizing&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;光 HikariCP・A solid, high-performance, JDBC connection pool at last. - brettwooldridge/HikariCP&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K2s2r/btsMLwk79UX/khUbvIDJxJ0AcplWk15LsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K2s2r/btsMLwk79UX/khUbvIDJxJ0AcplWk15LsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K2s2r/btsMLwk79UX/khUbvIDJxJ0AcplWk15LsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK2s2r%2FbtsMLwk79UX%2FkhUbvIDJxJ0AcplWk15LsK%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;576&quot; height=&quot;432&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. Thread Pool 크기 &amp;lt; Connection Pool 크기 &lt;br /&gt;2. 적절한 connection 크기 = ((core_count) * 2) + effective_spindle_count)&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp;&amp;gt; core_count: 현재 사용하는 서버 환경에서의 CPU 개수&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp;&amp;gt; effective_spindle_count: DB 서버가 관리할 수 있는 동시 I/O 요청 수 (Disk 관련)&lt;/blockquote&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; 1번&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thread Pool은 Connection Pool과 비슷한 개념으로 Connection이 아닌 Thread를 미리 일정 개수만큼 만들어 놓고 사용하는 Pool 이다. 자바에서는 Thread를 운영체제의 자원으로 사용하기 때문에 우리가 Thread를 무한정으로 만들면 자원고갈로 서버가 다운될 위험이 있다. 따라서 Thread 개수를 관리하기 위한 것이 Thread Pool이다.&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;즉, Thread는 Connection을 사용하는 주체이기 때문에 Thread Pool 크기보다 Connection Pool 크기가 작으면 &lt;u&gt;Thread가 Connection을 기다려야하는 상황&lt;/u&gt;이 있을 수 있다. 따라서 Thread Pool 크기 &amp;lt; Connection Pool 크기로 설정하는 것이 권장된다.&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;b&gt;Core_count&lt;/b&gt;: 전제 조건은 CPU 코어 1개는 한번에 하나의 Thread만 실행할 수 있다는 것이다. 만약 CPU Core 1개, Connection이 1개인 상황에서 Thread가 Connection 1개를 가져와서 대기상태에 있게 되면 Core가 다른 Thread를 실행하더라도 Connection이 이미 첫번째 Thread에 의해 점유중이기 때문에 Core는 놀게 된다. 그러나 운영체제의 기본 조건이 &lt;u&gt;CPU는 놀게 하지 않는다!&lt;/u&gt; 이기 때문에 이는 비효율적인 상황으로 판단된다. 따라서 경험적으로 CPU Core 개수의 2배 정도가 적절한 Connection 크기라고 권장된다.&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;Effective_spindle_count&lt;/b&gt;: DB가 동시에 처리할 수 있는 요청 수(Disk) 인데, 너무 많은 Connection이 있다면 병목 현상이 발생하게 된다. 따라서 이정도 Connection이 있다면 병목 현상 없이 감당할 수 있는 &lt;u&gt;적절한 Connection 수를 맞추기 위한 값&lt;/u&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;Connection Pool 관련 기술 아티클&amp;gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Connection Pool과 이를 관리하는 HikariCP의 설정값에 대해 알아봤다. 관련된 기술 아티클을 간단하게 정리해보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. HikariCP의 설정을 최적화하여 TPS(Transactions Per Second) 변화에 유연하게 대응하기&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(출처:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://cheese10yun.github.io/mysql-connection-pool-timeout-1/&quot;&gt;https://cheese10yun.github.io/mysql-connection-pool-timeout-1/&lt;/a&gt;)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 글 저자는 TPS변화에 따른 커넥션 풀의 동작을 테스트 해보고 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. TPS가 낮은 경우: 즉, 트래픽이 적은 경우 HikariCP는 최소 커넥션(minimum-idle)만 유지하여 리소스 사용을 최적화&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. TPS가 높아질 경우: TPS가 증가하여 커넥션이 필요한 상황이 되면, HikariCP는 최대 커넥션(maximum-pool-size)까지 확장하여 대량의 요청을 처리할 수 있게 함.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. TPS가 다시 낮아지는 경우: TPS가 다시 낮아지면 hikariCP는 idle-timeout에 따라 불필요한 커넥션을 풀에서 제거하고, minimum-idle만 유지하여 리소스를 절약&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;좀 더 자세한 모니터링 이미지로 알아보자.&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;TPS가 높아지는 상황이라면?&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본 설정은 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1742144543776&quot; class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;spring:
    datasource:
        hikari:
            minimum-idle: 10 // 최소 유휴 커넥션 수. TPS가 낮을 때 리소스 절약 가능
            maximum-pool-size: 10 // 커넥션 풀의 최대 크기. TPS가 높아질 때 최대 10개까지 생성
            idle-timeout: 30000 // 지정된 시간(밀리초) 동안 유휴 상태인 커넥션이 있을 경우 풀에서 제거
            connection-timeout: 20000 // 커넥션을 얻기 위해 대기하는 최대 시간. 이 시간안에 커넥션을 확보하지 못하면 예외가 발생&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;2976&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7MAXj/btsMMA059ET/g4E9i3UQN3KrdSgwbY9mH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7MAXj/btsMMA059ET/g4E9i3UQN3KrdSgwbY9mH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7MAXj/btsMMA059ET/g4E9i3UQN3KrdSgwbY9mH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7MAXj%2FbtsMMA059ET%2Fg4E9i3UQN3KrdSgwbY9mH1%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;839&quot; height=&quot;507&quot; data-origin-width=&quot;2976&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Total Requests per Second
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RPS (초록색 라인): 초당 요청 처리량&lt;/li&gt;
&lt;li&gt;Failures (빨간색 라인): 실패한 요청&lt;/li&gt;
&lt;li&gt;TPS가 증가하여 초당 12 요청 수준에 도달했을 때 실패한 요청이 발생. Connection Pool이 최대 용량인 10에 도달해서 더 이상 추가 요청을 처리하지 못하는 상황을 의미&lt;/li&gt;
&lt;li&gt;이후 TPS는 유지되지만 실패한 요청이 지속적으로 발생하면서 Connectino Pool의 제한에 따른 성능 저하가 보임. 이후에 TPS는 응답 지연으로 인해 더 이상 올라가지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Response Times
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TPS가 증가함에 따라 응답시간이 증가하는 모습을 보임&lt;/li&gt;
&lt;li&gt;커넥션 풀이 가득 차서 새로운 요청이 대기 상태로 전환되었기 때문이며, 트래픽 증가와 함께 시스템의 성능 한계에 도달했음을 보여줌&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Number of Users
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 수가 점진적으로 증가하며 약 300명 이상일 때부터 시스템은 커넥션 풀이 한계에 도달하며 그 이후로 성능 저하가 본격적으로 발생함&lt;/li&gt;
&lt;li&gt;Connection Pool 크기를 초과하는 사용자 요청은 실패하거나 긴 대기 시간을 초래하며, 이는 응답 시간 증가와 TPS 유지의 원인이 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;어떻게 해결하면 좋을까?&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;maximum-pool-size 증가: TPS 수요 충족시키기 위해서 maximum-pool-size값을 10 이상으로 설정해서 Connection Pool이 더 많은 요청을 처리할 수 있도록 한다.&lt;/li&gt;
&lt;li&gt;모니터링 및 지속적인 튜닝&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 TPS 감소 상황에서는?&lt;/p&gt;
&lt;pre id=&quot;code_1742144427970&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
    datasource:
        hikari:
            maximum-pool-size: 200           # 최대 커넥션 수
            minimum-idle: 10                 # 최소 유휴 커넥션 수
            max-lifetime: 300000             # 커넥션이 유지될 최대 시간 (밀리초)
            idle-timeout: 250000             # 유휴 커넥션이 유지될 최대 시간 (밀리초)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 설정에서 TPS가 감소하고 있는 상황이라고 가정해보자.&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;2976&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wYXOg/btsMLOeNXHy/pk1f5CGC7P22WvzmvmDhDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wYXOg/btsMLOeNXHy/pk1f5CGC7P22WvzmvmDhDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wYXOg/btsMLOeNXHy/pk1f5CGC7P22WvzmvmDhDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwYXOg%2FbtsMLOeNXHy%2Fpk1f5CGC7P22WvzmvmDhDK%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;643&quot; height=&quot;389&quot; data-origin-width=&quot;2976&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Total Requests per Second (RPS)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TPS가 낮아지면서 초당 요청 처리량이 약 5 수준으로 안정화 됨&lt;/li&gt;
&lt;li&gt;요청 실패가 없고 모든 요청이 성공적으로 처리됨&lt;/li&gt;
&lt;li&gt;이는 트래픽이 줄면서 커넥션 풀이 유휴 상태로 돌아가고 있음을 의미&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Response Times
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응답 시간 모두 약 1,000ms 내외로 일정하게 유지되고 잇음&lt;/li&gt;
&lt;li&gt;변동이 크지 않고 안정적인 수준을 보여, TPS가 낮아진 상황에도 일관된 성능을 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Number of Users
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트에서 사용자 수가 10명으로 일정하게 유지되고 있으며, TPS가 낮은 상태로 안정화됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 테스트에서는 TPS가 줄면서 totalConnections도 함께 감소하며 &lt;b&gt;리소스가 효율적으로 관리되고 있는 모습&lt;/b&gt;을 보여준다. minimum-idle 설정을 통해 connection pool이 최소 10개의 유휴 커넥션을 유지하고, idleTiemout이 설정된 시간 동안 유휴 상태인 커넥션을 자동으로 해제하여 불필요한 자원 낭비를 방지한다. 또한 maxLifetime 설정으로 각 커넥션의 유지 시간을 제한하여 일정 시간이 지나면 커넥션이 새롭게 갱신된도록 한다.&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;HikariCP를 사용한 Connection Pool 설정이 트래픽 변화에 유연하게 대응할 수 있고, 성능 최적화를 위한 중요한 도구&lt;/b&gt;임을 알 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. HikariCP와 Dead Lock - 우아한 형제들 기술블로그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(출처: &lt;a href=&quot;https://techblog.woowahan.com/2664/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://techblog.woowahan.com/2664/&lt;/a&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;pre id=&quot;code_1741868832829&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: null
o.h.engine.jdbc.spi.SqlExceptionHelper : hikari-pool-1 &amp;ndash; Connection is not available, request timed out after 30000ms.
org.hibernate.exception.JDBCConnectionException: unable to obtain isolated JDBC connection
Could not open JPA EntityManager for transaction; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; color: #24292e; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HikariCP maximum pool size&lt;/li&gt;
&lt;li&gt;DB에 insert하고자 하는 전체 Thread Count&lt;/li&gt;
&lt;li&gt;하나의 Task에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;동시에&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;필요한 Connection 수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 위 3개의 상관관계에 따라 발생한 이슈라고 한다. 왜인지 아래에서 간단하게 요약해본다.&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;1062&quot; data-origin-height=&quot;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjfU4O/btsMKEhP5d0/QJ0Kb1OF9e3mjGnvmEbfH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjfU4O/btsMKEhP5d0/QJ0Kb1OF9e3mjGnvmEbfH1/img.png&quot; data-alt=&quot;이슈 상황&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjfU4O/btsMKEhP5d0/QJ0Kb1OF9e3mjGnvmEbfH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjfU4O%2FbtsMKEhP5d0%2FQJ0Kb1OF9e3mjGnvmEbfH1%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;656&quot; height=&quot;337&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;546&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이슈 상황&lt;/figcaption&gt;
&lt;/figure&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;blockquote data-ke-style=&quot;style3&quot;&gt;1. 약 30초 주기로 발생하는 에러 로그&lt;br /&gt;SQL Error: 0, SQLState: null &lt;br /&gt;HikariPool-1 - Connection is not available, request timed out after 30000ms.&lt;br /&gt;&lt;br /&gt;2. HikariCP HouseKeeper가 찍어주는 pool stats 로그&lt;br /&gt;HikariPool-1 - Timeout failure stats (total=10, active=10, idle=0, waiting=16)&lt;br /&gt;&lt;br /&gt;3. Message Queue Consumer Thread 갯수 = 16개&lt;br /&gt;&lt;br /&gt;4. HikariCP maximum pool size 갯수 = 10개 (default로 사용하고 있었습니다.)&lt;br /&gt;&lt;br /&gt;5. 장애 시간 대 네트워크 이슈 없음&lt;/blockquote&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;❌ Thread간 Connection을 차지 하기 위한 &lt;b&gt;Race Condition 상태&lt;/b&gt;가 된 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❌ 메시지 1개를 저장하는데 한 Transaction에서 동시에 Connection 2개를 사용하면서 &lt;b&gt;hikariCP에 대한 Thread간 데드락이 발생&lt;/b&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;HikariCP에서 Connection을 어떻게 할당받고 반납하길래 이런 현상이 일어난 걸까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 틀은 이런식으로 되어있을 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1741870786235&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  Connection connection = null;
  PreparedStatement preparedStatement = null

  try {
    connection = hikariDataSource.getConnection();
    preparedStatement = connection.preparedStatement(sql);
    preparedStatement.executeQuery();
  } catch(Throwable e) {
    throw new RuntimeException(e);
  } finally {
    if (preparedStatement != null) {
      preparedStatement.close();
    }
    if (connection != null) {
      connection.close(); // 여기서 connection pool에 반납됩니다.
    }
  }&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;HikariPool에서의 getConnection() 로직&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;535&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AxBOL/btsMNcenLJb/SKrkohTO8stVYVdNlbjyuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AxBOL/btsMNcenLJb/SKrkohTO8stVYVdNlbjyuK/img.png&quot; data-alt=&quot;hikariPool.getConnection()&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AxBOL/btsMNcenLJb/SKrkohTO8stVYVdNlbjyuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAxBOL%2FbtsMNcenLJb%2FSKrkohTO8stVYVdNlbjyuK%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;780&quot; height=&quot;387&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;535&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;hikariPool.getConnection()&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariPool 클래스 내부에 getConnection() 메서드를 살펴보자. hikariPool.getConnection()을 하면 concurrentBag.borrow() 라는 메서드로 사용가능한 Connection을 리턴하도록 되어있다. (HikariCP은 내부적으로 ConcurrentBag이라는 구조체를 이용해서 Connection을 관리하고 있다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;1240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AWrOX/btsMNfIUMZJ/9x8CoqtIsdURT5GAEqxCFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AWrOX/btsMNfIUMZJ/9x8CoqtIsdURT5GAEqxCFK/img.png&quot; data-alt=&quot;concurrentBag.borrow()&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AWrOX/btsMNfIUMZJ/9x8CoqtIsdURT5GAEqxCFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAWrOX%2FbtsMNfIUMZJ%2F9x8CoqtIsdURT5GAEqxCFK%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;813&quot; height=&quot;813&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;1240&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;concurrentBag.borrow()&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;borrow() 메서드를 타고 들어가보면 크게 4가지 파트로 나뉜다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Try the thread-local list first&lt;/b&gt;&lt;br /&gt;현재 Thread가 이전에 사용한 Connection 정보가 있는지 확인한다. 이전에 사용한 Connection list 중에 현재 사용가능한(idle) 상태의 Connection이 있다면 해당 connection (bagEntry)를 리턴하고, 아니라면 2번으로 넘어간다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Otherwise, scan the shared list&lt;/b&gt; &lt;br /&gt;Hikari Pool 전체 Connection 중 현재 사용가능한(idle) 상태의 Connection이 있는지 확인한다. 있다면 해당 connection (bagEntry)를 리턴하고, 없다면 3번으로 넘어간다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;then poll the handoff queue&lt;/b&gt;&lt;br /&gt;concurrentBag.handOffQueue에 사용가능한 Connection이 있는지 확인한다. 있다면 해당 connection (bagEntry)를 리턴하고, 없다면 3번을 반복한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;do while문&lt;/b&gt;&lt;br /&gt;Timeout 시간이 지났다면 null을 리턴한다.&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;이를 Flow chart로 작성하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;1051&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czFSai/btsMKQCl1Sk/cZi11nBslIe8JspkStbry1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czFSai/btsMKQCl1Sk/cZi11nBslIe8JspkStbry1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czFSai/btsMKQCl1Sk/cZi11nBslIe8JspkStbry1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczFSai%2FbtsMKQCl1Sk%2FcZi11nBslIe8JspkStbry1%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;441&quot; height=&quot;620&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;1051&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;HikariCP에서의 Connection.close()의 로직&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 transaction이 commit되거나 에러로 인해 rollback이 호출되면 connection.close()가 호출되어 connection이 반납된다. connection.close() -&amp;gt; concurrentBag.requite() 가 실행된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1059&quot; data-origin-height=&quot;830&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qas0H/btsMM6FmKVF/xKvJvdtgIlgudO6nQmuQ6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qas0H/btsMM6FmKVF/xKvJvdtgIlgudO6nQmuQ6k/img.png&quot; data-alt=&quot;concurrentBag.requite()&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qas0H/btsMM6FmKVF/xKvJvdtgIlgudO6nQmuQ6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqas0H%2FbtsMM6FmKVF%2FxKvJvdtgIlgudO6nQmuQ6k%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;692&quot; height=&quot;542&quot; data-origin-width=&quot;1059&quot; data-origin-height=&quot;830&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;concurrentBag.requite()&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;requite() 메서드를 타고 들어가보면 다음과 같은 로직이 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;bagEntry.setState(STATE_NOT_IN_USE);&lt;/b&gt;&lt;br /&gt;connection을 idle connection으로 상태를 바꾼다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;for (... waiters.get()...)&lt;/b&gt;&lt;br /&gt;handOffQueue에서 connection을 받길 원하는 다른 thread가 있는지 확인한다. 있다면 handOffQueue에 Connection을 삽입한다. 이후에 3번으로 넘어간다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;threadLocalEntries&lt;/b&gt;&lt;br /&gt;사용한 connection 정보를 threadLocalList에 등록하여 다음에 또 connection을 요청하는 경우 borrow에서 활용할 수 있도록 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 Flow chart로 작성하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nVIcF/btsMKfo8zG7/0PP6s7FykQcbzvowlJIbCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nVIcF/btsMKfo8zG7/0PP6s7FykQcbzvowlJIbCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nVIcF/btsMKfo8zG7/0PP6s7FykQcbzvowlJIbCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnVIcF%2FbtsMKfo8zG7%2F0PP6s7FykQcbzvowlJIbCk%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;415&quot; height=&quot;472&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;702&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;h4 data-ke-size=&quot;size20&quot;&gt;그래서 Dead Lock이 왜 발생했는데?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MQ의 consumer thread 갯수&lt;/b&gt;보다 &lt;b&gt;hikariCP의 maximum pool size&lt;/b&gt;를 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;충분하게 설정하지 못해서&lt;/span&gt;&lt;/b&gt; Dead Lock이 발생했다. 아래와 같이 조건이 있다고 가정해보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #24292e; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Thread Count: 1개&lt;/li&gt;
&lt;li&gt;HikariCP Maximum pool size: 1개&lt;/li&gt;
&lt;li&gt;하나의 Task에서 동시에 요구되는 Connection 갯수:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;2개!!&lt;/b&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;1080&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ASpf9/btsMKAsZUEp/dBevXT6cfJnnuPOakhlW1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ASpf9/btsMKAsZUEp/dBevXT6cfJnnuPOakhlW1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ASpf9/btsMKAsZUEp/dBevXT6cfJnnuPOakhlW1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FASpf9%2FbtsMKAsZUEp%2FdBevXT6cfJnnuPOakhlW1k%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;418&quot; height=&quot;308&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;796&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;ol style=&quot;list-style-type: decimal; color: #24292e; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Thread가 Repository.save(entity) 라는 insert query를 실행하기 위해 Transaction을 시작합니다.&lt;/li&gt;
&lt;li&gt;Root Transaction용 Connection을 하나 가져옵니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;(PoolStats : total=1, active=1, idle=0, waiting=0)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Transaction을 시작하였고 Repository.save를 하기 위해 Connection이 하나 더 필요하다고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;가정&lt;/b&gt;해보겠습니다.&lt;/li&gt;
&lt;li&gt;Thread-1은 Hikari Pool에 Connection을 요청합니다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;위의 3단계 절차대로, 현재 자기 Thread의 방문내역을 살펴봅니다.&lt;br /&gt;아직 방문내역이 등록된 게 없습니다.&lt;/li&gt;
&lt;li&gt;전체 Hikari Pool에서 idle상태의 Connection을 스캔합니다.&lt;br /&gt;Pool Size는 1개이고 1개 있던 Connection은 Thread-1에서 이미 사용중입니다.&lt;/li&gt;
&lt;li&gt;마지막으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;handOffQueue에서 누군가 반납한 Connection이 있길 기대하며 30초 동안 기다립니다.&lt;/b&gt;&lt;br /&gt;하나 있던 Connection을 자기 자신이 사용하고 있기 때문에 자기 자신이 반납하지 않는 이상 사용할 Connection이 없습니다.&lt;br /&gt;&lt;b&gt;(PoolStats : total=1, active=1, idle=0, waiting=1)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;결국 30초가 지나고 Connection Timeout이 발생하고&lt;br /&gt;hikari-pool-1 - Connection is not available, request timed out after 30000ms.&lt;span&gt;&amp;nbsp;&lt;/span&gt;에러 발생&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;SQLTransientConnectionException으로 인해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Sub Transaction이 Rollback 됩니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Sub Transaction의 Rollback으로 인해 Root Transaction이 rollbackOnly = true가 되며 Root Transaction이 롤백 됩니다. JpaTransactionManager를 사용한다고 가정하겠습니다.&lt;br /&gt;(&lt;a style=&quot;color: #0366d6;&quot; href=&quot;http://woowabros.github.io/experience/2019/01/29/exception-in-transaction.html&quot;&gt;우아한형제들 기술블로그 &amp;ndash; 응? 이게 왜 롤백되는거지?&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;참고)&lt;/li&gt;
&lt;li&gt;Rollback 됨과 동시에 Root Transaction용 Connection은 다시 Pool에 반납됩니다.&lt;br /&gt;&lt;b&gt;(PoolStats : total=1, active=0, idle=1, waiting=0)&lt;/b&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;정리하자면, Connection Pool에 connection이 총 1개 있었는데 Thread-1이 한개를 가져다가 쓰고 있는 상태에서 또 다른 Connection을 또 요청하는 경우 &lt;b&gt;결국 자기 자기자신이 사용중인 connection을 반납하지 않는 이상 사용할 connection이 없기 때문에 Connection Timeout이 발생&lt;/b&gt;하고 &lt;span style=&quot;color: #24292e; text-align: left;&quot;&gt;hikari-pool-1 - Connection is not available, request timed out after 30000ms. 가 발생하는 것이다.&lt;/span&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;span style=&quot;color: #24292e; text-align: left;&quot;&gt;이를 해결하기 위해 우리는 Max Pool Size를 다음과 같이 조정하면 Dead Lock을 피할 수 있다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/brettwooldridge/HikariCP/issues/442#issuecomment-146096704&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #24292e; text-align: left;&quot;&gt;HikariCP Github에 등록된 이슈&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #24292e; text-align: left;&quot;&gt;HikariCP Github Wiki에 작성된 가이드&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;915&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C9mAT/btsMMBeEj2b/EkQKVHwqzmanmMTFkwJxiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C9mAT/btsMMBeEj2b/EkQKVHwqzmanmMTFkwJxiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C9mAT/btsMMBeEj2b/EkQKVHwqzmanmMTFkwJxiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC9mAT%2FbtsMMBeEj2b%2FEkQKVHwqzmanmMTFkwJxiK%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;468&quot; height=&quot;507&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;915&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;98&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cje2OZ/btsMMoT3ya0/vYMvc3KAFz79iUkhaCP6Xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cje2OZ/btsMMoT3ya0/vYMvc3KAFz79iUkhaCP6Xk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cje2OZ/btsMMoT3ya0/vYMvc3KAFz79iUkhaCP6Xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcje2OZ%2FbtsMMoT3ya0%2FvYMvc3KAFz79iUkhaCP6Xk%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;599&quot; height=&quot;62&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;98&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #24292e; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tn&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 전체 Thread 갯수&lt;/li&gt;
&lt;li&gt;Cm&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 하나의 Task에서 동시에 필요한 Connection 수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 최적의 pool 사이즈는 아니지만 DeadLock을 피하기 위한 최소한의 Pool Size라고 한다. 말그대로 최소한의 Pool Size이기 때문에 우아한 형제들 기술블로그 작성자는 위의 공식을 확장하여 사용하는 방향으로 장애에 대한 후속처리를 진행했다고 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reference&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://escapefromcoding.tistory.com/713&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://escapefromcoding.tistory.com/713&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/2664/&quot;&gt;https://techblog.woowahan.com/2664/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cheese10yun.github.io/mysql-connection-pool-timeout-1/&quot;&gt;https://cheese10yun.github.io/mysql-connection-pool-timeout-1/&lt;/a&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>dbconnectionpool</category>
      <category>HikariCP</category>
      <author>심심231</author>
      <guid isPermaLink="true">https://simsim231.tistory.com/297</guid>
      <comments>https://simsim231.tistory.com/297#entry297comment</comments>
      <pubDate>Fri, 14 Mar 2025 00:30:25 +0900</pubDate>
    </item>
    <item>
      <title>헥사고날 아키텍처 Hexagonal Architecture 란?</title>
      <link>https://simsim231.tistory.com/296</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;사이드 프로젝트를 하면서 헥사고날 아키텍처를 접하게 되었는데,&lt;br /&gt;새로운 아키텍처를 사용하면서 이런 부분이 왜 개선이 되었구나를 직접 느끼게 되어&lt;br /&gt;그 내용에 대해 정리해본다.&lt;/blockquote&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;전통적인 웹-도메인-영속성 구조에서는 한 계층의 변화가 다른 계층에도 영향을 끼칠 수 있다. 사실 평소에 개발하면서 느꼈던 것이었다. 예를 들어 다른 Controller가 같은 Service를 사용하면 Controller의 변경이 Service 계층에 영향을 주면서 애플리케이션의 유연성과 확장성을 떨어뜨린다.&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;700&quot; data-origin-height=&quot;157&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkKiX5/btsLS24sGGB/wRnK7XKbrb1Kk4wsOvFlXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkKiX5/btsLS24sGGB/wRnK7XKbrb1Kk4wsOvFlXk/img.png&quot; data-alt=&quot;전통적인 레이어드 아키텍처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkKiX5/btsLS24sGGB/wRnK7XKbrb1Kk4wsOvFlXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkKiX5%2FbtsLS24sGGB%2FwRnK7XKbrb1Kk4wsOvFlXk%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;700&quot; height=&quot;157&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;157&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;전통적인 레이어드 아키텍처&lt;/figcaption&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;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;클린아키텍처를 는 이런 문제에 대한 해결책을 제공한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;각 계층이 본연의 책임을 가지고 독립적으로 동작&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;하도록 설계되어 있어,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;외부의 변화에 덜 민감하고 내부 로직의 변화가 외부에 영향을 미치는 것을 최소화&lt;/b&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;700&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YDqm1/btsLSqdGsGT/5QOQKt4HurJ3CflHDjLuek/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YDqm1/btsLSqdGsGT/5QOQKt4HurJ3CflHDjLuek/img.jpg&quot; data-alt=&quot;클린아키텍처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YDqm1/btsLSqdGsGT/5QOQKt4HurJ3CflHDjLuek/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYDqm1%2FbtsLSqdGsGT%2F5QOQKt4HurJ3CflHDjLuek%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;639&quot; height=&quot;469&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;클린아키텍처&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;클릭 아키텍처의 주요 개념&lt;/b&gt;&lt;br /&gt;- 비즈니스 로직을 구현한 계층은 어떤 의존성도 가지지 않는 최상위 계층이어야 하며, 외부 계층의 변경에 영향을 받지 않아야 한다.&lt;br /&gt;- 해당 계층으로의 입출력을 추상화 계층으로 감싸면, 모든 외부 의존성의 방향을 도메인 계층을 향하도록 만들 수 있다.&lt;br /&gt;- 그로 인해 비즈니스 로직을 구현할때 외부 계층에 대한 결정(REST API, DB 등)을 미루거나 쉽게 변경할 수 있고, 비즈니스 로직 구현에 우선적으로 집중할 수 있다.&lt;br /&gt;- 외부 계층의 상세 구현에 영향을 받지 않고 비즈니스 로직의 테스트가 가능하며, 모든 계층의 컴포넌트들은 독립적으로 각 계층의 역할에 맞는 테스트를 할 수 있다.&lt;/blockquote&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;헥사고날 아키텍처&lt;/h3&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;'느슨하게 결합'할 수 있게 해주는 데에는 헥사고날 아키텍처에 있는 '포트 Port'와 '어댑터 Adapter'라고 부르는 요소의 역할이 크다. 이 두개의 이름을 붙여 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;포트 &amp;amp; 어댑터 아키텍처&lt;/b&gt;&lt;/span&gt;라고도 부른다. 포트와 어댑터가 외부와 애플리케이션 코어를 연결하여 경계를 느슨하게 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9bDtO/btsLSsihO1W/lWJ3W5diqXooDkFuGf3jw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9bDtO/btsLSsihO1W/lWJ3W5diqXooDkFuGf3jw0/img.png&quot; data-alt=&quot;헥사고날 아키텍처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9bDtO/btsLSsihO1W/lWJ3W5diqXooDkFuGf3jw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9bDtO%2FbtsLSsihO1W%2FlWJ3W5diqXooDkFuGf3jw0%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;700&quot; height=&quot;344&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;헥사고날 아키텍처&lt;/figcaption&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;&lt;b&gt;어댑터(Adapter)&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어댑터는 포트를 통해 애플리케이션 코어와 외부 세계를 연결한다. &lt;u&gt;특정 외부 기술이나 프레임워크에 의존적인 로직을 담당&lt;/u&gt;하며, 이를 통해 애플리케이션 코어는 외부와의 결합도를 최소화한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인커밍 어댑터(Incoming Adapter): 사용자 인터페이스(UI), 테스트 또는 외부 시스템으로부터의 요청을 애플리케이션 코어로 주도하는데 사용. (ex) Spring web MVC의 Controller가 이 역할을 한다고 볼 수 있음&lt;/li&gt;
&lt;li&gt;아웃고잉 어댑터(Outgoing Adapter): 애플리케이션 코어에서 외부에 데이터를 전달하는 역할을 담당. 예를 들어, 데이터베이스에 데이터를 저장하거나 외부 시스템에 메시지를 전송하는 등의 역할을 한다. (ex) Spring web MVC의 Repository가 이 역할을 한다고 볼 수 있음&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;b&gt;포트(Port)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트는 애플리케이션 코어의 경계를 정의하며, &lt;u&gt;애플리케이션 코어가 제공해야할 기능을 나타내며&lt;/u&gt; 어댑터를 통해 애플리케이션 코어에 접근하는 인터페이스다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인커밍 포트(Incoming Port): 외부 요청이 애플리케이션 코어로 들어오는 경로를 정의. 예를 들어, 웹 요청, 스케쥴링 이벤트 등이 인커밍 포트를 통해 애플리케이션 코어로 들어올 수 있다. (ex) Spring web MVC의 Controller와 Service 사이의 인터페이스 (아래에서는 UseCase)&lt;/li&gt;
&lt;li&gt;아웃고잉 포트(Outgoing Port): 애플리케이션 코어가 외부 세계에 서비스를 제공하기 위한 경로를 정의한다. 예를 들어, 데이터베이스, 메시징 시스템, 웹서비스 등에 데이터를 전송하거나 요청하는 경우에 사용한다. (ex) Spring web MVC의 Repository와 Service 사이의 인터페이스&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드&lt;/h3&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;Controller에 해당. 인커밍 어댑터는 포트를 통해 도메인 로직 내부에 접근할 수 있음.&lt;/p&gt;
&lt;pre id=&quot;code_1736775074251&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/v1/member&quot;)
class MyAlilmHistoryContoller(
    private val myAlilmHistoryUseCase: MyAlilmHistoryUseCase
) {
    @GetMapping(&quot;/my-alilm-history&quot;)
    fun myAlilmHistory(
        @AuthenticationPrincipal customMemberDetails: CustomMemberDetails
    ) : ResponseEntity&amp;lt;MyAlilmHistoryResponse&amp;gt; {
        val result = myAlilmHistoryUseCase.myAlilmHistory(
        	MyAlilmHistoryUseCase.MyAlilmHistoryCommand(customMemberDetails.member)
        )
        val response = MyAlilmHistoryResponse.from(result)

        return ResponseEntity.ok(response)
    }
}&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스인 유스케이스&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 유스케이스(UseCase): 도메인 로직을 실행하는 코드의 집합으로, 인터페이스로 정의해 어떤 작업을 수행하고 어떤 결과를 반환해야 하는지에 대한 규칙을 정의한다&lt;br /&gt;- 커맨드(Command): 유스케이스 메서드의 입력객체&lt;/blockquote&gt;
&lt;pre id=&quot;code_1736775113981&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface MyAlilmHistoryUseCase {
    fun myAlilmHistory(command: MyAlilmHistoryCommand): List&amp;lt;MyAlilmHistoryResult&amp;gt;
}&lt;/code&gt;&lt;/pre&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;인커밍 어댑터인 Controller 객체는 도메인 로직을 담당하는 애플리케이션 코어(Service)에 접근하기 위해 인커밍 포트 인터페이스를 통한다. 이 과정에서 컨트롤러는 포트가 사용하는 입력 객체(Command)로의 매핑 작업을 수행하며, 이 객체는 생성 시점에서 생성자를 통해 입력의 유효성을 검사한다.&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;어댑터인 Controller는 애플리케이션 코어의 구체적인 사항을 알 필요 없이 사용하는 포트만 알면 접근할 수 있다.&lt;/u&gt; 이렇게 함으로써 헥사고날 아키텍처는 외부 요구사항의 변경이 애플리케이션 코어 즉, 비즈니스 로직에 영향을 미치는 것을 방지한다. 그 결과, 각 계층은 자신의 책임에만 집중하면 되므로 결합도는 낮아지고 응집도는 높아지게 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&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;/p&gt;
&lt;pre id=&quot;code_1736775985464&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@Transactional
class MyAlilmHistoryService(
    val loadMyAlilmHistoryPort: LoadMyAlilmHistoryPort
) : MyAlilmHistoryUseCase {

    override fun myAlilmHistory(command: MyAlilmHistoryUseCase.MyAlilmHistoryCommand): List&amp;lt;MyAlilmHistoryUseCase.MyAlilmHistoryResult&amp;gt; {
        ...
    }

}&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 코어에서 외부 세계로 향하는 통로&lt;/p&gt;
&lt;pre id=&quot;code_1736776085481&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface LoadMyAlilmHistoryPort {

    fun loadMyAlilmHistory(member: Member, dayLimit: Long): List&amp;lt;MyAlilmHistory&amp;gt;

    data class MyAlilmHistory(
        val alilm: Alilm,
        val product: Product
    ) {
        companion object {
            ...
        }
    }
}&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;
&lt;pre id=&quot;code_1736776101590&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class AlilmAndProductAdapter(
    private val alilmRepository: AlilmRepository,
    private val alilmMapper: AlilmMapper,
    private val productMapper: ProductMapper
) : LoadMyAlilmHistoryPort {

    override fun loadMyAlilmHistory(member: Member, dayLimit: Long): List&amp;lt;LoadMyAlilmHistoryPort.MyAlilmHistory&amp;gt; {
        return alilmRepository
               .findAlilmAndProductByMemberId(...)
               .map {
                   LoadMyAlilmHistoryPort.MyAlilmHistory(
                       alilm = alilmMapper.mapToDomainEntity(it.alilmJpaEntity),
                       product = productMapper.mapToDomainEntity(it.productJpaEntity)
                   )
               }
    }
}&lt;/code&gt;&lt;/pre&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;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;아웃고잉 포트, 아웃고잉 어댑터는 어떻게 결합도를 낮췄을까?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인커밍 포트인 UseCase의 구현체(Service)는 한 개의 아웃고잉 포트 인터페이스를 의존하고 있다. 이처럼 &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;또한 아웃고잉 어댑터를 보면 한 개의 아웃고잉 포트의 인터페이스를 구현하고 있다. 이는 ISP, 인터페이스 분리 원칙을 지킨 것으로 볼 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;헥사고날 아키텍처를 사용하고 느낀 장단점?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 헥사고날 아키텍처로 애플리케이션을 개발하면서 가장 크게 달라진 것은 API 하나를 개발할 때 일단 API request dto부터 만드는게 아닌 도메인 로직부터 고민한다는 점이었다. 어차피 클라이언트로부터 들어올 값들은 포트와 어댑터를 통해 도메인 로직에 크게 영향을 받지않고 세팅할 수 있다. 그 반대로 DB와 연결할 때도 마찬가지로, 테이블의 스키마가 당장 중요하지는 않게 느껴졌다. 도메인 모델과 로직에 좀 더 집중을 할 수 있는 것이다. 도메인을 우선적으로 고려하고, 그 다음 순서로 외부에서 어떻게 요청이 들어오고 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;또한, 테스트할 때 편리하다는 생각이 들었다. 포트는&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; color: #292929; text-align: left;&quot;&gt;인터페이스이기 때문에 쉽게 모킹이 가능했기 때문이다.&lt;/span&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;span style=&quot;background-color: #ffffff; color: #292929; text-align: left;&quot;&gt;다른 기술블로그에서는 모듈을 교체할 때 큰 효과를 봤다고 설명한다. 새로운 ORM으로 바꿔끼울 때 그저 도메인 로직은 인터페이스인 Port만 의존하고 있었기 때문에 그 구현체만 바꿔끼우면 쉽게 교체가 가능하다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvVNXZ/btsLRinsVYr/KnsxrxOKUqty3XZ5ktgh80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvVNXZ/btsLRinsVYr/KnsxrxOKUqty3XZ5ktgh80/img.png&quot; data-alt=&quot;모듈 교체하기 (네이버페이 공식 블로그)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvVNXZ/btsLRinsVYr/KnsxrxOKUqty3XZ5ktgh80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvVNXZ%2FbtsLRinsVYr%2FKnsxrxOKUqty3XZ5ktgh80%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;700&quot; height=&quot;188&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;188&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모듈 교체하기 (네이버페이 공식 블로그)&lt;/figcaption&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;물론 헥사고날 아키텍처가 무조건 좋다는 것은 아니다. 장점이 있는만큼 단점도 있다.&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: #8a8a8a; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: inherit;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292929;&quot;&gt;웹 계층: ProductResponse&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: inherit;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292929;&quot;&gt;어플리케이션 계층: Product&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: inherit;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292929;&quot;&gt;영속성 계층: ProductRow&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292929;&quot;&gt;위와 같이 계층별로 모델이 필요하다. 계층끼리 영향을 최대한 받지 않아야 하고 웹 계층의 모델이 바뀐다고해서 어플리케이션 계층의 모델이 바뀌면 안되기 때문이다. 그치만 이렇게 하면 만약 계층별 모델이 거의 비슷하다면 코드의 중복이 많다고 느껴질 수도 있고, 모델별로 변환하는 로직이 불필요하다는 생각이 들 수도 있다. 기존의 레이어드 아키텍처처럼 계층끼리 서로 공유하는 모델이 더 낫다고 느껴질 수도 있다. 그렇지만 그렇게 바꾸면 클린 아키텍처의 큰 의미인 의존성을 최소화하고, 외부의 영향을 받지 않도록 해야한다는 것이 사라진다는 생각이 들었다. 그럼 결국 클라이언트가 바뀌면 도메인 로직이 바뀔 수 있을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #292929; text-align: left;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reference&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.naver.com/naverfinancial/223155125321&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.naver.com/naverfinancial/223155125321&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737111986041&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;프로젝트에 새로운 아키텍처 적용하기 | 네이버파이낸셜 기술블로그&quot; data-og-description=&quot;네이버페이는 외부 쇼핑몰이 주문형페이의 시스템을 이용해서 결제 및 주문을 진행할 수 있는 프로세스를 ...&quot; data-og-host=&quot;blog.naver.com&quot; data-og-source-url=&quot;https://blog.naver.com/naverfinancial/223155125321&quot; data-og-url=&quot;https://blog.naver.com/naverfinancial/223155125321&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/hwGOk/hyX0zYbKZu/08glY7AUAhWZBcbfmWWLck/img.jpg?width=600&amp;amp;height=600&amp;amp;face=0_0_600_600&quot;&gt;&lt;a href=&quot;https://blog.naver.com/naverfinancial/223155125321&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.naver.com/naverfinancial/223155125321&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/hwGOk/hyX0zYbKZu/08glY7AUAhWZBcbfmWWLck/img.jpg?width=600&amp;amp;height=600&amp;amp;face=0_0_600_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트에 새로운 아키텍처 적용하기 | 네이버파이낸셜 기술블로그&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;네이버페이는 외부 쇼핑몰이 주문형페이의 시스템을 이용해서 결제 및 주문을 진행할 수 있는 프로세스를 ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;a href=&quot;https://velog.io/@coconenne/%ED%97%A5%EC%82%AC%EA%B3%A0%EB%82%A0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%96%B4%EB%8C%91%ED%84%B0%EC%99%80-%ED%8F%AC%ED%8A%B8-%EA%B2%B0%ED%95%A9%EB%8F%84%EB%A5%BC-%EB%82%AE%EC%B6%B0%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@coconenne/%ED%97%A5%EC%82%AC%EA%B3%A0%EB%82%A0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%96%B4%EB%8C%91%ED%84%B0%EC%99%80-%ED%8F%AC%ED%8A%B8-%EA%B2%B0%ED%95%A9%EB%8F%84%EB%A5%BC-%EB%82%AE%EC%B6%B0%EB%B3%B4%EC%9E%90&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737111992350&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;헥사고날 아키텍처: 어댑터와 포트! 결합도를 낮춰보자&quot; data-og-description=&quot;학습 계기 최근 항상 의문이었던 계층간 책임에 대해 자세히 알아보기 시작하였다. 전통적인 웹-도메인-영속성 구조에서는 한 계층의 변화가 다른 계층에도 영향을 끼칠 수 있다는 것을 알게 됐&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@coconenne/%ED%97%A5%EC%82%AC%EA%B3%A0%EB%82%A0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%96%B4%EB%8C%91%ED%84%B0%EC%99%80-%ED%8F%AC%ED%8A%B8-%EA%B2%B0%ED%95%A9%EB%8F%84%EB%A5%BC-%EB%82%AE%EC%B6%B0%EB%B3%B4%EC%9E%90&quot; data-og-url=&quot;https://velog.io/@coconenne/헥사고날-아키텍처-어댑터와-포트-결합도를-낮춰보자&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bC7cvd/hyX0Apf7Sv/WCrLHSonJMCHWto3w9wITk/img.png?width=1892&amp;amp;height=944&amp;amp;face=0_0_1892_944&quot;&gt;&lt;a href=&quot;https://velog.io/@coconenne/%ED%97%A5%EC%82%AC%EA%B3%A0%EB%82%A0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%96%B4%EB%8C%91%ED%84%B0%EC%99%80-%ED%8F%AC%ED%8A%B8-%EA%B2%B0%ED%95%A9%EB%8F%84%EB%A5%BC-%EB%82%AE%EC%B6%B0%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@coconenne/%ED%97%A5%EC%82%AC%EA%B3%A0%EB%82%A0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%96%B4%EB%8C%91%ED%84%B0%EC%99%80-%ED%8F%AC%ED%8A%B8-%EA%B2%B0%ED%95%A9%EB%8F%84%EB%A5%BC-%EB%82%AE%EC%B6%B0%EB%B3%B4%EC%9E%90&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bC7cvd/hyX0Apf7Sv/WCrLHSonJMCHWto3w9wITk/img.png?width=1892&amp;amp;height=944&amp;amp;face=0_0_1892_944');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;헥사고날 아키텍처: 어댑터와 포트! 결합도를 낮춰보자&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;학습 계기 최근 항상 의문이었던 계층간 책임에 대해 자세히 알아보기 시작하였다. 전통적인 웹-도메인-영속성 구조에서는 한 계층의 변화가 다른 계층에도 영향을 끼칠 수 있다는 것을 알게 됐&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>3계층구조와차이</category>
      <category>클린아키텍처</category>
      <category>헥사고날</category>
      <category>헥사고날아키텍처</category>
      <author>심심231</author>
      <guid isPermaLink="true">https://simsim231.tistory.com/296</guid>
      <comments>https://simsim231.tistory.com/296#entry296comment</comments>
      <pubDate>Fri, 17 Jan 2025 00:53:54 +0900</pubDate>
    </item>
    <item>
      <title>웹 어플리케이션에서의 동시성 제어 (feat. Lock)</title>
      <link>https://simsim231.tistory.com/295</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;화면에서 사용자가 &lt;b&gt;버튼을 N번 연달아 클릭&lt;/b&gt;했다고 생각해보자. API가 연달아 호출된다. 만약 DB에 insert, update, delete하는 로직이 있었다면 &lt;b&gt;같은 데이터가 N개 insert되는 등의 동시성 이슈&lt;/b&gt;가 발생한다. 물론 화면에서 더블클릭을 막는 등 여러 방법이 있겠지만 이번 포스팅에서는 백엔드 서버에서 Lock을 통해 해결하는 방식에 대해 알아본다.&lt;/blockquote&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;사실 Lock에 대해서는 이전에 운영체제(OS)를 다루면서 포스팅한 적이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://simsim231.tistory.com/134&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://simsim231.tistory.com/134&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1732162165415&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[OS] Process 동기화(1)&quot; data-og-description=&quot;[OS] Process 동기화(1) Process 동기화란 여러 프로세스가 공유하는 자원의 일관성을 유지하는 것 임계구역(Critical Section) 문제 do { entry section critical section exit section remainder section } while(TRUE); 임계구역&quot; data-og-host=&quot;simsim231.tistory.com&quot; data-og-source-url=&quot;https://simsim231.tistory.com/134&quot; data-og-url=&quot;https://simsim231.tistory.com/134&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bzbWeI/hyXzUVShSy/N6gWlz6nUsWJ9R2H1UtP10/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dtKWU0/hyXDdM0Aoi/64MXtauPLz0VE3zaGU429k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://simsim231.tistory.com/134&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://simsim231.tistory.com/134&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bzbWeI/hyXzUVShSy/N6gWlz6nUsWJ9R2H1UtP10/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dtKWU0/hyXDdM0Aoi/64MXtauPLz0VE3zaGU429k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[OS] Process 동기화(1)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[OS] Process 동기화(1) Process 동기화란 여러 프로세스가 공유하는 자원의 일관성을 유지하는 것 임계구역(Critical Section) 문제 do { entry section critical section exit section remainder section } while(TRUE); 임계구역&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;simsim231.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Critical Section, DeadLock 등 공통된 개념들이 나오는데, 이번 포스팅에서는 OS에서의 동시성 제어가 아닌, 웹 어플리케이션에서의 동시성 제어에 좀 더 집중해서 작성한다.&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;웹 어플리케이션에서 우리는 API가 연달아 호출되면 발생하는 이슈를 Lock을 통해 막아야 한다. 그럼 여러 방식중에 선택을 해야하는데, 바로 생각해볼 수 있는 것은 2가지다. 데이터를 읽는 시점에 락을 거는 &lt;b&gt;비관적 락&lt;/b&gt;, 그리고 충돌이 발생했을 때 이를 감지하고 처리하는&amp;nbsp;&lt;b&gt;낙관적 락&lt;/b&gt;. 이 둘은 충돌을 미리 막는지, 혹은 발생했을 때 처리하는지 그 시점의 차이라고 할 수 있다. 이 둘에 대해 자세히 알아보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비관적 락&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락은 &lt;b&gt;동시 업데이트가 빈번하게 발생할 것이라고 '비관적으로' 가정하고 데이터를 읽는 시점에 락을 걸어 다른 트랜잭션의 접근을 차단하는 락&lt;/b&gt;이다. 이 방식은 &lt;u&gt;데이터 무결성을 강하게 보장하지만, DeadLock이 발생하는 등 동시성 처리 성능이 낮아질 수 있다&lt;/u&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;DBMS&lt;/b&gt;에서 제공하는 다양한 메커니즘을 사용해 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 시작(Auto Commit 비활 or BEGIN or START TRANSACTION)될 때&lt;b&gt; Shared Lock&lt;/b&gt; 또는 &lt;b&gt;Exclusive Lock&lt;/b&gt;을 걸고 시작한다. 트랜잭션이 커밋되거나 롤백되어 완료가 되면 Lock이 반환된다. 혹은 UNLOCK TABLE 키워드로 명시적으로 해제할 수도 있다.&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;Shared Lock 공유잠금&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;u&gt;읽기 잠금&lt;/u&gt;이다.&lt;/li&gt;
&lt;li&gt;리소스를 다른 사용자가 &lt;b&gt;동시에 읽을 수 있지만 변경은 불가&lt;/b&gt;하게 하는 것이다.&lt;/li&gt;
&lt;li&gt;어떤 자원에 Shared lock이 하나라도 걸려있으면 Exclusive lock을 걸 수 없다. (Shared lock은 여러 개 걸릴 수 있음)&lt;/li&gt;
&lt;li&gt;일반적인 SELECT 쿼리는 락을 사용하지 않고 DB를 읽지만 (그래서 Shared Lock이나 Exclusive Lock이 걸려있는 Row도 그냥 읽을 수 있음), SELECT ... FOR SHARE 등 일부 SELECT 쿼리는 특정 Row를 읽을 때 각각 Row에 Shared Lock을 건다.&lt;b&gt;&lt;/b&gt;&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;&lt;b&gt;Exclusive Lock 배타적 잠금&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;u&gt;쓰기 잠금&lt;/u&gt;이다.&lt;/li&gt;
&lt;li&gt;리소스를 다른 사용자가 &lt;b&gt;동시에 읽지도, 변경하지도 못하게&lt;/b&gt; 한다.&lt;/li&gt;
&lt;li&gt;Exclusive Lock에 걸리면 Shared Lock, 다른 Exclusive Lock을 걸 수 없다.&lt;/li&gt;
&lt;li&gt;SELECT ... FOR UPDATE 나 UPDATE, DELETE 등의 수정 쿼리를 날릴 때 각 Row에 걸리는 Lock이다.&amp;nbsp;&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;&lt;b&gt;DBMS에서 제공하는 Lock&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 서버 전체에 락을 거는 것&lt;/li&gt;
&lt;li&gt;테이블 락(TM): 개별 테이블 단위로 설정되는 락&lt;/li&gt;
&lt;li&gt;네임드 락: 임의의 문자열에 대한 잠금 설정. 분산락 구현에 활용될 수 있음&lt;/li&gt;
&lt;li&gt;레코드 락(TX): 레코드 자체만을 잠그는 것&lt;/li&gt;
&lt;li&gt;갭 락: 레코드 자체가 아닌 레코드와 바로 인접한 레코드 사이의 간격만을 잠그는 것. 레코드와 레코드 사이의 간격에 새로운 레코드가 생성(insert) 되는 것을 제어한다.&lt;/li&gt;
&lt;li&gt;넥스트 키 락: 레코드 락과 갭 락을 합쳐놓은 형태이다. 바이너리 로그에 기록되는 쿼리가 replica server에서 실행될 때 source server에서 만들어낸 결과와 동일한 결과를 만들어 내도록 보장해주는 것이다.&lt;/li&gt;
&lt;li&gt;Auto Increment Lock: AUTO_INCREMENT 칼럼이 적용된 테이블에 동시에 여러 레코드가 insert되는 경우, 저장되는 각 레코드는 중복되지 않고 순서대로 증가하는 일련번호를 가져야 하기 때문에 내부적으로 auto_increment lock이라는 테이블 수준의 lock을 사용한다. 트랜잭션과 관계 없이 auto_increment 값 가져오는 순간만 락이 걸렸다가 즉시 해제되며, 테이블에서 Lock은 단 하나만 존재한다.&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;그 외에 &lt;b&gt;프로그래밍에서도 비관적 락 구현을 제공&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;JPA에서는 Annotation으로 간단하게 비관적 락을 적용할 수 있다.&amp;nbsp;&lt;br /&gt;@Lock(LockModeType.PESSIMISTIC_WRITE)&lt;/li&gt;
&lt;li&gt;Java에서 제공하는 synchronized 키워드나 ReentrantLock 클래스를 사용해 비관적 락을 구현할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1732796552639&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** Synchronized 예시 **/
public synchronized void criticalSection() {
 // 이 코드 블록은 한 번에 하나의 스레드만 접근 가능
}

/** ReentrantLock 예시 **/
ReentrantLock lock = new ReentrantLock();

lock.lock(); // 락 획득
try {
    // critical section
} finally {
    lock.unlock(); // 락 해제
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;낙관적 락&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙관적 락은 데이터 수정 시 &lt;b&gt;충돌이 발생하지 않을 것이라고 이름 그대로 '낙관적으로' 가정하고, 충돌이 발생했을 때 이를 감지하고 처리&lt;/b&gt;한다. 이는 직접적으로 DB에 락을 거는 방법이 아니기에 &lt;u&gt;간단하고 성능에 유리하지만, 높은 동시성 환경에서는 오히려 재시도 비용이 증가하는 등 한계&lt;/u&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;낙관적 락은 어플리케이션에서 version과 같은 컬럼값을 통해 제어한다. 내가 먼저 이 값을 수정했다고 명시해서 다른 사람이 동일한 조건으로 값을 수정할 수 없게 하는 것이다. (Hashcode나 Timestamp를 이용하기도 한다.)&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;예를 들어, A였던 데이터를 B로 바꿔주면서 Version2로 바꿔주고 다른 트랜잭션에서 C로 바꿔주면서 Version2로 바꿔준다면 이미 Version2로 바껴있기 때문에 업데이트가 되지 않을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비관적 락 vs 낙관적 락&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;낙관적 락은 데이터 업데이트 전의 조회와 Lock 작업, 그리고 트랜잭션이 필요없다. 성능적으로 비관적 락보다 좋다.&lt;/li&gt;
&lt;li&gt;낙관적 락은 롤백이 어렵다. 개발자가 수동으로 롤백처리 해줘야 한다. 비관적 락은 트랜잭션을 롤백하면 된다.&lt;/li&gt;
&lt;li&gt;결론적으로 낙관적 락은 충돌이 많이 예상되거나 충돌이 발생했을 때 비용이 많이 들 것이라고 판단되는 곳에서는 사용하지 않는 것이 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;분산 락&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 락들 중에서 '분산 락'이라는 것이 있다. 분산 락은 &lt;b&gt;스케일 아웃된 DB 확경 또는 여러대의 서버가 있을 때 사용하는 락&lt;/b&gt;이다. 즉, 비관적 락의 확장된 형태로 볼 수 있다. 분산 락은 락을 획득한 프로세스 혹은 스레드만이 공유 자원 혹은 Critical Section에 접근할 수 있도록 한다. 구현하는 방법으로는 ZooKeeper, MySQL, Redis 등이 있다.&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;MySQL로 구현하는 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MySQL의 네임드락&lt;/b&gt;: 락을 자동으로 반납할 수 없어 명시적으로 락을 release 시켜야하는 방식. DB에서 락을 관리하기 때문에 부담이 있음. 락 획득 시도는 스핀락(Race Condition 상황에서 Lock이 반환될 때까지 프로세스가 재시도하며 대기하는 상태)으로 구현해야하기 때문에 WAS에도 부담이 있음. 그리고 connection pool 따로 관리 필요.&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(str, timeout): 1 (잠금 획득 성공), 0 (timeout초 동안 잠금 획득 실패), null (잠금 획득 중 에러)&lt;/li&gt;
&lt;li&gt;RELEASE_LOCK(str): 1 (잠금 해제 성공), 0 (잠금 해제되지 않음. 현재 쓰레드에서 획득한 잠금이 아닌 경우), null (잠금 존재하지 않음)&lt;/li&gt;
&lt;li&gt;RELEASE_ALL_LOCKS(): 현재 세션에서 유지되고 있는 모든 잠금을 해제하고 해제한 잠금 갯수를 반환&lt;/li&gt;
&lt;li&gt;IS_FREE_LOCK(str): 잠금 획득 가능여부 확인. 1 (입력한 이름의 잠금이 없음), 0 (입력한 이름의 잠금이 있음). null (에러 발생)&lt;/li&gt;
&lt;li&gt;IS_USED_LOCK(str): 잠금이 사용중인지 확인. 잠금 존재시 connection id를 반환하고 없으면 null 반환&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;&lt;b&gt;구현 예제&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732719593201&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class UserLevelLockFinal {

private static final String GET_LOCK = &quot;SELECT GET_LOCK(?, ?)&quot;;
private static final String RELEASE_LOCK = &quot;SELECT RELEASE_LOCK(?)&quot;;
private static final String EXCEPTION_MESSAGE = &quot;LOCK 을 수행하는 중에 오류가 발생하였습니다.&quot;;

private final DataSource dataSource;

public UserLevelLockFinal(DataSource dataSource) {
    this.dataSource = dataSource;
}

public &amp;lt;T&amp;gt; T executeWithLock(String userLockName,
                             int timeoutSeconds,
                             Supplier&amp;lt;T&amp;gt; supplier) {

    try (Connection connection = dataSource.getConnection()) {
        try {
            log.info(&quot;start getLock=[], timeoutSeconds ], connection=[]&quot;, userLockName, timeoutSeconds, connection);
            getLock(connection, userLockName, timeoutSeconds);
            log.info(&quot;success getLock=[], timeoutSeconds ], connection=[]&quot;, userLockName, timeoutSeconds, connection);
            return supplier.get();

        } finally {
            log.info(&quot;start releaseLock=[], connection=[]&quot;, userLockName, connection);
            releaseLock(connection, userLockName);
            log.info(&quot;success releaseLock=[], connection=[]&quot;, userLockName, connection);
        }
    } catch (SQLException | RuntimeException e) {
        throw new RuntimeException(e.getMessage(), e);
    }
}

private void getLock(Connection connection,
                     String userLockName,
                     int timeoutseconds) throws SQLException {

    try (PreparedStatement preparedStatement = connection.prepareStatement(GET_LOCK)) {
        preparedStatement.setString(1, userLockName);
        preparedStatement.setInt(2, timeoutseconds);

        checkResultSet(userLockName, preparedStatement, &quot;GetLock_&quot;);
    }
}

private void releaseLock(Connection connection,
                         String userLockName) throws SQLException {
    try (PreparedStatement preparedStatement = connection.prepareStatement(RELEASE_LOCK)) {
        preparedStatement.setString(1, userLockName);

        checkResultSet(userLockName, preparedStatement, &quot;ReleaseLock_&quot;);
    }
}

private void checkResultSet(String userLockName,
                            PreparedStatement preparedStatement,
                            String type) throws SQLException {
    try (ResultSet resultSet = preparedStatement.executeQuery()) {
        if (!resultSet.next()) {
            log.error(&quot;USER LEVEL LOCK 쿼리 결과 값이 없습니다. type = [], userLockName ], connection=[]&quot;, type, userLockName, preparedStatement.getConnection());
            throw new RuntimeException(EXCEPTION_MESSAGE);
        }
        int result = resultSet.getInt(1);
        if (result != 1) {
            log.error(&quot;USER LEVEL LOCK 쿼리 결과 값이 1이 아닙니다. type = [], result ] userLockName ], connection=[]&quot;, type, result, userLockName, preparedStatement.getConnection());
            throw new RuntimeException(EXCEPTION_MESSAGE);
        }
    }
}
}&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;DataSource 를 주입받아 JDBC 를 이용하여 직접 구현하여 Connection 을 직접 관리 &lt;br /&gt;&amp;rarr; GET_LOCK() 과 REALSE_LOCK() 모두 동일한 Connection 을 사용&lt;/li&gt;
&lt;li&gt;위 코드에서, Lock을 획득하여 작업한 스레드의 트랜잭션이 커밋되기 전에 lock이 해제되고 lock이 해제되어 다음 스레드가 트랜잭션에서 작업을 시작하면 문제가 되기 때문에 트랜잭션 커밋 후 lock을 해제해야함 &lt;br /&gt;&amp;rarr; AOP로 Lock을 걸고 푸는 것의 관심사를 분리하고, Transaction의 전파수준을 REQUIRES_NEW로 설정해서 분산 락의 트랜잭션을 분리하는 것으로 개선할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Redis로 구현하는 방법&lt;/h4&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;&lt;b&gt;Redis 구현체&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;Jedis: 성능이 좋지 않음&lt;/li&gt;
&lt;li&gt;Lettuce: setnx(SET if Not eXists 값이 존재하는지 확인 &amp;rarr; 없으면 값 세팅. 레디스에서 제공하는 원자적 연산)를 활용한 스핀락 사용&lt;/li&gt;
&lt;li&gt;Redisson: pubsub 방식 사용. 리소스 절약할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Redis 사용하는 이유?&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;Lock을 구현하는 여러 기능들을 제공한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간단하게 Redis의 명령어를 사용하여 Lock을 구현할 수도 있다.&lt;/li&gt;
&lt;li&gt;Pub/Sub 구조로 Lock을 구현할 수도 있다.&lt;/li&gt;
&lt;li&gt;retry, timeout과 같은 부가 기능들을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MySQL에서도 네임드 락으로 분산 락을 구현할 수 있지만, Redis에 비해 단점이 존재한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 중요 비즈니스 데이터가 존재하는 DB에서 Lock을 관리하기 때문에 부담이 있다.&lt;/li&gt;
&lt;li&gt;Lock을 자동으로 해제할 수 없어서 명시적으로 해제해줘야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Redis의 구조가 싱글 스레드의 Multiplexing 기술을 사용해서 원자성 보장에 적합하다.
&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;/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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reference&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://juno-juno.tistory.com/111&quot;&gt;https://juno-juno.tistory.com/111&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sabarada.tistory.com/175&quot;&gt;https://sabarada.tistory.com/175&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@a01021039107/분산락으로-해결하는-동시성-문제이론편&quot;&gt;https://velog.io/@a01021039107/분산락으로-해결하는-동시성-문제이론편&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/2631/&quot;&gt;https://techblog.woowahan.com/2631/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ksh-coding.tistory.com/150&quot;&gt;https://ksh-coding.tistory.com/150&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://helloworld.kurly.com/blog/distributed-redisson-lock/&quot;&gt;https://helloworld.kurly.com/blog/distributed-redisson-lock/&lt;/a&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>낙관적락</category>
      <category>분산락</category>
      <category>비관적락</category>
      <author>심심231</author>
      <guid isPermaLink="true">https://simsim231.tistory.com/295</guid>
      <comments>https://simsim231.tistory.com/295#entry295comment</comments>
      <pubDate>Thu, 28 Nov 2024 21:40:02 +0900</pubDate>
    </item>
    <item>
      <title>MSA 환경에서 서비스간 데이터 정합성 맞추기 (SAGA Pattern, Outbox Pattern)</title>
      <link>https://simsim231.tistory.com/294</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;MSA(Microservice Architecture) 환경에서 서비스간 데이터 정합성은 어떻게 맞추는지 고민한 내용을 정리해본다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;문제상황 (예시)&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;전제&lt;/b&gt;&lt;br /&gt;- 모놀리틱 구조에서 MSA 환경으로의 변경으로 주문 서비스, 배송 서비스를 분리&lt;br /&gt;- MSA 환경에서 서비스간 결합도가 낮은 방식으로 통신하기 위해 비동기 방식인 Kafka 사용&lt;br /&gt;&lt;br /&gt;&lt;b&gt;메인 로직&lt;/b&gt;&lt;br /&gt;주문완료(주문서비스) -&amp;gt; 주문완료 이벤트 발행(Kafka) -&amp;gt; 배송정보 생성(배송서비스)&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;위와 같은 상황에서 이런 의문이 든다. 주문서비스와 배송서비스가 분리된 환경에서 &lt;b&gt;주문완료는 성공&lt;/b&gt;했는데 &lt;b&gt;배송정보 생성이 실패&lt;/b&gt;하면 이전에 성공한 로직은 어떻게 처리해줄까? (분리된 서비스의 트랜잭션은 어떻게 처리해주는게 좋을까)&lt;br /&gt;&lt;br /&gt;간단하게 생각해보자. 배송정보 생성이 실패했다? 그럼 그 전에 실행했던 것들도 다 취소해주면 되지 않을까? 그럼 배송정보 생성을 위해 이벤트를 발행했던 것처럼 취소를 위한 이벤트도 발행해주면 되지 않을까? 이처럼 배송정보 생성이 실패했을 때 주문완료 취소 로직(보상 트랜잭션)을 실행해주는&lt;b&gt; &lt;/b&gt;트랜잭션 관리 패턴을 &lt;b&gt;SAGA 패턴&lt;/b&gt;이라고 한다.&lt;br /&gt;(Two Phase Commit 등 다른 방식도 있다.)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;SAGA Pattern&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;SAGA 패턴은 연속된 개별 서비스의 로컬 트랜잭션이 이어져 전체 비즈니스 트랜잭션을 구성한다. 각 서비스가 로컬 트랜잭션을 실행하고 이벤트를 게시해서 다음 로컬 트랜잭션 실행을 트리거하며, 연속적으로 로컬 트랜잭션을 실행하다가 중간에 문제가 생기면 이전의 로컬 트랜잭션에 의해 변경했던 사항을 취소하는 보상 트랜잭션을 실행한다. 주문완료 후 배송정보 생성이 실패한 경우 실패 이벤트를 발행해 주문완료를 취소하는 것이다.&lt;br /&gt;&lt;br /&gt;SAGA 패턴의 핵심은 &lt;b&gt;트랜잭션 관리의 주체가 DBMS 가 아닌 서비스에 있다는 것&lt;/b&gt;이다. 서비스 별로 따로 존재하는 DB는 각각의 로컬 트랜잭션만 담당한다. 즉, DBMS가 해주던 롤백 처리(보상 트랜잭션)를 서비스단에서 구현해준다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;SAGA를 이해하기 위해 어떤 말의 약자일까 찾아봤지만 SAGA는 약자가 아닌, 특정 논문에서 언급되어 시작된 일종의 신조어라고 한다. 추측하기로는 Long Lived Transaction이 마치 중세 아이슬란드 문학의 산문과 '말해진 것, 말로 전하다' 라는 뜻의 '사가'(Saga)에서 온것 같다고 스택오버플로우 답변자가 말한다.&lt;br /&gt;&lt;br /&gt;참고: &lt;a href=&quot;https://krksap.tistory.com/2113&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://krksap.tistory.com/2113&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Orchestration과 Choreography 두가지 구현 방식이 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;587&quot; data-origin-height=&quot;259&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgA5oR/btsJ4upNdY2/oqleMmcPFLd7k32uMoTpmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgA5oR/btsJ4upNdY2/oqleMmcPFLd7k32uMoTpmK/img.png&quot; data-alt=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=165445&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgA5oR/btsJ4upNdY2/oqleMmcPFLd7k32uMoTpmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgA5oR%2FbtsJ4upNdY2%2FoqleMmcPFLd7k32uMoTpmK%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;587&quot; height=&quot;259&quot; data-origin-width=&quot;587&quot; data-origin-height=&quot;259&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://devocean.sk.com/blog/techBoardDetail.do?ID=165445&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;Orchestration 방식&lt;/b&gt;은 중앙(manager)에서 트랜잭션을 통제하는 방식으로 manager에 의해 트랜잭션이 생성 및 종료가 되고 manager에 의해 보상 트랜잭션이 실행된다. 이는 다른 서비스들과 manager 서비스간에 의존도가 높다는 단점이 있고 별도로 중앙 서비스를 만들어줘야하기 때문에 구성이 어렵다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Cheoreography 방식&lt;/b&gt;은 비동기 방식 기반으로 각 서비스 별로 트랜잭션을 관리하는 로직이 있다. 서비스 로직이 성공하는 경우 그 서비스에서 다음 트랜잭션 실행을 트리거하고, 실패하는 경우 그 서비스에서 보상 트랜잭션 실행을 트리거한다. 이는 중앙 서비스가 없기 때문에 구성이 쉽고 연계 서비스의 변경이 쉽다는 장점이 있다. 다만, 중심이 없어 trouble shooting이 어렵다는 단점이 있다.&lt;br /&gt;&lt;br /&gt;Cheoreography 방식으로 예시는 다음과 같다.&lt;br /&gt;주문이 완료되면 주문서비스에서 주문완료 이벤트를 발행하고, 배송서비스에서는 이 이벤트를 수신해서 배송정보를 생성한다. 만약 배송정보 생성이 실패한다면 배송 서비스에서 실패 이벤트를 발행해서 주문완료를 취소한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;Transactional Outbox Pattern&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위에서 보면 우리가 &amp;ldquo;주문완료&amp;rdquo; 와 &amp;ldquo;주문완료 이벤트 발행&amp;rdquo;을 원자적으로 실행해줘야하는 것을 알 수 있다. 주문완료가 됐는데 주문완료 이벤트 발행이 실패하게 되면 그 이후 처리에 문제가 생길 것이기 때문이다. (주문은 들어왔는데 배송이 안된다던지..)&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이처럼 DB 업데이트가 발생하는 도메인 로직과 이벤트 발행을 원자적으로 실행하는 것을 &lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #242424;&quot;&gt;트랜잭셔널 메시징(Transactional Messaging) 이라고 한다. 트랜잭셔널 메시징을 구현하는 방법은 크게 2가지가 있다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #242424;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;트랜잭셔널 아웃박스 패턴 (Transactional Outbox Pattern)&lt;/li&gt;
&lt;li&gt;변경 데이터 캡쳐 (Change Data Capture)&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그중에서 사용했던 트랜잭셔널 아웃박스 패턴에 대해 알아보자. &lt;br /&gt;&lt;br /&gt;트랜잭셔널 아웃박스 패턴은 &lt;b&gt;DB를 업데이트하는 트랜잭션 안에서 발행해야할 메시지를 DB에 저장하고, 별도의 프로세스가 DB에 저장된 이벤트를 읽어서 메시지 브로커에 전송하는 것&lt;/b&gt;을 말한다.&lt;br /&gt;&lt;br /&gt;좀 더 자세하게 구현방식에 대해 얘기해보자. 여기서는 다음과 같이 구현해줬다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;DB를 업데이트(&lt;b&gt;주문완료&lt;/b&gt;)하는 트랜잭션 안에서 발행해야할 메시지(&lt;b&gt;주문완료 이벤트 발행 정보&lt;/b&gt;)를 DB(&lt;b&gt;outbox 테이블&lt;/b&gt;)에 저장하고, 별도의 프로세스(&lt;b&gt;Spring의 Event - @TransactionalEventListener&lt;/b&gt;)가 DB에 저장된 이벤트를 읽어서 메시지 브로커(&lt;b&gt;Kafka&lt;/b&gt;)에 전송하는 것&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;1. 주요한 도메인 로직(ex. 주문완료)&lt;/b&gt;&lt;br /&gt;ㄴ @Transactional 선언&lt;br /&gt;ㄴ 주문완료 처리 로직&lt;br /&gt;ㄴ Spring의 ApplicationEventPublisher.publishEvent()를 활용하여 주문완료 이벤트 발행하는 로직&lt;br /&gt;ㄴ Transactional로 묶었기 때문에 위의 2가지 로직들이 모두 성공하거나 모두 실패함&lt;br /&gt;&lt;br /&gt;&lt;b&gt;2. outbox 테이블에 주문 완료 이벤트 저장 (이벤트 발행 요청 기록)&lt;/b&gt;&lt;br /&gt;ㄴ 별도의 Listener로 분리. 1번에서 발행한 이벤트 구독&lt;br /&gt;ㄴ 이벤트 발행 상태를 init으로 저장&lt;br /&gt;ㄴ @TransactionalEventListener(phase=BEFORE_COMMIT) -&amp;gt; 1번에서 트랜잭션 커밋이 되기 전에 outbox 테이블에 주문완료 이벤트가 저장된다)&lt;br /&gt;ㄴ 별도의 listener로 뺀 이유: 도메인 로직과 별개로 그 이후 로직은 이벤트 구독 처리로 분리해서 일관되게 개발할 수 있음. 도메인 로직은 도메인의 요구사항에만 집중할 수 있음. 확장성과 응집성이 더 높은 방식.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;3. 주문완료 이벤트를 리스닝하는 메서드 구현 (Polling Publisher Pattern)&lt;/b&gt;&lt;br /&gt;ㄴ 별도의 Listener로 분리. 1번에서 발행한 이벤트 구독&lt;br /&gt;ㄴ @TransactionalEventListener(phase=AFTER_COMMIT) 선언 (1, 2번에서 트랜잭션 커밋이 실행된 이후에만 리스너가 동작하게됨. 즉, &amp;rdquo;도메인 로직 + 주문완료 이벤트 발행&amp;ldquo;이 오류없이 온전히 실행되었을 때만 메서드가 실행됨)&lt;br /&gt;ㄴ Kafka의 주문완료 Topic에 메시지 발행하는 부분 + 완료시 이벤트 발행 상태를 send_success로 업데이트&lt;br /&gt;ㄴ 근데 이 리스너 메서드가 이벤트를 수신해서 카프카로 메시지를 발행할 때 카프카 클러스터 장애 또는 네트워크 상의 장애로 메시지 발행이 실패할 수 있음. 이때 1번의 outbox 테이블에 기록된 메시지 발행 정보를 주기적으로 체크하면서 재발행을 시도하는 (ex. init 상태인 것들만 재처리하는 재처리 배치) 별도의 메시지 릴레이 로직을 통해 카프카로 메시지 발행을 다시 시도할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;이렇게 하면 (1번 + 2번)과 3번이 성공 or 실패하는 경우 모두 처리할 수 있다. (=원자성 보장)&lt;/b&gt;&lt;br /&gt;1. (1번 + 2번) 성공 3번 성공 -&amp;gt; 정상&lt;br /&gt;2. (1번 + 2번) 실패 3번 성공 -&amp;gt; 1번에서 트랜잭션 commit이 실패하면 TransactionalEventListener에서 이벤트를 리스닝 하지 않기 때문에 발생할 수 없는 케이스이다.&lt;br /&gt;3. (1번 + 2번) 성공 3번 실패 -&amp;gt; TransactionalEventListener에서 카프카에 메시지를 발행하는 과정이 실패한 케이스인데, outbox 테이블에 발행할 이벤트 정보를 기록해놨기 때문에 일정 주기로 동작하는 배치 등을 통해 카프카 메시지 발행을 다시 실행할 수 있음&lt;br /&gt;4. (1번 + 2번) 실패 3번 실패 -&amp;gt; 정상. 또 다른 서비스에서 트리거된 이벤트였을 경우 앞단에서 동일하게 처리할 것임.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;구현 예시&lt;/h4&gt;
&lt;pre class=&quot;Java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;Java&quot;&gt;&lt;code&gt;// 1번
@Transactional
public void order(TestDto testDto) {
	// 1. 도메인 로직
	createOrder(testDto);

	// 2. 이벤트 발행
	eventService.eventPublish(testDto);
}

@EventHandler
@RequiredArgsConstructor
public class OrderEventListener {

	private fianl EventRecorder eventRecorder;
	private final KafkaService kafkaService;
	
	// 2번 - outbox 테이블에 이벤트 기록
	@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
	public void eventRecordHandler(TestEvent testEvent) {
		eventRecorder.save(testEvent);
	}

	// 3번 - 카프카에 메시지 전송, outbox 테이블 상태 업데이트
	@Async(EVENT_ASYNC_TASK_EXECUTOR)
	@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
	public void sendKafkaHandler(TestEvent testEvent) {
		kafkaService.send(testEvent);
	}
}&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;이렇게 MSA에서 전체 서비스의 데이터 정합성은 어떻게 맞춰주는지 (SAGA 패턴), 좀 더 세부적인 구현으로 들어가서 이벤트 발행의 원자성은 어떻게 보장해주는지(Outbox 패턴) 알아봤다.&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;Reference&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/7835/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://techblog.woowahan.com/7835/&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;회원시스템 이벤트기반 아키텍처 구축하기 | 우아한형제들 기술블로그&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;최초의 배달의민족은 하나의 프로젝트로 만들어졌습니다. 배달의민족의 주문수는 J 커브를 그리는 빠른 속도로 성장했고, 주문수가 커지면서 자연스럽게 트래픽 또한 매우 커졌습니다. 하나의 &quot; data-og-host=&quot;techblog.woowahan.com&quot; data-og-source-url=&quot;https://techblog.woowahan.com/7835/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dSVCau/hyXee61cgw/gQKCfGgUTqjgSWVfrdHQwK/img.jpg?width=1640&amp;amp;height=856&amp;amp;face=0_0_1640_856&quot; data-og-url=&quot;https://techblog.woowahan.com/7835/&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/7835/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://techblog.woowahan.com/7835/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dSVCau/hyXee61cgw/gQKCfGgUTqjgSWVfrdHQwK/img.jpg?width=1640&amp;amp;height=856&amp;amp;face=0_0_1640_856');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;회원시스템 이벤트기반 아키텍처 구축하기 | 우아한형제들 기술블로그&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;최초의 배달의민족은 하나의 프로젝트로 만들어졌습니다. 배달의민족의 주문수는 J 커브를 그리는 빠른 속도로 성장했고, 주문수가 커지면서 자연스럽게 트래픽 또한 매우 커졌습니다. 하나의&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;techblog.woowahan.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/@greg.shiny82/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%94%EB%84%90-%EC%95%84%EC%9B%83%EB%B0%95%EC%8A%A4-%ED%8C%A8%ED%84%B4%EC%9D%98-%EC%8B%A4%EC%A0%9C-%EA%B5%AC%ED%98%84-%EC%82%AC%EB%A1%80-29cm-0f822fc23edb&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://medium.com/@greg.shiny82/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%94%EB%84%90-%EC%95%84%EC%9B%83%EB%B0%95%EC%8A%A4-%ED%8C%A8%ED%84%B4%EC%9D%98-%EC%8B%A4%EC%A0%9C-%EA%B5%AC%ED%98%84-%EC%82%AC%EB%A1%80-29cm-0f822fc23edb&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;트랜잭셔널 아웃박스 패턴의 실제 구현 사례 (29CM)&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;이 글에서는 실무 관점에서의 Apache Kafka 활용 에서 잠깐 소개했던 트랜잭셔널 아웃박스 패턴 (Transactional Outbox Pattern) 을 실제로 구현하여 활용하고 있는 29CM 의 사례를 소개하고자 한다.&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/@greg.shiny82/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%94%EB%84%90-%EC%95%84%EC%9B%83%EB%B0%95%EC%8A%A4-%ED%8C%A8%ED%84%B4%EC%9D%98-%EC%8B%A4%EC%A0%9C-%EA%B5%AC%ED%98%84-%EC%82%AC%EB%A1%80-29cm-0f822fc23edb&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ZO7NX/hyXd9R9yE5/VrRnRSKOoh2W7sxATc0rl1/img.png?width=1200&amp;amp;height=632&amp;amp;face=0_0_1200_632&quot; data-og-url=&quot;https://medium.com/@greg.shiny82/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%94%EB%84%90-%EC%95%84%EC%9B%83%EB%B0%95%EC%8A%A4-%ED%8C%A8%ED%84%B4%EC%9D%98-%EC%8B%A4%EC%A0%9C-%EA%B5%AC%ED%98%84-%EC%82%AC%EB%A1%80-29cm-0f822fc23edb&quot;&gt;&lt;a href=&quot;https://medium.com/@greg.shiny82/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%94%EB%84%90-%EC%95%84%EC%9B%83%EB%B0%95%EC%8A%A4-%ED%8C%A8%ED%84%B4%EC%9D%98-%EC%8B%A4%EC%A0%9C-%EA%B5%AC%ED%98%84-%EC%82%AC%EB%A1%80-29cm-0f822fc23edb&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/@greg.shiny82/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%94%EB%84%90-%EC%95%84%EC%9B%83%EB%B0%95%EC%8A%A4-%ED%8C%A8%ED%84%B4%EC%9D%98-%EC%8B%A4%EC%A0%9C-%EA%B5%AC%ED%98%84-%EC%82%AC%EB%A1%80-29cm-0f822fc23edb&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ZO7NX/hyXd9R9yE5/VrRnRSKOoh2W7sxATc0rl1/img.png?width=1200&amp;amp;height=632&amp;amp;face=0_0_1200_632');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;트랜잭셔널 아웃박스 패턴의 실제 구현 사례 (29CM)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서는 실무 관점에서의 Apache Kafka 활용 에서 잠깐 소개했던 트랜잭셔널 아웃박스 패턴 (Transactional Outbox Pattern) 을 실제로 구현하여 활용하고 있는 29CM 의 사례를 소개하고자 한다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=165445&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://devocean.sk.com/blog/techBoardDetail.do?ID=165445&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;[MSA 패턴] SAGA, Transactional Outbox 패턴 활용하기&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot; &quot; data-og-host=&quot;devocean.sk.com&quot; data-og-source-url=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=165445&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jTwEJ/hyXaw9iUNZ/eKFycu7wI9oualL4HFhVh0/img.png?width=720&amp;amp;height=380&amp;amp;face=0_0_720_380&quot; data-og-url=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=165445&quot;&gt;&lt;a href=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=165445&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=165445&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jTwEJ/hyXaw9iUNZ/eKFycu7wI9oualL4HFhVh0/img.png?width=720&amp;amp;height=380&amp;amp;face=0_0_720_380');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[MSA 패턴] SAGA, Transactional Outbox 패턴 활용하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devocean.sk.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>MSA</category>
      <category>outbox패턴</category>
      <category>saga패턴</category>
      <category>트랜잭셔널메시징</category>
      <author>심심231</author>
      <guid isPermaLink="true">https://simsim231.tistory.com/294</guid>
      <comments>https://simsim231.tistory.com/294#entry294comment</comments>
      <pubDate>Mon, 7 Oct 2024 10:53:25 +0900</pubDate>
    </item>
    <item>
      <title>Kafka Consumer의 commit을 제어하는 enable.auto.commit과 AckMode</title>
      <link>https://simsim231.tistory.com/293</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 Kafka Consumer의 commit을 제어한다고 했을 때 보통 2가지 설정값을 떠올린다. enable.auto.commit과 AckMode다. 그렇다면 enable.auto.commit도 Consumer의 commit을 제어하는 설정값이고, AckMode도 Consumer의 commit을 제어하는데 이 둘이 따로 관리되는 걸까?&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;일단 consumer가 offset을 commit 하는 것은 Kafka의 내부 Topic인 __consumer_offsets에 저장하는 행위이며, consumer는 이 offset 값을 참조해서 다음에 처리한 메시지를 읽어온다는 것이 기본 개념이다. Commit에는 자동커밋과 수동커밋이 있는데 &lt;b&gt;enable.auto.commit으로 Kafka 클라이언트의 커밋방식 제어를 해서 자동커밋과 수동커밋을 설정할 수 있다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Setting&lt;b&gt; enable.auto.commit&lt;/b&gt; means that offsets are committed automatically with a frequency controlled by the config&amp;nbsp;&lt;br /&gt;auto.commit.interval.ms.&lt;br /&gt;&lt;br /&gt;출처) https://kafka.apache.org/23/javadoc/org/apache/kafka/clients/consumer/KafkaConsumer.html&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;enable.auto.commit은 Kafka 클라이언트의 자동 커밋(true, default), 수동 커밋(false)을 세팅한다. &lt;b&gt;auto.commit.interval.ms와 함께 쓰이는데 여기서 설정한 시간 간격마다 commit이 된다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724162012010&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private Map&amp;lt;String, Object&amp;gt; consumerConfigs() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Map&amp;lt;String, Object&amp;gt; configs = new HashMap&amp;lt;&amp;gt;();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// ...
    // enable.auto.commit 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;configs.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, &quot;false&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return configs;
}&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;enable.auto.commit=true로 했을 때 많이 얘기하는게 &lt;b&gt;중복/유실 문제&lt;/b&gt;인데, Consumer에서 메시지를 처리했던 못했던 상관없이 일정 시간이 지나면 알아서 offset을 갱신(commit)하기 때문이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중복 - poll해서 가져온 메시지들을 아직 다 처리를 못했고 commit 타이밍도 안됐는데 오류가 나면 Consumer는 한번 poll해왔던 메시지를 다시 가져올 것이다.&lt;/li&gt;
&lt;li&gt;유실 - poll해서 가져온 메시지들을 아직 다 처리를 못한 상황에서 commit 타이밍이 돼서 commit을 했는데 메시지 처리중 오류가 나버리면 처리못한 메시지를 다시 읽어오지 않기 때문에 유실된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제를 방지하기 위해 enable.auto.commit=false로 설정해주면 자동으로 커밋되지 않도록 할 수 있다.&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;그런데 enable.auto.commit=false로 해줘도 offset commit 이 일어나는 걸 알 수 있다. Commit도 정상적으로 이뤄지고 lag이 쌓이지 않는다. 이는 &lt;b&gt;Kafka 클라이언트의 자동 커밋은 비활성화 되지만, Spring Kafka에 의해 수동으로 offset commit을 하기 때문이다&lt;/b&gt;. 이것이 AckMode 설정이고 기본값은 BATCH로 들어간다. 그래서 자동 커밋처럼 auto.commit.interval.ms의 영향을 받지는 않지만 listener 메서드가 완료되면 배치단위(default가 BATCH)로 commit이 일어난다.&lt;b&gt; 즉, enable.auto.commit=false로 해줬을 때는 AckMode 설정을 함께 사용해서 Spring Kafka의 commit까지 제어해서 진정한 수동 커밋을 해주게 된다.&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;AckMode의 종류는 다음과 같다.&lt;/p&gt;
&lt;table style=&quot;color: #444444; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff;&quot;&gt;AcksMode&lt;/td&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #dcdddd;&quot;&gt;RECORD&lt;/td&gt;
&lt;td&gt;레코드 단위로 프로세싱 이후 커밋한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #dcdddd;&quot;&gt;BATCH (default)&lt;/td&gt;
&lt;td style=&quot;background-color: #f0f0f0;&quot;&gt;poll() 메서드로 호출된 레코드가 모두 처리된 이후 커밋한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #dcdddd;&quot;&gt;TIME&lt;/td&gt;
&lt;td&gt;특정 시간 이후에 커밋한다.&lt;br /&gt;이 옵션을 사용할 경우에는 시간 간격을 선언하는 `AckTime`옵션을 설정해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #dcdddd;&quot;&gt;COUNT&lt;/td&gt;
&lt;td style=&quot;background-color: #f0f0f0;&quot;&gt;특정 개수만큼 레코드가 처리된 이후에 커밋한다.&lt;br /&gt;이 옵션을 사용할 경우에는 레코드 개수를 선언하는 `AckCount`옵션을 설정해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #dcdddd;&quot;&gt;COUNT_TIME&lt;/td&gt;
&lt;td&gt;TIME, COUNT 옵션 중 맞는 조건이 하나라도 나올 경우 커밋한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #dcdddd;&quot;&gt;MANUAL&lt;/td&gt;
&lt;td style=&quot;background-color: #f0f0f0;&quot;&gt;Acknowledgement.acknowledge() 메서드가 호출되면 다음번 poll()때 커밋한다.&lt;br /&gt;매번 acknowledge() 메서드를 호출하면 BATCH 옵션과 동일하게 동작한다.&lt;br /&gt;이 옵션을 사용할 경우 AcknowledgingMessageListener 또는 BatchAcknowledgingMessageListener를 리스너로 사용해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #dcdddd;&quot;&gt;MANUAL_IMMEDIATE&lt;/td&gt;
&lt;td&gt;Acknowledgement.acknowledge() 메서드를 호출한 즉시 커밋한다.&lt;br /&gt;이 옵션을 사용할 경우 AcknowledgingMessageListener 또는 BatchAcknowledgingMessageListener를 리스너로 사용해야 한다.&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;보통 설정은 ContainerFactory를 통해 해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1724162685788&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; testKafkaListenerContainerFactory() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; testFactory = new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;testFactory.getContainerProperties().setAckMode(AckMode.MANUAL);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;testFactory.setConsumerFactory(testConsumerFactory());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return testFactory;
}&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;AckMode는 &lt;b&gt;acknowledgement.acknowledge() 메서드 호출을 통해 개발자가 코드상에서 수동커밋&lt;/b&gt;을 할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1724155478486&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@KafkaListener(topics = &quot;topic_name&quot;, ackMode = &quot;manual&quot;)
public void listen(ConsumerRecord&amp;lt;String, String&amp;gt; record, Acknowledgment acknowledgment) {
    // 메시지 처리
    acknowledgment.acknowledge(); // 메시지를 수동으로 ack
}&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;이렇게 @KafkaListener를 사용해서 Acknowledgment를 인자로 받아와서 수동 commit을 한다. 그럼 메시지 처리가 성공했을 경우만 acknowledge()를 호출하고, 실패했을 경우는 재처리하는 식으로 활용할 수 있을 것이다.&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;다만, 수동 커밋으로 설정하고 acknowledge()를 호출하지 않으면 offset이 커밋되지 않아 메시지가 처리되지 않은 상태가 되고, 오류가 났을 경우 중복/유실의 문제가 enable.auto.commit=true일 경우와 같이 생길 수 있어 &lt;b&gt;명시적으로 acknowledge()를 호출해주는게 중요&lt;/b&gt;하다. (물론 acknowledge()를 호출하지 않았다고 consumer가 다음 메시지를 못읽는 건 아니다. 다만 재처리를 해야하는 경우 문제가 발생할 수 있다.)&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;정리하자면, enable.auto.commit은 &lt;b&gt;Kafka 클라이언트의 커밋 방식에 대한 설정&lt;/b&gt;이고, AckMode는 &lt;b&gt;Spring Kafka 프레임워크 내에서 메시지 처리 후 커밋을 관리하는 설정&lt;/b&gt;이다. 둘은 상호 보완적인 역할을 하며, 각각의 설정이 Kafka 클라이언트와 Spring Kafka의 서로 다른 레벨에서 작동하기 때문에 사용법을 명확히 알고 쓰는게 중요하다.&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;</description>
      <category>TIL</category>
      <category>ackmode</category>
      <category>enable.auto.commit</category>
      <category>kafka</category>
      <author>심심231</author>
      <guid isPermaLink="true">https://simsim231.tistory.com/293</guid>
      <comments>https://simsim231.tistory.com/293#entry293comment</comments>
      <pubDate>Tue, 20 Aug 2024 23:51:49 +0900</pubDate>
    </item>
    <item>
      <title>Servlet부터 Filter와 Interceptor까지</title>
      <link>https://simsim231.tistory.com/292</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Servlet&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 요청이 WAS, Servlet에 의해 처리되고 응답이 되는 과정을 보면 Servlet이 어떤 것인지 알 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;HTTP 요청 -&amp;gt; WAS는 Request, Response 객체를 만들어서 Servlet 객체 호출 -&amp;gt; Servlet 객체에서 Request&amp;nbsp; 객체에서 HTTP 요청 정보를 꺼내서 비즈니스 로직 수행 -&amp;gt; 다시 Response 객체에 HTTP 응답 정보를 담아서 WAS가 Response 객체에 담겨있는 내용으로 HTTP 응답 정보를 생성해서 클라이언트로 보냄&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Servlet은 &lt;b&gt;Web Application에서 HTTP 프로토콜을 이용해 요청을 처리하고 응답을 하는 자바 클래스&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 servlet을 관리해주는 것이 Servlet Container이다. Servlet은 어떠한 역할을 수행하는 정의서라고 하면, Servlet Container는 그 정의서를 보고 수행하는 역할인 것이다. (예) 톰캣&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;간략하게 작성하면 Servlet Container 역할은 &lt;b&gt;1) 웹서버와의 통신 지원 2) Servlet 생명주기 관리 3) 멀티스레드 지원 및 관리 4) 선언적인 보안 관리&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;참고로 Servlet은 자바 코드 속에 HTML 코드가 들어가는 형태인데, JSP는 HTML 코드 속에 자바 코드가 들어가는 구조다. JSP는 Servlet 규칙이 복잡해서 나온 기술인고 WAS에 의해서 Servlet 클래스로 변환하여 사용되어 진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;337&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZWJFS/btsIGD9EnL4/UHkAYM9bTagd2zg9hIOhBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZWJFS/btsIGD9EnL4/UHkAYM9bTagd2zg9hIOhBK/img.png&quot; data-alt=&quot;Servlet 동작 방식&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZWJFS/btsIGD9EnL4/UHkAYM9bTagd2zg9hIOhBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZWJFS%2FbtsIGD9EnL4%2FUHkAYM9bTagd2zg9hIOhBK%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;647&quot; height=&quot;266&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;337&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Servlet 동작 방식&lt;/figcaption&gt;
&lt;/figure&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;946&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kyx4P/btsIHAc6mT9/CRFoatGL04wDCelZzwwND0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kyx4P/btsIHAc6mT9/CRFoatGL04wDCelZzwwND0/img.png&quot; data-alt=&quot;JSP 동작 방식&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kyx4P/btsIHAc6mT9/CRFoatGL04wDCelZzwwND0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fkyx4P%2FbtsIHAc6mT9%2FCRFoatGL04wDCelZzwwND0%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;546&quot; height=&quot;257&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JSP 동작 방식&lt;/figcaption&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;h3 data-ke-size=&quot;size23&quot;&gt;Dispatcher Servlet&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet 중 한가지인 Dispatcher Servlet (디스패처 서블릿)은&lt;b&gt; HTTP 프로토콜로 들어오는 모든 요청을 가장 먼저 받아 적합한 컨트롤러에 위임해주는 프론트 컨트롤러&lt;/b&gt;다. Dispatcher Servlet은 Tomcat과 같은 Servlet Container에게 HTTP 요청을 가장 먼저 받기 때문에 공통적인 작업을 먼저 처리하게 된다. Dispatcher Servlet이 나옴으로써 과거에는 모든 Servlet을 URL 매핑을 위해 web.xml에 등록해줘야 했지만 이제는 dispatcher servlet이 요청을 핸들링하고 공통 작업을 처리해주게 되었다. 우리는 컨트롤러를 구현해두기만 하면 dispatcher servlet이 알아서 적합한 컨트롤러로 위임을 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring에서의 Filter&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLP5P3/btsIIkt4gz8/0PStpswUW8B1KN6k2khXTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLP5P3/btsIIkt4gz8/0PStpswUW8B1KN6k2khXTK/img.png&quot; data-alt=&quot;filter와 interceptor&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLP5P3/btsIIkt4gz8/0PStpswUW8B1KN6k2khXTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLP5P3%2FbtsIIkt4gz8%2F0PStpswUW8B1KN6k2khXTK%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;778&quot; height=&quot;294&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;filter와 interceptor&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급한 Dispatcher Servlet에 요청이 전달되기 전후에 URL 패턴에 맞는 모든 요청에 대해 부가 작업을 처리할 수 있는 기능을 의미한다. 즉 Dispatcher Servlet은 스프링의 가장 앞단에 존재하는 프론트 컨트롤러이므로, Filter는 스프링 범위 밖에서 처리가 되는 것이다. (다만 스프링 빈으로 등록은 된다.)&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;Filter 사용법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;javax.servlet의 Filter 인터페이스를 구현해야 한다. Filter 인터페이스를 타고 들어가보면 다음과 같이 구성되어있다.&lt;/p&gt;
&lt;pre id=&quot;code_1721656071702&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package javax.servlet;

import java.io.IOException;

public interface Filter {
    default void init(FilterConfig filterConfig) throws ServletException {
    }

    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

    default void destroy() {
    }
}&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;&lt;b&gt;init()&lt;/b&gt;: 필터 객체를 초기화하고 서비스에 추가하기 위한 메소드. 웹 컨테이너가 1회 init 메소드를 호출하여 필터객체를 초기화하면 이후의 요청들은 doFilter를 통해 처리된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;doFilter()&lt;/b&gt;: 모든 HTTP 요청이 dispatcher servlet으로 전달되기 전에 웹 컨테이너에 의해 실행되는 메소드. 파라미터인 FilterChain chain 의 doFilter를 통해 다음 대상으로 요청을 전달하게 된다. chain.doFilter() 전/후에 우리가 필요한 처리 과정을 넣어줌으로써 원하는 처리를 진행할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;destory()&lt;/b&gt;: 필터 객체를 서비스에서 제거하고 사용하는 자원을 반환하기 위한 메소드. 웹 컨테이너에 의해 1번 호출되며 이후에는 이제 doFilter에 의해 처리되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 인터페이스를 구현하면 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1721657746152&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class DemoService implements Filter {

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		System.out.println(&quot;필터 생성&quot;);
	}

	@Override
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws
		IOException,
		ServletException {

		System.out.println(&quot;== 필터 시작 ==&quot;);
		filterChain.doFilter(servletRequest, servletResponse);
		System.out.println(&quot;== 필터 종료 ==&quot;);
	}

	@Override
	public void destroy() {
		System.out.println(&quot;필터 제거&quot;);
	}
}&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;추가로, 필터 등록을 위해 Configuration 설정을 통해 Bean으로 등록해줘야한다.&lt;/p&gt;
&lt;pre id=&quot;code_1721657784031&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class Config {

	@Bean
	public FilterRegistrationBean filterRegister() {
		FilterRegistrationBean registrationBean = new FilterRegistrationBean(new DemoService());
		return registrationBean;
	}
}&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;임의로 아래와 같이 Controller을 만들어주고 API 호출해주면 다음과 같이 필터가 실행되는 것을 알 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1721658145700&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
public class DemoController {

	@GetMapping(&quot;/&quot;)
	public String hello() {
		System.out.println(&quot;Hello world!&quot;);
		return &quot;Hello&quot;;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1721658120576&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::               (v2.7.17)

2024-07-22 23:20:42.968  INFO 16412 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication using Java 11 on DESKTOP-THAI4MF with PID 16412 (C:\Users\admin\Downloads\demo (4)\demo\target\classes started by admin in C:\Users\admin\Downloads\demo (4)\demo)
2024-07-22 23:20:42.970  INFO 16412 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to 1 default profile: &quot;default&quot;
2024-07-22 23:20:44.378  INFO 16412 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2024-07-22 23:20:44.389  INFO 16412 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2024-07-22 23:20:44.389  INFO 16412 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.82]
2024-07-22 23:20:44.487  INFO 16412 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2024-07-22 23:20:44.487  INFO 16412 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1476 ms
필터 생성
2024-07-22 23:20:44.817  INFO 16412 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2024-07-22 23:20:44.826  INFO 16412 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 2.214 seconds (JVM running for 2.574)
2024-07-22 23:21:28.810  INFO 16412 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-07-22 23:21:28.810  INFO 16412 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-07-22 23:21:28.811  INFO 16412 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
== 필터 시작 ==
Hello world!
== 필터 종료 ==&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;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;Filter를 등록하는 다양한 방법
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;@Configuration 별도로 붙여주고 작성하는 방식(위에 언급한 방식)&lt;/b&gt;: 설정을 위한 별개의 파일(@Configuration)이 필요하지만 setOrder()를 통해 순서를 정할 수 있고 addUrlPatterns()를 통해 베이스 URL 및 WhiteList를 설정할 수 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@Component를 필터에 붙여서 빈으로 등록하는 방식&lt;/b&gt;: 설정을 위한 별개의 파일이 필요하지 않고 @Order 어노테이션을 이용해 순서를 정할 수 있다. 기본 URL Pattern이 /*이며 설정할 수 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@ServletComponentScan을 @Configuration 어노테이션이 설정되어 있는 곳에 걸어주고, 필터에 @WebFilter 어노테이션을 설정해주는 방식&lt;/b&gt;: 설정을 위한 별개의 파일이 필요하지 않다. 필터에 대한 순서를 보장할 수 없다. @WebFilter의 value나 urlPatterns 파라미터로 베이스 URL을 설정할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@WebFilter, @Component를 필터 위에 붙이는 방식&lt;/b&gt;: 설정을 위한 별개의 파일이 필요하지 않고 컴포넌트 스캔 방식을 사용하기 때문에 @ServletComponentScan도 필요없음. @Order로 순서 설정할 수 있고, @WebFilter의 value나 urlPatterns 파라미터로 베이스 URL을 설정할 수 있다.&lt;br /&gt;&lt;br /&gt;
&lt;table style=&quot;background-color: #ffffff; color: #212529; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;설정 방법&lt;/td&gt;
&lt;td&gt;순서 설정&lt;/td&gt;
&lt;td&gt;베이스 URL&lt;/td&gt;
&lt;td&gt;Bean 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Configuration&lt;/td&gt;
&lt;td&gt;FilterRegistrationBean&lt;/td&gt;
&lt;td&gt;setOrder&lt;/td&gt;
&lt;td&gt;addUrlPatterns&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Component&lt;/td&gt;
&lt;td&gt;없어도 됨&lt;/td&gt;
&lt;td&gt;@Order&lt;/td&gt;
&lt;td&gt;사용할 수 없음&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@WebFilter&lt;/td&gt;
&lt;td&gt;@ServletComponentScan&lt;/td&gt;
&lt;td&gt;사용할 수 없음&lt;/td&gt;
&lt;td&gt;value, urlPatterns 파라미터&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@WebFilter +&lt;br /&gt;@Component&lt;/td&gt;
&lt;td&gt;없어도 됨&lt;/td&gt;
&lt;td&gt;@Order&lt;/td&gt;
&lt;td&gt;value, urlPatterns 파라미터&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;registrationBean.addUrlPatterns(&quot;/url&quot;);을 하면 특정 URL에 대해서만 Filter를 등록할 수 있다.&lt;/li&gt;
&lt;li&gt;Filter를 2개 등록하고 registrationBean.setOrder(1);을 통해 순서를 지정해주면 필터도 순서대로 작동하게 된다. 다만 주의할 점은 1번 필터, 2번 필터를 등록했을 경우 다음과 같이 나온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1721658253174&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;== 1번 필터 시작 ==
== 2번 필터 시작 ==
Hello world!
== 2번 필터 종료 ==
== 1번 필터 시작 ==&lt;/code&gt;&lt;/pre&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;1110&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccJyRg/btsIJcQVviL/wFCO5bnXrcPXCx1x6HSBH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccJyRg/btsIJcQVviL/wFCO5bnXrcPXCx1x6HSBH0/img.png&quot; data-alt=&quot;필터 동작 순서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccJyRg/btsIJcQVviL/wFCO5bnXrcPXCx1x6HSBH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccJyRg%2FbtsIJcQVviL%2FwFCO5bnXrcPXCx1x6HSBH0%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;743&quot; height=&quot;266&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;필터 동작 순서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring에서의 Interceptor&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvvJqL/btsIKUPSBLW/55QOcU0wrBrA2gtX5qassK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvvJqL/btsIKUPSBLW/55QOcU0wrBrA2gtX5qassK/img.png&quot; data-alt=&quot;Spring에서의 Interceptor&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvvJqL/btsIKUPSBLW/55QOcU0wrBrA2gtX5qassK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvvJqL%2FbtsIKUPSBLW%2F55QOcU0wrBrA2gtX5qassK%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;662&quot; height=&quot;250&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Spring에서의 Interceptor&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filter와 다르게 Interceptor는&lt;b&gt; Dispatcher Servlet이 Controller를 호출하기 전/후에 끼어들어 요청과 응답을 참조하거나 가공하는 역할&lt;/b&gt;을 하게 된다. Filter는 웹 컨테이너에서 동작했지만 Interceptor는 스프링 컨텍스트에서 동작한다.&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;Dispatcher Servlet이 Handler Mapping을 통해 Controller를 찾고, 그 결과로 실행 체인(HandlerExecutionChain)을 돌려준다. 여기서 1개 이상의 Interceptor가 등록되어 있으면 순차적으로 Interceptor들을 거쳐 Controller가 실행되고, 없다면 바로 Controller를 실행한다.&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;Interceptor 사용법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;org.springframework.web.servlet의 HandlerInterceptor 인터페이스를 구현해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1721739651218&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface HandlerInterceptor {
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    }
}&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;&lt;b&gt;preHandle()&lt;/b&gt;: Controller가 호출되기 전에 실행되는 메서드. 전처리 작업이나 요청정보 가공/추가에 활용 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;postHandle()&lt;/b&gt;: Controller가 호출된 후에 실행되는 메서드. (View 렌더링 전) 후처리 작업에 활용 가능. Controller 하위 계층에서 작업을 진행하다가 중간에 예외가 발생하면 호출되지 않음.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;afterCompletion()&lt;/b&gt;: 모든 작업이 완료된 후에 실행되는 메서드. (View 렌더링 후) 요청 처리 중에 사용한 리소스를 반환할 때 사용할 수 있음. Controller 하위 계층에서 작업을 진행하다가 중간에 예외가 발생해도 반드시 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1724331083710&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AuthCheckInterceptor implements HandlerInterceptor {

	@Override  // 컨트롤러 실행 전
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		HttpSession session = request.getSession(false);
		if(session != null) {
			Object loginInfo = session.getAttribute(&quot;loginInfo&quot;);
			if(authInfo != null) {
				return true;
			}
		} 
		response.sendRedirect(request.getContextPath() + &quot;/login&quot;);
		return false;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Filter와 Interceptor 차이&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Filter는 Request, Response를 조작할 수 있지만, Interceptor는 조작할 수 없다. (내부 상태 변경한다X, 다른 객체로 바꿔친다O)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필터는 다음 필터 호출을 위해 필터 체이닝(다음 필터 호출)을 해주는데 이때 request, response를 넘겨주므로 원하는 request, response를 바꿔치기 하는게 가능하다.&lt;/li&gt;
&lt;li&gt;인터셉터는 Dispatcher Servlet이 여러 인터셉터 목록을 가지고 순차적으로 실행시키는 방식이고, 반환값이 true, false이기 때문에 다음 인터셉터의 실행여부만 리턴할 뿐 request, response 객체를 넘길 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Filter는 스프링과 무관하게 전역적으로 처리해야 하는 작업들을 처리하지만, Interceptor는 클라이언트의 요청과 관련되어 전역적으로 처리해야 하는 작업들을 처리할 수 있다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Filter 사용 사례: 인증, 모든 요청에 대한 로깅 및 검사, 이미지/데이터 압축 및 문자열 인코딩, Spring과 분리되어야 하는 기능, &amp;nbsp;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;XSS방어, 인코딩 변환처리,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;요청에 대한 인증, 권한 체크&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Interceptor 사용 사례: 세부적인 인증, API 호출에 대한 로깅 또는 검사, Controller로 넘겨주는 정보의 가공, &lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;로그인 체크, 권한 체크, 프로그램 실행시간 작업, 로그확인&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AOP&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로, Filter, Interceptor와 많이 비교되는 AOP는 비즈니스 로직에 좀 더 집중한다. 메소드 전 후로 자유롭게 설정할 수 있고, 주소로 대상을 구분하는 Filter, Interceptor와 다르게 주소나 파라미터, 어노테이션 등 다양한 방법으로 대상을 지정할 수 있다.&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;Filter가 스프링빈으로 관리될 수 있는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는 Filter는 스프링 빈으로 관리되지 않았다. 하지만 위에서 언급했듯이 지금은 등록이 가능하고 빈 주입도 가능한데, 이는 DelegatingFilterProxy가 등장했기 때문이다. 다만 현재는 SpringBoot를 쓰면서 웹서버를 직접 관리하고, DelegatingFilterProxy조차 필요없게 되었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mangkyu.tistory.com/14&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mangkyu.tistory.com/14&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1721572533493&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[JSP] 서블릿(Servlet)이란?&quot; data-og-description=&quot;1. Servlet(서블릿) 서블릿을 한 줄로 정의하자면 아래와 같습니다. 클라이언트의 요청을 처리하고, 그 결과를 반환하는 Servlet 클래스의 구현 규칙을 지킨 자바 웹 프로그래밍 기술 간단히 말해서, &quot; data-og-host=&quot;mangkyu.tistory.com&quot; data-og-source-url=&quot;https://mangkyu.tistory.com/14&quot; data-og-url=&quot;https://mangkyu.tistory.com/14&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/PmhbM/hyWCL5TSU0/Z8o32ktJxOdmjGFyxlcok1/img.png?width=800&amp;amp;height=287&amp;amp;face=0_0_800_287,https://scrap.kakaocdn.net/dn/dRt2HE/hyWCHvFthT/hMkhuLkEcrXGYYfKsldrp1/img.png?width=800&amp;amp;height=287&amp;amp;face=0_0_800_287,https://scrap.kakaocdn.net/dn/bXB8rX/hyWCLrjYBM/xJnCcf2xpjQyQk7apKqHUK/img.jpg?width=740&amp;amp;height=330&amp;amp;face=0_0_740_330&quot;&gt;&lt;a href=&quot;https://mangkyu.tistory.com/14&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mangkyu.tistory.com/14&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/PmhbM/hyWCL5TSU0/Z8o32ktJxOdmjGFyxlcok1/img.png?width=800&amp;amp;height=287&amp;amp;face=0_0_800_287,https://scrap.kakaocdn.net/dn/dRt2HE/hyWCHvFthT/hMkhuLkEcrXGYYfKsldrp1/img.png?width=800&amp;amp;height=287&amp;amp;face=0_0_800_287,https://scrap.kakaocdn.net/dn/bXB8rX/hyWCLrjYBM/xJnCcf2xpjQyQk7apKqHUK/img.jpg?width=740&amp;amp;height=330&amp;amp;face=0_0_740_330');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[JSP] 서블릿(Servlet)이란?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. Servlet(서블릿) 서블릿을 한 줄로 정의하자면 아래와 같습니다. 클라이언트의 요청을 처리하고, 그 결과를 반환하는 Servlet 클래스의 구현 규칙을 지킨 자바 웹 프로그래밍 기술 간단히 말해서,&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mangkyu.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>Filter</category>
      <category>interceptor</category>
      <category>servlet</category>
      <author>심심231</author>
      <guid isPermaLink="true">https://simsim231.tistory.com/292</guid>
      <comments>https://simsim231.tistory.com/292#entry292comment</comments>
      <pubDate>Wed, 24 Jul 2024 00:04:38 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] 심화 개념 및 이해</title>
      <link>https://simsim231.tistory.com/291</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Producer Acks, Batch, Page Cache, Flush&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Producer Acks Parameter&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer는 Kafka가 메시지를 잘 받았는지 어떻게 알까?&lt;br /&gt;이를 확인하기 위해 &lt;b&gt;Producer Acks&lt;/b&gt;라는 Producer Parameter를 쓴다. (Network의 그 ack 개념과 유사)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 메시지를 받으면 잘 받았다는 의미의 acks를 보내고 Producer는 acks가 와야 다음 메시지를 보낸다.&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;acks = 0 : ack가 필요하지 않음. 이 수준은 자주 사용되지 않음. 메시지 손실이 다소 있더라도 빠르게 메시지를 보내야 하는 경우에 사용&lt;/li&gt;
&lt;li&gt;acks = 1 : (default 값) Leader가 메시지를 수신하면 ack를 보냄. Leader가 Producer에게 ACK를 보낸 후, Follower가 복제하기 전에 Leader에 장애가 발생하면 메시지가 손실. &quot;At most once 최대한번&quot; 전송을 보장&lt;/li&gt;
&lt;li&gt;acks = -1 : (acks = all과 동일) 메시지가 Leader와 모든 Replica까지 Commit되면 ack를 보냄. Leader를 잃어도 데이터가 살아남을 수 있도록 보장. 그러나 대기 시간이 더 길고 특정 실패 사례에서 반복되는 데이터 발생 가능성 있음. &quot;At least once최소 한번&quot; 전송을 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 관련 Parameter&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;min.insync.replicas: acks=all로 설정되어 있을 때 write를 성공하기 위한 최소 복제본의 수. Producer의 옵션이 아니고 broker의 옵션. Broker의 config/sever.propercies 파일에서 설정을 변경할 수 있음. (default 1). min.insync.replicas의 값과 replication factor(복제본수 +1) 수에 다라 프로듀서의 메시지가 성공 또는 실패로 결정 되므로 정확히 이해하고 사용하는것이 중요.&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;Producer Retry를 위한 Parameter&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;retries: 메시지를 send하기 위해 재시도하는 횟수. 즉 에러가 나는 경우 몇 번 전송을 재시도 하겠는지 (default 0)&lt;br /&gt;retry.backoff.ms: 재시도 사이에 추가되는 대기 시간. ex) 1회 시도와 2회 시도의 사이의 시간 (defualt 100)&lt;br /&gt;request.timeout.ms: Producer가 응답을 기다리는 최대 시간&lt;br /&gt;delivery.timeout.ms: send() 후 성공 또는 실패를 보고하는 시간의 상한시간.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; delivery.timeout.ms &amp;gt;= linger.ms + request.timeout.ms 여야 함.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Producer Batch 처리 Parameter&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리량을 향상시키기 위해 batch로 전송한다.&lt;br /&gt;사용되는 옵션&lt;br /&gt;1. linger.ms(default: 0): 메시지가 함께 Batch 처리될 때까지 대기 시간. Batch에 적재되지 전까지 얼마나 데이터를 기다리느냐.&lt;br /&gt;2. batch.size(default: 16kb): 보내기 전 Batch 최대 크기&lt;br /&gt;-&amp;gt; 보통 linger.ms = 100, batch.size = 1000000&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; linger.ms = 0의 경우 send()를 호출하자마자 바로 전송함. 이 경우 매번 send()가 호출될 때마다 TCP 통신을 맺어 데이터를 전송하기 때문에 대규모 데이터 전송에서는 적합하지 않음. 이를 위해 일부 딜레이를 가지고 batch.size만큼 데이터가 모일 때까지 기다렸다가 한번에 전체 데이터를 보내는 방법을 사용 &lt;br /&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;561&quot; data-origin-height=&quot;262&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0Rtyj/btsHJSLyzuX/V854EOaN4iRPpWjdoUKGrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0Rtyj/btsHJSLyzuX/V854EOaN4iRPpWjdoUKGrK/img.png&quot; data-alt=&quot;Producer가 생성한 Record를 send()할 때의 Life Cycle&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0Rtyj/btsHJSLyzuX/V854EOaN4iRPpWjdoUKGrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0Rtyj%2FbtsHJSLyzuX%2FV854EOaN4iRPpWjdoUKGrK%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;404&quot; height=&quot;189&quot; data-origin-width=&quot;561&quot; data-origin-height=&quot;262&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Producer가 생성한 Record를 send()할 때의 Life Cycle&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;3. max.in.flight.requests.per.connection: Producer가 한번에 보내는 Batch 수. 이때 앞에 Batch가 실패하고 다음 배치가 성공하면 순서가 달라짐 enable.idempotence parameter 사용해서 해결.&lt;br /&gt;4. enable.idempotence: 하나의 Batch가 실패하면, 같은 Partition으로 들어오는 후속 Batch들도 OutOfOrderSequenceException과 함께 실패함. 메시지 순서 보장(true), 메시지 순서 보장X(false)&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Page Cache와 Flush&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크 I/O의 비용이 크지만 Kafka는 데이터를 Broker의 로컬 디스크에 저장한다. 그럼에도 kafka가 빠른 이유 중 하나는 Page Cache를 사용하기 때문이다.&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;Producer에서 발행한 메시지가 저장되는 과정은 다음과 같다.&lt;br /&gt;Producer -&amp;gt; (send) -&amp;gt; Broker Process -&amp;gt; (write) -&amp;gt; &lt;b&gt;OS Page Cache&lt;/b&gt; -&amp;gt; (flush) -&amp;gt; Disk&lt;br /&gt;여기서 성능을 위해 메시지가 저장되는 Log Segment는 우선 OS Page Cache에 기록이된다.&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;Zero-copy: 데이터가 user space에 복사되지 않고, 'CPU 개입 없이' Page cache와 Network buffer 사이에서 직접 전송되는 것을 의미. 이것을 통해 'Broker Heap 메모리를 절약'하고 엄청난 처리량을 제공 (카프카 특징)&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;Flush 되는 시점: Broker가 종료되는 경우 &amp;amp; OS background &quot;Flusher Thread&quot;실행&lt;br /&gt;-&amp;gt; flush 되기 전에 Broker 장애가 발생하면? Replication 에서 데이터가 복구&lt;br /&gt;flush에는 여러 옵션이 있지만 카프카는 디폴트 옵션을 권장함&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Replica Failure&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Follower가 실패하는 경우, Leader에 의해 ISR 리스트에서 삭제되고, Leader는 새로운 ISR을 사용하여 Commit함&lt;/li&gt;
&lt;li&gt;Leader가 실패하는 경우, Controller는 Follower 중에서 새로운 Leader를 선출하고, Controller는 새 Leader와 ISR 정보를 먼저 ZooKeeper에 Push한 다음 로컬 캐싱을 위해 Broker에 Push함&lt;/li&gt;
&lt;li&gt;Leader가 선출될 때까지 해당 Partition을 사용할 수 없게 됨 -&amp;gt; Producer의 send()는 retries 파라미터가 설정되어 있으면 재시도하는데 retries = 0이면 NetworkException이 발생함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Replica Recovery&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. acks=all인 경우&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;770&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d2pANg/btsHKo6CDhc/6dPZWbwyxg1rhA1rS9ZuP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d2pANg/btsHKo6CDhc/6dPZWbwyxg1rhA1rS9ZuP0/img.png&quot; data-alt=&quot;Y는 M2까지 커밋, M3는 복제만. Z는 M2까지 복제 및 커밋&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d2pANg/btsHKo6CDhc/6dPZWbwyxg1rhA1rS9ZuP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd2pANg%2FbtsHKo6CDhc%2F6dPZWbwyxg1rhA1rS9ZuP0%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;439&quot; height=&quot;356&quot; data-origin-width=&quot;770&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Y는 M2까지 커밋, M3는 복제만. Z는 M2까지 복제 및 커밋&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 M3, M4를 커밋하기 전에 Leader X가 죽게되면?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;616&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b08wGw/btsHLvKr8Dr/Xuo3qKwHP18cgKEDzrpJFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b08wGw/btsHLvKr8Dr/Xuo3qKwHP18cgKEDzrpJFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b08wGw/btsHLvKr8Dr/Xuo3qKwHP18cgKEDzrpJFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb08wGw%2FbtsHLvKr8Dr%2FXuo3qKwHP18cgKEDzrpJFk%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;471&quot; height=&quot;350&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;616&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;Y가 새로운 Leader가 되고 Z는 Y의 M3를 복제해감&lt;br /&gt;Producer는 M3, M4에 대한 ack를 받지 못했으므로 M3, M4를 Y로 다시 보냄.&lt;br /&gt;idempotence = false(순서 보장X)이므로 M3는 중복발생&lt;br /&gt;&amp;nbsp;&lt;br /&gt;-&amp;gt; 만약 X가 다시 복구되면? Leader Y로부터 복제를 하기 시작하는데, Leader Epoch를 확인해서 Leader가 바뀐 시점부터 복제함.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;842&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4JEQh/btsHKpdpGQp/8R8xj4nOlx1gL3Kd495YN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4JEQh/btsHKpdpGQp/8R8xj4nOlx1gL3Kd495YN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4JEQh/btsHKpdpGQp/8R8xj4nOlx1gL3Kd495YN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4JEQh%2FbtsHKpdpGQp%2F8R8xj4nOlx1gL3Kd495YN0%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;448&quot; height=&quot;319&quot; data-origin-width=&quot;842&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;2. acks=1인 경우&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7Gisz/btsHKATqCO4/nzhd3YsANUVzxCKw4uBNq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7Gisz/btsHKATqCO4/nzhd3YsANUVzxCKw4uBNq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7Gisz/btsHKATqCO4/nzhd3YsANUVzxCKw4uBNq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7Gisz%2FbtsHKATqCO4%2Fnzhd3YsANUVzxCKw4uBNq1%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;515&quot; height=&quot;381&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Leader X가 장애나기 전에 Producer는 M4에 대한 ack를 이미 받았을 것임(acks=1)&lt;br /&gt;따라서 Y가 새로운 Leader로 선출되었을 때 M4를 영원히 잃어버리게됨&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;가용성(Availability)과 내구성(Durability) 관련 Parameter&lt;/b&gt; -&amp;gt; 상황에 맞게 선택 필요&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Topic Parameter - unclean.leader.election.enable
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ISR 리스트에 없는 Replica를 Leader로 선출할 것인지에 대한 옵션(default: false)&lt;/li&gt;
&lt;li&gt;false) ISR 리스트에 Replica가 하나도 없으면 Leader 선출을 안함 -&amp;gt; 서비스 중단&lt;/li&gt;
&lt;li&gt;true) ISR 리스트에 없는 Replica를 Leader로 선출 -&amp;gt; 데이터 유실&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Topic Parameter - min.insync.replicas
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최소 요구되는 ISR의 개수에 대한 옵션(default: 1)&lt;/li&gt;
&lt;li&gt;ISR이 min.insync.replicas보다 적은 경우, Producer는 NotEnoughReplicas 예외를 수신&lt;/li&gt;
&lt;li&gt;Producer에서 acks=all과 함께 사용할 때 더 강력한 옵션 (min.insync.replicas = 2)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;replicaion factor = 3인데 3개 다 기다리는게 아닌 2개까지만 복제되면 ack를 리턴하겠다 -&amp;gt; 성능상으로 유리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;N개의 Replica가 있고 min.insync.replicas = 2인 경우 (N-2)개의 장애를 허용할 수 있음 -&amp;gt; 2개 만큼의 replica가 있어야 하니까&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그렇다면 가용성과 내구성중 어떤걸 선택?&lt;/b&gt;&lt;br /&gt;내구성을 높이려면 (데이터 유실이 없도록 하려면)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;replication.factor &amp;gt; 2&lt;/li&gt;
&lt;li&gt;acks = all&lt;/li&gt;
&lt;li&gt;min.insync.replicas &amp;gt; 1&lt;/li&gt;
&lt;/ul&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;unclean.leader.election.enable = true&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Consumer Rebalance&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;기본 지식&lt;/b&gt;&lt;br /&gt;Consumer 동작방식: Consumer는 메시지를 가져오기 위해 Partition에서 연속적으로 Poll 함. 그리고 가져온 위치를 나타내는 offset 정보를 __consumer_offsets Topic에 저장해서 관리&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;Consumer Load Balancing: 동일한 group.id로 구성된 모든 Consumer들은 하나의 Consumer Group을 형성&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;Partition Assignment&lt;/b&gt;&lt;br /&gt;할당이 어떻게 될까?&lt;br /&gt;- 하나의 Partition은 지정된 Consumer Group 내의 하나의 Consumer만 사용&lt;br /&gt;- 동일한 key를 가진 메시지는 동일한 consumer가 사용&lt;br /&gt;- Consumer의 설정 파라미터 중에서 partition.assignment.strategy로 할당 방식 조정&lt;br /&gt;- Consumer Group은 Group Coordinator라는 프로세스에 의해 관리됨&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bG9Sz0/btsH3nLoQaQ/6c9bjWprlYO1V0NoJQMF00/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bG9Sz0/btsH3nLoQaQ/6c9bjWprlYO1V0NoJQMF00/img.jpg&quot; data-alt=&quot;Group Coordinator(Broker)와 Group Leader(Consumer)가 상호작용하는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bG9Sz0/btsH3nLoQaQ/6c9bjWprlYO1V0NoJQMF00/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbG9Sz0%2FbtsH3nLoQaQ%2F6c9bjWprlYO1V0NoJQMF00%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;762&quot; height=&quot;405&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Group Coordinator(Broker)와 Group Leader(Consumer)가 상호작용하는 모습&lt;/figcaption&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;Consumer가 어떤 partition을 할당할건지 &lt;b&gt;Group Coordinator와 Group Leader가 상호작용&lt;/b&gt;하며 관리됨&lt;br /&gt;&lt;br /&gt;전제: Partition 0 ~ 5까지 있는 Topic A가 하나 있고, Consumer 0~6까지 있는 Consumer Group이 있다. (Partition개수보다 Consumer가 개수가 1개더 많다)&lt;br /&gt;&lt;br /&gt;1. 각 Consumer는 group.id로 Kafka 클러스터에 자신을 등록한다. Kafka는 Consumer Group을 만들고 Consumer의 모든 Offset은 __consumer_offsets Topic의 하나의 Partition에 저장된다. 이 Partition의 Leader를 가지고 있는 Leader Broker는 Consumer Group의 &lt;b&gt;Group Coordinator로 선택&lt;/b&gt;된다.&lt;br /&gt;-&amp;gt; hash(group.id) % offsets.topic.num.partitions (default 50) 수식을 사용하여 offset이 저장될 __consumer_offsets의 partition을 결정&lt;br /&gt;-&amp;gt; 결정된 partition의 leader가 어디있는지에 따라 이 consumer group의 Group Coordinator가 결정됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;2. Broker(Group Coordinor)에 붙게된 Consumer들은 Group Coordinator에 &lt;b&gt;JoinGroup 요청&lt;/b&gt;을 날리게됨(7개의 모든 Consumer가 보냄)&lt;br /&gt;-&amp;gt; 이를 대기하는 시간을 결정하는 파라미터: group.initial.rebalance.delay.ms(default 3초)&lt;br /&gt;-&amp;gt; Group Coordinator는 JoinGroup 요청 순서에 따라 Consumer를 나열해서 Group Leader에게 던져준다. (Group Leader는 가장 먼저 JoinGroup 요청한 Consumer가 됨. 여기서는 Consumer 0번)&lt;br /&gt;&lt;br /&gt;3. Group Leader는 Group Coordinator로부터 Consumer 목록을 받아서 partition.assignment.strategy 파라미터에 지정된 할당 방식으로 &lt;b&gt;Consumer에 Partition을 할당&lt;/b&gt;함.&lt;br /&gt;&lt;br /&gt;4. Group Leader는 할당된 정보를 Group Coordinator로 보내게 됨. Group Coordinator는 맵핑정보를 메모리에 캐시하고 ZooKeeper에 유지&lt;br /&gt;&lt;br /&gt;5. Group Coordinator는 각 Consumer에 할당된 Partition 정보를 보냄. 각 Consumer는 할당된 Partition에서 Consume을 시작함.&lt;br /&gt;-&amp;gt;&lt;b&gt; 왜 Group Coordinator(Broker)가 할당하지 않고 Group Leader가 Partition을 할당할까?&lt;/b&gt; Kafka의 한가지 원칙은 가능한 한 많은 계산을 클라이언트에 수행하도록 하여, Broker의 부담을 줄임. 많은 Consumer Group이 있으면 Broker 혼자 Rebalance하기 위한 계산이 부담임&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Consumer Rebalancing Trigger&lt;/b&gt;&lt;br /&gt;이렇게 할당하는 과정은 Consumer가 새로 Consumer Group에 들어왔다던지 하면 바로 Trigger가 되어서 rebalancing이 시작됨 -&amp;gt; 불필요한 Rebalancing은 피해야함&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Rebalancing Trigger되는 상황&lt;/b&gt;&lt;br /&gt;- Consumer가 Consumer Group에서 탈퇴&lt;br /&gt;- 신규 Consumer가 Consumer Group에 합류&lt;br /&gt;- Consumer가 Topic 구독을 변경&lt;br /&gt;- Consumer Grouop은 Topic 메타데이터의 변경 사항을 인지 (ex. Partition 증가)&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Rebalancing Process&lt;/b&gt;&lt;br /&gt;1. Group Coordinator는 heartbeats의 플래그를 사용하여 Consumer에게 Rebalance 신호를 보냄&lt;br /&gt;2. 모든 Consumer가 일시 중지하고 Offset을 commit&lt;br /&gt;3. Consumer는 Consumer Group의 새로운 generation에 다시 합류&lt;br /&gt;4. partition 재할당&lt;br /&gt;5. Consumer는 새 Partition에서 다시 Consume을 시작&lt;br /&gt;&lt;br /&gt;-&amp;gt; Consumer Rebalancing시 Consumer들은 메시지를 Consume하지 못함. 따라서 불필요한 Rebalancing은 반드시 피해야 함&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Consumer Heartbeat&lt;/b&gt;&lt;br /&gt;: Consumer 장애를 인지하기 위함&lt;br /&gt;- Consumer는 데이터를 읽어오는 poll()과 별도로 백그라운드 Thread에서 Heartbeats를 계속 보냄(Consumer가 살아있음을 의미) -&amp;gt; heartbeat.interval.ms(default 3초)&lt;br /&gt;- 아래 시간동안 heartbeats가 수신되지 않으면 Consumer는 Consumer group에서 삭제됨 -&amp;gt; session.timeout.ms(default 10초)&lt;br /&gt;- poll()은 heartbeats과 상관없이 주기적으로 호출되어야 함 -&amp;gt; max.poll.interval.ms (default 5분)&lt;br /&gt;&lt;br /&gt;&lt;b&gt;과도한 rebalancing을 피하는 방법&lt;/b&gt;&lt;br /&gt;1. Consumer Group 멤버 고정하기&lt;br /&gt;- Group의 각 Consumer에게 고유한 group.instance.id를 할당하기&lt;br /&gt;- Consumer는 LeaveGroupRequest를 사용하지 않아야 함. 사용하면 rebalancing 되기 때문. 동일한 group.instance.id를 사용한다는것은 잠시 나갔다가 다시들어온다는 의미임(rebalancing X)&lt;br /&gt;- 즉, Rejoin(재가입)은 알려진 group.instance.id에 대한 rebalance를 trigger하지 않음&lt;br /&gt;&lt;br /&gt;2. session.timeout.ms 튜닝하기&lt;br /&gt;- heartbeat.interval.ms를 session.timeout.ms의 1/3로 설정&lt;br /&gt;- group.min.session.timeout.ms (default 6초)와 group.max.session.timeout.ms(default 5분)의 사이값&lt;br /&gt;- 숫자가 크면? Consumer가 rejoin할수 있는 더 많은 시간을 제공&lt;br /&gt;- 숫자가 작으면? Consumer 장애를 감지하는데 시간이 더 오래 걸림&lt;br /&gt;&lt;br /&gt;3. max.poll.interval.ms 튜닝하기&lt;br /&gt;- Consumer에게 poll()한 데이터를 처리할 수 있는 충분한 시간을 제공해야함. 단, 너무 크게 해도 안됨 -&amp;gt; 테스트하면서 적절한 값을 찾아가야함&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Partition Assignment Strategy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Partition 할당 방식 -&amp;gt;&lt;b&gt; partition.assignment.strategy&lt;/b&gt;로 할당방식 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;1. org.apache.kafka.client.consumer.RangeAssigner (default)&lt;/b&gt;&lt;br /&gt;-&amp;gt; Topic별로 작동. &amp;ldquo;순서대로&amp;rdquo;&lt;br /&gt;-&amp;gt; 동일한 Key를 가지고 있는 메시지들에 대한 Topic들 간에 co-partitioning하기 유리&lt;br /&gt;&lt;br /&gt;&lt;b&gt;2. org.apache.kafka.clients.consumer.RoundRobinAssignor&lt;/b&gt;&lt;br /&gt;-&amp;gt; Round Robin 방식으로 Consumer에게 partition을 할당&lt;br /&gt;-&amp;gt; Topic별로 작동하지 않고 &amp;ldquo;순서대로&amp;rdquo;&lt;br /&gt;-&amp;gt; 재할당될 때, Consumer가 동일한 Partition을 유지한다고 보장하지 않음&lt;br /&gt;-&amp;gt; Consumer간 Subscribe해오는 Topic이 다른 경우, 할당 불균형이 발생할 가능성 있음 -&amp;gt; 문제해결 sticky assignor&lt;br /&gt;&lt;br /&gt;&lt;b&gt;3. org.apache.kafka.clients.consumer.StickyAssigner&lt;/b&gt;&lt;br /&gt;-&amp;gt; 최대한 많은 기존 Partition 할당을 유지하면서 최대 균형을 이루는 할당을 보장&lt;br /&gt;-&amp;gt; Round Robin 문제해결: Consumer들에게 할당된 Topic Partition 수는 최대 1만큼 다름. 특정 Consumer(A)가 다른 Consumer(B)들에 비해 2개 이상 더 적은 Topic Partition이 할당된 경우, A에 할당된 Topic의 나머지 Partition들은 B에 할당될 수 없음.&lt;br /&gt;-&amp;gt; 재할당이 발생했을 경우, 기존 할당을 최대한 많이 보존하여 유지: topic partition이 하나의 consumer에서 다른 consumer로 이동할 때의 오버헤드를 줄임&lt;br /&gt;-&amp;gt; 특정 Consumer가 죽었다고 할 때, Round Robin은 전체 재할당하지만 stickyAssignor는 기존 할당은 유지하면서 갈곳잃은 partition들만 재할당함&lt;br /&gt;&lt;br /&gt;&lt;b&gt;4. org.apache.kafka.clients.consumer.CooperativeStickyAssignor&lt;/b&gt;&lt;br /&gt;-&amp;gt; 동일한 StickyAssignor 논리를 따르지만 협력적인 rebalance를 허용&lt;br /&gt;&lt;br /&gt;&lt;b&gt;5. org.apache.kafka.clients.consumer.ConsumerPartitionAssignor&lt;/b&gt;&lt;br /&gt;-&amp;gt; 인터페이스를 구현하면 사용자 지정 할당 전략을 사용할 수 있음&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kafka Log File&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Segment File을 좀 더 자세히 살펴보자.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;Kafka Log Segment File Directory&lt;/b&gt;&lt;br /&gt;- Segment File이 생성되는 위치: 각 Broker의 server.properties 파일 안에서 log.dirs 파라미터로 정의됨 (콤마로 구분하여 여러 디렉토리 지정 가능)&lt;br /&gt;ex) log.dirs = /data/kafka/kafka-log-a,/data/kafka/kafka-log-b&lt;br /&gt;- 각 Topic과 그 Partition은 log.dirs 아래에 하위 디렉토리로 구성&lt;br /&gt;- 생성되는 Log file명에는 의미가 있음 -&amp;gt; 00000000...12345.log -&amp;gt; 시작 offset을 의미. 만약 다음 파일이 0000...34567.log라면 0000...34567 - 1 까지 offset이 작성되었다는 의미&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;Partition 디렉토리에 생성되는 File Types 최소 4가지&lt;/b&gt;&lt;br /&gt;- Log Segmenet File: 메시지와 metadata 저장 -&amp;gt; .log&lt;br /&gt;- Index File: 각 메시지의 offset을 Log segment 파일의 Byte 위치에 매핑 -&amp;gt; .index&lt;br /&gt;- Time-based Index File: 각 메시지의 timestamp 기반으로 메시지를 검색하는데 사용 -&amp;gt; .timeindex&lt;br /&gt;- Leader Epoch Checkpoint File: Leader Epoch과 관련 Offset 정보를 저장 -&amp;gt; leader-epoch-checkpoint&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #777777;&quot;&gt;출처: 패스트캠퍼스 Kafka 완전 정복: 클러스터 구축부터 MSA 환경 활용까지&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java &amp;amp; Spring</category>
      <category>kafka</category>
      <author>심심231</author>
      <guid isPermaLink="true">https://simsim231.tistory.com/291</guid>
      <comments>https://simsim231.tistory.com/291#entry291comment</comments>
      <pubDate>Mon, 17 Jun 2024 23:34:12 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] 기본 개념 및 이해</title>
      <link>https://simsim231.tistory.com/290</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Apache Kafka란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;이벤트 스트리밍 플랫폼&lt;/u&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;&lt;b&gt;이벤트&lt;/b&gt;? 비즈니스에서 일어나는 모든 일(데이터)을 의미&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 웹사이트에서 뭔가 클릭하는것. 돈을 송금하는 것 등&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;이벤트는 bigdata의 특징을 가짐 -&amp;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;size16&quot;&gt;이렇게 많은 이벤트들을 처리하기 위해 LinkedIn에서 개발된 것이 Apache Kafka&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;3가지 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 이벤트 스트림을 안전하게 전송 (publish &amp;amp; Subscribe 기능)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 이벤트 스트림을 디스크에 저장함 (아파치 카프카 이전의 솔루션들과의 차이점. Write to Disk)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 이벤트 스트림을 분석 및 처리함&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;- 앱에서 발생하는 로그 수집&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Messaging System&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- IOT 디바이스로부터 데이터 수집&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- DB 동기화 (MSA 기반의 분리된 DB간 동기화)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 실시간 ETL(데이터를 extract -&amp;gt; transfrom -&amp;gt; load)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- spark, flink, storm, hadoop 과 같은 빅데이터 기술들과 함께 사용&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Topic, Partition, Segment&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1303&quot; data-origin-height=&quot;657&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/osdS6/btsGCs9ZyEQ/HvhOggXzUm89d3F3LejrBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/osdS6/btsGCs9ZyEQ/HvhOggXzUm89d3F3LejrBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/osdS6/btsGCs9ZyEQ/HvhOggXzUm89d3F3LejrBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FosdS6%2FbtsGCs9ZyEQ%2FHvhOggXzUm89d3F3LejrBK%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;749&quot; height=&quot;378&quot; data-origin-width=&quot;1303&quot; data-origin-height=&quot;657&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Topic&lt;/b&gt;: 카프카 내에서 전송되는 메시지가 저장되는 논리적인 장소. 파일이나 디렉터리처럼 눈에 보이지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Producer&lt;/b&gt;: 메시지를 만들어서 topic으로 보내줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Consumer&lt;/b&gt;: topic의 메시지를 가져와서 비즈니스에 활용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Consumer group&lt;/b&gt;: topic의 메시지를 사용하기 위해 협력하는 consumer들의 집합. 하나의 consumer는 하나의 consumer group에 포함되며, consumer group 내의 consumer들은 협력하여 topic의 메시지를 분산 병렬 처리함&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;producer와 consumer의 분리(decoupling)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- producer와 consumer는 서로 알지 못하며, producer와 consumer는 각각 고유의 속도로 commit log에 write 및 read를 수행함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 다른 consumer group에 속한 consumer들은 서로 관련이 없으며, commit log 에 있는 event(message)를 동시에 다른 위치에서 read할 수 있음&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;commit log&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/leOeF/btsGEjqV66W/lKxc5Qqqs2nNEh7bF1joN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/leOeF/btsGEjqV66W/lKxc5Qqqs2nNEh7bF1joN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/leOeF/btsGEjqV66W/lKxc5Qqqs2nNEh7bF1joN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FleOeF%2FbtsGEjqV66W%2FlKxc5Qqqs2nNEh7bF1joN0%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;806&quot; height=&quot;402&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 추가만 가능하고 변경 불가능한 데이터 스트럭처. 데이터(event)는 항상 로그 끝에 추가되고 변경되지 않음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 0번에 event가 write -&amp;gt; 1번에 event가 write ...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;u&gt;카프카도 이와 같은 commit log 개념 기반으로 만들어짐&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Producer가 데이터를 추가(write)하는 가장 마지막 로그를 &lt;b&gt;LOG-END-OFFSET&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Consumer가 데이터를 읽(read)고 마킹(commit)하는 offset을 &lt;b&gt;CURRENT-OFFSET&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- LOG-END-OFFSET과 CURRENT-OFFSET 의 위치의 차이를 &lt;b&gt;consumer lag&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;&lt;b&gt;Partition&lt;/b&gt;: Commit Log. Topic을 구성하는 컴포넌트. 하나의 Topic은 하나의 Partition 이상으로 구성을 하도록 만들어져 있음. 다수의 Partition은 사용하는 것은 병렬처리를 하기 위함.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Segment&lt;/b&gt;: Partition을 구성하고 있는 실제 물리 파일. Segment file이 지정된 크기보다 크거나 지정된 기간보다 오래되면 새 파일이 열리고 메시지는 새 파일에 추가됨&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;1222&quot; data-origin-height=&quot;641&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UIc6Z/btsGE4fZufV/i6Qld2ikv5n99MMyRqMj4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UIc6Z/btsGE4fZufV/i6Qld2ikv5n99MMyRqMj4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UIc6Z/btsGE4fZufV/i6Qld2ikv5n99MMyRqMj4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUIc6Z%2FbtsGE4fZufV%2Fi6Qld2ikv5n99MMyRqMj4K%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;658&quot; height=&quot;345&quot; data-origin-width=&quot;1222&quot; data-origin-height=&quot;641&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Topic은 생성시 Partition개수를 지정하게 되어있음&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 각 Partition은 broker들에 분산되며 Segmenet file들로 구성됨 (예를들어 1번 Topic의 partition A는 1번 broker에, 1번 Topic의 partition B는 2번 broker에 있을 수 있음)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 카프카 클러스터는 여러개의 broker들로 구성되어있음&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Partition당 오직 하나의 Segment만 활성화되어 있음. 데이터가 계속 쓰여지고 있는 중이기 때문. 1번 segment가 꽉차면 2번 segment에 이어서 쓰고.. 이런식임&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Broker, Zookeeper&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1287&quot; data-origin-height=&quot;741&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cs5Lgt/btsGGjxGNMR/s6BI772j7a9NgH5zBC96Dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cs5Lgt/btsGGjxGNMR/s6BI772j7a9NgH5zBC96Dk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cs5Lgt/btsGGjxGNMR/s6BI772j7a9NgH5zBC96Dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcs5Lgt%2FbtsGGjxGNMR%2Fs6BI772j7a9NgH5zBC96Dk%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;730&quot; height=&quot;420&quot; data-origin-width=&quot;1287&quot; data-origin-height=&quot;741&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Kafka Broker&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Topic과 Partition을 유지 및 관리하는 소프트웨어&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Kafka Server, Bootstrap Server라고 부르기도 함&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 각각의 Broker들은 ID로 식별됨&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Topic의 일부 Partition들을 포함 (Topic에 N개의 Partition이 있을 때 여러 broker에 분산되어서 관리되기 때문. 장애 발생에 대비하기 위한 것 중 하나)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 여러 개의 Broker들을 Kafka Cluster라고 함&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Client(Consumer, Producer 등)는 특정 Broker에 연결하면 전체 클러스터에 연결됨. 연결된 특정 Broker가 전체 Broker들의 리스트를 전달하고 Client는 접속해야할 Topic이 어디 있는지 알게돼서 필요한 Broker쪽으로 접속하게 됨. (장애를 대비해 각각의 Broker는 모든 Broker, Topic, Partition에 대해 알고 있음)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 최대 3대 이상의 Broker들로 하나의 Cluster 구성해야 함. 4대 이상을 권장함&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Topic 생성시 Kafka가 자동으로 Topic을 구성하는 전체 Partition들을 모든 Broker에게 할당해주고 분배해 줌&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Zookeeper&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Broker들의 목록/설정을 관리하고 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;변경사항(Topic 생성 제거 등)에 대해 Broker들에게 알려주는 역할을 하는 소프트웨어&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Zookeeper는 홀수의 서버로 작동하게 설계되어 있음(최소 3, 권장 5) -&amp;gt; Quorum 쿼럼 알고리즘 기반으로 의결을 하는게 필요한 최소한도의 인원수를 충족시키기 위함. 앙상블이 3대 있을 때 예상치 못한 장애가 발생해 1개의 서버에서 장애가 발생하더라도 2대, 즉 과반수 이상이 존재하므로 정상 동작 (쿼럼은 2)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- N개의 Zookeeper 서버: &lt;b&gt;Zookeeper Ensemble 앙상블&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 대용량 분산 시스템을 위한 네이밍 레지스트리를 제공하는 소프트웨어 -&amp;gt; 멀티 Kafka Broker들 간의 정보 공유, 동기화를 수행하기 위해 Tree 형태의 데이터 저장소로 구성되어 있음&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Zookeeper 없애는 버전 출시됨&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;참고) 왜 Zookeeper를 없앴을까?&lt;/b&gt;&lt;br /&gt;- 3.3.1 버전부터&lt;br /&gt;- 카프카 외부에서 메타데이터 관리하다보니 데이터 중복, 브로커의 메타 데이터와 주키퍼의 메타데이터의 불일치, 시스템 복잡성 증가, 서버 추가 필요 등의 문제 -&amp;gt; 카프카 외부에서 메타데이터를 관리하는게 문제가 아닐까? -&amp;gt; KRaft 모드(이벤트 기반의 Quorum 쿼럼 컨트롤러 서비스를 사용)&lt;br /&gt;&lt;br /&gt;&lt;b&gt;KRaft 모드&lt;/b&gt;&lt;br /&gt;- 메타데이터와 configuration을 @metadata라는 이름의 토픽으로 저장함&lt;br /&gt;- 몇개의 선택된 서버가 controller가 되고 이것들로 내부의 쿼럼이 구성되어짐. 쿼럼이 @metadata를 관리함&lt;br /&gt;- controller는 active 혹은 standby mode 둘중 하나가 되고, 만약 현재 active인 controller 서버가 탈락하면 standby mode인 서버가 active 된다고 함&lt;br /&gt;&lt;br /&gt;https://hoing.io/archives/4029&lt;br /&gt;https://medium.com/free-code-camp/in-search-of-an-understandable-consensus-algorithm-a-summary-4bc294c97e0d&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Producer&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Producer&lt;/b&gt;: 메시지를 생성해서 kafka의 topic으로 메시지를 보내는 애플리케이션&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;Producer가 보내는 (Message == Record == Event == Data)의 구성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Headers&amp;nbsp; &amp;nbsp;&lt;/b&gt; -&amp;gt; Metadata&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Key, Value&lt;/b&gt; -&amp;gt; 실제 보내고자 하는 데이터, 다양한 형태가 가능(JSON, String 등)&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;Kafka는 producer가 생성한 record를 byte array 로 저장(Serializers)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 원본 데이터 형태로 바꿔 Consumer에서 활용(Deserializers)&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;코드 상에서는 데이터, serializer만 세팅해서 넘기면 그 외의 과정은 kafka에서 처리(byte로 변환, 성공 및 재시도 처리 등)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1397&quot; data-origin-height=&quot;690&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5rc6T/btsGNcr6oVs/rDsuCw3kV1l9XUQabkwGD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5rc6T/btsGNcr6oVs/rDsuCw3kV1l9XUQabkwGD0/img.png&quot; data-alt=&quot;Producer에서 생성된 record가 kafka에서 처리되는 과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5rc6T/btsGNcr6oVs/rDsuCw3kV1l9XUQabkwGD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5rc6T%2FbtsGNcr6oVs%2FrDsuCw3kV1l9XUQabkwGD0%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;706&quot; height=&quot;349&quot; data-origin-width=&quot;1397&quot; data-origin-height=&quot;690&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Producer에서 생성된 record가 kafka에서 처리되는 과정&lt;/figcaption&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;&lt;b&gt;Partitioner&lt;/b&gt;: 메시지를 Topic의 어떤 Partition으로 보낼지 결정. 성능, 작동 방식이 다양함&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Consumer&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Consumer&lt;/b&gt;: Topic의 메시지를 가져와서 소비하는 애플리케이션. 각각 고유의 속도로 Commit log로부터 순서대로 read(poll)를 수행&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;Consumer Offset&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Consumer Group이 읽은 위치를 표시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- __consumer_offsets 라는 Internal Topic에서 Consumer Offset을 저장하려 관리&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;tempsnip.png&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;319&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckZKSc/btsGMJqliLs/W6kAORn3eMWzdB0GWT5kXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckZKSc/btsGMJqliLs/W6kAORn3eMWzdB0GWT5kXK/img.png&quot; data-alt=&quot;Consumer가 데이터를 읽어와서 offset을 저장하는 과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckZKSc/btsGMJqliLs/W6kAORn3eMWzdB0GWT5kXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckZKSc%2FbtsGMJqliLs%2FW6kAORn3eMWzdB0GWT5kXK%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;631&quot; height=&quot;274&quot; data-filename=&quot;tempsnip.png&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;319&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Consumer가 데이터를 읽어와서 offset을 저장하는 과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Consumer Group&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 동일한 group.id로 구성된 모든 Consumer들은 하나의 Consumer group 을 형성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- partition은 항상 consumer group내의 하나의 consumer에 의해서만 사용됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Consumer는 주어진 topic에서 0개 이상의 많은 partition을 사용할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Consumer Group의 Consumer들은 작업량을 어느 정도 균등하게 분할함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 동일한 Topic에서 consume하는 여러 Consumer Group이 있을 수 있음&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;Message Ordering(순서)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Partition이 2개 이상인 경우 모든 메시지에 대한 전체 &lt;b&gt;순서 보장 불가능 &lt;/b&gt;(하나의 Consumer가 여러개의 partition읽기 때문)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Topic 1개에 Partition 1개만 만든다면 Consumer가 하나의 Partition만 읽기 때문에 순서보장이 됨 -&amp;gt; 하지만 처리량이 떨어짐 -&amp;gt; &lt;u&gt;순서보장을 해야하는 경우가 많을까?&lt;/u&gt; 고민 필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 동일한 Key를 가진 메시지는 동일한 Partition에 전달되어(Partitioner) Key 레벨의 순서 보장은 가능함 (partition에서 hash 알고리즘으로 Partition에 분배하기 때문)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 운영중에 Partition 개수를 변경하면 -&amp;gt; 순서 보장 불가&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pfVYU/btsGNue00XA/5FYSmtAnBOMmeiBwj6uryk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pfVYU/btsGNue00XA/5FYSmtAnBOMmeiBwj6uryk/img.png&quot; data-alt=&quot;key레벨의 순서보장&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pfVYU/btsGNue00XA/5FYSmtAnBOMmeiBwj6uryk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpfVYU%2FbtsGNue00XA%2F5FYSmtAnBOMmeiBwj6uryk%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;633&quot; height=&quot;262&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;537&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;key레벨의 순서보장&lt;/figcaption&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;&lt;b&gt;Consumer Failure&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Consumer를 rebalancing해서 fail이 난 consumer가 가져가던 partition을 다른 consumer가 가져가게됨&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Replication&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 Broker에서 장애가 발생한다면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애가 난 Broker에 있는 Partition들을 다른 Broker에서 생성하면 해결될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 기존 메시지와 Offset 정보들은 버릴 것인가? -&amp;gt; 데이터 꼬임 -&amp;gt; 이를 해결하기 위한 기술이 Replication(복제)&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;Replication&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Partition을 복제하여 다른 Broker상에서 복제물(Replicas)을 만들어서 장애를 미리 대비함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 원본은 Leader Partition, 복제본을 Follower Partition이라고 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Replication Factor = Leader, Follower 다 포함해서 N개 (Topic이나 Broker쪽에 세팅할 수 있음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Producer, Consumer는 Leader만 read, write 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Follower는 Leader의 Commit log에서 데이터를 가져오기 요청(Fetch Request)으로 복제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 장애가 발생하면? -&amp;gt; Kafka Cluster가 새로운 Leader를 선출 -&amp;gt; Producer, Consumer는 바뀐 Leader에 read, write&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 하나의 Broker에만 Partition의 Leader들이 몰려있다면? -&amp;gt; 하나의 Broker에 Producer, Consumer가 몰림 -&amp;gt; 이를 방지하기 위한 옵션 -&amp;gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;auto.leader.rebalance.enable, leader.imbalance.check.interval.seconds, leader.imbalnce.per.broker.percentage&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;- Rack Awareness: Broker를 저장할 때 Leader, Follower 간에 최대한 Rack별로 분산하여 Rack 장애를 대비하는 옵션&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;In-Sync Replicas(ISR)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;In-Sync Replicas&lt;/b&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;- Leader가 장애가 났을 때 어떤 걸 Leader로 선출해야할지 고를때 사용됨 -&amp;gt; ISR이면 다음 Leader 후보가 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- High Water Mark&lt;span style=&quot;color: #212529; text-align: left;&quot;&gt;(가장 최근에 Committed 메시지의 Offset 추적)&lt;/span&gt; 라고 하는 지점까지 동일하게 복제해간 Replicas(Leader, Follwer 모두)의 목록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;replica.lag.max.messages&lt;/b&gt;: 해당 옵션이 4일 때 아래의 Broker103에서 Leader와 4이상으로 차이나므로 ISR이 아니고 OSR이라고 부르게 됨 -&amp;gt; 메시지 유입량이 갑자기 늘어나는 경우 지연으로 판단될 수 있음 -&amp;gt; replica.lag.time.max.ms로 판단해야 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;replica.lag.time.max.ms&lt;/b&gt;: Follwer가 Leader로 fetch 요청을 보내는 Interval을 체크. 해당 옵션안에 fetch 요청이 온다면 잘 따라잡고 있구나-라고 판단&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ISR은 해당 Leader가 떠있는 Broker가 관리함&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;tempsnip.png&quot; data-origin-width=&quot;1366&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDu1YE/btsGOKOTEwo/o3x76BYPNZUFN7QrWYExXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDu1YE/btsGOKOTEwo/o3x76BYPNZUFN7QrWYExXK/img.png&quot; data-alt=&quot;ISR 개념&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDu1YE/btsGOKOTEwo/o3x76BYPNZUFN7QrWYExXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDu1YE%2FbtsGOKOTEwo%2Fo3x76BYPNZUFN7QrWYExXK%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;743&quot; height=&quot;293&quot; data-filename=&quot;tempsnip.png&quot; data-origin-width=&quot;1366&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ISR 개념&lt;/figcaption&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;&lt;b&gt;Controller&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Kafka Cluster 내의 Broker 중 하나가 Controller가 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Zookeeper를 통해 Broker Liveness를 모니터링. Leader와 Replica 정보를 Cluster 내의 다른 Broker들에게 전달(캐싱해서 속도 높이기 위해)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Leader 장애시 Leader Election(선출)을 수행함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Controller가 장애시 다른 Active Broker들 중에서 재선출&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;Consumer 관련 Position들&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Last Commited Offset(Current Offset): Consumer가 최종 Commit한 Offset&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Current Position: Consumer가 읽어간 위치(처리 중, Commit 전)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- High Water Mark (Committed): ISR(Leader-Follower)간에 복제된 Offset&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Log End Offset: Producer가 메시지를 보내서 저장된, 로그의 맨 끝 Offset&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Consumer Lag: Log End Offset과 Last Commited Offset의 차이&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;Committed의 의미&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ISR 목록의 모든 Replicas가 메시지를 성공적으로 가져오면 Committed&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Consumer는 Committed 메시지만 읽을 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Leader는 메시지를 Commit 할 시기를 결정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Committed 메시지는 모든 Follwer에서 동일한 Offset을 갖도록 보장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 어떤 Replica가 Leader인지에 관계없이 모든 Consumer는 해당 Offset에서 같은 데이터를 볼 수 있음&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;Leader Epoch&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 새 Leader가 선출된 시점을 Offset으로 표시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Leader-epoch-checkpoint 파일에 체크포인트를 기록&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #777777; text-align: center;&quot;&gt;출처: 패스트캠퍼스 Kafka 완전 정복: 클러스터 구축부터 MSA 환경 활용까지&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java &amp;amp; Spring</category>
      <category>kafka</category>
      <author>심심231</author>
      <guid isPermaLink="true">https://simsim231.tistory.com/290</guid>
      <comments>https://simsim231.tistory.com/290#entry290comment</comments>
      <pubDate>Tue, 16 Apr 2024 00:30:41 +0900</pubDate>
    </item>
    <item>
      <title>[쿠버네티스/도커] 3 - 쿠버네티스에서의 도커, 젠킨스, 프로메테우스, 그라파나</title>
      <link>https://simsim231.tistory.com/288</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;도커&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;쿠버네티스를 이루는 기본 오브젝트가 파드고, 파드는 컨테이너로 이루어져 있다. 그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&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;size16&quot;&gt;간단하게 &lt;b&gt;도커 이미지를 내려받아 컨테이너로 실행하고 도커 이미지와 컨테이너를 삭제하는 방법&lt;/b&gt;을 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 이미지는 우리가 앞서 얘기했던 베이그런트 이미지와 유사하다. 이미지 그대로는 사용할 수 없고 도커와 같은 CRI로 불러들여야 컨테이너가 실제로 작동한다.&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;컨테이너 이미지는 도커 허브(https://hub.docker.com)와 같은 레지스트리(registry)에 모여있다. 레지스트리는 저장소를 의미하는데&lt;b&gt; docker search &amp;lt;검색어&amp;gt;&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;1506&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMe1ve/btsGCXt3SY1/81GAh9HoPEcqHOorOF1fEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMe1ve/btsGCXt3SY1/81GAh9HoPEcqHOorOF1fEK/img.png&quot; data-alt=&quot;docker search nginx&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMe1ve/btsGCXt3SY1/81GAh9HoPEcqHOorOF1fEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMe1ve%2FbtsGCXt3SY1%2F81GAh9HoPEcqHOorOF1fEK%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;867&quot; height=&quot;335&quot; data-origin-width=&quot;1506&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;docker search nginx&lt;/figcaption&gt;
&lt;/figure&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;NAME: 검색된 이미지 이름&lt;/li&gt;
&lt;li&gt;DESCRIPTION: 이미지에 대한 설명&lt;/li&gt;
&lt;li&gt;STARS: 해당 이미지를 내렵다은 사용자에게 받은 평가 횟수. 숫자가 클수록 신뢰성 높은 이미지일 수 있음&lt;/li&gt;
&lt;li&gt;OFFICIAL: [OK] 표시는 해당 이미지에 포함된 애플리케이션, 미들웨어 등을 개발한 업체에서 공식적으로 제공한 이미지라는 의미&lt;/li&gt;
&lt;li&gt;AUTOMATED: [OK] 표시는 도커 허브에서 자체적으로 제공하는 이미지 빌드 자동화 기능을 활용해 생성한 이미지를 의미&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;b&gt;docker pull&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;809&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cecHNl/btsGAOr8hTX/LToec6kmzDH2Wq4Jyb2vAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cecHNl/btsGAOr8hTX/LToec6kmzDH2Wq4Jyb2vAK/img.png&quot; data-alt=&quot;docker pull nginx&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cecHNl/btsGAOr8hTX/LToec6kmzDH2Wq4Jyb2vAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcecHNl%2FbtsGAOr8hTX%2FLToec6kmzDH2Wq4Jyb2vAK%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;662&quot; height=&quot;221&quot; data-origin-width=&quot;809&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;docker pull nginx&lt;/figcaption&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;이제 컨테이너를 생성해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker run [옵션] &amp;lt;사용할 이미지 이름&amp;gt;[:태그 | @다이제스트] 와 같이 입력하면 컨테이너를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래에서 사용한 옵션은 -d(--detach)와 --restart always 이 2개이고 각각 컨테이너를 백그라운드에서 구동한다, 가상 머신을 중지한 후 다시 실행해도 자동으로 컨에티너가 기존 상태를 이어 갈 수 있다- 라는의 미의 옵션이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;660&quot; data-origin-height=&quot;77&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhqVGG/btsGBYHgxDs/8RpRuQ61O5fv3nbiXsAy0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhqVGG/btsGBYHgxDs/8RpRuQ61O5fv3nbiXsAy0K/img.png&quot; data-alt=&quot;docker run -d --restart always nginx&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhqVGG/btsGBYHgxDs/8RpRuQ61O5fv3nbiXsAy0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhqVGG%2FbtsGBYHgxDs%2F8RpRuQ61O5fv3nbiXsAy0K%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;626&quot; height=&quot;73&quot; data-origin-width=&quot;660&quot; data-origin-height=&quot;77&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;docker run -d --restart always nginx&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker ps 명령으로 생성한 컨테이너의 상태를 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;64&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9mbNr/btsGByouFAh/WC4TmerJuoK5cGN67L9FTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9mbNr/btsGByouFAh/WC4TmerJuoK5cGN67L9FTK/img.png&quot; data-alt=&quot;docker ps&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9mbNr/btsGByouFAh/WC4TmerJuoK5cGN67L9FTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9mbNr%2FbtsGByouFAh%2FWC4TmerJuoK5cGN67L9FTK%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;837&quot; height=&quot;37&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;64&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;docker ps&lt;/figcaption&gt;
&lt;/figure&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;docker stop &amp;lt;컨테이너 이름 | ID&amp;gt; 를 통해 컨테이너를 정지하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker rm &amp;lt;컨테이너 이름 | ID&amp;gt; 를 통해 컨테이너를 삭제하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker rmi $(docker images -q nginx)를 통해 이미지를 삭제할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;젠킨스&amp;nbsp;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;컨테이너 인프라 환경에서 쿠버네티스를 사용하는 이유는 컨테이너 어플리케이션을 유연하고 빠르게 배포하고 운영하기 위함이다. 배포 자동화는 크게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;지속적 통합(CI, Continuous Integration), 지속적 배포(CD, Continuous Deployment)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;두 가지로 정의된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CI는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;코드를 커밋하고 빌드했을 때 정상적으로 작동하는지 반복적으로 검증해 애플리케이션의 신뢰성을 높이는 작업&lt;/u&gt;이다. CD는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;CI 과정에서 생성된 신뢰할 수 있는 애플리케이션을 실제 상용 환경에 자동으로 배포하는 것&lt;/u&gt;을 의미한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다르게 얘기하면 개발자가 소스를 커밋하고 푸시하면 CI단계로 들어가 애플리케이션이 자동 빌드되고 테스트를 거쳐 배포할 수 있는 애플리케이션인지 확인한다. 확인되면 CD 단계로 넘어서 애플리케이션을 컨테이너 이미지로 만들어서 파드 등 다양한 오브젝트 조건에 맞춰 미리 설정한 파일을 통해 배포한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;젠킨스를 사용하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;애플리케이션을 컨테이너로 만들고 배포하는 과정을 자동화&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개발자가 작성한 애플리케이션 코드를 소스 코드 저장소에 푸시하면, 쿠버네티스 내부에 설치된 젠킨스는 애플리케이션 코드를 빌드하고 레지스트리에 푸시한 후에 쿠버네티스에서 사용 가능한 형태로 배포한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;모니터링&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;애플리케이션을 컨테이너 인프라 환경에 빌드와 배포를 했더라도 상용 환경에서 여러 이슈가 생길 수 있다. 이러한 상황을 모니터링 하기 위해 여러 툴을 사용하는데 그 중에 프로메테우스와 그라파나에 대해 알아본다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쿠버네티스의 특정 노드에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;bpytop&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;명령을 실행하면 다음과 같이 시스템 상태 정보를 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2317&quot; data-origin-height=&quot;1259&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cB7Ld7/btsGEqKmBiQ/zxvZvCL8cnhVoF35Y3qHF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cB7Ld7/btsGEqKmBiQ/zxvZvCL8cnhVoF35Y3qHF1/img.png&quot; data-alt=&quot;bpytop&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cB7Ld7/btsGEqKmBiQ/zxvZvCL8cnhVoF35Y3qHF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcB7Ld7%2FbtsGEqKmBiQ%2FzxvZvCL8cnhVoF35Y3qHF1%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;823&quot; height=&quot;447&quot; data-origin-width=&quot;2317&quot; data-origin-height=&quot;1259&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;bpytop&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위의 이미지에서 리소스의 상태와 문제가 될 가능성이 있는 정보를 한눈에 파악할 수 있지만 현재 노드에 대한 정보만 알 수 있고 다수의 노드에 대해서는 알 수가 없다. 그래서 다수의 노드로 구성된 클러스터의 정보를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;수집&lt;/b&gt;하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;통합&lt;/b&gt;해서&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;시각화&lt;/b&gt;하는 툴을 사용하게 된다. 우리는&lt;u&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;모니터링 데이터를 프로메테우스로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;수집&lt;/b&gt;하고 수집한 정보를 한곳에 모아(&lt;b&gt;통합&lt;/b&gt;) 그라파나로&lt;span&gt;&amp;nbsp;&lt;/span&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reference&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도서: 컨테이너 인프라 환경 구축을 위한 쿠버네티스/도커&lt;/p&gt;</description>
      <category>Java &amp;amp; Spring</category>
      <category>그라파나</category>
      <category>도커</category>
      <category>젠킨스</category>
      <category>쿠버네티스</category>
      <category>프로메테우스</category>
      <author>심심231</author>
      <guid isPermaLink="true">https://simsim231.tistory.com/288</guid>
      <comments>https://simsim231.tistory.com/288#entry288comment</comments>
      <pubDate>Sun, 14 Apr 2024 17:57:38 +0900</pubDate>
    </item>
  </channel>
</rss>