<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>옌의 로그</title>
    <link>https://dev-yen.tistory.com/</link>
    <description>일하고 공부하고 사랑하는 블로그</description>
    <language>ko</language>
    <pubDate>Fri, 21 Aug 2026 01:33:36 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>dev-yen</managingEditor>
    <image>
      <title>옌의 로그</title>
      <url>https://tistory1.daumcdn.net/tistory/5905973/attach/fab0254104694e1f91fa3e5e73ec769d</url>
      <link>https://dev-yen.tistory.com</link>
    </image>
    <item>
      <title>[디자인패턴] validation 로직에 visitor pattern 적용하기</title>
      <link>https://dev-yen.tistory.com/entry/%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4-validation-%EB%A1%9C%EC%A7%81%EC%97%90-visitor-pattern-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
      <description>&lt;h3 data-path-to-node=&quot;3&quot; data-ke-size=&quot;size23&quot;&gt;Visitor 패턴이란?&lt;/h3&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4&quot;&gt;Visitor(방문자) 패턴&lt;/b&gt;은 GoF 디자인 패턴 중 하나로, &quot;&lt;u&gt;데이터 구조와 연산을 분리&lt;/u&gt;&quot;하는 것이 핵심인 행위 패턴이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;5&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot;&gt;핵심 철학:&lt;/b&gt; 데이터 객체(Element)는 가만히 있고, 로직을 가진 객체(Visitor)가 돌아다니며 작업을 수행한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot;&gt;언제 쓰는가?&lt;/b&gt; 객체의 구조(DTO 등)는 안정적인데, 그 안에서 수행해야 할 로직(검증, 계산, 변환)이 계속 추가되거나 복잡해질 때 사용&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,0,0&quot;&gt;데이터(Element):&lt;/b&gt; 자기 자신을 고치지 않고 '방문자'를 받아들일 입구(accept)만 열어둔다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,0&quot;&gt;방문자(Visitor):&lt;/b&gt; 데이터 안으로 들어가서 특정 로직(검증, 계산, 변환 등)을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size20&quot;&gt;보통 언제 쓰나?&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;10&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,0,0&quot;&gt;객체의 구조는 안정적인데, 새로운 로직이 자주 추가될 때&lt;br /&gt;&lt;/b&gt;: DTO 구조는 거의 안 변하는데 검증 규칙만 계속 늘어날 때 유용&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,0&quot;&gt;서로 관련 없는 수많은 연산을 수행해야 할 때&lt;br /&gt;&lt;/b&gt;: 하나의 객체 안에 보고서 생성, 데이터 검증, 통계 계산 로직을 다 때려 넣고 싶지 않을 때 사용&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,2,0&quot;&gt;데이터 객체의 캡슐화를 지키고 싶을 때&lt;/b&gt;&lt;br /&gt;: 객체 내부를 복잡하게 만들지 않고 외부에서 로직을 주입하고 싶을 때 적합&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size20&quot;&gt;클래식 vs 현대적(함수형) 방식&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,0,0&quot;&gt;클래식:&lt;/b&gt; 복잡한 Visitor 인터페이스를 만들고 모든 데이터 타입을 메서드로 정의해야 함 (오버엔지니어링 위험)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,1,0&quot;&gt;현대적(Kotlin):&lt;/b&gt; 고차 함수나 람다를 활용해 인터페이스 없이도 가볍게 구현 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-path-to-node=&quot;6&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;검증 지옥에 빠진 Facade...&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;서비스가 커지면서 Facade 클래스는 수많은 서비스를 주입받고, 비즈니스 로직 사이사이에 복잡한 검증 코드가 끼어들기 시작했다..&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;이걸 처음엔 ValidationService 클래스를 만들어서, Facade에 주입하고 검증처리를 했는데, 코드리뷰를 통해 visitor pattern 적용을 제안받고 시도해보게 되었다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size23&quot;&gt;함수형 Visitor 패턴 적용으로 해결하기&lt;/h3&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;DTO가 외부 검증기(Visitor)를 받아들일 수 있도록 공통 입구를 만든다&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahcKEwi1soaj1aSTAxUAAAAAHQAAAAAQUA&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;/**
 * 데이터(Element)는 가만히 있고, 외부 로직(Visitor)을 받아들입니다.
 * 검증 후 자기 자신을 반환하여 메서드 체이닝이 가능하게 설계합니다.
 */
