<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>chaewss</title>
    <link>https://chaewsscode.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Tue, 14 Apr 2026 20:30:55 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>서채리</managingEditor>
    <image>
      <title>chaewss</title>
      <url>https://tistory1.daumcdn.net/tistory/4762774/attach/2de2475a5d8f4e34903e876acbff59f5</url>
      <link>https://chaewsscode.tistory.com</link>
    </image>
    <item>
      <title>[코틀린 코루틴의 정석] 코루틴 빌더와 Job</title>
      <link>https://chaewsscode.tistory.com/266</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Join을 사용한 코루틴 순차 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴을 사용하다 보면 특정 코루틴이 완료된 후에 다음 코루틴을 실행해야 하는 상황이 발생한다. 이런 순차적인 실행을 위해 Job 객체의 join 함수를 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1736685907360&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlockint&amp;lt;Unit&amp;gt; {
	val updateTokenJob = launch(Dispatchers.IO) {
    	// 토큰 업데이트 작업
    }
    updateTokenJob.join()	// updateTokenJob이 완료될 때까지 대기
    val networkCallJob = launch(Dispatchers.IO) {
    	// 네트워크 호출 작업
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서 updateTokenJob.join()이 호출되면, runBlocking 코루틴은 updateTokenJob이 완료될 때까지 대기하고, updateTokenJob이 완료된 후에야 networkCallJob이 실행된다.&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;이때 사용되는 join() 함수는 일시 중단 함수(suspend function)로, 다음과 같은 순서로 동작한다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;join()을 호출하는 코루틴(runBlocking)은 잠시 실행을 멈춘다&lt;/li&gt;
&lt;li&gt;join()의 대상이 되는 코루틴(updateTokenJob)이 실행을 완료할 때까지 기다린다&lt;/li&gt;
&lt;li&gt;대상 코루틴이 완료되면 원래 코루틴이 다시 실행을 시작한다&lt;/li&gt;
&lt;/ol&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;u&gt;코루틴 내부나 다른 일시 중단 함수 안에서만 호출&lt;/u&gt;될 수 있기 때문에, join 함수 역시 일반 함수에서는 호출할 수 없고 코루틴 스코프나 suspend 함수 안에서만 호출해야 한다.&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;2. JoinAll을 사용한 다중 코루틴 순차 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 코루틴이 모두 완료된 후에 다음 작업을 실행해야 하는 경우가 있다. 이런 경우 Job 객체의 joinAll 함수를 사용하면 여러 코루틴의 완료를 한 번에 대기할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1736687182635&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val convertImageJob1 = launch(Dispatchers.Default) {
        // 이미지1 변환 작업
    }
    val convertImageJob1 = launch(Dispatchers.Default) {
        // 이미지2 변환 작업
    }
    joinAll(convertImageJob1, convertImageJob2)    // 두 Job이 모두 완료될 때까지 대기
    
    val uploadImageJob = launch(Dispatchers.IO) {
        // 이미지 업로드
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서 joinAll()이 호출되면, runBlocking 코루틴은 convertImageJob1과 convertImageJob2이 모두 완료될 때까지 대기하고, 두 작업이 모두 완료된 후 processJob이 실행된다.&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;코루틴을 먼저 생성해 놓고 나중에 실행해야 하는 경우 코루틴 라이브러리가 제공하는 CorutimeStart.LAZY 옵션을 사용할 수 있다. 지연 시작 옵션 적용되어 생성된 코루틴은 &lt;b&gt;지연 코루틴&lt;/b&gt;으로 생성된 후 후 &lt;u&gt;대기 상태&lt;/u&gt;에 놓이며, &lt;u&gt;실행을 요청하지 않으면 시작되지 않는다&lt;/u&gt;.&lt;/p&gt;
&lt;pre id=&quot;code_1736687737329&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlockint&amp;lt;Unit&amp;gt; {
    val startTime = System.currentTimeMillis()
    val lazyJob: Job = launch(start = CoroutineStart.LAZY) {
        // 지연 실행
    }
    lazyJob.start() // 코루틴 실행
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지연 코루틴은 명시적으로 실행을 요청하지 않으면 실행 되지 않기 때문에 위 예제에서처럼 start 함수를 명시적으로 호출하지 않으면 lazyJob이 실행되지 않는다.&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;4. 코루틴 취소하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 중인 코루틴을 취소할 때는 Job 객체의 cancel 함수와 cancelAndJoin 함수를 사용할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) cancel() 사용&lt;/h4&gt;
&lt;pre id=&quot;code_1736688279794&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val longJob: Job = launch(Dispatchers.Default) {
        repeat(10) { repeatTime -&amp;gt;
            delay(1000L)
            println(&quot;job: $repeatTime&quot;)
        }
    }
    delay(3500L) // 3.5초 대기
    longJob.cancel() // 코루틴 취소
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cancel 함수를 사용하면 실행 중인 코루틴을 취소할 수 있다. 하지만 cancel의 대상이 된 Job 객체는 곧바로 취소되는 것이 아니라 미래의 어느 시점에 취소되기 때문에, 코루틴이 실제로 취소되었는지 확인이 필요할 수 있다.&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;2) cancelAndJoin() 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cancelAndJoin 함수는 cancel과 join을 순차적으로 실행하는 함수다. 이 함수는 코루틴 취소를 요청하고 해당 코루틴이 실제로 취소되어 종료될 때까지 대기한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cancel 함수를 호출한 후 곧바로 다른 작업을 실행하면 해당 작업은 코루틴이 취소되기 전에 실행될 수 있기 때문에, 코루틴이 완전히 취소된 후에 작업을 실행하고 싶다면 cancelAndJoin 함수를 사용해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1736688567784&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val longJob: Job = launch(Dispatchers.Default) {
        // 작업 실행
    }
    longJob.cancelAndJoin() // longJob이 취소될 때까지 runBlocking 코루틴 일시 중단
    executeAfterJobCancelled()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cancelAndJoin 함수를 호출하면 대상 코루틴의 취소가 완료될 때까지 호출부의 코루틴(runBlocking)이 일시 중단된다. 따라서 위 예제에서는 longJob 코루틴이 취소된 이후 executeAfterJobCancelled 함수가 실행되는 것이 보장된다.&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;5. 코루틴의 취소 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 나온 cancel과 cancelAndJoin 함수를 사용해도 코루틴이 즉시 취소되는 것이 아니고, Job 객체 내부에 있는 취소 확인용 플래그를 바꾸기만 하며, 코루틴이 이 플래그를 확인하는 시점에 비로소 취소된다. 코루틴이 취소를 확인하는 시점은 일반적으로 일시 중단 지점이나 코루틴이 실행을 대기하는 시점이며, 이 시점들이 없다면 코루틴은 취소되지 않는다.&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;1) delay&lt;/h4&gt;
&lt;pre id=&quot;code_1736689045668&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val whileJob = launch(Dispatchers.Default) {
        while(true) {
            println(&quot;작업 중&quot;)
            delay(1L) // 취소 확인 지점
        }
    }
    delay(100L)
    whileJob.cancel()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;delay 함수는 일시 중단 함수로, 특정 시간만큼 호출부의 코루틴을 일시 중단한다. 코루틴은 일시 중단되는 시점에 취소 여부를 확인하기 때문에 delay 함수를 통해 코루틴 취소를 확인할 수 있다.&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;하지만 이 방법은 while문이 반복될 때마다 작업을 강제로 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;2) yield&lt;/h4&gt;
&lt;pre id=&quot;code_1736689297379&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val whileJob = launch(Dispatchers.Default) {
        while(true) {
            println(&quot;작업 중&quot;)
            yield()
        }
    }
    delay(100L)
    whileJob.cancel()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yield 함수가 호출되면 코루틴은 자신이 사용하던 스레드 사용을 중단하며, 이 시점에 코루틴이 취소됐는지 확인된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서 Dispatchers.Default를 사용하는 코루틴은 whileJob밖에 없으므로 whileJob 코루틴은 yield를 호출하면 일시 중단 후 곧바로 재개되지만 잠깐 일시 중단된 시점에 취소 체크가 일어나 100밀리초 정도 후에 코루틴이 정상적으로 취소된다.&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;하지만 delay와 마찬가지로 while 문을 돌 때마다 스레드 사용이 양보되면서 일시 중단되는 문제가 있다.&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;3) CoroutineScope.isActive&lt;/h4&gt;
&lt;pre id=&quot;code_1736689500643&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val whileJob = launch(Dispatchers.Default) {
        while(this.isActive) {
            println(&quot;작업 중&quot;)
        }
    }
    delay(100L)
    whileJob.cancel()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineScope는 코루틴이 활성화됐는지 확인할 수 있는 Boolean 타입의 isActive 프로퍼티를 제공한다. 코루틴에 취소가 요청되면 isActive 값이 false로 바뀌며, while 문의 인자로 this.isActive를 넘겨 코루틴이 취소 요청될 때 while 문이 종료되도록 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 코루틴이 잠시 멈추지도 않고 스레드 사용을 양보하지도 않기 때문에 가장 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 코루틴의 상태와 Job의 상태 변수&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1️⃣ 생성&lt;/h4&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;코루틴 빌더를 통해 코루틴을 생성하면 기본적으로 생성 상태에 놓였다가 자동으로 실행 중 상태로 넘어간다. 만약 생성 상태에서 실행 중 상태로 자동 변경되지 않도록 하고 싶다면 코루틴 빌더의 start 인자로 CoroutineStart.Lazy를 넘겨 지연 시작이 적용된 코루틴을 생성해야 한다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2️⃣ 실행 중&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 빌더로 지연 코루틴이 아닌 코루틴을 만들면 자동으로 CoroutineDispatcher에 의해 스레드로 보내져 실행된다. 코루틴이 실제 실행 중일 때뿐만 아니라 실행된 후 일시 중단된 때도 해당 상태로 본다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;3️⃣ 실행 완료&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 중인 코루틴이 모두 정상적으로 실행돼 실행 완료된 상태이다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;4️⃣ 취소 중&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job.cancel() 등을 통해 코루틴에 취소가 요청됐을 경우이다. 아직 취소된 상태가 아니기 때문에 코루틴은 계속해서 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취소가 요청되면 실제로 코드가 실행 중이더라도 코루틴이 활성화된 상태로 보지 않는다.(isActive = false)&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;5️⃣ 취소&amp;nbsp; 완료&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: 114px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;코루틴 상태&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;isActive&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;isCancelled&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;isCompleted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;생성(New)&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;실행 중(Active)&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;true&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;실행 완료(Completed)&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;취소 중(Cancelling)&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;true&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;취소 완료(Cancelled)&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;true&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>Study</category>
      <author>서채리</author>
      <guid isPermaLink="true">https://chaewsscode.tistory.com/266</guid>
      <comments>https://chaewsscode.tistory.com/266#entry266comment</comments>
      <pubDate>Sun, 12 Jan 2025 22:51:44 +0900</pubDate>
    </item>
    <item>
      <title>[코틀린 코루틴의 정석] 멀티 스레드 vs 코루틴</title>
      <link>https://chaewsscode.tistory.com/265</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. JVM 프로세스와 스레드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린 애플리케이션이 실행되면 JVM이 프로세스를 시작하고 메인 스레드를 생성하여 메인 함수의 코드를 수행한다. 메인 스레드는 프로세스의 생명주기와 밀접하게 연관되어 있어, 프로세스의 시작부터 종료까지 함께 실행된다. JVM 프로세스는 기본적으로 이 단일 메인 스레드로 동작하며, 메인 함수의 코드가 모두 실행되거나 메인 스레드가 비정상적으로 종료되면 프로세스도 함께 종료된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  메인 스레드가 항상 프로세스의 끝을 함께할까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않다. JVM의 프로세스는 모든 &lt;b&gt;사용자 스레드&lt;/b&gt;가 종료될 때 종료되며, &lt;u&gt;메인 스레드는 여러 사용자 스레드 중 하나&lt;/u&gt;일 뿐이다. 따라서 멀티 스레드 환경에서는 메인 스레드에서 예외가 발생하더라도, 다른 사용자 스레드가 실행 중이라면 프로세스는 계속 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 단일 스레드의 한계와 멀티 스레드 프로그래밍&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 스레드만을 사용하는 &lt;b&gt;단일 스레드&lt;/b&gt; 애플리케이션은 &lt;u&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;이러한 한계는 &lt;b&gt;멀티 스레드&lt;/b&gt; 프로그래밍을 통해 극복할 수 있다. 큰 작업을 여러 개의 독립적인 작은 작업으로 분할하고, 이를 여러 스레드에 분산하여 동시에 처리함으로써 전체적인 응답 속도를 향상시킬 수 있다. 이렇게 여러 스레드가 작업을 동시에 처리하는 방식을 병렬 처리라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 스레드, 스레드풀을 사용한 멀티 스레드 프로그래밍&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM에서는 &lt;b&gt;Thread 클래스를 상속&lt;/b&gt;하여 사용자 스레드를 생성하고, 이를 통해 작업을 병렬로 실행할 수 있다. 하지만 이 방식에는 두 가지 문제점이 있다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Thread 클래스를 상속한 클래스의 인스턴스를 생성할 때마다 새로운 스레드가 만들어지는데, 이 생성 비용이 비싸다.&lt;/li&gt;
&lt;li&gt;스레드의 생성과 관리를 개발자가 직접 담당해야 하므로, 프로그램의 복잡도가 증가하고 실수로 인한 오류나 메모리 누수가 발생할 위험이 높아진다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제들을 해결하기 위해 &lt;b&gt;Executor 프레임워크&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;Executor 프레임워크는 작업 처리를 위한 스레드 집합인 스레드풀을 사전에 생성하여 관리한다. 새로운 작업이 요청되면 스레드풀 내의 쉬고 있는 스레드에 작업을 할당하고, 작업이 완료된 스레드는 종료하지 않고 다음 작업을 위해 재사용한다. 스레드의 생성, 관리, 작업 분배 등 모든 책임은 Executor 프레임워크가 담당하므로, 개발자는 단순히 스레드풀의 크기를 설정하고 작업을 제출하기만 하면 된다.&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;하지만 Executor 프레임워크에도 여러 문제가 있는데, 그중 가장 큰 문제는 스레드 블로킹이다. 스레드 블로킹은 스레드가 작업을 수행하지 못하고 대기 상태에 머무는 현상을 말한다. 스레드는 생성과 유지에 많은 비용이 드는 자원이므로, 블로킹 상태가 자주 발생하면 애플리케이션의 전반적인 성능이 저하될 수 있다.&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;4. 기존 멀티 스레드 프로그래밍의 한계와 코루틴&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;&amp;nbsp;&lt;/p&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;u&gt;스레드 사용을 최적화하고 블로킹을 방지&lt;/u&gt;한다는 점이다. &lt;u&gt;스레드에 비해 생성과 전환 비용이 매우 적어, 작업 처리에 필요한 리소스와 시간을 크게 절약&lt;/u&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더불어 코루틴은 구조화된 동시성을 제공하여 비동기 작업을 안전하게 처리하고, 효과적인 예외 처리가 가능하며, 실행 스레드를 손쉽게 전환하는 등 기존의 멀티 스레드 프로그래밍에 비해 많은 장점이 있다.&lt;/p&gt;</description>
      <category>Study</category>
      <author>서채리</author>
      <guid isPermaLink="true">https://chaewsscode.tistory.com/265</guid>
      <comments>https://chaewsscode.tistory.com/265#entry265comment</comments>
      <pubDate>Sun, 5 Jan 2025 23:36:15 +0900</pubDate>
    </item>
    <item>
      <title>Hibernate의 배치 처리 성능 개선</title>
      <link>https://chaewsscode.tistory.com/264</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;데이터베이스는 쿼리의 성능을 높이기 위해&lt;span&gt; &lt;b&gt;PreparedStatement&lt;/b&gt;를 캐싱한다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;동일한 모양의 쿼리가 여러 번 실행되면, 그 쿼리의 구조를 미리 캐싱해 두어 다시 파싱하거나 실행 계획을 새로 세우지 않도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;b&gt;PreparedStatement&lt;/b&gt;: 미리 작성된 SQL 쿼리의 틀, 실제 실행할 때 쿼리 안의 변수를 동적으로 대입해 성능을 향상시키고, 보안적으로도 SQL 인젝션을 방지할 수 있다.&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: #333333; text-align: start;&quot;&gt;하지만 보통의 SELECT * FROM table WHERE id IN (?)라는 쿼리를 만든다고 가정할 때,&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;데이터가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;1개일 때&lt;/b&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;SELECT * FROM table WHERE id IN (?)&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;2개일 때&lt;/b&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;SELECT * FROM table WHERE id IN (?, ?)&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이런 식으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;IN&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;절 안에 들어가는 값들이 1개일 때와 100개일 때의 쿼리의 모양이 다르다.&lt;/span&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;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;위처럼 &lt;/span&gt;&lt;/span&gt;IN&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;연산자에 따라 쿼리의 모양이 매번 달라지면, 쿼리의 개수가 늘어나고 데이터베이스는 그만큼 더 많은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;PreparedStatement&lt;/b&gt;를 캐싱해야 하므로 성능이 떨어지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하이버네이트의 최적화 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 하이버네이트는 이러한 문제를 해결하기 위해 내부적으로 &lt;b&gt;IN 연산자의 조건에 들어가는 값의 개수를 효율적으로 분배하여 쿼리의 모양을 최소화&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;1. 쿼리 모양 최적화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, &lt;span&gt;IN&lt;/span&gt; 연산자에 최대 100개의 값을 넣을 수 있다고 가정했을 때 원래대로라면 100개의 값을 처리하기 위해 100개의 &lt;b&gt;PreparedStatement&lt;/b&gt; 모양이 필요하다. 하지만 Hibernate는 이를 14개의 쿼리로 최적화한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;1~10까지는 자주 사용되므로 이 부분은 그대로 둔다. (IN (?), IN (?, ?), &amp;hellip;, IN (?, ?, &amp;hellip; ?) 10개)&lt;/li&gt;
