<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>날샘 코딩</title>
    <link>https://devsh.tistory.com/</link>
    <description>페이스북 : https://www.facebook.com/digimon1740
이메일 : digimon.1740@gmail.com
github : https://github.com/digimon1740
linked in : https://www.linkedin.com/in/sanghoontony/</description>
    <language>ko</language>
    <pubDate>Sun, 8 Mar 2026 16:25:36 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>devsh</managingEditor>
    <image>
      <title>날샘 코딩</title>
      <url>https://tistory1.daumcdn.net/tistory/1566350/attach/31a08c0718dd46e6a2c19bf95f74015f</url>
      <link>https://devsh.tistory.com</link>
    </image>
    <item>
      <title>Java TestFixture를 활용한 Test 의존성 관리</title>
      <link>https://devsh.tistory.com/entry/Java-TestFixture%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Test-%EC%9D%98%EC%A1%B4%EC%84%B1-%EA%B4%80%EB%A6%AC</link>
      <description>&lt;h1 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot;&gt;서비스 모듈에 Java Test Fixtures 플러그인 적용 가이드&lt;/h1&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;이 문서는 각 서비스 모듈에 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;java-test-fixtures&lt;/span&gt; 플러그인을 적용하고 테스트 픽스처 모듈을 구성하는 방법을 설명합니다.&lt;/p&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;java-test-fixtures&lt;/span&gt; 플러그인은 테스트에 필요한 공통 데이터와 유틸리티를 모듈 내에서 공유할 수 있게 해주는 Gradle 플러그인입니다. 이 가이드는 각 서비스 모듈에서 테스트 픽스처 모듈을 구성하고 활용하는 방법을 설명합니다.&lt;/p&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;이점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;코드 중복 감소&lt;/b&gt;: 여러 테스트에서 반복적으로 사용되는 테스트 데이터를 중앙 집중화&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;유지보수성 향상&lt;/b&gt;: 테스트 데이터 변경 시 한 곳에서만 수정하면 됨&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;테스트 가독성 향상&lt;/b&gt;: 명확한 팩토리 메소드를 통해 테스트 데이터 생성 의도 전달&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;독립성 확보&lt;/b&gt;: 각 서비스 모듈이 자신의 테스트 픽스처를 직접 관리&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;빌드 성능 최적화&lt;/b&gt;: 모듈별로 필요한 테스트 픽스처만 컴파일하고 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;적용 방법&lt;/h2&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;1. 테스트 픽스처 모듈 생성&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;서비스 모듈 내에 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;test-fixtures&lt;/span&gt; 디렉토리를 생성하고 다음과 같이 구성합니다&lt;/p&gt;
&lt;pre id=&quot;code_1762403644545&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services/
  your-service/           # 예: rate-plan
    test-fixtures/        # 테스트 픽스처 모듈
      build.gradle        # 테스트 픽스처 모듈 빌드 스크립트
      README.md           # 사용 방법 문서
      src/
        testFixtures/     # 테스트 픽스처 소스 코드
          java/
            com/you-service/test/fixtures/
              data/       # 데이터 모델 픽스처
              util/       # 테스트 유틸리티
              annotation/ # 편의 어노테이션
              {domain}/   # 도메인별 테스트 픽스처&lt;/code&gt;&lt;/pre&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;
&lt;div style=&quot;background-color: #000000;&quot; contenteditable=&quot;false&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;2. 빌드 스크립트 설정&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;test-fixtures/build.gradle&lt;/span&gt; 파일을 다음과 같이 설정합니다&lt;/p&gt;
&lt;pre id=&quot;code_1762403707760&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'java'
    id 'java-test-fixtures'
    id 'java-library'
}

group = 'com.your-service'  // 예: com.rate
version = '1.0.0'

sourceCompatibility = JavaVersion.VERSION_11  // 서비스에 맞게 조정
targetCompatibility = JavaVersion.VERSION_11  // 서비스에 맞게 조정