fun ProductDto.validate(validator: (ProductDto) -&amp;gt; Unit): ProductDto {
    validator(this)
    return this
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-path-to-node=&quot;16&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;클래스 파일을 무분별하게 늘리지 않기 위해, 성격이 비슷한 로직들을 object 싱글톤 안에 모았다. (외부 데이터가 필요한 경우 함수를 반환하는 고차 함수 형식을 취함)&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahcKEwi1soaj1aSTAxUAAAAAHQAAAAAQUQ&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;object ProductValidator {
    // 외부 데이터(요청 값)를 주입받아 검증 함수를 반환 (고차 함수)
    fun categoryPolicy(categoryType: CategoryType, policy: DiscountPolicy?) = { product: ProductDto -&amp;gt;
        if (product.type == ProductType.EVENT) {
            if (policy?.maxDiscountRate ?: 0 &amp;gt; 50) {
                throw CommerceException(&quot;이벤트 상품은 50% 이상 할인 불가&quot;)
            }
            if (!categoryType.isAllowed(product.id)) {
                throw CommerceException(&quot;허용되지 않는 카테고리입니다.&quot;)
            }
        }
    }

    // 재사용 가능한 공통 검증 (판매 상태 체크)
    fun isSaleActive() = { product: ProductDto -&amp;gt;
        if (product.status == ProductStatus.ON_SALE &amp;amp;&amp;amp; product.isExpired()) {
            throw CommerceException(&quot;판매 기간이 종료된 상품은 수정할 수 없습니다.&quot;)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Facade에서 빈번하게 체크되는 로직인 경우에 이 객체를 사용했고, 단건인 경우 Facade안에서 람다로 처리하였다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-path-to-node=&quot;19&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;이제 Facade는 복잡한 if문을 쓰거나 별도의 서비스를 주입받을 필요 없이, 어떤 검증 부품을 쓸지 &lt;b data-index-in-node=&quot;36&quot; data-path-to-node=&quot;20&quot;&gt;선언&lt;/b&gt;만 하면 된다!&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahcKEwi1soaj1aSTAxUAAAAAHQAAAAAQUg&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class ProductFacade(
    private val productService: ProductService,
    // ... 수많은 서비스들
) {
    @Transactional
    suspend fun updateProduct(request: ProductUpdateRequest): ProductResponse {
        // 1. 상품을 가져와서 즉시 상태 검증 (상단 호출)
        val product = productService.getProduct(request.productId)
            .validate(ProductValidator.isSaleActive())

        // ... 카테고리 생성 등 중간 비즈니스 로직 수행 ...

        // 2. 새로운 정책과 카테고리 타입 검증 (하단 호출)
        product.validate(ProductValidator.categoryPolicy(request.categoryType, request.policy))

        return ProductResponse.of(product)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;23&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-path-to-node=&quot;23&quot; data-ke-size=&quot;size23&quot;&gt;이 방식이 주는 실무적 이점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;24&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;24,0,0&quot;&gt;관심사의 분리 (SoC):&lt;/b&gt; DTO는 데이터만, Validator는 규칙만, Facade는 흐름만 관리한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;24,1,0&quot;&gt;DTO의 순수성 유지:&lt;/b&gt; 예외 처리 코드나 특정 프레임워크 의존성이 DTO에 침투하지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;24,2,0&quot;&gt;유연한 호출 시점:&lt;/b&gt; 하나의 거대한 검증 클래스에 묶이지 않고, 필요한 시점에 필요한 검증 함수만 골라 쓸 수 있다. (제일 큰 장점)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;24,3,0&quot;&gt;OCP(개방-폐쇄 원칙) 준수:&lt;/b&gt; 새로운 검증 규칙이 추가되어도 기존 DTO나 Facade 원본 코드를 수정할 필요가 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-path-to-node=&quot;25&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;26&quot; data-ke-size=&quot;size23&quot;&gt;마치며 . . .&lt;/h3&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;visitor 패턴 정말 처음 들어봤고, 이번에 validation 리팩토링 하면서 제대로 공부할 수 있는 기회였다. 단순히 검증 서비스 하나 만들면 되는거 아냐?라고 생각했었는데,, 추후에 로직이 더 복잡해졌을 때를 대비해 확장성을 고려해보자는 시니어 개발자분의 의견에 감탄할 수밖에 없었다. 나도 멋진 시니어 개발자가 되야지.&lt;/p&gt;</description>
      <category>스터디/기타</category>
      <category>validation리팩토링</category>
      <category>VisitorPattern</category>
      <category>디자인패턴</category>
      <author>dev-yen</author>
      <guid isPermaLink="true">https://dev-yen.tistory.com/81</guid>
      <comments>https://dev-yen.tistory.com/entry/%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4-validation-%EB%A1%9C%EC%A7%81%EC%97%90-visitor-pattern-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0#entry81comment</comments>
      <pubDate>Tue, 17 Mar 2026 00:24:18 +0900</pubDate>
    </item>
    <item>
      <title>[회고] 4년차 백엔드 개발자, 첫 이직로그</title>
      <link>https://dev-yen.tistory.com/entry/%ED%9A%8C%EA%B3%A0-4%EB%85%84%EC%B0%A8-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%B2%AB-%EC%9D%B4%EC%A7%81%EB%A1%9C%EA%B7%B8</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8&quot;&gt;시작은 노션 한 페이지였다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2022년 2월 첫 커리어를 시작하고 어느덧 3년...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매일같이 물경력이 되어가는건 아닐까 하는 두려움과 불안정한 회사 상황속에서&amp;nbsp;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;고민 끝에 이직을 결정하고 가장 먼저 한 일은 노션에 '이직 진행 과정' 페이지를 만든 것이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;236&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMyCWB/dJMcacPKwsd/KianGzf8RxL5p7wUVU7LW0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMyCWB/dJMcacPKwsd/KianGzf8RxL5p7wUVU7LW0/img.jpg&quot; data-alt=&quot;당시 회사 상황&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMyCWB/dJMcacPKwsd/KianGzf8RxL5p7wUVU7LW0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMyCWB%2FdJMcacPKwsd%2FKianGzf8RxL5p7wUVU7LW0%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;281&quot; height=&quot;271&quot; data-origin-width=&quot;236&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;당시 회사 상황&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 하반기, 생성형 AI의 등장으로 신입부터 경력까지 모든 취업길이 막힌 것 같은 기분이었다. 공고는 씨가 말랐고, 하나둘 쌓여가는 지원 리스트를 보며 '내가 이직을 할 수나 있을까?' 하는 생각에 하루하루가 고통이었다. (신입 공고는 정말 찾아보기 힘들었고, 3년차 공고도 많이 없었음 ㅠ)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷_2026-02-28_오후_8_25_31.png&quot; data-origin-width=&quot;3432&quot; data-origin-height=&quot;2228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lQ5Vy/dJMcahDuMXt/KU3daIjnMkpe61SyNh81vK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lQ5Vy/dJMcahDuMXt/KU3daIjnMkpe61SyNh81vK/img.png&quot; data-alt=&quot;내 서류를 읽긴 했을까? 싶은 곳도 있음&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lQ5Vy/dJMcahDuMXt/KU3daIjnMkpe61SyNh81vK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlQ5Vy%2FdJMcahDuMXt%2FKU3daIjnMkpe61SyNh81vK%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;3432&quot; height=&quot;2228&quot; data-filename=&quot;스크린샷_2026-02-28_오후_8_25_31.png&quot; data-origin-width=&quot;3432&quot; data-origin-height=&quot;2228&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;내 서류를 읽긴 했을까? 싶은 곳도 있음&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연이은 서류 불합격의 흔적 ㅎㅎ...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 너무 정직하게 서류를 썼는데, 일단 서류가 붙어야 면접이라도 볼 거아닌가? 라는 생각에 경험을 뻠핑x3 시켜서 쓰니까 그후론 서류가 좀 붙었다. 이거 관련해서.. 초반엔 양심의 가책이 느껴졌는데, 서류에 쓴 내용을 내거로 만들기 위해 계속 보고 공부했더니 진짜 내가 한 것 처럼 느껴져서 결국에 진실이 되었음. ㅋㅋ&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b data-path-to-node=&quot;10&quot; data-index-in-node=&quot;0&quot;&gt;&quot;분명 외웠는데...&quot; 자괴감과의 싸움&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-28 오후 8.42.11.png&quot; data-origin-width=&quot;3878&quot; data-origin-height=&quot;3550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rTnzE/dJMcabi1uVO/YwFEOcAGpoGiuh90PYU2k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rTnzE/dJMcabi1uVO/YwFEOcAGpoGiuh90PYU2k0/img.png&quot; data-alt=&quot;진짜 열심히 외웠다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rTnzE/dJMcabi1uVO/YwFEOcAGpoGiuh90PYU2k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrTnzE%2FdJMcabi1uVO%2FYwFEOcAGpoGiuh90PYU2k0%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;3878&quot; height=&quot;3550&quot; data-filename=&quot;스크린샷 2026-02-28 오후 8.42.11.png&quot; data-origin-width=&quot;3878&quot; data-origin-height=&quot;3550&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;진짜 열심히 외웠다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술 면접 준비는 정말이지 지옥 같았다. 분명 머릿속으로 이해하고 달달 외웠던 개념인데, 막상 면접관 앞에만 서면 머릿속이 하얘져벌임..;; 대답 못하는 날보며 &lt;u&gt;&lt;i&gt;'얘는 안되겠다..'&lt;/i&gt;&lt;/u&gt; 라는 눈빛으로 변하는 면접관들의 모습은 내 안에 트라우마로 남아 여러번의 악몽을 꾸게 했다. ㅋㅋ큐ㅠ&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dakB8V/dJMcabDjm6N/bEHanZrPeSMajkhL0evJw1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dakB8V/dJMcabDjm6N/bEHanZrPeSMajkhL0evJw1/img.jpg&quot; data-alt=&quot;내 얘기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dakB8V/dJMcabDjm6N/bEHanZrPeSMajkhL0evJw1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdakB8V%2FdJMcabDjm6N%2FbEHanZrPeSMajkhL0evJw1%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;320&quot; height=&quot;305&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;내 얘기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;지금 입사한 회사는 기술 면접 과정 중에 '시스템 설계' 세션이 따로 있다고 사전 안내를 해줬다. 막막한 마음에 제미나이를 붙잡고 예상 문제와 설계 구조를 미친 듯이 시뮬레이션했다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-path-to-node=&quot;13&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-path-to-node=&quot;13,0&quot; data-ke-size=&quot;size16&quot;&gt;&quot;이런 도메인에서 대용량 트래픽이 발생하면 어떤 아키텍처가 좋을까?&quot; &quot;여기서 발생할 수 있는 병목 현상은?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;그런데 웬걸, 면접 당일 제미나이와 함께 고민했던 시나리오가 거의 그대로 나왔다! 속으로 '대박!'을 외치며 준비한 내용을 쏟아냈고, 그 덕분에 기분 좋게 합격 통보를 받을 수 있었다. (고맙다 제미나이...)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;309&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPAVnl/dJMcaiI9BBX/1oMRHWZHK2EGQZKJ4FeEH0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPAVnl/dJMcaiI9BBX/1oMRHWZHK2EGQZKJ4FeEH0/img.jpg&quot; data-alt=&quot;잼민이, 너 내 동료가 돼라&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPAVnl/dJMcaiI9BBX/1oMRHWZHK2EGQZKJ4FeEH0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPAVnl%2FdJMcaiI9BBX%2F1oMRHWZHK2EGQZKJ4FeEH0%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;550&quot; height=&quot;309&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;309&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;잼민이, 너 내 동료가 돼라&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;17&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;17&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-path-to-node=&quot;17&quot; data-index-in-node=&quot;0&quot;&gt;마무리하며...&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;수많은 '지원 완료'와 '불합격' 사이에서 마침내 얻어낸 '최종 합격'.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;낯선 사무실로 첫 출근을 하며 4년 전의 내 모습을 떠올렸다. 그때보다 조금 더 단단해졌을까?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;이직은 단순히 연봉을 높이는 과정이 아니라, 내가 어떤 개발자인지, 어떤 환경에서 몰입하는 사람인지 처절하게 깨닫는 시간이었던 것 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;어느덧 새 회사에서 벌써 한달이 지났는데, 정말 배울 것도 너무 많고(=모르는게 많다) 적응할 것도 너무 많은 것 같다. 다시 신입의 맘으로 돌아가서 열심히 살고 있는데 역시나 쉽지 않다 .ㅋㅋㅋ&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;앞으로는 AI한테 밀리지 않기 위해 앞으로 3년간은 뒤지게 공부하고.. 자격증도 따고.. 그럴 예정이다.. 그것에 대해서도 꾸준히 포스팅 해볼 예정ㅎㅎ&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;이직 준비를 하며 마음이 참 힘들었는데, 신기하게도 지나고 보니 벌써 미화가 됐는지 '다시 하라면 할 수 있을 것 같은데?' 하는 기분도 든다. 지금 이직을 준비하며 나와 비슷한 고민을 하는 분이 있다면, &lt;b data-index-in-node=&quot;115&quot; data-path-to-node=&quot;8&quot;&gt;마음을 단단히 먹는 게 가장 중요하다&lt;/b&gt;고 말하고 싶다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;그냥 꾸준히 하다 보면 결국 된다! 그러니 절대 포기하지 말자. 포기는 배추 셀 때나 하는 거니까! (낄낄)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cD3Ikb/dJMcachULEc/cIOgeTBm0jAVbyTZJVKZ01/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cD3Ikb/dJMcachULEc/cIOgeTBm0jAVbyTZJVKZ01/img.jpg&quot; data-alt=&quot;좋아~가는거야~!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cD3Ikb/dJMcachULEc/cIOgeTBm0jAVbyTZJVKZ01/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcD3Ikb%2FdJMcachULEc%2FcIOgeTBm0jAVbyTZJVKZ01%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;295&quot; height=&quot;215&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;좋아~가는거야~!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>일상/종알종알</category>
      <category>기술면접</category>
      <category>백엔드개발자</category>
      <category>이직기</category>
      <category>회고</category>
      <author>dev-yen</author>
      <guid isPermaLink="true">https://dev-yen.tistory.com/80</guid>
      <comments>https://dev-yen.tistory.com/entry/%ED%9A%8C%EA%B3%A0-4%EB%85%84%EC%B0%A8-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%B2%AB-%EC%9D%B4%EC%A7%81%EB%A1%9C%EA%B7%B8#entry80comment</comments>
      <pubDate>Sat, 28 Feb 2026 21:09:13 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] 자바 백엔드 개발자의 코틀린 이주 1</title>
      <link>https://dev-yen.tistory.com/entry/Kotlin-%EC%9E%90%EB%B0%94-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%9D%B4%EC%A3%BC-1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;자바(Java) 기반으로 Spring Boot 서버를 개발해오다, 최근 코틀린(Kotlin)으로 넘어가게 되어 학습하며 남기는 기록이다.&lt;br&gt;&lt;br&gt;&lt;b&gt;코틀린(Kotlin)?&lt;/b&gt;&lt;br&gt;-&amp;gt; 실용주의(Pragmatism), 간결성(Conciseness), 안전성(Safety)이라는 확실한 철학을 바탕으로 설계된 언어.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;1. 변수와 타입 시스템: 불변성과 타입 추론&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;자바는 타입을 명시하는 정적 타입 언어의 성격이 강하지만, 코틀린은 컴파일러의 &lt;b&gt;강력한 타입 추론(Type Inference)&lt;/b&gt;을 기반으로 동작한다. &lt;br&gt;&lt;br&gt;&lt;b&gt;문법 (Grammar)&lt;/b&gt;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;val (Value): 읽기 전용 변수 (Java의 final). 초기화 후 변경 불가&lt;/li&gt;&lt;li&gt;var (Variable): 수정 가능한 변수&lt;/li&gt;&lt;li&gt;타입 생략: 컴파일러가 문맥을 보고 타입을 유추&lt;/li&gt;&lt;/ul&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Kotlin&quot; data-ke-language=&quot;Kotlin&quot;&gt;&lt;code&gt;val name = &quot;Yeeun&quot; // String으로 자동 추론
var age = 10&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // Int로 자동 추론&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;&lt;b&gt;개념 (Concept: Immutability)&lt;/b&gt;&lt;br&gt;코틀린은 &quot;기본적인 불변성(Immutability)&quot;을 지향한다.&lt;br&gt;자바에서는 final을 매번 붙이기 귀찮아서 생략하는 경우가 많지만, 코틀린은 &lt;b&gt;val&lt;/b&gt; 사용을 기본으로 권장한다. &lt;br&gt;이는 멀티스레드 환경에서의 안전성(Thread Safety)을 높이고, 사이드 이펙트(Side Effect)를 줄여 코드의 예측 가능성을 높여준다.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;2. Lombok의 은퇴와 &lt;b&gt;data class&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;자바 개발자라면 떼려야 뗄 수 없는 @Getter, @Setter, @RequiredArgsConstructor, @ToString...&lt;br&gt;코틀린에서는 이 모든 어노테이션 지옥에서 해방될 수 있다.&lt;br&gt;&lt;br&gt;&lt;b&gt;Before (Java)&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Java&quot; data-ke-language=&quot;Java&quot;&gt;&lt;code&gt;@Getter
@RequiredArgsConstructor
public class UserDto {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final String name;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final int age;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;&lt;b&gt;After (Kotlin)&lt;/b&gt;&lt;br&gt;주생성자(Primary Constructor) 문법을 사용하면 클래스 선언과 필드 정의, 생성자 주입이 한 줄로 끝난다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Kotlin&quot; data-ke-language=&quot;Kotlin&quot;&gt;&lt;code&gt;// val: 읽기 전용 (Getter만 생성), var: 수정 가능 (Getter/Setter 생성)
data class UserDto(val name: String, val age: Int)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;&lt;b&gt;개념 (Concept: Boilerplate reduction)&lt;/b&gt;&lt;br&gt;data class는 단순한 클래스가 아니고, 컴파일러가 내부적으로 다음 기능들을 자동으로 생성해 준다.&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;equals() &amp;amp; hashCode(): 데이터 값 비교를 위해&lt;/li&gt;&lt;li&gt;toString(): 객체 정보를 예쁘게 출력&lt;/li&gt;&lt;li&gt;copy(): 불변 객체의 일부 값만 변경하여 복사 (매우 유용)&lt;/li&gt;&lt;li&gt;componentN(): 구조 분해 선언 지원&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;&lt;/p&gt;&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;3. Null Safety: 언어 차원의 NPE 방지&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;코틀린은 Null이 될 수 있는 타입과 없는 타입을 컴파일 단계에서 엄격하게 구분한다.&lt;br&gt;&lt;br&gt;&lt;b&gt;문법 (Grammar)&lt;/b&gt;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;String: 절대 Null이 들어갈 수 없음&lt;/li&gt;&lt;li&gt;String?: Null이 들어갈 수 있음&lt;/li&gt;&lt;li&gt;Safe Call (?.): Null이 아니면 실행, Null이면 Null 반환&lt;/li&gt;&lt;li&gt;Elvis Operator (?:): Null일 경우 사용할 기본값을 지정한다&lt;/li&gt;&lt;/ul&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Java&quot; data-ke-language=&quot;Java&quot;&gt;&lt;code&gt;// Java: 런타임에 터져봐야 앎
String name = null; 
// name.length(); // NPE 발생!&lt;/code&gt;&lt;/pre&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Kotlin&quot; data-ke-language=&quot;Kotlin&quot;&gt;&lt;code&gt;// Kotlin: 컴파일조차 안 됨 (컴파일 타임 방어)
val name: String = null // [Compile Error] Null can not be a value of a non-null type

val nullableName: String? = null
// nullableName.length // [Compile Error] 그냥 호출 불가능
println(nullableName?.length ?: 0) // 안전하게 0 출력&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;&lt;b&gt;개념 (Concept: Type System)&lt;/b&gt;&lt;br&gt;이것은 단순한 체크 기능이 아닌 코틀린의 타입 시스템 자체가 &quot;Null 가능성을 타입의 속성으로 포함&quot;시켰다는 뜻. &lt;br&gt;즉, 런타임 에러를 컴파일 타임 에러로 끌어와 소프트웨어의 안정성을 획기적으로 높였다.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;4. 표현식(Expression) 중심의 언어&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;자바의 if, try-catch는 문장(Statement)이지만, 코틀린에서는 &lt;b&gt;표현식(Expression)&lt;/b&gt;이다.&lt;br&gt;&lt;br&gt;&lt;b&gt;개념 (Concept: Expression vs Statement)&lt;/b&gt;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;문장(Statement): 명령을 실행하고 끝남 (값을 반환하지 않음)&lt;/li&gt;&lt;li&gt;표현식(Expression): 실행 후 결과값(Value)을 반환&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;이 개념 덕분에 코틀린에서는 3항 연산자가 필요 없고, 함수형 프로그래밍 스타일의 코딩이 가능해진다.&lt;br&gt;&lt;br&gt;&lt;b&gt;문법 (Grammar)&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Java&quot; data-ke-language=&quot;Java&quot;&gt;&lt;code&gt;// Java (Statement 스타일)
String tier;
if (score &amp;gt; 90) { tier = &quot;A&quot;; } else { tier = &quot;B&quot;; }&lt;/code&gt;&lt;/pre&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Kotlin&quot; data-ke-language=&quot;Kotlin&quot;&gt;&lt;code&gt;// Kotlin (Expression 스타일)
val tier = if (score &amp;gt; 90) &quot;A&quot; else &quot;B&quot; // if 자체가 값을 반환

// 함수도 표현식으로 정의 가능 (Single-Expression Function)
fun getTier(score: Int) = if (score &amp;gt; 90) &quot;A&quot; else &quot;B&quot;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;/p&gt;&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;5. 유틸 클래스의 종말: &lt;b&gt;확장 함수&lt;/b&gt; (Extension Function)&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;StringUtils, DateUtils 같은 정적 유틸리티 클래스를 만들어서 Utils.method(obj) 처럼 호출하던 방식이, 객체 지향적으로 obj.method() 형태로 바뀐다.&lt;br&gt;(개인적으로 가장 신기했던 부분이다)&lt;br&gt;&lt;br&gt;&lt;b&gt;Before (Java)&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Java&quot; data-ke-language=&quot;Java&quot;&gt;&lt;code&gt;// 수동적인 호출: 주어(path)가 목적어로 들어감
String url = ImgPathUtil.buildUrl(path); &lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;&lt;b&gt;After (Kotlin)&lt;/b&gt;&lt;br&gt;기존 클래스(String)를 수정하지 않고도, 나만의 함수를 '확장'해서 붙일 수 있다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Kotlin&quot; data-ke-language=&quot;Kotlin&quot;&gt;&lt;code&gt;// String 클래스 확장
fun String?.toS3Url(): String? {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (this.isNullOrEmpty()) return null
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return &quot;https://s3.aws.com/$this&quot; // this가 곧 호출한 객체
}

// 능동적인 호출: 주어(path)가 동작을 수행
val url = path.toS3Url() &lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;&lt;b&gt;개념 (Concept: Pragmatism)&lt;/b&gt;&lt;br&gt;&quot;어떻게 남의 클래스에 메소드를 추가하지?&quot; 이상하게 보이지만, 컴파일되면 자바의 public static 메소드로 변환된다. &lt;br&gt;즉, 성능 저하 없이 가독성을 극대화하는 실용적인 기능이다.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;6. 프로퍼티(Property)와 캡슐화&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;자바에서는 데이터를 숨기기 위해 private field를 만들고 public getter/setter를 연다(Java Beans 패턴). &lt;br&gt;코틀린은 이를 &lt;b&gt;프로퍼티(Property)&lt;/b&gt;라는 하나의 개념으로 통합했다.&lt;br&gt;&lt;br&gt;&lt;b&gt;문법 (Grammar)&lt;/b&gt;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;코틀린 클래스의 변수는 단순한 변수가 아니라 프로퍼티이다.&lt;/li&gt;&lt;li&gt;val 선언 시: field + getter 자동 생성&lt;/li&gt;&lt;li&gt;var 선언 시: field + getter + setter 자동 생성&lt;/li&gt;&lt;/ul&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Kotlin&quot; data-ke-language=&quot;Kotlin&quot;&gt;&lt;code&gt;class User(val name: String) 
// 내부적으로 private final String name 필드와 public String getName() 메소드가 생성됨

사용하는 쪽의 변화
val user = User(&quot;Gemini&quot;)
println(user.name) // 실제로는 user.getName()이 호출됨&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;자바 개발자가 코틀린 코드를 볼 때 &quot;왜 필드에 직접 접근하지? 캡슐화 위반 아닌가?&quot;라고 오해하기 쉽지만, 실제로는 캡슐화된 접근자(Accessor)를 통해 안전하게 접근하는 것.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;7. Static의 실종과 동반 객체 (Companion Object)&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;코틀린은 static 대신 &lt;b&gt;동반 객체(Companion Object)&lt;/b&gt;라는 독특한 개념을 사용한다.&lt;br&gt;&lt;br&gt;&lt;b&gt;개념 (Concept: True OOP)&lt;/b&gt;&lt;br&gt;자바의 static 메소드는 객체 지향적이지 않다. &lt;br&gt;=&amp;gt; 클래스라는 틀 안에 있지만, 실제로는 인스턴스와 무관하게 떠 있는 절차적 함수에 가깝다.&lt;br&gt;&lt;br&gt;코틀린은 &quot;&lt;u&gt;모든 것은 객체다&lt;/u&gt;&quot;라는 철학을 따른다.&lt;/p&gt;&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;&lt;li&gt;자바의 static: 클래스에 붙어 있는 '정적 멤버'&lt;/li&gt;&lt;li&gt;코틀린의 Companion Object: 클래스와 '동반(Companion)'되는 '유일한 객체(Singleton Object)'&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;=&amp;gt; 즉, 코틀린에서는 정적 멤버처럼 보이는 것들도 실제로는 &quot;특수한 객체(Companion)의 인스턴스 멤버&quot;로 취급.&lt;br&gt;&lt;br&gt;&lt;b&gt;문법 (Grammar)&lt;/b&gt;&lt;br&gt;클래스 안에 companion object 블록을 만들고, 그 안에 변수나 함수를 넣습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Kotlin&quot; data-ke-language=&quot;Kotlin&quot;&gt;&lt;code&gt;class AwsConfig {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 1. 일반 인스턴스 프로퍼티
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;val serviceName = &quot;S3&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 2. 동반 객체 (Java의 static 영역 대체)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;companion object {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 컴파일 타임 상수 (static final)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const val TIMEOUT = 10_000 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 팩토리 메소드 (static method 처럼 사용)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fun create(): AwsConfig = AwsConfig()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;사용하는 쪽의 변화&lt;br&gt;(코드 자체는 자바의 static 호출과 거의 같아 보이지만, 실제로는 객체 접근이 일어난다)&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Kotlin&quot; data-ke-language=&quot;Kotlin&quot;&gt;&lt;code&gt;// Java: AwsConfig 클래스의 static 변수 접근
// int t = AwsConfig.TIMEOUT;

// Kotlin: AwsConfig 클래스의 ‘동반 객체’의 프로퍼티 접근
val t = AwsConfig.TIMEOUT
// 실제 동작: AwsConfig.Companion.getTIMEOUT()&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;++추가 (자바와 혼용할떈?)&lt;br&gt;만약 자바 코드에서 코틀린의 Companion Object를 호출해야 한다면, @JvmStatic 어노테이션을 붙여줘야 자바의 static 메소드처럼 편하게 호출할 수 있다.&lt;br&gt;안그러면 .Companion을 붙여야 하는 번거로움이 생긴다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Kotlin&quot; data-ke-language=&quot;Kotlin&quot;&gt;&lt;code&gt;companion object {
	@JvmStatic
	fun create(): AwsConfig = AwsConfig()
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;&lt;/p&gt;&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;번외: QueryDSL과 JPA의 실무 팁&lt;/h4&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;b&gt;Stream API 제거&lt;/b&gt;: 코틀린 컬렉션(List) 자체가 강력한 함수형 API를 내장하고 있어 .stream().collect() 과정이 필요 없다.&lt;/li&gt;&lt;li&gt;프로젝션: Projections.constructor 사용 시 ::class.java 문법을 사용.&lt;/li&gt;&lt;li&gt;예약어 처리: in 같은 예약어는 백틱(`)으로 감싸준다.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;실전 코드 (QueryDSL Repository)&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Kotlin&quot; data-ke-language=&quot;Kotlin&quot;&gt;&lt;code&gt;fun findCreators(creatorNos: List&amp;lt;Long&amp;gt;): List&amp;lt;Creator&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return queryFactory
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.selectFrom(creator)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.where(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;creator.creatorNo.`in`(creatorNos), // `in` 예약어 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;creator.status.eq(Status.ACTIVE)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.fetch() // 결과가 없으면 빈 리스트 반환 (Null Safe)
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;&lt;/p&gt;&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;마무리하며. . .&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;새로 이직할 곳의 기술스택이 코틀린을 쓴다고 하여, 미리 공부해보는 중인데 자바의 개념은 가져가면서도 훨씬 실용주의적인 면이 신기하다.&lt;br&gt;특히, 확장함수는 진짜 신박했다(ㅋ̤̮ㅋ͈̮)&lt;br&gt;다음 포스팅에선 코루틴에 대해 다뤄보도록 하겠다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;</description>
      <category>스터디/기타</category>
      <category>Java</category>
      <category>kotlin</category>
      <category>Spring</category>
      <author>dev-yen</author>
      <guid isPermaLink="true">https://dev-yen.tistory.com/79</guid>
      <comments>https://dev-yen.tistory.com/entry/Kotlin-%EC%9E%90%EB%B0%94-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%9D%B4%EC%A3%BC-1#entry79comment</comments>
      <pubDate>Mon, 22 Dec 2025 15:33:24 +0900</pubDate>
    </item>
    <item>
      <title>FFmpeg와 RTMP 서버로 오디오 스트리밍 실험하기</title>
      <link>https://dev-yen.tistory.com/entry/FFmpeg%EC%99%80-RTMP-%EC%84%9C%EB%B2%84%EB%A1%9C-%EC%98%A4%EB%94%94%EC%98%A4-%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D-%EC%8B%A4%ED%97%98%ED%95%98%EA%B8%B0</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 음원 스트리밍은 어떻게 구현할 수 있을까?&lt;br /&gt;라는 궁금증에서 시작된 작은 실험 기록&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FFmpeg와 HLS, RTMP란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;FFmpeg&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 멀티미디어 변환 툴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FFmpeg은 오디오&amp;middot;비디오를 &lt;b&gt;인코딩 / 디코딩 / 변환 / 스트리밍&lt;/b&gt;할 수 있는 오픈소스 멀티미디어 프레임워크다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;거의 모든 포맷 지원 (mp3, mp4, flv, mpeg 등)&lt;/li&gt;
&lt;li&gt;실시간 변환 및 송출 가능 (-re, -f flv, -c:a aac 등)&lt;/li&gt;
&lt;li&gt;방송, CCTV, OTT 등 다양한 시스템에서 내부적으로 사용됨FFmpeg 명령어 옵션&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FFmpeg 옵션&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;옵션&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;전체 표기&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;비고 / 사용시점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-re&lt;/td&gt;
&lt;td&gt;-re&lt;/td&gt;
&lt;td&gt;입력 속도를 &lt;b&gt;실시간(real-time)&lt;/b&gt; 으로 제한&lt;/td&gt;
&lt;td&gt;주로 스트리밍 송출 시 사용. 파일을 한 번에 처리하지 않고 실제 재생 속도로 읽음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-i&lt;/td&gt;
&lt;td&gt;-i &amp;lt;input&amp;gt;&lt;/td&gt;
&lt;td&gt;입력 파일 지정&lt;/td&gt;
&lt;td&gt;예: -i input.mp3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-c:a&lt;/td&gt;
&lt;td&gt;-c:a &amp;lt;codec&amp;gt;&lt;/td&gt;
&lt;td&gt;오디오 인코더(codec) 지정&lt;/td&gt;
&lt;td&gt;예: -c:a aac &amp;rarr; AAC로 인코딩. HLS 호환성 좋음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-b:a&lt;/td&gt;
&lt;td&gt;-b:a &amp;lt;bitrate&amp;gt;&lt;/td&gt;
&lt;td&gt;오디오 비트레이트 지정&lt;/td&gt;
&lt;td&gt;예: -b:a 128k &amp;rarr; 128kbps로 인코딩&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-f&lt;/td&gt;
&lt;td&gt;-f &amp;lt;format&amp;gt;&lt;/td&gt;
&lt;td&gt;출력 컨테이너 포맷 지정&lt;/td&gt;
&lt;td&gt;예: -f flv &amp;rarr; RTMP 송출 시 필수 (RTMP는 FLV 포맷으로 전송됨)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-vn&lt;/td&gt;
&lt;td&gt;-vn&lt;/td&gt;
&lt;td&gt;비디오 스트림 제거&lt;/td&gt;
&lt;td&gt;오디오만 송출할 때 사용 (선택사항)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-ac&lt;/td&gt;
&lt;td&gt;-ac &amp;lt;channels&amp;gt;&lt;/td&gt;
&lt;td&gt;오디오 채널 수 지정&lt;/td&gt;
&lt;td&gt;예: -ac 2 &amp;rarr; 스테레오&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-ar&lt;/td&gt;
&lt;td&gt;-ar &amp;lt;rate&amp;gt;&lt;/td&gt;
&lt;td&gt;샘플링 레이트 지정&lt;/td&gt;
&lt;td&gt;예: -ar 44100 &amp;rarr; CD 품질&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-y&lt;/td&gt;
&lt;td&gt;-y&lt;/td&gt;
&lt;td&gt;기존 파일 덮어쓰기 허용&lt;/td&gt;
&lt;td&gt;자동화 스크립트에 자주 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-loglevel&lt;/td&gt;
&lt;td&gt;-loglevel &amp;lt;level&amp;gt;&lt;/td&gt;
&lt;td&gt;로그 출력 레벨 설정&lt;/td&gt;
&lt;td&gt;예: -loglevel info, -loglevel quiet 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-f hls&lt;/td&gt;
&lt;td&gt;-f hls&lt;/td&gt;
&lt;td&gt;HLS 세그먼트로 직접 출력&lt;/td&gt;
&lt;td&gt;RTMP 없이 HLS만 생성할 때 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-hls_time&lt;/td&gt;
&lt;td&gt;-hls_time &amp;lt;sec&amp;gt;&lt;/td&gt;
&lt;td&gt;HLS 세그먼트 길이(초) 지정&lt;/td&gt;
&lt;td&gt;예: -hls_time 5 &amp;rarr; 5초 단위 TS 조각 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-hls_list_size&lt;/td&gt;
&lt;td&gt;-hls_list_size &amp;lt;n&amp;gt;&lt;/td&gt;
&lt;td&gt;m3u8 리스트에 포함할 세그먼트 개수&lt;/td&gt;
&lt;td&gt;0이면 모든 세그먼트 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-hls_segment_filename&lt;/td&gt;
&lt;td&gt;-hls_segment_filename &amp;lt;path&amp;gt;&lt;/td&gt;
&lt;td&gt;세그먼트 파일명 패턴 지정&lt;/td&gt;
&lt;td&gt;예: segment_%03d.ts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오디오 코덱(Audio Codec)?&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Codec = Coder + Decoder&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오디오 코덱은 &lt;b&gt;소리를 디지털 데이터로 &amp;ldquo;압축하고(encode)&amp;rdquo; 다시 &amp;ldquo;복원(decode)&amp;rdquo;하는 기술이다&lt;/b&gt;&lt;br /&gt;압축을 통해 파일 용량을 줄이면서도 음질 손실을 최소화하는 것이 핵심 목표이며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오디오 스트리밍에서는 코덱의 종류에 따라&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전송 효율&lt;/li&gt;
&lt;li&gt;음질&lt;/li&gt;
&lt;li&gt;디바이스 호환성&lt;br /&gt;이 달라지기 때문에, FFmpeg으로 변환할 때 -c:a 옵션으로 코덱을 지정하는 것이 중요하다&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 22.6161%;&quot;&gt;&lt;b&gt;코덱&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 12.5917%;&quot;&gt;&lt;b&gt;형식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.1247%;&quot;&gt;&lt;b&gt;압축 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.0637%;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.6039%;&quot;&gt;&lt;b&gt;주 사용처&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 22.6161%;&quot;&gt;&lt;b&gt;AAC (Advanced Audio Coding)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 12.5917%;&quot;&gt;손실(Lossy)&lt;/td&gt;
&lt;td style=&quot;width: 11.1247%;&quot;&gt;효율적 압축&lt;/td&gt;
&lt;td style=&quot;width: 36.0637%;&quot;&gt;MP3보다 더 높은 압축 효율과 음질을 제공. HLS, RTMP, YouTube 등 표준 지원&lt;/td&gt;
&lt;td style=&quot;width: 17.6039%;&quot;&gt;스트리밍 서비스, 모바일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 22.6161%;&quot;&gt;&lt;b&gt;MP3 (MPEG Layer-3)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 12.5917%;&quot;&gt;손실(Lossy)&lt;/td&gt;
&lt;td style=&quot;width: 11.1247%;&quot;&gt;중간 효율&lt;/td&gt;
&lt;td style=&quot;width: 36.0637%;&quot;&gt;가장 널리 쓰이는 전통적 포맷. 호환성은 높지만 효율은 AAC보다 낮음&lt;/td&gt;
&lt;td style=&quot;width: 17.6039%;&quot;&gt;음악 파일, 웹 다운로드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 22.6161%;&quot;&gt;&lt;b&gt;Opus&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 12.5917%;&quot;&gt;손실(Lossy)&lt;/td&gt;
&lt;td style=&quot;width: 11.1247%;&quot;&gt;가변 압축&lt;/td&gt;
&lt;td style=&quot;width: 36.0637%;&quot;&gt;음성 통화 및 실시간 전송에 특화. 저비트레이트에서도 좋은 품질&lt;/td&gt;
&lt;td style=&quot;width: 17.6039%;&quot;&gt;WebRTC, Discord, Zoom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 22.6161%;&quot;&gt;&lt;b&gt;FLAC (Free Lossless Audio Codec)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 12.5917%;&quot;&gt;무손실(Lossless)&lt;/td&gt;
&lt;td style=&quot;width: 11.1247%;&quot;&gt;비압축&lt;/td&gt;
&lt;td style=&quot;width: 36.0637%;&quot;&gt;원음 그대로 보존. 용량 큼&lt;/td&gt;
&lt;td style=&quot;width: 17.6039%;&quot;&gt;고음질 음원, 오디오 아카이브&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 22.6161%;&quot;&gt;&lt;b&gt;PCM (Pulse Code Modulation)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 12.5917%;&quot;&gt;무손실(Raw)&lt;/td&gt;
&lt;td style=&quot;width: 11.1247%;&quot;&gt;비압축&lt;/td&gt;
&lt;td style=&quot;width: 36.0637%;&quot;&gt;완전한 원음. 용량 매우 큼, 실시간 전송 비효율적&lt;/td&gt;
&lt;td style=&quot;width: 17.6039%;&quot;&gt;CD, 녹음 스튜디오, 내부 처리용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정리하자면&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실시간 송출(HLS, RTMP)에는 &lt;b&gt;AAC&lt;/b&gt;가 표준이며,&lt;/li&gt;
&lt;li&gt;실시간 음성통화나 회의(WebRTC 등)에는 &lt;b&gt;Opus&lt;/b&gt;가 가장 효율적&lt;/li&gt;
&lt;li&gt;음질 보존이 중요한 음원 관리나 분석 목적이라면 &lt;b&gt;FLAC&lt;/b&gt; 또는 &lt;b&gt;PCM&lt;/b&gt;을 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비트레이트(Bit Rate)?&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;1초 동안 전송하거나 저장되는 데이터의 양 (단위: kbps)&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오디오의 &lt;b&gt;비트레이트(Bit Rate)&lt;/b&gt; 는 소리의 세밀함과 데이터 양을 동시에 결정하는 지표이다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단위: kbps (kilobits per second)&lt;/li&gt;
&lt;li&gt;수치가 높을수록 &lt;b&gt;음질은 좋지만&lt;/b&gt;, &lt;b&gt;용량과 대역폭&lt;/b&gt;도 커진다&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;비트레이트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;음질 수준&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;64 kbps 이하&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;음성 통화 수준 (주로 Opus 사용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;96~128 kbps&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;일반적인 스트리밍 품질 (AAC 표준)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;192~256 kbps&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;td&gt;음악 감상용 고음질&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;320 kbps 이상&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;매우 높음&lt;/td&gt;
&lt;td&gt;거의 원음에 가까움, 파일 크기 큼&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어,&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;-b:a 128k
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령은 &lt;b&gt;오디오 비트레이트를 128kbps&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;샘플링 레이트(Sampling Rate)?&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;&lt;b&gt;1초에 아날로그 소리를 몇 번 디지털 신호로 측정(Sample)&lt;/b&gt; 하는가&amp;rdquo;를 의미&lt;br /&gt;단위는 &lt;b&gt;Hz (헤르츠)&lt;/b&gt; 또는 &lt;b&gt;kHz (킬로헤르츠)&lt;/b&gt; 로 표시&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소리는 원래 &lt;b&gt;연속적인 파형(아날로그)&lt;/b&gt; 형태로 존재.&lt;/li&gt;
&lt;li&gt;컴퓨터는 이걸 직접 저장할 수 없기 때문에 &lt;b&gt;일정한 간격으로 파형의 값을 &amp;lsquo;찍어서&amp;rsquo;(sampling)&lt;/b&gt; 디지털 신호로 변환&lt;/li&gt;
&lt;li&gt;이때 1초 동안 몇 번을 찍느냐가 바로 &lt;b&gt;샘플링 레이트&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;샘플링 레이트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;용도&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;8,000 Hz (8kHz)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1초에 8,000번 측정&lt;/td&gt;
&lt;td&gt;전화 음성 품질 (통화용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;22,050 Hz (22.05kHz)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1초에 22,050번 측정&lt;/td&gt;
&lt;td&gt;저음질 음원, 스트리밍 라디오&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;44,100 Hz (44.1kHz)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1초에 44,100번 측정&lt;/td&gt;
&lt;td&gt;CD 음질 (가장 일반적인 표준)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;48,000 Hz (48kHz)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1초에 48,000번 측정&lt;/td&gt;
&lt;td&gt;방송, 영상, 스트리밍 표준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;96,000 Hz 이상&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1초에 96,000번 이상 측정&lt;/td&gt;
&lt;td&gt;고음질 스튜디오 녹음용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;샘플링 레이트가 높을수록&lt;/b&gt;, 더 세밀하게 소리를 표현할 수 있다. 하지만 &lt;b&gt;파일 용량도 비례해서 커진다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람의 귀가 들을 수 있는 최대 주파수는 약 &lt;b&gt;20kHz&lt;/b&gt;이므로, 이를 정확히 표현하려면 &lt;b&gt;2배 이상(40kHz 이상)&lt;/b&gt; 샘플링해야 한다는&lt;br /&gt;&lt;b&gt;&amp;ldquo;나이퀴스트 이론(Nyquist Theorem)&amp;rdquo;&lt;/b&gt; 을 따른다.&lt;br /&gt;&amp;rarr; 그래서 &lt;b&gt;44.1kHz(CD 표준)&lt;/b&gt; 이 탄생&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;코덱(Codec)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;압축/복원 방식&lt;/td&gt;
&lt;td&gt;어떤 방법으로 그림을 그릴지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;비트레이트(Bit Rate)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;kbps&lt;/td&gt;
&lt;td&gt;정보량(음질&amp;middot;용량 결정)&lt;/td&gt;
&lt;td&gt;얼마나 진하게 색을 칠할지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;샘플링 레이트(Sampling Rate)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Hz (kHz)&lt;/td&gt;
&lt;td&gt;시간 단위당 샘플링 횟수&lt;/td&gt;
&lt;td&gt;얼마나 자주 점을 찍을지 (해상도)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정리하자면&lt;/b&gt;,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샘플링 레이트는 &lt;b&gt;음의 해상도&lt;/b&gt;,&lt;br /&gt;비트레이트는 &lt;b&gt;음의 밀도&lt;/b&gt;,&lt;br /&gt;코덱은 &lt;b&gt;압축의 방식&lt;/b&gt;을 결정한다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&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;HLS (HTTP Live Streaming)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;: 미디어 파일을 작은 세그먼트 단위로 나누어 전송하는 스트리밍 프로토콜&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apple이 개발한 HLS는 하나의 긴 미디어 파일을 &lt;b&gt;작은 조각(.ts)&lt;/b&gt; 으로 나누고, 이를 재생 순서대로 나열한 &lt;b&gt;플레이리스트(.m3u8)&lt;/b&gt; 파일을 통해 재생한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 기반으로 동작 &amp;rarr; 별도 소켓 서버 불필요&lt;/li&gt;
&lt;li&gt;네트워크 환경이 불안정해도 안정적인 재생&lt;/li&gt;
&lt;li&gt;CDN 캐싱에 유리하여 OTT 서비스에서 많이 활용됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;RTMP (Real-Time Messaging Protocol)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;: 송출(ingest)용 프로토콜&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RTMP는 원래 Flash 기반 실시간 스트리밍 프로토콜로, 지금은 주로 &lt;b&gt;송출(ingest)&lt;/b&gt; 용도로만 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;FFmpeg이 RTMP로 데이터를 송출하고&lt;/b&gt;, &lt;b&gt;Nginx의 RTMP 모듈이 이를 받아 HLS 파일로 변환&lt;/b&gt;하는 구조를 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  FFmpeg &amp;rarr; Nginx-RTMP &amp;rarr; HLS(.m3u8)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실험 목표&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로컬 mp3 파일을 FFmpeg으로 송출 &amp;rarr; Nginx-RTMP 서버가 이를 받아 HLS로 변환 &amp;rarr; 브라우저나 VLC에서 재생&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;macOS&lt;/li&gt;
&lt;li&gt;Docker Compose&lt;/li&gt;
&lt;li&gt;FFmpeg 최신 버전&lt;/li&gt;
&lt;li&gt;Nginx (RTMP + HLS 모듈 빌드 버전)&lt;/li&gt;
&lt;li&gt;VLC (재생 확인용)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;디렉토리 구조&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760882309521&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;project-root/
├── docker-compose.yml
├── nginx.conf
├── hls/                  # HLS 세그먼트(.ts), index.m3u8 생성 위치
└── input.mp3             # 송출 테스트용 파일&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;&lt;b&gt;docker-compose.yml&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;version: &quot;3.8&quot;

services:
  nginx_rtmp_server:
    image: nginx:latest
    container_name: nginx_rtmp_server
    ports:
      - &quot;1935:1935&quot;   # RTMP 입력 포트
      - &quot;8080:80&quot;     # HTTP 접근 포트 (HLS 재생용)
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./hls:/usr/share/nginx/html/hls
    command: [&quot;/bin/bash&quot;, &quot;-c&quot;, &quot;nginx -g 'daemon off;'&quot;]
    restart: unless-stopped

  ffmpeg_transcoder:
    image: jrottenberg/ffmpeg:latest
    container_name: ffmpeg_transcoder
    depends_on:
      - nginx_rtmp_server
    volumes:
      - ./input.mp3:/input.mp3:ro
    command: &amp;gt;
      ffmpeg -re -i /input.mp3
      -c:a aac -b:a 128k -f flv
      rtmp://nginx_rtmp_server/live/stream&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;nginx_rtmp_server는 RTMP 입력을 받고 .m3u8 / .ts 파일을 생성&lt;/li&gt;
&lt;li&gt;ffmpeg_transcoder는 로컬 mp3 파일을 실시간 송출
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;-re | 실시간 재생 속도로 입력&lt;/li&gt;
&lt;li&gt;-c:a aac | 오디오를 AAC로 인코딩&lt;/li&gt;
&lt;li&gt;-b:a 128k | 오디오 비트레이트 128kbps&lt;/li&gt;
&lt;li&gt;-f flv | RTMP 송출용 컨테이너 포맷&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;./hls 폴더는 HLS 세그먼트 파일이 실시간으로 쌓이는 곳&lt;/li&gt;
&lt;li&gt;8080 포트로 접근하면 VLC나 브라우저에서 재생 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;nginx.conf&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;# 프로세스 설정
worker_processes 1;

#   반드시 있어야 함: event 루프 정의
events {
    worker_connections 1024;
}

# RTMP 서버 블록
rtmp {
    server {
        listen 1935;          # RTMP 송출 수신 포트
        chunk_size 4096;

        application live {
            live on;
            record off;

            # RTMP 입력 &amp;rarr; HLS 변환 설정
            hls on;
            hls_path /usr/share/nginx/html/hls; 
            hls_fragment 3s;                    # 각 TS 조각 길이
            hls_playlist_length 15s;            # 재생목록 길이
            hls_cleanup on;                     # 오래된 세그먼트 자동삭제
        }
    }
}

# HTTP 서버 블록 (HLS 파일 서빙)
http {
    include mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;

    server {
        listen 80;
        server_name localhost;

        location /hls {
            # RTMP에서 생성한 HLS 파일 디렉토리
            root /usr/share/nginx/html;
            
            # 브라우저 접근 허용 (CORS)
            add_header Access-Control-Allow-Origin *;

            # 캐시 방지
            add_header Cache-Control no-cache;

            # HLS MIME 타입 지정
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트용 mp3 파일 준비&lt;/b&gt;&lt;br /&gt;input.mp3 파일을 프로젝트 루트에 배치&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;Docker Compose 실행&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker-compose up
&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;FFmpeg 송출 로그 확인&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2272&quot; data-origin-height=&quot;1252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ennk0R/dJMb83EzubL/JhFQ8KY0N37ApVXNg9QRe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ennk0R/dJMb83EzubL/JhFQ8KY0N37ApVXNg9QRe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ennk0R/dJMb83EzubL/JhFQ8KY0N37ApVXNg9QRe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fennk0R%2FdJMb83EzubL%2FJhFQ8KY0N37ApVXNg9QRe0%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;2272&quot; height=&quot;1252&quot; data-origin-width=&quot;2272&quot; data-origin-height=&quot;1252&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;&lt;b&gt;HLS 파일 확인&lt;/b&gt;&lt;br /&gt;./hls 폴더에 .ts 조각들과 index.m3u8 생성됨 (실시간으로 ts파일이 생성되는 걸 확인할 수 있다)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;588&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1NG3n/dJMb9Qywrwv/KFqy4tEWDYrCzw2BNRS72K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1NG3n/dJMb9Qywrwv/KFqy4tEWDYrCzw2BNRS72K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1NG3n/dJMb9Qywrwv/KFqy4tEWDYrCzw2BNRS72K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1NG3n%2FdJMb9Qywrwv%2FKFqy4tEWDYrCzw2BNRS72K%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;197&quot; height=&quot;322&quot; data-origin-width=&quot;588&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;VLC나 브라우저에서 재생&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;http://localhost:8080/hls/index.m3u8
&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;스크린샷 2025-10-19 오후 11.27.18.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1890&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddMSU9/dJMb9MplJ7j/CkaLFKRkqCk7rtZ8lEhE80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddMSU9/dJMb9MplJ7j/CkaLFKRkqCk7rtZ8lEhE80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddMSU9/dJMb9MplJ7j/CkaLFKRkqCk7rtZ8lEhE80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddMSU9%2FdJMb9MplJ7j%2FCkaLFKRkqCk7rtZ8lEhE80%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;3024&quot; height=&quot;1890&quot; data-filename=&quot;스크린샷 2025-10-19 오후 11.27.18.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1890&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1656&quot; data-origin-height=&quot;946&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWS4an/dJMb9W6u4MW/zCERjiywDH4w6tq5bfBTc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWS4an/dJMb9W6u4MW/zCERjiywDH4w6tq5bfBTc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWS4an/dJMb9W6u4MW/zCERjiywDH4w6tq5bfBTc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWS4an%2FdJMb9W6u4MW%2FzCERjiywDH4w6tq5bfBTc1%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;1656&quot; height=&quot;946&quot; data-origin-width=&quot;1656&quot; data-origin-height=&quot;946&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;정상적으로 오디오가 스트리밍처럼 재생된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;다음엔 CDN을 붙여서 다시 듣기 기능(Replay)을 추가해보려한다 : )&lt;/p&gt;</description>
      <category>프로젝트</category>
      <category>ffmpeg</category>
      <category>HLS</category>
      <category>nginx</category>
      <category>RTMP</category>
      <category>VLC</category>
      <author>dev-yen</author>
      <guid isPermaLink="true">https://dev-yen.tistory.com/78</guid>
      <comments>https://dev-yen.tistory.com/entry/FFmpeg%EC%99%80-RTMP-%EC%84%9C%EB%B2%84%EB%A1%9C-%EC%98%A4%EB%94%94%EC%98%A4-%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D-%EC%8B%A4%ED%97%98%ED%95%98%EA%B8%B0#entry78comment</comments>
      <pubDate>Sun, 19 Oct 2025 23:46:15 +0900</pubDate>
    </item>
    <item>
      <title>[Algorithm] 프로그래머스 - 단어 변환</title>
      <link>https://dev-yen.tistory.com/entry/Algorithm-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%8B%A8%EC%96%B4-%EB%B3%80%ED%99%98</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43163&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[프로그래머스] 단어 변환&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1760186171869&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;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43163&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bAgEnw/hyZKaI0pBa/oyprOtIlgXQw3d3GUr92Z0/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/bayOn6/hyZLqi0FLk/rrtGYrVqA06kg3TRy54wPk/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43163&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43163&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bAgEnw/hyZKaI0pBa/oyprOtIlgXQw3d3GUr92Z0/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/bayOn6/hyZLqi0FLk/rrtGYrVqA06kg3TRy54wPk/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&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;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;사용 알고리즘&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;- BFS (너비우선탐색) / 그래프 탐색&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;- 시간복잡도 O(N&amp;sup2; &amp;times; L) (N: 단어개수, L: 단어길이)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;해결방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단어 배열에 타겟이 있는지 체크 후, 없다면 바로 종료(return 0)해준다&lt;/li&gt;
&lt;li&gt;각 단어를 노드로 보고, 한 글자만 다른 단어를 간선(edge)으로 연결해 그래프를 먼저 만든다&lt;/li&gt;
&lt;li&gt;begin과 한 글자 차이나는 단어들을 시작 노드로 보고 큐에 먼저 넣은 후 BFS를 돌린다&lt;/li&gt;
&lt;li&gt;BFS는 가까운 단어부터 탐색하므로, 타겟 단어에 처음 도달한 시점의 단계 수가 곧 최단 변환 횟수가 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;소스코드&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;사용언어 : java&lt;/p&gt;
&lt;pre id=&quot;code_1673406231868&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public int solution(String begin, String target, String[] words) {        
        int n = words.length;
        List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
        boolean isTargetIn = false;
        
        // target 여부
        for (String w : words) {
            if (w.equals(target)) isTargetIn = true;
        }
        if (!isTargetIn) return 0;
        
        
        // 그래프 생성 (연결 가능한 것들끼리 연결)
        for (int i=0; i&amp;lt;n; i++) {
            List&amp;lt;Integer&amp;gt; row = new ArrayList&amp;lt;&amp;gt;();
            for (int j=0; j&amp;lt;n; j++) {
                if (isOneCharDiff(words[i], words[j])) {
                    row.add(j);
                }
            }
            list.add(row);
        }
        
        
        // BFS
        Queue&amp;lt;Integer&amp;gt; q = new LinkedList&amp;lt;&amp;gt;();
        int[] visited = new int[n];
        
        for (int i=0; i&amp;lt;n; i++) {
            if (isOneCharDiff(begin, words[i])) {
                q.offer(i);
                visited[i] = 1;
            }
        }
        
        while (!q.isEmpty()) {
            int cur = q.poll();
            if (words[cur].equals(target)) return visited[cur];
            
            for (int next : list.get(cur)) {
                if (visited[next] == 0) {
                    visited[next] = visited[cur] + 1;
                    q.offer(next);
                }
            }
        }
        
        return 0;

    }
    
    static boolean isOneCharDiff(String a, String b) {
        int diffCount = 0;

        for (int i = 0; i &amp;lt; a.length(); i++) {
            if (a.charAt(i) != b.charAt(i)) {
                diffCount++;
                if (diffCount &amp;gt; 1) return false; // 2개 이상 다르면 바로 종료
            }
        }

        return diffCount == 1;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;.equals()&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바에서 String은 객체(reference type)이라서, ==는 메모리 주소를 비교하므로, &lt;br /&gt;문자열의 실제 내용이 같은지 비교하고 싶을 땐 equals를 사용한다&lt;/li&gt;
&lt;li&gt;String 뿐만 아니라 모든 객체 타입들 (Integer, Boolean 등)은 값 자체를 비교하고 싶으면 equals를 사용해야한다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760186488865&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String a = new String(&quot;abc&quot;);
String b = new String(&quot;abc&quot;);

System.out.println(a == b);      // false (서로 다른 객체)
System.out.println(a.equals(b)); // true  (내용이 같음)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문자열 리터럴(코드 안에 직접 써놓은 문자)은 String Constant Pool 영역에 저장되 같은 주소를 가진다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760186643064&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String a = &quot;abc&quot;;
String b = &quot;abc&quot;;

System.out.println(a == b); // ✅ true &amp;mdash; 같은 리터럴이므로 같은 주소&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;한줄평&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;너무 오랜만에 그래프 문제를 풀었는데,, 코드를 어떻게 짜야할 지 다 까먹어서 문제다 . ㅜ&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>스터디/알고리즘</category>
      <category>Algorithm</category>
      <category>BFS</category>
      <category>너비우선탐색</category>
      <category>알고리즘</category>
      <category>프로그래머스</category>
      <author>dev-yen</author>
      <guid isPermaLink="true">https://dev-yen.tistory.com/76</guid>
      <comments>https://dev-yen.tistory.com/entry/Algorithm-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%8B%A8%EC%96%B4-%EB%B3%80%ED%99%98#entry76comment</comments>
      <pubDate>Tue, 14 Oct 2025 13:49:42 +0900</pubDate>
    </item>
    <item>
      <title>[Reactive Programming] 마블 다이어그램(Marble Diagram)</title>
      <link>https://dev-yen.tistory.com/entry/Reactive-Programming-%EB%A7%88%EB%B8%94-%EB%8B%A4%EC%9D%B4%EC%96%B4%EA%B7%B8%EB%9E%A8Marble-Diagram</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;609&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1UiQ3/btsQb2NqKAV/ld0vsDmFi0KujkyOPzzrJ1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1UiQ3/btsQb2NqKAV/ld0vsDmFi0KujkyOPzzrJ1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1UiQ3/btsQb2NqKAV/ld0vsDmFi0KujkyOPzzrJ1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1UiQ3%2FbtsQb2NqKAV%2Fld0vsDmFi0KujkyOPzzrJ1%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;186&quot; height=&quot;247&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;609&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;본 포스팅은 스프링으로 시작하는 리액티브 프로그래밍 책을 참고하여 작성하였습니다. ( _ _)&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마블 다이어그램(Marble Diagram) 이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마블 다이어그램은 &lt;b&gt;비동기적인 데이터 흐름을 시간의 흐름에 따라 시각적으로 표시한 다이어그램&lt;/b&gt;을 의미한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C91an/btsQ65PClTF/QoSDrAZ6RkB9hxq5XcAjN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C91an/btsQ65PClTF/QoSDrAZ6RkB9hxq5XcAjN0/img.png&quot; data-alt=&quot;제미니가 그려준 그래프&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C91an/btsQ65PClTF/QoSDrAZ6RkB9hxq5XcAjN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC91an%2FbtsQ65PClTF%2FQoSDrAZ6RkB9hxq5XcAjN0%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;417&quot; height=&quot;417&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;제미니가 그려준 그래프&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그림에서 두 개의 타임라인이 존재하는데, 첫 번째가 Publisher가 데이터를 emit 하는 타임라인이다&lt;/li&gt;
&lt;li&gt;두 번째는 Operator 함수에서 가공 처리되어 출력으로 내보내진 데이터의 타임라인이다&lt;/li&gt;
&lt;li&gt;Marble로 표시된 데이터는 Publisher가 emit하는 데이터를 의미한다. 타임라인은 왼쪽에서 오른쪽으로 시간이 흐르는 것을 의미하기 때문에 가장 왼쪽에 있는 1번 구슬이 시간상으로 가장 먼저 emit된 데이터이다&lt;/li&gt;
&lt;li&gt;중간 박스는 Publisher로부터 전달받은 데이터를 처리하는 Operator 함수이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reactor는 굉장히 많은 수의 Operator를 지원하며, 각각의 Operator마다 해당 Operator를 잘 설명하는 마블 다이어그램을 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;가장 오른쪽의 수직 바가 데이터의 emit이 정상적으로 끝났음을 의미한다. (onComplete Signal)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kK79H/btsQ7vtW6vn/zub8eYju9ghsk64xxmCWK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kK79H/btsQ7vtW6vn/zub8eYju9ghsk64xxmCWK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kK79H/btsQ7vtW6vn/zub8eYju9ghsk64xxmCWK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkK79H%2FbtsQ7vtW6vn%2Fzub8eYju9ghsk64xxmCWK1%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;445&quot; height=&quot;445&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;X표시는 에러가 발생해 데이터 처리가 종료되었음을 의미한다. (onError Signal)&lt;/li&gt;
&lt;/ul&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;마블 다이어그램으로 Publisher 이해하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKhPBW/btsQ7yqHPkg/xrNopxUfatN6A657BkwzC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKhPBW/btsQ7yqHPkg/xrNopxUfatN6A657BkwzC1/img.png&quot; data-alt=&quot;지피티가 열심히 그려준 그래프&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKhPBW/btsQ7yqHPkg/xrNopxUfatN6A657BkwzC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKhPBW%2FbtsQ7yqHPkg%2FxrNopxUfatN6A657BkwzC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;662&quot; height=&quot;441&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;지피티가 열심히 그려준 그래프&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 Reactor의 Publisher 타입 중 하나인 &lt;span style=&quot;color: #f3c000; background-color: #000000;&quot;&gt;&lt;b&gt;Mono&lt;/b&gt;&lt;/span&gt;를 마블 다이어그램으로 표현한 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mono는 단 하나의 데이터를 emit하는 Publisher이기 때문에 그림에서도 하나의 데이터 (3)만 표현한다&lt;/li&gt;
&lt;li&gt;정확히는 0개 또는 1개의 데이터를 emit하는 Publisher이다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760275066872&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example6_1 {
    public static void main(String[] args) {
        Mono.just(&quot;Hello Reactor&quot;)
                .subscribe(System.out::println);
    }
}&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;just()는 한 개 이상의 데이터를 emit 하기 위한 대표적인 Operator로서 2개 이상의 데이터를 파라미터로 전달할 경우, 내부적으로 fromArray()를 이용해 데이터를 emit 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760275162353&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example6_2 {
    public static void main(String[] args) {
        Mono
            .empty()
            .subscribe(
                    none -&amp;gt; System.out.println(&quot;# emitted onNext signal&quot;),
                    error -&amp;gt; {},
                    () -&amp;gt; System.out.println(&quot;# emitted onComplete signal&quot;)
            );
    }
}&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;데이터를 한 건도 emit하지 않는 경우, empty()를 사용해 onComplete Signal을 전송한다&lt;/li&gt;
&lt;li&gt;실행해보면 # emitted onComplete signal이 출력되는데, 이를 통해 emit할 데이터가 없는 경우 바로 onComplete Signal을 전송한다는 걸 알 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;subscrite()의 람다 표현식 파라미터&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 번째 람다 표현식은 Publisher가 onNext Signal을 전송하면 실행&lt;br /&gt;즉, Subscriber가 Publisher로부터 데이터를 전달받기 위해 사용&lt;/li&gt;
&lt;li&gt;두 번째 람다 표현식은 Publisher가 onError Signal을 전송하면 실행&lt;/li&gt;
&lt;li&gt;세 번째 람다 표현식은 Publisher가 onComplete Signal을 전송하면 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&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;blob&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AsaWT/btsQ6K50xo1/qnW2VA9j3nH3OSjTK5BuKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AsaWT/btsQ6K50xo1/qnW2VA9j3nH3OSjTK5BuKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AsaWT/btsQ6K50xo1/qnW2VA9j3nH3OSjTK5BuKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAsaWT%2FbtsQ6K50xo1%2FqnW2VA9j3nH3OSjTK5BuKK%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;612&quot; height=&quot;408&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 Reactor의 Publisher 타입 중 하나인 &lt;span style=&quot;color: #99cefa; background-color: #000000;&quot;&gt;&lt;b&gt;Flux&lt;/b&gt;&lt;/span&gt;를 마블 다이어그램으로 표현한 것이다&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mono와 차이점은 Flux는 emit되는 데이터가 여러 개라는 점이다.&lt;/li&gt;
&lt;li&gt;Flux는 0개 또는 1개 이상의 데이터를 emit할 수 있기 때문에 Mono의 데이터 emit 범위를 포함한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760275901332&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example6_4 {
    public static void main(String[] args) {
        Flux.just(6, 9, 13)
                .map(num -&amp;gt; num % 2)
                .subscribe(System.out::println);
    }
}&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;just()에서 emit하는 세 개의 숫자들을 전달받은 후에 map()에서 2로 나눈 나머지를 Subscriber에 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760276010783&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example6_5 {
    public static void main(String[] args) {
        Flux.fromArray(new Integer[]{3, 6, 7, 9})
                .filter(num -&amp;gt; num &amp;gt; 6)
                .map(num -&amp;gt; num * 2)
                .subscribe(System.out::println);
    }
}&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;데이터 소스로 제공되는 배열 데이터를 처리하기 위해 fromArray()를 사용한다&lt;/li&gt;
&lt;li&gt;전달받은 배열의 원소를 하나씩 차례대로 emit하면 filter()에서 6보다 큰 수만 필터링 한 후, 다시 map()으로 전달한다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760276139304&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example6_6 {
    public static void main(String[] args) {
        Flux&amp;lt;String&amp;gt; flux =
                Mono.justOrEmpty(&quot;Steve&quot;)
                        .concatWith(Mono.justOrEmpty(&quot;Jobs&quot;));
        flux.subscribe(System.out::println);
    }
}&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;두 개의 Mono를 연결해 Flux로 변환&lt;/li&gt;
&lt;li&gt;just()의 경우 파라미터의 값으로 null을 허용하지 않지만 justOrEmpty()는 null을 허용한다
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;justOrEmpty()의 파라미터로 null이 전달되면 내부적으로 empty()를 호출하도록 구현되어 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;concatWith()는 concatWith()를 호출하는 Publisher와 concatWith()의 파라미터로 전달되는 Publisher가 각각 emit하는 데이터들을 하나로 연결해서 새로운 Publisher의 데이터 소스로 만들어준다
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;String의 concat처럼 하나로 이어 붙이는게 아니라, emit할 데이터들을 일렬로 줄 세워서 하나의 소스로 만든 후 차례차례 emit한다고 보면 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjbsmN/btsQ571W9Ww/J2mhjhGHSA4xiElAunDPOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjbsmN/btsQ571W9Ww/J2mhjhGHSA4xiElAunDPOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjbsmN/btsQ571W9Ww/J2mhjhGHSA4xiElAunDPOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjbsmN%2FbtsQ571W9Ww%2FJ2mhjhGHSA4xiElAunDPOK%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;561&quot; height=&quot;374&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div style=&quot;background-color: #ffffff;&quot;&gt;
&lt;div&gt;
&lt;div style=&quot;color: #0d0d0d;&quot;&gt;
&lt;div id=&quot;prompt-textarea&quot; contenteditable=&quot;true&quot; data-virtualkeyboard=&quot;true&quot;&gt;
&lt;p data-placeholder=&quot;추가, 제거 또는 교체할 부분을 설명해 주세요....&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1760276978946&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example6_7 {
    public static void main(String[] args) {
        Flux.concat(
                        Flux.just(&quot;Mercury&quot;, &quot;Venus&quot;, &quot;Earth&quot;),
                        Flux.just(&quot;Mars&quot;, &quot;Jupiter&quot;, &quot;Saturn&quot;),
                        Flux.just(&quot;Uranus&quot;, &quot;Neptune&quot;, &quot;Pluto&quot;))
                .collectList()
                .subscribe(planets -&amp;gt; System.out.println(planets));
    }
}&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;concatWith()의 경우 두 개의 데이터 소스만 연결할 수 있지만, concat()은 여러 개의 데이터 소스를 원하는 만큼 연결할 수 있다&lt;/li&gt;
&lt;li&gt;collectList()는 Upstream Publisher에서 emit하는 데이터를 모아서 List의 원소로 포함시킨 새로운 데이터 소스로 만들어준다&lt;/li&gt;
&lt;li&gt;간단한 문제 ㅎㅎ&amp;nbsp;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Q. concat()에서 리턴하는 Publisher는 Mono일까 Flux일까?
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;A. &lt;span style=&quot;color: #ffffff;&quot;&gt;Flux이다. concat()을 이용해 3개의 Flux가 가지고 있는 아홉 개의 데이터를 연결하기 때문에 concat()의 리턴 값은 Flux일 수밖에 없음!&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Q. collectList()에서 리턴하는 Publisher는 Mono일까 Flux일까?
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;A. &lt;span style=&quot;color: #ffffff;&quot;&gt;Mono이다. collectList()는 여러 개의 데이터를 하나의 List에 원소로 포함시킨다. List에 포함된 원소는 여러 개이지만 List 자체는 하나이기 때문에 한 개의 데이터만 emit할 수 있는 Mono를 리턴한 것.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Q. 최종적으로 출력되는 데이터의 형태는 무엇일까?
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;A. &lt;span style=&quot;color: #ffffff;&quot;&gt;[Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune, Pluto]&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;정답이 List이기 때문에 각각의 데이터를 반복적으로 출력하는 것이 아니라, List 객체를 그대로 출력한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;마무리하며,,,&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마블 다이어그램을 가장한 Flux, Mono에 대한 포스팅이었다. (ㅋㅋ)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 데이터 흐름을 파악하는게 쉽지 않기 때문에 마블 다이어그램이 참 유용한 것 같다!&amp;nbsp;&lt;/p&gt;</description>
      <category>스터디/기타</category>
      <category>Flux</category>
      <category>MarbleDiagram</category>
      <category>Mono</category>
      <category>ReactiveProgramming</category>
      <category>reactor</category>
      <category>마블다이어그램</category>
      <author>dev-yen</author>
      <guid isPermaLink="true">https://dev-yen.tistory.com/77</guid>
      <comments>https://dev-yen.tistory.com/entry/Reactive-Programming-%EB%A7%88%EB%B8%94-%EB%8B%A4%EC%9D%B4%EC%96%B4%EA%B7%B8%EB%9E%A8Marble-Diagram#entry77comment</comments>
      <pubDate>Sun, 12 Oct 2025 23:14:19 +0900</pubDate>
    </item>
    <item>
      <title>[Algorithm] 프로그래머스 - 숫자 게임</title>
      <link>https://dev-yen.tistory.com/entry/Algorithm-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%88%AB%EC%9E%90-%EA%B2%8C%EC%9E%84</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12987&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[프로그래머스] 숫자 게임&lt;/a&gt;&lt;br /&gt;(Summer/Winter Coding(~2018))&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;사용 알고리즘&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;- 투 포인터 (two pointer) / Greedy&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;- 시간복잡도 O(N logN)&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;해결방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A, B를 모두 오름차순 정렬&lt;/li&gt;
&lt;li&gt;A의 각 원소에 대해, B에서 A보다 큰 숫자를 찾아 이긴 횟수를 증가시킨다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;소스코드&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;사용언어 : java&lt;/p&gt;
&lt;pre id=&quot;code_1673406231868&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public int solution(int[] A, int[] B) {
        int answer = 0;
        
        // A, B 오름차순 정렬
        Arrays.sort(A);
        Arrays.sort(B);
        
        
        int j = 0; // B index
        for (int a : A) {
            while (j &amp;lt; B.length &amp;amp;&amp;amp; B[j] &amp;lt;= a) {
                j++;
            }
            
            if (j &amp;gt;= B.length) break;
            
            answer++;
            j++;
        }
        
        return answer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;한줄평&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;처음엔 B의 인덱스를 조절할 생각을 못하고, B자체를 큐로 변환해, 각 a에 대해 큰 값이 나올대 까지 poll하는 방식으로 구현했는데, (물론 이렇게 해도 정답이었다)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;지피티 선생님이 이 문제는 투포인터를 활용하는 문제라고 정정해주셔서 다시 풀었다. (ㅋㅋ)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>스터디/알고리즘</category>
      <category>Algorithm</category>
      <category>greedy</category>
      <category>알고리즘</category>
      <category>투포인터</category>
      <category>프로그래머스</category>
      <author>dev-yen</author>
      <guid isPermaLink="true">https://dev-yen.tistory.com/75</guid>
      <comments>https://dev-yen.tistory.com/entry/Algorithm-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%88%AB%EC%9E%90-%EA%B2%8C%EC%9E%84#entry75comment</comments>
      <pubDate>Sat, 11 Oct 2025 13:09:45 +0900</pubDate>
    </item>
    <item>
      <title>[Algorithm] 프로그래머스 - 야근 지수</title>
      <link>https://dev-yen.tistory.com/entry/Algorithm-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%95%BC%EA%B7%BC-%EC%A7%80%EC%88%98</link>
      <description>&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12927&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[프로그래머스] 야근 지수&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;사용 알고리즘&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;- Heap / Greedy&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;- 우선순위 큐 (PriorityQueue)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;- 시간복잡도 O(n logN) (n: 남은 시간, N: 작업 수)&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;해결방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주어진 works를 우선순위 큐에 작업량이 많은 순으로(내림차순) 넣는다&lt;/li&gt;
&lt;li&gt;남은 작업시간 n 동안 반복문을 돌면서, 가장 많은 작업량을 가진 일을 찾아서(조회) 1씩 줄이고 다시 큐에 넣는다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 동작을 효율적으로 처리하기 위해 힙 구조를 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;소스코드&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;사용언어 : java&lt;/p&gt;
&lt;pre id=&quot;code_1673406231868&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public long solution(int n, int[] works) {
        long answer = 0;

        PriorityQueue&amp;lt;Integer&amp;gt; pq = new PriorityQueue&amp;lt;&amp;gt;(works.length, Collections.reverseOrder());
        for (int w : works) pq.offer(w);
        
        while (n-- &amp;gt; 0 &amp;amp;&amp;amp; !pq.isEmpty()) {
            int maxWork = pq.poll();
            if (maxWork &amp;lt;= 0) break;
            pq.offer(--maxWork);
        }
        
        for (int w : pq) {
            answer += (long) w*w;
        }
        
        return answer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;PriorityQueue&amp;lt;Integer&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본은 Min Heap (오름차순)&lt;/li&gt;
&lt;li&gt;가장 큰 값을 꺼내기 위해 Collections.reverseOrder()를 사용해 Max Heap으로 만든다&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 29.7674%;&quot;&gt;&lt;b&gt;방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.2558%;&quot;&gt;&lt;b&gt;가장 큰 값 조회&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.814%;&quot;&gt;&lt;b&gt;시간복잡도&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.1628%;&quot;&gt;&lt;b&gt;desc&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 29.7674%;&quot;&gt;&lt;b&gt;정렬된 배열 (List, Array등) 유지&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.2558%;&quot;&gt;O(1) 조회 가능&lt;/td&gt;
&lt;td style=&quot;width: 25.814%;&quot;&gt;❌ 하지만 삽입 시 O(N)&lt;/td&gt;
&lt;td style=&quot;width: 21.1628%;&quot;&gt;매번 삽입/삭제 느림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 29.7674%;&quot;&gt;&lt;b&gt;PriorityQueue(Heap)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.2558%;&quot;&gt;O(log N)&lt;/td&gt;
&lt;td style=&quot;width: 25.814%;&quot;&gt;✅ 삽입/삭제 모두 log N&lt;/td&gt;
&lt;td style=&quot;width: 21.1628%;&quot;&gt;효율적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 29.7674%;&quot;&gt;&lt;b&gt;매번 정렬 (sort)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.2558%;&quot;&gt;O(N log N) * n&lt;/td&gt;
&lt;td style=&quot;width: 25.814%;&quot;&gt;❌ 매우 비효율적&lt;/td&gt;
&lt;td style=&quot;width: 21.1628%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;(힙 정리)&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;자료구조 형태&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;완전 이진 트리 (Complete Binary Tree)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;정렬 규칙&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;부모 노드 &amp;ge; 자식 노드 (Max Heap) / &amp;le; 자식 노드 (Min Heap)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;대표 구현&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;PriorityQueue (Java 표준 라이브러리에서 Heap 기반으로 구현됨)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;주요 기능&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;가장 큰(작은) 값&amp;rdquo;을 &lt;b&gt;O(1)&lt;/b&gt; 로 조회, &lt;b&gt;O(log N)&lt;/b&gt; 으로 삽입/삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내부 구조&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전 이진 트리(complete binary tree) 구조 (왼쪽부터 차례로 채워짐)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) &lt;b&gt;Max Heap&lt;/b&gt; (부모 &amp;ge; 자식) 구조:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;          9
       /     \
      7       8
     / \     / \
    3   5   2   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;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;index:  0  1  2  3  4  5  6
value: [9, 7, 8, 3, 5, 2, 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;부모-자식 인덱스 관계&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;노드&lt;/td&gt;
&lt;td&gt;왼쪽 자식&lt;/td&gt;
&lt;td&gt;오른쪽 자식&lt;/td&gt;
&lt;td&gt;부모&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;2*i + 1&lt;/td&gt;
&lt;td&gt;2*i + 2&lt;/td&gt;
&lt;td&gt;(i-1)/2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;삽입(offer) 시 동작 &amp;mdash; &amp;ldquo;위로 올리기 (heapify up)&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 원소를 마지막에 추가한 후, 부모와 비교해서 &lt;b&gt;힙 조건(부모 &amp;ge; 자식)&lt;/b&gt; 을 만족할 때까지 &lt;b&gt;올라간다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;기존 힙: [9, 7, 8]
삽입: 10

배열: [9, 7, 8, 10]
&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;이제 10은 부모(7)보다 크므로 자리 교환&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[9, 10, 8, 7]
&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;rarr; 10은 부모(9)보다 크므로 한 번 더 교환&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[10, 9, 8, 7]
&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;&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;삽입 복잡도: &lt;/span&gt;&lt;b&gt;O(log N)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(트리의 높이만큼만 올라가면 됨)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;삭제(poll) 시 동작 &amp;mdash; &amp;ldquo;아래로 내리기 (heapify down)&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;poll() 은 항상 &lt;b&gt;루트(가장 큰 값)&lt;/b&gt; 을 꺼낸다&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[10, 9, 8, 7]  &amp;rarr; poll() &amp;rarr; 루트(10) 삭제
&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;이때, 마지막 노드(7)를 루트로 올려놓고 &amp;ldquo;힙 재정렬&amp;rdquo; 시작&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[7, 9, 8]
&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;부모(7) &amp;lt; 자식(9, 8) &amp;rarr; 더 큰 자식(9)과 교환&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[9, 7, 8]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙 속성 복구 완료&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;삭제 복잡도: &lt;/span&gt;&lt;b&gt;O(log N)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(트리의 높이만큼만 내려가면 됨)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정렬 기준에 따른 힙 종류&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Min Heap&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;부모 &amp;le; 자식&lt;/td&gt;
&lt;td&gt;new PriorityQueue&amp;lt;&amp;gt;() (기본)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Max Heap&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;부모 &amp;ge; 자식&lt;/td&gt;
&lt;td&gt;new PriorityQueue&amp;lt;&amp;gt;(Collections.reverseOrder())&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java의 PriorityQueue는 내부적으로 &lt;b&gt;배열 기반의 힙 구조&lt;/b&gt;로 동작&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public class PriorityQueue&amp;lt;E&amp;gt; extends AbstractQueue&amp;lt;E&amp;gt; {
    private transient Object[] queue; // 힙을 배열로 저장
    private int size = 0;             // 현재 원소 개수
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;삽입 시 siftUp()&lt;/li&gt;
&lt;li&gt;삭제 시 siftDown()&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 메서드로 heapify 과정을 수행&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시간 복잡도&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;삽입 (offer)&lt;/td&gt;
&lt;td&gt;O(log N)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;삭제 (poll)&lt;/td&gt;
&lt;td&gt;O(log N)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최대값/최소값 조회 (peek)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;한줄평&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;첨에 그냥 List로 풀었다가 효율성 테스트에서 실패떠서, 우선순위큐로 바꿨더니 해결되었다. . .&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그냥 느끼기엔 둘 다 내부적이든 외부적이든 정렬하는 거니까 똑같은 시간복잡도를 가질 것 같은데, 우선순위큐는 완전 이진 트리 형태라 삽입 삭제가 O(log N)만에 동작한다는 게 큰 장점인 것 같다 : ) . . .&lt;/p&gt;</description>
      <category>스터디/알고리즘</category>
      <category>Algorithm</category>
      <category>greedy</category>
      <category>Heap</category>
      <category>priorityqueue</category>
      <category>알고리즘</category>
      <category>우선순위큐</category>
      <category>프로그래머스</category>
      <author>dev-yen</author>
      <guid isPermaLink="true">https://dev-yen.tistory.com/74</guid>
      <comments>https://dev-yen.tistory.com/entry/Algorithm-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%95%BC%EA%B7%BC-%EC%A7%80%EC%88%98#entry74comment</comments>
      <pubDate>Fri, 10 Oct 2025 18:56:02 +0900</pubDate>
    </item>
    <item>
      <title>@TransactionalEventListener(AFTER_COMMIT)에서 INSERT 쿼리가 증발한 이유: REQUIRED와 REQUIRES_NEW의 결정적 차이</title>
      <link>https://dev-yen.tistory.com/entry/TransactionalEventListenerAFTERCOMMIT%EC%97%90%EC%84%9C-INSERT-%EC%BF%BC%EB%A6%AC%EA%B0%80-%EC%A6%9D%EB%B0%9C%ED%95%9C-%EC%9D%B4%EC%9C%A0-REQUIRED%EC%99%80-REQUIRESNEW%EC%9D%98-%EA%B2%B0%EC%A0%95%EC%A0%81-%EC%B0%A8%EC%9D%B4</link>
      <description>&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://dev-yen.tistory.com/68&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 포스팅&lt;/a&gt;의 연장선이다 ㅎㅋㅋ), 아래와 같은 이벤트 리스너 코드를 작성했는데 이상한 현상을 발견했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
@Component
public class MessageEventHandler {

    private final MessageRepository messageRepository;

    @Transactional
    @TransactionalEventListener(
        phase = TransactionPhase.AFTER_COMMIT,
        fallbackExecution = true
    )
    public void handleCreateTemplateMessageEvent(MessageByTemplateRequest request) {
        log.info(&quot;[handleCreateTemplateMessageEvent] event received: {}&quot;, request);
        messageRepository.save(...);  // INSERT 쿼리가 안 나감!
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그는 정상적으로 출력됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조회 쿼리는 찍힘 (상위 트랜잭션에서 발생한)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;INSERT 쿼리는 아무리 기다려도 안 나감 (실제로 저장도 안됨 ㅠㅠ)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;원인) 트랜잭션 전파(propagation) 때문, ,&amp;nbsp; ,&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;REQUIRED (&lt;b&gt;기본값&lt;/b&gt;)&lt;/td&gt;
&lt;td&gt;기존 트랜잭션이 있으면 참여, 없으면 새로 생성&lt;/td&gt;
&lt;td&gt;일반적인 서비스 로직&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REQUIRES_NEW&lt;/td&gt;
&lt;td&gt;&lt;b&gt;항상 새 트랜잭션 생성&lt;/b&gt;, 기존 트랜잭션은 일시 중단됨&lt;/td&gt;
&lt;td&gt;이벤트 리스너, 로깅, 감사 로그&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NESTED&lt;/td&gt;
&lt;td&gt;기존 트랜잭션 안에 &lt;b&gt;Savepoint 생성&lt;/b&gt;, 내부 실패 시 롤백 가능&lt;/td&gt;
&lt;td&gt;트랜잭션 내 일부만 롤백하고 싶을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SUPPORTS&lt;/td&gt;
&lt;td&gt;트랜잭션이 있으면 참여, 없으면 트랜잭션 없이 실행&lt;/td&gt;
&lt;td&gt;단순 조회 메서드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NOT_SUPPORTED&lt;/td&gt;
&lt;td&gt;트랜잭션이 있으면 &lt;b&gt;중단하고 비트랜잭션 방식&lt;/b&gt;으로 실행&lt;/td&gt;
&lt;td&gt;외부 시스템 호출 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MANDATORY&lt;/td&gt;
&lt;td&gt;&lt;b&gt;반드시 기존 트랜잭션이 있어야 함&lt;/b&gt;, 없으면 예외 발생&lt;/td&gt;
&lt;td&gt;트랜잭션 환경에서만 호출되어야 할 로직&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NEVER&lt;/td&gt;
&lt;td&gt;트랜잭션이 있으면 예외 발생&lt;/td&gt;
&lt;td&gt;트랜잭션 없이 실행되어야 할 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실무에서 자주 쓰이는 옵션&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. REQUIRED (기본값)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 일반적인 옵션&lt;/li&gt;
&lt;li&gt;기존 트랜잭션에 합류 &amp;rarr; 같은 트랜잭션 범위에서 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Transactional // REQUIRED가 기본
public void orderService() {
    userRepository.save(...); // 같은 트랜잭션으로 묶임
    paymentRepository.save(...);
}
&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;2. REQUIRES_NEW&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 트랜잭션과 &lt;b&gt;완전히 분리된 새로운 트랜잭션&lt;/b&gt;을 시작&lt;/li&gt;
&lt;li&gt;호출 메서드에서 예외가 발생해도 &lt;b&gt;기존 트랜잭션에 영향 없음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;알림, 로깅, 외부 통신, @TransactionalEventListener(AFTER_COMMIT) 같은 곳에 자주 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditLog(...) {
    auditLogRepository.save(...); // 독립 트랜잭션
}
&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;3. NESTED&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부모 트랜잭션 안에서 &lt;b&gt;Savepoint&lt;/b&gt;를 만들어 두고, &lt;u&gt;내부 트랜잭션만 롤백 가능&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;DB가 savepoint를 지원해야 함 (예: MySQL은 지원)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.NESTED)
public void processWithRetry() {
    try {
        riskyOperation(); // 실패해도 전체 롤백 안함
    } catch (Exception e) {
        // nested 트랜잭션만 롤백
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주의사항&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;REQUIRES_NEW는 &lt;b&gt;별도의 트랜잭션&lt;/b&gt;이므로, 기존 트랜잭션에서 롤백되어도 &lt;b&gt;영향을 받지 않음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;NESTED는 마치 트랜잭션 내부에서 &lt;b&gt;undo checkpoint&lt;/b&gt;를 두는 것처럼 동작함&lt;/li&gt;
&lt;li&gt;@Async는 아예 다른 스레드에서 동작하므로, 트랜잭션 전파가 되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional 기본 전파 옵션은 &lt;b&gt;REQUIRED&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;rarr;&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;그런데 TransactionalEventListener의 AFTER_COMMIT은?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;rarr;&amp;nbsp;이미 기존 트랜잭션이 &lt;/span&gt;&lt;b&gt;끝난 후&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;에 호출됨&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 아래 상황이 되는 것&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;@Transactional 메서드 &amp;rarr; 비즈니스 로직 수행 + 커밋&lt;/li&gt;
&lt;li&gt;커밋 후 @TransactionalEventListener(AFTER_COMMIT) 발동&lt;br /&gt;&amp;gt;&lt;b&gt; TransactionSynchronizationManager.isSynchronizationActive() = false &lt;br /&gt;&lt;/b&gt;&amp;gt; 현재 스레드에 활성화된 트랜잭션 동기화 컨텍스트가 없음을 의미&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactional(REQUIRED) 동작 불가 (AOP 프록시 무력화)&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;결국 save()는 영속성 컨텍스트는 있으나 &lt;b&gt;flush/commit되지 않음&lt;/b&gt; &amp;rarr; &lt;b&gt;INSERT 누락&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;b&gt;@Transactional(REQUIRED)는 왜 실패?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@TransactionalEventListener는 트랜잭션 종료 후 &lt;b&gt;TransactionSynchronizationManager의 콜백&lt;/b&gt;을 통해 리스너 메서드를 &lt;b&gt;직접 실행&lt;/b&gt;한다. 이 방식은 @Transactional을 처리하는 &lt;b&gt;Spring AOP 프록시 레이어를 우회&lt;/b&gt;하게 되는데,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 REQUIRED에 명시된 &lt;b&gt;&quot;없으면 새로 생성&quot;&lt;/b&gt; 로직(AOP가 담당)이 &lt;b&gt;실행될 기회조차 얻지 못하고&lt;/b&gt;, 리스너 내부는 트랜잭션 없는(&lt;b&gt;비트랜잭션&lt;/b&gt;) 환경으로 남게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(EntityManager 메서드 정리)&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;persist(Object entity)&lt;/td&gt;
&lt;td&gt;엔티티를 &lt;b&gt;영속 상태&lt;/b&gt;로 만듦 (INSERT 예정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;merge(Object entity)&lt;/td&gt;
&lt;td&gt;준영속 상태나 새로운 객체를 &lt;b&gt;병합&lt;/b&gt;하여 영속 상태로 만듦&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;remove(Object entity)&lt;/td&gt;
&lt;td&gt;영속 상태의 엔티티를 삭제 (DELETE 예정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;find(Class&amp;lt;T&amp;gt; clazz, ID)&lt;/td&gt;
&lt;td&gt;PK 기준으로 DB에서 엔티티를 &lt;b&gt;조회&lt;/b&gt;함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getReference(Class&amp;lt;T&amp;gt;, ID)&lt;/td&gt;
&lt;td&gt;프록시 객체(Lazy)로 조회함 (실제 조회는 필요 시)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flush()&lt;/td&gt;
&lt;td&gt;영속성 컨텍스트의 변경 내용을 &lt;b&gt;DB에 즉시 반영&lt;/b&gt; (SQL 실행)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clear()&lt;/td&gt;
&lt;td&gt;영속성 컨텍스트를 &lt;b&gt;초기화&lt;/b&gt; (모든 엔티티 준영속화)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;detach(Object entity)&lt;/td&gt;
&lt;td&gt;특정 엔티티를 &lt;b&gt;영속성 컨텍스트에서 분리&lt;/b&gt; (준영속 상태)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;contains(Object entity)&lt;/td&gt;
&lt;td&gt;해당 엔티티가 &lt;b&gt;영속성 컨텍스트에 존재하는지&lt;/b&gt; 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;close()&lt;/td&gt;
&lt;td&gt;EntityManager 종료 (스프링에서는 직접 호출 X)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;createQuery(String jpql)&lt;/td&gt;
&lt;td&gt;JPQL 쿼리를 실행할 수 있는 Query 객체 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;각 메서드 상세 설명 및 예시&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. persist()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트에 엔티티를 등록 &amp;rarr; 트랜잭션 커밋 시 INSERT 실행&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;Member member = new Member(&quot;micky&quot;);
em.persist(member);  // INSERT 예정
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. merge()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;준영속 상태의 엔티티를 다시 영속성 컨텍스트에 병합&lt;/p&gt;
&lt;pre class=&quot;protobuf&quot;&gt;&lt;code&gt;Member detachedMember = ...; // 준영속 상태
Member merged = em.merge(detachedMember); // 병합 후 영속 상태
&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;3. remove()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속 상태의 엔티티를 삭제 예약&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;Member member = em.find(Member.class, 1L);
em.remove(member); // DELETE 예정
&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;4. find() vs getReference()&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Member m1 = em.find(Member.class, 1L); // 즉시 조회
Member m2 = em.getReference(Member.class, 1L); // 프록시 리턴 (Lazy)
&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;5. flush()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에 쿼리를 &lt;b&gt;즉시 반영&lt;/b&gt;하지만, 트랜잭션은 아직 끝나지 않음&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;em.persist(entity);
em.flush();  // INSERT 쿼리 즉시 실행
&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;6. clear()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 엔티티를 &lt;b&gt;영속성 컨텍스트에서 분리&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;em.clear();  // 1차 캐시 초기화
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. detach()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 엔티티만 영속성 컨텍스트에서 분리&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;em.detach(member);  // 준영속 상태가 됨
&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;8. contains()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 객체가 영속성 컨텍스트에 관리되고 있는지 확인&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;em.contains(member);  // true or false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;flush vs clear vs detach 차이&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;flush()&lt;/td&gt;
&lt;td&gt;DB에 SQL 즉시 반영&lt;/td&gt;
&lt;td&gt;쿼리 실행되지만 트랜잭션은 유지됨&lt;/td&gt;
&lt;td&gt;강제로 쿼리 날려야 할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clear()&lt;/td&gt;
&lt;td&gt;전체 detach&lt;/td&gt;
&lt;td&gt;영속성 컨텍스트 초기화&lt;/td&gt;
&lt;td&gt;강제 초기화 필요 시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;detach()&lt;/td&gt;
&lt;td&gt;단일 엔티티 detach&lt;/td&gt;
&lt;td&gt;해당 엔티티만 분리&lt;/td&gt;
&lt;td&gt;특정 객체 변경 막고 싶을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;결국 save()는 영속성 컨텍스트는 있으나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;flush/commit되지 않음&lt;/b&gt;&lt;span&gt; ? &amp;lt; 이게 무슨말인지 알아보자&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트 생명주기 (JpaRepository 기준)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비영속 (new/transient)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아직 EntityManager에 의해 관리되지 않음 (DB에도 없음)&lt;/li&gt;
&lt;li&gt;new User(&quot;micky&quot;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;영속 (managed)&lt;/b&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EntityManager가 관리 중. 트랜잭션 커밋 시 DB 반영&lt;/li&gt;
&lt;li&gt;userRepository.save(user) &amp;rarr; 내부에서 em.persist(user)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;준영속 (detached)&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;더 이상 영속성 컨텍스트에서 관리되지 않음&lt;/li&gt;
&lt;li&gt;em.detach(user) 또는 em.clear()&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt;삭제 (removed)&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;삭제 상태로 표시됨. 커밋 시 DELETE&lt;/li&gt;
&lt;li&gt;userRepository.delete(user) &amp;rarr; 내부에서 em.remove(user)&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;pre class=&quot;pf&quot;&gt;&lt;code&gt;userRepository.save(user);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;save()때 발생하는 동작&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;SimpleJpaRepository 구현체가 동작&lt;/li&gt;
&lt;li&gt;내부적으로 EntityManager의 persist() 또는 merge() 호출&lt;/li&gt;
&lt;li&gt;현재 트랜잭션이 있으면 해당 &lt;b&gt;영속성 컨텍스트&lt;/b&gt;에 user 객체 등록 (Managed 상태)&lt;/li&gt;
&lt;li&gt;트랜잭션이 커밋될 때 flush() &amp;rarr; DB에 INSERT 쿼리 전송 &amp;rarr; commit()&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// 대략적인 구조
@Override
@Transactional
public &amp;lt;S extends T&amp;gt; S save(S entity) {
    if (isNew(entity)) {
        em.persist(entity); // 영속화
        return entity;
    } else {
        return em.merge(entity); // 병합
    }
}
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장 로직 생명주기 흐름 예시&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Transactional
public void createUser() {
    User user = new User(&quot;micky&quot;); // 비영속
    userRepository.save(user);     // 영속 상태로 전환
    // 아직 DB 반영은 안 됨 &amp;rarr; 영속성 컨텍스트에만 있음
    // flush() 또는 트랜잭션 커밋 시 DB INSERT 실행
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.2326%;&quot;&gt;new User()&lt;/td&gt;
&lt;td style=&quot;width: 21.6279%;&quot;&gt;비영속&lt;/td&gt;
&lt;td style=&quot;width: 53.1395%;&quot;&gt;메모리에만 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.2326%;&quot;&gt;save() 호출&lt;/td&gt;
&lt;td style=&quot;width: 21.6279%;&quot;&gt;영속&lt;/td&gt;
&lt;td style=&quot;width: 53.1395%;&quot;&gt;영속성 컨텍스트에 등록됨 (DB X)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.2326%;&quot;&gt;flush or commit&lt;/td&gt;
&lt;td style=&quot;width: 21.6279%;&quot;&gt;DB 반영&lt;/td&gt;
&lt;td style=&quot;width: 53.1395%;&quot;&gt;SQL INSERT 실행됨&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 밖에서는 save()만 호출해도 바로 DB 반영됨 (flush 발생)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JpaRepository의 save() 메서드 자체도 @Transactional이 적용된 메서드이므로&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션이 없으면 영속성 컨텍스트 생명주기는 짧아지고&lt;/b&gt;, 의미도 달라짐 &amp;rarr; 가능한 한 트랜잭션 안에서 작업&lt;/li&gt;
&lt;li&gt;save(), find(), delete()는 모두 EntityManager 기반이므로, 영속성 컨텍스트에 반영됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Flush가 일어나는 시점 (자동 or 수동)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(위에서 계속 얘기했지만ㅎㅎ) JPA는 EntityManager.persist()나 save()를 호출해도 &lt;b&gt;바로 INSERT 쿼리를 날리지 않는다.&lt;/b&gt;&lt;br /&gt;&amp;rarr; 대신 엔티티를 &lt;b&gt;영속성 컨텍스트(Persistence Context)&lt;/b&gt; 에 저장해두고, &lt;b&gt;flush() 시점&lt;/b&gt;에 쿼리를 날린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, flush는 &lt;b&gt;영속성 컨텍스트의 변경 내용을 DB에 반영(동기화)&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;1. 트랜잭션 커밋 직전&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; @Transactional 메서드가 끝나며 &lt;b&gt;정상 커밋&lt;/b&gt;되기 직전, 자동으로 flush 발생 (가장 일반적이고 실무에서 자주 보는 케이스)&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;2. JPQL, Criteria Query, Native Query 실행 전&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 쿼리 실행 전에 &lt;b&gt;dirty checking&lt;/b&gt;된 변경사항을 DB에 반영하기 위해 자동 flush 발생&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;memberRepository.save(member); // 아직 쿼리 안 나감
List&amp;lt;Member&amp;gt; members = memberRepository.findAll(); // flush 발생 &amp;rarr; SELECT 전에 INSERT 됨
&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;3. EntityManager.flush() 명시적으로 호출 시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 개발자가 직접 flush 강제 수행 가능&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;entityManager.flush(); // 강제 flush
&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;4. FlushModeType이 AUTO일 경우 (기본값)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 위 1~3 조건에 따라 자동 flush&lt;br /&gt;&amp;rarr; 필요 시 FlushModeType.COMMIT으로 변경 가능 (성능 최적화 시)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의: flush는 &quot;DB 반영&quot;이지 &quot;commit&quot;이 아니다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flush는 &lt;b&gt;SQL 실행까지만&lt;/b&gt; 수행하고, &lt;b&gt;DB에 실제 반영은 트랜잭션 커밋 시점&lt;/b&gt;에 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, flush만 하면 DB에 INSERT 쿼리는 날아가지만, 트랜잭션이 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;롤백&lt;/span&gt;&lt;/b&gt;되면 &amp;rarr; 이 INSERT도 &lt;b&gt;무효 처리됨&lt;/b&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;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. @Transactional(propagation = &lt;b&gt;REQUIRES_NEW&lt;/b&gt;)로 트랜잭션 새로 시작&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(
    phase = TransactionPhase.AFTER_COMMIT,
    fallbackExecution = true
)
public void handleCreateTemplateMessageEvent(MessageByTemplateRequest request) {
    messageRepository.save(...);  // 이제 INSERT 쿼리 나감
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;REQUIRES_NEW는 &lt;b&gt;무조건 새 트랜잭션을 시작&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;기존 트랜잭션이 끝난 이후(AFTER_COMMIT)에도 안전하게 트랜잭션 생성 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;REQUIRES_NEW는 왜.. 성공하는걸까..?&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional(REQUIRES_NEW)는 &quot;호출자의 트랜잭션 상태와 무관하게 &lt;b&gt;무조건&lt;/b&gt; 새로운 독립적인 트랜잭션을 시작하라&quot;는 가장 강력한 요구를 담고 있다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring의 트랜잭션 처리 메커니즘은 REQUIRES_NEW의 &lt;b&gt;독립성&lt;/b&gt;과 &lt;b&gt;강제성&lt;/b&gt;을 보장하기 위해, 트랜잭션 동기화(Synchronization) 상태에 덜 의존하고 &lt;b&gt;독립적인 트랜잭션 생성 경로&lt;/b&gt;를 따르도록 설계되어 있다.&lt;/li&gt;
&lt;li&gt;비록 AFTER_COMMIT 콜백으로 호출되더라도, Spring의 이벤트 처리 로직은 REQUIRES_NEW가 감지되면 &lt;b&gt;AOP 프록시를 통해&lt;/b&gt; &lt;b&gt;새로운 트랜잭션 경계를 강제로 설정&lt;/b&gt;하는 방식으로 작동&lt;/li&gt;
&lt;li&gt;이 새로운 트랜잭션 내에서 messageRepository.save()가 실행되고, 리스너 메서드가 종료되면 &lt;b&gt;독립적으로 commit/flush가 발생&lt;/b&gt;하여 데이터가 DB에 정상적으로 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 트랜잭션 필요 없으면 아예 @Transactional 제거&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순 로그만 출력하거나 외부 요청 정도만 한다면 트랜잭션 불필요&lt;/li&gt;
&lt;li&gt;오히려 @Transactional이 쓸데없는 기대를 만들 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;fallbackExecution = true는 ?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 옵션은 &lt;b&gt;트랜잭션이 없는 상황에서도 이벤트를 실행&lt;/b&gt;할 수 있게 해준다.&lt;/li&gt;
&lt;li&gt;테스트 코드처럼 @Transactional이 없는 컨텍스트에서 이벤트를 발행할 때 유용함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실무에서는 거의 항상 AFTER_COMMIT을 트랜잭션 내부에서 발행하므로&lt;br /&gt;&lt;b&gt;명시적으로 REQUIRES_NEW를 지정하는 게 더 중요하다&lt;/b&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;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리된 전체 코드 예시&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
@Slf4j
public class MessageEventHandler {

    private final MessageRepository messageRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @TransactionalEventListener(
        phase = TransactionPhase.AFTER_COMMIT,
        fallbackExecution = true
    )
    public void handleCreateTemplateMessageEvent(MessageByTemplateRequest request) {
        log.info(&quot;[handleCreateTemplateMessageEvent] saving message: {}&quot;, request);
        messageRepository.save(
            Message.builder()
                .userId(request.getUserId())
                .title(&quot;템플릿 메시지&quot;)
                .content(request.getContent())
                .build()
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마무리하며...&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 기반 동작하는 것들은 정말 어려운 것 같다. 생각한대로 동작을 안한달까..ㅋㅋㅋ&lt;br /&gt;AOP 기반 애너테이션은 트랜잭션의 시점과 컨텍스트에 따라 예민하게 반응하니,&lt;br /&gt;꼼꼼하게 트랜잭션 전파 속성과 생명주기를 고려해야함을 느꼈다 . .&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>스터디/스프링</category>
      <category>AOP</category>
      <category>entitymanager</category>
      <category>JPA</category>
      <category>propagation</category>
      <category>TranactionalEventListener</category>
      <category>Transaction</category>
      <category>영속성컨텍스트</category>
      <author>dev-yen</author>
      <guid isPermaLink="true">https://dev-yen.tistory.com/73</guid>
      <comments>https://dev-yen.tistory.com/entry/TransactionalEventListenerAFTERCOMMIT%EC%97%90%EC%84%9C-INSERT-%EC%BF%BC%EB%A6%AC%EA%B0%80-%EC%A6%9D%EB%B0%9C%ED%95%9C-%EC%9D%B4%EC%9C%A0-REQUIRED%EC%99%80-REQUIRESNEW%EC%9D%98-%EA%B2%B0%EC%A0%95%EC%A0%81-%EC%B0%A8%EC%9D%B4#entry73comment</comments>
      <pubDate>Thu, 9 Oct 2025 01:46:49 +0900</pubDate>
    </item>
    <item>
      <title>Querydsl transform() 사용 시 발생할 수 있는 커넥션 누수 이슈</title>
      <link>https://dev-yen.tistory.com/entry/Querydsl-transform-%EC%82%AC%EC%9A%A9-%EC%8B%9C-%EB%B0%9C%EC%83%9D%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EC%BB%A4%EB%84%A5%EC%85%98-%EB%88%84%EC%88%98-%EC%9D%B4%EC%8A%88</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;조회쿼리 작성시 연관관계를 갖는 경우, transform을 처음에 사용하려했었는데, &lt;br /&gt;이 때 발견한 커넥션 leack 문제에 대해 포스팅하고자 한다.&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 data-ke-size=&quot;size20&quot;&gt;Querydsl transform()이란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;transform()은 Querydsl에서 &lt;b&gt;그룹핑된 데이터를 원하는 구조로 변환&lt;/b&gt;할 때 사용하는 메서드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 fetch()는 단순히 List&amp;lt;Tuple&amp;gt; 형태로 데이터를 반환하지만,&lt;br /&gt;transform()은 &lt;b&gt;결과를 Map이나 DTO 등으로 그룹핑 및 가공&lt;/b&gt;해서 반환할 수 있게 해준다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;groupBy().transform() 형태로 많이 사용됨&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 아래처럼 1:N 관계의 데이터를 조회할 때&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 작가가 여러 권의 책을 갖고 있을 때&lt;/li&gt;
&lt;li&gt;하나의 주문에 여러 주문상품이 연결되어 있을 때&lt;/li&gt;
&lt;li&gt;하나의 게시글에 여러 댓글이 달릴 때 등등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity
public class Author {
    @Id Long id;
    String name;

    @OneToMany(mappedBy = &quot;author&quot;)
    List&amp;lt;Book&amp;gt; books;
}

@Entity
public class Book {
    @Id Long id;
    String title;

    @ManyToOne
    Author author;
}
&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;transform() 사용 예&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;Map&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; result = queryFactory
    .select(author.name, book.title)
    .from(book)
    .join(book.author, author)
    .transform(
        groupBy(author.name)
        .as(list(book.title))
    );
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;{
    &quot;한강&quot;: [&quot;소년이 온다&quot;, &quot;채식주의자&quot;],
    &quot;정유정&quot;: [&quot;7년의 밤&quot;, &quot;완전한 행복&quot;],
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO로 변환&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;List&amp;lt;AuthorWithBooksDto&amp;gt; result = queryFactory
    .select(author.id, author.name, book.title)
    .from(book)
    .join(book.author, author)
    .transform(
        groupBy(author.id)
        .list(Projections.constructor(
            AuthorWithBooksDto.class,
            author.id,
            author.name,
            list(book.title)
        ))
    );
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class AuthorWithBooksDto {
    Long authorId;
    String authorName;
    List&amp;lt;String&amp;gt; bookTitles;

    public AuthorWithBooksDto(Long authorId, String authorName, List&amp;lt;String&amp;gt; bookTitles) {
        this.authorId = authorId;
        this.authorName = authorName;
        this.bookTitles = bookTitles;
    }
}
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Connection leack 발생&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;transform()은 내부적으로&lt;b&gt;&amp;nbsp;ResultSet을 lazy하게 순회&lt;/b&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;== 프록시처럼 순차적으로 돌면서 결과를 처리하는 것&amp;nbsp;&lt;/li&gt;
&lt;li&gt;즉, 커넥션이 열려 있는 상태에서 모든 데이터를 다 순회하기 전까지는 커넥션이 반납되지 않는다 (헐;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(원인)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;846&quot; data-start=&quot;439&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;515&quot; data-start=&quot;439&quot;&gt;Querydsl 내부적으로 transform() 은 HibernateHandler#iterate(...) 를 통해 실행&lt;/li&gt;
&lt;li data-end=&quot;582&quot; data-start=&quot;516&quot;&gt;이때 Hibernate의 Query.scroll(ScrollMode) 을 호출하면서 새로운 커넥션을 획득&lt;/li&gt;
&lt;li data-end=&quot;662&quot; data-start=&quot;583&quot;&gt;문제는 &lt;b&gt;이 커넥션을 반환하는 종료(close) 처리 로직이 transform() 경로에는 명시적으로 포함되지 않는다는 것&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;846&quot; data-start=&quot;663&quot;&gt;반면, fetch(), fetchOne(), fetchResults() 같은 메서드는 &lt;b&gt;JPA&lt;/b&gt;의 getResultList(), getSingleResult() 를 호출하는데, 이들은 커넥션 반환을 트리거하는 &lt;b&gt;종료 메서드(termination methods)&lt;/b&gt; 에 포함되어 있어 문제가 발생하지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;931&quot; data-start=&quot;848&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;즉, transform() 은 Querydsl/JPA 통합 계층에서 &amp;ldquo;쿼리를 종료하는 메서드&amp;rdquo;로 인식되지 않아 커넥션 반환이 누락된다&lt;/i&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-end=&quot;952&quot; data-start=&quot;938&quot; data-ke-size=&quot;size16&quot;&gt;transform() 내부 동작 흐름&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1327&quot; data-start=&quot;1006&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1034&quot; data-start=&quot;1006&quot;&gt;query.transform(...) 호출&lt;/li&gt;
&lt;li data-end=&quot;1091&quot; data-start=&quot;1035&quot;&gt;FetchableQueryBase / AbstractJPAQuery 에서 실행 경로 진입&lt;/li&gt;
&lt;li data-end=&quot;1135&quot; data-start=&quot;1092&quot;&gt;내부적으로 HibernateHandler#iterate(...) 실행&lt;/li&gt;
&lt;li data-end=&quot;1205&quot; data-start=&quot;1136&quot;&gt;Query.scroll(ScrollMode) 호출 &amp;rarr; ScrollableResults 획득 (커넥션 점유 시작)&lt;/li&gt;
&lt;li data-end=&quot;1250&quot; data-start=&quot;1206&quot;&gt;ScrollableResultsIterator 로 감싸 결과 변환 진행&lt;/li&gt;
&lt;li data-end=&quot;1285&quot; data-start=&quot;1251&quot;&gt;커넥션을 닫는 명시적 호출이 누락됨 &amp;rarr; 커넥션 반환 불가&lt;/li&gt;
&lt;li data-end=&quot;1327&quot; data-start=&quot;1286&quot;&gt;트랜잭션이 없는 경우, 커넥션이 풀에서 IN_USE 상태로 방치됨&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&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;span style=&quot;color: #333333; text-align: left;&quot;&gt;@Transactional(readOnly = true)&lt;/span&gt;&lt;b&gt;&amp;nbsp;붙여서 커넥션을 트랜잭션으로 관리해야함&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링은 메서드에 트랜잭션 설정이 없으면 &lt;b&gt;명시적으로 커넥션 관리하지 않음&lt;br /&gt;&lt;/b&gt;&amp;rarr;&amp;nbsp;커넥션이 &lt;b&gt;자동 반납되지 않음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;transform()은 커넥션을 오래 붙잡고 있음 &amp;rarr; 누수 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1:N 응답 데이터 조회 해결 방법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. @Transactional(readOnly = true) 명시하기&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;@Transactional(readOnly = true)
public Map&amp;lt;Long, List&amp;lt;BookDto&amp;gt;&amp;gt; findBooks() {
    return queryFactory
        .select(...)
        .from(book)
        .join(...)
        .transform(...);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;readOnly = true (트랜잭션은 읽기전용으로 작동)&lt;/li&gt;
&lt;li&gt;Spring이 커넥션을 &lt;b&gt;조기에 반납할 수 있음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;transform()을 사용해도 커넥션 누수 없이 안전&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. fetch() + 직접 DTO 조립하기&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;List&amp;lt;Tuple&amp;gt; tuples = queryFactory
    .select(author.id, book.title)
    .from(book)
    .join(book.author, author)
    .fetch();

Map&amp;lt;Long, List&amp;lt;String&amp;gt;&amp;gt; grouped = new LinkedHashMap&amp;lt;&amp;gt;();
for (Tuple tuple : tuples) {
    Long authorId = tuple.get(author.id);
    String bookTitle = tuple.get(book.title);
    grouped.computeIfAbsent(authorId, k -&amp;gt; new ArrayList&amp;lt;&amp;gt;()).add(bookTitle);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 데이터를 한 번에 가져옴 &amp;rarr; 커넥션 즉시 반납&lt;/li&gt;
&lt;li&gt;수동 조립이 필요하긴 하지만 &lt;b&gt;커넥션 누수 X&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;HikariCP 6.x 환경에서도 완전 호환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;HikariCP 6.x + Querydsl 5.x 에서는 transform() 사용 주의&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HikariCP 6부터는 Statement/ResultSet이 명확히 닫히지 않으면 &lt;b&gt;예외 발생&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;transform() 내부 구현이 auto-closeable을 완전히 보장하지 않기 때문에 &lt;b&gt;예외 발생 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;일부 버전에선 transform()이 사실상 &lt;b&gt;Deprecated 취급&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마무리하며 .. . .&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실, 처음엔 transform을 쓸 생각도 못한게, 현재 플젝에서 hikaricp 6+ 버전을 쓰고 있는데, transform이 deprecated 되서 써지지도 않았다. 그래서, hikaricp 버전 다운해서 쓰려고 찾아보다가, transform의 커넥션 누수 문제를 확인하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽이 많은 경우가 아니라면, transform을 사용하면 좋을 것 같고(물론 트랜잭션으로 커넥션 누수를 방지해야한다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그게 아니라면, fetch() + DTO 조립을 활용하는게 장기적으로 옳은 방향인 것 같다.&lt;/p&gt;</description>
      <category>스터디/스프링</category>
      <category>connection leack</category>
      <category>hikaricp</category>
      <category>querydsl</category>
      <category>transform</category>
      <author>dev-yen</author>
      <guid isPermaLink="true">https://dev-yen.tistory.com/72</guid>
      <comments>https://dev-yen.tistory.com/entry/Querydsl-transform-%EC%82%AC%EC%9A%A9-%EC%8B%9C-%EB%B0%9C%EC%83%9D%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EC%BB%A4%EB%84%A5%EC%85%98-%EB%88%84%EC%88%98-%EC%9D%B4%EC%8A%88#entry72comment</comments>
      <pubDate>Fri, 3 Oct 2025 14:56:46 +0900</pubDate>
    </item>
  </channel>
</rss>