&lt;li&gt;이후, 최대값인 100을 기준으로 절반씩 나누어 캐싱 가능한 쿼리의 모양을 제한한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;100개의 값을 나누어 처리하기 위해 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;IN&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 절에서 100, 50, 25, 12 등의 단위로 나누어 처리하는 방식이다.&lt;/span&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;2.&lt;span&gt; &lt;/span&gt;&lt;/span&gt;실제 쿼리 분할 방식&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약&amp;nbsp;&lt;span&gt;IN&lt;/span&gt; 연산자에 18개의 값을 넣는 경우, 하이버네이트는 이를 효율적으로 처리하기 위해 12와 6으로 나눈 두 개의 쿼리가 실행된다&lt;/p&gt;
&lt;pre id=&quot;code_1728121495527&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM table WHERE id IN (?, ?, ..., ?) # (12개)
SELECT * FROM table WHERE id IN (?, ?, ?, ?, ?, ?) # (6개)&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;PreparedStatement&lt;/b&gt;의 수를 줄이고, 데이터베이스에서 &lt;b&gt;캐시할 쿼리의 개수를 최소화&lt;/b&gt;하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;batch fetching 옵션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Legacy&lt;/b&gt; 방식(기본값)&lt;/p&gt;
&lt;pre id=&quot;code_1728121891728&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.jpa.properties.hibernate.batch_fetch_style: legacy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Legacy 방식은 Hibernate 5.0 이전 버전에서 사용되던 기본 배치 페칭 방식이며, 설정된 &lt;span&gt;batch size&lt;/span&gt;만큼의 데이터를 한 번에 가져 온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 배치 사이즈가 &lt;span&gt;10&lt;/span&gt;으로 설정되었다면, 한 번의 쿼리로 10개의 엔티티를 가져오도록 최적화된다.&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;size18&quot;&gt;&lt;b&gt;Padded&lt;/b&gt; 방식&lt;/p&gt;
&lt;pre id=&quot;code_1728121903627&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.jpa.properties.hibernate.batch_fetch_style: padded&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Padded 방식은 Hibernate 5.0 이후에 추가된 방식으로,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;배치 사이즈보다 적은&lt;/b&gt; 데이터를 가져와야 할 때, 배치 크기를 채우기 위해 빈 자리를 가짜 값으로 채워서 쿼리를 실행해 데이터베이스 쿼리의 캐싱 효율성을 높인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 배치 사이즈가 10이고 7개의 엔티티만 필요하다면 나머지 3개의 자리는 가짜 값으로 패딩하여 쿼리의 모양을 일정하게 유지한다. 이를 통해 쿼리가 매번 다른 모양을 가지는 문제를 방지할 수 있어 &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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Dynamic&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;/p&gt;
&lt;pre id=&quot;code_1728121914108&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.jpa.properties.hibernate.batch_fetch_style: dynamic&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dynamic 방식은 패딩이나 고정 크기 없이, &lt;b&gt;가변적인 배치 사이즈&lt;/b&gt;를 사용하여 필요한 만큼만 데이터를 가져오는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 만약 18개의 엔티티를 가져와야 한다면, 정확히 18개의 데이터를 처리할 수 있는 배치 쿼리를 생성한다.&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;b&gt;PreparedStatement&lt;/b&gt; 캐싱의 이점을 크게 누리지 못하게 된다는 단점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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: left;&quot;&gt;데이터베이스는 자주 사용되는 쿼리의 모양을 미리 캐싱해 두고 재사용할 때 캐시된 쿼리를 불러와 성능을 높이지만, IN 절처럼 쿼리의 변수가 많고 변동성이 클 경우 쿼리 모양이 다양해져 캐싱이 어려워지게 된다. 하이버네이트는 이를 최소한의 캐시로도 성능을 최적화하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: left;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.jboss.org/hibernate/orm/4.2/manual/en-US/html/ch20.html#performance-fetching-batch&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.jboss.org/hibernate/orm/4.2/manual/en-US/html/ch20.html#performance-fetching-batch&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1728122856947&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Chapter&amp;nbsp;20.&amp;nbsp;Improving performance&quot; data-og-description=&quot;Sometimes, you probably don't want to implement an intrusive interface, maybe due to portable concern, which is fine and Hibernate will take care of this internally with a wrapper class which implements that interface, and also an internal cache that maps &quot; data-og-host=&quot;docs.jboss.org&quot; data-og-source-url=&quot;https://docs.jboss.org/hibernate/orm/4.2/manual/en-US/html/ch20.html#performance-fetching-batch&quot; data-og-url=&quot;https://docs.jboss.org/hibernate/orm/4.2/manual/en-US/html/ch20.html#performance-fetching-batch&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.jboss.org/hibernate/orm/4.2/manual/en-US/html/ch20.html#performance-fetching-batch&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.jboss.org/hibernate/orm/4.2/manual/en-US/html/ch20.html#performance-fetching-batch&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Chapter&amp;nbsp;20.&amp;nbsp;Improving performance&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Sometimes, you probably don't want to implement an intrusive interface, maybe due to portable concern, which is fine and Hibernate will take care of this internally with a wrapper class which implements that interface, and also an internal cache that maps&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.jboss.org&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>Back-end</category>
      <author>서채리</author>
      <guid isPermaLink="true">https://chaewsscode.tistory.com/264</guid>
      <comments>https://chaewsscode.tistory.com/264#entry264comment</comments>
      <pubDate>Sat, 5 Oct 2024 19:29:26 +0900</pubDate>
    </item>
    <item>
      <title>[Java] GC(: Garbage Collection) 로그로 G1GC 동작과정 확인하기</title>
      <link>https://chaewsscode.tistory.com/261</link>
      <description>&lt;figure id=&quot;og_1726040620346&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;[Java] 가비지 컬렉션(Garbage Collection: GC)&quot; data-og-description=&quot;1. 가비지 컬렉션(Garbage&amp;nbsp;Collection)이란?가비지 컬렉션(Garbage Collection, 이하 GC)은 자바의 메모리 관리 방법 중의 하나로 JVM(자바 가상 머신)의 Heap 영역에서&amp;nbsp;동적으로 할당했던 메모리&amp;nbsp;중&amp;nbsp;필요 없&quot; data-og-host=&quot;chaewsscode.tistory.com&quot; data-og-source-url=&quot;https://chaewsscode.tistory.com/187&quot; data-og-url=&quot;https://chaewsscode.tistory.com/187&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dRs0Sn/hyW21fAevj/cmyca5r5iK8UWtPc6wahx1/img.png?width=774&amp;amp;height=438&amp;amp;face=0_0_774_438,https://scrap.kakaocdn.net/dn/bytbWP/hyW2PGd0Lf/RlzGBBMFzEMoNhoPgLPQhk/img.png?width=774&amp;amp;height=438&amp;amp;face=0_0_774_438,https://scrap.kakaocdn.net/dn/dt4Cfp/hyWZfGyrZC/go46ScD3dv1gdTLk9CJ3k0/img.png?width=1434&amp;amp;height=714&amp;amp;face=0_0_1434_714&quot;&gt;&lt;a href=&quot;https://chaewsscode.tistory.com/187&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chaewsscode.tistory.com/187&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dRs0Sn/hyW21fAevj/cmyca5r5iK8UWtPc6wahx1/img.png?width=774&amp;amp;height=438&amp;amp;face=0_0_774_438,https://scrap.kakaocdn.net/dn/bytbWP/hyW2PGd0Lf/RlzGBBMFzEMoNhoPgLPQhk/img.png?width=774&amp;amp;height=438&amp;amp;face=0_0_774_438,https://scrap.kakaocdn.net/dn/dt4Cfp/hyWZfGyrZC/go46ScD3dv1gdTLk9CJ3k0/img.png?width=1434&amp;amp;height=714&amp;amp;face=0_0_1434_714');&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;[Java] 가비지 컬렉션(Garbage Collection: GC)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. 가비지 컬렉션(Garbage&amp;nbsp;Collection)이란?가비지 컬렉션(Garbage Collection, 이하 GC)은 자바의 메모리 관리 방법 중의 하나로 JVM(자바 가상 머신)의 Heap 영역에서&amp;nbsp;동적으로 할당했던 메모리&amp;nbsp;중&amp;nbsp;필요 없&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chaewsscode.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM의 메모리 관리를 다시 공부하면서, 내 프로젝트에서 메모리가 실제로 어떻게 관리되는지 궁금해졌다. 그래서 프로젝트에서 사용하는 GC가 메모리를&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;어떻게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;회수하고 얼마나 효율적으로 동작하는지, GC 알고리즘이 애플리케이션 성능에 미치는 영향을 알아보기로 했다. 특히, G1GC에 대해 이론적으로만 알고 있었던 부분을 실제로 어떻게 작동하는지 알아보면 재밌겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;G1GC&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;536&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btksbS/btsJyKslIOz/5E4rEKENnCKs2YlMukmf4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btksbS/btsJyKslIOz/5E4rEKENnCKs2YlMukmf4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btksbS/btsJyKslIOz/5E4rEKENnCKs2YlMukmf4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtksbS%2FbtsJyKslIOz%2F5E4rEKENnCKs2YlMukmf4K%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;536&quot; height=&quot;362&quot; data-origin-width=&quot;536&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #3f93d0;&quot;&gt;&lt;b&gt;파란색 원&lt;/b&gt;&lt;/span&gt;: 오라클 문서에서 regular young-only collection이라고 표현하며, Young 영역에 대한 GC 발생을 나타낸다.(STW)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #e75e9e;&quot;&gt;&lt;b&gt;분홍색 원&lt;/b&gt;&lt;/span&gt;: 오라클 문서에서 multiple mixed collection이라고 표현하며, Young과 Old 영역 모두에 대한 GC 발생을 나타낸다.(STW)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원의 크기는 STW의 소요 시간과 비례한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #fffebd;&quot;&gt;&lt;b&gt;Regular Young-only Collection&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Eden 영역&lt;/b&gt;이 가득 찼을 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Young GC&lt;/b&gt;가 트리거 되어 Survivor 영역으로 이동하거나, Old 영역으로 승격된다. Young GC는 주기적으로 발생한다. 이 시간 동안 STW가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #fffebd;&quot;&gt;&lt;b&gt;Initial Mark&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Old 영역&lt;/b&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;Initial Mark&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;단계가 시작된다. 이 단계는 Young GC와 함께 트리거 되며, Old 영역에서&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;Space Reclamation&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;단계에서도 살아남아야 하는 객체들이 마킹된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;*&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Initial Mark는 비교적 빠르게 완료된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* Space Reclamation 단계는 Mixed GC라고 불리는 과정으로, Young 세대와 Old 세대를 함께 수집하는 단계이며 Old 세대에서 회수할 수 있는 메모리를 확보하는 것이 목적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #fffebd;&quot;&gt;&lt;b&gt;Concurrent Mark&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Initial Mark&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;후에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Concurrent Mark&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;단계에서 Old 영역 전체에 대해 살아있는 객체를 병렬로 마킹한다. 애플리케이션은 이 단계에서 계속 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #fffebd;&quot;&gt;&lt;b&gt;Remark&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Remark&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;단계는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Concurrent Mark&lt;/b&gt;가 끝난 후에 실행되며, GC가 멈추고 남은 객체들을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;최종적으로 마킹&lt;/b&gt;한다. 이 단계에서 Old 영역에 남아 있는 객체들이 올바르게 마킹되었는지 확인하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Class Unloading&lt;/b&gt;이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #fffebd;&quot;&gt;&lt;b&gt;Cleanup&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Cleanup&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;단계는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Empty Region&lt;/b&gt;을 회수하는 과정이다. Old 영역에서 살아남지 않은 객체들이 있는 Region을 정리하고, 회수 가능한 객체들을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Remark&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이후, Cleanup 단계에서 Old 영역에 대한 객체 회수가 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #fffebd;&quot;&gt;&lt;b&gt;Space Reclamation (Mixed GC)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Space Reclamation&lt;/b&gt;은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Mixed GC&lt;/b&gt;라고도 하며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Old Generation&lt;/b&gt;을 수집하는 과정이다. 이 단계는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Old 영역과 Young 영역을 함께&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;수집하며, Young GC로 충분하지 않을 때 수행된다. 이 과정에서 G1이 Old 영역의 객체들을 수집하고, 힙에서의 공간 회수가 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;다시 Young-only GC&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;bull;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;G1GC는 Space Reclamation(혼합 GC)이 끝난 후, 다시 Young-only GC로 돌아가며, Eden과 Survivor 영역을 수집한다.&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;h2 data-ke-size=&quot;size26&quot;&gt;GC 모니터링&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  jstat&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jstat 명령어를 통해 GC를 모니터링할 수 있다. 그전에 JVM 프로세스 ID(PID)를 확인해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1725945449505&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;% jsp
14337 AdminApplication
45329 Launcher
93603 
45330 ServerApplication
79954 Console
45337 Jps
93624 SonarLintServerCli&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jsp 명령어를 통해 현재 실행 중인 JVM 프로세스 목록이 출력되고, 각 프로세스의 PID를 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;GC 통계 조회: &lt;span style=&quot;background-color: #fffebd;&quot;&gt;&lt;b&gt;-gc 옵션&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1725945767076&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jstat -gc &amp;lt;PID&amp;gt; &amp;lt;interval&amp;gt; &amp;lt;count&amp;gt;
jstat -gc 12345 1000 10
# JVM 프로세스 ID
# 데이터를 수집할 간격(초단위)
# 데이터를 수집할 횟수(생략 시 무한 반복)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM의 전체 GC 활동을 요약하여 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어는 PID가 12345인 JVM 프로세스의 GC 활동을 1초 간격으로 10번 출력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725946377843&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;S0C         S1C         S0U         S1U          EC           EU           OC           OU          MC         MU       CCSC      CCSU     YGC     YGCT     FGC    FGCT     CGC    CGCT       GCT   
0.0      4096.0         0.0      2812.0      34816.0      26624.0      51200.0      33352.0    75264.0    74666.1   10624.0   10324.9     21     0.311     0     0.000     8     0.006     0.316&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;S0C, S1C&lt;/b&gt;: Survivor 영역 0과 1의 크기(KB 단위)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;S0U, S1U&lt;/b&gt;: Survivor 영역 0과 1의 사용량(KB 단위)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EC, EU&lt;/b&gt;: Eden 영역의 크기와 사용량&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OC, OU&lt;/b&gt;: Old generation의 크기와 사용량&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MC, MU&lt;/b&gt;: 메타스페이스 크기와 사용량&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CCSC, CCSU&lt;/b&gt;: Compressed Class Space의 총 크기와 사용량&lt;/li&gt;
&lt;li&gt;&lt;b&gt;YGC&lt;/b&gt;: Young GC 횟수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;YGCT&lt;/b&gt;: Young GC에 소비된 시간(초 단위)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FGC&lt;/b&gt;: Full GC 횟수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FGCT&lt;/b&gt;: Full GC에 소요된 시간(초 단위)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CGC&lt;/b&gt;: 병렬로 발생한 GC횟수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CGCT&lt;/b&gt;: 병렬 GC에 소요된 시간(초 단위)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GCT&lt;/b&gt;: 총 GC 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0.311 / 21 = 0.014 (14ms)&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;size18&quot;&gt;GC 시간 모니터링: &lt;span style=&quot;background-color: #fffebd;&quot;&gt;&lt;b&gt;&lt;b&gt;-gcutil 옵션&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1725947361917&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jstat -gcutil &amp;lt;PID&amp;gt; &amp;lt;interval&amp;gt; &amp;lt;count&amp;gt;
jstat -gcutil 12345 1000 10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC 시간과 힙 공간 사용 비율을 모니터링할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725947463720&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;S0     S1     E      O      M     CCS    YGC     YGCT     FGC    FGCT     CGC    CGCT       GCT   
0.00  68.65  82.35  65.14  99.21  97.18     21     0.311     0     0.000     8     0.006     0.316&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;S0, S1&lt;/b&gt;: Survivor 영역 0과 1의 사용률(%)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;E&lt;/b&gt;: Eden 영역 사용률(%)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;O&lt;/b&gt;: Old generation 사용률(%)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;M&lt;/b&gt;: Metaspace 사용률(%)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CCS&lt;/b&gt;: Compressed Class Space 사용률(%)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;YGC, &lt;b&gt;YGCT&lt;/b&gt;&lt;/b&gt;: Young GC 횟수와 소요된 시간(초 단위)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FGC, &lt;b&gt;FGCT&lt;/b&gt;&lt;/b&gt;: Full GC 횟수와 소요된 시간(초 단위)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CGC, &lt;b&gt;CGCT&lt;/b&gt;&lt;/b&gt;: 병렬로 발생한 GC 횟수와 소요된 시간(초 단위)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GCT&lt;/b&gt;: 총 GC에 소요된 시간(초 단위)&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;size18&quot;&gt;Young / Old Generation 모니터링: &lt;span style=&quot;background-color: #fffebd;&quot;&gt;&lt;b&gt;-gcnew / -gcold 옵션&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1725956582046&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jstat -gcnew &amp;lt;PID&amp;gt; &amp;lt;interval&amp;gt; &amp;lt;count&amp;gt;
jstat -gcold &amp;lt;PID&amp;gt; &amp;lt;interval&amp;gt; &amp;lt;count&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Young Generation과 Old Generation의 메모리 사용량을 자세하게 모니터링할 수 있다.&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;이렇게 jstat을 사용해 로그를 분석하는 데에는 한계가 있다. 로그를 남기는 주기에 따라 GC가 한 번 발생할 수도 있고, 여러 번 발생할 수도 있기&amp;nbsp;때문이다. 따라서 정확하게 분석을 하고 싶을 때는 -verbosegc 옵션을 사용하는 것이 좋다.&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;b&gt;&lt;b&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;-verbosegc&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-verbosegc 옵션은 자바 애플리케이션을 실행할 때 지정하는 JVM 옵션 중 하나이다. jstat과 달리 GC가 발생할 때마다 직관적인 출력 결과를 보여주기 때문에 GC 정보를 모니터링하기 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1725947786613&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# GC 활동을 자세하게 출력
# GC가 언제 발생했는지, GC 전후의 메모리 사용량과 소요 시간을 알 수 있음
java -verbose:gc -jar admin-0.0.1-SNAPSHOT.jar