repositories {
    mavenCentral()
    maven { url &quot;&amp;lt;https://&amp;lt;nexus&amp;gt;/repository/maven-public/&amp;gt;&quot; }
}

dependencies {
    // 기본 테스트 라이브러리
    api 'org.junit.jupiter:junit-jupiter:5.9.2'
    api 'org.assertj:assertj-core:3.24.2'
    api 'org.mockito:mockito-core:5.2.0'
    api 'org.mockito:mockito-junit-jupiter:5.2.0'
    
    // Spring Boot 테스트 관련 의존성
    api 'org.springframework.boot:spring-boot-starter-test:2.7.6'  // 버전은 서비스에 맞게 조정
    
    // TestContainers
    api 'org.testcontainers:testcontainers:1.18.3'
    api 'org.testcontainers:junit-jupiter:1.18.3'
    api 'org.testcontainers:mysql:1.18.3'  // 필요한 DB에 맞게 조정
    
    // 도메인 특화 의존성
    // 예: Kafka 관련 의존성이 필요한 경우
    // api 'org.apache.avro:avro:1.8.2'
    // api 'io.confluent:kafka-avro-serializer:5.0.0'
    
    // Lombok
    compileOnly 'org.projectlombok:lombok'
    testFixturesCompileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testFixturesAnnotationProcessor 'org.projectlombok:lombok'
}

java {
    withSourcesJar()
    withJavadocJar()
}

test {
    useJUnitPlatform()
}&lt;/code&gt;&lt;/pre&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;
&lt;div style=&quot;background-color: #000000;&quot; contenteditable=&quot;false&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;3. 서비스 모듈의 settings.gradle 수정&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;서비스 모듈의 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;settings.gradle&lt;/span&gt; 파일에 테스트 픽스처 모듈을 포함합니다&lt;/p&gt;
&lt;pre id=&quot;code_1762403726545&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rootProject.name = 'your-service'  // 예: 'rate-plan'

include 'your-service-module-1'    // 예: 'rate-plan-api'
include 'your-service-module-2'    // 예: 'rate-plan-management'
// ... 기존 모듈들 ...

include 'test-fixtures'  // 테스트 픽스처 모듈 추가&lt;/code&gt;&lt;/pre&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;4. 서비스 하위 모듈에 테스트 픽스처 의존성 추가&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;각 서비스 하위 모듈의 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;build.gradle&lt;/span&gt; 파일에 테스트 픽스처 의존성을 추가합니다&lt;/p&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1762403747526&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    // 기존 의존성 ...
    
    // 테스트 픽스처 의존성 추가
    testImplementation testFixtures(project(':test-fixtures'))
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;구현 가이드&lt;/h2&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;1. 기본 테스트 컨테이너 유틸리티 구현&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;src/testFixtures/java/com/yourservice/test/fixtures/util/MySQLTestContainer.java&lt;/span&gt;:&lt;/p&gt;
&lt;pre id=&quot;code_1762403775842&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.yourservice.test.fixtures.util;

import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;

public class MySQLTestContainer {
    private static final String MYSQL_VERSION = &quot;mysql:8.0&quot;;
    private static MySQLContainer&amp;lt;?&amp;gt; container;
    
    public static MySQLContainer&amp;lt;?&amp;gt; getInstance() {
        if (container == null) {
            container = new MySQLContainer&amp;lt;&amp;gt;(DockerImageName.parse(MYSQL_VERSION))
                    .withDatabaseName(&quot;testdb&quot;)
                    .withUsername(&quot;test&quot;)
                    .withPassword(&quot;test&quot;)
                    .withReuse(true);
        }
        return container;
    }
    
    public static void start() {
        getInstance().start();
    }
    
    public static void stop() {
        if (container != null &amp;amp;&amp;amp; container.isRunning()) {
            container.stop();
        }
    }
    
    // 편의 메소드
    public static String getJdbcUrl() {
        return getInstance().getJdbcUrl();
    }
    
    public static String getUsername() {
        return getInstance().getUsername();
    }
    
    public static String getPassword() {
        return getInstance().getPassword();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;
&lt;div style=&quot;background-color: #000000;&quot; contenteditable=&quot;false&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;2. 통합 테스트 어노테이션 구현&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;src/testFixtures/java/com/yourservice/test/fixtures/annotation/IntegrationTest.java&lt;/span&gt;:&lt;/p&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1762403805960&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.yourservice.test.fixtures.annotation;

import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@Testcontainers
@ActiveProfiles(&quot;test&quot;)
public @interface IntegrationTest {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-node-block=&quot;true&quot; data-ke-size=&quot;size23&quot;&gt;3. 기본 데이터 픽스처 구현&lt;/h3&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1762403845110&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.yourservice.test.fixtures.data;

import lombok.Builder;
import lombok.Data;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

@Data
@Builder
public class TestUserFixture {
    private Long id;
    private String username;
    private String email;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    public static TestUserFixture createDefault() {
        return TestUserFixture.builder()
                .id(1L)
                .username(&quot;testUser&quot;)
                .email(&quot;test@example.com&quot;)
                .createdAt(LocalDateTime.now())
                .updatedAt(LocalDateTime.now())
                .build();
    }
    
    public static List&amp;lt;TestUserFixture&amp;gt; createTestUsers(int count) {
        return Arrays.asList(
            TestUserFixture.builder().id(1L).username(&quot;user1&quot;).email(&quot;user1@example.com&quot;).build(),
            TestUserFixture.builder().id(2L).username(&quot;user2&quot;).email(&quot;user2@example.com&quot;).build(),
            TestUserFixture.builder().id(3L).username(&quot;user3&quot;).email(&quot;user3@example.com&quot;).build()
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;4. 도메인 픽스처 구현&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;서비스의 도메인에 맞는 픽스처 클래스를 구현합니다. 예를 들어 요금제 서비스의 경우:&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;src/testFixtures/java/com/yourservice/test/fixtures/rate/RatePlanFixture.java&lt;/span&gt;&lt;/p&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1762403865196&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.yourservice.test.fixtures.rate;

import lombok.Builder;
import lombok.Data;

import java.math.BigDecimal;
import java.time.LocalDate;

@Data
@Builder
public class RatePlanFixture {
    private Long id;
    private String name;
    private String code;
    private BigDecimal basePrice;
    private LocalDate startDate;
    private LocalDate endDate;
    private boolean active;
    
    public static RatePlanFixture createDefault() {
        return RatePlanFixture.builder()
                .id(1L)
                .name(&quot;Standard Rate&quot;)
                .code(&quot;STD-RATE&quot;)
                .basePrice(new BigDecimal(&quot;100000&quot;))
                .startDate(LocalDate.now())
                .endDate(LocalDate.now().plusDays(30))
                .active(true)
                .build();
    }
    
    public static RatePlanFixture createPromotionalRate() {
        return RatePlanFixture.builder()
                .id(2L)
                .name(&quot;Summer Promotion&quot;)
                .code(&quot;SUMMER-PROMO&quot;)
                .basePrice(new BigDecimal(&quot;80000&quot;))
                .startDate(LocalDate.now())
                .endDate(LocalDate.now().plusDays(14))
                .active(true)
                .build();
    }
}&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;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;테스트 컨테이너 사용&lt;/h3&gt;
&lt;pre id=&quot;code_1762403888165&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@IntegrationTest
public class ServiceTest {

    @Container
    private static final MySQLContainer&amp;lt;?&amp;gt; mysqlContainer = MySQLTestContainer.getInstance();
    
    @DynamicPropertySource
    static void registerDatabaseProperties(DynamicPropertyRegistry registry) {
        registry.add(&quot;spring.datasource.url&quot;, MySQLTestContainer::getJdbcUrl);
        registry.add(&quot;spring.datasource.username&quot;, MySQLTestContainer::getUsername);
        registry.add(&quot;spring.datasource.password&quot;, MySQLTestContainer::getPassword);
    }
    
    @Test
    void testService() {
        // 테스트 코드
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;
&lt;div style=&quot;background-color: #000000;&quot; contenteditable=&quot;false&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;데이터 픽스처 사용&lt;/h3&gt;
&lt;pre id=&quot;code_1762403901848&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void testWithFixtures() {
    // given
    TestUserFixture user = TestUserFixture.createDefault();
    RatePlanFixture ratePlan = RatePlanFixture.createDefault();
    
    // when
    // 테스트 로직 실행
    
    // then
    assertThat(user.getUsername()).isEqualTo(&quot;testUser&quot;);
    assertThat(ratePlan.getName()).isEqualTo(&quot;Standard Rate&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;모범 사례&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;orderedList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;팩토리 메소드 패턴 사용&lt;/b&gt;: &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;createDefault()&lt;/span&gt;, &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;createCustom(...)&lt;/span&gt; 등의 팩토리 메소드로 픽스처 생성&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;충분한 주석&lt;/b&gt;: 픽스처의 목적과 데이터 특성을 주석으로 설명&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;불변 객체 선호&lt;/b&gt;: 가능한 경우 픽스처 객체를 불변으로 설계&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;의미 있는 데이터&lt;/b&gt;: 실제 비즈니스 시나리오를 반영하는 의미 있는 테스트 데이터 사용&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;테스트 컨테이너 재사용&lt;/b&gt;: &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;withReuse(true)&lt;/span&gt; 옵션을 사용하여 테스트 컨테이너 재사용&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;모듈화&lt;/b&gt;: 관련 픽스처를 적절한 패키지로 그룹화&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>devsh</author>
      <guid isPermaLink="true">https://devsh.tistory.com/159</guid>
      <comments>https://devsh.tistory.com/entry/Java-TestFixture%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Test-%EC%9D%98%EC%A1%B4%EC%84%B1-%EA%B4%80%EB%A6%AC#entry159comment</comments>
      <pubDate>Thu, 6 Nov 2025 13:38:49 +0900</pubDate>
    </item>
    <item>
      <title>OpenRewrite 마이그레이션 가이드</title>
      <link>https://devsh.tistory.com/entry/OpenRewrite-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;OpenRewrite는 소스 코드를 자동으로 리팩토링하고 마이그레이션할 수 있는 도구입니다. 이 문서에서는 OpenRewrite를 활용하여 다음과 같은 레시피를 적용해서 마이그레이션을 수행하는 방법을 설명합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Spring Boot 3.X으로 마이그레이션&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Java 17/21로 마이그레이션&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Java 코드 자동 포맷팅&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;레시피 선택하기&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;OpenRewrite는 &lt;a href=&quot;https://docs.openrewrite.org/recipes&quot; data-prosemirror-mark-name=&quot;link&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;레시피&lt;/a&gt;라는 코드 변환 규칙 세트를 사용합니다. 이번에 살펴 볼 레시피는 다음과 같습니다.&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;1. Spring Boot 3.0 마이그레이션&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot 2.x 애플리케이션을 Spring Boot 3.0으로 업그레이드하기 위한 레시피입니다.&lt;/p&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;pre id=&quot;code_1762403098670&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;관련 레시피 : &lt;span data-prosemirror-node-inline=&quot;true&quot; data-prosemirror-node-name=&quot;inlineCard&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://docs.openrewrite.org/recipes/java/spring/boot3/upgradespringboot_3_0&quot;&gt;https://docs.openrewrite.org/recipes/java/spring/boot3/upgradespringboot_3_0&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;2. Java 버전 마이그레이션&lt;/h3&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;Java 17 마이그레이션&lt;/h4&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1762403115922&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.openrewrite.java.migrate.UpgradeToJava17&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;관련 레시피 : &lt;span data-prosemirror-node-inline=&quot;true&quot; data-prosemirror-node-name=&quot;inlineCard&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://docs.openrewrite.org/recipes/java/migrate/upgradetojava17&quot;&gt;https://docs.openrewrite.org/recipes/java/migrate/upgradetojava17&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;Java 21 마이그레이션&lt;/h4&gt;
&lt;pre id=&quot;code_1762403132742&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.openrewrite.java.migrate.UpgradeToJava21&lt;/code&gt;&lt;/pre&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;관련 레시피 : &lt;span data-prosemirror-node-inline=&quot;true&quot; data-prosemirror-node-name=&quot;inlineCard&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://docs.openrewrite.org/running-recipes/popular-recipe-guides/migrate-to-java-21&quot;&gt;https://docs.openrewrite.org/running-recipes/popular-recipe-guides/migrate-to-java-21&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;3. Java 코드 포맷팅&lt;/h3&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1762403148060&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.openrewrite.java.format.AutoFormat&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;관련 레시피 : &lt;span data-prosemirror-node-inline=&quot;true&quot; data-prosemirror-node-name=&quot;inlineCard&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://docs.openrewrite.org/recipes/java/format/autoformat&quot;&gt;https://docs.openrewrite.org/recipes/java/format/autoformat&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;적용 방법&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;OpenRewrite를 프로젝트에 적용하는 방법은 여러 가지가 있습니다. 여기서는 Gradle을 사용한 방법을 설명합니다.&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;방법 1: Gradle Init Script 사용&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;init.gradle&lt;/span&gt; 파일을 생성하고 다음 내용을 추가합니다&lt;/p&gt;
&lt;pre id=&quot;code_1762403173428&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;initscript {
    repositories {
        maven { url &quot;&amp;lt;https://plugins.gradle.org/m2&amp;gt;&quot; }
    }
    dependencies { classpath(&quot;org.openrewrite:plugin:latest.release&quot;) }
}
rootProject {
    plugins.apply(org.openrewrite.gradle.RewritePlugin)
    dependencies {
        rewrite(&quot;org.openrewrite:rewrite-java&quot;)
    }
    rewrite {
        // Java 코드 포맷팅 레시피
        activeRecipe(&quot;org.openrewrite.java.format.AutoFormat&quot;) 
        setExportDatatables(true)
    }
    afterEvaluate {
        if (repositories.isEmpty()) {
            repositories {
                mavenCentral()
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;expand&quot; data-prosemirror-content-type=&quot;node&quot; data-expanded=&quot;true&quot; data-title=&quot;init.gradle&quot; data-node-type=&quot;expand&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;실행&lt;/p&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;pre id=&quot;code_1762403187675&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew --init-script init.gradle rewriteRun&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;방법 2: Build.Gradle 사용&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;build.gradle&lt;/span&gt; 파일에 다음 내용을 추가합니다&lt;/p&gt;
&lt;pre id=&quot;code_1762403203144&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; plugins {
    id(&quot;org.openrewrite.rewrite&quot;) version(&quot;7.2.1&quot;)
}

rewrite {
    // Java 코드 포맷팅 레시피
    activeRecipe(&quot;org.openrewrite.java.format.AutoFormat&quot;)
    setExportDatatables(true)
}

repositories {
    mavenCentral()
}&lt;/code&gt;&lt;/pre&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;expand&quot; data-prosemirror-content-type=&quot;node&quot; data-expanded=&quot;true&quot; data-title=&quot;build.gradle&quot; data-node-type=&quot;expand&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;실행&lt;/p&gt;
&lt;pre id=&quot;code_1762403228095&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew --init-script init.gradle rewriteRun&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>devsh</author>
      <guid isPermaLink="true">https://devsh.tistory.com/158</guid>
      <comments>https://devsh.tistory.com/entry/OpenRewrite-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EA%B0%80%EC%9D%B4%EB%93%9C#entry158comment</comments>
      <pubDate>Thu, 6 Nov 2025 13:27:30 +0900</pubDate>
    </item>
    <item>
      <title>OpenRewrite를 사용한 대규모 리팩토링 자동화</title>
      <link>https://devsh.tistory.com/entry/OpenRewrite%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%9E%90%EB%8F%99%ED%99%94</link>
      <description>&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;OpenRewrite는 소스 코드를 대규모로 리팩토링하기 위한 오픈소스 도구입니다. 특히 Java, SpringBoot 버전 업그레이드와 같은 복잡한 마이그레이션 작업을 자동화하는 데 매우 유용합니다.&lt;/p&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;1. OpenRewrite 소개&lt;/h2&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;embedCard&quot; data-prosemirror-content-type=&quot;node&quot; data-original-height=&quot;480&quot; data-original-width=&quot;853.34&quot; data-width=&quot;83.33&quot; data-layout=&quot;align-start&quot; data-card-url=&quot;https://www.youtube.com/watch?v=uViRlX-RahA&quot; data-embed-card=&quot;&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=uViRlX-RahA&quot;&gt;https://www.youtube.com/watch?v=uViRlX-RahA&lt;/a&gt;&lt;/div&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;주요 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;소스 코드의 &lt;a href=&quot;https://docs.openrewrite.org/concepts-and-explanations/lossless-semantic-trees&quot; data-prosemirror-mark-name=&quot;link&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;&lt;b&gt;LST(Lossless Semantic Trees)&lt;/b&gt;&lt;/a&gt; 기반 분석 및 변환&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;다양한 마이그레이션 레시피 제공&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;대규모 코드베이스에서 일관된 스타일로 코드 리팩토링 변경 지원&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;변경 사항에 대한 미리보기(dryRun) 및 검증 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;사용 시나리오&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Java 버전 업그레이드 (e.g. 8 &amp;rarr; 11 &amp;rarr; 17 &amp;rarr; 21)&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;SpringBoot 버전 업그레이드 (e.g. 2.x &amp;rarr; 3.x)&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;대대적인 보안 취약점 업데이트 (e.g. &lt;a href=&quot;https://www.ibm.com/kr-ko/topics/log4j&quot; data-prosemirror-mark-name=&quot;link&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;2021년 log4j 보안취약점&lt;/a&gt;)&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;종속성 버전 업데이트&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;코드 스타일 및 패턴 업데이트&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;2. OpenRewrite의 예시&lt;/h2&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;마이그레이션의 고통 포인트&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;SpringBoot 2.x에서 3.x로의 마이그레이션은 다음과 같은 귀찮은 작업들이 필요합니다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;javax.*&lt;/span&gt; &amp;rarr; &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;jakarta.*&lt;/span&gt; 패키지 변경 (수백 개의 import 문 수정)&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;deprecated API들의 대체 작업&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Spring Security 설정 방식 변경&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;수많은 라이브러리 버전 호환성 체크&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Java 17 마이그레이션&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1762402764263&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 이런 코드가 프로젝트에 수백 개...
import javax.persistence.Entity;
import javax.validation.constraints.NotNull;

// 모두 아래와 같이 변경 필요
import jakarta.persistence.Entity;
import jakarta.validation.constraints.NotNull;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;3. OpenRewrite의 기술적 해결책&lt;/h2&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;1. LST(Lossless Semantic Trees) 기반 분석&lt;/h3&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;LST의 타입 속성(Type Attribution)&lt;/h4&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1762402786944&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 원본 코드
List&amp;lt;String&amp;gt; names = getNames();
names.stream()
     .filter(name -&amp;gt; name.length() &amp;gt; 5)
     .collect(Collectors.toList());

// LST는 다음과 같은 타입 정보를 보존
List&amp;lt;String&amp;gt; names = getNames();          // names: java.util.List&amp;lt;java.lang.String&amp;gt;
names.stream()                            // stream(): java.util.stream.Stream&amp;lt;java.lang.String&amp;gt;
     .filter(name -&amp;gt; name.length() &amp;gt; 5)   // filter(): Stream&amp;lt;T&amp;gt; 
     .collect(Collectors.toList());       // collect(): java.util.List&amp;lt;java.lang.String&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;2. 포맷 보존(Format Preservation)&lt;/h3&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;주석과 공백 보존&lt;/h4&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1762402804659&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* 원본 코드의 복잡한 포맷팅 */
public class User {
    
    private final String name;    // 사용자 이름
    
    // 생성자 주석
    public User(String name) {
        this.name = name;         // 필드 초기화
    }                             // 후행 주석
}

// LST는 위의 모든 주석, 공백, 들여쓰기를 그대로 보존&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1762402816445&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 기존 코드의 스타일
public class Example {
    private void method1() {
        if (condition) {
            doSomething();
        }
    }
    
    // OpenRewrite가 새로 추가하는 코드는 주변 스타일을 따름
    private void newMethod() {    // 동일한 들여쓰기 적용
        if (condition) {          // 중괄호 스타일 매칭
            doSomethingNew();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;2. 정교한 의존성 분석&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;프로젝트의 모든 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;pom.xml&lt;/span&gt;/&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;build.gradle&lt;/span&gt; 파일 스캔&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;호환되지 않는 라이브러리 버전 자동 감지&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;전이 의존성 충돌 해결&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;4. 실제 사용 예시&lt;/h2&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;1. Gradle 프로젝트 설정&lt;/h3&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1762402832545&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id(&quot;org.openrewrite.rewrite&quot;) version(&quot;5.x.x&quot;)
}

rewrite {
    activeRecipe(&quot;org.openrewrite.java.spring.boot3.SpringBoot3Migration&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;2. 마이그레이션 실행&lt;/h3&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1762402849713&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Gradle의 경우
./gradlew rewriteDryRun # 변경 예정 사항 미리보기
./gradlew rewriteRun    # 실제 변경 적용&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;Junit Assert 리팩토링&lt;/h3&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1762402868462&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 변경 전 
import org.junit.Assert;

Assert.assertTrue(condition);

// 변경 후
import static org.junit.Assert.assertTrue;

assertTrue(condition);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;Spring Security 설정 마이그레이션&lt;/h3&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1762402892415&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 변경 전 WebSecurityConfigurerAdapter 사용
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers(&quot;/api/**&quot;).authenticated();
    }
}

// 변경 후 컴포넌트 기반 설정
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -&amp;gt; 
            auth.requestMatchers(&quot;/api/**&quot;).authenticated()
        );
        return http.build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;JPA 엔티티 마이그레이션&lt;/h3&gt;
&lt;div data-prosemirror-mark-name=&quot;breakout&quot; data-prosemirror-content-type=&quot;mark&quot; data-width=&quot;760&quot; data-mode=&quot;wide&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1762402931505&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 변경 전
import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class User {
    @Id
    private Long id;
}

// 변경 후
import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class User {
    @Id
    private Long id;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;5. 마이그레이션 전략&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;orderedList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;코어 모듈부터 시작&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;테스트가 많은 모듈 우선 적용&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;의존성이 적은 모듈부터 진행&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1762402952714&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 변경 사항 미리보기
./gradlew rewriteDryRun

# 실제 변경 적용
./gradlew rewriteRun

# 특정 모듈만 적용
./gradlew :core:rewriteRun&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;6. 레퍼런스&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/java-openrewrite&quot; data-prosemirror-mark-name=&quot;link&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;A Guide to OpenRewrite | Baeldung&lt;/a&gt;&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;a href=&quot;https://docs.openrewrite.org/recipes&quot; data-prosemirror-mark-name=&quot;link&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;Recipe catalog | OpenRewrite Docs&lt;/a&gt;&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;a href=&quot;https://github.com/openrewrite/rewrite/issues&quot; data-prosemirror-mark-name=&quot;link&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;embedCard&quot; data-prosemirror-content-type=&quot;node&quot; data-original-height=&quot;480&quot; data-original-width=&quot;853.34&quot; data-width=&quot;100&quot; data-layout=&quot;center&quot; data-card-url=&quot;https://www.youtube.com/watch?v=KlQZH6WHa2c&quot; data-embed-card=&quot;&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=KlQZH6WHa2c&quot;&gt;https://www.youtube.com/watch?v=KlQZH6WHa2c&lt;/a&gt;&lt;/div&gt;</description>
      <author>devsh</author>
      <guid isPermaLink="true">https://devsh.tistory.com/157</guid>
      <comments>https://devsh.tistory.com/entry/OpenRewrite%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%9E%90%EB%8F%99%ED%99%94#entry157comment</comments>
      <pubDate>Thu, 6 Nov 2025 13:22:40 +0900</pubDate>
    </item>
    <item>
      <title>Kotlin과 Spring Cloud Function 기반 애플리케이션을 AWS EventBridge와 Lambda에 배포하기</title>
      <link>https://devsh.tistory.com/entry/Kotlin%EA%B3%BC-Spring-Cloud-Function-%EA%B8%B0%EB%B0%98-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98%EC%9D%84-AWS-EventBridge%EC%99%80-Lambda%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kotlin과 Spring Cloud Function 기반 애플리케이션을 AWS EventBridge와 Lambda에 배포하기개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 B2C 서비스를 개발하며 배치 작업의 필요성이 발생했는데 기존 서비스의 배치는 SpringBatch로 작성하고 Jenkins를 트리거로 사용하고 있었습니다. 처음엔 기존 서비스와 같은 기술 스택을 고민했지만 과연 현재 요구사항에서 SpringBatch와 Jenkins를 사용할 정도의 규모일까라는 의문이 들었고 인프라 관리 부담 또한 적지 않은 고민이 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 고민을 해결하기 위해 개발자가 쉽게 조작할 수 있어야 하고 트리거가 백엔드 API를 호출할 수 있는 형태를 고민했고 &lt;b&gt;Kotlin&lt;/b&gt;과 &lt;b&gt;Spring Cloud Function&lt;/b&gt;으로 애플리케이션을 개발하고 &lt;b&gt;AWS EventBridge와 Lambda&lt;/b&gt;를 같이 사용하는 구조를 생각했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아키텍쳐&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-01-07 오후 4.49.17.png&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0i1Kc/btsLCEpS44A/VKOyD92Owe7BDAzDQnM87K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0i1Kc/btsLCEpS44A/VKOyD92Owe7BDAzDQnM87K/img.png&quot; data-alt=&quot;AWS EventBridge, Lambda를 사용해 서비스 API를 호출하는 과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0i1Kc/btsLCEpS44A/VKOyD92Owe7BDAzDQnM87K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0i1Kc%2FbtsLCEpS44A%2FVKOyD92Owe7BDAzDQnM87K%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;702&quot; height=&quot;279&quot; data-filename=&quot;스크린샷 2024-01-07 오후 4.49.17.png&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;279&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AWS EventBridge, Lambda를 사용해 서비스 API를 호출하는 과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 위와 같은 고민을 해결한 과정과 애플리케이션 배포 과정을 소개하도록 하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Kotlin과 Spring Cloud Function의 조합&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kotlin 적용 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 Lambda를 사용하면 Python과 Node.js을 많이 사용하지만 백엔드팀이 익숙한 기술 스택이 Kotlin이고 Lambda에서 JDK 최신 버전인 JVM 21 런타임도 지원하므로 Kotlin을 사용하게 됐습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Kotlin의 장점&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간결한 문법&lt;/li&gt;
&lt;li&gt;안전한 타입 시스템&lt;/li&gt;
&lt;li&gt;자바와의 높은 호환성&lt;/li&gt;
&lt;li&gt;코루틴과 같은 현대적 동시성 모델 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring Cloud Function 이란&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-function/reference/spring-cloud-function/introduction.html&quot;&gt;&lt;b&gt;Spring Cloud Function&lt;/b&gt;&lt;/a&gt;은 서버리스 아키텍처를 위한 Spring Cloud 프로젝트 중 하나로 AWS Lambda, Azure Function, GCP Function 등 클라우드 위에서 동작하는 서버리스 플랫폼과 통합을 &lt;a href=&quot;https://docs.spring.io/spring-cloud-function/reference/spring-cloud-function/serverless-platform-adapters.html&quot;&gt;&lt;b&gt;Adapter&lt;/b&gt;&lt;/a&gt;로써 지원하며 비즈니스 로직을 간단한 함수 형태로 작성할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 경험해본 Spring Cloud Function의 다른 사용 예시는 주식 시세 애플리케이션을 개발하며 &lt;a href=&quot;https://docs.spring.io/spring-cloud-stream/docs/current/reference/html/&quot;&gt;Spring Cloud Stream&lt;/a&gt; 을 사용해 Source, Processor, Sink 단계별로 비즈니스 로직을 함수 단위 애플리케이션으로 만들어주던 경험이 너무 좋았는데 Spring Cloud Stream도 각 함수 단위 애플리케이션을 개발할때 Spring Cloud Function을 사용하고 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring Cloud Function의 장점&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버리스 환경에 최적화된 스프링 부트 모델&lt;/li&gt;
&lt;li&gt;다양한 클라우드 플랫폼과의 호환성&lt;/li&gt;
&lt;li&gt;함수 단위의 비즈니스 로직 구현 용이&lt;/li&gt;
&lt;li&gt;특정 플랫폼에 의존성이 없는 프로그래밍 모델 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;AWS EventBridge와 Lambda&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://aws.amazon.com/ko/eventbridge/&quot;&gt;&lt;b&gt;AWS EventBridge&lt;/b&gt;&lt;/a&gt;는 서버리스 이벤트 버스 서비스로, 다양한 AWS 서비스 및 외부 애플리케이션 간 이벤트 드리븐 아키텍쳐 형태로 쉽게 구축할 수 있도록 지원합니다. &lt;a href=&quot;https://aws.amazon.com/ko/lambda/&quot;&gt;&lt;b&gt;AWS Lambda&lt;/b&gt;&lt;/a&gt;는 서버리스 컴퓨팅을 제공하여, 코드 실행을 위한 서버 관리 필요 없이 비즈니스 로직에 집중할 수 있게 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EventBridge의 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;높은 확장성과 느슨한 결합 제공&lt;/li&gt;
&lt;li&gt;실시간 데이터 처리 및 반응형 시스템 구축 가능&lt;/li&gt;
&lt;li&gt;cron, rate 기반의 스케쥴러 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Lambda의 장점&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠른 시작 시간과 효율적인 리소스 관리&lt;/li&gt;
&lt;li&gt;다양한 프로그래밍 언어 지원&lt;/li&gt;
&lt;li&gt;간편한 배포 및 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Cloud Function 애플리케이션 개발 과정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제로 사용한 기술 스택&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot 3.2.1&lt;/li&gt;
&lt;li&gt;Spring Cloud 2023.0.0&lt;/li&gt;
&lt;li&gt;Kotlin + Gradle Kotlin&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://start.spring.io/&quot;&gt;&lt;b&gt;Spring Initializr&lt;/b&gt;&lt;/a&gt;에 접속해서 아래와 같이 사용할 버전과 Spring Cloud Function 의존성을 추가한 뒤 초기 프로젝트 구성을 다운로드 받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드한 프로젝트를 Intellij IDEA에 import 한 뒤 build.gradle.kts 를 열어 아래와 같이 설정합니다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
	id(&quot;org.springframework.boot&quot;) version &quot;3.2.1&quot;
	id(&quot;io.spring.dependency-management&quot;) version &quot;1.1.4&quot;
	id(&quot;com.github.johnrengelman.shadow&quot;) version &quot;7.1.2&quot;
	kotlin(&quot;jvm&quot;) version &quot;1.9.21&quot;
	kotlin(&quot;plugin.spring&quot;) version &quot;1.9.21&quot;
}

group = &quot;com.example&quot;
version = &quot;0.0.1-SNAPSHOT&quot;

java {
	sourceCompatibility = JavaVersion.VERSION_21
}

repositories {
	mavenCentral()
}

extra[&quot;springCloudVersion&quot;] = &quot;2023.0.0&quot;

dependencies {
	implementation(&quot;org.springframework.cloud:spring-cloud-function-web&quot;)
	implementation(&quot;org.springframework.cloud:spring-cloud-function-adapter-aws&quot;)
	implementation(&quot;com.fasterxml.jackson.module:jackson-module-kotlin&quot;)
	implementation(&quot;org.jetbrains.kotlin:kotlin-reflect&quot;)
	implementation(&quot;com.github.kittinunf.fuel:fuel:3.0.0-alpha1&quot;)

	testImplementation(&quot;org.springframework.boot:spring-boot-starter-test&quot;)
}

dependencyManagement {
	imports {
		mavenBom(&quot;org.springframework.cloud:spring-cloud-dependencies:${property(&quot;springCloudVersion&quot;)}&quot;)
	}
}

tasks.withType&amp;lt;KotlinCompile&amp;gt; {
	kotlinOptions {
		freeCompilerArgs += &quot;-Xjsr305=strict&quot;
		jvmTarget = &quot;21&quot;
	}
}

tasks.withType&amp;lt;Test&amp;gt; {
	useJUnitPlatform()
}

tasks.withType&amp;lt;Jar&amp;gt; {
	manifest {
		attributes[&quot;Start-Class&quot;] = &quot;com.example.function.FunctionApplicationKt&quot;
	}
}

tasks.assemble {
	dependsOn(&quot;shadowJar&quot;)
}

tasks.withType&amp;lt;com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar&amp;gt; {
	archiveClassifier.set(&quot;aws&quot;)
	archiveFileName.set(&quot;batch.jar&quot;)
	dependencies {
		exclude(&quot;org.springframework.cloud:spring-cloud-function-web&quot;)
	}
	mergeServiceFiles()
	append(&quot;META-INF/spring.handlers&quot;)
	append(&quot;META-INF/spring.schemas&quot;)
	append(&quot;META-INF/spring.tooling&quot;)
	append(&quot;META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports&quot;)
	append(&quot;META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports&quot;)
	transform(com.github.jengelman.gradle.plugins.shadow.transformers.PropertiesFileTransformer::class.java) {
		paths.add(&quot;META-INF/spring.factories&quot;)
		mergeStrategy = &quot;append&quot;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설정에서 핵심적인 부분은 shadowJar 입니다. &lt;a href=&quot;https://imperceptiblethoughts.com/shadow/introduction/&quot;&gt;shadowJar&lt;/a&gt;는 프로젝트의 공통 의존성을 하나의 jar안에 모두 결합하기 위해 사용하는 Gradle 플러그인입니다. 이런 형태를 일반적으로 fat-jar라고도 부릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;애플리케이션 개발&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;resources/application.yml 파일을 생성한 뒤 아래와 같이 작성한다. 이때 definition에 매핑되는 값은 Lambda를 통해 호출될 함수명과 일치해야합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;spring:
  cloud:
    function:
      definition: batchCaller
&lt;/code&gt;&lt;/pre&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;application.yml의 definition에 설정한대로 batchCaller 라는 이름의 함수를 Bean으로 구성한 애플리케이션입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.example.function

import com.fasterxml.jackson.databind.ObjectMapper
import fuel.Fuel
import fuel.get
import kotlinx.coroutines.runBlocking
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean

@SpringBootApplication
class FunctionApplication(
    private val objectMapper: ObjectMapper,
) {

    data class Payload(
        val url: String,
    )

    @Bean
    fun batchCaller(): (String) -&amp;gt; (String) {
        return { payloadAsString -&amp;gt;
            val payload = objectMapper.readValue(payloadAsString, Payload::class.java)

            runBlocking {
                val response = Fuel.get(payload.url)
                response.statusCode.toString()
            }

        }
    }

}

fun main(args: Array&amp;lt;String&amp;gt;) {
    runApplication&amp;lt;FunctionApplication&amp;gt;(*args)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제의 batchCaller Bean은 아래와 순서로 동작합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;EventBridge로부터 넘어온 JSON payload를 함수의 인자로 전달&lt;/li&gt;
&lt;li&gt;인자로 전달받은 JSON payload를 Kotlin의 data class인 Payload로 변환&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kittinunf/fuel&quot;&gt;Fuel client&lt;/a&gt;를 사용해 Payload.url을 그대로 호출. 이때 호출된 url은 서비스 API의&amp;nbsp; 엔드 포인트입니다.&amp;lt;/aside&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;aside&amp;gt;   Fuel client는 코루틴 기반의 경량 HTTP Client입니다. 예제는 Spring Web이나 Spring WebFlux 의존성이 없으므로 Spring에서 기본 제공하는 RestClient, WebClient등을 사용할 수 없고 서버리스 플랫폼에 올라가는 특성상 가벼운 Client가 유리하므로 Fuel을 선택했습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;애플리케이션 빌드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 최상위에서 shadowJar 플러그인를 사용해 애플리케이션을 jar로 빌드합니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;./gradlew shadowJar
&lt;/code&gt;&lt;/pre&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;빌드가 완료되면 프로젝트의 build/libs/ 디렉토리에 jar 파일이 생성됩니다. 예제의 경우 batch.jar 로 빌드 되게 설정된 상태입니다. batch.jar는 이후 Lambda에 코드를 업로드할 때 사용됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;AWS EventBridge와 Lambda에 애플리케이션 배포하기&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lambda에 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Lambda 콘솔에 접속해서 아래와 같이 함수를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희가 중요한 것은 런타임이므로 아키텍쳐나 함수 이름 등 기타 설정은 상황에 맞게 설정하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수가 생성되면 코드를 업로드 합니다. 이때 S3 버킷에 링크를 연결하거나 .jar 파일을 직접 업로드할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업로드가 완료되었다면 &lt;b&gt;런타임 설정&lt;/b&gt;에서 &lt;b&gt;편집&lt;/b&gt;을 눌러 아래와 같이 설정을 변경합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핸들러 설정을 org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest 로 변경해주셔야 정상적으로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정이 완료되었다면 정상적으로 동작하는지 테스트해 보겠습니다. &lt;b&gt;테스트&lt;/b&gt; 탭에 들어가서 이벤트 JSON을 수정합니다. 이벤트 JSON은 람다 함수로 전달되는 Payload이고 앞서 Spring Cloud Function 애플리케이션을 만들 때 data class로 만들었던 Payload 형태가 되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 함수 실행 테스트가 완료되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 EventBridge의 스케쥴러와 앞서 설정한 Lambda 함수를 연결해주겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EventBridge 스케쥴러 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EventBridge 콘솔에 접속해 &lt;b&gt;일정 생성&lt;/b&gt; 버튼을 눌러 스케쥴러를 생성해보겠습니다. 예제의 경우 Rate 기반 일정을 사용해 1분 마다 Event가 발생하도록 설정했습니다. EventBridge는 Rate 기반 일정과 Cron 기반 일정을 같이 지원하므로 편리하게 스케쥴러를 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음엔 EventBridge와 Lambda를 연결합니다. 페이로드에는 Lambda 테스트시 사용했던 것과 같이 Lambda에서 호출할 서비스 API 엔드포인트를 적어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 설정이 완료되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS EventBridge와 Lambda에 애플리케이션을 배포하는 것은 서버리스 아키텍처를 활용하는 효과적인 방법입니다. 이 과정을 통해 개발자는 인프라 관리의 부담 없이 비즈니스 로직에 집중할 수 있으며, 빠르고 유연한 클라우드 애플리케이션 개발이 가능해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 JVM 언어만 사용해본 개발자들은 Lambda 기반의 서버리스 애플리케이션을 개발하는데 있어서 부담을 느끼는 상황을 많이 봐왔습니다. 주로 Lambda 함수를 Python과 Node.js로 만드는 경우가 많기 때문인데 Spring Cloud Function을 사용하면 Java나 Kotlin과 같은 JVM 언어에서도 빠르고 쉽게 서버리스 애플리케이션을 개발할 수 있습니다. 또한 Spring Cloud Function은 플랫폼에 상관없이 독립 실행형(Standalone) 애플리케이션을 만들어주므로 비즈니스 로직에 집중한 서비스를 만드는데 도움을 줍니다.&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;a href=&quot;https://github.com/digimon1740/spring-cloud-function-kotlin-aws-lambda&quot;&gt;https://github.com/digimon1740/spring-cloud-function-kotlin-aws-lambda&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-function/reference/adapters/aws-intro.html&quot;&gt;https://docs.spring.io/spring-cloud-function/reference/adapters/aws-intro.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html&quot;&gt;https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kittinunf/fuel&quot;&gt;https://github.com/kittinunf/fuel&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 B2C 서비스를 개발하며 배치 작업의 필요성이 발생했는데 기존 서비스의 배치는 SpringBatch로 작성하고 Jenkins를 트리거로 사용하고 있었습니다. 처음엔 기존 서비스와 같은 기술 스택을 고민했지만 과연 현재 요구사항에서 SpringBatch와 Jenkins를 사용할 정도의 규모일까라는 의문이 들었고 인프라 관리 부담 또한 적지 않은 고민이 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 고민을 해결하기 위해 개발자가 쉽게 조작할 수 있어야 하고 트리거가 백엔드 API를 호출할 수 있는 형태를 고민했고 &lt;b&gt;Kotlin&lt;/b&gt;과 &lt;b&gt;Spring Cloud Function&lt;/b&gt;으로 애플리케이션을 개발하고 &lt;b&gt;AWS EventBridge와 Lambda&lt;/b&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;AWS EventBridge, Lambda를 사용해 서비스 API를 호출하는 과정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 위와 같은 고민을 해결한 과정과 애플리케이션 배포 과정을 소개하도록 하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Kotlin과 Spring Cloud Function의 조합&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kotlin 적용 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 Lambda를 사용하면 Python과 Node.js을 많이 사용하지만 백엔드팀이 익숙한 기술 스택이 Kotlin이고 Lambda에서 JDK 최신 버전인 JVM 21 런타임도 지원하므로 Kotlin을 사용하게 됐습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Kotlin의 장점&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간결한 문법&lt;/li&gt;
&lt;li&gt;안전한 타입 시스템&lt;/li&gt;
&lt;li&gt;자바와의 높은 호환성&lt;/li&gt;
&lt;li&gt;코루틴과 같은 현대적 동시성 모델 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring Cloud Function 이란&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-function/reference/spring-cloud-function/introduction.html&quot;&gt;&lt;b&gt;Spring Cloud Function&lt;/b&gt;&lt;/a&gt;은 서버리스 아키텍처를 위한 Spring Cloud 프로젝트 중 하나로 AWS Lambda, Azure Function, GCP Function 등 클라우드 위에서 동작하는 서버리스 플랫폼과 통합을 &lt;a href=&quot;https://docs.spring.io/spring-cloud-function/reference/spring-cloud-function/serverless-platform-adapters.html&quot;&gt;&lt;b&gt;Adapter&lt;/b&gt;&lt;/a&gt;로써 지원하며 비즈니스 로직을 간단한 함수 형태로 작성할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 경험해본 Spring Cloud Function의 다른 사용 예시는 주식 시세 애플리케이션을 개발하며 &lt;a href=&quot;https://docs.spring.io/spring-cloud-stream/docs/current/reference/html/&quot;&gt;Spring Cloud Stream&lt;/a&gt; 을 사용해 Source, Processor, Sink 단계별로 비즈니스 로직을 함수 단위 애플리케이션으로 만들어주던 경험이 너무 좋았는데 Spring Cloud Stream도 각 함수 단위 애플리케이션을 개발할때 Spring Cloud Function을 사용하고 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring Cloud Function의 장점&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버리스 환경에 최적화된 스프링 부트 모델&lt;/li&gt;
&lt;li&gt;다양한 클라우드 플랫폼과의 호환성&lt;/li&gt;
&lt;li&gt;함수 단위의 비즈니스 로직 구현 용이&lt;/li&gt;
&lt;li&gt;특정 플랫폼에 의존성이 없는 프로그래밍 모델 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;AWS EventBridge와 Lambda&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://aws.amazon.com/ko/eventbridge/&quot;&gt;&lt;b&gt;AWS EventBridge&lt;/b&gt;&lt;/a&gt;는 서버리스 이벤트 버스 서비스로, 다양한 AWS 서비스 및 외부 애플리케이션 간 이벤트 드리븐 아키텍쳐 형태로 쉽게 구축할 수 있도록 지원합니다. &lt;a href=&quot;https://aws.amazon.com/ko/lambda/&quot;&gt;&lt;b&gt;AWS Lambda&lt;/b&gt;&lt;/a&gt;는 서버리스 컴퓨팅을 제공하여, 코드 실행을 위한 서버 관리 필요 없이 비즈니스 로직에 집중할 수 있게 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EventBridge의 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;높은 확장성과 느슨한 결합 제공&lt;/li&gt;
&lt;li&gt;실시간 데이터 처리 및 반응형 시스템 구축 가능&lt;/li&gt;
&lt;li&gt;cron, rate 기반의 스케쥴러 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Lambda의 장점&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠른 시작 시간과 효율적인 리소스 관리&lt;/li&gt;
&lt;li&gt;다양한 프로그래밍 언어 지원&lt;/li&gt;
&lt;li&gt;간편한 배포 및 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Cloud Function 애플리케이션 개발 과정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제로 사용한 기술 스택&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot 3.2.1&lt;/li&gt;
&lt;li&gt;Spring Cloud 2023.0.0&lt;/li&gt;
&lt;li&gt;Kotlin + Gradle Kotlin&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://start.spring.io/&quot;&gt;&lt;b&gt;Spring Initializr&lt;/b&gt;&lt;/a&gt;에 접속해서 아래와 같이 사용할 버전과 Spring Cloud Function 의존성을 추가한 뒤 초기 프로젝트 구성을 다운로드 받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드한 프로젝트를 Intellij IDEA에 import 한 뒤 build.gradle.kts 를 열어 아래와 같이 설정합니다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
	id(&quot;org.springframework.boot&quot;) version &quot;3.2.1&quot;
	id(&quot;io.spring.dependency-management&quot;) version &quot;1.1.4&quot;
	id(&quot;com.github.johnrengelman.shadow&quot;) version &quot;7.1.2&quot;
	kotlin(&quot;jvm&quot;) version &quot;1.9.21&quot;
	kotlin(&quot;plugin.spring&quot;) version &quot;1.9.21&quot;
}

group = &quot;com.example&quot;
version = &quot;0.0.1-SNAPSHOT&quot;

java {
	sourceCompatibility = JavaVersion.VERSION_21
}

repositories {
	mavenCentral()
}

extra[&quot;springCloudVersion&quot;] = &quot;2023.0.0&quot;

dependencies {
	implementation(&quot;org.springframework.cloud:spring-cloud-function-web&quot;)
	implementation(&quot;org.springframework.cloud:spring-cloud-function-adapter-aws&quot;)
	implementation(&quot;com.fasterxml.jackson.module:jackson-module-kotlin&quot;)
	implementation(&quot;org.jetbrains.kotlin:kotlin-reflect&quot;)
	implementation(&quot;com.github.kittinunf.fuel:fuel:3.0.0-alpha1&quot;)

	testImplementation(&quot;org.springframework.boot:spring-boot-starter-test&quot;)
}

dependencyManagement {
	imports {
		mavenBom(&quot;org.springframework.cloud:spring-cloud-dependencies:${property(&quot;springCloudVersion&quot;)}&quot;)
	}
}

tasks.withType&amp;lt;KotlinCompile&amp;gt; {
	kotlinOptions {
		freeCompilerArgs += &quot;-Xjsr305=strict&quot;
		jvmTarget = &quot;21&quot;
	}
}

tasks.withType&amp;lt;Test&amp;gt; {
	useJUnitPlatform()
}

tasks.withType&amp;lt;Jar&amp;gt; {
	manifest {
		attributes[&quot;Start-Class&quot;] = &quot;com.example.function.FunctionApplicationKt&quot;
	}
}

tasks.assemble {
	dependsOn(&quot;shadowJar&quot;)
}

tasks.withType&amp;lt;com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar&amp;gt; {
	archiveClassifier.set(&quot;aws&quot;)
	archiveFileName.set(&quot;batch.jar&quot;)
	dependencies {
		exclude(&quot;org.springframework.cloud:spring-cloud-function-web&quot;)
	}
	mergeServiceFiles()
	append(&quot;META-INF/spring.handlers&quot;)
	append(&quot;META-INF/spring.schemas&quot;)
	append(&quot;META-INF/spring.tooling&quot;)
	append(&quot;META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports&quot;)
	append(&quot;META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports&quot;)
	transform(com.github.jengelman.gradle.plugins.shadow.transformers.PropertiesFileTransformer::class.java) {
		paths.add(&quot;META-INF/spring.factories&quot;)
		mergeStrategy = &quot;append&quot;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설정에서 핵심적인 부분은 shadowJar 입니다. &lt;a href=&quot;https://imperceptiblethoughts.com/shadow/introduction/&quot;&gt;shadowJar&lt;/a&gt;는 프로젝트의 공통 의존성을 하나의 jar안에 모두 결합하기 위해 사용하는 Gradle 플러그인입니다. 이런 형태를 일반적으로 fat-jar라고도 부릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;애플리케이션 개발&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;resources/application.yml 파일을 생성한 뒤 아래와 같이 작성한다. 이때 definition에 매핑되는 값은 Lambda를 통해 호출될 함수명과 일치해야합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;spring:
  cloud:
    function:
      definition: batchCaller
&lt;/code&gt;&lt;/pre&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;application.yml의 definition에 설정한대로 batchCaller 라는 이름의 함수를 Bean으로 구성한 애플리케이션입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.example.function

import com.fasterxml.jackson.databind.ObjectMapper
import fuel.Fuel
import fuel.get
import kotlinx.coroutines.runBlocking
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean

@SpringBootApplication
class FunctionApplication(
    private val objectMapper: ObjectMapper,
) {

    data class Payload(
        val url: String,
    )

    @Bean
    fun batchCaller(): (String) -&amp;gt; (String) {
        return { payloadAsString -&amp;gt;
            val payload = objectMapper.readValue(payloadAsString, Payload::class.java)

            runBlocking {
                val response = Fuel.get(payload.url)
                response.statusCode.toString()
            }

        }
    }

}

fun main(args: Array&amp;lt;String&amp;gt;) {
    runApplication&amp;lt;FunctionApplication&amp;gt;(*args)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제의 batchCaller Bean은 아래와 순서로 동작합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;EventBridge로부터 넘어온 JSON payload를 함수의 인자로 전달&lt;/li&gt;
&lt;li&gt;인자로 전달받은 JSON payload를 Kotlin의 data class인 Payload로 변환&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kittinunf/fuel&quot;&gt;Fuel client&lt;/a&gt;를 사용해 Payload.url을 그대로 호출. 이때 호출된 url은 서비스 API의&amp;nbsp; 엔드 포인트입니다.&amp;lt;/aside&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;aside&amp;gt;   Fuel client는 코루틴 기반의 경량 HTTP Client입니다. 예제는 Spring Web이나 Spring WebFlux 의존성이 없으므로 Spring에서 기본 제공하는 RestClient, WebClient등을 사용할 수 없고 서버리스 플랫폼에 올라가는 특성상 가벼운 Client가 유리하므로 Fuel을 선택했습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;애플리케이션 빌드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 최상위에서 shadowJar 플러그인를 사용해 애플리케이션을 jar로 빌드합니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;./gradlew shadowJar
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;빌드 완료 예시&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-01-07 오후 3.42.01.png&quot; data-origin-width=&quot;2484&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmPsuf/btsLA86voeD/gD9OuxIkv1q67KF2WsEfF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmPsuf/btsLA86voeD/gD9OuxIkv1q67KF2WsEfF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmPsuf/btsLA86voeD/gD9OuxIkv1q67KF2WsEfF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmPsuf%2FbtsLA86voeD%2FgD9OuxIkv1q67KF2WsEfF0%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;2484&quot; height=&quot;506&quot; data-filename=&quot;스크린샷 2024-01-07 오후 3.42.01.png&quot; data-origin-width=&quot;2484&quot; data-origin-height=&quot;506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드가 완료되면 프로젝트의 build/libs/ 디렉토리에 jar 파일이 생성됩니다. 예제의 경우 batch.jar 로 빌드 되게 설정된 상태입니다. batch.jar는 이후 Lambda에 코드를 업로드할 때 사용됩니다.&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;b&gt;AWS EventBridge와 Lambda에 애플리케이션 배포하기&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lambda에 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Lambda 콘솔에 접속해서 아래와 같이 함수를 생성합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;SCR-20240107-rews.png&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;909&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HQFQm/btsLAezqevA/VKTdwiYZDl3kFk90CAIdDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HQFQm/btsLAezqevA/VKTdwiYZDl3kFk90CAIdDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HQFQm/btsLAezqevA/VKTdwiYZDl3kFk90CAIdDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHQFQm%2FbtsLAezqevA%2FVKTdwiYZDl3kFk90CAIdDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;631&quot; height=&quot;909&quot; data-filename=&quot;SCR-20240107-rews.png&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;909&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;p data-ke-size=&quot;size16&quot;&gt;함수가 생성되면 코드를 업로드 합니다. 이때 S3 버킷에 링크를 연결하거나 .jar 파일을 직접 업로드할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.46.39.png&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;650&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mCOJR/btsLBVL9ZaC/TDXHycDNKb1zeJMdusnOwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mCOJR/btsLBVL9ZaC/TDXHycDNKb1zeJMdusnOwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mCOJR/btsLBVL9ZaC/TDXHycDNKb1zeJMdusnOwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmCOJR%2FbtsLBVL9ZaC%2FTDXHycDNKb1zeJMdusnOwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;631&quot; height=&quot;508&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.46.39.png&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;650&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;런타임 설정&lt;/b&gt;에서 &lt;b&gt;편집&lt;/b&gt;을 눌러 아래와 같이 설정을 변경합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.52.12.png&quot; data-origin-width=&quot;1616&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bccjID/btsLBYoAfSe/gvfgOjxKLsy35C2WoN3hj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bccjID/btsLBYoAfSe/gvfgOjxKLsy35C2WoN3hj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bccjID/btsLBYoAfSe/gvfgOjxKLsy35C2WoN3hj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbccjID%2FbtsLBYoAfSe%2FgvfgOjxKLsy35C2WoN3hj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;622&quot; height=&quot;610&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.52.12.png&quot; data-origin-width=&quot;1616&quot; data-origin-height=&quot;610&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;핸들러 설정을 org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest 로 변경해주셔야 정상적으로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정이 완료되었다면 정상적으로 동작하는지 테스트해 보겠습니다. &lt;b&gt;테스트&lt;/b&gt; 탭에 들어가서 이벤트 JSON을 수정합니다. 이벤트 JSON은 람다 함수로 전달되는 Payload이고 앞서 Spring Cloud Function 애플리케이션을 만들 때 data class로 만들었던 Payload 형태가 되어야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.54.30.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;869&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCaf1R/btsLCmJJD33/hKWkbbl3LmUjygRvfH782k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCaf1R/btsLCmJJD33/hKWkbbl3LmUjygRvfH782k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCaf1R/btsLCmJJD33/hKWkbbl3LmUjygRvfH782k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCaf1R%2FbtsLCmJJD33%2FhKWkbbl3LmUjygRvfH782k%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;651&quot; height=&quot;707&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.54.30.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;869&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;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.57.10.png&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;761&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kPnTq/btsLC1E5FtS/7wUbzNXqgRepgKYWHkYHr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kPnTq/btsLC1E5FtS/7wUbzNXqgRepgKYWHkYHr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kPnTq/btsLC1E5FtS/7wUbzNXqgRepgKYWHkYHr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkPnTq%2FbtsLC1E5FtS%2F7wUbzNXqgRepgKYWHkYHr1%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;634&quot; height=&quot;648&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.57.10.png&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;761&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 EventBridge의 스케쥴러와 앞서 설정한 Lambda 함수를 연결해주겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EventBridge 스케쥴러 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EventBridge 콘솔에 접속해 &lt;b&gt;일정 생성&lt;/b&gt; 버튼을 눌러 스케쥴러를 생성해보겠습니다. 예제의 경우 Rate 기반 일정을 사용해 1분 마다 Event가 발생하도록 설정했습니다. EventBridge는 Rate 기반 일정과 Cron 기반 일정을 같이 지원하므로 편리하게 스케쥴러를 설정할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.59.46.png&quot; data-origin-width=&quot;569&quot; data-origin-height=&quot;1177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qc5gS/btsLCEQVuDh/2OmLsRHIVpgiuimLC7CEtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qc5gS/btsLCEQVuDh/2OmLsRHIVpgiuimLC7CEtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qc5gS/btsLCEQVuDh/2OmLsRHIVpgiuimLC7CEtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqc5gS%2FbtsLCEQVuDh%2F2OmLsRHIVpgiuimLC7CEtK%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;491&quot; height=&quot;1016&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.59.46.png&quot; data-origin-width=&quot;569&quot; data-origin-height=&quot;1177&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;다음엔 EventBridge와 Lambda를 연결합니다. 페이로드에는 Lambda 테스트시 사용했던 것과 같이 Lambda에서 호출할 서비스 API 엔드포인트를 적어줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-01-07 오후 8.02.02.png&quot; data-origin-width=&quot;541&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y3ZGX/btsLDmaZhRn/vpKtR7dPUH6ka697r0Cr0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y3ZGX/btsLDmaZhRn/vpKtR7dPUH6ka697r0Cr0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y3ZGX/btsLDmaZhRn/vpKtR7dPUH6ka697r0Cr0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy3ZGX%2FbtsLDmaZhRn%2FvpKtR7dPUH6ka697r0Cr0K%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;541&quot; height=&quot;393&quot; data-filename=&quot;스크린샷 2024-01-07 오후 8.02.02.png&quot; data-origin-width=&quot;541&quot; data-origin-height=&quot;393&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;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS EventBridge와 Lambda에 애플리케이션을 배포하는 것은 서버리스 아키텍처를 활용하는 효과적인 방법입니다. 이 과정을 통해 개발자는 인프라 관리의 부담 없이 비즈니스 로직에 집중할 수 있으며, 빠르고 유연한 클라우드 애플리케이션 개발이 가능해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 JVM 언어만 사용해본 개발자들은 Lambda 기반의 서버리스 애플리케이션을 개발하는데 있어서 부담을 느끼는 상황을 많이 봐왔습니다. 주로 Lambda 함수를 Python과 Node.js로 만드는 경우가 많기 때문인데 Spring Cloud Function을 사용하면 Java나 Kotlin과 같은 JVM 언어에서도 빠르고 쉽게 서버리스 애플리케이션을 개발할 수 있습니다. 또한 Spring Cloud Function은 플랫폼에 상관없이 독립 실행형(Standalone) 애플리케이션을 만들어주므로 비즈니스 로직에 집중한 서비스를 만드는데 도움을 줍니다.&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;a href=&quot;https://github.com/digimon1740/spring-cloud-function-kotlin-aws-lambda&quot;&gt;https://github.com/digimon1740/spring-cloud-function-kotlin-aws-lambda&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-function/reference/adapters/aws-intro.html&quot;&gt;https://docs.spring.io/spring-cloud-function/reference/adapters/aws-intro.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html&quot;&gt;https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kittinunf/fuel&quot;&gt;https://github.com/kittinunf/fuel&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Kotlin</category>
      <author>devsh</author>
      <guid isPermaLink="true">https://devsh.tistory.com/156</guid>
      <comments>https://devsh.tistory.com/entry/Kotlin%EA%B3%BC-Spring-Cloud-Function-%EA%B8%B0%EB%B0%98-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98%EC%9D%84-AWS-EventBridge%EC%99%80-Lambda%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0#entry156comment</comments>
      <pubDate>Tue, 31 Dec 2024 23:09:09 +0900</pubDate>
    </item>
    <item>
      <title>Backend Engineer 채용 원칙</title>
      <link>https://devsh.tistory.com/entry/Backend-Engineer-%EC%B1%84%EC%9A%A9-%EC%9B%90%EC%B9%99</link>
      <description>&lt;h1&gt;Backend Engineer 채용 원칙&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;DALL&amp;amp;middot;E Cover Image Blog Software Hiring Nov 5.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m9him/btsLBgwRJJW/JgicQx2w6lqTOJE65fiFdK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m9him/btsLBgwRJJW/JgicQx2w6lqTOJE65fiFdK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m9him/btsLBgwRJJW/JgicQx2w6lqTOJE65fiFdK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm9him%2FbtsLBgwRJJW%2FJgicQx2w6lqTOJE65fiFdK%2Fimg.webp&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;604&quot; height=&quot;604&quot; data-filename=&quot;DALL&amp;middot;E Cover Image Blog Software Hiring Nov 5.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 회사에서 작성했던 Backend Engineer 채용 원칙을 공유합니다. 채용 원칙을 작성했던 이유는 당시 채용을 위해 팀에서 지원자의 이력을 검토하고 인터뷰를 진행하며 각자의 평가 기준이 달라 불필요한 커뮤니케이션 비용이 발생하고 몇 번의 좋은 지원자를 놓치는 상황이 발생해 조직 내부에서 기대하는 &lt;b&gt;필수 역량(Must Have)&lt;/b&gt; 및 &lt;b&gt;우대 조건(Nice To Have)을 정&lt;/b&gt;리해 팀원들과 인재상을 얼라인하고 지원자를 객관적으로 평가할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채용 원칙의 모든 내용은 저의 과거 경험과 몇개의 아티클을 참고했고 Java/Kotlin 기반의 Backend Engineer 채용을 목적으로 작성했지만 범용적인 역량을 기준으로 작성해 기술 스택이 다른 직군에서도 충분히 활용 가능할 것이라 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 &lt;b&gt;IC(Individual Contributor)&lt;/b&gt; 채용에 목적을 둔 원칙이므로 매니저를 채용할 때는 다른 기준이 필요합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;원칙&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;객관성을 유지하고 채용의 기준을 낮추지 않습니다.&lt;/li&gt;
&lt;li&gt;서류 스크리닝 과정에서 우려가 있다면 시간을 충분히 확보하여 &lt;b&gt;심도 있는 면접&lt;/b&gt;을 진행합니다.&lt;/li&gt;
&lt;li&gt;각 면접 단계에선 각 레벨의 기대 역량을 기준으로 검증합니다.&lt;/li&gt;
&lt;li&gt;지원자의 경험이 우리가 기대하는 경험과 얼마나 부합하는지 검토합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;주니어-미드 엔지니어 채용 기준&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;경력&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;3 ~ 8년차 엔지니어&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;기대 역량&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Must Have&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순수 백엔드 경험으로 최소 연차는 3년 이상 (타 직군 경험이 있어도 됨)&lt;/li&gt;
&lt;li&gt;Java/Spring , Kotlin/Spring을 활용한 백엔드 개발 경험&lt;/li&gt;
&lt;li&gt;제품 개발을 통해 비즈니스 임팩트를 내본 경험이 있으신 분&lt;/li&gt;
&lt;li&gt;협업 및 &lt;b&gt;프로젝트 주도 경험&lt;/b&gt;이 있으며, 다른 팀원과 원활하게 협력할 수 있는 분&lt;/li&gt;
&lt;li&gt;중/소규모 프로젝트를 리드하여 제품을 출시해본 경험을 가지신 분&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Nice To Have&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;소속팀에서 영향력있는 기술 결정을 내릴 수 있으신 분&lt;/li&gt;
&lt;li&gt;지속적인 학습과 공유를 하고 계신 분 (e.g. 블로그, 깃헙, 강의 등)&lt;/li&gt;
&lt;li&gt;플랫폼 엔지니어링 역량 또는 경험이 있으신 분 (e.g. 성능 최적화, 공통 플랫폼 개발, 트러블 슈팅)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;시니어 엔지니어 채용 기준&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;경력&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;8 ~ 20년차 엔지니어&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;기대 역량&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Must&lt;/b&gt; &lt;b&gt;Have&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순수 백엔드 경험으로 최소 연차는 8년 이상 (타 직군 경험이 있어도 됨)&lt;/li&gt;
&lt;li&gt;Java/Spring , Kotlin/Spring을 활용한 백엔드 개발 경험&lt;/li&gt;
&lt;li&gt;제품 개발을 통해 비즈니스 임팩트를 내본 경험이 있으신 분&lt;/li&gt;
&lt;li&gt;협업 및 &lt;b&gt;프로젝트 주도 경험&lt;/b&gt;이 있으며, 다른 팀원과 원활하게 협력할 수 있는 분&lt;/li&gt;
&lt;li&gt;플랫폼 엔지니어링 역량 또는 경험이 있으신 분 (e.g. 성능 최적화, 공통 플랫폼 개발, 트러블 슈팅)&lt;/li&gt;
&lt;li&gt;주니어/미드 레벨의 엔지니어를 멘토링하거나 도와준 경험&lt;/li&gt;
&lt;li&gt;소속팀과 회사 차원에서 영향력있는 기술 결정을 내릴 수 있으신 분&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Nice To Have&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지속적인 학습과 공유를 하고 계신 분 (e.g. 블로그, 깃헙, 강의 등)&lt;/li&gt;
&lt;li&gt;중/대규모 프로젝트를 리드하여 제품을 출시해본 경험을 가지신 분&lt;/li&gt;
&lt;li&gt;팀 매니징이나 리드 경력을 가지신 분&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;시간을 잘 맞추는 능력 (e.g. 일정 산정, 일정내에 프로젝트 완수)&lt;/li&gt;
&lt;li&gt;본인의 역할을 완벽하게 이해하고 수행하는 능력&lt;/li&gt;
&lt;li&gt;팀 전체에 긍정적인 영향을 줄 수 있는 능력&lt;/li&gt;
&lt;li&gt;회사 목표와 방향성을 이해하고 기여하는 능력&lt;/li&gt;
&lt;li&gt;다른 팀과 협력하여 팀과 회사의 목표를 달성하려는 자세&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주니어가 시니어에 대해 갖는 오해 몇 가지&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;시니어는 모든 것을 알고 있어야 한다.&lt;/li&gt;
&lt;li&gt;최신 기술을 모두 다루고 있어야 한다.&lt;/li&gt;
&lt;li&gt;많은 책임을 한꺼번에 감당할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;모든 결정은 시니어가 내린다.&lt;/li&gt;
&lt;li&gt;결정에 대해 반박할 수 없는 사람이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-22_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_2.05.26.png&quot; data-origin-width=&quot;368&quot; data-origin-height=&quot;355&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWZc3u/btsLBfx2AZy/94MuzIUi3r2kJ3Wm6fmk4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWZc3u/btsLBfx2AZy/94MuzIUi3r2kJ3Wm6fmk4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWZc3u/btsLBfx2AZy/94MuzIUi3r2kJ3Wm6fmk4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWZc3u%2FbtsLBfx2AZy%2F94MuzIUi3r2kJ3Wm6fmk4K%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;368&quot; height=&quot;355&quot; data-filename=&quot;%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-07-22_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_2.05.26.png&quot; data-origin-width=&quot;368&quot; data-origin-height=&quot;355&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.metridev.com/metrics/software-engineer-levels-understanding-the-different-roles/&quot;&gt;https://www.metridev.com/metrics/software-engineer-levels-understanding-the-different-roles/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vadimkravcenko.com/shorts/falsehoods-junior-developers-believe-about-becoming-senior/&quot;&gt;https://vadimkravcenko.com/shorts/falsehoods-junior-developers-believe-about-becoming-senior/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://engineeringbolt.com/tech/google-software-engineer-levels-roles-expectations-salary/#senior-software-engineer-l-5-manager-i-equivalent&quot;&gt;https://engineeringbolt.com/tech/google-software-engineer-levels-roles-expectations-salary/#senior-software-engineer-l-5-manager-i-equivalent&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>devsh</author>
      <guid isPermaLink="true">https://devsh.tistory.com/155</guid>
      <comments>https://devsh.tistory.com/entry/Backend-Engineer-%EC%B1%84%EC%9A%A9-%EC%9B%90%EC%B9%99#entry155comment</comments>
      <pubDate>Tue, 31 Dec 2024 23:01:48 +0900</pubDate>
    </item>
    <item>
      <title>코루틴 API 살펴보기</title>
      <link>https://devsh.tistory.com/entry/%EC%BD%94%EB%A3%A8%ED%8B%B4-API-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</link>
      <description>&lt;h1&gt;코루틴과 동시성&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코루틴은 매우 가볍다&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코루틴은 JVM의 플랫폼 스레드보다 리소스 집약도가 낮다. 즉, 훨씬 적은 메모리를 사용하여 더 많은 일을 할 수 있게된다.&lt;/li&gt;
&lt;li&gt;예제는 1,000,000개의 코루틴을 만들어서 각각 5초를 기다린 다음 마침표(.)을 출력하는 예제이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(1_000_000) { // launch a lot of coroutines
        launch {
            delay(5000L)
            print(&quot;.&quot;)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 예제) 테스트 PC 기준 4000개 정도 생성된 후 OOME가 발생한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;fun main() {
    repeat(1_000_000) { // launch a lot of threads
        thread {
            Thread.sleep(5000L)
            print(&quot;.&quot;)
        }
    }
    Thread.sleep(10000L)
}

[0.344s][warning][os,thread] Failed to start thread &quot;Unknown thread&quot; - pthread_create failed (EAGAIN) for attributes: stacksize: 2048k, guardsize: 16k, detached.
[0.344s][warning][os,thread] Failed to start the native thread for java.lang.Thread &quot;Thread-4074&quot;
Exception in thread &quot;main&quot; java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
	at java.base/java.lang.Thread.start0(Native Method)
	at java.base/java.lang.Thread.start(Thread.java:1526)
	at kotlin.concurrent.ThreadsKt.thread(Thread.kt:42)
	at kotlin.concurrent.ThreadsKt.thread$default(Thread.kt:20)
	at Example1Kt.main(example1.kt:17)
	at Example1Kt.main(example1.kt)
&lt;/code&gt;&lt;/pre&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;h3 data-ke-size=&quot;size23&quot;&gt;1. runBlocking&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;runBlocking은 코루틴을 생성하는 코루틴 빌더이다.&lt;/li&gt;
&lt;li&gt;runBlocking으로 감싼 코드는 코루틴 내부의 코드가 수행이 끝날때 까지 스레드가 블로킹된다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import kotlinx.coroutines.*

fun main() {
    
    runBlocking {
        println(&quot;Hello&quot;)
    }
     println(&quot;World&quot;)         
}

// Hello
// World
&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;일반적으로 코루틴은 스레드를 차단하지 않고 사용해야하므로 runBlocking을 사용하는 것은 좋지 않지만 꼭 사용해야하는 경우가 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코루틴을 지원하지 않는 경우 예) 테스트 코드, 스프링 배치 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;실행옵션에 -Dkotlinx.coroutines.debug 을 붙여주면 코루틴에서 수행되는 스레드는 이름 뒤에 @coroutine#1 이 붙어있는 것을 볼 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. launch&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;launch는 스레드 차단 없이 새 코루틴을 시작하고 결과로 job을 반환하는 코루틴 빌더이다&lt;/li&gt;
&lt;li&gt;launch는 결과를 만들어내지 않는 비동기 작업에 적합하기 때문에 Unit을 반환하는 람다를 인자로 받는다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() =  runBlocking&amp;lt;Unit&amp;gt; {
    launch {
        delay(500L)
        println(&quot;World!&quot;)
    }
    println(&quot;Hello&quot;)
}

// Hello
// World
&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;delay() 함수는 코루틴 라이브러리에 정의된 일시 중단 함수이며 Thread.sleep() 과 유사하지만 현재 스레드를 차단하지 않고 일시 중단 시킨다. 이때 일시 중단 된 스레드는 코루틴내에서 다른 일시 중단 함수를 수행한다&lt;/li&gt;
&lt;li&gt;launch를 사용해서 여러개의 작업을 동시에 수행할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

fun main() {

    runBlocking {
        launch {
            val timeMillis = measureTimeMillis {
                delay(150)
            }
            println(&quot;async task-1 $timeMillis ms&quot;)
        }

        launch {
            val timeMillis = measureTimeMillis {
                delay(100)
            }
            println(&quot;async task-2 $timeMillis ms&quot;)
        }

    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;launch가 반환하는 Job 을 사용해 현재 코루틴의 상태를 확인하거나 실행 또는 취소도 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val job1: Job = launch {
        val timeMillis = measureTimeMillis {
            delay(150)
        }
        println(&quot;async task-1 $timeMillis ms&quot;)
    }
    job1.cancel() // 취소 

		
    val job2: Job = launch(start = CoroutineStart.LAZY) {
        val timeMillis = measureTimeMillis {
            delay(100)
        }
        println(&quot;async task-2 $timeMillis ms&quot;)
    }

    println(&quot;start task-2&quot;)

    job2.start()

}
&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;job1.cancel() 을 호출해 코루틴을 취소할 수 있다&lt;/li&gt;
&lt;li&gt;launch(start = CoroutineStart.LAZY) 를 사용해서 start 함수를 호출하는 시점에 코루틴을 동작시킬 수 있다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;start 함수를 주석처리하면 launch가 동작하지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. async&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;async 빌더는 비동기 작업을 통해 결과를 만들어내는 경우에 적합하다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import kotlinx.coroutines.*

fun sum(a: Int, b: Int) = a + b

fun main() = runBlocking&amp;lt;Unit&amp;gt; {

    val result1: Deferred&amp;lt;Int&amp;gt; = async {
        delay(100)
        sum(1, 3)
    }

    println(&quot;result1 : ${result1.await()}&quot;)

    val result2: Deferred&amp;lt;Int&amp;gt; = async {
        delay(100)
        delay(100)
        sum(2, 5)
    }

    println(&quot;result2 : ${result2.await()}&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;async는 비동기 작업의 결과로 Deferred 라는 특별한 인스턴스를 반환하는데 await 이라는 함수를 통해 async로 수행한 비동기 작업의 결과를 받아올 수 있다&lt;/li&gt;
&lt;li&gt;자바 스크립트나 파이썬과 같이 다른 언어의 async-await은 키워드 인 경우가 보통이지만 코틀린의 코루틴은 async-await이 함수인 점이 차이점이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자바 스크립트의 async-await 예시)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function showAvatar() {

  // JSON 읽기
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();

  // github 사용자 정보 읽기
  let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
  let githubUser = await githubResponse.json();

  // 아바타 보여주기
  let img = document.createElement('img');
  img.src = githubUser.avatar_url;
  img.className = &quot;promise-avatar-example&quot;;
  document.body.append(img);

  // 3초 대기
  await new Promise((resolve, reject) =&amp;gt; setTimeout(resolve, 3000));

  img.remove();

  return githubUser;
}

showAvatar();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구조적 동시성&lt;/h2&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;h3 data-ke-size=&quot;size23&quot;&gt;suspend 함수&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;suspend 함수는 코루틴의 핵심 요소로써 일시 중단이 가능한 함수를 말한다&lt;/li&gt;
&lt;li&gt;suspend는 키워드이다&lt;/li&gt;
&lt;li&gt;suspend 함수는 일반 함수를 마음껏 호출할 수 있지만 일반 함수에선 suspend 함수를 호출할 수 없다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package structuredconcurrency

fun main() {
    printHello() // 컴파일 에러
}

suspend fun printHello() = println(&quot;hello&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Caller 함수에서 suspend 키워드를 붙여주면 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package structuredconcurrency

suspend fun main() {
    printHello()
}

suspend fun printHello() = println(&quot;hello&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일시 중단 함수는 IntelliJ 에서 suspension point가 표시된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;coroutineScope&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;runBlocking은 현재 스레드를 블로킹시키고 결과를 기다리지만 coroutineScope 는 스레드가 블로킹되지 않고 결과를 기다린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package structuredconcurrency

import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

suspend fun main() {
    doSomething()
}

private suspend fun doSomething() = coroutineScope {
    launch {
        delay(200)
        println(&quot;world!&quot;)
    }

    launch {
        println(&quot;hello&quot;)
    }

}
&lt;/code&gt;&lt;/pre&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;coroutineScope 내부의 자식 코루틴에서 에러가 발생하면 모든 코루틴이 종료된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private suspend fun doSomething() = coroutineScope {

    launch {
        delay(200)
        println(&quot;world!&quot;) // 실행안됨

    }

    launch {
        throw RuntimeException(&quot;error&quot;)
    }
}
Exception in thread &quot;DefaultDispatcher-worker-2&quot; java.lang.RuntimeException
	at structuredconcurrency.CoroutineScopeErrorKt$doSomething$2$2$1.invokeSuspend(coroutineScopeError.kt:21)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
	Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@4bbea199, Dispatchers.Default]

&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;코루틴 빌더 내부에서 try-catch로 핸들링&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;private suspend fun doSomething() = coroutineScope {

    launch {
        delay(200)
        println(&quot;world!&quot;)

    }

    launch {
        try {
            throw RuntimeException()
        } catch (e: Exception) {
            println(&quot;hello&quot;)
        }
    }
}

hello
world!
&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;supervisorScope 를 사용해서 예외를 부모 코루틴으로 전파하지않는 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private suspend fun doSomething() = coroutineScope {

    launch {
        delay(200)
        println(&quot;world!&quot;)

    }

    supervisorScope {
        launch {
            throw RuntimeException()
        }
    }
}

Exception in thread &quot;DefaultDispatcher-worker-2&quot; java.lang.RuntimeException
	at structuredconcurrency.CoroutineScopeErrorKt$doSomething$2$2$1.invokeSuspend(coroutineScopeError.kt:21)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
	Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@4bbea199, Dispatchers.Default]
world!
&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;코루틴의 예외 상황을 체계적으로 관리하고 싶을때 CEH(CoroutineExceptionHandler 를 사용한다&lt;/li&gt;
&lt;li&gt;supervisorScope 와 CoroutineExceptionHandler 함께 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;private suspend fun doSomething() = coroutineScope {

    launch {
        delay(200)
        println(&quot;world!&quot;)
    }

    supervisorScope {
        launch(handler) {
            throw RuntimeException()
        }
    }

}

val handler = CoroutineExceptionHandler { _, throwable -&amp;gt;
    println(&quot;Caught $throwable&quot;)
}

Caught java.lang.RuntimeException
world!
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;코루틴 채널&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;**Channel** 은 코루틴간의 통신을 위한 파이프라인이다&lt;/li&gt;
&lt;li&gt;기본 컨셉은 BlockingQueue 와 유사한 자료구조로 볼 수 있다.&lt;/li&gt;
&lt;li&gt;데이터 발신자가 채널에 send 로 데이터를 전달하고 수신 측에서 receive 를 사용해 채널로 부터 데이터를 받아온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BlockingQueue 를 사용해 주식 시세를 처리하는 처리하는 예제&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package channel

import kotlinx.coroutines.*
import java.util.concurrent.BlockingQueue
import java.util.concurrent.LinkedBlockingQueue
import kotlin.random.Random

fun main() = runBlocking {
    val queue: BlockingQueue&amp;lt;String&amp;gt; = LinkedBlockingQueue()

    // 생산자와 소비자 코루틴을 실행
    producer(queue, 20)
    consumer(queue)
}

suspend fun producer(queue: BlockingQueue&amp;lt;String&amp;gt;, count: Int) = coroutineScope {
    launch {
        repeat(count) {
            val stockPrice = &quot;APPLE : ${Random.nextInt(100, 200)}&quot;
            queue.put(stockPrice)
            delay(100L) // 0.1초 간격으로 데이터 생성
        }
    }
}

fun consumer(queue: BlockingQueue&amp;lt;String&amp;gt;) {
    for (stockPrice in queue) {
        println(&quot;Processed $stockPrice&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;put, take 가 모두 블로킹으로 이뤄진다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Channel 를 사용해 주식 시세를 처리하는 처리하는 예제&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package channel

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.util.concurrent.BlockingQueue
import java.util.concurrent.LinkedBlockingQueue
import kotlin.random.Random

fun main() = runBlocking {
    val channel: Channel&amp;lt;String&amp;gt; = Channel()

    // 생산자와 소비자 코루틴을 실행
    producer(channel, 20)
    consumer(channel)
}

suspend fun CoroutineScope.producer(channel: Channel&amp;lt;String&amp;gt;, count: Int) {
    launch {
        repeat(count) {
            val stockPrice = &quot;APPLE : ${Random.nextInt(100, 200)}&quot;
            channel.send(stockPrice)
            delay(100L) // 0.1초 간격으로 데이터 생성
        }
        channel.close()  // 모든 데이터를 보냈으면 채널을 닫습니다.
    }
}

suspend fun consumer(channel: Channel&amp;lt;String&amp;gt;) {
    for (stockPrice in channel) {
        println(&quot;Processed $stockPrice&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 송수신 연산(send, receive)이 모두 일시중단되므로 논-블로킹으로 이뤄진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;플로우 : 비동기 데이터 스트림&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;**Flow** 는 코루틴에서 리액티브 프로그래밍 스타일로 작성할 수 있도록 만들어진 비동기 스트림 API이다&lt;/li&gt;
&lt;li&gt;코루틴의 suspend 함수는 단일 값을 반환하지만 Flow를 사용하면 무한대 값을 반환할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;플로우 빌더&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;flow 빌더를 사용해 코드를 작성하고 emit 을 사용해 데이터를 전달한다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package flow

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val flow = simple()
    flow.collect { value -&amp;gt; println(value) }
}

fun simple(): Flow&amp;lt;Int&amp;gt; = flow {
    println(&quot;Flow started&quot;)
    for (i in 1..3) {
        delay(100)
        emit(i)
    }
}
&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;리액티브 스트림과 같이 Terminal Operator(최종 연산자) 인 collect 를 호출하지 않으면 아무런 일도 일어나지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flowOf 를 사용하면 여러 값을 인자로 전달해서 플로우를 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    flowOf(1, 2, 3, 4, 5).collect { value -&amp;gt;
        println(value)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;asFlow 를 사용하면 존재하는 컬렉션이나 시퀀스를 플로우로 변환할 수 있다&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    listOf(1, 2, 3, 4, 5).asFlow().collect { value -&amp;gt;
        println(value)
    }
    (6..10).asFlow().collect {
        println(it)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다양한 연산자 제공&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kotlinlang.org/docs/flow.html&quot;&gt;https://kotlinlang.org/docs/flow.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;리액티브 스트림 수준으로 다양한 연산자를 제공하진 못하고 있지만 일반적인 상황에선 충분하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package flow

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

suspend fun filterExample() {
    (1..10)
        .asFlow()
        .filter { (it % 2) == 0 }
        .collect {
            println(it)
        }
}

suspend fun takeExample() {
    (1..10)
        .asFlow()
        .take(2)
        .collect {
            println(it)
        }
}

suspend fun mapExample() {
    (1..10)
        .asFlow()
        .map { it * it }
        .collect {
            println(it)
        }
}

suspend fun zipExample() {
    val nums = (1..3).asFlow()
    val strs = listOf(&quot;one&quot;, &quot;two&quot;, &quot;three&quot;).asFlow()

    nums.zip(strs) { a, b -&amp;gt; &quot;$a -&amp;gt; $b&quot; }
        .collect { println(it) }
}

suspend fun flattenExample() {
    val left = flowOf(1, 2, 3)
    val right = flowOf(4, 5, 6)
    val flow = flowOf(left, right)

    flow.flattenConcat().collect { println(it) }
}
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Kotlin</category>
      <author>devsh</author>
      <guid isPermaLink="true">https://devsh.tistory.com/154</guid>
      <comments>https://devsh.tistory.com/entry/%EC%BD%94%EB%A3%A8%ED%8B%B4-API-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0#entry154comment</comments>
      <pubDate>Mon, 1 Jan 2024 19:56:46 +0900</pubDate>
    </item>
    <item>
      <title>스프링 웹플럭스와 코루틴 톺아보기</title>
      <link>https://devsh.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9B%B9%ED%94%8C%EB%9F%AD%EC%8A%A4%EC%99%80-%EC%BD%94%EB%A3%A8%ED%8B%B4-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0</link>
      <description>&lt;h1&gt;1. 비동기-논블로킹 프로그래밍&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전통적 웹 방식 (스프링 MVC)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전통적 웹 방식은 1개 요청당 1개의 스레드를 사용하는 &lt;b&gt;Thread per Request&lt;/b&gt; 모델&lt;/li&gt;
&lt;li&gt;요청 당 스레드를 사용하는 방식은 DB, Network IO 등이 발생할 경우 결과를 받기전까지 해당 스레드는 블로킹 상태가 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2021-06-03 오후 7.31.40.png&quot; data-origin-width=&quot;1776&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qQyzv/btrDMADdg7X/rSNuFlDi2X2rW0R1uG1GPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qQyzv/btrDMADdg7X/rSNuFlDi2X2rW0R1uG1GPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qQyzv/btrDMADdg7X/rSNuFlDi2X2rW0R1uG1GPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqQyzv%2FbtrDMADdg7X%2FrSNuFlDi2X2rW0R1uG1GPK%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;1776&quot; height=&quot;660&quot; data-filename=&quot;스크린샷 2021-06-03 오후 7.31.40.png&quot; data-origin-width=&quot;1776&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청을 처리하기 위해 스레드를 생성하고 전환하는데 따른 &lt;b&gt;Context Switching&lt;/b&gt; 비용이 발생&lt;/li&gt;
&lt;li&gt;요청에 따라 무한정 스레드를 생성할 수 없기 때문에 &lt;b&gt;스레드풀&lt;/b&gt;을 사용해 스레드를 재사용(톰캣 기본 200개)&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;h2 data-ke-size=&quot;size26&quot;&gt;비동기-논블로킹 방식&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IO(DB, File, Network) 처리 시 스레드가 대기하지 않고 처리가 완료되면 다양한 방식으로 이벤트를 통지받는 방식 (콜백, 프로미스, async-await 등)&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BOf7Q/btrDRi8Unuu/FXlQhZTUI5kFqchfIL0LB0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BOf7Q/btrDRi8Unuu/FXlQhZTUI5kFqchfIL0LB0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BOf7Q/btrDRi8Unuu/FXlQhZTUI5kFqchfIL0LB0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBOf7Q%2FbtrDRi8Unuu%2FFXlQhZTUI5kFqchfIL0LB0%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;692&quot; height=&quot;210&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;더 적은 스레드로 같은 일&lt;/b&gt;을 할 수 있기 때문에 Context Switching에 따른 비용이 훨씬 적다. 즉 하드웨어 자원을 더 적게 사용한다&lt;/li&gt;
&lt;li&gt;순차적으로 동작하지 않으므로 코드의 흐름을 파악하기 어렵고 &lt;b&gt;콜백헬(Callback Hell)&lt;/b&gt;로 인해 가독성과 유지보수성이 현저히 떨어짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bd8VCV/btrDMWtfHz7/u6PHUxhgId0mi2yKGMmnYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bd8VCV/btrDMWtfHz7/u6PHUxhgId0mi2yKGMmnYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bd8VCV/btrDMWtfHz7/u6PHUxhgId0mi2yKGMmnYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbd8VCV%2FbtrDMWtfHz7%2Fu6PHUxhgId0mi2yKGMmnYk%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;640&quot; height=&quot;344&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;C10K 문제&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴퓨터 과학에서 C10K 문제란 1만명의 사용자가 동시에 접속했을때(1만개의 소켓이 열림) 하드웨어가 충분함에도 기존의 I/O 통지 방식(e.g. select)으로는 제대로 처리하지 못하는 것을 말함&lt;/li&gt;
&lt;li&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;b&gt;Event-Driven&lt;/b&gt; 모델&lt;/span&gt;을 사용해 해결한다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커널 레벨에선 OS에서 제공하는 epoll, kqueue와 같은 통지 모델을 사용해 해결할 수 있다.&lt;/li&gt;
&lt;li&gt;프로그래머는 로우레벨을 직접 다루지 않고 Netty, Node.js, Nginx, Vert.x 등을 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S5FCz/btrDPVT61Lr/HNNq5fORDCmXfa3IzspPbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S5FCz/btrDPVT61Lr/HNNq5fORDCmXfa3IzspPbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S5FCz/btrDPVT61Lr/HNNq5fORDCmXfa3IzspPbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS5FCz%2FbtrDPVT61Lr%2FHNNq5fORDCmXfa3IzspPbK%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;1288&quot; height=&quot;482&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h1&gt;2. 리액티브 프로그래밍&lt;/h1&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;2010년 에릭마이어에 의해 마이크로소프트 .NET 에코 시스템으로 정의됨&lt;/li&gt;
&lt;li&gt;리액티브 프로그래밍은 기본적으로 옵저버 패턴 처럼 데이터 제공자가 소비하는 측에 데이터를 통지하는 푸시 기반&lt;b&gt;(Push-Based)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;구현체에 따라서 풀과 푸시를 모두 제공하는 경우도 있음 &lt;b&gt;(Pull-Push Hybrid)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;데이터의 통지, 완료, 에러 처리를 &lt;b&gt;옵저버 패턴&lt;/b&gt;에 영감을 받아 설계되었고 데이터의 손쉬운 비동기 처리를 위해 함수형 언어의 접근 방식을 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;옵저버 패턴&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관찰 대상이 되는 객체가 변경되면 대상 객체를 관찰하고 있는 &lt;b&gt;옵저버(Observer)&lt;/b&gt;에게 변경사항을 통지하는 디자인 패턴&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;직접 구현한 옵저버 패턴의 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import java.util.*

class Coffee(val name: String)


// 데이터 제공자
class Barista : Observable() {

    private fun makeCoffee(name: String) = Coffee(name)

    fun serve(name: String) {
         setChanged()
         notifyObservers(makeCoffee(name))
    }
}

// 데이터 구독자
class Customer : Observer {

    fun orderCoffee() = &quot;Iced Americano&quot;

    override fun update(o: Observable?, arg: Any?) {
         val coffee = arg as Coffee
         println(&quot;I got a cup of ${coffee.name}&quot;)
    }
}


fun main(args: Array&amp;lt;String&amp;gt;) {
    val barista = Barista()
    val customer = Customer()

    barista.addObserver(customer)
    barista.serve(customer.orderCoffee())
}

--------------------
출력 결과)
--------------------
I got a cup of Iced Americano&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리액티브 스트림&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JVM 환경에서 리액티브 프로그래밍의 표준 API 사양으로 &lt;b&gt;비동기 데이터 스트림&lt;/b&gt;과 &lt;b&gt;논블로킹-백프레셔(Back-Pressure)&lt;/b&gt;에 대한 사양을 제공&lt;/li&gt;
&lt;li&gt;리액티브 스트림 이전의 비동기식 애플리케이션에서는 멀티 코어를 제대로 활용하기 위해 복잡한 병렬 처리 코드가 필요&lt;/li&gt;
&lt;li&gt;처리할 데이터가 무한정 많아져서 시스템의 한계를 넘어서는 경우 애플리케이션은 &lt;b&gt;병목 현상(bottleneck)&lt;/b&gt;이 발생하거나 심각한 경우 애플리케이션이 정지되는 경우도 발생할 수 있음(논블로킹-백프레셔로 해결)&lt;/li&gt;
&lt;li&gt;Netflix, Vmware(Pivotal), Lightbend, Red Hat과 같은 유명 회사들이 표준화에 참여 중&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리액티브 스트림 인터페이스&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;377&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cug1tY/btrDRbaQtYq/kaJOJZ1uxhakq0C8L5wj51/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cug1tY/btrDRbaQtYq/kaJOJZ1uxhakq0C8L5wj51/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cug1tY/btrDRbaQtYq/kaJOJZ1uxhakq0C8L5wj51/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcug1tY%2FbtrDRbaQtYq%2FkaJOJZ1uxhakq0C8L5wj51%2Fimg.webp&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;957&quot; height=&quot;377&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;377&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;Publisher : 데이터를 생성하고 구독자에게 통지&lt;/li&gt;
&lt;li&gt;Subscriber : 데이터를 구독하고 통지 받은 데이터를 처리&lt;/li&gt;
&lt;li&gt;Subscription : Publisher, Subscriber간의 데이터를 교환하도록 연결하는 역할을 하며 전달받을 데이터의 개수와 구독을 해지할 수 있다&lt;/li&gt;
&lt;li&gt;Processor : Publisher, Subscriber을 모두 상속받은 인터페이스&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리액티브 스트림에서 Publisher와 Subscriber 간의 데이터 처리 흐름&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;377&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkrhxb/btrDP0ASm5Z/SAt6RnkvsTxa5IQrT2Wjl1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkrhxb/btrDP0ASm5Z/SAt6RnkvsTxa5IQrT2Wjl1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkrhxb/btrDP0ASm5Z/SAt6RnkvsTxa5IQrT2Wjl1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbkrhxb%2FbtrDP0ASm5Z%2FSAt6RnkvsTxa5IQrT2Wjl1%2Fimg.webp&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;957&quot; height=&quot;377&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;377&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;Project Reactor&lt;/li&gt;
&lt;li&gt;RxJava&lt;/li&gt;
&lt;li&gt;JDK9 Flow&lt;/li&gt;
&lt;li&gt;Akka Streams&lt;/li&gt;
&lt;li&gt;Vert.x&lt;/li&gt;
&lt;li&gt;Hibernate Reactive(Vert.x 사용)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h1&gt;3. 프로젝트 리액터&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프로젝트 리액터(Project Reactor)&lt;/b&gt;는 리액티브 스트림의 구현체 중 하나로 스프링의 에코시스템 범주에 포함된 프레임워크이다&lt;/li&gt;
&lt;li&gt;리액티브 스트림 사양을 구현하고 있으므로 리액티브 스트림에서 사용하는 용어와 규칙을 그대로 사용한다&lt;/li&gt;
&lt;li&gt;리액터를 사용하면 애플리케이션에 리액티브 프로그래밍을 적용할 수 있고 비동기-논블로킹을 적용할 수 있다&lt;/li&gt;
&lt;li&gt;함수형 프로그래밍의 접근 방식을 사용해서 비동기-논블로킹 코드의 난해함을 해결한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백프레셔(Backpressure)&lt;/b&gt; 를 사용해 시스템의 부하를 효율적으로 조절할 수 있다&lt;/li&gt;
&lt;li&gt;리액터는 리액티브 스트림의 &lt;b&gt;Publisher&lt;/b&gt; 인터페이스를 구현하는 &lt;b&gt;모노(Mono)&lt;/b&gt;와 &lt;b&gt;플럭스(Flux)&lt;/b&gt;라는 두 가지 핵심 타입을 제공한다&lt;/li&gt;
&lt;li&gt;두 타입 모두 리액티브 스트림 데이터 처리 프로토콜대로 onComplete 또는 onError 시그널이 발생할 때 까지 onNext를 사용해 구독자에게 데이터를 통지한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모노&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0..1개의 단일 요소 스트림을 통지하는 &lt;b&gt;발행자(Publisher)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;445&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fwhzr/btrDOT3Ab3A/kiaBYqRTXfJyTHc6Qz6Aj1/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fwhzr/btrDOT3Ab3A/kiaBYqRTXfJyTHc6Qz6Aj1/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fwhzr/btrDOT3Ab3A/kiaBYqRTXfJyTHc6Qz6Aj1/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFwhzr%2FbtrDOT3Ab3A%2FkiaBYqRTXfJyTHc6Qz6Aj1%2Ftfile.svg&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;851&quot; height=&quot;445&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;445&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;0..N개로 이뤄진 무한대 요소를 통지하는 발행자&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;907&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRRiEP/btrDPZBX1kB/DevKwIVAnK1hHqkkss4qMk/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRRiEP/btrDPZBX1kB/DevKwIVAnK1hHqkkss4qMk/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRRiEP/btrDPZBX1kB/DevKwIVAnK1hHqkkss4qMk/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRRiEP%2FbtrDPZBX1kB%2FDevKwIVAnK1hHqkkss4qMk%2Ftfile.svg&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;907&quot; height=&quot;417&quot; data-origin-width=&quot;907&quot; data-origin-height=&quot;417&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;연산자를 사용하기 위해서는 터미널 오퍼레이터인 &lt;b&gt;구독(subcribe)이 필수&lt;/b&gt;이다. 구독하지 않으면 연산자는 실행되지 않는다. (이 개념은 Java8 스트림과 유사하며 WebFlux에선 컨트롤러에서 Mono,Flux 타입 반환시 자동으로 해줌)&lt;/li&gt;
&lt;li&gt;CompletableFuture, DefferedResult, ListenableFuture 등 기존의 비동기 처리 방식은 구독 형태가 아니며, 단순한 기능만 가지고 있다 (async, join, callback 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h1&gt;4. 스프링 웹플럭스&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 Project Reactor 기반 (RxJava와 같은 다른 구현체도 사용 가능함)&lt;/li&gt;
&lt;li&gt;서블릿 기반인 스프링 MVC와 대비되는 리액티브 기반의 웹 스택&lt;/li&gt;
&lt;li&gt;스프링 MVC와 베타적으로 동작하지만 부분적으로 상호 운용이 가능함 예) WebClient, 애노테이션 등&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;800&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oX8nl/btrDRjmrNDG/SCK8MYDt9eJrbLYxWbOXi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oX8nl/btrDRjmrNDG/SCK8MYDt9eJrbLYxWbOXi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oX8nl/btrDRjmrNDG/SCK8MYDt9eJrbLYxWbOXi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoX8nl%2FbtrDRjmrNDG%2FSCK8MYDt9eJrbLYxWbOXi0%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;800&quot; height=&quot;446&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;446&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;내부적으로 블로킹 API가 있으면 성능이 현저하게 떨어짐&lt;/li&gt;
&lt;li&gt;예시로 Spring Data JPA는 내부에서 JDBC API를 사용하는데 JDBC 드라이버는 &lt;b&gt;블로킹 API (Thread per Connection)&lt;/b&gt;이므로 JPA를 쓰는 경우 Spring MVC 쓰는게 더 나은 옵션이다&lt;/li&gt;
&lt;li&gt;꼭 블로킹 API 써야한다면 아래와 같이 별도의 스케쥴러로 동작시켜야한다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val blockingWrapper = Mono.fromCallable {

        /* make a remote synchronous call */ 

}.subscribeOn(Schedulers.boundedElastic())&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리액터의 기능은 막강하고 가독성도 좋지만 제대로 쓰려면 &lt;b&gt;연산자(Operator)&lt;/b&gt;를 따로 공부해야함&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://projectreactor.io/docs/core/release/reference/index.html#which-operator&quot;&gt;https://projectreactor.io/docs/core/release/reference/index.html#which-operator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;그래도 모르겠으면 &lt;b&gt;마블 다이어그램&lt;/b&gt;을 보라는데.....&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVMzt8/btrDPBBAYvG/C153zbiQAukFrfwRKSk9lK/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVMzt8/btrDPBBAYvG/C153zbiQAukFrfwRKSk9lK/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVMzt8/btrDPBBAYvG/C153zbiQAukFrfwRKSk9lK/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVMzt8%2FbtrDPBBAYvG%2FC153zbiQAukFrfwRKSk9lK%2Ftfile.svg&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;829&quot; height=&quot;376&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;376&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;그 외에도 깊게 공부하면 &lt;b&gt;연산자 융합(operator-fusion)&lt;/b&gt;, &lt;b&gt;Hot-Cold 퍼블리셔&lt;/b&gt; 등등 공부할게 정말 많다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 웹플럭스의 코루틴 지원&lt;/h2&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-04-23 오전 1.39.22.png&quot; data-origin-width=&quot;1642&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lI4b3/btrDPBuRUf5/Iyjkm7um3a64Vk2Ct1zU9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lI4b3/btrDPBuRUf5/Iyjkm7um3a64Vk2Ct1zU9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lI4b3/btrDPBuRUf5/Iyjkm7um3a64Vk2Ct1zU9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlI4b3%2FbtrDPBuRUf5%2FIyjkm7um3a64Vk2Ct1zU9K%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;1642&quot; height=&quot;892&quot; data-filename=&quot;스크린샷 2022-04-23 오전 1.39.22.png&quot; data-origin-width=&quot;1642&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;코루틴(Coroutine)&lt;/b&gt;은 코틀린에서 비동기-논블로킹 프로그래밍을 명령형 스타일로 작성할 수 있도록 도와주는 라이브러리이다&lt;/li&gt;
&lt;li&gt;코루틴은 멀티 플랫폼을 지원하여 코틀린을 사용하는 안드로이드, 서버 등 여러 환경에서 사용할 수 있다&lt;/li&gt;
&lt;li&gt;코루틴은 &lt;b&gt;일시 중단 가능한 함수(suspend function)&lt;/b&gt; 를 통해 스레드가 실행을 잠시 중단했다가 중단한 지점부터 다시 &lt;b&gt;재개(resume)&lt;/b&gt; 할 수 있다&lt;/li&gt;
&lt;li&gt;다른 언어와는 다르게 코틀린의 코루틴은 특별하다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;코루틴을 사용한 구조적 동시성 예시&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1704611413571&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun combineApi() = coroutineScope {
	val response1 = async { getApi1() }
	val response2 = async { getApi2() }
	
	return ApiResult (
		response1.await()
		response2.await()
	)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리액티브가 코루틴으로 변환되는 방식&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;//Mono &amp;rarr; suspend 
fun handler(): Mono&amp;lt;Void&amp;gt; -&amp;gt; suspend fun handler()

//Flux &amp;rarr; Flow
fun handler(): Flux&amp;lt;T&amp;gt; -&amp;gt; fun handler(): Flow&amp;lt;T&amp;gt;&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;코루틴을 적용한 컨트롤러 코드&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1704611474104&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
class UserController(
	private val userService : UserService,
	private val userDetailService: UserDetailService
) {
	
	@GetMapping(&quot;/{id}&quot;)
	suspend fun get(@PathVariable id: Long) : User {
		return userService.getById(id)
	}
	
	@GetMapping(&quot;/users&quot;)
	suspend fun gets() = withContext(Dispatchers.IO) {
		val usersDeffered = async { userService.gets() }
		val userDetailsDeffered = async { userDetailService.gets() }
				
		return UserList(usersDeffered.await(), userDetailsDeffered.await())	
	}

}&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;Spring Data R2DBC는 아래와 같이 &lt;b&gt;CoroutineCrudRepository&lt;/b&gt;를 지원한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;기존 리액티브 스택의 Spring Data R2DBC, Spring Data Mongo Reactive 등에서&lt;/span&gt;&lt;span data-token-index=&quot;2&quot;&gt;&lt;b&gt; ReactiveRepository&lt;/b&gt; 로 개발된 코드에서 코루틴으로 변환한 예제 &lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;interface ContentReactiveRepository : ReactiveCrudRepository&amp;lt;Content, Long&amp;gt; {

    fun findByUserId(userId: Long) : Mono&amp;lt;Content&amp;gt;

    fun findAllByUserId(userId: Long): Flux&amp;lt;Content&amp;gt;
}


class ContentService (
	val repository : ContentReactiveRepository
) {

	
	fun findByUserIdMono(userId: Long) : Mono&amp;lt;Content&amp;gt; {
		return repository.findByUserId(userId)
	}	

	suspend findByUserId (userId: Long) : Content {
		return repository.findByUserId(userId).awaitSingle()
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;더 좋은 방법 : &lt;b&gt;CoroutineCrudRepository&lt;/b&gt; &lt;/span&gt;&lt;span data-token-index=&quot;3&quot;&gt;를 사용하면 &lt;b&gt;awaitXXX&lt;/b&gt;&lt;/span&gt;&lt;span data-token-index=&quot;5&quot;&gt; 확장 함수 없이 사용 가능&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1704611638979&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface ContentCouroutineRepository : CoroutineCrudRepository&amp;lt;Content, Long&amp;gt; {

    suspend fun findByUserId(userId:Long) : Content?

    fun findAllByUserId(userId: Long): Flow&amp;lt;Content&amp;gt;
}


class ContentService (
	val repository : ContentCouroutineRepository
) {


	suspend findByUserId (userId: Long) : Content {
		return repository.findByUserId(userId)
	}

}&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;h3 data-ke-size=&quot;size23&quot;&gt;Flow&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Flow&lt;/b&gt; 는 코루틴에서 리액티브 프로그래밍 스타일로 작성할 수 있도록 만들어진 API이다&lt;/li&gt;
&lt;li&gt;코루틴의 suspend 함수는 단일 값을 비동기로 반환하지만 Flow를 사용하면 무한대 값을 반환할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1704611684097&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking


fun main() = runBlocking&amp;lt;Unit&amp;gt; {
    val flow = simple()
    flow.collect { value -&amp;gt; println(value) }
}

fun simple(): Flow&amp;lt;Int&amp;gt; = flow {
    println(&quot;Flow started&quot;)
    for (i in 1..3) {
        delay(100)
        emit(i)
    }
}

// Flow started
// 1
// 2
// 3&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;리액티브 스트림과 같이 &lt;b&gt;Terminal Operator(최종 연산자)&lt;/b&gt; 인 &lt;b&gt;collect&lt;/b&gt; 를 호출하지 않으면 아무런 일도 일어나지 않는다&lt;/li&gt;
&lt;li&gt;코루틴의 플로우는 리액티브 스트림에서 영감을 받아 제작
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리액터의 플럭스(Flux)는 Pull-Push Hybrid&lt;/li&gt;
&lt;li&gt;코틀린의 플로우(Flow)는 Push Only&lt;/li&gt;
&lt;li&gt;아직은 리액터 플럭스에 비해 기능이 적다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 MVC에서도 리액티브와 코루틴을 쓸수 있다&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코루틴, 리액티브를 쓰고 싶은 경우 의존성 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;implementation(&quot;org.jetbrains.kotlinx:kotlinx-coroutines-core&quot;)
implementation(&quot;org.jetbrains.kotlinx:kotlinx-coroutines-reactor&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebClient와 같은 비동기-논블로킹 라이브러리가 필요하다면 의존성 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;implementation(&quot;org.springframework.boot:spring-boot-starter-webflux&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹플럭스를 바로 도입하기 어렵거나 MVC에서 비동기-논블로킹 구현을 우아하게 작성하고 싶은 경우 이 방법을 추천&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;DEMO&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예제 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/digimon1740/backend-techtalk&quot;&gt;https://github.com/digimon1740/backend-techtalk&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;부하 테스트 시나리오&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Mock 데이터를 응답하는 유저 API 서버가 존재함&lt;/li&gt;
&lt;li&gt;유저 API 서버는 API 서버로 부터 요청을 받으면 응답하는데까지 2초가 소요된다&lt;/li&gt;
&lt;li&gt;유저 API 서버가 최초 실행되면 균형있는 테스트를 위해 warm-up을 수행한다&lt;/li&gt;
&lt;li&gt;10초 동안 MVC, WebFlux 서버에 아래 조건으로 요청을 보내고 Vegeta 리포트와, VisualVM 모니터를 확인한다&lt;/li&gt;
&lt;li&gt;100/s 요청 전송&lt;/li&gt;
&lt;li&gt;200/s 요청 전송&lt;/li&gt;
&lt;li&gt;300/s 요청 전송&lt;/li&gt;
&lt;li&gt;1000/s 요청 전송&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704611775876&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# MVC test
echo 'GET http://localhost:8080/mvc/users/1?delay=2000' | vegeta attack -duration=10s -rate=300/s | vegeta report&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;pre id=&quot;code_1704611784513&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# WebFlux test
echo 'GET http://localhost:8081/webflux/users/1?delay=2000' | vegeta attack -duration=10s -rate=300/s | vegeta report&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h1&gt;6. 정리&lt;/h1&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;li&gt;스프링 MVC는 느리고 구리다? 스프링 웹플럭스는 빠르고 멋지다?&lt;/li&gt;
&lt;li&gt;일반적으론 스프링 MVC도 충분하지만 한정된 자원에서 대량의 트래픽이 요구되는 서비스거나 비동기-논블로킹 IO가 필요한 경우 추천&lt;/li&gt;
&lt;li&gt;기존 스프링 MVC 프로젝트에 스프링 웹플럭스 의존성을 추가해서 써보는 것도 좋은 방법이다&lt;/li&gt;
&lt;li&gt;블로킹 API(JDBC, RestTemplate 등)을 쓸거면 그냥 마음 편히 스프링 MVC로 개발하자&lt;/li&gt;
&lt;li&gt;코틀린 + 웹플럭스 조합의 경우 메인은 코루틴을 사용하고 필요에 따라 리액티브를 같이 적용하는 것을 추천&lt;/li&gt;
&lt;li&gt;코루틴의 지원 범위를 넘어선 기능을 찾거나 연산자에 대한 학습이 충분하다면 리액티브는 여전히 매력적인 기술이다&lt;/li&gt;
&lt;li&gt;새로운 패러다임을 공부하는 것은 항상 어렵지만 도전했을때 얻는 것도 많다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;은탄환은 없다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;레퍼런스&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://devsh.tistory.com/category/Reactive%20Programming&quot;&gt;https://devsh.tistory.com/category/Reactive Programming&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tech.lezhin.com/2020/07/15/kotlin-webflux&quot;&gt;https://tech.lezhin.com/2020/07/15/kotlin-webflux&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/digimon1740/webflux-demo&quot;&gt;https://github.com/digimon1740/webflux-demo&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/index.html&quot;&gt;https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/index.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/languages.html#coroutines&quot;&gt;https://docs.spring.io/spring-framework/docs/current/reference/html/languages.html#coroutines&lt;/a&gt;&lt;/p&gt;</description>
      <category>Reactive Programming</category>
      <author>devsh</author>
      <guid isPermaLink="true">https://devsh.tistory.com/153</guid>
      <comments>https://devsh.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9B%B9%ED%94%8C%EB%9F%AD%EC%8A%A4%EC%99%80-%EC%BD%94%EB%A3%A8%ED%8B%B4-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0#entry153comment</comments>
      <pubDate>Thu, 2 Jun 2022 19:33:52 +0900</pubDate>
    </item>
    <item>
      <title>프로젝트 리액터 고급 활용</title>
      <link>https://devsh.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%EC%95%A1%ED%84%B0-%EA%B3%A0%EA%B8%89-%ED%99%9C%EC%9A%A9</link>
      <description>&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://devsh.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%EC%95%A1%ED%84%B0-%EA%B8%B0%EC%B4%88-1-%EB%AA%A8%EB%85%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;프로젝트 리액터 기초 1 - 모노&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devsh.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%EC%95%A1%ED%84%B0-%EA%B8%B0%EC%B4%88-2-%ED%94%8C%EB%9F%AD%EC%8A%A4&quot;&gt;프로젝트 리액터 기초 2 - 플럭스&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로젝트 리액터 고급 활용&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Hot vs Cold&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리액터에는 두 개의 발행자 타입이 존재하는데 바로 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Hot 발행자&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;와&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; Cold 발행자&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;입니다. 먼저 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Cold 발행자&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;는 각각의 구독에 대해 새로운 데이터를 생성하는 구조입니다. 발행자에 대한 구독이 이뤄지지 않으면 데이터는 결코 새롭게 생성되지 않습니다. Cold 발행자에 대한 예를 들어보면 HTTP 요청이 Cold에 해당합니다. HTTP 요청은 클라이언트로부터 새로운 요청이 발생할 때마다 데이터를 생성하여 응답하는데 이때 여러 개의 클라이언트에서 요청이 발생하더라도 동일한 요청이라면 동일한 데이터 구조를 새로 응답할 것입니다. 또 다른 예로 넷플릭스에서 여러 사용자가 동일한 드라마 회차를 감상하는 경우 사용자들은 자신만의 타임라인을 가집니다. 즉 사용자별로 드라마를 처음 재생하면 처음부터 재생되고 재생을 시작한 시점부터 현재 보고 있는 시점이 각각 다른데 이러한 것들을 Cold 방식으로 볼 수 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.36은 동일한 플럭스를 3개의 구독자가 구독하여도 동일한 데이터를 통지하는 Cold 발행자의 특징을 보여줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.36 Cold 발행자 예제 - reactor/reactor-basic/src/hotcold/Cold.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642000256816&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package hotcold
 
import reactor.core.publisher.Flux
 
fun main() {
    val list = listOf(&quot;one&quot;, &quot;two&quot;, &quot;three&quot;)
    val flux: Flux&amp;lt;String&amp;gt; = Flux.fromIterable(list)
 
    flux.subscribe { println(&quot;Subscriber-1 : $it&quot;) }
    flux.subscribe { println(&quot;Subscriber-2 : $it&quot;) }
    flux.subscribe { println(&quot;Subscriber-3 : $it&quot;) }
}
 
--------------------
출력 결과)
--------------------
Subscriber-1 : one
Subscriber-1 : two
Subscriber-1 : three
Subscriber-2 : one
Subscriber-2 : two
Subscriber-2 : three
Subscriber-3 : one
Subscriber-3 : two
Subscriber-3 : three&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출력된 결과를 확인해보면 구독자가 각각 동일한 데이터를 통지받은 것을 확인할 수 있습니다. 즉 각각의 구독자가 새로운 구독을 만들었다는 것을 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Hot 발행자&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;는 구독자의 상태에 상관없이 데이터를 지속적으로 제공합니다. 예를 들어 주식 데이터를 제공하는 API의 경우 실시간으로 최신 데이터를 제공합니다. 국내 주식의 경우 정규장으로 오전 9시부터 시작됩니다. 만약 출근 후에 주식 정보를 보고 싶어서 네이버에 접속해 오전 10시에 주식 정보를 확인한다면 정규장이 시작된 오전 9시의 가격이 아닌 오전 10시의 주식가격을 확인하게 됩니다. 또 다른 예로 유투브의 실시간 스트리밍을 들 수 있습니다. 유저가 구독 중인 크리에이터의 실시간 스트리밍을 1시간이 지난 뒤 입장하면 당연히 처음부터 보는 게 아니라 실시간으로 크리에이터와 상호작용하게 됩니다. 이렇게 나중에 구독에 참가한 구독자는 처음부터 새로운 데이터를 얻는 게 아닌 새로이 구독한 시점부터 데이터를 제공받는 개념을 Hot 방식이라고 합니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.37은 Hot 발행자인 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커넥터블 플럭스(ConnectableFlux)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 사용하여 각각의 구독자를 구독한 시점부터 데이터를 통지받도록 연결하여 출력하는 예제입니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.37 Hot 발행자 예제 - reactor/reactor-basic/src/hotcold/Hot.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642000273971&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package hotcold
 
import reactor.core.publisher.ConnectableFlux
import reactor.core.publisher.Flux
import java.time.Duration
 
 
fun main() {
    val flux: ConnectableFlux&amp;lt;Int&amp;gt; =
        Flux.range(1, 5)
            .delayElements(Duration.ofMillis(100))
            .publish()
 
    flux.connect()
 
    flux.subscribe { println(&quot;Subscriber-1 : $it&quot;) }
 
    Thread.sleep(Duration.ofMillis(200).toMillis())
    flux.subscribe { println(&quot;Subscriber-2 : $it&quot;) }
 
    Thread.sleep(Duration.ofMillis(200).toMillis())
    flux.subscribe { println(&quot;Subscriber-3 : $it&quot;) }
 
    Thread.sleep(Duration.ofSeconds(2).toMillis())
}
 
--------------------
출력 결과)
--------------------
Subscriber-1 : 1
Subscriber-1 : 2
Subscriber-2 : 2
Subscriber-1 : 3
Subscriber-2 : 3
Subscriber-1 : 4
Subscriber-2 : 4
Subscriber-3 : 4
Subscriber-1 : 5
Subscriber-2 : 5
Subscriber-3 : 5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;span style=&quot;color: #000000;&quot;&gt;range(1, 5)를 사용해 1부터 5까지의 플럭스를 생성하고 delayElements(Duration.ofMillis(100)) 함수를 사용해 각각의 데이터를 100밀리초 지연 후 publish로 데이터를 통지합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 flux.connect()가 없으면 발행자를 구독을 하더라도 아무 일도 일어나지 않습니다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3개의 구독자인 flux.subscribe{}는 다음 구독자로 넘어가는 시점을 200밀리초 뒤로 미루고 현재 시점에서 통지 받은 데이터를 출력합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출력 결과를 확인해보면 Subscriber-1은 1부터 5까지의 데이터를 모두 전달받았지만 Subscriber-2와 Subscriber-3은 지연된 만큼 데이터를 전달받지 못한 것을 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞서 커넥터블 플럭스를 사용해 Hot 발행자의 동작 방식에 대해 설명했습니다. 커넥터블 플럭스는 대표적인 Hot 발행자로 하나의 데이터를 생성하는 발행자에 여러 구독자가 동시에 구독하는 것을 허용합니다. 특징은 일반적인 Cold 방식의 발행자와 다른 점은 구독자가 구독을 위해 subscribe를 호출하더라도 데이터를 통지하지 않고 connect 연산자를 호출해야 비로소 데이터를 통지합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Cold 발행자인 플럭스를 커넥터블 플럭스로 변환하는 방법에는 2가지가 있습니다. 표 1.1은 커넥터블 플럭스로 변환하기 위한 연산자에 대해 설명합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;표 1.1 플럭스를 커넥터블 플럭스로 변환하는 연산자&lt;/span&gt;&lt;/p&gt;
&lt;div&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;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;함수&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설명&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;publish&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구독자가 연결된 시점 부터 새롭게 생성되는 데이터를 통지하는 커넥터블 플럭스를 제공하는 연산자입니다.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;replay&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인자로 지정한 만큼의 데이터를 캐시하고 있으면서 새롭게 구독자가 연결되어 구독을 시작하는 경우 시존에 캐시 된 데이터를 먼저 통지하고 캐시 된 데이터 데이터가 없다면 구독 이후로 생성된 데이터를 통지하는 연산자입니다.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생성된 커넥터블 플럭스는 connect와 같이 구독에 대한 추가적인 처리를 위한 방법도 제공합니다. 표 1.2는 커넥터블 플럭스가 제공하는 연산자에 대해 설명합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;표 1.2 커넥터블 플럭스가 제공하는 연산자&lt;/span&gt;&lt;/p&gt;
&lt;div&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;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;함수&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설명&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;connect&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커넥터블 플럭스에 대한 구독을 활성화하기 위해 수동으로 호출하여 사용합니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;autoConnect&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;함수에 지정된 인자의 개수만큼 구독이 연결되면 자동으로 데이터를 통지합니다.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;refCount&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커넥터블 플럭스를 새로운 플럭스로 변환하고 함수에 지정된 인자의 구독자 개수만큼 연결된 구독자가 있을 경우에만 데이터를 처음부터 통지하고 새롭게 연결된 구독자에게는 구독 이후로 생성된 데이터를 통지합니다.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;백프레셔&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;백프레셔(Back-Pressure)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;는 리액티브 스트림의 핵심 사양 중 하나입니다. 백프레셔는 데이터를 소비하는 측인 구독자가 자체적으로 처리 가능한 만큼의 데이터를 발행자에게 역으로 요청(request)합니다. 데이터를 소비하는 속도 즉 처리 속도보다 데이터를 생성하고 통지하는 속도가 빠르다면 구독자 입장에선 압박(pressure)이 될 수 있습니다. 백프레셔는 이런 상황에서 구독자가 처리 가능한 만큼 데이터를 통지할 수 있도록 조절해주는 기능입니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서비스를 예를 들어 동시 접속자가 최대 100명인 시스템에 1000명이 동시에 접속한다면 장애가 발생할 수 있습니다. 이런 경우 인프라를 증설하여 처리해야 하지만 예상치 못한 동시 접속자 증가는 대응하기가 쉽지 않습니다. 이럴 때 서버 앞단에서 요청에 대한 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요청 제한(Rate Limiting)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;을 적용해 HTTP 상태 코드 429 Too Many Requests를 반환하여 처리하면 최악의 상황인 서비스 장애는 방지할 수 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.38은 Flux.range를 사용하여 1부터 10까지 순차적으로 데이터를 통지하고 구독자(Subscriber)를 직접 구현하여 요청을 조절하는 간단한 백프레셔를 구현하는 예제입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.38 백프레셔 예제 - reactor/reactor-basic/src/backpressure/Backpressure.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001235907&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package backpressure
 
import org.reactivestreams.Subscriber
import org.reactivestreams.Subscription
import reactor.core.publisher.Flux
 
fun main() {
 
    Flux.range(1, 10)
        .log()
        .subscribe(object : Subscriber&amp;lt;Int?&amp;gt; {
            private var s: Subscription? = null
            private var count = 0
 
            override fun onSubscribe(s: Subscription) {
                println(&quot;onSubscribe : 구독 시작&quot;)
                println(&quot;==================&quot;)
                this.s = s
                s.request(5)
            }
 
            override fun onNext(integer: Int?) {
                count++
                if (count % 5 == 0) {
                    println(&quot;==================&quot;)
                    s!!.request(5)
                }
            }
 
            override fun onError(t: Throwable) {}
            override fun onComplete() {
                 println(&quot;==================&quot;)
                 println(&quot;onComplete : 구독 완료&quot;)
                 println(&quot;통지된 numbers 개수 : $count&quot;)
            }
        })
}
 
--------------------
출력 결과)
--------------------
onSubscribe : 구독 시작
==================
[ INFO] (main) | request(5)
[ INFO] (main) | onNext(1)
[ INFO] (main) | onNext(2)
[ INFO] (main) | onNext(3)
[ INFO] (main) | onNext(4)
[ INFO] (main) | onNext(5)
==================
[ INFO] (main) | request(5)
[ INFO] (main) | onNext(6)
[ INFO] (main) | onNext(7)
[ INFO] (main) | onNext(8)
[ INFO] (main) | onNext(9)
[ INFO] (main) | onNext(10)
==================
[ INFO] (main) | request(5)
[ INFO] (main) | onComplete()
==================
onComplete : 구독 완료
최종 통지된 numbers 개수 : 10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;subscribe 함수의 인자에는 리액티브 스트림의 구독자 인터페이스인 Subscriber를 직접 구현하였습니다. 우선 구독이 시작되면서 최초에 한번 실행되는 onSubscribe 함수에서는 Subscription을 초기화하고 s.request(5)를 호출해서 발행자에게 request에 전달받은 만큼의 데이터를 통지하도록 요청합니다. 그다음 호출되는 onNext는 발행자가 생성한 데이터를 담아 통지 신호가 발생할 때마다 호출됩니다. onNext 내부에는 데이터 통지 횟수를 count 변수에 기록하고 count가 5의 배수인 경우에 request를 호출해서 발행자에게 데이터 통지를 요청합니다. 마지막으로 onComplete는 구독에 대한 통지가 완료되었을 때 동작합니다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출력 결과를 확인해보면 request(5)로 요청한 대로 onNext(1).. onNext(5)까지 순서대로 통지된 후 다음엔 onNext(6).. onNext(10)까지 차례로 통지된 것을 알 수 있습니다. 또한 더이상 통지할 데이터가 없는 경우에는 request(5)를 요청했지만 onNext가 호출되지 않고 onComplete 시그널이 호출되며 구독이 종료되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리액터로 작성한 코드를 테스트하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자동화된 테스트를 작성하는 것은 프로그램의 잠재된 버그의 발견을 쉽게 하고 코드의 품질을 높여줍니다. 그러나 비동기-논 블로킹 형태의 코드는 테스트가 번거롭고 예측하기 어렵습니다. 예를 들어 여러 스레드를 동시에 생성한 후 각각 다른 작업을 한 후 합친 결과에 대해 테스트를 하거나 시간차로 동작하는 코드는 자동화된 테스트를 어렵게 하는 요인으로 알려져 있지만 리액터는 자동화된 테스트를 위한 여러 가지 도구로써 reactor-test라는 라이브러리를 제공합니다. 표 1.3은 reactor-test에서 제공하는 핵심 3가지 패키지에 대한 설명입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;표 1.3 reactor-test가 제공하는 패키지&lt;/span&gt;&lt;/p&gt;
&lt;div&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;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;함수&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설명&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;reactor.test&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;발행자에 대한 검증 및 테스트를 위한 메인 테스트 컴포넌트 패키지&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;reactor.test.publisher&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트 가능한 발행자의 생성을 위한 컴포넌트 패키지&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;reactor.test.scheduler&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트 가능한 스케쥴러의 생성을 위한 컴포넌트 패키지&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;StepVerifier&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리액터 테스트 패키지에는 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;StepVerifier&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;라는 리액티브 스트림의 각 단계를 테스트하기 위한 API를 제공합니다.&amp;nbsp; StepVerifier를 사용하면 데이터 통지와 구독에 대한 조건이 예상한 결과를 만족하는지를 판단하는 테스트 시나리오를 작성할 수 있으므로 편리합니다. 또한 다음에 예상되는 동작과 주어진 시간 동안 딜레이를 주는 등의 다양한 테스트를 가능하게 합니다. StepVerifier는 빌더 패턴 형태로 제공되는데 빌더 패턴을 적용한 함수의 반환값은 this 즉 자기 자신이기 때문에 함수를 연쇄적으로 호출할 수 있다는 장점이 있습니다. 이런 기법을 메소드 체이닝(Method Chaining)이라고 부릅니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;expectNext&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리액터가 제공하는 테스트 도구를 사용하기 위해 우선 코틀린 그레이들 프로젝트를 생성합니다. 정상적으로 생성되었다면 아래와 같은 구조로 프로젝트가 생성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001276541&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;├── build.gradle.kts
├── gradlew
├── settings.gradle.kts
└── src
    ├── main
    │   ├── kotlin
    │   └── resources
    └── test
        ├── kotlin
        └── resources&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로젝트가 정상적으로 생성되었다면 build.gradle.kts를 열어 예제 1.39와 같이 작성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.39 build.gradle.kts - reactor/reactor-test/build.gradle.kts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001292011&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
plugins {
    kotlin(&quot;jvm&quot;) version &quot;1.4.21&quot;
}
 
group = &quot;com.reactor.reactortest&quot;
version = &quot;1.0-SNAPSHOT&quot;
 
repositories {
    mavenCentral()
}
 
dependencies {
    implementation(&quot;org.jetbrains.kotlin:kotlin-stdlib&quot;)
    implementation(&quot;io.projectreactor:reactor-core:3.4.0&quot;)
  
    testImplementation(kotlin(&quot;test-junit5&quot;))
    testImplementation(&quot;org.junit.jupiter:junit-jupiter-api:5.6.0&quot;)
    testRuntimeOnly(&quot;org.junit.jupiter:junit-jupiter-engine:5.6.0&quot;)
    testImplementation(&quot;io.projectreactor:reactor-test:3.4.0&quot;)
}
 
tasks.test {
    useJUnitPlatform()
}
 
tasks.withType&amp;lt;KotlinCompile&amp;gt;() {
    kotlinOptions.jvmTarget = &quot;11&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제에서는 리액터 테스트를 위한 reactor-test 라이브러리와 유닛 테스트 프레임워크인 JUnit5를 의존성에 추가했습니다. 그다음 test/kotlin 디렉토리 하위에 ReactorTest1 클래스를 생성한뒤 예제 1.40과 같이 첫번째 테스트 코드를 작성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.40 첫번째 테스트 - reactor/reactor-test/src/test/kotlin/ReactorTest1.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001303092&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.junit.jupiter.api.Test
import reactor.core.publisher.Flux
import reactor.test.StepVerifier
 
class ReactorTest1 {
 
    @Test
    fun `짝수로 된 데이터만 통지해야 한다`() {
        val flux = Flux.just(1, 2, 3, 4)
            // rem은 나머지를 구하는 %와 같다.
            .filter { num -&amp;gt; num.rem(2) == 0 }
 
        StepVerifier
            .create(flux)
            .expectNext(2)
            .expectNext(4)
            .expectComplete()
            .verify()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.40는 Flux.just의 인자에 1부터 4까지 지정하고 filter 연산자에서 통지된 정수 값을 2로 나눈 나머지가 0인 데이터만 필터링하므로 짝수인 2, 4만 통지됩니다. 그다음 테스트의 핵심인 StepVerifier는 첫 단계로 create(publisher) 연산자를 사용해 초기화합니다. StepVerifier가 초기화되면 expect로 시작하는 연산자를 사용해 단계별로 검증이 가능합니다. 가장 먼저 사용된 expectNext는 onNext로 통지받은 데이터가 인자로 지정된 값과 일치하는지를 검증합니다. 더 이상 검증할 값이 없다면 expectComplete 연산자로 OnComplete 신호를 통지합니다. 그다음 최종적으로 verify 함수를 호출하면 테스트 시나리오에 대한 검증이 완료됩니다. 검증이 정상이라면 테스트를 통과할 것이고 검증이 잘못됬다면 테스트가 실패할 것 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;recordWith, expectNextCount, expectRecordedMatches&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.41은 여러개의 과일을 가진 플럭스를 생성하고 과일의 개수와 특정한 과일이 존재하는지 검증하는 예제입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.41 reocrdWith 관련 테스트 - reactor/reactor-test/src/test/kotlin/ReactorTest2.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001324400&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.junit.jupiter.api.Test
import reactor.core.publisher.Flux
import reactor.test.StepVerifier
import java.util.ArrayList
 
class ReactorTest2 {
 
    @Test
    fun `사과가 들어있는지 확인`() {
        val flux: Flux&amp;lt;String&amp;gt; = Flux.just(&quot;사과&quot;, &quot;바나나&quot;, &quot;딸기&quot;)
 
        StepVerifier
            .create(flux)
            .recordWith { ArrayList() }
            .expectNextCount(3)
            .expectRecordedMatches { fruits -&amp;gt;
                fruits.contains(&quot;사과&quot;)
            }
            .expectComplete()
            .verify()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;먼저 StepVerifier를 create로 초기화 후 recordWith{ ArrayList() }를 사용했습니다. recordWith는 create로 초기화된 발행자가 통지하는 onNext 값을 인자로 제공받은 컬렉션에 기록합니다. recordWith로 기록된 데이터는 내부에 컬렉터로 불리는 내부 큐에 보관되며 expectRecordedMatches와 consumeRecordedWith 같은 테스트 연산자를 사용하기 위해 필수로 호출돼야 합니다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음 스텝으로 expectNextCount는 통지된 데이터의 개수가 인자로 지정된 개수와 일치하는지 검증합니다. 복수의 데이터가 통지되는 경우 유용하게 사용할 수 있는 연산자입니다. 그다음 expectRecordedMatches는 인자로&amp;nbsp; 함수형 인터페이스인 Predicate를 전달받습니다. Predicate 내부의 test 함수는 Boolean을 리턴하기 때문에 전달받은 조건이 true인 경우에 테스트가 통과합니다. 예제에서는 fruits로 제공받은 리스트 안에 &amp;lsquo;사과&amp;rsquo;가 존재하므로 이 경우에는 true을 반환하므로 테스트를 통과하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;expectError, expectErrorMessage, expectErrorSatisfies&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트 케이스를 작성할 때&amp;nbsp; 특정 조건에서 의도한 에러가 발생하는지 검증하는 것도 중요한 테스트입니다. 애플리케이션이 운영 중인 상황에서 의도치 않은 에러가 발생하여 시스템에 장애가 발생하게 될 경우 간단한 코드 상의&amp;nbsp; 에러라면 빠르게 처리하여 핫픽스 배포를 진행할 수 있지만 돈에 관련된 문제라든지 데이터의 일관성에 부정확을 초래하는 에러인 경우 잠깐 사이에도 큰 장애가 될 수 있습니다. 이런 경우를 방지하기 위해 특정한 조건에서 의도한 에러를 발생시키면 개발자가 에러를 핸들링할 수 있기 때문에 에러 검증을 위한 테스트 케이스는 매우 중요합니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.42는 기존에 존재하는 플럭스에 임의의 에러 플럭스를 합친 다음 정상적인 데이터가 통지된 이후 onError 신호가 발생하는지 검증하는 테스트입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.42 expectError 테스트 - reactor/reactor-test/src/test/kotlin/ReactorTest3.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001346001&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.junit.jupiter.api.Test
import reactor.core.publisher.Flux
import reactor.test.StepVerifier
 
class ReactorTest3 {
 
    @Test
    fun `IllegalArgumentException 에러가 발생해야 한다`() {
        val error = IllegalArgumentException(&quot;error&quot;)
        val errorFlux = Flux.error&amp;lt;String&amp;gt;(error)
 
        val source = Flux.just(&quot;success-1&quot;, &quot;success-2&quot;)
            .concatWith(errorFlux)
 
        // 종류에 상관없이 에러가 발생하면 통과
        StepVerifier.create(source)
            .expectNext(&quot;success-1&quot;)
            .expectNext(&quot;success-2&quot;)
            .expectError()
            .verify()
 
        // 인자로 지정한 에러가 발생할 때만 통과
        StepVerifier.create(source)
            .expectNext(&quot;success-1&quot;)
            .expectNext(&quot;success-2&quot;)
            .expectError(IllegalArgumentException::class.java)
            .verify()
    }
}&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;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;expectError는 현재 구독하는 발행자가 에러를 통지하는지 검증합니다. 리액티브 스트림에는 서브스크라이버의 onError에 대한 사양이 있기 때문에 내부적으로 onError 신호가 발생하는지를 판단하여 통과여부를 판단하게됩니다. 예제에서 먼저 사용한 인자 없는 expectError 연산자는 에러의 종류에 상관없이 onError 신호가 발생하면 테스트를 통과하게되고 상세한 에러를 인자로 받는 expectError(Throwable)는 지정한 에러가 발생하는 경우에만 테스트를 통과합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모든 에러의 최상위 부모인 Throwable은 기본 생성자 외에도 사용자 정의 에러 메시지를 인자로 받는 Throwable(message)을 제공합니다. 이러한 사용자 정의 에러 메시지를 검증하고 싶은 경우 expectErrorMessage 연산자를 사용해 테스트할 수 있습니다. 예제 1.43에서는 expectErrorMessage 연산자를 사용해 사용자 정의 에러 메시지에 대한 테스트 방법을 확인할 수 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.43 expectErrorMessage 테스트 - reactor/reactor-test/src/test/kotlin/ReactorTest4.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001361537&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.junit.jupiter.api.Test
import reactor.core.publisher.Flux
import reactor.test.StepVerifier
 
class ReactorTest4 {
 
    @Test
    fun `An Error Occurred 에러 메시지가 발생해야 한다`() {
        val error = IllegalArgumentException(&quot;An error occurred&quot;)
        val errorFlux = Flux.error&amp;lt;String&amp;gt;(error)
 
        val source = Flux.just(&quot;success-1&quot;, &quot;success-2&quot;)
            .concatWith(errorFlux)
 
        StepVerifier.create(source)
            .expectNext(&quot;success-1&quot;)
            .expectNext(&quot;success-2&quot;)
            .expectErrorMessage(&quot;An error occurred&quot;)
            .verify()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;IllegalArgumentException의 인자로 &amp;ldquo;An error occurred&amp;rdquo; 에러 메시지를 지정하고 에러를 통지하면 expectErrorMessage는 내부적으로 통지받은 에러 플럭스를 Exception의 최상위 타입인 Throwable로 변환한 뒤 메시지를 비교하고 기대하는 결과가 동일하다면 테스트를 통과시킵니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;마지막으로 expectErrorSatisfies 함수는 인자로 Consumer&amp;lt;Throwable&amp;gt;을 받아서 개발자가 에러에 대한 검증을 직접 구현할 수 있습니다. 인자로 받는 Consumer 역시 함수형 인터페이스의 하나로 반환 값 없이 인자로 전달된 함수를 단순히 실행하는 함수형 인터페이스입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001371953&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface Consumer&amp;lt;T&amp;gt; {
    void accept(T t);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.44는 expectErrorSatisfies에 전달되는 람다 함수로 JUnit5의 Assertions API를 사용해 통지된 에러의 타입을 assertTrue로 우선 체크 한 후 에러 메시지가 기대한 결과와 일치하는지를 assertEquals로 검증하는 코드입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.44 expectErrorMessage 테스트 - reactor/reactor-test/src/test/kotlin/ReactorTest5.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001388167&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import reactor.core.publisher.Flux
import reactor.test.StepVerifier
 
class ReactorTest5 {
 
    @Test
    fun `An Error Occurred 에러 메시지가 발생해야 한다`() {
        val error = IllegalArgumentException(&quot;An error occurred&quot;)
        val errorFlux = Flux.error&amp;lt;String&amp;gt;(error)
 
        val source = Flux.just(&quot;success-1&quot;, &quot;success-2&quot;)
            .concatWith(errorFlux)
 
        StepVerifier.create(source)
            .expectNext(&quot;success-1&quot;)
            .expectNext(&quot;success-2&quot;)
            .expectErrorSatisfies {
                assertTrue(it is IllegalArgumentException)
                assertEquals(&quot;An error occurred&quot;, it.message)
            }
            .verify()
    }
}&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;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;발생할 에러의 타입이 다양하여 분기 처리가 필요하거나 상세한 검증 로직이 필요한 경우 expectErrorSatisfies를 사용하면 쉽게 구현이 가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;verify로 시작하는 편의 함수&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞서 알아본 예제의 경우&amp;nbsp; 코드의 마지막에 최종적으로 verify를 호출하여 테스트를 실행하였는데 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;편의 함수(Convenience Function)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 사용하면 조금 더 간결하게 코드를 작성할 수 있습니다. StepVerifier는 verify로 시작하는 이름의 편의 함수를 제공합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;먼저 예제 1.41에서 작성한 테스트 코드의 마지막 부분인 expectComplete.verify()입니다. 예제 1.45와 같이 편의 함수인 verifyComplete를 사용해서 한 번의 함수 호출로 코드를 재작성 할 수 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.45 verifyComplete 예제 - reactor/reactor-test/src/test/kotlin/ReactorTest6.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001405806&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.junit.jupiter.api.Test
import reactor.core.publisher.Flux
import reactor.test.StepVerifier
import java.util.*
 
class ReactorTest6 {
 
    @Test
    fun `사과가 들어있는지 확인`() {
        val flux: Flux&amp;lt;String&amp;gt; = Flux.just(&quot;사과&quot;, &quot;바나나&quot;, &quot;딸기&quot;)
 
        StepVerifier
            .create(flux)
            .recordWith { ArrayList() }
            .expectNextCount(3)
            .expectRecordedMatches { fruits -&amp;gt;
                fruits.contains(&quot;사과&quot;)
            }
//          .expectComplete()
//          .verify()
            .verifyComplete()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.42의 expectError.verify()와 expectError(Throwable).verify()는 예제 1.46와 같이 verifyError와 verifyError(Throwable)을 사용해 코드를 재작성 할 수 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.46 verifyError 예제 - reactor/reactor-test/src/test/kotlin/ReactorTest7.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001421321&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.junit.jupiter.api.Test
import reactor.core.publisher.Flux
import reactor.test.StepVerifier
 
class ReactorTest7 {
 
    @Test
    fun `IllegalArgumentException 에러가 발생해야 한다`() {
        val error = IllegalArgumentException(&quot;error&quot;)
        val errorFlux = Flux.error&amp;lt;String&amp;gt;(error)
 
        val source = Flux.just(&quot;success-1&quot;, &quot;success-2&quot;)
            .concatWith(errorFlux)
 
        // 종류에 상관없이 에러가 발생하면 통과
        StepVerifier.create(source)
            .expectNext(&quot;success-1&quot;)
            .expectNext(&quot;success-2&quot;)
//          .expectError()
//          .verify()
            .verifyError()
 
        // 인자로 지정한 에러가 발생할 때만 통과
        StepVerifier.create(source)
            .expectNext(&quot;success-1&quot;)
            .expectNext(&quot;success-2&quot;)
//          .expectError(IllegalArgumentException::class.java)
//          .verify()
            .verifyError(IllegalArgumentException::class.java)
    }
}&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;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.43의 expectErrorMessage.verify()는 예제 1.47과 같이 편의 함수인 verifyErrorMessage(message)를 사용해서 한 번의 함수 호출로 코드를 재작성 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.47 verifyErrorMessage 예제 - reactor/reactor-test/src/test/kotlin/ReactorTest8.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001436891&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.junit.jupiter.api.Test
import reactor.core.publisher.Flux
import reactor.test.StepVerifier
 
class ReactorTest8 {
 
    @Test
    fun `An Error Occurred 에러 메시지가 발생해야 한다`() {
        val error = IllegalArgumentException(&quot;An error occurred&quot;)
        val errorFlux = Flux.error&amp;lt;String&amp;gt;(error)
 
        val source = Flux.just(&quot;success-1&quot;, &quot;success-2&quot;)
            .concatWith(errorFlux)
 
        StepVerifier.create(source)
            .expectNext(&quot;success-1&quot;)
            .expectNext(&quot;success-2&quot;)
//          .expectErrorMessage(&quot;An error occurred&quot;)
//          .verify()
            .verifyErrorMessage(&quot;An error occurred&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;마지막으로 예제 1.44의 expectErrorSatisfies.verify()입니다. 예제 1.48과 같이 편의 함수인 verifyErrorMessage를 사용해서 한 번의 함수 호출로 코드를 재작성 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.48 verifyErrorSatisfies 예제 - reactor/reactor-test/src/test/kotlin/ReactorTest9.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001453608&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import reactor.core.publisher.Flux
import reactor.test.StepVerifier
 
class ReactorTest9 {
 
    @Test
    fun `An Error Occurred 에러 메시지가 발생해야 한다`() {
        val error = IllegalArgumentException(&quot;An error occurred&quot;)
        val errorFlux = Flux.error&amp;lt;String&amp;gt;(error)
 
        val source = Flux.just(&quot;success-1&quot;, &quot;success-2&quot;)
            .concatWith(errorFlux)
 
        StepVerifier.create(source)
            .expectNext(&quot;success-1&quot;)
            .expectNext(&quot;success-2&quot;)
//          .expectErrorSatisfies {
//              assertTrue(it is IllegalArgumentException)
//              assertEquals(&quot;An error occurred&quot;, it.message)
//          }
//          .verify()
            .verifyErrorSatisfies {
                assertTrue(it is IllegalArgumentException)
                assertEquals(&quot;An error occurred&quot;, it.message)
            }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 verify로 시작하는 편의 함수를 사용하면 마지막에 호출되는 expect 테스트 함수를 줄일 수 있으므로 조금 더 간결하고 편리하게 코드를 작성할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;부수 효과 적용&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;부수 효과(Side Effect&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;는 핵심 로직이 아닌 부가적으로 동작하는 별개의 기능을 추가하는 것을 말합니다. 리액터는 기존에 작성한 연산자를 수정하지 않고 부수 효과를 지원하는 연산자를 통해 로직을 특정한 신호와 함께 동작하도록 추가하거나 통지되는 데이터를 들여다보기 위한 방법을 제공합니다. 이러한 연산자는 대부분 &amp;lsquo;do&amp;rsquo;라는 접두어를 사용합니다. 주로 사용되는 연산자는 &amp;lsquo;doOn&amp;rsquo;으로 시작하는 연산자로 특정 신호(onSubscribe, onNext 등)가 동작하면 콜백 형태로 동작하므로 각각의 신호가 발생할 때마다 쉽게 부수 효과를 적용할 수 있습니다. 예를 들어 데이터를 변환하거나 필터링 후 데이터가 어떻게 통지되는지 로그로 출력하는 등 핵심 로직과는 별개의 작업이 필요한 경우 유용하게 사용될 수 있을 것입니다. 표 1.4는 주로 사용되는 doOn으로 시작하는 연산자들을 소개합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;표 1.4 주로 사용되는 doOn으로 시작하는 연산자&lt;/span&gt;&lt;/p&gt;
&lt;div&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;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;함수&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설명&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;doOnSubscribe&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구독을 시작하고 onSubscribe 신호가 발생하면 호출&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;doOnNext&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;onNext로 데이터를 통지한뒤 호출&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;doOnComplete&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;onComplete 신호가 발생하기 직전에 호출. 플럭스 전용 연산자&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;doOnError&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;onError 신호가 발생하면 호출&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이외에도 더 많은 부수 효과를 위한 연산자들은 리액터 공식 문서&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에서 확인할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;doOnSubscribe&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;doOnSubscribe는 구독자의 onSubscribe가 호출되면 이어서 동작하는 연산자입니다. onSubscribe는 구독이 일어나면 최초 1회만 호출되는 것이 특징입니다. doOnSubscribe은 함수형 인터페이스인 Consumer를 제공받아서 반환 값 없이 함수 내부의 코드만 실행하는 것이 특징입니다. 개발자는 Consumer를 작성하면 인자로 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서브스크립션(Subscription)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;을 제공받으므로 현재 구독에 대한 부수 효과도 같이 처리할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.49는 &amp;ldquo;red&amp;rdquo;와 &amp;ldquo;blue&amp;rdquo;라는 문자열을 가진 플럭스를 생성하고 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 연산자를 사용해 내부적으로 어떤 동작이 일어나는지 로그로 출력하고 전달받은 문자열을 대문자로 변환한 뒤 각각 출력합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.49 doOnSubscribe 예제 - reactor/reactor-basic/src/main/kotlin/sideeffect/DoOnSub1.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001486009&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package sideeffect
 
import reactor.core.publisher.Flux
 
fun main() {
    Flux.just(&quot;red&quot;, &quot;blue&quot;)
        .log()
        .map {
            it.toUpperCase()
        }
        .subscribe(::println)
}
 
--------------------
출력 결과)
--------------------
[ INFO] (main) | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
[ INFO] (main) | request(unbounded)
[ INFO] (main) | onNext(red)
RED
[ INFO] (main) | onNext(blue)
BLUE
[ INFO] (main) | onComplete()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출력 결과를 보면 [ INFO]로 시작하는 로그를 확인할 수 있는데 이것들은 모두 log() 연산자를 사용하였기 때문에 내부 동작이 출력되는 것입니다. 실무에서는 디버깅 용도를 제외하면 성능을 하락시킬 수 있기 때문에 유의하여 사용해야 합니다.&amp;nbsp; 로그 내용을 보면 최초 onSubscribe가 호출된 후 request(unbounded)가 호출된 것을 알 수 있습니다. unbounded는 즉 Long.MAX_VALUE 값이기 때문에 무한대의 값을 요청한 것과 마찬가지입니다. request 이후에는 onNext(red)로 데이터가 통지되고 subscribe(::println)에서 &amp;lsquo;RED&amp;rsquo;를 출력합니다. 그다음 onNext(blue)로 두 번째 데이터가 다시 통지되고 &amp;lsquo;BLUE&amp;rsquo;가 다시 subscribe(::println)에 의해 콘솔에 출력됩니다. 최종적으로 구독이 완료되어 onComplete 신호가 호출된 것을 확인할 수 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.50은 doOnSubscribe를 적용하면 어떻게 동작하는지 알아보기 위해 예제 1.49에 doOnSubscribe를 추가하고 몇 가지 부수 효과를 적용한 예제입니다. 먼저 부수 효과를 주기위해 적용한 doOnSubscribe 내부에선 인자로 전달받은 서브스크립션을 조작합니다. 우선 request(1)을 호출하여 onNext로 통지 될 데이터를 1개씩으로 제한합니다. 그런다음 cancel을 호출하여 구독을 취소한뒤 부수 효과가 제대로 적용됬는지 확인하기 위해 &amp;ldquo;doOnSubscribe is called&amp;rdquo;를 콘솔에 출력합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.50 doOnSubscribe 예제2 - reactor/reactor-basic/src/main/kotlin/sideeffect/DoOnSub2.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001511862&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package sideeffect
 
import reactor.core.publisher.Flux
 
fun main() {
    Flux.just(&quot;red&quot;, &quot;blue&quot;)
        .log()
        .doOnSubscribe { subscription -&amp;gt;
            subscription.request(1)
            subscription.cancel()
            println(&quot;doOnSubscribe is called&quot;)
        }
        .map {
            it.toUpperCase()
        }
        .subscribe(::println)
}
 
--------------------
출력 결과)
--------------------
[ INFO] (main) | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
[ INFO] (main) | request(1)
[ INFO] (main) | onNext(red)
RED
[ INFO] (main) | cancel()
doOnSubscribe is called
[ INFO] (main) | request(unbounded)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출력 결과를 확인해보면 최초 onSubscribe가 호출되고 이후 request(1)이 출력되었습니다. 예제에서 subscription.request(1)을 호출했기 때문에 데이터 통지가 1로 제한된 것입니다. 그다음엔 onNext(red)로 데이터가 통지되고 &amp;lsquo;RED&amp;rsquo;가 출력됩니다. 그다음 이전 예제에서는 &amp;lsquo;BLUE&amp;rsquo;가 출력됐지만 이번 예제에서는 cancel()이 호출되고 저희가 작성한 &amp;lsquo;doOnSubscribe is called&amp;rsquo;가 출력되었습니다. 즉 subscription.cancel()을 호출하여 구독을 취소했기 때문에 처음에 request(1)로 요청한 데이터는 정상 통지되고 두 번째로 데이터를 통지하려는 시점에서는 이미 구독이 취소되어 데이터가 출력되지 않은 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;doOnNext&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;doOnNext는 데이터가 통지되는 시점인 onNext가 호출된 후 동작하는 연산자입니다. doOnNext는 onNext로 통지된 값을 인자로 전달받습니다. 그러므로&amp;nbsp; 통지된 값을 확인하거나 통지된 값을 가지고 별도의 부수 효과를 적용할 수 있으므로 유용하게 사용될 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.51은 1부터 5까지 데이터를 통지하는 플럭스를 생성하고 doOnNext에서 통지된 값을 확인할 수 있도록 출력하는 예제입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.51 doOnNext 예제1 - reactor/reactor-basic/src/main/kotlin/sideeffect/DoOnNext1.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001530278&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package sideeffect
 
import reactor.core.publisher.Flux
 
fun main() {
    Flux.range(1, 5)
        .log()
        .doOnNext {
            println(&quot;통지된 값 : $it&quot;)
        }
        .subscribe()
}
 
--------------------
출력 결과)
--------------------
[ INFO] (main) | onSubscribe([Synchronous Fuseable] FluxRange.RangeSubscription)
[ INFO] (main) | request(unbounded)
[ INFO] (main) | onNext(1)
통지된 값 : 1
[ INFO] (main) | onNext(2)
통지된 값 : 2
[ INFO] (main) | onNext(3)
통지된 값 : 3
[ INFO] (main) | onNext(4)
통지된 값 : 4
[ INFO] (main) | onNext(5)
통지된 값 : 5
[ INFO] (main) | onComplete()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출력 결과를 보면 Flux.range에 지정된 값의 순서대로 onNext로 데이터 통지 신호가 발생하고 각각의 값이 doOnNext로 전달되는 것을 확인할 수 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.52에서는 AtomicInteger 클래스를 사용해 onNext 신호가 발생할 때마다 데이터 통지를 기록할 카운터를 생성하고 카운트한 뒤 출력하는 예제입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.52 doOnNext 예제2 - reactor/reactor-basic/src/main/kotlin/sideeffect/DoOnNext2.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001543577&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package sideeffect
 
import reactor.core.publisher.Flux
import java.util.concurrent.atomic.AtomicInteger
 
fun main() {
   val counter = AtomicInteger()
 
   Flux.range(1, 5)
       .log()
       .doOnNext {
           counter.incrementAndGet()
           println(&quot;카운터 증가&quot;)
       }
       .subscribe()
 
   println(&quot;총 개수 : ${counter.get()}&quot;)
}
 
--------------------
출력 결과)
--------------------
[ INFO] (main) | onSubscribe([Synchronous Fuseable] FluxRange.RangeSubscription)
[ INFO] (main) | request(unbounded)
[ INFO] (main) | onNext(1)
카운터 증가
[ INFO] (main) | onNext(2)
카운터 증가
[ INFO] (main) | onNext(3)
카운터 증가
[ INFO] (main) | onNext(4)
카운터 증가
[ INFO] (main) | onNext(5)
카운터 증가
[ INFO] (main) | onComplete()
총 개수 : 5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출력 결과를 보면 onNext 신호가 발생할때마다 &amp;ldquo;카운터 증가&quot; 메시지가 콘솔에 출력되는 것을 확인할 수 있습니다. 총 5번의 카운터가 증가되고 최종적으로 &amp;ldquo;총 개수 :5&amp;rdquo;가 출력됩니다. doOnNext를 사용하면 각각 데이터 통지 마다 이러한 부수 효과를 적용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;doOnComplete&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;doOnComplete는 통지가 성공적으로 완료되는 시점에 동작하는 연산자로 인자로 함수형 인터페이스인 Runnable을 전달 받아서 동작하는 것이 특징입니다. 또한 doOnComplete는 플럭스만 제공하는 연산자로 모노의 경우 비슷한 동작을 하는 doOnSuccess가 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.53은 A부터 C를 통지하는 플럭스를 생성하고 데이터 통지가 완료되면 doOnComplete 내부에서 AtomicBoolean을 사용해 성공 여부를 true로 변경한 후 데이터 통지 성공 여부를 출력합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.53 doOnComplete 예제1 - reactor/reactor-basic/src/main/kotlin/sideeffect/DoOnComp1.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001566516&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package sideeffect
 
import reactor.core.publisher.Flux
import java.util.concurrent.atomic.AtomicBoolean
 
fun main() {
    val completed = AtomicBoolean(false)
    Flux.just(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;)
        .log()
        .doOnComplete {
            completed.set(true)
        }
        .subscribe()
 
    if (completed.get()) {
        println(&quot;데이터 통지 완료&quot;)
    } else {
        println(&quot;데이터 통지 실패&quot;)
    }
}
 
--------------------
출력 결과)
--------------------
[ INFO] (main) | onSubscribe([Fuseable] FluxPeekFuseable.PeekFuseableSubscriber)
[ INFO] (main) | request(unbounded)
[ INFO] (main) | onNext(A)
[ INFO] (main) | onNext(B)
[ INFO] (main) | onNext(C)
doOnComplete 동작
[ INFO] (main) | onComplete()
데이터 통지 성공&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출력 결과를 보면 우선 onNext로 A, B, C가 순차적으로 통지됩니다. 그다음 doOnComplete에 작성한 &amp;ldquo;doOnComplete 동작&amp;rdquo;이 출력되고 onComplete 신호가 발생하고 최종적으로 &amp;ldquo;데이터 통지 완료&quot; 메시지가 콘솔에 출력됩니다. 그러므로 doOnComplete는 onComplete 신호 직전에 호출된다는 것을 확인할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번엔 에러가 통지될 경우 doOnComplete가 제대로 동작하는지 알아보기 위해 예제 1.54에서는 기존의 플럭스에 concatWith로 Flux.error(IllegalArgumentException())을 추가한 뒤 실행해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.54 doOnComplete 예제2 - reactor/reactor-basic/src/main/kotlin/sideeffect/DoOnComp2.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001584099&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package sideeffect
 
import reactor.core.publisher.Flux
import java.util.concurrent.atomic.AtomicBoolean
 
fun main() {
    val completed = AtomicBoolean(false)
    Flux.just(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;)
        .concatWith(Flux.error(IllegalArgumentException()))
        .log()
        .doOnComplete {
            println(&quot;doOnComplete 동작&quot;)
            completed.set(true)
        }
        .subscribe()
 
    if (completed.get()) {
        println(&quot;데이터 통지 완료&quot;)
    } else {
        println(&quot;데이터 통지 실패&quot;)
    }
}
 
--------------------
출력 결과)
--------------------
[ INFO] (main) onSubscribe(FluxConcatArray.ConcatArraySubscriber)
[ INFO] (main) request(unbounded)
[ INFO] (main) onNext(A)
[ INFO] (main) onNext(B)
[ INFO] (main) onNext(C)
데이터 통지 실패
[ERROR] (main) onError(java.lang.IllegalArgumentException)
[ERROR] (main)  - java.lang.IllegalArgumentException&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출력 결과를 보면 doOnComplete와 onComplete가 호출되지 않고 &amp;ldquo;데이터 통지 실패&quot;가 출력된 후 onError 신호가 발생하는 것을 확인할 수 있습니다. 또한 에러에 대한 별도 처리가 없으므로 java.lang.IllegalArgumentException이 발생하게 됩니다. onComplete는 에러 없이 정상적으로 데이터 통지가 완료될 경우에만 발생하는데 doOnComplete는 onComplete 직전에 동작하므로 에러 통지가 없어야 동작하는 것을 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;doOnError&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;doOnError는 에러가 통지되는 시점에&amp;nbsp; 지정한 부수 효과를 적용할 수 있습니다. 에러가 통지되는 시점에는 onError 신호가 발생하는데 doOnError 연산자는 인자로 함수형 인터페이스인 Consumer를 제공받아서&amp;nbsp; onError 신호가 발생하기 직전에 실행합니다. 인자로 제공된 Consumer에는 최상위 에러 클래스인 Throwable가 전달됩니다. 그렇기 때문에 내부에서 어떤 에러가 발생했는지 확인하여 처리를 다르게 할 수도 있습니다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.55는 에러를 통지하는 플럭스를 생성하고 에러가 발생할 경우 doOnError가 동작하는 예제입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.55 doOnError 예제 - reactor/reactor-basic/src/main/kotlin/sideeffect/DoOnError.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001621024&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package sideeffect
 
import reactor.core.publisher.Flux
import java.util.concurrent.atomic.AtomicBoolean
 
fun main() {
    val errorOccurred = AtomicBoolean(false)
    Flux.range(1,3)
        .concatWith(Flux.error(IllegalArgumentException()))
        .log()
        .doOnError { throwable: Throwable -&amp;gt;
            println(&quot;doOnError 동작&quot;)
            println(&quot;발생한 에러 : ${throwable.javaClass}&quot;)
            errorOccurred.set(true)
        }
        .subscribe()
 
    if (errorOccurred.get()) {
        println(&quot;에러 발생&quot;)
    }
}
 
--------------------
출력 결과)
--------------------
[ INFO] (main) onSubscribe(FluxConcatArray.ConcatArraySubscriber)
[ INFO] (main) request(unbounded)
[ INFO] (main) onNext(1)
[ INFO] (main) onNext(2)
[ INFO] (main) onNext(3)
doOnError 동작
발생한 에러 : class java.lang.IllegalArgumentException
에러 발생
[ERROR] (main) onError(java.lang.IllegalArgumentException)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출력 결과를 보면 onNext로 데이터를 정상적으로 통지하다가 에러가 통지되면 doOnError가 먼저 발생한 후 최종적으로 onError가 발생합니다. doOnError 내부에서는 람다 함수로 제공되는 Consumer는 인자로 Throwable을 받기 때문에 에러의 종류와 어떤 메시지를 출력하는지 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리액터 코틀린 지원&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스프링 프로젝트는 코틀린에 대한 지원을 점점 늘리고 있는데 최신 스프링 프로젝트 공식 문서를 보면 자바 예제외에도 코틀린 예제를 쉽게 확인할 수 있습니다. 또한 코틀린이 제공하는 여러 라이브러리도 스프링 프로젝트에 기본으로 탑재되고 있는데 리액터 또한 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리액터 코틀린 확장(reactor-kotlin-extensions)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;을 지원합니다 . 리액터 코틀린 확장 기능을 사용하면 코틀린의 우아한 문법을 사용해 리액터 코드를 자바를 사용했을 때보다 더 간결하게 작성이 가능합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;먼저 리액터 코틀린 확장 기능을 사용하기 위해 예제 1.56과 같이 build.gradle.kts에 reactor-kotlin-extensions 모듈을 추가 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.56 프로젝트에 reactor-kotlin-extensions 의존성 추가 - reactor/reactor-basic/build.gradle.kts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001637909&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
   id(&quot;org.jetbrains.kotlin.jvm&quot;) version (&quot;1.4.10&quot;)
}
 
group = &quot;com.reactor.reactorbasic&quot;
version = &quot;1.0-SNAPSHOT&quot;
 
repositories {
    mavenCentral()
}
 
dependencies {
   implementation(&quot;org.jetbrains.kotlin:kotlin-stdlib&quot;)
   implementation(&quot;io.projectreactor:reactor-core:3.4.0&quot;)
 implementation(&quot;io.projectreactor.kotlin:reactor-kotlin-extensions:1.1.2&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;toMono&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;toMono는 객체를 모노로 변환하기 위한 코틀린 확장 함수입니다. toMono 함수를 사용하면 Mono.justOrEmpty, Mono.fromCallable, Mono.fromFuture와 같은 팩토리 함수를 쓰지 않아도 객체를 모노로 쉽게 변환할 수 있습니다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음은 리액터 코틀린 확장 모듈의 모노 확장 함수 구현 코드입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001652084&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun &amp;lt;T&amp;gt; Publisher&amp;lt;T&amp;gt;.toMono(): Mono&amp;lt;T&amp;gt; = Mono.from(this)
 
fun &amp;lt;T&amp;gt; (() -&amp;gt; T?).toMono(): Mono&amp;lt;T&amp;gt; = Mono.fromSupplier(this)
 
fun &amp;lt;T : Any&amp;gt; T?.toMono(): Mono&amp;lt;T&amp;gt; = Mono.justOrEmpty(this)
 
fun &amp;lt;T&amp;gt; Callable&amp;lt;T?&amp;gt;.toMono(): Mono&amp;lt;T&amp;gt; = Mono.fromCallable(this::call)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;코드를 보면 toMono는 코틀린의 최상위 객체인 Any에 확장한 함수부터 Callable, CompletableFuture, 리액터의 Publisher를 지원하는 확장 함수가 구현돼있습니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.57은 앞서 살펴본 toMono를 사용해 다양한 객체를 모노로 변환하는 예제입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.57 toMono 예제 - reactor/reactor-basic/src/main/kotlin/extensions/toMono.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001664748&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package extensions
 
import reactor.kotlin.core.publisher.toMono
import java.util.concurrent.Callable
import java.util.concurrent.CompletableFuture
 
fun main() {
    &quot;String.toMono&quot;
        .toMono()
        .subscribe(::println)
 
    listOf(&quot;Collection.toMono&quot;)
        .toMono()
        .subscribe(::println)
 
    Callable { &quot;Callable.toMono&quot; }
        .toMono()
        .subscribe(::println)
 
    CompletableFuture.supplyAsync { &quot;supplyAsync.toMono&quot; }
        .toMono()
        .subscribe(::println)
}


--------------------
출력 결과)
--------------------
String.toMono
[Collection.toMono]
Callable.toMono
CompletableFuture.supplyAsync.toMono&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출력 결과를 보면 toMono로 변환한 객체들이 정상적으로 모노로 변환되어 데이터 통지가 이뤄진 것을 확인할 수 있습니다. toMono를 사용하면 다양한 객체를 팩토리 함수 없이 모노로 쉽게 변환할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;toFlux&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;toFlux는 객체를 플럭스로 변환하기 위한 코틀린 확장 함수입니다. toFlux는 toMono와 다르게 컬렉션이나 배열 같은 복수의 데이터를 저장하는 자료구조를 toFlux로 변환하는 것이 특징입니다. toFlux 함수를 사용하면 Flux.just, Flux.fromIterable, Flux.fromArray 같은 팩토리 함수를 쓰지 않아도 객체를 플럭스로 쉽게 변환할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음은 리액터 코틀린 확장 모듈의 플럭스 확장 함수 구현 코드입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001736679&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun &amp;lt;T : Any&amp;gt; Publisher&amp;lt;T&amp;gt;.toFlux(): Flux&amp;lt;T&amp;gt; = Flux.from(this)
 
fun &amp;lt;T : Any&amp;gt; Iterator&amp;lt;T&amp;gt;.toFlux(): Flux&amp;lt;T&amp;gt; = toIterable().toFlux()
 
fun &amp;lt;T : Any&amp;gt; Iterable&amp;lt;T&amp;gt;.toFlux(): Flux&amp;lt;T&amp;gt; = Flux.fromIterable(this)
 
fun &amp;lt;T : Any&amp;gt; Stream&amp;lt;T&amp;gt;.toFlux(): Flux&amp;lt;T&amp;gt; = Flux.fromStream(this)
 
fun BooleanArray.toFlux(): Flux&amp;lt;Boolean&amp;gt; = this.toList().toFlux()
 
fun ByteArray.toFlux(): Flux&amp;lt;Byte&amp;gt; = this.toList().toFlux()
 
fun ShortArray.toFlux(): Flux&amp;lt;Short&amp;gt; = this.toList().toFlux()
 
fun IntArray.toFlux(): Flux&amp;lt;Int&amp;gt; = this.toList().toFlux()
 
fun LongArray.toFlux(): Flux&amp;lt;Long&amp;gt; = this.toList().toFlux()
 
fun FloatArray.toFlux(): Flux&amp;lt;Float&amp;gt; = this.toList().toFlux()
 
fun DoubleArray.toFlux(): Flux&amp;lt;Double&amp;gt; = this.toList().toFlux()
 
fun &amp;lt;T&amp;gt; Array&amp;lt;out T&amp;gt;.toFlux(): Flux&amp;lt;T&amp;gt; = Flux.fromArray(this)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;코드를 보면 다양한 배열과 컬렉션 구조에 대한 toFlux 확장 함수가 구현돼 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.58은 toFlux를 사용해 자료구조를 플럭스로 변환하는 예제입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.58 toFlux 예제 - reactor/reactor-basic/src/main/kotlin/extensions/toFlux.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1642001754544&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package extensions
 
import reactor.kotlin.core.publisher.toFlux
import reactor.kotlin.core.publisher.toMono
 
fun main() {
    listOf(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;)
        .toFlux()
        .subscribe(::println)
 
    intArrayOf(1, 2, 3)
        .toFlux()
        .subscribe(::println)
 
    setOf(&quot;D&quot;, &quot;E&quot;, &quot;F&quot;)
        .toFlux()
        .subscribe(::println)
 
    &quot;MonoToFlux&quot;.toMono()
       .toFlux()
       .subscribe(::println)
}
 
--------------------
출력 결과)
--------------------
A
B
C
1
2
3
D
E
F
MonoToFlux&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출력 결과를 보면 컬렉션과 배열을 플럭스로 변환해서 데이터 통지가 정상적으로 이뤄진 것을 확인할 수 있습니다. toFlux의 한가지 특징은 컬렉션이나 배열의 경우 플럭스로 바로 변환이 가능하지만 &amp;ldquo;MonoToFlux&amp;rdquo; 문자열과 같은 일반 객체의 경우 toMono로 우선 변환한 뒤 toFlux 함수를 사용해 플럭스로 변환해야 합니다. toFlux의 경우 자료구조 외에는 발행자인 Publisher에만 확장할 수 있도록 구현돼 있기 때문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Reactive Programming</category>
      <author>devsh</author>
      <guid isPermaLink="true">https://devsh.tistory.com/152</guid>
      <comments>https://devsh.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%EC%95%A1%ED%84%B0-%EA%B3%A0%EA%B8%89-%ED%99%9C%EC%9A%A9#entry152comment</comments>
      <pubDate>Thu, 13 Jan 2022 00:11:20 +0900</pubDate>
    </item>
    <item>
      <title>프로젝트 리액터 기초 2 - 플럭스</title>
      <link>https://devsh.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%EC%95%A1%ED%84%B0-%EA%B8%B0%EC%B4%88-2-%ED%94%8C%EB%9F%AD%EC%8A%A4</link>
      <description>&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://devsh.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%EC%95%A1%ED%84%B0-%EA%B8%B0%EC%B4%88-1-%EB%AA%A8%EB%85%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;프로젝트 리액터 기초 1 - 모노&lt;/a&gt;&lt;b&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로젝트 리액터 기초 2 - 플럭스&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devsh.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%EC%95%A1%ED%84%B0-%EA%B3%A0%EA%B8%89-%ED%99%9C%EC%9A%A9&quot;&gt;프로젝트&lt;span&gt;&amp;nbsp;&lt;/span&gt;리액터&lt;span&gt;&amp;nbsp;&lt;/span&gt;고급 활용&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;플럭스의 연산자&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플럭스는 0..N개로 이뤄진 복수개의 요소를 통지하는 발행자입니다. 모노는 1개의 요소를 통지하지만 플럭스는 제한 없는 무한대의 요소를 통지합니다. 플럭스의 연산자는 모노의 연산자와 이름이 같고 역할이 같은 연산자가 많습니다. 대표적으로 앞선 장에서 소개한 map, flatMap, switchIfEmpty 등의 연산자는 플럭스를 사용할시에도 동일하게 사용할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;just&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flux.just는 플럭스를 생성하는 기본 팩토리 함수입니다. Flux.just의 인자는 가변 인자를 지원하기 때문에 데이터의 개수 제한 없이 추가하여 플럭스를 생성할 수 있습니다. 예제 1.20는 두 개의 CellPhone 객체를 생성하고 Flux.just의 인자로 전달하고 다른 작업 없이 출력합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package flux

import reactor.core.publisher.Flux

data class Cellphone(
    val name: String,
    val price: Int,
    val currency: Currency,
)

enum class Currency {
    KRW, USD
}

fun main() {
    val iphone =
        Cellphone(name = &quot;Iphone&quot;, price = 100, currency = Currency.KRW)
    val galaxy =
        Cellphone(name = &quot;Galaxy&quot;, price = 90, currency = Currency.KRW)

    val flux: Flux&amp;lt;Cellphone&amp;gt; =
        Flux.just(iphone, galaxy)

    flux.subscribe(::println)
}

--------------------
출력 결과)
--------------------
Cellphone(name=Iphone, price=100, currency=KRW)
Cellphone(name=Galaxy, price=90, currency=KRW)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 결과와 같이 Flux.just에 들어가는 인자의 순서대로 차례로 데이터가 통지됩니다. just 함수에 만약 null인 객체가 전달되면 예제 1.21과 같이 NullPointerException이 발생하기 때문에 주의해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.21 Flux.just에 null이 들어간 경우 - reactor/reactor-basic/src/flux/Flux1.kt&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package flux

import reactor.core.publisher.Flux

data class Cellphone(
    val name: String,
    val price: Int,
    val currency: Currency,
)

enum class Currency {
    KRW, USD
}

fun main() {
    val iphone =
        Cellphone(name = &quot;Iphone&quot;, price = 100, currency = Currency.KRW)
    val galaxy =
        Cellphone(name = &quot;Galaxy&quot;, price = 90, currency = Currency.KRW)

    val flux: Flux&amp;lt;Cellphone&amp;gt; =
        Flux.just(iphone, galaxy, null)

    flux.subscribe(::println)
}

--------------------
출력 결과)
--------------------
java.lang.NullPointerException: The 2th array element was null&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;fromArray, fromIterable&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flux.fromArray는 함수는 인자에 배열을 받아 플럭스를 생성하는 팩토리 함수입니다. 예제 1.22는 배열에 객체를 담고 Flux.fromArray를 사용해 플럭스를 생성한 뒤 순서대로 출력합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.22 Flux.fromArray - reactor/reactor-basic/src/flux/Flux2.kt&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;package flux

import reactor.core.publisher.Flux

data class Hero(
   val nickname: String,
   val name: String,
)

fun main() {
   val avengers = arrayOf(
       Hero(&quot;Black Widow&quot;, &quot;Natasha Romanoff&quot;),
       Hero(&quot;Iron Man&quot;, &quot;Tony Stark&quot;),
       Hero(&quot;Captain America&quot;, &quot;Steve Rogers&quot;),
       Hero(&quot;Thor&quot;, &quot;Thor Odinson&quot;),
       Hero(&quot;Hulk&quot;, &quot;Bruce Banner&quot;),
       Hero(&quot;Hawkeye&quot;, &quot;Clint Barton&quot;),
       Hero(&quot;Black Panther&quot;, &quot;T'Challa&quot;)
   )

   val flux: Flux&amp;lt;Hero&amp;gt; =
       Flux.fromArray(avengers)

   flux.subscribe(::println)
}

--------------------
출력 결과)
--------------------
Hero(nickname=Iron Man, name=Tony Stark)
Hero(nickname=Black Widow, name=Natasha Romanoff)
Hero(nickname=Captain America, name=Steve Rogers)
Hero(nickname=Thor, name=Thor Odinson)
Hero(nickname=Hulk, name=Bruce Banner)
Hero(nickname=Hawkeye, name=Clint Barton)
Hero(nickname=Black Panther, name=T'Challa)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.22의 출력 결과를 보면 예상한대로 배열에 지정한 순서대로 출력되는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열이 아닌 컬렉션을 이용해 플럭스를 생성할 경우 Flux.fromIterable 함수를 사용할 수 있습니다. fromArray와 마찬가지로 리스트에 지정한 순서대로 데이터를 통지합니다. 예제 1.23은 Flux.fromIterable을 사용해 플럭스를 생성하고 순서대로 출력합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.23 Flux.fromIterable - reactor/reactor-basic/src/flux/Flux3.kt&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;package flux

import reactor.core.publisher.Flux

fun main() {
   val avengers = listOf(
       Hero(&quot;Iron Man&quot;, &quot;Tony Stark&quot;),
       Hero(&quot;Black Widow&quot;, &quot;Natasha Romanoff&quot;),
       Hero(&quot;Captain America&quot;, &quot;Steve Rogers&quot;),
       Hero(&quot;Thor&quot;, &quot;Thor Odinson&quot;),
       Hero(&quot;Hulk&quot;, &quot;Bruce Banner&quot;),
       Hero(&quot;Hawkeye&quot;, &quot;Clint Barton&quot;),
       Hero(&quot;Black Panther&quot;, &quot;T'Challa&quot;),
   )

   val flux: Flux&amp;lt;Hero&amp;gt; =
       Flux.fromIterable(avengers)

   flux.subscribe(::println)
}

--------------------
출력 결과)
--------------------
Hero(nickname=Iron Man, name=Tony Stark)
Hero(nickname=Black Widow, name=Natasha Romanoff)
Hero(nickname=Captain America, name=Steve Rogers)
Hero(nickname=Thor, name=Thor Odinson)
Hero(nickname=Hulk, name=Bruce Banner)
Hero(nickname=Hawkeye, name=Clint Barton)
Hero(nickname=Black Panther, name=T'Challa)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 결과에서 확인할 수 있듯이 fromIterable도 리스트에 지정한 순서 즉 인덱스의 순서대로 데이터를 통지하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;range&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flux.range 함수는 첫 번째 인자인 시작 값부터 두 번째 인자인 카운트 값까지 값을 하나씩 증가하는 데이터를 통지하는 팩토리 함수입니다. 예제 1.24는 Flux.range를 사용해 1부터 100까지 더한 값을 출력합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.24 Flux.range로 sum한 값 구하기 - reactor/reactor-basic/src/flux/Flux4.kt&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;package flux

import reactor.core.publisher.Flux

fun main() {
    val sum = Flux.range(1, 100)
    .reduce { t, u -&amp;gt;
        t + u
    }
    sum.subscribe(::println)
}

--------------------
출력 결과)
--------------------
5050&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 결과를 보면 1부터 100까지 더한 값인 5050이 출력되었습니다. 즉 1부터 100까지 순서대로 데이터를 통지하고 reduce 함수를 사용해 통지받은 데이터를 더해졌다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 예제 1.25는 take를 사용해 인자로 지정한 10개 데이터를 통지하고 buffer는 통지받은 인자로 지정한 값만큼 데이터를 버퍼에 담아서 통지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.25 take와 buffer를 사용한 예제 - reactor/reactor-basic/src/flux/Flux5.kt&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package flux

import reactor.core.publisher.Flux

fun main() {
    val buffer: Flux&amp;lt;List&amp;lt;Int&amp;gt;&amp;gt; =
        Flux.range(1, 100)
            .take(10)
            .buffer(4)

    buffer.subscribe(::println)
}

--------------------
출력 결과)
--------------------
[1, 2, 3, 4]
[5, 6, 7, 8]
[9, 10]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 결과를 보면 버퍼에 할당된 4건씩 데이터가 나뉘어 통지된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;interval&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flux.Interval 함수를 사용하면 인자로 지정해준 주기대로 신호를 발생 시킵니다. 발생된 신호는 Long 타입으로 &amp;lsquo;0, 1, 2,3&amp;rsquo; 순서대로 전달 받을 수 있습니다. 예제 1.26은 0.5초 간격으로 랜덤한 식별자(id)를 가지는 쿠폰을 생성하는 예제 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.26 Flux.interval로 쿠폰 생성 예제 - reactor/reactor-basic/src/flux/Flux6.kt&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package flux

import reactor.core.publisher.Flux
import java.time.Duration
import java.util.UUID

data class Coupon(
    val id: String,
    val description: String,
)

fun main() {
    val flux: Flux&amp;lt;Coupon&amp;gt; =
        Flux.interval(Duration.ofMillis(500))
            .map { count -&amp;gt;
                val id = UUID.randomUUID().toString()
                Coupon(id = id, description = &quot;${count.plus(1)}번째 쿠폰&quot;)
            }
            .take(15)

    flux.subscribe(::println)

    Thread.sleep(Duration.ofSeconds(10).toMillis())
}


--------------------
출력 결과)
--------------------
Coupon(id=b755285e-476e-405a-8a9f-e0b49b3520a6, description=1번째 쿠폰)
Coupon(id=9c4eea7a-1f12-4123-9fae-678e6161f7de, description=2번째 쿠폰)
Coupon(id=6478a919-ae2b-47c9-b238-d41fe5d73010, description=3번째 쿠폰)
Coupon(id=faf7b65f-299e-4467-9308-964b901e978a, description=4번째 쿠폰)
Coupon(id=77db0f58-27c8-4abc-a47f-8966259cea78, description=5번째 쿠폰)
Coupon(id=ccfab126-a5b1-4e9e-bbf3-c55f16a0e475, description=6번째 쿠폰)
Coupon(id=c59c6f88-cf6f-4d05-80f9-261851e49359, description=7번째 쿠폰)
Coupon(id=a2ec4a36-c9f8-4087-ac93-6b963954f029, description=8번째 쿠폰)
Coupon(id=8663e6ee-293c-492f-98b7-8246e19aeb3e, description=9번째 쿠폰)
Coupon(id=c9cf3146-d1ad-4c2d-85f6-c66f3635d53e, description=10번째 쿠폰)
Coupon(id=1c66e15e-0794-4bc5-bc33-d0088e0384ca, description=11번째 쿠폰)
Coupon(id=c2e2c8c3-d5b2-47c9-be99-06aa3a679018, description=12번째 쿠폰)
Coupon(id=cf53e12c-8a54-4875-b6e3-11f5494c57c5, description=13번째 쿠폰)
Coupon(id=c37c3981-e34b-4147-8d8a-66eb3bc626ed, description=14번째 쿠폰)
Coupon(id=32942297-f457-4778-b559-c0f45023bbd6, description=15번째 쿠폰)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Flux.interval(Duration.ofMillis(500))는 0.5초 간격으로 신호를 발생시키고 순번을 데이터로 통지합니다. map 연산자 내부에선 UUID.randomUUID().toString()을 사용해 유니크한 식별자를 생성하고 쿠폰 객체의 id 프로퍼티에 할당합니다. description에는 몇 번째로 생성된 쿠폰인지를 확인하기 위해 count라는 람다 변수를 선언했습니다. 이때 count의 값은 최초에 0부터 시작하기 때문에 count.plus(1)을 사용해 기존 값에 1을 더해줍니다. 그 다음은 take로 15개의 데이터만 통지하도록 조절한 후 출력합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 마지막 라인에서 예시와 같이 스레드를 일시정지하는 코드가 있어야 정상적으로 출력됩니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt; Thread.sleep(Duration.ofSeconds(10).toMillis())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드를 일시정지하는 이유는 Flux.interval은 내부적으로 별도의 스레드에서 동작하도록 만들어져있는데 코드를 실행하는 스레드는 메인 함수의 스레드이므로 일시정지하지 않으면 subscribe에서 출력하기 전에 메인 스레드가 종료되기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.27에서 Thread.sleep을 주석 처리하여 어떤 결과가 출력되는지 확인해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package flux

import reactor.core.publisher.Flux
import java.time.Duration
import java.util.UUID

data class Coupon(
    val id: String,
    val description: String,
)

fun main() {
    val flux: Flux&amp;lt;Coupon&amp;gt; =
        Flux.interval(Duration.ofMillis(500))
            .map { count -&amp;gt;
                val id = UUID.randomUUID().toString()
                Coupon(id = id, description = &quot;${count.plus(1)}번째 쿠폰&quot;)
            }
            .take(15)

    flux.subscribe(::println)

    //Thread.sleep(Duration.ofSeconds(10).toMillis())
}


--------------------
출력 결과)
--------------------&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 결과를 보면 Flux.interval에서 사용하는 스레드가 완료되기 전에 메인 스레드가 종료되어 아무것도 출력되지 않은 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;single, singleOrEmpty&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;single 함수를 사용하면 최초에 전달받은 1개의 데이터만 통지받도록 제한할 수 있습니다. 그러므로 single을 연산자로 적용한 경우 통지받은 데이터가 정확히 1개 임을 보장해야 합니다. 데이터가 없거나 1개 이상인 경우는 예외가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.29에서는 데이터가 1개가 아닌 경우 어떤 예외가 발생하는지 확인하기 위해 Flux.just로 6개의 데이터를 인자로 받고 take(2)로 데이터를 2번 통지하여 예외를 발생시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.29 single 예제 - reactor/reactor-basic/src/flux/Flux7.kt&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;package flux

import reactor.core.publisher.Flux

fun main() {
    val flux = Flux.just(1, 2, 3, 4, 5, 6)
        .take(2)
        .single()

    flux.subscribe(::println)
}

--------------------
출력 결과)
--------------------
java.lang.IndexOutOfBoundsException: Source emitted more than one item&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 결과를 보면 IndexOutOfBoundsException가 발생한 것을 확인할 수 있습니다. 예제 1.30는 take(1)을 사용해 하나의 데이터만 통지하는 예제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.30 single 예제2 - reactor/reactor-basic/src/flux/Flux8.kt&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package flux

import reactor.core.publisher.Flux

fun main() {
    val flux = Flux.just(&quot;one&quot;, &quot;two&quot;, &quot;three&quot;)
        .take(1)
        .single()

    flux.subscribe(::println)
}

--------------------
출력 결과)
--------------------
one&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 결과를 확인해보면 정상적으로 Flux.just의 인자로 처음 전달받은 문자열 &amp;lsquo;one&amp;rsquo;이 출력된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Flux로 통지받은 데이터가 비어있을(empty) 가능성이 있는 경우 singleOrEmpty를 사용할 수 있습니다. singleOrEmpty 함수는 1개의 데이터 또는 빈 값을 허용합니다. 예제 1.31은 Flux.empty를 사용해 빈 값을 통지하고 singleOrEmpty를 사용해 통지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.31 singleOrEmpty 예제 - reactor/reactor-basic/src/flux/Flux9.kt&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;package flux

import reactor.core.publisher.Flux

fun main() {
   val flux = Flux.empty&amp;lt;Any&amp;gt;()
       .singleOrEmpty()

   flux.subscribe(::println)
}

--------------------
출력 결과)
--------------------&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flux.empty로 플럭스를 생성하고 singleOrEmpty를 사용했기 때문에 빈 결과가 출력되었습니다. singleOrEmpty 함수도 1개 또는 빈 값이 아닌 경우는 동일하게 IndexOutOfBoundsException가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.32는 Flux.just에 첫 번째 인자는 &amp;lsquo;1&amp;rsquo; 두 번째 인자는 Flux.empty()를 지정한 후 singleOrEmpty 함수를 사용하여 예외를 발생시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.32 singleOrEmpty 예제2 - reactor/reactor-basic/src/flux/Flux9.kt&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;package flux

import reactor.core.publisher.Flux

fun main() {
    val flux = Flux.just(1, Flux.empty&amp;lt;Int&amp;gt;())
        .singleOrEmpty()

    flux.subscribe(::println)
}

--------------------
출력 결과)
--------------------
java.lang.IndexOutOfBoundsException: Source emitted more than one item&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;merge, concat, mergeSequential&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flux.merge는 플럭스를 제공받은 순서대로 결합하는 팩토리 함수입니다. zip 함수와의 차이점은 merge 함수는 튜플 객체로 감싸지지 않습니다. 예제 1.33은 Flux.merge를 사용해 인자로 지정된 플럭스를 순서대로 통지하는 예제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.32 Flux.merge 예제 - reactor/reactor-basic/src/flux/Flux10.kt&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;package flux

import reactor.core.publisher.Flux

fun main() {
    val three = Flux.just(&quot;three&quot;)
    val two = Flux.just(&quot;two&quot;)
    val one = Flux.just(&quot;one&quot;)

    val flux: Flux&amp;lt;String&amp;gt; = Flux.merge(three, two, one)
    flux.subscribe(::println)
}

--------------------
출력 결과)
--------------------
three
two
one&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 결과를 확인해보면 인자로 지정한 순서대로 결과가 출력되었음을 확인할 수 있습니다. 예제 1.33은 예제 1.32를 조금 수정하여 Flux.merge 함수에 인자로 전달되는 플럭스에 delayElements 함수를 사용하여 정해진 지연 시간 이후에 각각 병렬로 처리되는데 이 경우에도 순서가 유지되는지 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.33 Flux.merge 예제2 - reactor/reactor-basic/src/flux/Flux11.kt&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;package flux

import reactor.core.publisher.Flux
import java.time.Duration

fun main() {
    val one = Flux.just(&quot;one&quot;).delayElements(Duration.ofMillis(300))
    val two = Flux.just(&quot;two&quot;).delayElements(Duration.ofMillis(400))
    val three = Flux.just(&quot;three&quot;).delayElements(Duration.ofMillis(500))

    val flux: Flux&amp;lt;String&amp;gt; = Flux.merge(three, two, one)
    flux.subscribe(::println)

    Thread.sleep(Duration.ofSeconds(2).toMillis())
}

--------------------
출력 결과)
--------------------
one
two
three&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 결과를 확인해보면 인자로 지정된 순서대로 통지되지 않고 지연 시간에 따라서 순서가 정해진 것을 확인할 수 있습니다. 만약 병렬 처리 시에도 인자로 지정된 순서대로 통지해야 하는 경우 예제 1.34와 같이 concat 함수를 사용해 순서를 보장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.34 Flux.concat 예제 - reactor/reactor-basic/src/flux/Flux12.kt&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;package flux

import reactor.core.publisher.Flux
import java.time.Duration

fun main() {
    val one = Flux.just(&quot;one&quot;).delayElements(Duration.ofMillis(300))
    val two = Flux.just(&quot;two&quot;).delayElements(Duration.ofMillis(400))
    val three = Flux.just(&quot;three&quot;).delayElements(Duration.ofMillis(500))

    val flux: Flux&amp;lt;String&amp;gt; = Flux.concat(three, two, one)
    flux.subscribe(::println)

    Thread.sleep(Duration.ofSeconds(2).toMillis())
}

--------------------
출력 결과)
--------------------
three
two
one&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 결과를 확인해보면 플럭스가 concat 함수에 병렬로 전달되었지만 인자로 지정된 순서대로 통지된 것을 확인할 수 있습니다. 순서가 보장되는 이유는 병렬 처리 시 인자로 지정된 플럭스를 결합한 다음 플럭스를 순서대로 구독하여 데이터를 통지하고 이전 플럭스에 대한 통지가 완료되면 다음 플럭스가 구독 처리될 때까지 기다리는 순차처리 방식의 연산자이기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.35에서 사용한 mergeSequential도 concat과 같이 순서가 보장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 1.35 Flux.concat 예제 - reactor/reactor-basic/src/flux/Flux12.kt&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;package flux

import reactor.core.publisher.Flux
import java.time.Duration

fun main() {
    val one = Flux.just(&quot;one&quot;).delayElements(Duration.ofMillis(300))
    val two = Flux.just(&quot;two&quot;).delayElements(Duration.ofMillis(400))
    val three = Flux.just(&quot;three&quot;).delayElements(Duration.ofMillis(500))

    val flux: Flux&amp;lt;String&amp;gt; = Flux.mergeSequential(three, two, one)
    flux.subscribe(::println)

    Thread.sleep(Duration.ofSeconds(2).toMillis())
}

--------------------
출력 결과)
--------------------
three
two
one&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;concat 과의 차이점이라면 concat은 여러 개의 플럭스를 인자로 전달받아 하나로 결합한 후에 순서대로 구독 처리하는 연산자라면 mergeSequential은 여러 개의 플럭스를 병렬로 구독 처리하고 최종 변환 시 인자로 지정한 구독 순서에 따라서 새로운 플럭스로 결합한다는 차이점이 있습니다.&lt;/p&gt;</description>
      <category>Reactive Programming</category>
      <author>devsh</author>
      <guid isPermaLink="true">https://devsh.tistory.com/151</guid>
      <comments>https://devsh.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%EC%95%A1%ED%84%B0-%EA%B8%B0%EC%B4%88-2-%ED%94%8C%EB%9F%AD%EC%8A%A4#entry151comment</comments>
      <pubDate>Wed, 12 Jan 2022 23:41:31 +0900</pubDate>
    </item>
    <item>
      <title>프로젝트 리액터 기초 1 - 모노</title>
      <link>https://devsh.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%EC%95%A1%ED%84%B0-%EA%B8%B0%EC%B4%88-1-%EB%AA%A8%EB%85%B8</link>
      <description>&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프로젝트 리액터 기초 1 - 모노&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devsh.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%EC%95%A1%ED%84%B0-%EA%B8%B0%EC%B4%88-2-%ED%94%8C%EB%9F%AD%EC%8A%A4&quot;&gt;프로젝트 리액터 기초 2 -&lt;/a&gt;&lt;a href=&quot;https://devsh.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%EC%95%A1%ED%84%B0-%EA%B8%B0%EC%B4%88-2-%ED%94%8C%EB%9F%AD%EC%8A%A4&quot;&gt; 플럭&lt;/a&gt;&lt;a href=&quot;https://devsh.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%EC%95%A1%ED%84%B0-%EA%B8%B0%EC%B4%88-2-%ED%94%8C%EB%9F%AD%EC%8A%A4&quot;&gt;스&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devsh.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%EC%95%A1%ED%84%B0-%EA%B3%A0%EA%B8%89-%ED%99%9C%EC%9A%A9&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;프로젝트 리액터 고급 활용&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로젝트 리액터&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로젝트 리액터(Project Reactor)&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;는 JVM기반의 환경에서 리액티브 애플리케이션 개발을 위한 오픈 소스 프레임워크입니다. 리액터는 스프링 에코시스템에서 리액티브 스택의 기반이 되는 프로젝트로 리액티브 스트림 사양을 구현하므로 리액티브 스트림에서 사용하는 용어와 규칙을 동일하게 사용하며 리액티브 스트림 사양에 포함되지 않은 리액터만의 다양한 기능들도 제공합니다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리액터를 사용하면 손쉽게 애플리케이션에 리액티브 프로그래밍을 적용할 수 있고 애플리케이션의 모든 구간에 비동기-논블로킹을 적용할 수 있습니다. 또한 리액터에서 제공하는 백프레셔 기법을 사용해 시스템의 부하를 효율적으로 조절할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모노와 플럭스&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리액터는 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모노(Mono)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;와 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;플럭스(Flux)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;라는 두 가지 핵심 타입을 제공합니다. 먼저 모노는 0..1개의 단일 요소 스트림을 통지합니다. 이에 반해 플럭스는 0..N개로 이뤄진 복수개의 요소를 통지하는 발행자입니다. 플럭스와 모노는 리액티브 스트림 사양의 발행자(Publisher)를 구현하여 데이터를 통지하기 때문에 onComplete 또는 onError 시그널이 발생할 때까지 onNext를 사용해 구독자에게 데이터를 통지합니다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모노와 플럭스는 리액터의 핵심이 되는 요소로써 리액티브 스트림 사양을 구현하는 하는 것 외에도 데이터를 가공하거나 필터링하는 기능과 에러가 발생하면 에러를 핸들링 할 수 있는 다양한 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;연산자(Operator)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 제공하며 두 타입이 공통으로 사용하는 연산자도 있고 자신만의 연산자도 있습니다. 이번장에서는 모노와 플럭스를 사용할때 자주 사용하는 연산자들에 대해 알아보도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모노의 연산자&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모노는 단일 요소에 대한 통지의 경우 사용됩니다. 모노의 사용 방법을 알아보기 위해 코틀린 그레이들 프로젝트를 생성합니다. 정상적으로 생성되었다면 예제와 같은 구조로 프로젝트가 생성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1641996842627&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;├── build.gradle.kts
├── gradlew
├── settings.gradle.kts
└── src
    ├── main
    │   ├── kotlin
    │   └── resources
    └── test
        ├── kotlin
        └── resources&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그다음 build.gradle.kts를 열어 예제 1.1과 같이 reactor-core 의존성을 추가합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.1 프로젝트에 reactor-core 의존성 추가 - reactor/reactor-basic/build.gradle.kts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641996910434&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id(&quot;org.jetbrains.kotlin.jvm&quot;) version (&quot;1.4.10&quot;)
}


group = &quot;com.reactor.reactorbasic&quot;
version = &quot;1.0-SNAPSHOT&quot;


repositories {
    mavenCentral()
}


dependencies {

    implementation(&quot;org.jetbrains.kotlin:kotlin-stdlib&quot;)

    implementation(&quot;io.projectreactor:reactor-core:3.4.0&quot;)

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;just&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모노를 생성하는 가장 간단한 방법은 Mono.just를 사용하는 것입니다. Mono.just(T data)는 객체를 인자로 받은 뒤 모노로 래핑하는 팩토리 함수입니다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.2 Mono.just 사용 - reactor/reactor-basic/src/mono/Mono1.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641996953250&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono

import reactor.core.publisher.Mono

fun main() {
    val mono: Mono&amp;lt;String&amp;gt; = Mono.just(&quot;Hello Reactive World&quot;)
    mono.subscribe(::println)
}

--------------------
출력 결과)
--------------------
Hello Reactive World&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인자로 &amp;ldquo;Hello Reactive World&amp;rdquo;라는 String을 인자로 받았기 때문에 String이 감싸진 Mono&amp;lt;String&amp;gt;을 반환하며 그다음 라인에 작성된 mono.subscribe(::println)는 리액티브 스트림 사양의 Publisher.subscribe 함수를 구현하여 데이터를 구독합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;중요한 점은 플럭스와 모노는 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;게으르게(lazy)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 동작하여 subscribe를 호출하지 않으면 연산자에 작성한 로직이 동작하지 않는다는 것입니다. 이런 특징은 Java8 스트림 API의 특성과 동일합니다. 스트림 API 역시 연산자를 트리거 하는 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;최종 연산자&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 호출하지 않으면 스트림 연산이 동작하지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;justOrEmpty&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Mono.justOrEmpty는 값이 null이 거나 null이 될 수 있는 데이터를 받아서 처리할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.3&amp;nbsp; Mono.justOrEmpty 사용 - reactor/reactor-basic/src/mono/Mono2.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997006834&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
 
fun main() {
    val mono: Mono&amp;lt;String&amp;gt; = Mono.justOrEmpty(&quot;Hello Reactive World&quot;)
    mono.subscribe(::println)
}
--------------------
출력 결과)
--------------------
Hello Reactive World&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우선 greeting 변수에 null을 할당하고 Mono.justOrEmpty의 인자로 넣어줍니다. Mono.just의 경우 null인 데이터가 인자로 들어오면 NullPointerException이 발생하므로 주의해야 합니다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;switchOrEmpty/defer&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;null인 데이터를 제공받을 경우 switchIfEmpty를 사용해 처리할 수 있습니다. switchIfEmpty는 전달받은 값이 null인 경우 새로운 데이터로 변환하는 연산자입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.4&amp;nbsp; 통지할 데이터가 null인 경우 처리 방법 - reactor/reactor-basic/src/mono/Mono3.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997038200&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
 
fun main() {
    val greeting: String? = null
    Mono.justOrEmpty(greeting)
        .switchIfEmpty(Mono.defer {
            Mono.just(&quot;Hello Reactive World&quot;)
        })
        .subscribe(::println)
}
--------------------
출력 결과)
--------------------
Hello Reactive World&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;greeting이 null로 전달되었으므로 switchIfEmpty에서 제공하는 Mono.just(&amp;ldquo;Hello Reactive World&amp;rdquo;)가 연산자의 결과로 전달됩니다. 이때 switchIfEmpty 내부에서 Mono.defer라는 연산자를 사용하면 내부 코드의 호출이 지연되어 실제 사용되는 시점에 호출됩니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 예제 1.5 같이 Mono.defer를 사용하지 않고 switchIfEmpty를 사용하게 되면 greeting의 값의 존재 유무에 상관없이 switchIfEmpty 내부의 코드가 동작하게 되는데 이는 불필요한 동작이므로 Mono.defer를 사용하여 실제 사용되는 시점까지 동작을 미루는 것이 유리합니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.5&amp;nbsp; Mono.defer를 사용하지 않은 경우 - reactor/reactor-basic/src/mono/Mono4.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997064884&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
 
fun main() {
    val greeting = &quot;Hello&quot;
    Mono.justOrEmpty(greeting)
        .switchIfEmpty(greetIfEmpty()) // null이 아니지만 호출됨
        .subscribe(::println)
}
 
fun greetIfEmpty() : Mono&amp;lt;String&amp;gt; {
    val greeting = &quot;Hello Reactive World&quot;
    println(greeting)
    return Mono.just(greeting)
}
--------------------
출력 결과)
--------------------
Hello Reactive World
Hello&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.6은 Mono.defer를 사용하여 greeting이 null인 경우에만 동작하므로 효율적입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.6&amp;nbsp; Mono.defer를 사용한 경우 - reactor/reactor-basic/src/mono/Mono5.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997091412&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
 
fun main() {
    val greeting = &quot;Hello&quot;
    Mono.justOrEmpty(greeting)
        .switchIfEmpty(Mono.defer {
            greetIfEmptyInDefer() // greeting이 null인 경우에만 동작
        })
        .subscribe(::println)
}
 
fun greetIfEmptyInDefer(): Mono&amp;lt;String&amp;gt; {
    val greeting = &quot;Hello Reactive World&quot;
    println(greeting)
    return Mono.just(greeting)
}
--------------------
출력 결과)
--------------------
Hello&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;defaultIfEmpty&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;전달받은 데이터가 null인 경우 예제 1.7의 defaultIfEmpty를 사용하여 기본 값을 제공할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.7 defaultIfEmpty를 사용해 기본 값 제공 - reactor/reactor-basic/src/mono/Mono6.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997123172&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
 
fun main() {
    val greeting: String? = null
    Mono.justOrEmpty(greeting)
        .defaultIfEmpty(&quot;Hello Reactive World&quot;)
        .subscribe(::println)
}
--------------------
출력 결과)
--------------------
Hello Reactive World&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;defaultIfEmtpy와 switchIfEmpty 연산자는 모두 전달받은 데이터가 null인 경우 사용할 수 있다는 것이 공통점입니다. 차이점은 switchIfEmpty는 내부에서 새로운 로직을 구현해 새로운 Mono를 생성할 수 있지만 defaultIfEmpty는 null을 대체하는 값을 지정한다는 점이 다릅니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;fromSupplier&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;내부에서 로직에 대한 결과로 모노 객체를 생성할 경우 예제 1.8과 같이 Mono.fromSupplier를 사용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.7 fromSupplier 예제 - reactor/reactor-basic/src/mono/Mono7.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997171876&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
 
fun main() {
    Mono.fromSupplier {
        val greeting = &quot;Reactive World&quot;
        var welcome = &quot;Welcome&quot;
        welcome += &quot; ${greeting.split(&quot; &quot;)[1]}&quot;
        welcome += &quot; ${greeting.split(&quot; &quot;)[2]}&quot;
        welcome
    }.subscribe(::println)
}
--------------------
출력 결과)
--------------------
Welcome Reactive World&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Mono.fromSupplier의 인자로 제공되는 Supplier는 Java8에 추가된 함수형 인터페이스입니다. Supplier는 별도의 인자는 없이 내부의 값을 반환하는 T get() 함수를 구현하도록 설계되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997185564&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface Supplier&amp;lt;T&amp;gt; {
    T get();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉 Mono.fromSupplier{} 내부에 작성하는 코드는 T get() 함수의 구현이 되어 Mono.fromSupplier로 제공되고 내부 로직의 반환되는 구조입니다. 모노를 생성하는데 특정한 구현 로직이 필요한 경우나 늦은 초기화가 필요한 경우 유용하게 사용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;error&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Mono.error는 로직을 처리하는 중 특정한 에러를 발생시키고 싶은 경우 유용하게 사용할 수 있습니다. 예제 1.8은 일부러 NullPointerException을 발생시키는 코드를 생성하고 예외가 발생했을때 사용자 정의 예외를 발생시킵니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.8 error를 사용자 사용자 정의 예외 발생 예제 - reactor/reactor-basic/src/mono/Mono8.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997208177&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
 
fun main() {
    Mono.fromSupplier {
        val greeting: String? = null
        Mono.just(greeting)
    }.onErrorResume {
        Mono.error(GreetingEmptyException(&quot;Greeting is Empty&quot;))
    }.subscribe(::println)
}
 
class GreetingEmptyException : Throwable {
    constructor() : super()
    constructor(message: String?) : super(message)
}
--------------------
출력 결과)
--------------------
Welcome Reactive World&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가장 먼저 Mono.from Supplier 내부에서 null을 반환하는 greeting을 Mono.just로 생성하였습니다. Mono.just는 null인 객체를 전달받으면 NullPointerException을 발생시킵니다. 에러가 발생한 경우 onErrorResume를 사용하면 예외가 발생할 경우 별도의 핸들링이 가능합니다. 예제 1.8에서는 NullPointerException이 발생한 경우 직접 생성한 GreetingEmptyException을 대신 발생시키기 위해 Mono.error를 사용했습니다. Mono.error는 최상위 Exception인 Throwable을 인자로 전달받기 때문에 모든 사용자 정의 예외 객체에 사용이 가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;map&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.9에서 소개하는 map 연산자는 전달받은 요소를 새로운 모노로 변환할 때 사용하는 연산자입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.9 map 사용 예제 - reactor/reactor-basic/src/mono/Mono9.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997231074&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
 
fun main() {
   val mono: Mono&amp;lt;String&amp;gt; =
       Mono.just(&quot;Hello Reactive World&quot;)
           .map {
               it.toLowerCase()
           }
   mono.subscribe(::println)
}
--------------------
출력 결과)
--------------------
Hello Reactive World&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;map 내부에서는 Mono.just에 담긴 문자열(Hello Reactive World)을 전달받아서 소문자로 변환하는 toLowerCase()를 호출합니다. 이렇게 되면 map 내부에서 변환된 데이터를 모노로 감싼 뒤 데이터를 통지하게 됩니다. map은 한 개의 데이터만 제공할 수 있고 다수의 데이터를 제공할 수 없습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;flatMap&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.10은 &amp;ldquo;Hello Reactive World&amp;rdquo;라는 String을 받아서 flatMap을 통해 각 단어의 처음 단어를 뽑아내 약자로 변환하여 출력하는 예제입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.10 flatMap 사용 예제 - reactor/reactor-basic/src/mono/Mono10.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997264946&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
 
fun main() {
    Mono.just(&quot;Hello Reactive World&quot;)
        .flatMap {
            val words = it.split(&quot; &quot;)
            val acronym = words.map { word -&amp;gt; word[0] }.toCharArray()
            Mono.just(acronym)
        }.subscribe(::println)
}
--------------------
출력 결과)
--------------------
HRW&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;flatMap은 주로 단일 요소를 복수개의 요소로 변환할 때 사용합니다. 유의할 점은 flatMap은 map과 다르게 마지막에서 반환할 요소를 앞서 소개한 Mono.just와 같은 팩토리 함수로 감싸서 반환해야 합니다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.11은 전달 받은 문자열에 &amp;ldquo;Hello&amp;rdquo;가 포함 되었는지를 판단하여 true면 문자열로 새롭게 생성한 모노를 반환하고 거짓이라면 Mono.empty를 반환합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.11 flatMap 사용 예제2 - reactor/reactor-basic/src/mono/Mono11.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997281258&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
 
fun main() {
    Mono.just(&quot;Reactive World&quot;)
        .flatMap {
            if (it.contains(&quot;Hello&quot;)) {
                Mono.just(it)
            } else {
                Mono.empty()
            }
        }.subscribe(::println)
}
--------------------
출력 결과)
--------------------
없음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;flatMap을 활용하면 특정 조건에 따라서 에러를 발생 시키거나 비어 있는 값을 리턴할 수 있습니다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;filter&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;filter 연산자는 특정 조건이 true인지 판단하여 true인 경우에만 데이터를 통지하는 연산자입니다. 예제 1.12는 전달받은 문자열 데이터가 존재하는 경우 정해진 문자열을 출력합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.12 filter를 사용해 데이터 정제 - reactor/reactor-basic/src/mono/Mono12.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997308810&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
 
fun main() {
    val mono: Mono&amp;lt;String&amp;gt; =
        Mono.just(&quot;Hello Reactive World&quot;)
            .filter {
                it.startsWith(&quot;Hello&quot;)
            }
            .map {
                &quot;this operator is working&quot;
            }
    mono.subscribe(::println)
}
--------------------
출력 결과)
--------------------
this operator is working&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.12의 filter 연산자에서는 전달받은 문자열이 Hello로 시작하는지 판단하여 true이라면 다음 연산자인 map 연산자로 이어지고 정해진 문자열 &amp;ldquo;this operator is working&amp;rdquo;을 출력합니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;filter 연산자는 Predicate라는 함수형 인터페이스를 전달받습니다. Predicate도 마찬가지로 Java8에 추가되었으며 test라는 함수를 구현해야 합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997321444&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface Predicate&amp;lt;T&amp;gt; {
    boolean test(T t);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;test 함수는 boolean을 반환하여 test 함수를 구현하는 구현체에서는 조건식의 결과를 boolean으로 반환해야 합니다.&amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.13은 filter 연산자 내부의 조건이 거짓인 경우 switchIfEmpty 내부의 로직이 동작하는 것을 확인 하는 예제입니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.13 filter를 사용해 데이터 정제2 - reactor/reactor-basic/src/mono/Mono13.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997347980&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
 
fun main() {
    val mono: Mono&amp;lt;String&amp;gt; =
        Mono.just(&quot;Hello Reactive World&quot;)
            .filter {
                it.startsWith(&quot;Bye&quot;)
            }
            .map {
                &quot;this operator is working&quot;
            }
            .switchIfEmpty(Mono.defer {
                Mono.just(&quot;switchIfEmpty is working&quot;)
            })
    mono.subscribe(::println)
}
--------------------
출력 결과)
--------------------
switchIfEmpty is working&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;filter 연산자내에서 전달받은 데이가 Bye로 시작하면 조건은 true지만 전달 받은 문자열이 Hello Reactive World 이므로 이 조건 식은 거짓이 됩니다. 이 경우 바로 이어진 map 연산자가 동작하지 않고 switchIfEmpty 연산자가 동작하여 출력 결과는 switchIfEmpty is working이 됩니다. 이런 형태로 filter의 조건에 따른 분기가 이뤄지는 조합은 매우 빈번하게 사용됩니다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;zip&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 여러 개의 모노 객체를 하나의 모노로 결합할 경우 zip을 사용해 처리할 수 있습니다. 예제 1.14는 zip을 사용해 3개의 모노를 합친 결과를 출력합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.14 zip을 사용한 결합 예제1 - reactor/reactor-basic/src/mono/Mono14.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997375862&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
 
fun main() {
    val mono1 = Mono.just(&quot;Hello&quot;)
    val mono2 = Mono.just(&quot;Reactive&quot;)
    val mono3 = Mono.just(&quot;World&quot;)
    Mono.zip(mono1, mono2, mono3)
        .map { tuple: Tuple3&amp;lt;String, String, String&amp;gt; -&amp;gt;
            &quot;${tuple.t1} ${tuple.t2} ${tuple.t3}&quot;
        }.subscribe(::println)
}
--------------------
출력 결과)
--------------------
Hello Reactive World&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우선 3개의 모노를 생성한 후 Mono.zip 함수의 인자로 추가합니다.&amp;nbsp; zip 함수의 처리가 완료되어 새로운 모노를 생성한 뒤 다음 연산자에 통지를 보내는 시점은 인자로 제공된 모노 중 가장 오래 수행된 모노의 시간을 기준으로 결합되는 것이 특징입니다. 이런 특징 때문에 각 모노의 로직을 비동기 처리한 뒤 결과에 대한 결합을 위해 zip을 사용할 수 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;zip으로 합쳐진 모노 객체는 튜플(Tuple) 객체의 프로퍼티로 저장됩니다. 튜플에 들어가는 순서는 함수에 인자로 추가한 순서대로 t1, t2, t3와 같은 방식으로 사용할 수 있습니다. 튜플은 리액터에서 제공하는 데이터 저장을 위한 구조체의 종류로 비슷한 예로 코틀린의 페어(Pair)와 트리플(Triple)이 있습니다. 튜플은 현재 8개까지 제공되고 있으며 reactor.util.function 패키지를 보면 Tuple2, Tuple3, Tuple4&amp;hellip; 과 같이 미리 정의되어 제공됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997391533&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Tuple2&amp;lt;T1, T2&amp;gt; implements Iterable&amp;lt;Object&amp;gt;, Serializable {
    final T1 t1;
    final T2 t2;
    // ...
}
 
public Tuple3&amp;lt;T1, T2, T3&amp;gt; extends Tuple2&amp;lt;T1, T2&amp;gt; {
    final T3, t3;
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본적으로 Mono.zip 함수에는 최대 8개의 모노를 인자로 받아서 튜플로 사용할 수 있는 오버로딩 함수가 있지만 그 이상의 모노를 결합할 경우 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컴비네이터 함수(Combinator Function)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;을 사용해 합칠 수 있습니다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.15는 리스트로 전달받은 모노를 컴비네이터 함수로 결합하는 예제 입니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.15 zip을 사용한 결합 예제2 - reactor/reactor-basic/src/mono/Mono15.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997468744&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
 
fun main() {
    val monoList = arrayListOf(
        Mono.just(&quot;Seoul&quot;),
        Mono.just(&quot;Tokyo&quot;),
        Mono.just(&quot;Washington, D.C&quot;),
        Mono.just(&quot;Wellington&quot;),
        Mono.just(&quot;Canberra&quot;),
        Mono.just(&quot;Paris&quot;),
        Mono.just(&quot;Prague&quot;),
        Mono.just(&quot;Manila&quot;),
        Mono.just(&quot;Doha&quot;),
        Mono.just(&quot;Singapore&quot;)
    )
 
    Mono.zip(monoList) { array: Array&amp;lt;Any&amp;gt; -&amp;gt; // 컴비네이터 함수
        array.joinToString {
            (it as String).toUpperCase()
        }
    }.subscribe(::println)
}
 
--------------------
출력 결과)
--------------------
SEOUL, TOKYO, WASHINGTON, D.C, WELLINGTON, CANBERRA, PARIS, PRAGUE, MANILA, DOHA, SINGAPORE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우선 Mono.zip 함수의 인자에 리스트에 담긴 모노 객체들을 추가합니다. 그런 다음 컴비네이터 함수에서 데이터를 변환하게 되는데 이때 앞서 제공된 모노가 담긴 리스트는 튜플이 아닌 배열에 담겨서 전달됩니다. 그다음 joinToString 함수 내부에서 배열의 개수만큼 반복하면서 문자열을 대문자로 변환하고 콤마(,)를 구분자로 해서 배열을 문자열로 변환한 후 다음 연산자로 제공합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;zipWith, zipWhen&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;zipWith 연산자는 기존 모노 객체에 새로운 모노를 결합합니다. 얼핏 보면 zip 함수와 비슷하지만 zip은 여러 개의 모노를 결합할 수 있는 반면에 zipWith는 한 번에 1개의 모노 객체를 기존 모노에 결합합니다.&amp;nbsp; 예제 1.16은 zipWith를 사용해 모노를 결합하는 예제입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.16 zipWith을 사용한 결합 예제 - reactor/reactor-basic/src/mono/Mono16.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997496722&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
import reactor.util.function.Tuple2
 
fun main() {
    val mono: Mono&amp;lt;Tuple2&amp;lt;String, String&amp;gt;&amp;gt; =
        Mono.just(&quot;Republic of Korea&quot;)
            .zipWith(Mono.just(&quot;Seoul&quot;))
 
    mono.map {
        val country = it.t1
        val capital = it.t2
        &quot;$capital is capital of $country&quot;
    }.subscribe(::println)
}
 
--------------------
출력 결과)
--------------------
Seoul is capital of Republic of Korea&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;zipWith는 한 번에 1개의 모노를 결합하는 연산자이기 때문에 2개의 모노를 담는 튜플인 Tuple2&amp;lt;T1,T2&amp;gt;를 생성합니다. 그러므로 예제 1.16의 zipWith 연산자는 Mono&amp;lt;Tuple2&amp;lt;String, String&amp;gt;&amp;gt;과 같은 형태를 반환하게 되고 map 연산자 내부에서 전달받은 튜플을 이용해 문자열을 생성합니다. 이때 튜플의 순서는 전달받은 모노의 순서와 동일합니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;zipWhen 연산자는 zipWith와 마찬가지로 기존 모노 객체에 새로운 모노를 결합하지만 함수의 인자로 모노를 직접 전달 받는 것이 아니라 함수형 인터페이스인 Function을 전달 받습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997506892&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface Function&amp;lt;T, R&amp;gt; {
    R apply(T t);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.17은 앞선 1.16 예제에서 zipWith를 사용하는 코드를&amp;nbsp; zipWhen으로 변경하였습니다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.17 zipWith을 사용한 결합 예제 - reactor/reactor-basic/src/mono/Mono17.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997522237&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
 
fun main() {
    val mono =
       Mono.just(&quot;Republic of Korea&quot;)
             .zipWhen {
                Mono.just(&quot;Seoul&quot;)
            }
 
    mono.map {
        val country = it.t1
        val capital = it.t2
        &quot;$capital is capital of $country&quot;
    }.subscribe(::println)
}
 
--------------------
출력 결과)
--------------------
Seoul is capital of Republic of Korea&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;zipWhen도 zipWith와 마찬가지로 한 번에 1개의 모노를 결합하는 연산자이기 때문에 2개의 모노를 담는 튜플인 Tuple2&amp;lt;T1,T2&amp;gt;를 생성합니다. 하지만 인자를 함수 형태로 전달받았기 때문에 내부에서 특정 로직을 구현할 결과를 모노로 변환하는 경우 유용하게 사용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;block&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리액터의 연산자는 게으르게 동작하여 subscribe가 호출되기 전까지 연산자가 동작하지 않는 것이 일반적입니다. 하지만 block을 사용하면 그 즉시 구독을 차단(block) 하고 이전 연산자로부터 수신된 데이터를 가져옵니다. 예제 1.18은 block을 사용해 데이터를 출력합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.18 block을 사용한 데이터 출력 예제 - reactor/reactor-basic/src/mono/Mono18.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997539124&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
 
fun main() {
    val mono: Mono&amp;lt;String&amp;gt; =
        Mono.just(&quot;Welcome Back&quot;)
            .zipWhen {
                Mono.just(&quot;Synchronous World&quot;)
            }.map {
                &quot;${it.t1} to the ${it.t2}&quot;
            }
 
    val text: String? = mono.block()
    println(text)
}
 
--------------------
출력 결과)
--------------------
Welcome Back to the Synchronous World&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;subscribe를 호출하지 않았지만 block 함수를 호출함으로써 구독이 완료되고 Mono&amp;lt;String&amp;gt;가 아닌 String이 반환되는 것을 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;block 함수는 논블로킹 스레드 내부에서 사용되면 IllegalStateException이 발생합니다. 예제 1.19는 리액터에서 지원하는 스케줄러를 사용해 단일 논블로킹 스레드를 생성하고 내부에서 block 함수를 호출하여 에러를 발생시키는 예제입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.19 논블로킹 스레드 내부에서 block을 사용 예제 - reactor/reactor-basic/src/mono/Mono19.kt&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1641997573797&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package mono
 
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
 
fun main() {
    Schedulers.newSingle(&quot;NonBlockingSingleThread&quot;).schedule {
        val mono: Mono&amp;lt;String&amp;gt; =
            Mono.just(&quot;Welcome Back&quot;)
                .zipWhen {
                    Mono.just(&quot;Synchronous World&quot;)
                }.map {
                    &quot;${it.t1} to the ${it.t2}&quot;
                }
        val text: String? = mono.block()
        println(text)
    }
}
 
--------------------
출력 결과)
--------------------
java.lang.IllegalStateException: 
block()/blockFirst()/blockLast() are blocking, which is not supported in thread NonBlockingSingleThread-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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제 1.19와 같이 논블로킹 스레드 내부에서 사용되면 경고와 함께 에러가 발생합니다. 일반적으로 block 함수를 사용하는 상황은 리액터의 비동기, 논블로킹 처리의 이점을 사용하지 않는다는 의미이기 때문에 한정적인 상황에서 사용됩니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Reactive Programming</category>
      <category>kotlin</category>
      <category>reactiveprogramming</category>
      <category>Reactor</category>
      <category>Webflux</category>
      <author>devsh</author>
      <guid isPermaLink="true">https://devsh.tistory.com/150</guid>
      <comments>https://devsh.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%EC%95%A1%ED%84%B0-%EA%B8%B0%EC%B4%88-1-%EB%AA%A8%EB%85%B8#entry150comment</comments>
      <pubDate>Wed, 12 Jan 2022 23:23:40 +0900</pubDate>
    </item>
  </channel>
</rss>