# GC 활동에 대한 더 많은 세부 정보 제공(-verbose 옵션만 사용한다면 해당 옵션 기본 적용됨)
# Young Generation, Old Generation 등 각 영역에서 얼마나 많은 메모리가 회수되었는지,
# GC의 종류(Minor GC, Full GC) 등에 대한 더 구체적인 정보를 얻을 수 있음
java -verbose:gc -XX:+PrintGCDetails admin-0.0.1-SNAPSHOT.jar

# GC 로그를 파일에 기록
java -verbose:gc -XX:+PrintGCDetails -Xloggc:gc.log -jar admin-0.0.1-SNAPSHOT.jar&lt;/code&gt;&lt;/pre&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;&lt;b&gt;&lt;b&gt;  -Xlog:&quot;gc*&quot;&lt;/b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 9 이후 -verbose:gc 등의 GC 로그 옵션은 -Xlog로 대체되었다. -Xlog 옵션을 사용하면 JVM의 다양한 이벤트 로그를 세밀하게 제어할 수 있으며, 특히 gc*는 GC 관련 로그를 모두 출력해 기본 GC 로그를 출력하는 -verbosegc보다 더 다양한 로그를 출력할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1726057294128&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java -Xlog:&quot;gc*&quot; -jar admin-0.0.1-SNAPSHOT.jar&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;나의 경우, GC 발생마다 구체적인 로그를 확인하기 위해 java -Xlog:&quot;gc*&quot; 옵션을 사용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;java -Xlog:&quot;gc*&quot;&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;Young Generation GC&lt;/h3&gt;
&lt;pre id=&quot;code_1725957976267&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[0.005s][info][gc] Using G1

#1
[0.167s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 23M-&amp;gt;3M(260M) 0.941ms
[0.308s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 43M-&amp;gt;4M(260M) 1.245ms

#2
[0.662s][info][gc] GC(2) Pause Young (Normal) (GCLocker Initiated GC) 152M-&amp;gt;8M(260M) 3.542ms

#3
[0.874s][info][gc] GC(3) Pause Young (Concurrent Start) (Metadata GC Threshold) 109M-&amp;gt;10M(260M) 3.362ms

#4
[0.874s][info][gc] GC(4) Concurrent Mark Cycle
[0.878s][info][gc] GC(4) Pause Remark 11M-&amp;gt;11M(54M) 0.746ms
[0.878s][info][gc] GC(4) Pause Cleanup 11M-&amp;gt;11M(54M) 0.002ms
[0.878s][info][gc] GC(4) Concurrent Mark Cycle 3.754ms

#5
[1.000s][info][gc] GC(5) Pause Young (Normal) (G1 Preventive Collection) 40M-&amp;gt;11M(54M) 4.661ms
[1.101s][info][gc] GC(6) Pause Young (Normal) (G1 Evacuation Pause) 33M-&amp;gt;12M(54M) 5.834ms
[1.219s][info][gc] GC(7) Pause Young (Normal) (G1 Evacuation Pause) 40M-&amp;gt;13M(54M) 3.073ms
[1.294s][info][gc] GC(8) Pause Young (Normal) (G1 Evacuation Pause) 37M-&amp;gt;14M(54M) 3.314ms&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;size18&quot;&gt;#1&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;GC(0) Pause Young (&lt;b&gt;Normal&lt;/b&gt;)&lt;br /&gt;Young Generation에서 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;JVM이 처음 수행한&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;일반적인 GC&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;G1 &lt;b&gt;Evacuation Pause&lt;/b&gt;&lt;br /&gt;Young Generation에서 살아남은 객체를 Old Generation으로 이동시키는 작업&lt;br /&gt;이 과정에서 GC가 잠시 멈추며, Evacuation(회피) 작업이 수행됨&lt;/li&gt;
&lt;li&gt;23M-&amp;gt;3M(260M)&lt;br /&gt;가비지 컬렉션 전에는 23MB의 메모리가 사용 중이었고, 가비지 컬렉션 후에는 3MB로 줄어들음&lt;/li&gt;
&lt;li&gt;0.941ms&lt;br /&gt;해당 GC 작업은 0.941밀리초 동안 지속됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;#2&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;GCLocker Initiated GC&lt;br /&gt;GCLocker에 의해 트리거 된 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;GC&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;* GCLocker는 JNI 코드가 실행되는 동안 GC를 방지하며,  잠금이 해제된 후 GC를 강제로 유발할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;#3&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Pause Young (&lt;b&gt;Concurrent Start&lt;/b&gt;)&lt;br /&gt;Young Generation에서 GC가 발생함과 동시에 &lt;u&gt;Old Generation에서 Concurrent Mark Cycle&lt;/u&gt;&amp;nbsp;시작&lt;/li&gt;
&lt;li&gt;Metadata GC Threshold&lt;br /&gt;메타데이터 영역이 GC 임계값에 도달하여 GC 발생. 주로 클래스 메타데이터나 관련 정보가 많이 쌓일 때 트리거 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;#4&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Concurrent Mark Cycle&lt;br /&gt;Old Generation에 대한 마크 작업 동시 진행. 이는 Young GC와 별개로 Old Generation의 객체를 마크하고 추적하는 동작이다.&lt;/li&gt;
&lt;li&gt;Pause Remark&lt;br /&gt;Concurrent 마크 작업이 완료된 후, &amp;lsquo;Remark&amp;rsquo; 단계에서 GC가 일시적 중단&lt;/li&gt;
&lt;li&gt;Pause Cleanup&lt;br /&gt;청소 작업을 위해 GC 일시적 중단&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Humongous Allocation과 Full GC&lt;/h3&gt;
&lt;pre id=&quot;code_1725981514857&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/makeFullGC&quot;)
public Void makeFullGC() {
    List&amp;lt;byte[]&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
    int size = 1024 * 1024; // 1MB
    Long iterations = 80L;

    for (int i = 0; i &amp;lt; iterations; i++) {
        byte[] array = new byte[size];
        size += 1024 * 1024;
        list.add(array);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    list.clear();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1725981614458&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#1
[7.610s][info][gc] GC(20) Pause Young (Normal) (G1 Preventive Collection) 100M-&amp;gt;39M(114M) 21.931ms
[7.819s][info][gc] GC(21) Pause Young (Concurrent Start) (G1 Humongous Allocation) 52M-&amp;gt;51M(114M) 2.211ms
[7.819s][info][gc] GC(22) Concurrent Mark Cycle
[7.843s][info][gc] GC(22) Pause Remark 59M-&amp;gt;59M(114M) 1.898ms
[7.854s][info][gc] GC(22) Pause Cleanup 59M-&amp;gt;59M(114M) 0.060ms
[7.855s][info][gc] GC(22) Concurrent Mark Cycle 36.097ms

#2
[16.218s][info][gc] GC(169) Pause Young (Concurrent Start) (G1 Humongous Allocation) 3308M-&amp;gt;3307M(4096M) 0.911ms
[16.218s][info][gc] GC(170) Concurrent Mark Cycle
[16.238s][info][gc] GC(170) Pause Remark 3389M-&amp;gt;3389M(4096M) 1.652ms
[16.250s][info][gc] GC(170) Pause Cleanup 3389M-&amp;gt;3389M(4096M) 0.133ms
[16.251s][info][gc] GC(170) Concurrent Mark Cycle 33.377ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부러 Full GC를 유도하기 위해 많은 양의 메모리를 할당하고, 이를 통해 GC가 어떻게 동작하는지 확인해 보았다. 위 로그에서 볼 수 있듯이, 메모리 사용량이 급격하게 증가하면서 G1GC가 빈번하게 실행되었고, 특히&amp;nbsp;Humongous Allocation과 관련된 GC 이벤트가 발생한 것을 확인할 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Humongous Allocation이 발생할 때, G1GC는 Full GC 대신 Minor GC 또는 Mixed GC로 해당 객체를 처리하려고 한다. Humongous Object는 Young Generation 또는 Old Generation의 여러 Regions에 걸쳐 할당되는데, G1GC는 이 큰 객체들을 처리하기 위해 추가적인 메모리 할당이나 기존의 메모리 청소를 통해 공간을 확보한다.&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;  G1GC에서 Full GC가 발생하지 않은 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;G1GC는 Humongous Allocation을 처리할 때 Young Generation의 GC 작업이나 Mixed GC를 사용하여 Old Generation의 Region을 관리함으로써 메모리 관리와 회수 작업을 효율적으로 처리하며, Full GC를 줄이기 위해 노력한다.&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; &lt;span&gt; Serial GC를 사용하면?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교를 위해 Serial GC를 사용하여 해당 동작을 분석해보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1726070880446&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[8.062s][info][gc] GC(14) Pause Young (Allocation Failure) 93M-&amp;gt;28M(247M) 14.456ms
[9.204s][info][gc] GC(15) Pause Young (Allocation Failure) 88M-&amp;gt;83M(247M) 50.409ms
[9.794s][info][gc] GC(16) Pause Young (Allocation Failure) 149M-&amp;gt;148M(247M) 68.099ms
[10.129s][info][gc] GC(17) Pause Young (Allocation Failure) 200M-&amp;gt;199M(282M) 22.628ms
[10.225s][info][gc] GC(18) Pause Full (Allocation Failure) 199M-&amp;gt;196M(474M) 95.795ms
[10.828s][info][gc] GC(19) Pause Young (Allocation Failure) 303M-&amp;gt;301M(474M) 56.671ms
[11.306s][info][gc] GC(20) Pause Young (Allocation Failure) 405M-&amp;gt;403M(552M) 56.836ms
[11.356s][info][gc] GC(21) Pause Full (Allocation Failure) 403M-&amp;gt;403M(974M) 50.429ms
[12.386s][info][gc] GC(22) Pause Young (Allocation Failure) 660M-&amp;gt;655M(974M) 107.471ms
[13.119s][info][gc] GC(23) Pause Young (Allocation Failure) 891M-&amp;gt;886M(1205M) 105.830ms
[13.166s][info][gc] GC(24) Pause Full (Allocation Failure) 886M-&amp;gt;886M(2142M) 46.883ms
[14.798s][info][gc] GC(25) Pause Young (Allocation Failure) 1467M-&amp;gt;1456M(2142M) 220.494ms
[16.039s][info][gc] GC(26) Pause Young (Allocation Failure) 1989M-&amp;gt;1978M(2610M) 176.171ms
[16.111s][info][gc] GC(27) Pause Full (Allocation Failure) 1978M-&amp;gt;1978M(3959M) 71.364ms
[18.176s][info][gc] GC(28) Pause Young (Allocation Failure) 2972M-&amp;gt;3848M(3959M) 384.172ms
[19.134s][info][gc] GC(29) Pause Full (Allocation Failure) 3848M-&amp;gt;2951M(3959M) 957.937ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복해서 Minor GC와 Full GC가 발생하며 소요시간 또한 상당히 길어진 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Serial GC와 G1GC와 비교했을 때 Serial GC는 메모리 회수를 더 자주 수행하여 Full GC로 인한 성능 저하가 더 발생하며, 이는 대규모 메모리 작업을 처리하는 데 불리할 수 있다. 또한 Serial GC를 사용했을 때 API 응답 시간이 12.69s이고, G1GC를 사용했을 때 10.28s로 Serial GC에서 더 긴 응답 시간을 기록하였다.&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;따라서 G1GC에서 Full GC를 유발하기 위해 size 크기를 기존 크기의 10배로 수정해보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1725982141457&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[15.355s][info][gc] GC(176) Pause Young (Normal) (G1 Humongous Allocation) 4019M-&amp;gt;4019M(4096M) 0.807ms
[15.386s][info][gc] GC(177) Pause Full (G1 Compaction Pause) 4019M-&amp;gt;4016M(4096M) 30.147ms
[15.425s][info][gc] GC(178) Pause Full (G1 Compaction Pause) 4016M-&amp;gt;4013M(4096M) 39.234ms
[15.426s][info][gc] GC(175) Concurrent Mark Cycle 71.041ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 Full GC가 발생하게 되는데, Compaction Pause 단계에서 힙 메모리의 압축 작업을 수행하게 된다. 이 과정에서 Humongous Allocation과 Full GC가 연이어 발생하면서 Old Generation의 메모리를 정리하고 확보한다.&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;로그에서 확인할 수 있듯이 Full GC는 Old Generation을 포함한 메모리 전반을 정리하는 작업이기 때문에 Young Generation에서만 메모리 회수가 발생하는 Minor GC보다 시간이 더 많이 소요된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Minor GC는 메모리 공간을 재배치하거나 Compaction(압축) 작업을 하지 않고, 살아남은 객체를 Old Generation으로 옮기는 역할만 하지만 Full GC는 Pause Full(G1 Compaction Pause) 단계에서 메모리 공간을 압축하고, 메모리 단편화를 줄이는 작업을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;GC 로그로 각 GC 단계 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;G1GC의 가비지 수집 과정에서 이루어지는 단계가 로그의 어느 시점에서 발생하는지 정리해 보았다.&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;size18&quot;&gt;1️⃣ Regular Young-only Collection - STW&lt;/p&gt;
&lt;pre id=&quot;code_1726036080662&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[0.845s][info][gc,start    ] GC(2) Pause Young (Normal) (G1 Evacuation Pause)
[0.845s][info][gc,task     ] GC(2) Using 6 workers of 8 for evacuation
[0.849s][info][gc,phases   ] GC(2)   Pre Evacuate Collection Set: 0.1ms
[0.849s][info][gc,phases   ] GC(2)   Merge Heap Roots: 0.1ms
[0.849s][info][gc,phases   ] GC(2)   Evacuate Collection Set: 3.8ms
[0.849s][info][gc,phases   ] GC(2)   Post Evacuate Collection Set: 0.4ms
[0.849s][info][gc,phases   ] GC(2)   Other: 0.1ms
[0.849s][info][gc,heap     ] GC(2) Eden regions: 74-&amp;gt;0(72)
[0.849s][info][gc,heap     ] GC(2) Survivor regions: 2-&amp;gt;4(10)
[0.849s][info][gc,heap     ] GC(2) Old regions: 0-&amp;gt;0
[0.849s][info][gc          ] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 152M-&amp;gt;8M(260M) 4.343ms

[1.725s][info][gc] GC(9) Pause Young (Normal) (G1 Evacuation Pause) 40M-&amp;gt;16M(156M) 5.364ms
[2.197s][info][gc] GC(12) Pause Young (Prepare Mixed) (G1 Preventive Collection) 69M-&amp;gt;18M(80M) 3.283ms

[15.355s][info][gc] GC(176) Pause Young (Normal) (G1 Humongous Allocation) 4019M-&amp;gt;4019M(4096M) 0.807ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 Young 영역에서 Old 영역으로 복사되며, STW가 일시적으로 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2️⃣ Initial Mark - STW&lt;/p&gt;
&lt;pre id=&quot;code_1726061305553&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[1.234s][info][gc] GC(5) Pause Young (Initial Mark) 30M-&amp;gt;5M(256M) 3.123ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Initial Mark 단계에서는 Old Generation의 살아있는 객체들을 마킹한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3️⃣&amp;nbsp;Concurrent Mark&lt;/p&gt;
&lt;pre id=&quot;code_1726030378480&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[1.977s][info][gc] GC(11) Concurrent Mark Cycle 20.744ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Concurrent Mark는 Old Generation에 대해 병렬로 살아있는 객체를 추적하는 단계이다. 이 과정에서 STW은 발생하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4️⃣&amp;nbsp;Remark - STW&lt;/p&gt;
&lt;pre id=&quot;code_1726030424227&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[0.878s][info][gc] GC(4) Pause Remark 11M-&amp;gt;11M(54M) 0.746ms
[7.843s][info][gc] GC(22) Pause Remark 59M-&amp;gt;59M(114M) 1.898ms
[16.238s][info][gc] GC(170) Pause Remark 3389M-&amp;gt;3389M(4096M) 1.652ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Remark 단계는 Concurrent Mark 작업 후 마킹되지 않은 살아있는 객체들을 다시 확인하고 마킹하는 최종 단계이다. 이 단계에서는 짧은 시간 STW가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5️⃣&amp;nbsp;Cleanup - STW&lt;/p&gt;
&lt;pre id=&quot;code_1726030464281&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[0.878s][info][gc] GC(4) Pause Cleanup 11M-&amp;gt;11M(54M) 0.002ms
[7.854s][info][gc] GC(22) Pause Cleanup 59M-&amp;gt;59M(114M) 0.060ms
[16.250s][info][gc] GC(170) Pause Cleanup 3389M-&amp;gt;3389M(4096M) 0.133ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clean up 단계는 불필요한 객체들이 제거되고 메모리 공간이 재정리되는 단계이며, 짧게 STW가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;6️⃣ Copy - STW&lt;/p&gt;
&lt;pre id=&quot;code_1726030497171&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[0.167s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 23M-&amp;gt;3M(260M) 0.941ms
[15.386s][info][gc] GC(177) Pause Full (G1 Compaction Pause) 4019M-&amp;gt;4016M(4096M) 30.147ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Copy 작업은 G1GC에서 Evacuation Pause로 나타난다. 살아남은 객체들은 Young Generation에서 Old Generation으로 이동하거나 메모리에서 복사되며, 이 과정에서 STW가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 첫 번째 로그(Pause Young)는 Young 영역에서의 Copy 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 두 번째 로그(Pause Full)는 Old 영역에서의 Compaction 작업이며, 이 과정에서도 객체 복사가 포함될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;7️⃣ Space Reclamation - STW&lt;/p&gt;
&lt;pre id=&quot;code_1726061760670&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[2.210s][info][gc] GC(13) Pause Young (Mixed) (G1 Evacuation Pause) 20M-&amp;gt;19M(80M) 4.240ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mixed GC와 Full GC에서 Young 및 Old 세대의 데이터를 동시에 정리하고, 전체 힙 압축이 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;느낀점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;G1GC 로그를 분석해 보았을 때, 생각했던 것보다 Minor GC가 자주 발생하지만, 각 GC의 소요 시간이 짧아 애플리케이션 성능에 크게 영향을 미치지 않는다는 것을 확인할 수 있었다. 또한, Minor GC가 대부분 빠르게 처리되며, G1 특징인 Concurrent Mark Cycle이 Old Generation에서 동시 실행되어 pause time이 최소화되고 있는 것도 실제로 확인할 수 있었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;뿐만 아니라 로그에서 Initial Mark와 Concurrent Mark를 통해 객체를 추적하고, Remark와 Cleanup 단계를 통해 메모리를 최적화하는 과정을 실제로 관찰할 수 있었다. 특히 메타데이터 임계값을 초과하여 발생하는 GC나 GCLocker Initiated GC처럼 특정 상황에서만 발생되는 GC를 보면서, GC가 발생하는 다양한 조건에 대해 알게 되었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;API 응답 시간을 비교해 본 결과, Serial GC를 사용했을 때 응답 시간이 12.69초로 가장 길었다. 반면, G1GC를 사용했을 때 응답 시간은 10.28초로 가장 짧았으며, 이를 통해 G1GC가 멀티 스레드와 region 기반의 힙 관리를 통해 GC 작업을 병렬로 수행하고, 메모리 회수가 효율적으로 이루어진다는 것을 확인할 수 있었다. 본문에는 따로 적지 않았지만 ZGC는 10.85초로 G1GC보다는 약간 긴 응답 시간을 기록했지만, 마찬가지로 비교적 짧은 응답 시간을 보였다. 이를 통해 ZGC는 최신 GC로 빠른 응답을 제공하지만, G1GC가 특정 조건에서는 더 효율적일 수 있다는 것을 알 수 있었다. 이러한 비교를 통해 각 GC의 특성과 성능을 이해하고, 애플리케이션의 요구사항에 맞는 적절한 GC를 선택하는 것이 중요하다는 것을 느낄 수 있었다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Back-end</category>
      <author>서채리</author>
      <guid isPermaLink="true">https://chaewsscode.tistory.com/261</guid>
      <comments>https://chaewsscode.tistory.com/261#entry261comment</comments>
      <pubDate>Wed, 11 Sep 2024 00:58:14 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] P6Spy 쿼리 로깅</title>
      <link>https://chaewsscode.tistory.com/260</link>
      <description>&lt;figure id=&quot;og_1725033924995&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;[JPA] N+1 문제 발생 지점 찾아 해결하기&quot; data-og-description=&quot;이전 글에서 N+1 문제와 해결 방법에 대해 정리해보았다.&amp;nbsp;[JPA] N+1 문제 해결 방법N+1 문제란?N+1 문제는 ORM(객체-관계 매핑) 기술에서 특정 객체를 조회할 때,  그 객체와 연관된 다른 객체들도 각각&quot; data-og-host=&quot;chaewsscode.tistory.com&quot; data-og-source-url=&quot;https://chaewsscode.tistory.com/259&quot; data-og-url=&quot;https://chaewsscode.tistory.com/259&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bMXbqe/hyWVTbONQa/rPgLNOWaCbFrMENFPLNg80/img.png?width=800&amp;amp;height=308&amp;amp;face=0_0_800_308,https://scrap.kakaocdn.net/dn/bthbid/hyWVYEaK6Z/vIqd6ee3Uqbiy21iUnfXzk/img.png?width=800&amp;amp;height=308&amp;amp;face=0_0_800_308&quot;&gt;&lt;a href=&quot;https://chaewsscode.tistory.com/259&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chaewsscode.tistory.com/259&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bMXbqe/hyWVTbONQa/rPgLNOWaCbFrMENFPLNg80/img.png?width=800&amp;amp;height=308&amp;amp;face=0_0_800_308,https://scrap.kakaocdn.net/dn/bthbid/hyWVYEaK6Z/vIqd6ee3Uqbiy21iUnfXzk/img.png?width=800&amp;amp;height=308&amp;amp;face=0_0_800_308');&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;[JPA] N+1 문제 발생 지점 찾아 해결하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이전 글에서 N+1 문제와 해결 방법에 대해 정리해보았다.&amp;nbsp;[JPA] N+1 문제 해결 방법N+1 문제란?N+1 문제는 ORM(객체-관계 매핑) 기술에서 특정 객체를 조회할 때,  그 객체와 연관된 다른 객체들도 각각&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chaewsscode.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 application.yml의 logging 설정을 통해 SQL 쿼리에 바인딩되는 파라미터 값을 확인할 수 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1724920467050&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true

logging:
  level:
    org:
      hibernate:
        SQL: debug
        type.descriptor.sql: trace
    org.hibernate.orm.jdbc.bind: trace&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;logging을 통한 Hibernate 로그 설정은 이처럼 application.yml 파일에서 간단하게 설정할 수 있지만 로그를 직관적으로 확인하기 불편하다는 단점이 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-29 18.18.49.png&quot; data-origin-width=&quot;2394&quot; data-origin-height=&quot;922&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3HxsT/btsJlYynWct/pomCapyNXabkzFtYvOzdmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3HxsT/btsJlYynWct/pomCapyNXabkzFtYvOzdmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3HxsT/btsJlYynWct/pomCapyNXabkzFtYvOzdmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3HxsT%2FbtsJlYynWct%2FpomCapyNXabkzFtYvOzdmK%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;760&quot; height=&quot;293&quot; data-filename=&quot;스크린샷 2024-08-29 18.18.49.png&quot; data-origin-width=&quot;2394&quot; data-origin-height=&quot;922&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;P6Spy란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;P6Spy는 기존 애플리케이션 코드를 변경하지 않고도 데이터베이스의 데이터를 가로채 로그를 남길 수 있는 프레임워크이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 DataSource를 P6SpyDataSource가 감싸고, JDBC 요청이 발생할 때마다 P6Spy가 프록시로 감싸 해당 정보를 분석하고 로그를 남기는 방식으로 작동한다.&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;SpringBoot 3.x.x 부터는 자동 설정(auto-configuration) 방식이 변화해 데이터 소스의 꾸미기(decorating) 작업을 위해 '&lt;a href=&quot;https://github.com/gavlyukovskiy/spring-boot-data-source-decorator&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gavlyukovskiy/spring-boot-data-source-decorator&lt;/a&gt;'와 같은 별도의 프로젝트를 활용한다. 따라서 기존처럼 P6Spy를 직접 프로젝트에 적용하는 방식이 아닌, gavlyukovskiy 프로젝트를 통해 더 체계적으로 P6Spy를&amp;nbsp;설정할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;P6Spy 구성&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  자동 구성 (start 사용 ⭕️)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle에 p6spy starter 의존성을 추가해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1725033366594&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
}&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; &lt;span&gt;&amp;nbsp;&lt;/span&gt;수동 구성 (start 사용 ❌)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다 세밀한 설정이나 커스터마이징이 필요할 경우, p6spy starter 의존성을 추가하지 않고 p6spy를 수동으로 추가하여 커스텀 설정을 구현할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1724924244304&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    implementation 'p6spy:p6spy:3.9.1'
    implementation 'com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle에 p6spy 라이브러리 의존성을 추가해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 의존성은 P6Spy 라이브러리와, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;스프링 부트와&amp;nbsp;&lt;/span&gt;P6Spy 간의 자동 구성을 지원하는 라이브러리를 제공한다.&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-filename=&quot;스크린샷 2024-08-29 18.40.48.png&quot; data-origin-width=&quot;1158&quot; data-origin-height=&quot;166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAUoWz/btsJkFShIu0/Y98d8slkHQoCNCfKFrsJV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAUoWz/btsJkFShIu0/Y98d8slkHQoCNCfKFrsJV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAUoWz/btsJkFShIu0/Y98d8slkHQoCNCfKFrsJV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAUoWz%2FbtsJkFShIu0%2FY98d8slkHQoCNCfKFrsJV0%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;760&quot; height=&quot;109&quot; data-filename=&quot;스크린샷 2024-08-29 18.40.48.png&quot; data-origin-width=&quot;1158&quot; data-origin-height=&quot;166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 resources 폴더 하위에 위와 같은 폴더 구조를 만든 후 org.springframework.boot.autoconfigure.AutoConfiguration.imports 이름의 파일을 만들어 아래 내용을 입력해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1724924642026&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;com.github.gavlyukovskiy.boot.jdbc.decorator.DataSourceDecoratorAutoConfiguration&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;이 파일은  스프링 부트가 DataSourceDecoratorAutoConfiguration 클래스를 찾아 자동으로 로드하도록 지시한다.&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;DataSourceDecoratorAutoConfiguration 클래스는 스프링 부트 애플리케이션의 데이터 소스를 자동으로 P6Spy 객체로 감싸는 역할을 한다. 이로 인해, 데이터베이스와의 상호작용은 P6Spy에 의해 로깅되고 추적되며, 이 모든 과정은 사용자가 코드를 직접 수정하지 않아도 자동으로 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설정 완료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;P6Spy를 설정한 후 테스트해보면, SQL 쿼리에서 ? 대신 실제 인자값이 표시되며, 쿼리가 한 줄로 출력되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1725035524641&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select p1_0.id,b1_0.id,b1_0.created_at,b1_0.modified_at,b1_0.name,p1_0.created_at,p1_0.is_deleted,p1_0.modified_at,p1_0.name,p1_0.price from product p1_0 join brand b1_0 on b1_0.id=p1_0.brand_id where (p1_0.is_deleted = false) order by p1_0.created_at desc offset 0 rows fetch first 21 rows only;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-31 01.58.52.png&quot; data-origin-width=&quot;1761&quot; data-origin-height=&quot;111&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bA5ij9/btsJnM35kiW/cJHwH07dZzh3TohKdBgod0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bA5ij9/btsJnM35kiW/cJHwH07dZzh3TohKdBgod0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bA5ij9/btsJnM35kiW/cJHwH07dZzh3TohKdBgod0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbA5ij9%2FbtsJnM35kiW%2FcJHwH07dZzh3TohKdBgod0%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;760&quot; height=&quot;48&quot; data-filename=&quot;스크린샷 2024-08-31 01.58.52.png&quot; data-origin-width=&quot;1761&quot; data-origin-height=&quot;111&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SQL 쿼리 포맷팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가독성을 위해 포맷 설정파일을 추가해 한 줄로 출력되는 SQL 쿼리를 포맷팅해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1725037540444&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class P6SpyConfig implements MessageFormattingStrategy, ChaewsstoreConfig {

    @PostConstruct
    public void setLogMessageFormat() {
        P6SpyOptions.getActiveInstance().setLogMessageFormat(this.getClass().getName());
    }

    @Override
    public String formatMessage(int connectionId, String now, long elapsed, String category,
        String prepared, String sql, String url) {
        return String.format(&quot;[%s] | %d ms | %s&quot;, category, elapsed, formatSql(category, sql));
    }

    private String formatSql (String category, String sql){
        if (sql != null &amp;amp;&amp;amp; !sql.trim().isEmpty() &amp;amp;&amp;amp; Category.STATEMENT.getName()
            .equals(category)) {
            String trimmedSQL = sql.trim().toLowerCase(Locale.ROOT);
            if (trimmedSQL.startsWith(&quot;create&quot;) || trimmedSQL.startsWith(&quot;alter&quot;)
                || trimmedSQL.startsWith(&quot;comment&quot;)) {
                sql = FormatStyle.DDL.getFormatter().format(sql);
            } else {
                sql = FormatStyle.BASIC.getFormatter().format(sql);
            }
            return sql;
        }
        return sql;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;포맷 설정(setLogMessageFormat 메서드)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;P6Spy의 로그 메세지 포맷을 현재 클래스 이름인 P6SpyConfig로 설정해 로그 메세지를 해당 클래스의 포맷 방식으로 출력한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SQL 포맷팅(formatMessage 메서드)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;SQL 쿼리 형식을 지정한 포맷에 맞게 변환하여 출력한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SQL 포맷 로직(formatSql 메서드)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;SQL 쿼리를 검사하여, 쿼리가 비어있지 않고 Category.STATEMENT 카테고리에 해당하는 경우 SQL 쿼리를 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;DDL쿼리와 일반 쿼리를 구분하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;포맷한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-31 02.04.59.png&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xcqqa/btsJlx17AGp/gjlkocjqm8S2BCje2QX9sK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xcqqa/btsJlx17AGp/gjlkocjqm8S2BCje2QX9sK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xcqqa/btsJlx17AGp/gjlkocjqm8S2BCje2QX9sK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXcqqa%2FbtsJlx17AGp%2Fgjlkocjqm8S2BCje2QX9sK%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;514&quot; height=&quot;448&quot; data-filename=&quot;스크린샷 2024-08-31 02.04.59.png&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;488&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;+ 호출 스택 트레이스&lt;/h4&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;pre id=&quot;code_1725039042341&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class P6SpyConfig implements MessageFormattingStrategy, ChaewsstoreConfig {

    @PostConstruct
    public void setLogMessageFormat() {
        P6SpyOptions.getActiveInstance().setLogMessageFormat(this.getClass().getName());
    }

    @Override
    public String formatMessage(int connectionId, String now, long elapsed, String category,
        String prepared, String sql, String url) {
        return String.format(&quot;[%s] | %d ms | %s&quot;, category, elapsed,
            formatSql(category, sql));
    }

    private String stackTrace() {
        return Stream.of(new Throwable().getStackTrace())
            .filter(t -&amp;gt; t.toString().startsWith(&quot;com.chaewsstore&quot;) &amp;amp;&amp;amp; !t.toString().contains(
                ClassUtils.getUserClass(this).getName()))
            .map(StackTraceElement::toString)
            .collect(Collectors.joining(&quot;\n&quot;));
    }

    private String formatSql(String category, String sql) {
        if (sql != null &amp;amp;&amp;amp; !sql.trim().isEmpty() &amp;amp;&amp;amp; Category.STATEMENT.getName().equals(category)) {
            String trimmedSql = sql.trim().toLowerCase(Locale.ROOT);
            return stackTrace() + (trimmedSql.startsWith(&quot;create&quot;) || trimmedSql.startsWith(&quot;alter&quot;)
                || trimmedSql.startsWith(&quot;comment&quot;)
                ? FormatStyle.DDL.getFormatter().format(sql)
                : FormatStyle.BASIC.getFormatter().format(sql));
        }
        return sql;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-31 02.54.39.png&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;661&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bt9yI7/btsJlc4Vfyp/2RPg7phUimJBgyV2KDMW50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bt9yI7/btsJlc4Vfyp/2RPg7phUimJBgyV2KDMW50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bt9yI7/btsJlc4Vfyp/2RPg7phUimJBgyV2KDMW50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbt9yI7%2FbtsJlc4Vfyp%2F2RPg7phUimJBgyV2KDMW50%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;748&quot; height=&quot;515&quot; data-filename=&quot;스크린샷 2024-08-31 02.54.39.png&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;661&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;%F0%9F%93%9A%20%EC%B0%B8%EA%B3%A0-1&quot; style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;참고&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://shanepark.tistory.com/415&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Spring Boot JPA] P6Spy 활용해 쿼리 로그 확인하기&lt;/a&gt;&lt;/p&gt;</description>
      <category>Back-end/TroubleShooting</category>
      <author>서채리</author>
      <guid isPermaLink="true">https://chaewsscode.tistory.com/260</guid>
      <comments>https://chaewsscode.tistory.com/260#entry260comment</comments>
      <pubDate>Sat, 31 Aug 2024 03:00:33 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] N+1 문제 발생 지점 찾아 해결하기</title>
      <link>https://chaewsscode.tistory.com/259</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1️⃣&lt;span&gt; N+1 문제와 해결 방법&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 N+1 문제와 해결 방법에 대해 정리해보았다.&lt;/p&gt;
&lt;figure id=&quot;og_1724920017762&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;[JPA] N+1 문제 해결 방법&quot; data-og-description=&quot;N+1 문제란?N+1 문제는 ORM(객체-관계 매핑) 기술에서 특정 객체를 조회할 때,  그 객체와 연관된 다른 객체들도 각각 조회되면서 N번의 추가 쿼리가 발생하는 문제를 말한다. N+1 문제는 단일 쿼리로&quot; data-og-host=&quot;chaewsscode.tistory.com&quot; data-og-source-url=&quot;https://chaewsscode.tistory.com/258&quot; data-og-url=&quot;https://chaewsscode.tistory.com/258&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/KqCR4/hyWVTbrcP6/goHWAVKNFoown0K1qsH61K/img.jpg?width=800&amp;amp;height=660&amp;amp;face=0_0_800_660,https://scrap.kakaocdn.net/dn/SQYK4/hyWVWss6eJ/i6J5TweDTcy3wFCj5lkaPK/img.jpg?width=800&amp;amp;height=660&amp;amp;face=0_0_800_660,https://scrap.kakaocdn.net/dn/cLDXj3/hyWVYRlh4l/Ba0AVb4v8Srx3g8fGghrt0/img.jpg?width=1558&amp;amp;height=1287&amp;amp;face=0_0_1558_1287&quot;&gt;&lt;a href=&quot;https://chaewsscode.tistory.com/258&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chaewsscode.tistory.com/258&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/KqCR4/hyWVTbrcP6/goHWAVKNFoown0K1qsH61K/img.jpg?width=800&amp;amp;height=660&amp;amp;face=0_0_800_660,https://scrap.kakaocdn.net/dn/SQYK4/hyWVWss6eJ/i6J5TweDTcy3wFCj5lkaPK/img.jpg?width=800&amp;amp;height=660&amp;amp;face=0_0_800_660,https://scrap.kakaocdn.net/dn/cLDXj3/hyWVYRlh4l/Ba0AVb4v8Srx3g8fGghrt0/img.jpg?width=1558&amp;amp;height=1287&amp;amp;face=0_0_1558_1287');&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;[JPA] N+1 문제 해결 방법&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;N+1 문제란?N+1 문제는 ORM(객체-관계 매핑) 기술에서 특정 객체를 조회할 때,  그 객체와 연관된 다른 객체들도 각각 조회되면서 N번의 추가 쿼리가 발생하는 문제를 말한다. N+1 문제는 단일 쿼리로&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chaewsscode.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방법 간단 요약&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #fffebd;&quot;&gt;Fetch Join 사용&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPQL의 join fetch를 사용하여 부모 엔티티를 조회할 때 연관된 엔티티를 함께 가져온다.&lt;br /&gt;@Query 어노테이션을 사용하여 메서드를 작성해야 한다.&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;span style=&quot;background-color: #fffebd;&quot;&gt;배치 사이즈 설정&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate의 배치 사이즈 설정을 통해 연관된 엔티티를 한 번의 쿼리로 가져오도록 최적화한다.&lt;br /&gt;application.yml에서 default_batch_fetch_size를 전역 설정하거나, @BatchSize로 특정 엔티티에 대해 설정할 수 있다.&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;span style=&quot;background-color: #fffebd;&quot;&gt;Entity Graph 사용&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EntityGraph를 통해 기본 Lazy 로딩 설정을 무시하고 연관된 엔티티를 Eager 로딩하도록 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2️⃣ 문제 발생 지점 찾기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제 프로젝트에서 N+1 문제가 발생하는 지점을 찾아 해결해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N+1 문제는 데이터베이스에서 발생하는 성능 이슈이기 때문에, 이를 해결하려면 쿼리가 실제로 어떻게 발생하고 있는지 확인해야 한다.&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;application.yml에서 아래 설정을 추가하면 Hibernate가 실행하는 SQL 쿼리와 바인딩되는 파라미터 값을 로그로 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1724920467050&quot; class=&quot;yaml&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  jpa:
    show-sql: true	# Hibernate가 실행하는 SQL 쿼리를 콘솔에 출력
    
    properties:
      hibernate:
        format_sql: true	# 출력되는 SQL 쿼리 포맷팅

logging:
  level:
    org:
      hibernate:
        SQL: debug
        type.descriptor.sql: trace
    org.hibernate.orm.jdbc.bind: trace	# SQL 쿼리에 바인딩되는 파라미터 값들을 함께 출력&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;ProductService 클래스에서 Slice&amp;lt;Product&amp;gt;를 반환하는 findAllByOrderByCreatedAtDesc 메서드는 모든 상품을 생성일 기준 내림차순으로 정렬하여 반환하는 역할을 한다. 하지만 &lt;u&gt;각 상품과 연관된 브랜드 정보를 조회할 때, 상품 하나하나에 대해 별도의 SELECT 쿼리가 발생&lt;/u&gt;할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1725032029168&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Product extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;brand_id&quot;)
    private Brand brand;
    
    ....
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1724953468748&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ProductService {
    
    @Transactional(readOnly = true)
    public Slice&amp;lt;ReadProductResponseDto&amp;gt; readProductList(Pageable pageable) {
        return productRepository.findAllByOrderByCreatedAtDesc(pageable).map(ReadProductResponseDto::of);
    }
}&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;해당 메서드의 SQL 로그를 출력해보면, 아래와 같은 결과를 얻을 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1724953105414&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2024-08-30T02:29:22.290+09:00 DEBUG 96740 --- [nio-8001-exec-2] org.hibernate.SQL                        : 
Hibernate: 
    select
        p1_0.id,
        p1_0.brand_id
    from
        product p1_0 
    order by
        p1_0.created_at desc offset ? rows fetch first ? rows only
2024-08-30T02:29:22.296+09:00 TRACE 96740 --- [nio-8001-exec-2] org.hibernate.orm.jdbc.bind              : binding parameter [1] as [INTEGER] - [0]
2024-08-30T02:29:22.297+09:00 TRACE 96740 --- [nio-8001-exec-2] org.hibernate.orm.jdbc.bind              : binding parameter [2] as [INTEGER] - [21]
2024-08-30T02:29:22.323+09:00 DEBUG 96740 --- [nio-8001-exec-2] org.hibernate.SQL                        : 
Hibernate: 
    select
        b1_0.id,
        b1_0.name 
    from
        brand b1_0 
    where
        b1_0.id=?
2024-08-30T02:29:22.323+09:00 TRACE 96740 --- [nio-8001-exec-2] org.hibernate.orm.jdbc.bind              : binding parameter [1] as [BIGINT] - [4]
2024-08-30T02:29:22.324+09:00 DEBUG 96740 --- [nio-8001-exec-2] org.hibernate.SQL                        : 
Hibernate: 
    select
        b1_0.id,
        b1_0.name 
    from
        brand b1_0 
    where
        b1_0.id=?
2024-08-30T02:29:22.324+09:00 TRACE 96740 --- [nio-8001-exec-2] org.hibernate.orm.jdbc.bind              : binding parameter [1] as [BIGINT] - [3]
2024-08-30T02:29:22.325+09:00 DEBUG 96740 --- [nio-8001-exec-2] org.hibernate.SQL                        : 
Hibernate: 
    select
        b1_0.id,
        b1_0.name 
    from
        brand b1_0 
    where
        b1_0.id=?
2024-08-30T02:29:22.325+09:00 TRACE 96740 --- [nio-8001-exec-2] org.hibernate.orm.jdbc.bind              : binding parameter [1] as [BIGINT] - [5]
2024-08-30T02:29:22.329+09:00 DEBUG 96740 --- [nio-8001-exec-2] org.hibernate.SQL                        : 
Hibernate: 
    select
        b1_0.id,
        b1_0.name 
    from
        brand b1_0 
    where
        b1_0.id=?
2024-08-30T02:29:22.329+09:00 TRACE 96740 --- [nio-8001-exec-2] org.hibernate.orm.jdbc.bind              : binding parameter [1] as [BIGINT] - [2]
2024-08-30T02:29:22.330+09:00 DEBUG 96740 --- [nio-8001-exec-2] org.hibernate.SQL                        : 
Hibernate: 
    select
        b1_0.id,
        b1_0.name 
    from
        brand b1_0 
    where
        b1_0.id=?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 로그를 보면, 각 Product 엔티티와 연관된 Brand 엔티티를 조회하기 위해 개별적인 SELECT 쿼리가 여러 번 실행되는 것을 확인할 수 있다. 이처럼 불필요하게 많은 쿼리가 발생하는 것을 &lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;N+1 문제&lt;/b&gt;&lt;/span&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3️⃣ 문제 해결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 ProductRepository에 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;@Query 어노테이션을&lt;span&gt; 사용하여 fetch join을 적용할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724954143774&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface ProductRepository extends JpaRepository&amp;lt;Product, Long&amp;gt; {

    @Query(&quot;SELECT p FROM Product p JOIN FETCH p.brand ORDER BY p.createdAt DESC&quot;)
    Slice&amp;lt;Product&amp;gt; findAllByOrderByCreatedAtDesc(Pageable pageable);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPQL의 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;fetch join을&lt;/span&gt;&amp;nbsp;사용하여 Product 엔티티를 조회할 때 연관된 Brand 엔티티도 함께 조회하도록 설정해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1724953840751&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2024-08-30T02:49:56.191+09:00 DEBUG 97058 --- [nio-8001-exec-2] org.hibernate.SQL                        : 
Hibernate: 
    select
        p1_0.id,
        b1_0.id,
        b1_0.name,
        p1_0.name 
    from
        product p1_0 
    join
        brand b1_0 
            on b1_0.id=p1_0.brand_id 
    offset ? rows fetch first ? rows only
2024-08-30T02:49:56.219+09:00 TRACE 97058 --- [nio-8001-exec-2] org.hibernate.orm.jdbc.bind              : binding parameter [1] as [INTEGER] - [0]
2024-08-30T02:49:56.219+09:00 TRACE 97058 --- [nio-8001-exec-2] org.hibernate.orm.jdbc.bind              : binding parameter [2] as [INTEGER] - [21]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 SQL 로그를 보면, 필요한 Product와 Brand 데이터를 모두 포함하고 있고 JOIN 쿼리가 한 번만 실행되었음을 확인할 수 있다.&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;이처럼 Fetch Join을 사용함으로써, 각 엔티티에 대해 개별적인 SELECT 쿼리를 실행하지 않고 한 번의 JOIN 쿼리로 부모 엔티티와 연관된 자식 엔티티를 함께 조회해 N+1 문제를 방지할 수 있다.&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;Fetch Join을 사용한 이유&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;부모인 Product와 자식 Brand 엔티티가 OneToOne 관계라 Fetch Join의 페이징 처리 제한 문제가 발생하지 않았다.&lt;/li&gt;
&lt;li&gt;Fetch Join은 INNER JOIN을 사용하여 Product와 연관된 Brand 엔티티가 반드시 있을 경우에만 데이터를 가져오므로 불필요한 데이터를 조회하지 않는다. EntityGraph는 LEFT OUTER JOIN을 사용해 연관된 Brand 엔티티가 없어도 데이터를 가져올 수 있다.&lt;/li&gt;
&lt;li&gt;Batch Size 조절은 OneToMany 관계에서 N+1 문제를 해결하기 유용하지만, OneToOne 관계에서는&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Fetch Join을 사용할 경우에도&lt;span&gt; 충분히 &lt;/span&gt;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 글: logging 옵션 대신, P6Spy로 쿼리 로깅 관리하기&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SQL 쿼리와 파라미터를 한눈에 확인하기 어려운&lt;span&gt;&amp;nbsp;&lt;/span&gt;logging 옵션 대신, P6Spy로 쿼리 로깅을 관리하는 글을 추가적으로 써보았다.&lt;/p&gt;
&lt;figure id=&quot;og_1725263936810&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;[Spring Boot] P6Spy 쿼리 로깅&quot; data-og-description=&quot;  문제 상황&amp;nbsp;[JPA] N+1 문제 발생 지점 찾아 해결하기이전 글에서 N+1 문제와 해결 방법에 대해 정리해보았다.&amp;nbsp;[JPA] N+1 문제 해결 방법N+1 문제란?N+1 문제는 ORM(객체-관계 매핑) 기술에서 특정 객체&quot; data-og-host=&quot;chaewsscode.tistory.com&quot; data-og-source-url=&quot;https://chaewsscode.tistory.com/260&quot; data-og-url=&quot;https://chaewsscode.tistory.com/260&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/VA8Gf/hyWZaQ1bHl/J5luKrijQn6C0YOvrVV8C0/img.png?width=800&amp;amp;height=308&amp;amp;face=0_0_800_308,https://scrap.kakaocdn.net/dn/dhOrsa/hyWVXeInK2/rf8ENo8dtkZ4n2319lbqU1/img.png?width=800&amp;amp;height=308&amp;amp;face=0_0_800_308,https://scrap.kakaocdn.net/dn/bbu4JI/hyWVYrabig/2mw3VyIDSiciHnHp8R52VK/img.png?width=2394&amp;amp;height=922&amp;amp;face=0_0_2394_922&quot;&gt;&lt;a href=&quot;https://chaewsscode.tistory.com/260&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chaewsscode.tistory.com/260&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/VA8Gf/hyWZaQ1bHl/J5luKrijQn6C0YOvrVV8C0/img.png?width=800&amp;amp;height=308&amp;amp;face=0_0_800_308,https://scrap.kakaocdn.net/dn/dhOrsa/hyWVXeInK2/rf8ENo8dtkZ4n2319lbqU1/img.png?width=800&amp;amp;height=308&amp;amp;face=0_0_800_308,https://scrap.kakaocdn.net/dn/bbu4JI/hyWVYrabig/2mw3VyIDSiciHnHp8R52VK/img.png?width=2394&amp;amp;height=922&amp;amp;face=0_0_2394_922');&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;[Spring Boot] P6Spy 쿼리 로깅&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  문제 상황&amp;nbsp;[JPA] N+1 문제 발생 지점 찾아 해결하기이전 글에서 N+1 문제와 해결 방법에 대해 정리해보았다.&amp;nbsp;[JPA] N+1 문제 해결 방법N+1 문제란?N+1 문제는 ORM(객체-관계 매핑) 기술에서 특정 객체&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chaewsscode.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>Back-end/TroubleShooting</category>
      <author>서채리</author>
      <guid isPermaLink="true">https://chaewsscode.tistory.com/259</guid>
      <comments>https://chaewsscode.tistory.com/259#entry259comment</comments>
      <pubDate>Fri, 30 Aug 2024 02:28:00 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] N+1 문제 해결 방법</title>
      <link>https://chaewsscode.tistory.com/258</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;N+1 문제란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N+1 문제는 ORM(객체-관계 매핑) 기술에서 특정 객체를 조회할 때,  그 객체와 연관된 다른 객체들도 각각 조회되면서 N번의 추가 쿼리가 발생하는 문제를 말한다. N+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;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;FetchType이 EAGER(즉시 로딩)인 경우&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;findAll() 호출&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;findAll() 메서드를 호출하면, JPQL의 SELECT t FROM Team t 구문이 생성된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;JPQL 분석 및 SQL 생성&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;JPQL을 분석한 후, 데이터베이스에서 실행될 SQL 쿼리 SELECT * FROM &lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;Team&lt;/span&gt; 가 실행된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Team 엔티티 인스턴스 생성&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;데이터베이스에서 Team 엔티티의 모든 결과를 가져와 Team 엔티티 인스턴스들이 메모리에 생성된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;연관된 User 엔티티 즉시 로딩&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;(N+1 문제 발생)&lt;/span&gt;&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #e6e6ff;&quot;&gt;모든 Team 엔티티에 대해 즉시 로딩 시도&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;EAGER 로딩 전략에 따라,&amp;nbsp;&lt;u&gt;각 Team 엔티티와 연관된 User 엔티티도 즉시 로딩&lt;/u&gt;해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;각 Team에 대해 추가 SQL 실행&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;각&amp;nbsp;Team&amp;nbsp;엔티티에 대해&amp;nbsp;User&amp;nbsp;엔티티를 로딩하기 위해&amp;nbsp;SELECT * FROM User WHERE team_id = ?&amp;nbsp;라는&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;추가적인&amp;nbsp;&lt;/span&gt;SQL 쿼리가 각&amp;nbsp;Team&amp;nbsp;엔티티마다 실행된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;N+1 문제 발생&lt;br /&gt;&lt;/span&gt;Team&lt;span style=&quot;caret-color: auto; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;엔티티가 10개라면, 10개의 추가 SQL 쿼리가 실행되어 N+1 문제가 발생한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&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;span style=&quot;color: #333333; text-align: start; background-color: #ffffff;&quot;&gt;FetchType이 LAZY(지연 로딩)인 경우&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;findAll() 호출&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;findAll() 메서드를 호출하면, JPQL의 SELECT t FROM &lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;Team&lt;/span&gt; t 구문이 생성된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;JPQL 분석 및 SQL 생성&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;JPQL을 분석한 후, 데이터베이스에서 실행될 SQL 쿼리 SELECT * FROM &lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;Team&lt;/span&gt; 가 실행된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Team 엔티티 인스턴스 생성&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;데이터베이스에서 Team 엔티티의 모든 결과를 가져와 Team 엔티티 인스턴스들이 메모리에 생성된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;연관된 User 엔티티 지연 로딩&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;(N+1 문제 발생)&lt;/span&gt;&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;User 엔티티를 실제로 필요로 할 때 로딩 시도&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;LAZY 로딩 전략에 따라, User 엔티티는&amp;nbsp;&lt;u&gt;실제로 필요할 때까지&lt;/u&gt;&amp;nbsp;로딩되지 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;background-color: #e6e6ff;&quot;&gt;각 Team 엔티티에서 User 엔티티에 접근할 때&lt;/span&gt; 추가 SQL 실행&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;team.getUsers()를 호출하는 순간&amp;nbsp;User&amp;nbsp;엔티티를 로딩하기 위해&amp;nbsp;SELECT * FROM User WHERE team_id = ?&amp;nbsp;라는&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;추가적인&amp;nbsp;&lt;/span&gt;SQL 쿼리가 각&amp;nbsp;Team&amp;nbsp;엔티티마다 실행된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;N+1 문제 발생&lt;br /&gt;&lt;/span&gt;Team&amp;nbsp;엔티티가 10개라면, 10개의 추가 SQL 쿼리가 실행되어 N+1 문제가 발생한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;gt; EAGER는 데이터를 조회할 때마다 연관된 엔티티를 자동으로 즉시 로딩하면서 N+1 문제가 발생한다.&lt;br /&gt;&amp;gt; LAZY는 연관된 엔티티에 접근할 때마다 지연 로딩이 발생해 N+1 문제가 발생한다.&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;Eager Loading은 N+1 문제를 부분적으로 해결할 수 있지만, 엔티티 관계가 복잡해지면 N+1 문제를 해결하지 못하는 경우가 많다. 또한 어떤 연관관계까지 Join 쿼리로 조회될지 예측하기 어려워 필요하지 않은 데이터까지 로딩되어 비효율적일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Fetch Join 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Fetch Join은&lt;span&gt; JPQL을 사용하여 &lt;u&gt;처음 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;부모 엔티티를&lt;/span&gt;&amp;nbsp;데이터베이스에서 데이터를 조회할 때부터&lt;/u&gt; Join 쿼리를 발생시켜 연관된 데이터까지 같이 가져오는 방법이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1724739549285&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface TeamRepository extends JpaRepository&amp;lt;Team, Long&amp;gt; {

    @Query(&quot;select t from Team t join fetch t.users&quot;)
    List&amp;lt;Team&amp;gt; findAllFetchJoin();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;이처럼 @Query 어노테이션을 사용하여 별도의 메서드를 만들어주어야 한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p 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;Fetch Join&amp;nbsp; vs&amp;nbsp; Join&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Fetch Join&lt;/b&gt;은 ORM에서 사용되는 기법으로, 데이터베이스 스키마를 엔티티로 변환하고 영속성 컨텍스트에 저장한다. Fetch Join을 통해 연관된 엔티티를 한 번의 쿼리로 조회하면, 연관 관계는 영속성 컨텍스트의 1차 캐시에 저장되어 이후에 추가 쿼리 없이 데이터를 가져와 성능이 향상된다.&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;Join&lt;/b&gt;은 SQL 쿼리에서 테이블을 결합하여 데이터를 조회하는 방법이며 ORM의 영속성 컨텍스트나 엔티티와는 관련이 없다.&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;rarr; Fetch Join은 ORM을 통해 RDB와의 패러다임을 차이를 줄이고 성능을 개선하는데 유리하지만, Join은 데이터 결합에만 중점을 두어 ORM의 캐시나 연관 엔티티 로딩에 영향을 주지 않는다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #f6fdd7;&quot;&gt;Pagination&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬렉션을 Fetch Join하여 페이징 처리하는 경우 다음과 같은 경고 메세지가 발생한다.&lt;/p&gt;
&lt;pre id=&quot;code_1724740691203&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WARN 79170 --- [    Test worker] o.h.h.internal.ast.QueryTranslatorImpl   : HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 페이징이 데이터베이스 레벨에서 수행되지 않고, 애플리케이션 메모리에서 수행된다는 경고이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컬렉션을 포함한 Fetch Join&lt;/b&gt;을 사용하면 여러 테이블을 조인하여 결과를 가져올 때, &lt;u&gt;컬렉션의 모든 데이터를 메모리에 로드&lt;/u&gt;한 후, 애플리케이션 레벨에서 필요한 페이지만큼 반환하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 사실상 페이징의 목적이 없어지는 것과 마찬가지이다. 예를 들어 100만건의 데이터 중 10건만 페이징하려고 했더라도 100만건을 모두 메모리에 로드하게 되어 &lt;u&gt;OOM&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Out Of Memory)이 발생&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;따라서 컬렉션 Fetch Join에서 페이징이 필요하다면 데이터베이스에서 직접 페이징을 처리할 수 있는 일반 Join 쿼리를 사용하거나&amp;nbsp;아래에서 설명할&amp;nbsp;&lt;u&gt;BatchSize&lt;/u&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;u&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;옵션&lt;/u&gt;을 설정하는 것이 바람직하다.&lt;/span&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;pre id=&quot;code_1724745143420&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;레코드   Team    User
1      Team1   User1
2      Team1   User2
3      Team1   User3&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;Fetch Join을 사용하게 되면 중복된 데이터가 발생할 수 있다. 예를 들어&lt;span&gt; Team&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;객체를 조회하는 경우, 결과는 N개의 자식 엔티티(ex.&lt;span&gt; User&lt;/span&gt;)만큼 중복 생성된다. 이러한 중복을 처리하기 위해&amp;nbsp;JPQL에서는&amp;nbsp;&lt;span&gt;Distinct를&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;마찬가지로 페이징을 처리할때도 중복된 데이터가 있을 수 있으니 JPA는 기존 페이징 처리 방식인 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;SQL 쿼리 레벨에서 LIMIT와 OFFSET을 사용하여 필요한 페이지만큼 결과를 제한적으로 가져오는 것이 아닌, 모든 데이터를 메모리에 로드한 후 애플리케이션 단에서 페이징을 처리하는 것이다.&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&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;size18&quot;&gt;  JPQL&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;Distinct !=&lt;/span&gt; SQL&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;Distinct&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SQL의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;Distinct&lt;/span&gt;&lt;/b&gt;는 &lt;u&gt;데이터베이스 레벨&lt;/u&gt;에서 작동하며&amp;nbsp;조인으로 인해 생성된 &lt;u&gt;결과 세트에서 각 행을 비교하여 중복된 행을 제거&lt;/u&gt;한다. SQL Distinct는 행 단위로 중복을 제거하기 때문에, 만약&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;Article1이 Opinion1&lt;/span&gt;과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;Opinion2라는 두 개의 연관된 의견을 가지고 있다면, 두 행은 서로 다른 것으로 간주되어 Distinct를 사용하더라도 두 행이 모두 결과에 포함된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;반면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;JPQL의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;Distinct&lt;/span&gt;&lt;/b&gt;는 &lt;u&gt;애플리케이션 레벨&lt;/u&gt;에서 작동하며,&amp;nbsp;조회한 엔티티(ex.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;Article&lt;/span&gt;)의 중복을 제거한다. 이는 SQL의 Distinct와 달리&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;메모리에 로드된 엔티티 객체를 기준&lt;/u&gt;으로 중복 여부를 판단하기 때문에, 중복된 Article 엔티티를 애플리케이션 매모리 내에서 제거하여 동일한 부모 엔티티가 여러 번 생성되는 문제를 해결할 수 있다.&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;&lt;span style=&quot;background-color: #f6fdd7;&quot;&gt;둘 이상의 Collection Fetch Join 제약&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SQL 쿼리에서 다수의 컬렉션을 한번에 조인할 경우, 조인 결과가 예상치 못하게 중복되거나 데이터가 의도하지 않은 방식으로 결합되어 잘못된 결과가 발생할 수 있다. 따라서 ~ToMany 컬렉션 조인이 2개 이상을 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Fetch Join 할&lt;/span&gt;&amp;nbsp;경우 너무 많은 값이 메모리로 들어와 MultipleBagFetchException이 발생한다. 이는 2개 이상의 bags, 즉 컬렉션 조인이 두 개 이상일 때 발생하는 Exception이다.&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;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;MultipleBagFetchException 해결방법 1: 자료형을 Set으로 변경&lt;/h4&gt;
&lt;pre id=&quot;code_1724750932837&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Team {

    ...
    
    @OneToMany(mappedBy = &quot;team&quot;, fetch = FetchType.LAZY)
    private Set&amp;lt;User&amp;gt; users = emptySet();

    @OneToMany(mappedBy = &quot;team&quot;, fetch = FetchType.LAZY)
    private Set&amp;lt;Event&amp;gt; events = emptySet();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;List는 순서를 보장하는 컬렉션이기 때문에 각 컬렉션의 순서를 유지하면서 데이터베이스에서 데이터를 로드해야 한다. 하지만 Set는 순서를 보장하지 않는 컬렉션이기 때문에 JPA가 중복을 허용하지 않으면서도 순서에 제한받지 않고 데이터를 처리할 수 있다.&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;즉, 자료형을 Set으로 변경하게 되면 여러 개의 컬렉션을 동시에 Fetch Join할 때 순서를 유지할 필요가 없어 효율적으로 중복 데이터를 처리할 수 있고, 이로 인해 MultipleBagFetchException이 발생하지 않게 된다.&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;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;MultipleBagFetchException 해결방법 2: BatchSize 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Fetch Join을 사용하면서 페이징을 하게되면,&lt;span&gt; &lt;/span&gt;&lt;/span&gt;Set을 사용하더라도 List를 사용할 때와 마찬가지로 인메모리 로딩 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서&amp;nbsp;이전 Pagination의 해결책과 마찬가지로 배치 사이즈 설정은 MultipleBagFetchException을 해결할 수 있는 방법이기도 하다.&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&gt;배치 사이즈&lt;/span&gt;를 설정하면, JPA는 한 번에 모든 데이터를 Fetch Join으로 가져오지 않고 배치 크기만큼 나눠 가져온다. 따라서 한 번에 처리하는 데이터의 양이 줄어들어 다수의 &lt;span&gt;List&lt;/span&gt; 형태의 컬렉션을 한꺼번에 로드할 때 발생하는 &lt;span&gt;MultipleBagFetchException 문제를&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  배치 사이즈 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 사이즈를 설정하면, Hibernate가 연관된 엔티티를 한 번의 쿼리로 가져오도록 할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 옵션은 정확히 N+1 문제를 해결하는 것은 아니지만, N+1 문제가 발생하더라도 SELECT * FROM user WHERE team_id = ? 대신 SELECT * FROM user WHERE team_id &lt;b&gt;IN (?, ?, ?)&lt;/b&gt; 방식으로 쿼리가 실행되도록 한다. 이를 통해 N+1 문제가 100번 발생할 상황을 1번의 추가 조회로 최적화할 수 있다.&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;application.yml에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;default_batch_fetch_size로 전역 설정하거나 @BatchSize를 통해 특정 엔티티나 컬렉션에 대해 배치 사이즈를 설정하여 개별적으로 성능을 최적화할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1724653930035&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class ParentEntity {

    @BatchSize(size = 20)
    @OneToMany(mappedBy = &quot;parent&quot;)
    private Set&amp;lt;ChildEntity&amp;gt; children;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1724653466459&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  jpa:
    properties:
        default_batch_fetch_size: 100&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;하지만 일반적인 경우 연관 관계 데이터 사이즈에 최적화된 크기를 알기 어렵기 때문에 데이터 사이즈를 모르는 상태에서 100에서 1000 사이의 값을 설정하는 경우 비효율적일 수 있다.&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;h2 data-ke-size=&quot;size26&quot;&gt;  Entity Graph 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EntityGraph를 사용하면 기본 Lazy 로딩 설정을 무시하고 연관된 엔티티를 Eager 로딩하도록 설정할 수 있다. 하지만 연관관계가 조금만 복잡해져도 예상치 못한 문제가 발생할 수 있어 로딩할 데이터의 양과 연관관계의 복잡성을 충분히 고려하여 사용해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1724659147373&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EntityGraph(attributePaths = {&quot;users&quot;, &quot;events&quot;})
@Query(&quot;SELECT t FROM Team t WHERE t.id = :id&quot;)
Team findTeamByIdWithUsersAndEvents(@Param(&quot;id&quot;) Long id);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위&amp;nbsp; 메서드는 Team 엔티티를 조회할 때 @EntityGraph를 사용하여 users와 events 엔티티들을 함께 로드한다.&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;&lt;span style=&quot;background-color: #f6fdd7;&quot;&gt;Pagination&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;EntityGraph&lt;/span&gt;도 Fetch Join과 비슷한 문제를 가진다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;EntityGraph&lt;/span&gt;는 내부적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;FetchType.EAGER&lt;/span&gt;로 동작하며 연관된 엔티티를 한 번의 쿼리로 모두 가져오지만, 1:N 관계의 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;Fetch Join&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #f6fdd7;&quot;&gt;Fetch Join&amp;nbsp; vs&amp;nbsp; @EntityGraph&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;EntityGraph&lt;/span&gt;&lt;/b&gt;는 기본적으로 &lt;span&gt;fetchType&lt;/span&gt;을 &lt;span&gt;Eager&lt;/span&gt;로 설정하여 연관된 엔티티를 함께 로드하며, 이 과정에서 &lt;u&gt;LEFT &lt;span&gt;OUTER JOIN&lt;/span&gt;&lt;/u&gt;을 수행한다. 이는 연관된 엔티티가 존재하지 않더라도 부모 엔티티를 반환하기 위해 사용된다. 따라서 EntityGraph를 사용하면 연관된 엔티티가 없어도 부모 엔티티가 결과에 포함된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &lt;b&gt;&lt;span&gt;Fetch Join&lt;/span&gt;&lt;/b&gt;은 연관된 엔티티를 로드할 때 기본적으로 &lt;u&gt;&lt;span&gt;INNER JOIN&lt;/span&gt;&lt;/u&gt;을 수행한다. 따라서&amp;nbsp;&lt;u&gt;연관된 엔티티가 존재하지 않는 경우 부모 엔티티도 결과에서 제외&lt;/u&gt;될 수 있다. 따라서 INNER JOIN은 연관된 모든 엔티티가 존재해야만 부모 엔티티가 결과에 포함된다.&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;Fetch Join은 N개의 연관된 엔티티를 한 번에 조인할 수 없기 때문에, 전체 결과가 중복되거나 성능 저하를 초래할 수 있다. 이 문제는 DISTINCT를 사용하여 해결하지만, 이는 쿼리 성능에 영향을 미칠 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 EntityGraph를 사용할 경우, LEFT OUTER JOIN을 사용하여 연관된 모든 데이터를 가져올 수 있기 때문에 Fetch Join의 단점 중 하나인 1:N 컬렉션 조인 시 최대 한 개의 컬렉션만 조인할 수 있는 제약이 없다. 또한 EntityGraph는 DISTINCT를 필요로 하지 않아 중복된 결과나 성능 저하를 방지할 수 있다.&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;이러한 차이 때문에, @EntityGraph는 모든 부모 엔티티를 가져오되, 연관된 엔티티가 없을 수도 있는 경우에 유용하고, Fetch Join은 연관된 엔티티가 반드시 존재하는 경우에 효율적이다.&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;  INNER JOIN, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;LEFT&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;OUTER&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;JOIN&lt;/span&gt;&amp;nbsp;차이&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;1287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HlOi4/btsJg3591fw/bYnztLQTxE6Cxib5Fxi6R0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HlOi4/btsJg3591fw/bYnztLQTxE6Cxib5Fxi6R0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HlOi4/btsJg3591fw/bYnztLQTxE6Cxib5Fxi6R0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHlOi4%2FbtsJg3591fw%2FbYnztLQTxE6Cxib5Fxi6R0%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;630&quot; height=&quot;520&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;1287&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INNER JOIN은 쉽게 말해 교집합이고, LEFT OUTER JOIN은 왼쪽 테이블을 기준으로 JOIN한 데이터를 보여준다.&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;h2 data-ke-size=&quot;size26&quot;&gt;다음 글: N+1&amp;nbsp;문제&amp;nbsp;발생&amp;nbsp;지점&amp;nbsp;찾아&amp;nbsp;해결하기&lt;/h2&gt;
&lt;figure id=&quot;og_1725032702036&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;[JPA] N+1 문제 발생 지점 찾아 해결하기&quot; data-og-description=&quot;이전 글에서 N+1 문제와 해결 방법에 대해 정리해보았다.&amp;nbsp;[JPA] N+1 문제 해결 방법N+1 문제란?N+1 문제는 ORM(객체-관계 매핑) 기술에서 특정 객체를 조회할 때,  그 객체와 연관된 다른 객체들도 각각&quot; data-og-host=&quot;chaewsscode.tistory.com&quot; data-og-source-url=&quot;https://chaewsscode.tistory.com/259&quot; data-og-url=&quot;https://chaewsscode.tistory.com/259&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bMXbqe/hyWVTbONQa/rPgLNOWaCbFrMENFPLNg80/img.png?width=800&amp;amp;height=308&amp;amp;face=0_0_800_308,https://scrap.kakaocdn.net/dn/bthbid/hyWVYEaK6Z/vIqd6ee3Uqbiy21iUnfXzk/img.png?width=800&amp;amp;height=308&amp;amp;face=0_0_800_308&quot;&gt;&lt;a href=&quot;https://chaewsscode.tistory.com/259&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chaewsscode.tistory.com/259&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bMXbqe/hyWVTbONQa/rPgLNOWaCbFrMENFPLNg80/img.png?width=800&amp;amp;height=308&amp;amp;face=0_0_800_308,https://scrap.kakaocdn.net/dn/bthbid/hyWVYEaK6Z/vIqd6ee3Uqbiy21iUnfXzk/img.png?width=800&amp;amp;height=308&amp;amp;face=0_0_800_308');&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;[JPA] N+1 문제 발생 지점 찾아 해결하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이전 글에서 N+1 문제와 해결 방법에 대해 정리해보았다.&amp;nbsp;[JPA] N+1 문제 해결 방법N+1 문제란?N+1 문제는 ORM(객체-관계 매핑) 기술에서 특정 객체를 조회할 때,  그 객체와 연관된 다른 객체들도 각각&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chaewsscode.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;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;%F0%9F%93%9A%20%EC%B0%B8%EA%B3%A0-1&quot; style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;참고&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@jinyoungchoi95/JPA-%EB%AA%A8%EB%93%A0-N1-%EB%B0%9C%EC%83%9D-%EC%BC%80%EC%9D%B4%EC%8A%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EC%B1%85#jpa%EC%97%90-%EB%94%B0%EB%9D%BC%EC%98%A4%EB%8A%94-%EA%BC%AC%EB%A6%AC%ED%91%9C-n1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JPA&amp;nbsp;모든&amp;nbsp;N+1&amp;nbsp;발생&amp;nbsp;케이스과&amp;nbsp;해결책&lt;/a&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@xogml951/JPA-N1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EC%B4%9D%EC%A0%95%EB%A6%AC#0-%EB%B0%B0%EA%B2%BD&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JPA&amp;nbsp;N+1&amp;nbsp;문제와&amp;nbsp;해결법&amp;nbsp;총정리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Back-end</category>
      <author>서채리</author>
      <guid isPermaLink="true">https://chaewsscode.tistory.com/258</guid>
      <comments>https://chaewsscode.tistory.com/258#entry258comment</comments>
      <pubDate>Tue, 27 Aug 2024 23:33:32 +0900</pubDate>
    </item>
    <item>
      <title>[Git] bad object {...} did not send all necessary objects</title>
      <link>https://chaewsscode.tistory.com/257</link>
      <description>&lt;h2 id=&quot;%F0%9F%8C%B1%20%EB%AC%B8%EC%A0%9C%20%EC%83%81%ED%99%A9-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;  문제 상황&lt;/h2&gt;
&lt;pre id=&quot;code_1724562585227&quot; class=&quot;console&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Update Failed remote: Total 104 (delta 38), reused 97 (delta 35), pack-reused 0 (from 0) bad object 
    refs/heads/feature#52 2 ... did not send all necessary objects&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;develop 브랜치를 Update 하려고 하니 해당 에러가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;%F0%9F%8C%B1%C2%A0%ED%95%B4%EA%B2%B0%20%EB%B0%A9%EC%95%88-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt; &amp;nbsp;해결 방안&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 디렉토리 내의 파일 이름에 &quot;2&quot;가 추가되어 발생하는 문제이다. Git 저장소가 아이클라우드로 관리되는 경우 간혹 발생할 수 있는 문제인 것 같다.&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;레포지토리 터미널에 git gc 명령어를 입력하면, 저장소 내에서 문제가 되는 객체나 참조를 찾아낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rm 명령어를 통해 잘못된 참조를 삭제하면 문제가 해결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;터미널에서 공백(스페이스)는 명령어의 인수 구분자로 사용되기 때문에, 파일 이름을 인식시키기 위해 '\'를 사용하여 공백을 이스케이프 처리한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724562670958&quot; class=&quot;console&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;% git gc
error: bad ref for .git/logs/refs/heads/feature#52 2
fatal: bad object refs/heads/feature#52 2
fatal: failed to run repack

% rm .git/refs/heads/feature#52\ 2
% rm .git/logs/refs/heads/feature#52\ 2&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 id=&quot;%F0%9F%93%9A%20%EC%B0%B8%EA%B3%A0-1&quot; style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;참고&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/72515916/git-fatal-bad-object-refs-heads-2-master&quot;&gt;Git fatal: bad object refs/heads 2/master&lt;/a&gt;&lt;/p&gt;</description>
      <category>Back-end/TroubleShooting</category>
      <author>서채리</author>
      <guid isPermaLink="true">https://chaewsscode.tistory.com/257</guid>
      <comments>https://chaewsscode.tistory.com/257#entry257comment</comments>
      <pubDate>Sun, 25 Aug 2024 14:20:00 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 정적 코드 분석을 위해 SonarCloud 사용하기</title>
      <link>https://chaewsscode.tistory.com/256</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;정적 코드 분석이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 코드 분석은 프로그램 실행 없이 소스 코드를 분석하여 코드의 품질, 성능, 보안 문제 등을 찾아내는 기법이다. 이를 통해 코드 작성 단계에서 잠재적인 오류가 결함을 조기에 발견하고 수정할 수 있다.&lt;/p&gt;
&lt;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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;잠재적인 버그가 발생할 수 있는 코드 발견&lt;/li&gt;
&lt;li&gt;코드 컨벤션 위반 여부 판단&lt;/li&gt;
&lt;li&gt;오타 검수&lt;/li&gt;
&lt;li&gt;사용되지 않는 코드(미사용 import) 발견&lt;/li&gt;
&lt;li&gt;잠재적 보안 취약점 발견&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SonarCloud란?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBGlba/btsI9F6kN82/bKukUlZ547yw96JLJErTS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBGlba/btsI9F6kN82/bKukUlZ547yw96JLJErTS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBGlba/btsI9F6kN82/bKukUlZ547yw96JLJErTS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBGlba%2FbtsI9F6kN82%2FbKukUlZ547yw96JLJErTS1%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;420&quot; height=&quot;120&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;120&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;figure id=&quot;og_1724167717044&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;[Spring] SonarQube로 프로젝트 정적 코드 분석&quot; data-og-description=&quot;SonarQube란?SonarQube는 클린 코드를 구현하기 위한 정적 코드 분석 도구이다.&amp;nbsp;  정적 코드 분석 vs 동적 코드 분석정적 코드 분석은 코드가 실행되기 전 소스 코드 또는 바이너리 코드를 분석해 코&quot; data-og-host=&quot;chaewsscode.tistory.com&quot; data-og-source-url=&quot;https://chaewsscode.tistory.com/255&quot; data-og-url=&quot;https://chaewsscode.tistory.com/255&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/KBOo4/hyWSc9H6In/Yn4205gj3RTz9wNByLxQdK/img.png?width=800&amp;amp;height=281&amp;amp;face=0_0_800_281,https://scrap.kakaocdn.net/dn/bWXlH5/hyWSiIP9pk/HcGhhvqHLNsPoUFWb2kim1/img.png?width=800&amp;amp;height=281&amp;amp;face=0_0_800_281,https://scrap.kakaocdn.net/dn/cNCT3u/hyWSpnFVPs/otBOvYgbCfwGuwB7oTMs81/img.png?width=1312&amp;amp;height=927&amp;amp;face=0_0_1312_927&quot;&gt;&lt;a href=&quot;https://chaewsscode.tistory.com/255&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chaewsscode.tistory.com/255&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/KBOo4/hyWSc9H6In/Yn4205gj3RTz9wNByLxQdK/img.png?width=800&amp;amp;height=281&amp;amp;face=0_0_800_281,https://scrap.kakaocdn.net/dn/bWXlH5/hyWSiIP9pk/HcGhhvqHLNsPoUFWb2kim1/img.png?width=800&amp;amp;height=281&amp;amp;face=0_0_800_281,https://scrap.kakaocdn.net/dn/cNCT3u/hyWSpnFVPs/otBOvYgbCfwGuwB7oTMs81/img.png?width=1312&amp;amp;height=927&amp;amp;face=0_0_1312_927');&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;[Spring] SonarQube로 프로젝트 정적 코드 분석&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SonarQube란?SonarQube는 클린 코드를 구현하기 위한 정적 코드 분석 도구이다.&amp;nbsp;  정적 코드 분석 vs 동적 코드 분석정적 코드 분석은 코드가 실행되기 전 소스 코드 또는 바이너리 코드를 분석해 코&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chaewsscode.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 SonarQube를 통해 프로젝트 정적 코드 분석을 해보았다. &lt;b&gt;SonarQube&lt;/b&gt;는 &lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;코드의 구조, 문법, 스타일, 잠재적인 버그, 코드 복잡도, 보안 취약점 등을 분석하고 테스트 코드의 커버리지를 측정하는 기능을 제공한다. 이러한 분석을 통해 개발자는 코드 품질을 유지하고 개선할 수 있는 장점이 있지만&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;u&gt;서버를 직접 구축&lt;/u&gt;해야 하는 단점이 있다.&amp;nbsp;&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: #333333; text-align: start;&quot;&gt;반면 &lt;b&gt;SonarCloud&lt;/b&gt;는 &lt;u&gt;이미 구축되어 있는 서버를 이용&lt;/u&gt;하기 때문에 이러한 서버 구축의 부담을 덜 수 있다. SonarCloud는 클라우드 기반 서비스이기 때문에 자체 서버를 설치하거나 유지 보수할 필요가 없다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&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: #333333; text-align: start;&quot;&gt;따라서 보안 상의 문제로 클라우드 기반 서비스를 사용할 수 없거나, SonarQube에서 제공하는 다양한 플러그인을 사용하고 싶은 경우 SonarQube를 사용하고, 그 외에는 서버를 직접 관리할 필요가 없는 SonarCloud를 사용하는 것이 좋을 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;SonarCloud 설정&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;SonarCloud는 자바 프로젝트의 커버리지 리포트를 생성하지 않아 별도의 코드 커버리지 측정 도구를 사용하여야 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1724168542123&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;[Spring Boot] 멀티모듈에 JaCoCo + JaCoCo Report Aggregation 적용하기&quot; data-og-description=&quot;macOSIntelliJ IDEAJava 17Spring Boot 3.xGroovy  JaCoCoJaCoCo 플러그인 적용JaCoCo :: Maven Plugin에서 JaCoCo의 최신 버전을 확인할 수 있다.처음에는 build.gradle에 JaCoCo 태스크를 설정했으나, 너무 복잡해져서 별도&quot; data-og-host=&quot;chaewsscode.tistory.com&quot; data-og-source-url=&quot;https://chaewsscode.tistory.com/249&quot; data-og-url=&quot;https://chaewsscode.tistory.com/249&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ArXIq/hyWSmR0FP0/0JdGZk4kDPe4qnUGWeOaik/img.png?width=304&amp;amp;height=505&amp;amp;face=0_0_304_505,https://scrap.kakaocdn.net/dn/b4TaIf/hyWSgxuQa0/BPxrF33PHVHyKfnQFuHJA1/img.png?width=304&amp;amp;height=505&amp;amp;face=0_0_304_505,https://scrap.kakaocdn.net/dn/dSdG4h/hyWSjAX6FN/MDPGKnLT62kTxg2UYgsaSK/img.png?width=1702&amp;amp;height=498&amp;amp;face=0_0_1702_498&quot;&gt;&lt;a href=&quot;https://chaewsscode.tistory.com/249&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chaewsscode.tistory.com/249&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ArXIq/hyWSmR0FP0/0JdGZk4kDPe4qnUGWeOaik/img.png?width=304&amp;amp;height=505&amp;amp;face=0_0_304_505,https://scrap.kakaocdn.net/dn/b4TaIf/hyWSgxuQa0/BPxrF33PHVHyKfnQFuHJA1/img.png?width=304&amp;amp;height=505&amp;amp;face=0_0_304_505,https://scrap.kakaocdn.net/dn/dSdG4h/hyWSjAX6FN/MDPGKnLT62kTxg2UYgsaSK/img.png?width=1702&amp;amp;height=498&amp;amp;face=0_0_1702_498');&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;[Spring Boot] 멀티모듈에 JaCoCo + JaCoCo Report Aggregation 적용하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;macOSIntelliJ IDEAJava 17Spring Boot 3.xGroovy  JaCoCoJaCoCo 플러그인 적용JaCoCo :: Maven Plugin에서 JaCoCo의 최신 버전을 확인할 수 있다.처음에는 build.gradle에 JaCoCo 태스크를 설정했으나, 너무 복잡해져서 별도&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chaewsscode.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 경우 이전에 JaCoCo를 이용해 코드 커버리지를 측정했기도 하고, SonarCloud에서도 JaCoCo 세팅을 추천하기 때문에 커버리지 측정 도구는 그대로 JaCoCo를 사용하기로 했다. 이전에 JaCoCo 적용 글을 작성해 해당 내용은 생략하려고 한다.&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;1️⃣ SonarCloud 기본 설정, GitHub에 토큰 등록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sonarcloud.io/login&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SonarCloud&lt;/a&gt;에서 회원가입을 하고 Organization과 분석할 프로젝트를 선택한다. 분석할 프로젝트가 Public 레포지토리인 경우 무료 플랜을 사용할 수 있다.&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;SonarCloud는 기본적으로 Automatic Analysis를 지원하지만 Github Actions CI를 사용할 경우 SonarCloud에서 제공하는 자동 분석을 꺼주어야 한다.(CI-based Analysis는 Automatic Analysis와 함께 실행 시 충돌 발생)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 Administration &amp;gt; Analysis Method &amp;gt; Automatic Analysis를 off 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1737&quot; data-origin-height=&quot;824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CVIca/btsJam52rs0/D0TPXYykeEIPLvAG4YP291/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CVIca/btsJam52rs0/D0TPXYykeEIPLvAG4YP291/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CVIca/btsJam52rs0/D0TPXYykeEIPLvAG4YP291/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCVIca%2FbtsJam52rs0%2FD0TPXYykeEIPLvAG4YP291%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;785&quot; height=&quot;372&quot; data-origin-width=&quot;1737&quot; data-origin-height=&quot;824&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후 하단에 있는 With GitHub Actions를 눌러 'SONAR_TOKEN' 발급을 받아 Github 레포지토리의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Secrets and variables &amp;gt; Actions의 Repository secrets&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;span style=&quot;color: #9d9d9d;&quot;&gt;Github Actions에서는 보안상의 이유로 소스코드에 노출하면 안되는 민감한 정보들(ex API 키, 인증 토큰, 비밀번호, SSH 키, 데이터베이스 자격 증명 및 기타 환경 변수)을 Github Secrets 변수에 등록해 정보를 안전하게 저장하고 GitHub Actions 워크플로우에서 필요할 때만 사용할 수 있도록 도와준다.&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;1236&quot; data-origin-height=&quot;695&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btQf4v/btsI8RfyqO9/pF3Q4zf0sX4kuxNOvqYhak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btQf4v/btsI8RfyqO9/pF3Q4zf0sX4kuxNOvqYhak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btQf4v/btsI8RfyqO9/pF3Q4zf0sX4kuxNOvqYhak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtQf4v%2FbtsI8RfyqO9%2FpF3Q4zf0sX4kuxNOvqYhak%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;785&quot; height=&quot;441&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;695&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;2️⃣ build.gradle에 sonar 의존성 추가 및 설정&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;그 후 SonarCloud에 나와있는 다음 yaml 파일 안내사항대로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;루트 build.gradle에 sonar 의존성을 추가해준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724169769076&quot; class=&quot;yaml&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
  // ...
  id &quot;org.sonarqube&quot; version &quot;5.0.0.4638&quot;
}

sonar {
  properties {
    property &quot;sonar.projectKey&quot;, &quot;프로젝트 키&quot;
    property &quot;sonar.organization&quot;, &quot;조직명&quot;
    property &quot;sonar.host.url&quot;, &quot;https://sonarcloud.io&quot;
    property 'sonar.sources', 'src'
    property 'sonar.language', 'java'
    property 'sonar.sourceEncoding', 'UTF-8'
    property &quot;sonar.profile&quot;, &quot;Sonar way&quot;
    property 'sonar.test.inclusions', '**/*Test.java'
    // 테스트 커버리지에서 제외할 클래스
    property 'sonar.exclusions', '**/Q*.java, **/config/**'
    property 'sonar.java.coveragePlugin', 'jacoco'
    // JaCoCo Coverage Report 주소
    property 'sonar.coverage.jacoco.xmlReportPaths', &quot;${project.rootDir}/support/jacoco/build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 projectKey와 organization은 본인 프로젝트에 맞게 설정해주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;projectKey는 SonarCloud 주소창에 나와있는 &lt;a href=&quot;https://sonarcloud.io/summary/overall?id='이&quot;&gt;https://sonarcloud.io/summary/overall?id={projectKey}'&lt;/a&gt; 값으로 projectKey'를 확인할 수 있다.&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;sonar.coverage.jacoco.xmlReportPaths 속성은 JaCoCo의 XML 파일 경로를 명시하는 역할을 한다. 프로젝트가 멀티 모듈인 경우, 각 모듈마다 별도의 XML 파일이 생성되게 된다. 이 경우 jacoco-aggregation을 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;여러 개의 XML 보고서를 하나의 파일로 합칠 수 있는데, 만약 통합 보고서를 사용하지 않고 개별 보고서를 사용한다면 각 JaCoCo XML 파일의 경로를 각각 설정해주어야 한다.&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;  멀티 모듈에서 jacoco-aggregation을 이용하지 않고 개별 보고서를 사용하는 경우&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;루트 build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1724170175238&quot; class=&quot;yaml&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;subprojects {
    apply plugin: 'org.sonarqube'

    sonar {
        properties {
            property 'sonar.java.binaries', &quot;${buildDir}/classes&quot;
            // jacoco-aggregation을 사용하지 않은 경우 각 프로젝트의 buildDir마다 따로 적용
            property 'sonar.coverage.jacoco.xmlReportPaths', &quot;${buildDir}/reports/jacoco.xml&quot;
        }
    }
}

sonar {
    properties {
        property &quot;sonar.projectKey&quot;, &quot;프로젝트 키&quot;
        property &quot;sonar.organization&quot;, &quot;조직명&quot;
        property &quot;sonar.host.url&quot;, &quot;https://sonarcloud.io&quot;
        property 'sonar.sources', 'src'
        property 'sonar.language', 'java'
        property 'sonar.sourceEncoding', 'UTF-8'
        property &quot;sonar.profile&quot;, &quot;Sonar way&quot;
        property 'sonar.test.inclusions', '**/*Test.java'
        property 'sonar.exclusions', '**/Q*.java, **/config/**'
        property 'sonar.java.coveragePlugin', 'jacoco'
    }
}&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;3️⃣ GitHub workflow 설정&lt;/h3&gt;
&lt;pre id=&quot;code_1724169950181&quot; class=&quot;yaml&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: SonarCloud
on:
  push:
    branches: [ &quot;develop&quot; ]
  pull_request:
    branches: [ &quot;develop&quot; ]

jobs:
  build_and_analyze:
    name: Build and SonarCloud Analysis
    runs-on: ubuntu-latest

    steps:
      - name: Checkout branch
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Cache Gradle packages
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: ${{ runner.os }}-gradle

      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew

      - name: Build with Gradle
        run: ./gradlew build

      - name: Cache SonarCloud packages
        uses: actions/cache@v4
        with:
          path: ~/.sonar/cache
          key: ${{ runner.os }}-sonar
          restore-keys: ${{ runner.os }}-sonar

      - name: SonarCloud Scan
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        run: ./gradlew sonar --info --stacktrace&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 workflow는 다음과 같은 기능을 수행한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;develop 브랜치로 체크아웃&lt;/li&gt;
&lt;li&gt;JDK 17 설치&lt;/li&gt;
&lt;li&gt;Gradle, SonarCloud 캐시 설정&lt;/li&gt;
&lt;li&gt;Gradle 빌드 수행&lt;/li&gt;
&lt;li&gt;SonarCloud 분석 실행&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;여기서 secrets.SONAR_TOKEN을 제외한 모든 변수들은 개발자가 따로 지정하지 않아도 자동으로 설정된다.(SONAR_TOKEN은 위 1번 과정에서 이미 저장한 값)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4️⃣ 결과 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 설정을 마친 후 PR을 올리면 SonarCloud bot이 요약된 정적 분석 결과를 comment로 남겨주는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-21 01.34.36.png&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;385&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brxJjC/btsJapuSIGa/1jCmKG8jKnckksn7XKXZJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brxJjC/btsJapuSIGa/1jCmKG8jKnckksn7XKXZJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brxJjC/btsJapuSIGa/1jCmKG8jKnckksn7XKXZJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrxJjC%2FbtsJapuSIGa%2F1jCmKG8jKnckksn7XKXZJ0%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;936&quot; height=&quot;385&quot; data-filename=&quot;스크린샷 2024-08-21 01.34.36.png&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;385&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;+ GitHub 브랜치 규칙 정하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;794&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VOnTa/btsJqGbE3nw/wweloeQX4uVaUbis7nHDAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VOnTa/btsJqGbE3nw/wweloeQX4uVaUbis7nHDAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VOnTa/btsJqGbE3nw/wweloeQX4uVaUbis7nHDAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVOnTa%2FbtsJqGbE3nw%2FwweloeQX4uVaUbis7nHDAk%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;826&quot; height=&quot;290&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;794&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용할 레포지토리의 [Settings &amp;gt; Rules &amp;gt; Rulesets]에서 New branch ruleset을 추가해 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;적용할 브랜치의 ruleset이 이미 있는 경우, 해당하는 브랜치의 ruleset을 수정해야 한다.&lt;/span&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-filename=&quot;스크린샷 2024-09-03 15.19.48.png&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;1434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6Mcvl/btsJqESsFAE/thnk5yIDnLy7mgKpCyrfDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6Mcvl/btsJqESsFAE/thnk5yIDnLy7mgKpCyrfDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6Mcvl/btsJqESsFAE/thnk5yIDnLy7mgKpCyrfDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6Mcvl%2FbtsJqESsFAE%2Fthnk5yIDnLy7mgKpCyrfDK%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;786&quot; height=&quot;705&quot; data-filename=&quot;스크린샷 2024-09-03 15.19.48.png&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;1434&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;develop 브랜치의 ruleset이기 때문에 Ruleset Name을 develop으로 지정해 주고, Enforcement status를 Active로 활성화시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Target branches의 Add target에서 규칙을 적용할 브랜치를 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 경우 현재 프로젝트의 Default 브랜치가 develop 브랜치로 설정되어 있어 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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-09-03 15.25.59.png&quot; data-origin-width=&quot;1588&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjWyRI/btsJqcvqfdH/if4h0MqwSp64wbrXJZ8WTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjWyRI/btsJqcvqfdH/if4h0MqwSp64wbrXJZ8WTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjWyRI/btsJqcvqfdH/if4h0MqwSp64wbrXJZ8WTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjWyRI%2FbtsJqcvqfdH%2Fif4h0MqwSp64wbrXJZ8WTk%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;768&quot; height=&quot;385&quot; data-filename=&quot;스크린샷 2024-09-03 15.25.59.png&quot; data-origin-width=&quot;1588&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;기준을 통과했을 때만 merge 할 수 있도록 [Require status checks to pass] 규칙을 선택하고, 세부 규칙으로 [Require branches to be up to date before merging]을 선택해 PR merge 전 해당 브랜치가 최신인지 확인한다.&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-filename=&quot;스크린샷 2024-09-03 15.32.12.png&quot; data-origin-width=&quot;1714&quot; data-origin-height=&quot;1142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXZLZX/btsJqb4mxjJ/XiFJ6z3sHWHwcJSFaNCEGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXZLZX/btsJqb4mxjJ/XiFJ6z3sHWHwcJSFaNCEGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXZLZX/btsJqb4mxjJ/XiFJ6z3sHWHwcJSFaNCEGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXZLZX%2FbtsJqb4mxjJ%2FXiFJ6z3sHWHwcJSFaNCEGK%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;741&quot; height=&quot;494&quot; data-filename=&quot;스크린샷 2024-09-03 15.32.12.png&quot; data-origin-width=&quot;1714&quot; data-origin-height=&quot;1142&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Add checks]에서 SonarCloud 분석 관련 checks를 추가해 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GALrg/btsJoBb86YM/HsIFqrIm3XGNTlvAIDUxWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GALrg/btsJoBb86YM/HsIFqrIm3XGNTlvAIDUxWK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;936&quot; data-filename=&quot;스크린샷 2024-09-03 15.32.31.png&quot; style=&quot;width: 43.548%; margin-right: 10px;&quot; data-widthpercent=&quot;44.06&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GALrg/btsJoBb86YM/HsIFqrIm3XGNTlvAIDUxWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGALrg%2FbtsJoBb86YM%2FHsIFqrIm3XGNTlvAIDUxWK%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;1722&quot; height=&quot;936&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H9JSo/btsJotL9Ye1/dnYMOLARAExkaSs7xj3Tkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H9JSo/btsJotL9Ye1/dnYMOLARAExkaSs7xj3Tkk/img.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;548&quot; data-is-animation=&quot;false&quot; style=&quot;width: 55.2892%;&quot; data-widthpercent=&quot;55.94&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H9JSo/btsJotL9Ye1/dnYMOLARAExkaSs7xj3Tkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH9JSo%2FbtsJotL9Ye1%2FdnYMOLARAExkaSs7xj3Tkk%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;1280&quot; height=&quot;548&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 위처럼 Suggestion이 나오지 않을 경우 [Add checks]에서 워크플로우의 작업 이름을 검색해 추가&amp;nbsp;후 [Any source]를 클릭해 SonarCloud를 연결해 준다.&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;설정을 완료한 후 PR 생성 시, GitHub Actions가 자동으로 build와 analyze를 진행한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-09-03 16.04.49.png&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;1174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5EMpm/btsJp69TtrU/X6Qv11Kr3AAstfD7exFtU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5EMpm/btsJp69TtrU/X6Qv11Kr3AAstfD7exFtU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5EMpm/btsJp69TtrU/X6Qv11Kr3AAstfD7exFtU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5EMpm%2FbtsJp69TtrU%2FX6Qv11Kr3AAstfD7exFtU0%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;770&quot; height=&quot;540&quot; data-filename=&quot;스크린샷 2024-09-03 16.04.49.png&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;1174&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 SonarCloud의 통과 조건인 커버리지 80% 이상을 충족하지 않으면 ruleset에서 PR merge를 제한하게 된다.&lt;/p&gt;</description>
      <category>Back-end</category>
      <author>서채리</author>
      <guid isPermaLink="true">https://chaewsscode.tistory.com/256</guid>
      <comments>https://chaewsscode.tistory.com/256#entry256comment</comments>
      <pubDate>Wed, 21 Aug 2024 01:24:21 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] SonarQube로 프로젝트 정적 코드 분석</title>
      <link>https://chaewsscode.tistory.com/255</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;SonarQube란?&lt;/h2&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;708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IrDAs/btsI7IavATC/L60GCXfmqxEcdjG8NntXR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IrDAs/btsI7IavATC/L60GCXfmqxEcdjG8NntXR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IrDAs/btsI7IavATC/L60GCXfmqxEcdjG8NntXR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIrDAs%2FbtsI7IavATC%2FL60GCXfmqxEcdjG8NntXR1%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;622&quot; height=&quot;344&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;708&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;SonarQube는 클린 코드를 구현하기 위한 정적 코드 분석 도구이다.&lt;/span&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;  정적 코드 분석 vs 동적 코드 분석&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;정적 코드 분석&lt;/b&gt;은 &lt;u&gt;코드가 실행되기 전&lt;/u&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;color: #333333; text-align: start;&quot;&gt;동적 코드 분석은 코드를 실제로 실행하는 동안 수행되는데, 프로그램을 실행하면서 그 동작을 모니터링하고, 런타임에서 발생할 수 있는 메모리 누수, 성능 문제, 예외 처리, 보안 문제 등을 분석한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;SonarQube 설치&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;a href=&quot;https://www.sonarsource.com/products/sonarqube/downloads/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SonarQube&lt;/a&gt;에서 community 버전을 다운로드한 후 터미널을 통해 sonar 스크립트를 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1723891594749&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# SonarQube 서버 실행 파일이 있는 디렉토리로 이동
% cd Downloads/sonarqube-10.6.0.92116/bin/macosx-universal-64

# sonar.sh 스크립트 실행
% ./sonar.sh start&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;그 후 localhost:9000 로 접속해 아이디와 패스워드에 admin을 입력하여 로그인한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-17 19.49.15.png&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;927&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYmGfN/btsI7j9HsgE/oyw0DA5ZRPAjGYkikLbpgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYmGfN/btsI7j9HsgE/oyw0DA5ZRPAjGYkikLbpgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYmGfN/btsI7j9HsgE/oyw0DA5ZRPAjGYkikLbpgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYmGfN%2FbtsI7j9HsgE%2Foyw0DA5ZRPAjGYkikLbpgK%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;785&quot; height=&quot;555&quot; data-filename=&quot;스크린샷 2024-08-17 19.49.15.png&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;927&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-17 20.00.25.png&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;927&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/06E4q/btsI6iD28im/mjwJANgkOSLgE9c8lQay3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/06E4q/btsI6iD28im/mjwJANgkOSLgE9c8lQay3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/06E4q/btsI6iD28im/mjwJANgkOSLgE9c8lQay3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F06E4q%2FbtsI6iD28im%2FmjwJANgkOSLgE9c8lQay3K%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;785&quot; height=&quot;555&quot; data-filename=&quot;스크린샷 2024-08-17 20.00.25.png&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;927&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;h3 data-ke-size=&quot;size23&quot;&gt;  로컬 레포지토리를 측정하고 싶은 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Create a local project를 선택해 프로젝트 정보를 입력한 후 아래&lt;span&gt;&amp;nbsp;&lt;/span&gt;페이지에서 Locally를 선택&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&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;1594&quot; data-origin-height=&quot;501&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C5lUN/btsI8iPXGtF/cbOXyiekpHBPHsBtyx5N20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C5lUN/btsI8iPXGtF/cbOXyiekpHBPHsBtyx5N20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C5lUN/btsI8iPXGtF/cbOXyiekpHBPHsBtyx5N20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC5lUN%2FbtsI8iPXGtF%2FcbOXyiekpHBPHsBtyx5N20%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;785&quot; height=&quot;247&quot; data-origin-width=&quot;1594&quot; data-origin-height=&quot;501&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;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;토큰 이름을 설정하고 Generate 버튼을 눌러 생성된 토큰을 저장해 놓는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zAdVu/btsI7LLJgOg/HGBK6w1EGLKHLkHaNkfHsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zAdVu/btsI7LLJgOg/HGBK6w1EGLKHLkHaNkfHsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zAdVu/btsI7LLJgOg/HGBK6w1EGLKHLkHaNkfHsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzAdVu%2FbtsI7LLJgOg%2FHGBK6w1EGLKHLkHaNkfHsk%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;785&quot; height=&quot;487&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;548&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;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 토큰을 잊었으면 Administration &amp;gt; Security &amp;gt; User &amp;gt; Tokens에서 토큰을 새로 발급해 준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;927&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgMMSm/btsI5S6B5OM/n2hXRlNIF13BBFeRbysn6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgMMSm/btsI5S6B5OM/n2hXRlNIF13BBFeRbysn6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgMMSm/btsI5S6B5OM/n2hXRlNIF13BBFeRbysn6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgMMSm%2FbtsI5S6B5OM%2Fn2hXRlNIF13BBFeRbysn6K%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;785&quot; height=&quot;555&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;927&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;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;IntelliJ의 Settings &amp;gt; Tools &amp;gt; SonarLint에 + 버튼을 누른 후 이름과 소나큐브 URL(http://localhost:9000)을 입력한 후 다음 창에서 소나큐브 토큰을 넣어준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lOacY/btsI6x2iejx/gKq8klwGXQuLvpF1bdJNb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lOacY/btsI6x2iejx/gKq8klwGXQuLvpF1bdJNb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lOacY/btsI6x2iejx/gKq8klwGXQuLvpF1bdJNb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlOacY%2FbtsI6x2iejx%2FgKq8klwGXQuLvpF1bdJNb0%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;785&quot; height=&quot;591&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;824&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-17 20.05.10.png&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;705&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QH8Pz/btsI5YlwLsc/Q8VB9yYp6jfLR4aibDQ0LK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QH8Pz/btsI5YlwLsc/Q8VB9yYp6jfLR4aibDQ0LK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QH8Pz/btsI5YlwLsc/Q8VB9yYp6jfLR4aibDQ0LK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQH8Pz%2FbtsI5YlwLsc%2FQ8VB9yYp6jfLR4aibDQ0LK%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;785&quot; height=&quot;467&quot; data-filename=&quot;스크린샷 2024-08-17 20.05.10.png&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;705&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;루트 build.gradle에 SonarQube 의존성을 추가해 준다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Gradle에서 사용할 수 있는 소나큐브의 최신 버전은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://plugins.gradle.org/plugin/org.sonarqube&quot;&gt;Gradle 플러그인 포털&lt;/a&gt;에서 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1724167515254&quot; class=&quot;yaml&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'java'
    id 'org.springframework.boot' version '3.0.4'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id &quot;org.sonarqube&quot; version &quot;5.0.0.4638&quot;	# 추가
}

allprojects {
    ...
}

subprojects {
    apply plugin: 'java'
    # 추가
    apply plugin: 'org.sonarqube'

    # (설정 1)
    sonar {
        properties {
            property 'sonar.java.binaries', &quot;${buildDir}/classes&quot;
        }
    }
}

# 추가 (설정 2)
sonar {
    properties {
        property 'sonar.host.url', 'http://localhost:9000'
        property 'sonar.token', System.getenv('SONAR_TOKEN')
        property 'sonar.sources', 'src'
        property 'sonar.language', 'java'
        property 'sonar.sourceEncoding', 'UTF-8'
        property 'sonar.test.inclusions', '**/*Test.java'
        property 'sonar.exclusions', '**/Q*.java, **/config/**, **/common/**, **/importer/**, ' +
                '**/*Application*.java, **/*Dto*.java, **/*Exception*.java, **/*ErrorCode*.java'
        property 'sonar.java.coveragePlugin', 'jacoco'
        property 'sonar.coverage.jacoco.xmlReportPaths', &quot;${project.rootDir}/support/jacoco/build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;  설정 1&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;subprojects 블록 내부에 있는 sonar 설정은 각 서브 프로젝트에 공통적으로 적용되는 설정이다. 멀티 프로젝트에서 각 서브 프로젝트의 buildDir 경로가 다를 수 있어 subprojects 블록 내부에 선언 하여 각 서브 프로젝트가 올바르게 경로를 참조하도록 설정한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;sonar.java.binaries 속성은 소나큐브가 코드 품질 및 커버리지 분석에 필요한 바이트코드 파일(.class 파일)을 찾기 위해 사용하는 경로를 지정한다.&lt;/li&gt;
&lt;/ul&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;size18&quot;&gt;  설정 2&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;루트 프로젝트(subprojects 외부)에 있는 sonar 설정은 모든 서브 프로젝트와 루트 프로젝트 자체에 적용되는 전역 설정을 정의한다. 이 설정에는 소나큐브 서버의 URL, 인증 토큰, 스캔할 소스 경로와 규칙들이 포함된다. 자세한 정보는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/scanners/sonarscanner-for-gradle/&quot;&gt;SonarScanner for Gradle&lt;/a&gt;&amp;nbsp;문서에서 확인할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;sonar.token 속성에는 소나큐브에서 발급받은 토큰 값을 넣어야하기 때문에 터미널에 아래 명령어를 입력하여 토큰을 환경 변수에 저장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1724167532801&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 환경 변수 설정
% export SONAR_TOKEN=토큰값

# 환경 변수 적용 여부 확인
% echo $SONAR_TOKEN&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;sonar.coverage.jacoco.xmlReportPaths 속성은 JaCoCo의 XML 파일 경로를 명시한다. JaCoCo의 XML 보고서가 여러 개의 파일로 생성되는 경우(멀티 모듈인 경우), jacoco-aggregation을 통해 하나의 파일로 합쳐 경로를 지정하거나, 각 JaCoCo XML 파일의 경로를 설정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1724167550589&quot; class=&quot;yaml&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;subprojects {
    apply plugin: 'org.sonarqube'

    sonar {
        properties {
            property 'sonar.java.binaries', &quot;${buildDir}/classes&quot;
            # jacoco-aggregation을 사용하지 않은 경우 설정 1에서 각 프로젝트의 buildDir마다 따로 적용
            property 'sonar.coverage.jacoco.xmlReportPaths', &quot;${buildDir}/reports/jacoco.xml&quot;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소나큐브 태스크는 두 가지 방법으로 실행할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;터미널 ./gradlew test sonar (권한이 없는 경우 chmod +x gradlew 입력)&lt;/li&gt;
&lt;li&gt;IntelliJ Gradle &amp;gt; verification &amp;gt; sonar(혹은 sonarqube)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-18 02.20.38.png&quot; data-origin-width=&quot;539&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EXES5/btsI77AKJWt/mYc65ssX0nW21BCdecjAKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EXES5/btsI77AKJWt/mYc65ssX0nW21BCdecjAKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EXES5/btsI77AKJWt/mYc65ssX0nW21BCdecjAKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEXES5%2FbtsI77AKJWt%2FmYc65ssX0nW21BCdecjAKk%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;478&quot; height=&quot;270&quot; data-filename=&quot;스크린샷 2024-08-18 02.20.38.png&quot; data-origin-width=&quot;539&quot; data-origin-height=&quot;304&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  GitHub Actions를 통해 프로젝트를 측정하고 싶은 경우&lt;/h3&gt;
&lt;figure id=&quot;og_1724225270756&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;[Spring] 코드 정적 분석을 위해 SonarCloud 사용하기&quot; data-og-description=&quot;SonarCloud란?&amp;nbsp;&amp;nbsp;[Spring] SonarQube로 프로젝트 정적 코드 분석SonarQube란?SonarQube는 클린 코드를 구현하기 위한 정적 코드 분석 도구이다.&amp;nbsp;  정적 코드 분석 vs 동적 코드 분석정적 코드 분석은 코드가 &quot; data-og-host=&quot;chaewsscode.tistory.com&quot; data-og-source-url=&quot;https://chaewsscode.tistory.com/256&quot; data-og-url=&quot;https://chaewsscode.tistory.com/256&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/iIYeg/hyWShpPW4Y/Uso8XEY8EhHp2okXIRjexK/img.png?width=420&amp;amp;height=120&amp;amp;face=0_0_420_120,https://scrap.kakaocdn.net/dn/bUAr4C/hyWSfZP0lL/644jBepC2CAkbeaQAoDHtk/img.png?width=420&amp;amp;height=120&amp;amp;face=0_0_420_120,https://scrap.kakaocdn.net/dn/kx7pJ/hyWSlZ5afV/Goc0REm788BFc103lbPLUK/img.png?width=1737&amp;amp;height=824&amp;amp;face=0_0_1737_824&quot;&gt;&lt;a href=&quot;https://chaewsscode.tistory.com/256&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chaewsscode.tistory.com/256&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/iIYeg/hyWShpPW4Y/Uso8XEY8EhHp2okXIRjexK/img.png?width=420&amp;amp;height=120&amp;amp;face=0_0_420_120,https://scrap.kakaocdn.net/dn/bUAr4C/hyWSfZP0lL/644jBepC2CAkbeaQAoDHtk/img.png?width=420&amp;amp;height=120&amp;amp;face=0_0_420_120,https://scrap.kakaocdn.net/dn/kx7pJ/hyWSlZ5afV/Goc0REm788BFc103lbPLUK/img.png?width=1737&amp;amp;height=824&amp;amp;face=0_0_1737_824');&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;[Spring] 코드 정적 분석을 위해 SonarCloud 사용하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SonarCloud란?&amp;nbsp;&amp;nbsp;[Spring] SonarQube로 프로젝트 정적 코드 분석SonarQube란?SonarQube는 클린 코드를 구현하기 위한 정적 코드 분석 도구이다.&amp;nbsp;  정적 코드 분석 vs 동적 코드 분석정적 코드 분석은 코드가&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chaewsscode.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>Back-end</category>
      <author>서채리</author>
      <guid isPermaLink="true">https://chaewsscode.tistory.com/255</guid>
      <comments>https://chaewsscode.tistory.com/255#entry255comment</comments>
      <pubDate>Mon, 19 Aug 2024 02:40:13 +0900</pubDate>
    </item>
  </channel>
</rss>